進程控制
fork 函數
fork 函數從已存在的進程中創建新的進程,已存在進程為父進程,新創建進程為子進程
fork 的常規用法
- 一個父進程希望復制自己,使父子進程同時執行不同的代碼段。例如,父進程等待客戶端請求,生成子進程來處理請求
- 一個進程要執行一個不同的程序。例如子進程從fork返回后,調用exec函數
fork 失敗的原因
- 系統中有太多的進程
- 實際用戶的進程數超過了限制
fork 通過寫時拷貝的方式進行內容的修改:
通常,父子代碼共享,父子再不寫入時,數據也是共享的,當任意一方試圖寫入,便以寫時拷貝的方式各自一份副本
返回值:子進程返回 0,父進程返回子進程 pid,出錯返回-1
返回子進程 pid 的原因:方便管理子進程
進程創建時,先分配 task_struct 然后分配空間
進程退出時,先回收資源,然后銷毀 task_struct
僵尸進程
一個進程的關閉是先回收資源,然后再將 PCB 清理,僵尸進程就是 PCB 未被清理的進程
在系統中有個 "?" 環境變量,這個變量用來獲取子進程的返回值,0 表示成功,非零表示失敗,同時不同的非零值可以表示不同的失敗原因,雖然status是int,但是僅有低8位可以被父進程所用。所以_exit(-1)時,在終端執行$?發現返回值是255
進程退出的方式
- 正常終止(可以通過echo $? 查看進程退出碼):
- 從main返回
- 調用exit
- _exit
- 異常退出:
- ctrl + c,信號終止
進程退出時,會產生退出碼和退出信號,進程會將這兩個值寫入 PCB 中,這樣就獲取到了退出的信息,如果進程是異常的,將會產生退出信號,通過退出信號就能判斷出異常的原因,如果沒有退出信息,就可以繼續查看退出碼
_exit 函數
#include<unistd.h>
void _exit(int status);
//參數:status 定義了進程的終止狀態,父進程通過wait來獲取該值
//雖然status是int,但是僅有低8位可以被父進程所用。
//所以_exit(-1)時,在終端執行$?發現返回值是255。
exit 函數
#include <unistd.h>
void exit(int status);
exit 和 _exit 的區別
exit()
:正常終止,會執行清理操作。_exit()
:立即終止,跳過清理操作,更底層。_exit()
是一個低層次系統調用,直接返回內核。
-
- 它保證:
- 立刻終止
- 不執行任何用戶態清理邏輯
子進程退出時推薦使用 _exit,因為 _exit 不會刷新緩沖區,避免了多次刷新,因為在父進程結束時還會再刷新一次
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main() {printf("Hello world\n"); // 緩沖區中暫存pid_t pid = fork();if (pid == 0) {// 子進程exit(0); // 刷新緩沖區 → 輸出 "Hello world"} else {// 父進程wait(NULL);}return 0; // 父進程也刷新緩沖區 → 再輸出一次
}
exit() 在退出進程時會刷新緩沖區,而 _exit() 不會
exit() 是庫函數,而 _exit() 是系統調用
exit最后也會調用 _exit,
但在調用exit之前,還做了其他工作:
- 執行用戶通過 atexit或on_exit定義的清理函數。
- 關閉所有打開的流,所有的緩存數據均被寫入
- 調用_exit
return退出
return
是一種常見的退出進程方法。執行return n;
等同于執行exit(n)
,因為調用main
的運行時函數會將main
的返回值當做 exit
的參數
僵死狀態(Zombies)是一個比較特殊的狀態。當進程退出并且父進程沒有讀取到子進程退出的返回代碼時就會產生僵死(尸)進程
進程等待
子進程退出后,若父進程不進行任何操作,將會產生僵尸進程,造成內存泄漏,一旦進程變成僵尸進程,將會無法被殺死
進行進程等待的原因:
父進程通過等待來解決僵尸進程的問題
父進程獲取子進程的退出信息,知道子進程是什么原因退出的
wait
#include <sys/types.h>/* 提供類型pid_t的定義*/#include <wait.h>pid_t wait(int *status)
返回值:成功返回被等待進程pid,失敗返回-1。
參數:輸出型參數,獲取子進程退出狀態,不關心則可以設置成為NULL
作用:進程一旦調用了wait,就會立刻阻塞自己,由wait分析當前進程中的某個子進程是否已經退出了,如果讓它找到這樣一個已經變成僵尸進程的子進程,wait會收集這個子進程的信息,并將它徹底銷毀后返回;如果沒有找到這樣一個子進程,wait會一直阻塞直到有一個出現
等待就是將父進程設置為 S 狀態,然后將父進程的 PCB 鏈接到子進程,此時就能獲取到子進程的退出狀態
阻塞:子進程沒有結束,父進程執行 wait,等待某種條件的發生,此時就發生了阻塞,阻塞本質上就是進程不在調度隊列上,CPU 不執行進程的代碼
非阻塞等待:在等待的過程中還可以繼續執行進程, 調用者立刻返回,如果事件未發生,不會停在那里等待 ,可能會導致沒有等待到子進程的問題
阻塞等待:在等待過程中只能等待,不能做其他任何事情,不就緒就不返回
特性 | 阻塞等待 | 非阻塞等待 |
調用行為 | 卡住,直到子進程結束 | 立即返回,可能沒等到子進程 |
CPU 資源使用 | 更節省(系統調度) | 需要你自己輪詢,可能浪費 CPU |
使用場景 | 同步執行、流程控制 | 異步程序、服務端進程池管理 |
接口實現 |
|
|
waitpid
waitpid(pid_t pid, int *status, int options);
返回值:當正常返回的時候waitpid返回收集到的子進程的進程ID;如果設置了選項WNOHANG,而調用中waitpid發現沒有已退出的子進程可收集,則返回0;如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在;返回值>0:等待成功,子進程退出,父進程成功獲取退出信息返回值<0:等待失敗返回值==0:檢測成功,但是子進程還沒退出,需要下一次重復等待參數:pid:Pid=-1,等待任一個子進程。與wait等效。Pid>0.等待其進程ID與pid相等的子進程。status:WIFEXITED(status): 若為正常終止子進程返回的狀態,則為真。(查看進程是否是正常退出)WEXITSTATUS(status): 若WIFEXITED非零,提取子進程退出碼。(查看進程的退出碼)options:WNOHANG: 若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待。若正常結束,則返回該子進程的ID。(非阻塞等待)
如果子進程已經退出,調用wait/waitpid時,wait/waitpid會立即返回,并且釋放資源,獲得子進程退出信息
如果在任意時刻調用wait/waitpid,子進程存在且正常運行,則進程可能阻塞
如果不存在該子進程,則立即出錯返回
waitpid 作用和 pid 等價
等待失敗的情況:id 值填錯
非阻塞等待+循環=非阻塞輪詢,較為常用,能夠允許父進程在等待的時候進行其他操作
獲取子進程status
wait和waitpid,都有一個status參數,該參數是一個輸出型參數,由操作系統填充。
如果傳遞NULL,表示不關心子進程的退出狀態信息。 否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程
status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16比特位):
waitpid 和 wait 的區別
函數 |
|
|
原型 |
|
|
功能 | 等待任意一個子進程結束 | 根據條件等待一個或多個特定子進程結束 |
waitpid 的參數:
pid
: 可指定要等待的子進程。
-
>0
: 等待指定 PID 的子進程。-1
: 等價于wait
,等待任意一個子進程。0
: 等待與當前進程同組的任何子進程。<-1
: 等待特定進程組 ID 的任何子進程。
options
: 控制行為,例如:
-
WNOHANG
: 非阻塞地檢查子進程是否結束。WUNTRACED
: 也報告已停止(但未終止)的子進程。
獲取子進程 status
- wait和waitpid,都有一個status參數,該參數是一個輸出型參數,由操作系統填充
- 如果傳遞NULL,表示不關心子進程的退出狀態信息
- 否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程
- status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16比特位)
printf("child exit code:%d\n", (status>>8)&0XFF);
//退出狀態在前8位,因此將后8位移除,通過和FF與操作,將退出狀態中為1的保留,為0的舍去
printf("sig code : %d\n", status&0X7F );//將低7位按位與得到終止信號
?
進程程序替換
用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec并不創建新進程,所以調用exec前后該進程的id并未改變
exec 函數可以讓進程替換掉自己的代碼和數據轉而執行其他的程序
原理:exec 將被替換程序從外存中加載到內存中,將原來進程的代碼和數據替換掉,task_struct 并沒有被替換,因此也就沒有創建新的進程
exec*系列函數在執行完畢后,后面的代碼也就不會被執行了,因為已經被 exec 所執行的函數替換掉了
exec*可以不用在乎其返回值,因為一旦執行成功,后續的代碼全部被替換;
一旦失敗,就會繼續向下執行
子進程執行 exec 時,由于進程具有獨立性,因此會將原來父進程的數據和代碼重新拷貝一份,然后在新拷貝的地方進行替換代碼,這樣就不會影響父進程,此時的父子進程就在數據結構和代碼層面上徹底的分離了
一共有 6 中 exec 系列函數#include<unistd.h>
int execl(const char *path, const char *arg, ...);后面可以加上多個命令的參數,但必須以NULL結尾
int execlp(const char *file, const char *arg, ...);不需要傳程序的路徑,因為會自動在環境變量中查找
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);和execl類似,只不過是將參數放入argv[]中
int execvp(const char *file, char *const argv[]);不需要傳程序的路徑,因為會自動在環境變量中查找
int execve(const char *path, char *const argv[], char *const envp[]);envp為環境變量,可以傳入,也可以自定義,整體把所有環境變量替換掉
命名解釋: l(list) : 表示參數采用列表
v(vector) : 參數用數組
p(path) : 有p自動搜索環境變量PATH
e(env) : 表示自己維護環境變量
exec/exit就像call/return
一個C程序有很多函數組成。一個函數可以調用另外一個函數,同時傳遞給它一些參數。被調用的函數執行一定的操作,然后返回一個值。每個函數都有他的局部變量,不同的函數通過call/return系統進行通信。
這種通過參數和返回值在擁有私有數據的函數間通信的模式是結構化程序設計的基礎。Linux鼓勵將這種應用于程序之內的模式擴展到程序之間。如下圖
一個C程序可以fork/exec另一個程序,并傳給它一些參數。這個被調用的程序執行一定的操作,然后通過exit(n)來 返回值。調用它的進程可以通過wait(&ret)來獲取exit的返回值