Linux 進(jìn)程編程核心:fork、wait 和 exec 深度解析
在 Linux 系統(tǒng)中,進(jìn)程是程序的一次執(zhí)行過(guò)程,是操作系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位。每個(gè)進(jìn)程都有自己獨(dú)立的地址空間、文件描述符、寄存器等資源,操作系統(tǒng)通過(guò)進(jìn)程控制塊(PCB,在 Linux 內(nèi)核中用task_struct結(jié)構(gòu)體表示)來(lái)管理進(jìn)程的相關(guān)信息。
進(jìn)程在操作系統(tǒng)中扮演著至關(guān)重要的角色,它是操作系統(tǒng)實(shí)現(xiàn)多任務(wù)處理的基礎(chǔ)。通過(guò)進(jìn)程,操作系統(tǒng)可以同時(shí)運(yùn)行多個(gè)程序,提高系統(tǒng)的利用率和響應(yīng)速度。例如,當(dāng)我們?cè)?Linux 系統(tǒng)中打開多個(gè)終端窗口,每個(gè)終端窗口都可以看作是一個(gè)獨(dú)立的進(jìn)程,它們可以同時(shí)執(zhí)行不同的命令,互不干擾。
在進(jìn)程編程中,fork、wait和exec是三個(gè)非常關(guān)鍵的函數(shù),它們分別用于創(chuàng)建新進(jìn)程、等待子進(jìn)程結(jié)束和執(zhí)行新的程序。接下來(lái),我們將深入探討這三個(gè)函數(shù)的用法和原理。
一、進(jìn)程創(chuàng)建:fork函數(shù)解析
1. fork 函數(shù)基礎(chǔ)
fork函數(shù)是 Linux 系統(tǒng)中用于創(chuàng)建新進(jìn)程的系統(tǒng)調(diào)用,其定義在<unistd.h>頭文件中 ,原型為pid_t fork(void);。這里的pid_t是一種數(shù)據(jù)類型,用來(lái)表示進(jìn)程 ID。fork函數(shù)的功能非常強(qiáng)大,它會(huì)創(chuàng)建一個(gè)與調(diào)用進(jìn)程(即父進(jìn)程)幾乎完全相同的新進(jìn)程,這個(gè)新進(jìn)程被稱為子進(jìn)程。
子進(jìn)程會(huì)復(fù)制父進(jìn)程的代碼段、數(shù)據(jù)段、堆、棧等資源,擁有自己獨(dú)立的進(jìn)程 ID(PID),但與父進(jìn)程共享一些資源,如打開的文件描述符。簡(jiǎn)單來(lái)說(shuō),就像是父進(jìn)程克隆了一個(gè)自己,這個(gè)克隆體(子進(jìn)程)有著與父進(jìn)程相似的 “外貌”(資源),但又有自己獨(dú)特的 “身份標(biāo)識(shí)”(PID) 。
2. fork 的返回值與執(zhí)行邏輯
fork函數(shù)的一個(gè)獨(dú)特之處在于它會(huì)返回兩次,一次是在父進(jìn)程中,一次是在子進(jìn)程中。在父進(jìn)程中,fork返回子進(jìn)程的 PID;在子進(jìn)程中,fork返回 0。如果fork函數(shù)執(zhí)行失敗,它會(huì)返回 - 1,并設(shè)置errno來(lái)指示錯(cuò)誤原因。這種不同的返回值為區(qū)分父子進(jìn)程提供了依據(jù),就像是給父子進(jìn)程分別發(fā)放了不同的 “通行證”,讓它們可以在后續(xù)的代碼中走不同的路徑 。
下面通過(guò)一段簡(jiǎn)單的 C 代碼來(lái)展示fork函數(shù)的返回值和執(zhí)行邏輯:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid;
// 調(diào)用fork函數(shù)創(chuàng)建子進(jìn)程
pid = fork();
// 判斷fork的返回值
if (pid < 0) {
// fork失敗
perror("fork error");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子進(jìn)程
printf("I am the child process, my pid is %d, my parent's pid is %d\n", getpid(), getppid());
} else {
// 父進(jìn)程
printf("I am the parent process, my pid is %d, and my child's pid is %d\n", getpid(), pid);
}
return 0;
}
在這段代碼中,首先調(diào)用fork函數(shù)創(chuàng)建子進(jìn)程。然后根據(jù)fork的返回值判斷當(dāng)前是父進(jìn)程還是子進(jìn)程。如果返回值小于 0,說(shuō)明fork失敗,輸出錯(cuò)誤信息并退出程序;如果返回值為 0,說(shuō)明是子進(jìn)程,輸出子進(jìn)程的 PID 和父進(jìn)程的 PID;如果返回值大于 0,說(shuō)明是父進(jìn)程,輸出父進(jìn)程的 PID 和子進(jìn)程的 PID。
運(yùn)行這段代碼,你會(huì)看到類似如下的輸出:
I am the parent process, my pid is 12345, and my child's pid is 12346
I am the child process, my pid is 12346, my parent's pid is 12345
從輸出結(jié)果可以清晰地看到父子進(jìn)程的 PID 以及它們的執(zhí)行路徑 。
3. 寫時(shí)復(fù)制機(jī)制
在早期的操作系統(tǒng)中,當(dāng)使用fork創(chuàng)建子進(jìn)程時(shí),會(huì)直接將父進(jìn)程的所有內(nèi)存空間完整地復(fù)制給子進(jìn)程,這在內(nèi)存使用和性能上都存在很大的問(wèn)題,尤其是對(duì)于大型程序來(lái)說(shuō),復(fù)制大量?jī)?nèi)存數(shù)據(jù)會(huì)消耗大量時(shí)間和內(nèi)存資源。為了解決這個(gè)問(wèn)題,Linux 引入了寫時(shí)復(fù)制(Copy - On - Write,COW)技術(shù) 。
寫時(shí)復(fù)制的原理是,在fork創(chuàng)建子進(jìn)程時(shí),內(nèi)核并不立即復(fù)制父進(jìn)程的整個(gè)地址空間,而是讓父進(jìn)程和子進(jìn)程共享同一個(gè)物理內(nèi)存拷貝,同時(shí)將這些共享內(nèi)存頁(yè)標(biāo)記為只讀。只有當(dāng)父子進(jìn)程中的某一個(gè)試圖對(duì)共享內(nèi)存頁(yè)進(jìn)行寫操作時(shí),才會(huì)觸發(fā)缺頁(yè)異常,此時(shí)內(nèi)核會(huì)為需要寫入的進(jìn)程創(chuàng)建該內(nèi)存頁(yè)的一個(gè)新副本,然后將新副本的權(quán)限設(shè)置為可寫,進(jìn)程再對(duì)新副本進(jìn)行寫操作 。
例如,假設(shè)父進(jìn)程有一個(gè)數(shù)據(jù)段,其中包含一個(gè)變量x,值為 10。在fork創(chuàng)建子進(jìn)程后,父子進(jìn)程共享這個(gè)數(shù)據(jù)段的物理內(nèi)存頁(yè)。當(dāng)父進(jìn)程或者子進(jìn)程想要修改x的值時(shí),就會(huì)觸發(fā)寫時(shí)復(fù)制機(jī)制。內(nèi)核會(huì)為執(zhí)行寫操作的進(jìn)程創(chuàng)建一個(gè)新的數(shù)據(jù)段內(nèi)存頁(yè),將原內(nèi)存頁(yè)的數(shù)據(jù)復(fù)制到新頁(yè),然后在新頁(yè)上進(jìn)行寫操作。這樣,另一個(gè)進(jìn)程的數(shù)據(jù)段仍然保持不變,實(shí)現(xiàn)了數(shù)據(jù)的獨(dú)立修改 。
寫時(shí)復(fù)制機(jī)制帶來(lái)了很多優(yōu)勢(shì):一方面,它顯著提高了fork操作的效率,因?yàn)椴恍枰趧?chuàng)建子進(jìn)程時(shí)立即復(fù)制大量?jī)?nèi)存數(shù)據(jù),減少了創(chuàng)建子進(jìn)程的時(shí)間開銷;另一方面,它有效地節(jié)省了內(nèi)存資源,尤其是在父子進(jìn)程共享大量數(shù)據(jù)且大部分?jǐn)?shù)據(jù)不需要修改的情況下,避免了不必要的內(nèi)存復(fù)制 。
二、進(jìn)程等待:wait函數(shù)解析
1. wait 函數(shù)作用
在 Linux 進(jìn)程編程中,wait函數(shù)是一個(gè)非常重要的函數(shù),它用于父進(jìn)程等待子進(jìn)程結(jié)束 。當(dāng)父進(jìn)程調(diào)用wait函數(shù)時(shí),會(huì)發(fā)生以下事情:首先,父進(jìn)程會(huì)被阻塞,暫停執(zhí)行,直到它的一個(gè)子進(jìn)程結(jié)束;然后,wait函數(shù)會(huì)回收子進(jìn)程的資源,包括釋放子進(jìn)程占用的內(nèi)存空間、關(guān)閉子進(jìn)程打開的文件描述符等;最后,wait函數(shù)還會(huì)獲取子進(jìn)程的退出狀態(tài),讓父進(jìn)程了解子進(jìn)程是如何結(jié)束的,比如是正常退出還是異常終止 。
wait函數(shù)對(duì)于資源回收和避免僵尸進(jìn)程的產(chǎn)生具有至關(guān)重要的意義。在 Linux 系統(tǒng)中,每個(gè)進(jìn)程都占用一定的系統(tǒng)資源,如果父進(jìn)程創(chuàng)建了子進(jìn)程后,不等待子進(jìn)程結(jié)束并回收其資源,子進(jìn)程就會(huì)變成僵尸進(jìn)程 。僵尸進(jìn)程雖然已經(jīng)結(jié)束運(yùn)行,但它的進(jìn)程控制塊(PCB)仍然保留在系統(tǒng)中,占用系統(tǒng)資源,長(zhǎng)期積累會(huì)導(dǎo)致系統(tǒng)資源浪費(fèi)和性能下降 。通過(guò)wait函數(shù),父進(jìn)程可以及時(shí)回收子進(jìn)程的資源,避免僵尸進(jìn)程的出現(xiàn),確保系統(tǒng)的穩(wěn)定運(yùn)行 。
2. wait 函數(shù)原型與參數(shù)
wait函數(shù)的原型定義在<sys/types.h>和<sys/wait.h>頭文件中,具體原型為pid_t wait(int *status);。其中,pid_t是一種數(shù)據(jù)類型,用于表示進(jìn)程 ID;status是一個(gè)指向整數(shù)的指針,用于存儲(chǔ)子進(jìn)程的退出狀態(tài)信息 。
如果status為NULL,表示父進(jìn)程不關(guān)心子進(jìn)程的退出狀態(tài),只希望等待子進(jìn)程結(jié)束并回收其資源 。如果status不為NULL,wait函數(shù)會(huì)將子進(jìn)程的退出狀態(tài)信息存儲(chǔ)在status指向的整數(shù)中 。通過(guò)一些宏定義,可以從這個(gè)整數(shù)中解析出子進(jìn)程的具體退出情況 。常用的宏有:
- WIFEXITED(status):用于判斷子進(jìn)程是否正常退出,如果正常退出返回非零值 。
- WEXITSTATUS(status):當(dāng)WIFEXITED(status)為真時(shí),通過(guò)這個(gè)宏可以獲取子進(jìn)程正常退出時(shí)的返回值 。
- WIFSIGNALED(status):判斷子進(jìn)程是否是因?yàn)槭盏叫盘?hào)而異常終止,如果是返回非零值 。
- WTERMSIG(status):當(dāng)WIFSIGNALED(status)為真時(shí),通過(guò)這個(gè)宏可以獲取導(dǎo)致子進(jìn)程異常終止的信號(hào)編號(hào) 。
3. wait 函數(shù)應(yīng)用示例
下面通過(guò)一段代碼示例來(lái)展示wait函數(shù)的具體應(yīng)用 :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid;
int status;
// 創(chuàng)建子進(jìn)程
pid = fork();
if (pid < 0) {
perror("fork error");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子進(jìn)程
printf("I am the child process, my pid is %d\n", getpid());
sleep(2); // 模擬子進(jìn)程執(zhí)行一些任務(wù)
exit(3); // 子進(jìn)程正常退出,返回值為3
} else {
// 父進(jìn)程
printf("I am the parent process, my pid is %d, and my child's pid is %d\n", getpid(), pid);
// 等待子進(jìn)程結(jié)束
wait(&status);
if (WIFEXITED(status)) {
printf("The child process exited normally, exit status is %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("The child process was terminated by a signal, signal number is %d\n", WTERMSIG(status));
}
}
return 0;
}
在這段代碼中,首先使用fork函數(shù)創(chuàng)建子進(jìn)程 。子進(jìn)程打印自己的 PID,然后睡眠 2 秒,最后以返回值 3 正常退出 。父進(jìn)程打印自己和子進(jìn)程的 PID,然后調(diào)用wait函數(shù)等待子進(jìn)程結(jié)束 。當(dāng)子進(jìn)程結(jié)束后,wait函數(shù)返回,通過(guò)WIFEXITED和WIFSIGNALED宏判斷子進(jìn)程的退出狀態(tài),并打印相應(yīng)信息 。
運(yùn)行這段代碼,你會(huì)看到類似如下的輸出:
I am the parent process, my pid is 12345, and my child's pid is 12346
I am the child process, my pid is 12346
The child process exited normally, exit status is 3
從輸出結(jié)果可以清晰地看到父子進(jìn)程的執(zhí)行過(guò)程以及子進(jìn)程的退出狀態(tài) 。
4. waitpid 函數(shù)拓展
waitpid函數(shù)是wait函數(shù)的擴(kuò)展,它提供了更靈活的等待方式 。waitpid函數(shù)的原型為pid_t waitpid(pid_t pid, int *status, int options); 。與wait函數(shù)相比,waitpid函數(shù)有以下幾個(gè)特點(diǎn):
- 可以指定等待的子進(jìn)程:pid參數(shù)用于指定要等待的子進(jìn)程的 PID 。當(dāng)pid > 0時(shí),等待進(jìn)程 ID 等于pid的子進(jìn)程;當(dāng)pid = -1時(shí),等待任意子進(jìn)程,此時(shí)waitpid與wait功能相同;當(dāng)pid = 0時(shí),等待和當(dāng)前調(diào)用waitpid函數(shù)的進(jìn)程同一個(gè)進(jìn)程組的所有子進(jìn)程;當(dāng)pid < -1時(shí),等待指定進(jìn)程組內(nèi)的任意子進(jìn)程,其中pid的絕對(duì)值表示進(jìn)程組的 ID 。
- 可以選擇是否阻塞等待:options參數(shù)用于控制waitpid的行為,常用的選項(xiàng)有WNOHANG(非阻塞模式) 。當(dāng)設(shè)置WNOHANG選項(xiàng)時(shí),如果沒(méi)有子進(jìn)程結(jié)束,waitpid函數(shù)會(huì)立即返回 0,而不是阻塞等待;如果有子進(jìn)程結(jié)束,則返回該子進(jìn)程的 PID 。
- 可以處理更多子進(jìn)程狀態(tài):除了可以獲取子進(jìn)程的正常退出和異常終止?fàn)顟B(tài)外,waitpid函數(shù)還可以通過(guò)WUNTRACED選項(xiàng)報(bào)告被跟蹤的子進(jìn)程(即使它們尚未停止),通過(guò)WCONTINUED選項(xiàng)報(bào)告被繼續(xù)執(zhí)行的子進(jìn)程(即被SIGCONT信號(hào)繼續(xù)執(zhí)行) 。
waitpid函數(shù)在一些復(fù)雜的場(chǎng)景中非常有用 。例如,當(dāng)父進(jìn)程需要同時(shí)管理多個(gè)子進(jìn)程,并且希望在不阻塞的情況下獲取子進(jìn)程的狀態(tài)時(shí),可以使用waitpid函數(shù)的非阻塞模式 。通過(guò)循環(huán)調(diào)用waitpid函數(shù),并設(shè)置WNOHANG選項(xiàng),父進(jìn)程可以在等待子進(jìn)程結(jié)束的同時(shí)繼續(xù)執(zhí)行其他任務(wù) 。
下面是一個(gè)使用waitpid函數(shù)非阻塞等待子進(jìn)程的示例代碼:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid;
int status;
// 創(chuàng)建子進(jìn)程
pid = fork();
if (pid < 0) {
perror("fork error");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子進(jìn)程
printf("I am the child process, my pid is %d\n", getpid());
sleep(5); // 模擬子進(jìn)程執(zhí)行一些任務(wù)
exit(3); // 子進(jìn)程正常退出,返回值為3
} else {
// 父進(jìn)程
printf("I am the parent process, my pid is %d, and my child's pid is %d\n", getpid(), pid);
// 非阻塞等待子進(jìn)程結(jié)束
while (1) {
pid_t ret = waitpid(pid, &status, WNOHANG);
if (ret == 0) {
// 沒(méi)有子進(jìn)程結(jié)束,繼續(xù)執(zhí)行其他任務(wù)
printf("The child process is still running, I can do other things\n");
sleep(1);
} else if (ret == pid) {
// 子進(jìn)程結(jié)束
if (WIFEXITED(status)) {
printf("The child process exited normally, exit status is %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("The child process was terminated by a signal, signal number is %d\n", WTERMSIG(status));
}
break;
} else {
// 錯(cuò)誤情況
perror("waitpid error");
break;
}
}
}
return 0;
}
在這個(gè)示例中,父進(jìn)程使用waitpid函數(shù)并設(shè)置WNOHANG選項(xiàng)非阻塞地等待子進(jìn)程結(jié)束 。在等待過(guò)程中,父進(jìn)程可以繼續(xù)執(zhí)行其他任務(wù),每隔 1 秒打印一次提示信息 。當(dāng)子進(jìn)程結(jié)束后,waitpid函數(shù)返回子進(jìn)程的 PID,父進(jìn)程獲取子進(jìn)程的退出狀態(tài)并打印相應(yīng)信息 。運(yùn)行這段代碼,你會(huì)看到父進(jìn)程在等待子進(jìn)程的同時(shí)還能執(zhí)行其他任務(wù),充分展示了waitpid函數(shù)的靈活性 。
三、程序替換:exec函數(shù)解析
1. exec 函數(shù)族概述
在 Linux 進(jìn)程編程中,當(dāng)我們需要讓一個(gè)進(jìn)程去執(zhí)行另一個(gè)不同的程序時(shí),就會(huì)用到exec函數(shù)族 。exec函數(shù)族的功能是用一個(gè)新的程序替換當(dāng)前進(jìn)程的正文段、數(shù)據(jù)段、堆段和棧段,使得當(dāng)前進(jìn)程從新程序的入口點(diǎn)開始執(zhí)行 。簡(jiǎn)單來(lái)說(shuō),就像是把進(jìn)程原本運(yùn)行的程序 “替換” 成了另一個(gè)程序,就如同給一個(gè)機(jī)器人換上了全新的 “大腦”(程序),讓它執(zhí)行新的任務(wù) 。
需要注意的是,exec函數(shù)族并不會(huì)創(chuàng)建新的進(jìn)程,進(jìn)程的 PID 在執(zhí)行exec前后保持不變 。這意味著,雖然進(jìn)程執(zhí)行的程序發(fā)生了變化,但它在系統(tǒng)中的 “身份標(biāo)識(shí)”(PID)并沒(méi)有改變 。例如,當(dāng)我們?cè)诮K端中輸入ls命令時(shí),shell 進(jìn)程會(huì)調(diào)用fork創(chuàng)建一個(gè)子進(jìn)程,然后子進(jìn)程調(diào)用exec函數(shù)族中的某個(gè)函數(shù),將自身替換為ls程序的執(zhí)行,此時(shí)子進(jìn)程的 PID 并沒(méi)有改變,只是它開始執(zhí)行l(wèi)s程序的代碼 。
2. exec 函數(shù)原型與參數(shù)
exec函數(shù)族包含多個(gè)函數(shù),它們的原型和功能相似,但在參數(shù)傳遞和查找可執(zhí)行文件的方式上有所不同 。常用的exec函數(shù)原型如下:
#include <unistd.h>
// 使用參數(shù)列表傳遞參數(shù),在指定路徑查找可執(zhí)行文件
int execl(const char *path, const char *arg, ...);
// 使用參數(shù)列表傳遞參數(shù),在PATH環(huán)境變量指定路徑查找可執(zhí)行文件
int execlp(const char *file, const char *arg, ...);
// 使用參數(shù)列表傳遞參數(shù),在指定路徑查找可執(zhí)行文件,并可指定新的環(huán)境變量
int execle(const char *path, const char *arg, ..., char *const envp[]);
// 使用參數(shù)數(shù)組傳遞參數(shù),在指定路徑查找可執(zhí)行文件
int execv(const char *path, char *const argv[]);
// 使用參數(shù)數(shù)組傳遞參數(shù),在PATH環(huán)境變量指定路徑查找可執(zhí)行文件
int execvp(const char *file, char *const argv[]);
// 使用參數(shù)數(shù)組傳遞參數(shù),在PATH環(huán)境變量指定路徑查找可執(zhí)行文件,并可指定新的環(huán)境變量
int execvpe(const char *file, char *const argv[], char *const envp[]);
這些函數(shù)的參數(shù)說(shuō)明如下:
- path:指定要執(zhí)行的可執(zhí)行文件的完整路徑,例如/bin/ls 。
- file:如果參數(shù)中包含/,則視為路徑并在指定路徑下查找可執(zhí)行文件;否則將在PATH環(huán)境變量指定的路徑中查找可執(zhí)行文件,例如ls 。
- arg:指定傳遞給可執(zhí)行文件的一系列參數(shù),以可變參數(shù)列表的形式傳遞,一般第一個(gè)參數(shù)為可執(zhí)行文件的名稱,且最后一個(gè)參數(shù)必須是NULL,用于表示參數(shù)列表的結(jié)束 。例如,execl("/bin/ls", "ls", "-l", NULL),其中"ls"是可執(zhí)行文件的名稱,"-l"是傳遞給ls命令的參數(shù),NULL表示參數(shù)列表結(jié)束 。
- argv:指定傳遞給可執(zhí)行文件的一系列參數(shù),以參數(shù)數(shù)組的形式傳遞,數(shù)組的最后一個(gè)元素必須是NULL,用于表示參數(shù)數(shù)組的結(jié)束 。例如,char *argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv); 。
- envp:指定新進(jìn)程的環(huán)境變量,是一個(gè)指向字符指針數(shù)組的指針,數(shù)組中的每個(gè)元素都是一個(gè)環(huán)境變量字符串,格式為"變量名=值",最后一個(gè)元素必須是NULL,用于表示環(huán)境變量數(shù)組的結(jié)束 。如果不使用該參數(shù),新進(jìn)程將繼承調(diào)用進(jìn)程的環(huán)境變量 。例如,char *envp[] = {"HELLO=world", "USER=root", NULL}; execle("/bin/echo", "echo", "$HELLO", (char *)NULL, envp); 。
3. exec 函數(shù)應(yīng)用示例
下面通過(guò)一個(gè)具體的代碼示例來(lái)展示exec函數(shù)的使用 :
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid;
// 創(chuàng)建子進(jìn)程
pid = fork();
if (pid < 0) {
perror("fork error");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子進(jìn)程
// 使用execlp函數(shù)執(zhí)行l(wèi)s命令,列出當(dāng)前目錄下的文件
// 第一個(gè)參數(shù)"ls"表示在PATH環(huán)境變量中查找ls程序
// 第二個(gè)參數(shù)"ls"是傳遞給ls程序的參數(shù),一般第一個(gè)參數(shù)是程序名本身
// 第三個(gè)參數(shù)"-l"是ls命令的參數(shù),用于以長(zhǎng)格式列出文件
// 最后一個(gè)參數(shù)NULL表示參數(shù)列表結(jié)束
if (execlp("ls", "ls", "-l", NULL) == -1) {
perror("execlp error");
exit(EXIT_FAILURE);
}
} else {
// 父進(jìn)程
wait(NULL); // 等待子進(jìn)程結(jié)束
printf("Child process has finished.\n");
}
return 0;
}
在這段代碼中,首先使用fork函數(shù)創(chuàng)建一個(gè)子進(jìn)程 。在子進(jìn)程中,調(diào)用execlp函數(shù)執(zhí)行l(wèi)s -l命令,用于列出當(dāng)前目錄下的文件 。execlp函數(shù)會(huì)在PATH環(huán)境變量指定的路徑中查找ls程序,并將"ls"、"-l"作為參數(shù)傳遞給ls程序 。如果execlp函數(shù)執(zhí)行失敗,會(huì)打印錯(cuò)誤信息并退出子進(jìn)程 。父進(jìn)程調(diào)用wait函數(shù)等待子進(jìn)程結(jié)束,然后打印提示信息 。運(yùn)行這段代碼,你會(huì)看到子進(jìn)程執(zhí)行l(wèi)s -l命令的輸出結(jié)果,展示了當(dāng)前目錄下文件的詳細(xì)信息 。
四、fork、wait 和 exec 三者之間的協(xié)同
1. 常見應(yīng)用場(chǎng)景
在實(shí)際的 Linux 編程中,fork、wait和exec這三個(gè)函數(shù)通常會(huì)協(xié)同工作,共同完成各種復(fù)雜的任務(wù) 。在 Shell 腳本的實(shí)現(xiàn)中,當(dāng)用戶在終端輸入一條命令,比如ls -l,Shell 進(jìn)程會(huì)首先調(diào)用fork創(chuàng)建一個(gè)子進(jìn)程 。這個(gè)子進(jìn)程繼承了 Shell 進(jìn)程的大部分資源,包括打開的文件描述符等 。然后子進(jìn)程調(diào)用exec函數(shù)族中的某個(gè)函數(shù),比如execlp,將自身替換為ls程序的執(zhí)行 。
此時(shí),子進(jìn)程開始執(zhí)行l(wèi)s程序的代碼,根據(jù)傳入的參數(shù)-l以長(zhǎng)格式列出當(dāng)前目錄下的文件 。而父進(jìn)程(即 Shell 進(jìn)程)則調(diào)用wait函數(shù)等待子進(jìn)程結(jié)束 。當(dāng)子進(jìn)程執(zhí)行完ls命令后,父進(jìn)程從wait函數(shù)返回,繼續(xù)等待用戶輸入下一條命令 。通過(guò)這樣的協(xié)同工作,Shell 能夠?qū)崿F(xiàn)對(duì)用戶輸入命令的解析和執(zhí)行 。
在服務(wù)器程序中,比如一個(gè)簡(jiǎn)單的 Web 服務(wù)器,fork、wait和exec的協(xié)同也起著關(guān)鍵作用 。當(dāng)服務(wù)器接收到一個(gè)客戶端的連接請(qǐng)求時(shí),主進(jìn)程會(huì)調(diào)用fork創(chuàng)建一個(gè)子進(jìn)程來(lái)處理這個(gè)連接 。子進(jìn)程調(diào)用exec函數(shù)族執(zhí)行處理客戶端請(qǐng)求的程序,比如一個(gè) CGI 腳本或者一個(gè)專門的處理程序 。
在這個(gè)過(guò)程中,主進(jìn)程可以繼續(xù)監(jiān)聽其他客戶端的連接請(qǐng)求,而子進(jìn)程負(fù)責(zé)處理當(dāng)前客戶端的具體請(qǐng)求 。當(dāng)子進(jìn)程處理完請(qǐng)求后,主進(jìn)程通過(guò)wait函數(shù)回收子進(jìn)程的資源,確保系統(tǒng)資源的有效利用 。通過(guò)這種方式,Web 服務(wù)器能夠同時(shí)處理多個(gè)客戶端的請(qǐng)求,提高了服務(wù)器的并發(fā)處理能力 。
2. 代碼實(shí)戰(zhàn)
下面給出一個(gè)完整的代碼示例,展示fork、wait和exec的協(xié)同工作流程 :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid;
int status;
// 創(chuàng)建子進(jìn)程
pid = fork();
if (pid < 0) {
perror("fork error");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子進(jìn)程
char *argv[] = {"ls", "-l", NULL};
// 使用execvp函數(shù)執(zhí)行l(wèi)s -l命令
if (execvp("ls", argv) == -1) {
perror("execvp error");
exit(EXIT_FAILURE);
}
} else {
// 父進(jìn)程
printf("I am the parent process, my pid is %d, and my child's pid is %d\n", getpid(), pid);
// 等待子進(jìn)程結(jié)束
wait(&status);
if (WIFEXITED(status)) {
printf("The child process exited normally, exit status is %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("The child process was terminated by a signal, signal number is %d\n", WTERMSIG(status));
}
}
return 0;
}
代碼執(zhí)行過(guò)程如下:
- 首先,主進(jìn)程調(diào)用fork函數(shù)創(chuàng)建子進(jìn)程 。
- 在子進(jìn)程中,fork返回 0,然后子進(jìn)程創(chuàng)建一個(gè)包含ls和-l的參數(shù)數(shù)組argv 。接著調(diào)用execvp函數(shù),execvp會(huì)在PATH環(huán)境變量指定的路徑中查找ls程序,并使用argv作為參數(shù)執(zhí)行l(wèi)s -l命令 。如果execvp執(zhí)行成功,子進(jìn)程的代碼段、數(shù)據(jù)段、堆段和棧段會(huì)被ls程序替換,開始執(zhí)行l(wèi)s程序的代碼,輸出當(dāng)前目錄下文件的詳細(xì)信息 。如果execvp執(zhí)行失敗,會(huì)打印錯(cuò)誤信息并退出子進(jìn)程 。
- 在父進(jìn)程中,fork返回子進(jìn)程的 PID,父進(jìn)程打印自己和子進(jìn)程的 PID 。然后調(diào)用wait函數(shù)等待子進(jìn)程結(jié)束 。當(dāng)子進(jìn)程結(jié)束后,wait函數(shù)返回,父進(jìn)程通過(guò)WIFEXITED和WIFSIGNALED宏判斷子進(jìn)程的退出狀態(tài),并打印相應(yīng)信息 。
通過(guò)這個(gè)代碼示例,我們可以清晰地看到fork、wait和exec是如何協(xié)同工作的 。fork用于創(chuàng)建子進(jìn)程,為執(zhí)行新程序提供載體;exec用于將子進(jìn)程替換為新的程序執(zhí)行;wait用于父進(jìn)程等待子進(jìn)程結(jié)束并回收其資源,確保系統(tǒng)資源的有效管理 。