概念:作業系統是管理計算機硬體與軟體資源的計算機程式,簡稱OS。
為什麼要有作業系統:
1.給使用者提供穩定、高效和安全的執行環境,為程式設計師提供各種基本功能(OS不信任任何使用者,不讓使用者或者程式設計師直接與硬體進行互動)。
2.管理好各種軟硬體資源。
從這張圖我們可以看到幾點內容:
我們平時所寫的C語言程式碼,通過編譯器的編譯,最終會成為一個可執行的程式,當這個可執行程式執行起來之後,它就變成了一個程序。
程式是存放在儲存媒介(程式平時都存放在磁碟當中)上的一個可執行檔案,而程序就是程式執行的過程。程序的狀態是變化的,其中包括程序的建立、排程和死亡。程式是靜態的,程序是動態的。
程序: 計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。
如何描述程序:
task_struct內容有哪些?
識別符號:描述本程序的唯一識別符號(就像是我們每個人的身份證)。
狀態:任務狀態、退出程式碼、退出訊號等。
優先順序: 程式被CPU執行的順序(後面會單獨介紹)。
程式計數器: 一個暫存器中存放了一個pc指標,這個指標永遠指向即將被執行的下一條指令的地址。
記憶體指標: 包含程式程式碼和程序相關的資料的指標,還有和其它程序共用的記憶體快的指標。這樣就可以PCB找到程序的實體。
上下文資料: 在單核CPU中,程序需要在執行佇列(run_queue) 中排隊,等待CPU排程,每個程序在CPU中執行時間是在一個時間片內的,時間片到了,就要從CPU上下來,繼續去執行佇列中排隊。
I/O狀態資訊: 包括顯示的I/O請求,分配給程序的I/ O裝置和被程序使用的檔案列表。
記賬資訊: 能包括處理器時間總和,使用的時鐘數總和,時間限制,記賬號等。
組織程序
在核心原始碼中發現,所有執行在系統裡的程序都以task_struct連結串列形式存在核心中。
程序的狀態反應程序執行過程的變化。這些狀態隨著程序的執行和外界的變化而轉換。
五態模型中,程序分為新建態,終止態,執行態,就緒態,就緒態。
(1)TASK_RUNNING(執行態):程序正在被CPU執行。當一個程序被建立的時候會處於TASK_RUNNABLE,表示已經準備就緒,正在準備被排程。
(2)TASK_INTERRUPTIBLE(可中斷狀態):程序正在睡眠(阻塞)等待某些條件的達成。一旦這些條件達成,核心就會把程序狀態設定成執行態。處於此狀態的程序也會因為接收到訊號而提前被喚醒,比如給一個TASK_INTERRUPTIBLE狀態的程序傳送SIGKILL訊號,這個程序將會被先喚醒(進入TASK_RUNNABLE狀態),然後再響應SIGKILL訊號而退出(變為TASK_ZOMBIE狀態),並不會從TASK_INTERRUPTIBLE狀態直接退出。
(3)TASK_UNINTERRUPTIBLE(不可中斷):處於等待中的程序,待資源被滿足的時候被喚醒,但是不可以由其他程序通過訊號或者中斷喚醒。由於不接受外來的任何訊號,因此無法用KILL殺掉這些處於該狀態的程序。而TASK_UNINTERRUPTIBLE狀態存在的意義就在於,核心的某些處理流程是不能被打斷的。
(4)TASK_ZOMBIE(僵死):表示程序已經結束,但是其父程序還沒有回收子程序的資源。為了父程序能夠獲知它的訊息,子程序的程序描述符仍然被保留著。一旦父程序呼叫wait函數釋放子程序的資源,子程序的程序描述符就會被釋放。
(5)TASK_STOPPED(停止):程序停止執行。當程序接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等訊號的時候。此外,在偵錯期間接收到任何訊號,都會使程序進入這種狀態。當接收到SIGCONT訊號,會重新回到TASK_RUNNABLE狀態。
下面是程序狀態在原始碼中的定義:
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
檢視程序狀態相關的命令:
ps命令可以檢視程序詳細的狀態,常用選項如下:
選項 | 含義 |
---|---|
-a | 顯示終端上的所有程序,包括其他程序 |
-u | 顯示程序的詳細狀態 |
-x | 顯示沒有控制終端的程序 |
-w | 顯示加寬,以便顯示更多的資訊 |
-r | 只顯示正在執行的程序 |
PID就是程序的程序號,STAT是程序此時處於什麼狀態。
有下面兩種命令(前者檢視所用程序的名字,後者可以檢視程序的父子關係):
ps aux/ps axj
每個程序都有一個程序號來標識,其型別為pid_t(整型)。程序號是唯一的,但是程序號是可以重用的。當一個程序終止後,其程序號可以再次使用。
程序號(PID)
getpid()可以獲取當前程序的程序號。
父程序號(PPID)
getppid()可以獲取當前程序的父程序號
行程群組號(PGID)
getpgid()可以獲取當前程序行程群組號
pid_t fork(void);
功能:通過複製當前程序,為當前程序建立一個子程序
返回值:成功:子程序中返回0,父程序中返回子程序的pid_t。
失敗:返回-1。
程序呼叫fork函數,核心需要做什麼?
fork之後執行什麼?
父子程序共用一份程式碼,fork之後,一起執行fork之後的程式碼,且二者之間是獨立的,不會相互影響。
父程序絕大部門東西都被子程序繼承,程式碼也是,但是在執行的過程中,父程序的PCB中存在一個pc指標,記錄著下一條指定的地址,當父程序執行到fork的時候,pc指標也只想fork的下一條指令,子程序也繼承了pc指標的虛擬地址,本來子程序全部繼承了父親的共用程式碼,但是此時pc也是指向fork的下一條指令,所以父子程序都從fork之後開始執行。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t ret = fork();
if (ret < 0)
{
perror("fork");
return 1;
}
else if (ret == 0)// 子程序
{
printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
else if (ret > 0)// 父程序
{
printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
sleep(1);
return 0;
}
使用fork函數得到的子程序是父程序的一個複製品,每個程序都有自己的過程控制塊PCB,再這個PCB中子程序從父程序中繼承了整個程序的地址空間:包括程序上下文,程序堆疊,開啟的檔案描述符,資訊控制設定,程序優先順序,行程群組號等等,但是程序的地址空間都是虛擬空間,子程序PCB繼承的都是虛擬地址。
通常情況下,父子程序共用一份程式碼,並且資料都是共用的,當任意一方試圖寫入更改資料的時候,那麼這一份便要以寫時拷貝的方式各自私有一份副本。
從圖中可以看出,發生寫時拷貝後,修改方將改變頁表中對該份資料的對映關係,父子程序各自私有那一份資料,且許可權由唯讀變成了只寫,虛擬地址沒有改變,改變的是實體記憶體頁的實體地址。(涉及到虛擬地址,可以看我上面發的文章)
問題思考:
1.為什麼程式碼要共用?
程式碼是不可以被修改的,所以各自私有很浪費空間,大多數情況下是共用的,但要注意的是,程式碼在特殊情況下也是會發生寫時拷貝的,也就是程序的程式替換(後面會單獨介紹)。
2.寫實拷貝的作用?
3.寫時拷貝是對所有資料進行拷貝嗎?
答案是否定的。如果沒有修改的資料進行拷貝,那麼這樣還是會造成空間浪費的,沒有被修改的資料還是可以共用的,我們只需要將修改的那份資料進行寫時拷貝即可。
理論還是太枯燥,上程式碼!
程式碼1:棧區區域性變數
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int var = 88;
//建立一個子程序
pid_t ret = fork();
if (ret < 0)
{
perror("fork");
return 1;
}
else if (ret == 0)// 子程序
{
sleep(1);
printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
printf("子程序睡醒之後 var = %d\n",var);
}
else if (ret > 0)// 父程序
{
printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
printf("父程序之前 var =%d\n", var);
var++;
printf("父程序之後 var =%d\n", var);
}
sleep(1);
return 0;
}
執行結果:
讀時共用,寫時拷貝。這裡的父程序一開始時共用var的資料給子程序,但是此時子程序睡了一秒,就執行父程序,父程序中var的值被改變,此時寫時拷貝,var會拷貝一份到子程序當中,所以父程序修改var的值不會影響到子程序中var的值。這裡的區域性變數在棧區。
程式碼2:全域性變數
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int var = 88;
int main()
{
//建立一個子程序
pid_t ret = fork();
if (ret < 0)
{
perror("fork");
return 1;
}
else if (ret == 0)// 子程序
{
sleep(1);
printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
printf("子程序睡醒之後 var = %d\n",var);
}
else if (ret > 0)// 父程序
{
printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
printf("父程序之前 var =%d\n", var);
var++;
printf("父程序之後 var =%d\n", var);
}
sleep(1);
return 0;
}
執行結果:
子程序var值也不會受到影響,遵循讀時共用,寫時拷貝的原則。
總結:
父子程序由獨立的資料段、堆、棧、共用程式碼段(每個程序都有屬於自己的PCB)。
Linux中每個程序都有4G的虛擬地址空間(獨立的3G使用者空間和共用的1G核心空間),fork建立的子程序也不例外。
(1)1G核心空間既然是所有程序共用,因此fork建立的子程序自然也將有用;
(2)3G的使用者空間是從父程序而來。
fork建立子程序時繼承了父程序的資料段、程式碼段、棧、堆,值得注意的是父程序繼承來的是虛擬地址空間,程序上下文,開啟的檔案描述符,資訊控制設定,程序優先順序,行程群組號,同時也複製了頁表(沒有複製物理塊)。因此,此時父子程序擁有相同的虛擬空間,對映的實體記憶體也是一致的。(獨立的虛擬地址空間,共用父程序的實體記憶體)。
由於父程序和子程序共用物理頁面,核心將其標記為「唯讀」,父子雙方均無法對其修改。無論父子程序嘗試對共用的頁面執行寫操作,就產生一個錯誤,這時核心就把這個頁複製到一個新的頁面給這個程序,並把原來的唯讀頁面標誌為可寫,留給另外一個程序使用----寫時複製技術。
核心在子程序分配實體記憶體的時候,並沒有將程式碼段對應的資料另外複製一份給子程序,最終父子程序對映的時同一塊實體記憶體。
可以通過echo$?檢視程序退出碼
main函數退出的時候,return的返回值就是程序的退出碼。0在函數的設計中,一般代表是正確而非0就是錯誤。
void exit(int status);
功能:結束當前正在執行的程序。
引數:返回給父程序的引數,根據需要填寫。
在任意位置呼叫,都會使得程序退出,呼叫之後會執行執行使用者通過 atexit或on_exit定義的清理函數,還會 關閉所有開啟的流,所有的快取資料均被寫入。
int main()
{
cout << "12345";
sleep(3);
exit(0);// 退出程序前前會執行使用者定義的清理函數,且重新整理緩衝區
return 0;
}//輸出12345
exit()和_exit()函數功能和用法都是一樣的,但是區別就在於exit()函數是標準庫函數,而__exit函數是系統呼叫。
在Linux的標準函數庫中,有一套稱做「高階I/O」的函數,我們熟知的printf(),fopen(),fread(),fwrite()都在此列,它們也被稱作緩衝IO (buffered IO)",其特徵是對應每一個開啟的檔案,在記憶體中都有一片緩衝區,每次讀檔案時,會多讀出若干條記錄,這樣下次讀檔案時就可以直接從記憶體的緩衝區中讀取,每次寫檔案的時候,也僅僅是寫入記憶體中的緩衝區,等滿足了一定的條件(達到一定數量,或遇到特定字元,如換行符\n和檔案結束EOF),再將緩衝區中的內容一次性寫入檔案,這樣就大大增加了檔案讀寫的速度,但也為我們程式設計帶來了一點點麻煩。如果有一些資料,我們認為已經寫入了檔案,實際上因為沒有滿足特定的條件,它們還只是儲存在緩衝區內,這時我們用_exit()函數直接將程序關閉,緩衝區中的資料就會丟失,反之,如果想保證資料的完整性,就一定要使用exit()函數。
int main()
{
cout << "12345";
sleep(3);
_exit(0);// 直接退出程序,不重新整理緩衝區
return 0;
}//不輸出12345
程序等待的必要性:
在每個程序退出的時候,核心釋放該程序所有的資源、包括開啟的檔案、佔用的記憶體等,這就是在執行exit時候執行的工作。但是仍然會保留一定的資訊,這些資訊主要指的是過程控制塊PCB的資訊(包括程序號,退出狀態,執行事件等),而這些資訊需要父程序呼叫wait或者waitpid函數得到他的退出狀態同時徹底清理掉這個程序殘留的資訊。
*pid_wait(int status);
功能:等待任意一個子程序結束,如果任意一個子程序結束了,此函數會回收子程序的資源。
引數:status程序退出時候的狀態。
返回值:成功:返回結束子程序的程序號。失敗:-1.
注意以下幾點:
演示:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t ret= fork();
if (ret< 0){
cerr << "fork error" << endl;
}
else if (ret== 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(1);
}
// parent
printf("father begins waiting...\n");
sleep(10);
pid_t id = wait(NULL);// 不關心子程序退出狀態
printf("father finish waiting...\n");
if (id > 0){
printf("child success exited\n");
} else{
printf("child exit failed\n");
}
//父程序再活5秒
sleep(5);
return 0;
}
由執行結果可以看出,父程序一隻等待子程序結束,等待的時候子程序變成殭屍程序,等父程序徹底釋放資源,子程序的狀態由殭屍變成死亡狀態。
*pid_t waitpid(pid_t pid, int status , int options);
功能:等待子程序結束,如果子程序終止,此函數就會回收子程序資源。
引數:
pid:引數pid有以下幾種型別:
pid>0 等待程序ID等於pid的子程序結束。
pid=0 等待同一個行程群組中的任何子程序,如果子程序已經進入了別的行程群組,waitpid不會等待它。
pid=-1 等待任意子程序,此時waitpid和wait的作用是一樣的。
pid<-1 等待指定行程群組中的任何子程序,這個行程群組的ID等於pid的絕對值。
options:options提供了一些額外的選項來控制waitpid()
0:通wait(),阻塞父程序,等待子程序退出。
WNOHANG: 若pid指定的子程序沒有結束,則waitpid()函數返回0,不予以等待。若正常結束,則返回該子程序的ID(可以進行基於阻塞等待的輪詢存取)。
WUNTRACED:如果子程序暫停了此函數立馬返回,並且不予理會子程序的結束狀態(很少呼叫)。
返回值:
waitpid有三種情況:
(1)正常返回的時候,waitpid返回收集到的已回收子程序的程序的程序號。
(2)如果設定了WNOHANG,而呼叫中發現了沒有已經退出的子程序可以等待,返回0。
(3)如果呼叫中出錯,返回-1,此時errno會被設定成相應的值來指示錯誤所在。
程式碼範例:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t ret= fork();
if (ret< 0){
cerr << "fork error" << endl;
}
else if (ret== 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(1);
}
// parent
printf("father begins waiting...\n");
sleep(10);
pid_t id = waitpid(-1, NULL, 0);// 不關心子程序退出狀態,以阻塞方式等待
printf("father finish waiting...\n");
if (id > 0){
printf("child success exited\n");
} else{
printf("child exit failed\n");
}
//父程序再活5秒
sleep(5);
return 0;
}
status的幾種狀態:(我們只研究status的低16位元)
看圖可以知道,低7位代表的是終止訊號,第8位元時core dump標誌,高八位是程序退出碼(只有正常退出是這個退出碼才有意義)
status的0-6位和8-15位有不同的意義。我們要先讀取低7位的內容,如果是0,說明程序正常退出,那就獲取高8位元的內容,也就是程序退出碼;如果不是0,那就說明程序是異常退出,此時不需要獲取高八位的內容,此時的退出碼是沒有意義的。
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t ret = fork();
if (ret < 0){
cerr << "fork error" << endl;
}
else if (ret == 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(1);
}
// parent
printf("father begins waiting...\n");
int status;
pid_t id = wait(&status);// 從status中獲取子程序退出的狀態資訊
printf("father finish waiting...\n");
if (id > 0 && (status&0x7f) == 0){
// 正常退出
printf("child success exited, exit code is:%d\n", (status>>8)&0xff);
}
else if (id > 0){
// 異常退出
printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
}
else{
printf("father wait failed\n");
}
if (id > 0){
printf("child success exited\n");
} else{
printf("child exit failed\n");
}
return 0;
}
執行結果如下:
操控者: 作業系統
阻塞的本質: 父程序從執行佇列放入到了等待佇列,也就是把父程序的PCB由R狀態變成S狀態,這段時間不可被CPU排程器排程
等待結束的本質: 父程序從等待佇列放入到了執行佇列,也就是把父程序的PCB由S狀態變成R狀態,可以由CPU排程器排程
阻塞等待: 父程序一直等待子程序退出,期間不幹任何事情
範例:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id < 0){
cerr << "fork error" << endl;
}
else if (id == 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(0);
}
// 阻塞等待
// parent
printf("father begins waiting...\n");
int status;
pid_t ret = waitpid(id, &status, 0);
printf("father finish waiting...\n");
if (id > 0 && WIFEXITED(status)){
// 正常退出
printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
}
else if (id > 0){
// 異常退出
printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
}
else{
printf("father wait failed\n");
}
}
執行結果如下:
非阻塞等待: 父程序不斷檢測子程序的退出狀態,期間會幹其他事情(基於阻塞的輪詢等待)
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id < 0){
cerr << "fork error" << endl;
}
else if (id == 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(0);
}
// 基於阻塞的輪詢等待
// parent
while (1){
int status;
pid_t ret = waitpid(-1, &status, WNOHANG);
if (ret == 0){
// 子程序還未結束
printf("father is running...\n");
sleep(1);
}
else if (ret > 0){
// 子程序退出
if (WIFEXITED(status)){
// 正常退出
printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
}
else{
// 異常退出
printf("child exited error,exit singal is:%d", status&0x7f);
}
break;
}
else{
printf("wait child failed\n");
break;
}
}
}
執行結果如下: