文章目錄
- 行程創建
- fock函式
- 寫時拷貝
- 行程終止
- 行程等待
- 行程程式替換
- 簡易shell
行程創建
作業系統允許一個行程創建另一個行程,并且允許子行程繼承父行程所擁有的資源,當子行程被終止時,其在父行程處繼承的資源應當還給父行程,同時,終止父行程同時也會終止其所有子行程,
注意:Linux作業系統對于終止有子行程的父行程,會把子行程交給1號行程接管,
行程創建:1、命令列啟動命令(程式、指令等) 2、通程序式自身,fork出子行程
創建行程的程序:
- 作業系統為新行程分配一個唯一的行程標識號,并申請一個空白的PCB,PCB是有限的,若申請失敗則創建失敗,
- 為行程分配資源,此處如果資源不足,行程就會進入等待狀態,以等待資源,
- 初始化PCB
- 如果行程的調度佇列能夠接納新行程,那就將行程插入到就緒佇列,等待被調度運行,
fock函式
父行程通過呼叫fork函式創建一個新的運行的子行程,
新創建的子行程幾乎但不完全與父行程相同,子行程得到與父行程用戶級虛擬地址空間相同的(但是獨立的)一份副本,包括代碼和資料段、堆、共享庫以及用戶堆疊,父行程和新創建的子行程之間的最大區別在于它們有不同的PID
#include <unistd.h>
pid_t fork(void);
//回傳值:子行程中回傳0,父行程回傳子行程id,出錯回傳-1
行程呼叫fork,當控制轉移到內核中的fork代碼后,OS做:
- 分配新的記憶體塊和內核資料結構給子行程
- 將父行程部分資料結構內容拷貝至子行程
- 添加子行程到系統行程串列當中
- fork回傳,開始調度器
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 const char *str = "hello world";
7
8 pid_t pid = fork();
9 //之后才會運行
10 if(pid == 0){
11 while(1){
12 printf("child: ppid: %d, pid: %d, str: %s\n", getppid(), getpid(), str);
13 sleep(1);
14 }
15 }
16 else if(pid > 0){
17 while(1){
18 printf("father: ppid: %d, pid: %d, str: %s\n", getppid(), getpid(), str);
19 sleep(1);
20 }
21 }
22 else{
23 perror("fork");
24 }
25 return 0;
26 }
注意:雖然父子行程代碼共享,但fork之后才有子行程,所以子行程是執行fork之后的代碼,
fork常規用法:
1、一個父行程希望復制自己,使父子行程同時執行不同的代碼段,例如:父行程等待客戶端請求,生成子行程來處理請求,
2、一個行程要執行一個不同的程式,例如子行程從fork回傳后,呼叫exec函式,
1.為什么fork有兩個回傳值?
2.一個變數里面,怎么會有兩個不同的值,從而讓父子進入不同的業務邏輯,
> fork后父行程回傳時,本質是把回傳值寫入變數pid,而此時子行程已經創建好了,必定發生了寫時拷貝,
所以這一個變數名,內容是不同的,而本質是父子頁表映射資料到了不同的記憶體區域,所以接下來父子行程讀取pid拿到的值就不一樣,
寫時拷貝
通常,父子代碼共享,父子在不寫入時,資料也是共享的,當任意一方試圖寫入,便以寫時拷貝的方式各自一份副本,
寫時拷貝的程序實作是由OS參與完成的,

為什么要有寫時拷貝(資料的)?
保證父子行程的“獨立性”
1.節省資源,父子行程創建時,拷貝不需要寫入修改的資料(只讀)是沒有意義的,如果直接把資料各自拷貝一份,就浪費了記憶體和系統資源,
2.提高fork創建的效率,fork時創建資料結構,如果還要將資料拷貝一份,fork效率降低
3.減少fork失敗的概率,fork本身就是向系統要更多的資源,而要越多的資源就越容易導致fork失敗,
行程終止
行程退出的情況分類:
1.代碼跑完,結果正確,退出碼:0
2.代碼跑完,結果不正確,邏輯問題,但是沒有導致程式崩潰,退出碼:!0
3.代碼沒有運行完畢,程式崩潰了,退出碼沒有意義,
行程常見退出方法:
正常終止(可以通過
echo $?查看行程退出碼):
1.main函式return
2.任何函式exit
例外退出:
ctrl+c,信號終止
main函式中,return的值(退出碼)代表行程退出,結果是否運行正確,0代表成功,而return的0是給系統看的,以此確認行程執行結果是否正確,如果我們想看最近一次執行的一個程式運行結束時的退出碼,可以用echo $?來查看

退出碼:可以認為定義,也可以使用系統的錯誤碼list
當程式運行失敗時,最關心的是失敗的原因,而計算機擅長處理整數型別的資料(0, 1, 2, 3…), int(整數)-> string(錯誤碼描述)

父行程一般需要知道子行程退出的結果,即行程的退出碼,但父行程也可以不關心子行程的運行結果,
行程非正常結束:野指標、/0、越界等,此時退出碼無意義,(此時是由信號來終止的)
main函式return,非main函式的return不是終止行程,而是結束函式,例如:
int show()
{
return 0;
}
int main()
{
show();
return 0;
}
這里main函式中呼叫完show,這個行程并不會終止,
exit:在任何函式中exit都表示直接終止行程

exit:在退出時會執行用戶定義的資源清理函式,包括重繪緩沖區,關閉流等,
_exit:在退出時不會進行后續資源處理,直接終止行程,
可以看到使用_exit時,退出碼照樣是11,
站在OS角度,如何理解行程終止?
核心思想:歸還資源
1."釋放"曾經為了管理行程所維護的所有的資料結構物件,
2."釋放"程式代碼和資料占用的記憶體空間,
3.取消曾經該行程的鏈接關系,
釋放:不是真的把資料結構物件銷毀,而是設定為不用狀態,然后保存起來,如果不用的物件多了,就有了一個"資料結構的池",
記憶體池:先申請分配一定大小的空間,在需要使用時再使用記憶體池中的空間,就不需要每次需要記憶體時都進行new/malloc申請空間,提高了用戶的效率,
釋放資料結構物件:當要創建行程時,需要將記憶體池中拿出一塊空間,并將這塊空間強轉成task_struct*,再進行訪問,但如果每次都要強轉就太麻煩,當一個pcb沒人用時,可以將該pcb取出并鏈接到資料結構池中,該程序就是釋放不用的資料結構物件,而需要用時再從池中取出,就不用進行強轉了,這種釋放規則叫做Slab分派器,
釋放代碼:不是將代碼和資料結構清空,而是把記憶體設定為無效即可,
例如我們在下載電影資源時,所需下載拷進電腦的時間很多,洗掉卻很快,說明寫入和刪的邏輯是不同的,寫入時需要開辟空間,而刪的本質是標識資料對應在磁盤上無效,一旦標識無效即意味著可以被覆寫,在寫入新資料時,將該無效資料被覆寫也就是被清除了,

行程等待
如果一個父行程終止了,內核會安排init行程成為它的孤兒行程的養父,init行程的PID為1,是在系統啟動時由內核創建的,它不會終止,是所有行程的祖先,如果父行程沒有回收它的僵死子行程就終止了,那么內核會安排init行程去回收它們,不過,長時間運行的程式,比如shell或者服務器,總是應該回收它們的僵死子行程,即使僵死子行程沒有運行,它們依然消耗系統的記憶體資源,
一個行程可以通過呼叫waitpid函式來等待它的子行程終止或停止,
等待的必要性:
- 回收僵尸,解決記憶體泄漏,僵尸狀態無法被殺死
- 父行程需要獲取子行程的運行結束狀態(不是必須的)
- 父行程要盡量晚于子行程退出,可以規范化進行資源回收,(編碼相關)
行程等待的方法:
wait/waitpid
- 如果子行程已經退出,呼叫wait/waitpid時,wait/waitpid會立即回傳,并且釋放資源,獲得子行程退出資訊,
- 如果在任意時刻呼叫wait/waitpid,子行程存在且正常運行,則行程可能阻塞,
- 如果不存在該子行程,則立即出錯回傳,
**wait:**等待任意一個子行程,當子行程退出,wait就可以回傳,
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
回傳值:成功則回傳被等待行程pid,失敗回傳-1
引數:輸出型引數,獲取子行程退出狀態,不關心則可以設定為NULL
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6
7 int main()
8 {
9 pid_t id = fork();
10 if(id < 0){
11 perror("fork");
12 return 1;//自定義
13 }
14 else if(id == 0){
15 //child
16 int count = 5;
17 while(count){
18 printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
19 sleep(1);
20 }
21 printf("child quit...\n");
22 exit(0);
23 }
24 else{
25 printf("father is waiting...\n");
26 pid_t ret = wait(NULL);
27 printf("father is wait done, ret: %d\n", ret);
28 }
29 }

int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;//自定義
}
else if(id == 0){
//child
int count = 5;
while(count){
printf("child is running: %d, ppid: %d,pid:%d\n", count- -, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
exit(0);
}
else{
printf("father is waiting...\n");
sleep(10);
pid_t ret = wait(NULL);
printf("father is wait done, ret: %d\n", ret);
sleep(3);
printf("father quit...\n");
}
return 0;
}
可以看到,5s后子行程變為僵死狀態,再過5s后子行程被回收,

一般而言,我們需要fork之后,讓父行程等待,
waitpid方法:
pid_ t waitpid(pid_t pid, int *status, int options);
回傳值:
當正常回傳的時候waitpid回傳收集到的子行程的行程ID;
如果設定了選項WNOHANG,而呼叫中waitpid發現沒有已退出的子行程可收集,則回傳0;
如果呼叫中出錯,則回傳-1,這時errno會被設定成相應的值以指示錯誤所在;
引數:
pid:
pid=-1,等待任一個子行程,與wait等效,
pid>0.等待其行程ID與pid相等的子行程, (等待指定的行程)
status:
WIFEXITED(status): 若為正常終止子行程回傳的狀態,則為真,(查看行程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子行程退出碼,(查看行程的退出碼)
options:
WNOHANG: 若pid指定的子行程沒有結束,則waitpid()函式回傳0,不予以等待,若正常結束,則回傳該子行程的ID,
默認options設定為0,是阻塞式等待,
int main()
{
pid_t id = fork();
if(id == 0){
int count = 5;
while(count){
printf("child is runing: %d, ppid: %d, pid:%d\n", count-- , getppid(),getpid());
sleep(1);
}
printf("child quit...\n");
exit(0);
}
//father
sleep(8);
pid_t ret = waitpid(id, NULL, 0);
printf("father wait done, ret : %d\n", ret);
sleep(3);
}

獲取子行程status
wait和waitpid都有一個status引數,該引數是一個輸出型引數,由作業系統填充,如果傳遞NULL,則表示不關心子行程的退出狀態資訊,否則,作業系統會根據該引數,將子行程的推出資訊反饋給父行程,status不能簡單地當作整型來看待,可以當作位圖來看待,(之研究status低16bit位)

正常終止:
int main()
{
pid_t id = fork();
if(id == 0){
int count = 5;
while(count){
printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
exit(123);
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
int code = (status >> 8) & 0xFF;
printf("father wait done, ret : %d\n, exit code: %d\n", ret, code);
if(code == 0){
printf("漂亮,事情辦成了!\n");
}
else{
printf("完了,需要重來了!\n");
}
}

注意:不能定義全域變數code來拿到子行程的退出結果,因為父子行程是獨立的,當寫入變數時,會進行寫時拷貝,此時父行程看不到該變數,也就無法取得子行程退出狀態,
子行程雖然已經結束了,但子行程還是僵尸,子行程資料結構并沒有完全被釋放,當行程退出時,如task_struct里會被填上子行程退出時的退出碼,所以waitpid拿到的status的值,是通過task_struct內部拿到的,
例外終止:
一般行程提前終止,本質是該行程收到了os發送的信號,
此時status的低7位標識當前行程退出時的終止信號,
信號是從1開始的,也就是說如果檢測到低7位全是0,那就是正常終止,此時的退出狀態才有意義,
int main()
8 {
9 pid_t id = fork();
10 if(id == 0){
11 int count = 5;
12 while(count){
13 printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
14 sleep(1);
E> 15 int *p = 0x12345;
16 *p = 100;
17 }
18 printf("child quit...\n");
19 exit(10);
20 }
21 //father
22 int status = 0;
23 pid_t ret = waitpid(-1, &status, 0);
24 int code = (status >> 8) & 0xFF;
25 int sig = status & 0x7F; //0111 1111
26 printf("father wait done, ret : %d\n, exit code: %d, sig: %d\n", ret, code, sig);
}
野指標操作,程式崩潰,sigle不為0,父行程得知子行程例外終止,


完整的等待程序:
7 int main()
8 {
9 pid_t id = fork();
10 if(id == 0){
11 int count = 5;
12 while(count){
13 printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
14 sleep(1);
15 }
16 printf("child quit...\n");
17 exit(10);
18 }
19 int status = 0;
20 pid_t ret = waitpid(id, &status, 0);
21 if(ret > 0){
22 printf("wait success!\n");
23 if((status & 0x7F) == 0){
24 printf("process quit normal!\n");
25 printf("exit code: %d\n", (status>>8)&0xFF);
26 }
27 else{
28 printf("process quit error!\n");
29 printf("sig: %d\n", status&0x7F);
30 }
31 }
}

系統提供了一堆的宏(函式),可以用來判斷退出碼,退出狀態,

7 int main()
8 {
9 pid_t id = fork();
10 if(id == 0){
11 int count = 5;
12 while(count){
13 printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
14 sleep(1);
15 }
16 printf("child quit...\n");
17 exit(10);
18 }
19 int status = 0;
20 pid_t ret = waitpid(id, &status, 0);
21 if(ret > 0){
22 printf("wait success!\n");
23 if(WIFEXITED(status)){
24 printf("process quit normal!\n");
25 printf("exit code: %d\n", WEXITSTATUS(status));
26 }
27 else{
28 printf("process quit error!\n");
29 }
30 }
}

如果options傳WNOHANG,等待方式為非阻塞,如果傳0,默認是阻塞的,我們目前所呼叫的函式(都是單執行流,簡單),都是阻塞函式,阻塞等待:呼叫方一直在等待,期間不做任何事,而非阻塞是在不斷檢測狀態,
非阻塞輪詢方案:父行程多次呼叫waitpid,檢測子行程的運行狀態,最終如果檢測到了子行程的退出狀態,waitpid才成功回傳,而在此之前都是失敗回傳,
注意:waitpid的失敗回傳有兩種意思:1、并不是真正失敗,僅僅是對方的狀態還沒有達到預期(下次再檢測),2、真的失敗了
阻塞等待中,是父行程在等待,子行程在跑代碼,
“等”:將當前行程放入等待佇列,并將行程狀態設定為非R狀態,
喚醒行程->等待佇列->運行佇列->R
當我們運行過多程式,計算機卡住時,有可能就是因為運行行程太多,導致OS把行程放入等待佇列,
非阻塞等待:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6
7 int main()
8 {
9
10 pid_t id = fork();
11 if(id == 0){
12 int count = 3;
13 while(count){
14 printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
15 sleep(1);
16 }
17 printf("child quit..\n");
18 exit(10);
19 }
20 int status = 0;
21 while(1){
22 pid_t ret = waitpid(id, &status, WNOHANG);
23 if(ret == 0){
24 printf("wait next!\n");
25 printf("father do other thing!\n");
26 }
27 else if(ret > 0){
28 printf("wait success, ret: %d, code: %d\n", ret, WEXITSTATUS(status));
29 break;
30 }
31 else{
32 printf("wait failed\n");
32 printf("wait failed\n");
33 break;
34 }
35 }
36 }
行程程式替換
創建子行程的目的有:1.執行父行程的部分代碼,2.執行其它的代碼,
進行行程替換的目的就是讓子行程執行其它程式的代碼,
子行程不改變行程內核的資料結構,只修改部分的頁表資料,然后將新程式的代碼和資料加載到記憶體,重新構建映射關系,和父行程徹底脫離關系,就是行程替換,
在進行程式替換的時候,沒有創建新的行程,子行程的pid沒改變,

用fork創建子行程后執行的是和父行程相同的程式(但有可能執行不同的代碼分支),子行程往往要呼叫一種exec函式以執行另一個程式,當行程呼叫一種exec函式時,該行程的用戶空間代碼和資料完全被新程式替換,從新程式的啟動例程開始執行,呼叫exec并不創建新行程,所以呼叫exec前后該行程的id并未改變,
如何進行行程替換?
替換函式:

execl:
其中l代表list,串列
int execl(const char *path, const char *arg, ...);

int main()
{
printf("my process begin\n");
execl("/usr/bin/ls", "ls", "-a", "-l", "-i", NULL);
printf("my process end!\n");
return 0;
}

exec* 程式替換,一旦替換完成,原程式后面的代碼就不再執行,所以end也沒有輸出,回傳值也沒有意義,所以exec*函式不用考慮回傳值,只要回傳,
exec是特殊的加載器,當要運行軟體時,可以直接將行程讀取進記憶體,
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6
7 int main()
8 {
9 pid_t id = fork();
10 if(id == 0){
11 //child
12 printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
13 execl("/usr/bin/ls", "ls", "-l", NULL);
14 exit(1);
15 }
16
17 int count = 3;
18 while(count){
19 printf("I am father, pid: %d\n", getpid());
20 sleep(1);
21 count--;
22 }
23 int status = 0;
24 //father
25 pid_t ret = waitpid(id, &status, 0);
26 if(ret > 0){
27 printf("child status -> sig: %d, code: %d\n", status&0x7F, (status >> 8) & 0xFF);
28 }
29 else{
30 printf("wait error!\n");
31 }
32 return 0;
}
這樣讓創建子行程后讓子行程去執行新的程式(沒有創建新行程),父行程,父行程得到結果,檢測命令并回收子行程的退出資訊
這里的退出碼code是子行程的退出碼,但是是執行完ls后的退出碼,

其它介面:
int execv(const char *path, char *const argv[]);
將child中代碼改為:
char *const my_argv[]={
"ls",
"-l",
"-a",
"-i",
NULL
};
execv("/usr/bin/ls", my_argv);
結果同上
命名理解:
l(list):表示引數采用串列
v(vector):引數用陣列
p(path):有p自動搜索環境變數PATH
e(env):表示自己維護環境變數
execlp:
含有p:可以自動搜索環境變數PATH(系統的命令才可以找到,或者把自己的命令匯入到PATH中),不用寫路徑
int execlp(const char *file, const char *arg, ...);
例如:
execlp("ls", "ls", "-l", "-a", NULL);
execlp("top", "top", NULL);
execvp類似,只不過后面是用一個指標陣列傳參
execle:
e:傳入默認的或者自定義的環境變數給目標可執行程式
int execle(const char *path, const char *arg, ...,char *const envp[]);
20 char *const my_env[] = {
E> 21 "MYENV=helloworld!",
22 NULL
23 };
24 execle("./mycmd", "mycmd", NULL, my_env);
25 exit(1);
26 }

在exec_cmd中呼叫execl傳入環境變數MYENV的值,mycmd中接收并列印出,如果單獨運mycmd依舊為空,

exec_cmd能執行系統的命令,也可以執行自己寫的命令,
如果想要跨語言之間耦合,如C語言想調C++的代碼,就可以exec這樣的程式替換,
execve:
int execve(const char *path, char *const argv[], char *const envp[]);
20 char *const my_argv[] = {
W> 21 "mycmd",
22 NULL
23 };
24 char *const my_env[] = {
W> 25 "MYENV=helloworld!",
26 NULL
27 };
28 execve("./mycmd", my_argv, my_env);
也可以將main函式中的環境變數引數env傳入,但需要匯出環境變數export MYENV=helloworld,main函式可以獲得這個環境變數,并把這個環境變數導給子行程,
1.什么是程式替換:通過exec系列的函式,讓特定行程去加載磁盤中的其它程式,以達到運行的目的,期間不創建新的行程,
2.為什么要程式替換:子行程執行新的程式的需求,
3.如何進行程式替換:原理->行程地址空間的問題->磁盤換入程式到記憶體->對可執行程式的理解(exe 檔案) exec*
4.后續:a.exec*只要回傳了,就說明出錯了,b.各種借口的理解:l,v,p,e
簡易shell
用戶在命令列輸入某些命令,交給shell解釋器,shell解釋器解釋命令時,并不是自己解釋,而是呼叫fork創建子行程,子行程再執行命令(其實就是OS執行命令),OS再把結果通過shell解釋器回傳給用戶,而子行程是通過exec系列函式執行命令的,
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#define NUM 128
#define SIZE 32
char command_line[NUM];
char *command_parse[SIZE];
int main()
{
while(1){
memset(command_line, '\0', sizeof(command_line));
printf("[ymz@myhost 我的shell]$ ");
fflush(stdout);
//1. 資料讀取
if(fgets(command_line, NUM-1, stdin)){
command_line[strlen(command_line) - 1] = '\0';
//ls -a -l -i
//2. 字串(命令列資料分析)
int index = 0;
command_parse[index] = strtok(command_line, " ");
while(1){
index++;
command_parse[index] = strtok(NULL, " ");
if(command_parse[index] == NULL){
break;
}
}
//3. 判斷命令
//a. 內置命令
//b. 第三方命令
if(strcmp(command_parse[0], "cd") == 0 && chdir(command_parse[1]) == 0){
continue;
}
//4. 執行非內置命令
if(fork() == 0){
//子行程
execvp(command_parse[0], command_parse);
exit(1);
}
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
if(ret > 0 && WIFEXITED(status)){
printf("Exit Code: %d\n", WEXITSTATUS(status));
}
}
}
return 0;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/414031.html
標籤:其他

