【Linux系统编程学习】Linux进程控制原语(fork、exec函数族、wait)
此為牛客Linux C++和黑馬Linux系統編程課程筆記。
1. fork函數
1.1 fork創建單個子進程
#include<unistd.h> pid_t fork(void);作用:創建一個子進程。
pid_t類型表示進程ID,但為了表示-1,它是有符號整型。(0不是有效進程ID,init最小,為1)返回值:失敗返回-1;成功返回:① 父進程返回子進程的ID(非負) ②子進程返回 0
注意返回值,不是fork函數能返回兩個值,而是fork后,fork函數變為兩個,父子需【各自】返回一個。創建失敗主要有以下兩個原因:
- 當前系統的進程數已經達到了系統規定的上限,這時 errno 的值被設置為 EAGAIN
- 系統內存不足,這時 errno 的值被設置為 ENOMEM
我們常常利用fork()的返回值來判斷當前在父進程還是在子進程中。
示例程序:
#include <unistd.h> #include <stdio.h>int main() {pid_t pid = fork();if(pid > 0) {// 父進程printf("This is parent process, pid is %d\n", getpid());} else if(pid == 0) {// 子進程printf("This is child process, pid is %d, my parent's pid is %d\n", getpid(), getppid());}return 0; }運行結果為:
1.2 循環創建多個子進程
如果現在想要使用fork()編寫一個能夠創建多個子進程的程序,該如何編寫?直觀的想法是直接循環:
#include <unistd.h> #include <stdio.h>int main() {int i;for(i = 0; i < 3; ++i) {pid_t pid = fork();}printf("im a process, my pid is %d\n", getpid());return 0; }執行發現,輸出了8個語句,說明一共有8個進程。
這是因為fork出的子進程也在執行當前程序,也就是說當前趟循環創建出的子進程在也會執行下一次循環的fork(),創建出子進程的子進程,最后一共創建了1+2+4=7個子進程,故一共有8個進程。
要想只創建當前父進程的3個子進程,需要如此編寫:
#include <unistd.h> #include <stdio.h>int main() {int i;for(i = 0; i < 3; ++i) {pid_t pid = fork();if(pid == 0) {break;}}printf("im a process, my pid is %d, my ppid is %d\n", getpid(), getppid());return 0; }再執行,發現輸出了4個語句:
其中前三個都是父進程22000的子進程。
1.3 fork父子進程的虛擬內存空間
父子進程之間在fork后。有哪些相同,那些相異之處呢?
剛fork之后:
父子相同處: 全局變量、.data、.text、棧、堆、環境變量、用戶ID、宿主目錄、進程工作目錄、信號處理方式…
父子不同處: 1.進程ID 2.fork返回值 3.父進程ID 4.進程運行時間 5.鬧鐘(定時器) 6.未決信號集
似乎,子進程復制了父進程0-3G用戶空間內容,以及父進程的PCB,但pid不同。真的每fork一個子進程都要將父進程的0-3G地址空間完全拷貝一份,然后在映射至物理內存嗎?
當然不是!父子進程間遵循讀時共享寫時復制的原則。這樣設計,無論子進程執行父進程的邏輯還是執行自己的邏輯都能節省內存開銷。
以全局變量為例,一旦父進程或子進程要對一全局變量做修改,其對應的子進程或父進程就把該全局變量復制一份到自己的虛擬內存空間中,映射至新的物理內存,看如下示例代碼:
#include <unistd.h> #include <stdio.h>int var = 1;int main() {int i;pid_t pid = fork();if(pid > 0) {var = 2;printf("parent var = %d\n", var);}else if(pid == 0) {var = 3;printf("child var = %d\n", var);}return 0; }執行結果為:
可見全局變量是讀時共享寫時復制。
【重點】:父子進程共享:1. 文件描述符(打開文件的結構體) 2. mmap建立的映射區 (進程間通信詳解)
特別的,fork之后父進程先執行還是子進程先執行不確定。取決于內核所使用的調度算法。
2. exec函數族
2.1 介紹
exec 函數族的作用是根據指定的文件名找到可執行文件,并用它來取代調用進程的內容,換句話說,就是在調用進程內部執行一個可執行文件。
fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec并不創建新進程,所以調用exec前后該進程的pid并未改變。
exec 函數族的函數執行成功后不會返回,因為調用進程的實體,包括代碼段,數據段和堆棧等都已經被新的內容取代,只留下進程 ID 等一些表面上的信息仍保持原樣,頗有些神似“三十六計”中的“金蟬脫殼”。看上去還是舊的軀殼,卻已經注入了新的靈魂。只有調用失敗了,它們才會返回 -1,從原程序的調用點接著往下執行,如圖:
左邊是某進程的虛擬地址空間及內容,該進程內部使用exec執行了a.out可執行文件,右邊紅色的是a.out文件虛擬地址空間中的用戶區,則調用結束以后該進程如圖:
替換原進程用戶區內容,內核區不變。
exec函數族如下:
int execl(const char *path, const char *arg, ...)/* (char *) NULL */); int execlp(const char *file, const char *arg, ...) /* (char *) NULL */); int execle(const char *path, const char *arg, ...)/*, (char *) NULL, char * const envp[] */); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]); int execve(const char *filename, char *const argv[], char *const envp[]);前六個函數是標準C庫函數,最后一個是linux系統函數。前六個函數是調用最后一個函數實現的。
各個函數名都已exec開頭,后面為以下字母組合,分別代表不同功能:
- l(list) 參數地址列表,以空指針結尾
- v(vector) 存有各參數地址的指針數組的地址
- p(path) 按 PATH 環境變量指定的目錄搜索可執行文件
- e(environment) 存有環境變量字符串地址的指針數組的地址
最常用函數為execl函數。
2.2 execl
int execl(const char *path, const char *arg, ...)參數:
-
path:需要指定的執行的文件的路徑或者名稱,推薦使用絕對路徑。
-
arg:是執行可執行文件所需要的參數列表,需要注意:
第一個參數一般沒有什么作用,為了方便,一般寫的是執行的程序的名稱,
從第二個參數開始往后,就是程序執行所需要的的參數列表。
參數最后需要以NULL結束(哨兵)
返回值:只有當調用失敗,才會有返回值,返回-1,并且設置errno;如果調用成功,沒有返回值。
示例程序:
主程序execl.c:
#include <unistd.h> #include <stdio.h>int var = 1;int main() {pid_t pid = fork();if(pid > 0) {printf("im parent, pid is %d\n", getpid());sleep(1);}else if(pid == 0) {execl("child", "child", NULL);printf("its execl.c program\n");}return 0; }調用的子程序child.c:
#include <unistd.h> #include <stdio.h>int main() {printf("im child, pid is %d, ppid is %d\n", getpid(), getppid());return 0; }執行主程序,結果如下:
印證了之前的說法,子進程調用execl執行child程序后,進程號不變。
而主程序中的printf("its execl.c program\n");沒有被執行,說明子進程調用execl后程序段已經被替換。
3. wait和waitpid函數
學習wait函數之前,我們最好先要了解什么是孤兒進程,什么是僵尸進程。
3.1 孤兒進程
看下面的示例程序:
fork出子進程后,讓子進程睡眠1秒后再執行printf語句,此時父進程已經運行結束,該子進程便成了孤兒進程,運行結果如下:
一秒后輸出:
可以看到子進程的父進程的pid為1,在linux中正是init進程對應的進程號,可見當出現孤兒進程時,init進程會”收養“該進程。
3.2 僵尸進程
如果父進程調用了wait( )或者waitpid( ),父進程將會釋放已經執行完的子進程的PCB資源;如果父進程沒有調用了wait( )或者waitpid( ),父進程結束后,init進程收養了子進程后,init進程也將負責釋放子進程的PCB資源;但是,如果父進程是一個循環,或者一直在執行,那父進程結束之前它的已經執行完的子進程就成為了僵尸進程。
看如下示例程序:
#include <unistd.h> #include <stdio.h>int main() {pid_t pid = fork();if(pid > 0) {while(1) {printf("im parent, pid is %d\n", getpid());sleep(1);}} else if(pid == 0) {printf("im child, pid is %d\n", getpid());}return 0; }父進程循環,子進程變成了僵尸進程,使用ps -aux查看進程狀態,可以看到子進程的狀態為Z+,意思時僵尸進程。
接下來繼續介紹wait和waitpid函數。
3.3 wait函數
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *wstatus);功能:等待當前進程的任意一個子進程結束,如果任意一個子進程結束了,此函數會回收該子進程的資源。
參數:int *wstatus 為進程退出時的狀態信息,傳入的是一個int指針類型的變量,傳出參數。
返回值:
-
成功:返回被回收的子進程的id
-
失敗:-1 (代表所有的子進程都是結束的,調用函數失敗)
調用wait函數的進程會被掛起(阻塞),直到它的一個子進程退出或者收到一個不能被忽略的信號時才被喚醒(相當于繼續往下執行)
如果沒有子進程了,函數立刻返回,返回-1;如果子進程都已經結束了,也會立即返回,返回-1.
示例程序如下:
#include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h>int main() {pid_t pid = fork();if(pid > 0) {int ret = wait(NULL);if(ret == -1) {printf("no child");} else {printf("child %d is dead", ret);}sleep(1);} else if(pid == 0) {while(1) {printf("im child, pid is %d\n", getpid());sleep(1);}}return 0; }程序運行后,子進程無限循環,父進程由于調用了wait函數,在等待子進程退出而處于阻塞態,現在我們使用kill -9 殺死子進程后,輸出如下:
可見子進程結束后,父進程從wait處開始繼續執行,并且返回了子進程的pid。
3.4 waitpid函數
與wait相近,只不過能夠指定回收的進程pid。
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *wstatus, int options);功能:回收指定進程號的子進程,可以設置是否阻塞。
參數:
pid:
- pid > 0 : 某個子進程的pid
- pid = 0 : 回收當前進程組的所有子進程
- pid = -1 : 回收任意子進程,相當于 wait()
- pid < -1 : 某個進程組的組id的絕對值,回收指定進程組中的子進程
options:設置阻塞或者非阻塞
- 0 : 阻塞
- WNOHANG : 非阻塞
返回值:
- > 0 返回子進程的id
- 0 : options=WNOHANG, 且子進程正在運行。
- -1 :錯誤,或者沒有子進程了
注意:一次wait或waitpid調用只能清理一個子進程,清理多個子進程應使用循環。
總結
以上是生活随笔為你收集整理的【Linux系统编程学习】Linux进程控制原语(fork、exec函数族、wait)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 卵巢早衰想做卵巢移植术能生肓
- 下一篇: 输卵管积液危害大吗