Linux作業系統
- 1. 認識fork
- 1.1 fork父子執行順序,代碼,和資料復制問題
- 1.2 為什么fork會有兩個回傳值?多行程怎么運行的?
- 1.3 為何給父行程回傳pid,給子行程回傳0呢?
- 2. 行程狀態
- 2.1 理解行程的各個狀態
- 2.3 查看行程之間父子關系
- 3. 僵尸行程
- 3.1 為什么要有僵尸狀態?
- 3.2 退出碼
- 3.4 僵尸行程的危害
- 4. 孤兒行程
- 拓展:systemd與init
1. 認識fork
通過man fork可以看出這個函式創建了一個子行程

呼叫fork函式,查看實際表現出來的現象
#include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 printf("輸出一遍\n");
7 fork();
8 sleep(1);
9 printf("輸出兩遍pid:%d,ppid:%d\n",getpid(),getppid());
10 sleep(1);
11 return 0;
12 }
執行程式,fork創建了子行程,之后的printf輸出了兩遍,

我們可以看到第一次輸出的行程id它的父行程是bash,
第二次輸出的行程id的父行程id是第一次輸出的行程,
所以可以這么理解:
bash - 爺爺
第一個行程 - 兒子
第二個行程 - 孫子

通過閱讀幫助檔案,上面說明fork函式是有兩個回傳值的,回傳值為當前行程的子行程id,我們將這兩個回傳值看一下,
#include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 printf("輸出一遍\n");
7 pid_t ret = fork();
8 sleep(1);
9 printf("輸出兩遍pid:%d,ppid:%d,ret:%d\n",getpid(),getppid(),ret);
10 sleep(1);
11 return 0;
12 }
可以看到程式的行程通過fork創建子行程,子行程id是20044(孫子)
這個子行程沒有子行程所以ret為0;

但是上面的編碼不夠嚴謹,一般多行程書寫方式應該如下:用if-else陳述句嵌套分流,
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 pid_t ret = fork();
7 if(ret>0)
8 {
9 printf("pid:%d , ppid:%d , ret:%d\n",getpid(),getppid(),ret);
10 }
11
12 else if(ret==0)
13 {
14 printf("pid:%d , ppid:%d , ret: %d\n",getpid(),getppid(),ret);
15 }
16 else{
17 printf("fork err");
18
19 }
20 sleep(1);
21 return 0;
22 }
~

在c和c++學習程序中,if和else if是不能同時運行的,同樣死回圈也是同樣的道理,但是在多行程中他可以運行,因為是兩個執行流分別執行,
1.1 fork父子執行順序,代碼,和資料復制問題
學習行程的時候知道,行程就是可執行程式的鏡像加載到記憶體,但是行程比可執行程式要大,原因是還有了組織管理他所用的資料結構(目前學到的task_struct)
那么創建了子行程就要在創建一組資料結構+代碼+資料

在創建子行程的時候,代碼是共享的(只讀),資料是自己私有的(在寫時進行了拷貝),創建完畢之后,兩者穿插開始執行,隨機運行,
1.2 為什么fork會有兩個回傳值?多行程怎么運行的?
第一個問題:
首先fork是一個函式,還是系統呼叫,

return id之前,函式邏輯(共享的代碼)被執行完,子行程已經創建,現在已經有兩個行程,依次回傳自己的id(私有的資料),所以就有了兩個回傳值,這也正說明了行程的獨立性,
注意:雖然共享了所有代碼,但是由于有程式計數器(背景關系)的存在,所以子行程在fork之后開始執行,
第二個問題:
cpu內部為了實作行程調動,為行程創建一個回圈佇列,回圈佇列在記憶體里面,運行的時候一個個加載在佇列里,
1.3 為何給父行程回傳pid,給子行程回傳0呢?
父行程可能存在很多個子行程,所以必須通過這個回傳的子行程ID來跟蹤某個子行程,
而子行程只有一個父行程,父行程的ID可以通過getppid取得,這個子行程的id可以通過getpid來獲取自己的id,
通過論壇可以看看大佬的回答(手動翻譯)

2. 行程狀態
在內核當中行程狀態采用陣列保存

- R運行狀態(running):行程在運行當中或者在運行佇列中,
- S睡眠狀態(sleeping):行程在等待事件完成(也有的時候叫可中斷睡眠(interruptible sleep)),
- D磁盤休眠狀態(disk sleep):也叫不可中斷休眠狀態(uninterruptible sleep ),在這個狀態通常在等待io結束,
- T停止狀態(stopped): 可以通過發送SIGSTOP信號給行程來停止這個行程,這個被暫停的行程可已通過發送SIGCONT信號來讓行程繼續運行,
- X死亡狀態(dead): 這個狀態只是一個回傳狀態,你不會在任務串列里看到這個狀態,
2.1 理解行程的各個狀態
-
R狀態代表正在運行狀態或者處于運行佇列之中
-
寫一個死回圈,并運行他,為什么是s狀態呢,因為cpu是很快的,有printf就意味著有io要顯示到控制臺,導致cpu調度行程99%的時間都在等待,導致查看的時候是S狀態,+代表行程在前臺運行可以被ctrl+c終止,

要看到R狀態,我們可以把printf陳述句注釋掉,只進行死回圈

通常在等待事件結束,結束完之后可以繼續運行 -
當一個行程正在記憶體進行Io操作,以硬碟為例,當這個行程正在從硬碟中讀資料,由于硬碟速度較慢,此時作業系統失誤,或者由于記憶體不足,殺掉行程,致使IO出現錯誤,所以把一個行程設定為D狀態,代表不可被中斷
-
通過kill -l列出命令資訊

SIGSTOP命令是19號,SIGCOUT是18號,
kill -19來讓它進入T狀態

kill -18來讓他重新運行

-
死亡狀態我們看不到,一經死亡作業系統會立馬回收,
2.3 查看行程之間父子關系
ps aux | ps axj 命令

3. 僵尸行程
- 僵尸狀態(zombies)是一個比較特殊的狀態,當行程退出且父行程沒有讀取到子行程退出的回傳代碼時就會產生僵尸行程,
- 僵尸行程會以終止狀態保持在行程表中,并且一直在等待父行程讀取自己的退出代碼,
- 所以只要子行程退出,父行程還在運行,但父行程沒有讀取到子行程退出狀態的代碼子行程進入Z狀態,
手動寫一個僵尸行程,當10s過后子行程退出,
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 int main()
5 {
6 pid_t ret = fork();
7 if(ret>0)
8 {
9 while(1)
10 {
11 printf("pid:%d , ppid:%d , ret:%d\n",getpid(),getppid(),ret);
12 sleep(1);
13 }
14 }
15
16 else if(ret==0)
17 {
18 int cout=0;
19 while(cout<5)
20 {
21 printf("pid:%d , ppid:%d , ret: %d\n",getpid(),getppid(),ret);
22 cout++;
23 sleep(2);
24 }
25 exit(0);
26 }
27 else{
28 printf("fork err");
29
30 }
31 sleep(1);
32 return 0;
33 }
~
~
撰寫一個腳本進行觀察,發現子行程出現僵尸狀態
while :; do ps axj | head -1 && ps axj | grep proc| grep -v grep; sleep 1 ; echo "#############"; done

3.1 為什么要有僵尸狀態?
保持行程基本退出資訊,方便父行程讀取,獲得退出原因,
一般的話,子行程處于僵尸狀態,task_struct是會被保留,行程的退出資訊,是放在行程控制塊的,
3.2 退出碼
每一條命令都是一個行程,echo $?,可以顯示出最近一條命令的退出碼,0表示執行成功,

2表示失敗

3.4 僵尸行程的危害
- 僵尸行程是保留了基本退出資訊和原因,他在等待父行程讀取原因,但是一直不讀取呢那么子行程就會一直處于Z狀態,
- 維護退出狀態需要資料,也就是他也屬于基本資訊處于task_struct中,Z狀態一直不退出,就一直需要資料去維護,那么pcb就需要一直維護他,
- 一個行程可以創建多個子行程,如果都不回收,就會造成大量資源的浪費,資料結構物件本身就是要占用記憶體的,
- 嚴重可能會導致記憶體泄漏
4. 孤兒行程
父行程提前退出,子行程還在,子行程就會被稱為孤兒行程,
將代碼簡單修改一下,父行程提前退出
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 int main()
5 {
6 pid_t ret = fork();
7 if(ret>0)
8 {
9 int cout=0;
10 while(cout<5)
11 {
12 printf("pid:%d , ppid:%d , ret:%d\n",getpid(),getppid(),ret);
13 cout++;
14 sleep(1);
15 }
16 exit(0);
17 }
18
19 else if(ret==0)
20 {
21 while(1)
22 {
23 printf("pid:%d , ppid:%d , ret: %d\n",getpid(),getppid(),ret);
24 sleep(2);
25 }
26 }
27 else{
28 printf("fork err");
29
30 }
31 sleep(1);
32 return 0;
33 }
~
~
可以看到15540父行程退出,15541子行程還在運行

那么當15541這個行程要退出的時候,父行程由于已經退出沒有辦法讀取回傳代碼,符合僵尸行程的條件變成僵尸狀態,由于父行程已經退出,永遠不會回收資源,造成記憶體泄漏,

所以os考慮了這種情況的發生,在父行程退出,子行程還在運行(子行程被稱為孤兒行程)的時候重新給他找一個父親,如圖所示就是1號行程systemd,
注:作業系統是一套管理方式,由1號行程幫我們完成管理作業,
拓展:systemd與init
在以前版本1號行程是init,它有以下缺點
- 啟動時間長,init行程是串行啟動,只有前一個行程啟動完,才會啟動下一個行程,
- 啟動腳本復雜,init行程只是執行啟動腳本,不管其他事情,腳本需要自己處理各種情況,這往往使得腳本變得很長,
所以Systemd 就是為了解決這些問題而誕生的,它的設計目標是,為系統的啟動和管理提供一套完整的解決方案,根據 Linux 慣例,字母d是守護行程(daemon)的縮寫, Systemd 這個名字的含義,就是它要守護整個系統,
Systemd 的優點是功能強大,使用方便,缺點是體系龐大,非常復雜,事實上,現在還有很多人反對使用 Systemd,理由就是它過于復雜,與作業系統的其他部分強耦合,違反"keep it simple && stupid"的Unix 哲學;但是現在的Centos 7 已經將它作為系統的守護行程組件了,
注:Kelly Johnson提出了KISS原則,指在大戰中即使普通機械師也可以修理飛機,所以stupid并不是愚蠢,而是指一般人也能運用(傻瓜式操作),也有簡潔,一目了然的意思,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/260001.html
標籤:其他
