🧑?💻作者: @情話0.0
📝專欄:《Linux從入門到放棄》
👦個人簡介:一名雙非編程菜鳥,在這里分享自己的編程學習筆記,歡迎大家的指正與點贊,謝謝!
進程退出和等待
- 前言
- 一、進程創建
- 1.1 fork函數
- 1.2 寫時拷貝
- 1.3 fork常規用法
- 1.4 fork調用失敗的原因
- 二、進程退出
- 2.1 進程退出場景
- 2.1.1 查看退出碼
- 2.1.2 退出碼的含義
- 2.2 如何理解進程退出?
- 2.3 進程退出的方式
- 三、進程等待
- 3.1 進程等待的原因
- 3.2 什么是進程等待?
- 3.3 進程等待的方式
- 3.3.1 wait方法
- 3.3.2 waitpid方法
- 3.4 子進程退出狀態
- 3.5 非阻塞式等待
- 總結
前言
之前的幾篇博客已經是對進程的相關概念做了詳細了解,現階段對進程的定義為內核數據結構加上該進程對應的代碼和數據,操作系統對進程通過先描述再組織的方式做管理。有了這些預備知識,接下來就是要學習如何控制進程,也就是在操作上該怎么做。
一、進程創建
1.1 fork函數
??關于fork函數的知識,此篇博客有詳細介紹:進程創建
進程調用fork,當控制轉移到內核中的fork代碼后,內核做:
- 分配新的內存塊和內核數據結構給子進程
- 將父進程部分數據結構內容拷貝至子進程
- 添加子進程到系統進程列表當中
- fork返回,開始調度器調度
在調用fork函數之后,系統會將父進程的代碼拷貝一份給子進程,同時會有兩個執行流分別執行父進程和子進程,要注意的是子進程不會去執行fork之前的代碼。
1.2 寫時拷貝
??父子進程代碼共享,父子再不寫入時,數據也是共享的,當任意一方試圖寫入,便以寫時拷貝的方式各自一份副本。具體見下圖:
在修改內容之前,父子進程的在物理內存頁的數據、代碼指向同一塊位置,如子進程對數據進行修改,,那么此時就會發生寫時拷貝,在物理內存頁重新開辟一塊空間將修改后的數據存入其中。
因為在操作系統是不允許空間的浪費,所以不會將父進程的所有代碼數據都在物理內存中重新拷貝一份,而是通過寫時拷貝的方式在子進程需要使用(修改)數據的時候才會重新開辟空間,它是一種按需申請資源的策略。
1.3 fork常規用法
- 一個父進程希望復制自己,使父子進程同時執行不同的代碼段。例如,父進程等待客戶端請求,生成子進程來處理請求。
- 一個進程要執行一個不同的程序。例如子進程從fork返回后,調用exec函數。
1.4 fork調用失敗的原因
- 系統中有太多的進程
- 實際用戶的進程數超過了限制
二、進程退出
2.1 進程退出場景
a. 正常運行完畢(1. 結果正確 ?2. 結果不正確)
b. 崩潰了(進程異常) ?崩潰的本質:進程因為某些原因,導致進程收到了來自操作系統的信號(kill -9)
2.1.1 查看退出碼
我們一般在寫C語言程序都會在main函數結束時返回 0,這個0代表著該進程的退出碼,在linux中,可通過這樣的指令查看進程的退出碼:echo $?
。看下面代碼:
int add_to_top(int num)
{int sum=0;for(int i=1;i<=num;i++){sum+=i;}return sum;
}int main()
{int ret=add_to_top(100);if(ret==5050)return 1;else return 0;
}
上面的代碼要實現的功能:從1加到100,若和為5050,則返回1,否則返回0。通過下圖可以看到該進程的退出碼為1,表示結果正確。但是奇怪的是,后兩次的查看退出碼都為了0,這是因為該指令只會保留最近一次執行的進程的退出碼!后兩次代表著該條指令執行后的退出碼。
2.1.2 退出碼的含義
我們看到的退出碼都是數字,對于程序員來說,我們可能知道一些退出碼所代表的含義,但是對于一般人來說看到這些數字并不了解所蘊含的意義。所以對于一般人來說,如果你只給他退出碼是沒有價值,因為他并不知道這些退出碼代表的含義。關于退出碼的含義我們可以自定義,下面看一下C語言所提供的退出碼的含義。
int main()
{for(int i=0;i<200;i++){printf("%d:%s\n",i,strerror(i));}return 0;
}
這只是前二十個,后面還有更多。當然這是在linux操作系統下,在windows下所提供的退出碼含義是不同的。
2.2 如何理解進程退出?
關于進程的退出,可以理解的是操作系統內少了一個進程,操作系統要釋放進程對應的內核數據結構+代碼和數據。
2.3 進程退出的方式
- main函數return。而其他函數的return僅僅代表該函數的返回。對于這種方式來說,進程執行本質是main執行流執行,當main函數執行完時代表著進程也就結束了。
- exit函數退出。exit函數所包含的數字為該進程的退出碼,在函數任意位置調用直接使進程退出。
- _exit函數退出。直觀感覺上和exit的功能是一樣的,但是在一些細節是不一樣的。exit函數在退出的時候會自動刷新緩沖區,而_exit函數不會刷新緩沖區。它們兩個的關系是一種包含和被包含的關系。從下面這個圖可以得到一個暗藏的點:緩沖區不在操作系統內。
三、進程等待
3.1 進程等待的原因
- 之前講過若子進程先退出,而父進程并沒有讀取子進程狀態,就可能造成‘僵尸進程’的問題,進而造成內存泄漏。
- 進程一旦變成僵尸狀態,那就刀槍不入,“殺人不眨眼”的kill -9 也無能為力,因為誰也沒有辦法殺死一個已經死去的進程。
- 我們為什么要創建子進程,目的就是為了讓子進程幫助我們去完成某些事情,關于父進程派給子進程的任務完成的情況,可能我們不會關心完成的對不對,也可能會關心子進程運行完成的結果對還是不對,亦或是否正常退出。
- 避免內存泄漏(必)
- 獲取子進程的執行結果。(可能)
關于子進程的退出結果,有三種可能性:
a. 代碼跑完,結果對;
b. 代碼跑完,結果不對;
c. 代碼運行異常;
關于結果對或不對,可以通過退出碼的方式判別,代碼運行異常則是收到某種信號。因此衡量一個進程運行的怎樣是通過退出碼+信號的方式來執行的。
3.2 什么是進程等待?
通過系統調用,獲取子進程退出碼或者退出信號的方式,同時釋放內存問題。
3.3 進程等待的方式
3.3.1 wait方法
pid_t wait(int *status);
返回值:成功返回被等待進程pid,失敗返回-1。
參數:輸出型參數,獲取子進程退出狀態,不關心則可以設為NULL
//代碼功能:父進程在休眠5秒的過程中子進程先運行2秒,然后子進程退出,2秒之后,父進程對子進程做進程等待操作。
int main()
{pid_t ret=fork();if(ret==0){//子進程int cnt=2;while(cnt--){printf("我是子進程,我現在活著呢,我離死亡還有%d秒,pid:%d,ppid:%d\n",cnt,getpid(),getppid());sleep(1);}_exit(0);}sleep(5);//父進程pid_t ret_id=wait(NULL);printf("我是父進程,等待子進程成功,pid:%d,ppid:%d\n",getpid(),getppid());return 0;
}
在運行代碼之后我們應該觀察到的現象:父子進程的狀態最開始都為運行狀態,子進程經2秒輸出2條語句,然后退出變為僵尸狀態,父進程依然為運行狀態,再過3秒之后,父進程對子進程等待回收,然后全部退出。
3.3.2 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。
3.4 子進程退出狀態
- 在 wait 和 waitpid 中,都有一個status參數,該參數是一個輸出型參數,由操作系統填充。它的功能是為了獲取子進程的退出狀態。如果傳遞NULL,表示不關心子進程的退出狀態信息。否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程。
- status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16比特位):
- 通過對上圖理解,我們應該明白關于子進程的退出狀態。如果進程是正常退出,那么status位圖的低八位為0,次低八位為進程的退出狀態,也就是通過這次低八位獲取進程的退出碼。如果進程是被某種信號所殺而導致的異常退出,則只需要關心低七位,讀到的結果為導致該進程退出的終止信號所對應的數字,coredump標志位目前不需要了解。
int main()
{pid_t id=fork();if(id==0){//子進程int cnt=2;while(cnt--){printf("我是子進程,我現在活著呢,我離死亡還有%d秒,pid:%d,ppid:%d\n",cnt,getpid(),getppid());sleep(1);}// int a=10;// a/=0;_exit(123);}sleep(5);int status=0;pid_t ret_id=waitpid(id,&status,0);printf("我是父進程,等待子進程成功,pid:%d,ppid:%d,status signal:%d,status code:%d\n",getpid(),getppid(),(status&0x7F),((status>>8)&0xff));return 0;
}
看上面這段代碼,如果按照這樣的邏輯,那么最終的運行結果為(只看退出狀態):父進程獲取到子進程的退出信號肯定為0,因為是正常退出,退出狀態則為數字123;若將那兩條注釋的代碼取消,那么子進程就會因為除0操作導致異常退出,那么此時父進程就會讀到對應的退出信號,輸出結果為該信號對應的數字。
- 父進程是如何獲取子進程的退出狀態信息的呢?子進程有自己的PCB、地址空間、頁表和內存,而在PCB的內部會有兩個屬性:exit_code、exit_signal。當子進程執行完畢時將main函數的返回值寫到 exit_code 中,如果出現異常操作系統則將遇到信號所對應的數字編號寫到 exit_signal 中。當子進程退出后,操作系統會將這份PCB維護起來,所以就需要通過wait/waitpid這樣的系統調用接口將從這份PCB讀到的這兩個屬性以上面那種位圖的方式設置到status參數中。
- 父進程在wait的時候,如果子進程沒退出,那父進程在干什么?在子進程沒有退出的時候,父進程只能一直在調用waitpid進行等待——阻塞等待。
3.5 非阻塞式等待
waitpid(id,&status,WNOHANG)
??上一小節的 waitpid 方法為阻塞等待,而非阻塞等待與阻塞等待的區別在于第三個參數的不同,阻塞等待是在子進程還沒有退出的時候父進程只能一直等待直到子進程退出,非阻塞等待是子進程還沒有退出時,父進程可以干一些其他事情而不是什么事情不干就在等待子進程退出。
??下面這段代碼將通過非阻塞的形式讓父進程在還未等待到子進程的退出信息的時候去執行其他事情。
#define TASK_NUM 10
void sync_disk()
{printf("這是一個刷新數據的任務!\n");
}
void sync_log()
{printf("這是一個同步日志的任務!\n");
}
void net_send()
{printf("這是一個進行網絡發送的任務!\n");
}
typedef void (*func_t)();
func_t other_task[TASK_NUM] = {NULL}; //函數指針數組int LoadTask(func_t func)
{int i = 0;for(; i < TASK_NUM; i++){if(other_task[i] == NULL) break;}if(i == TASK_NUM) return -1;else other_task[i] = func;return 0;
}
void InitTask()
{for(int i = 0; i < TASK_NUM; i++) other_task[i] = NULL;LoadTask(sync_disk);LoadTask(sync_log);LoadTask(net_send);
}
void RunTask()
{for(int i = 0; i < TASK_NUM; i++){if(other_task[i] == NULL) continue;other_task[i]();}
}
int main()
{pid_t id=fork();if(id==0){//子進程int cnt=5;while(cnt--){printf("我是子進程,我現在活著呢,我離死亡還有%d秒,pid:%d,ppid:%d\n",cnt,getpid(),getppid());sleep(1);}_exit(123);}InitTask();while(1){int status=0;pid_t ret_id=waitpid(id,&status,WNOHANG);if(ret_id==-1){printf("等待錯誤!\n");break;}else if(ret_id==0){//子進程還未退出,父進程執行RunTask函數RunTask();sleep(1);}else{if(WIFEXITED(status))//正常退出{printf("我是父進程,等待子進程成功,pid:%d,ppid:%d,status signal:%d,status code:%d\n",getpid(),getppid(),(status&0x7F),WEXITSTATUS(status));}else//非正常退出printf("我是父進程,等待子進程成功,pid:%d,ppid:%d,status signal:%d,status code:%d\n",getpid(),getppid(),(status&0x7F),((status>>8)&0xff));break;}}return 0;
}
在子進程正常退出并且父進程等待成功的時候可以通過宏的方式來獲取子進程的退出碼,之前的方法優雅度或者可擴展性都不太好,當
WIFEXITED(status)
為真的時候,通過WEXITSTATUS(status)
獲取退出碼,若不為真也就是異常退出時只能使用以前的方法。
總結
總結:
??本文深入探討了操作系統中進程管理的三個核心方面:進程的創建、退出和等待。首先,我們了解了進程創建的過程,它涉及到操作系統如何為新進程分配必要的資源,包括內存空間和處理器時間,并初始化進程表以跟蹤和管理進程狀態。接著,我們討論了進程退出的不同方式,如正常退出、異常退出以及由于接收到信號導致的退出,每種方式都對系統穩定性和資源管理產生不同的影響。
??最后,我們詳細分析了進程等待的概念,即一個進程可能需要暫停執行,直到滿足特定條件。這可能包括等待I/O操作完成、等待獲取資源或等待其他進程的結束。文章強調了實現有效等待機制的重要性,并指出了同步和通信在確保系統資源合理利用和進程間順暢協作中的關鍵作用。
??通過這篇博客,我們不僅學習了關于進程操作的基本知識,還加深了對于操作系統內部機制如何協同工作的理解。這些內容為我們進一步研究計算機科學的其他領域打下了堅實的基礎。