一、fork函數的使用
fork()函數通過系統調用創建一個與原來進程幾乎完全相同的進程,也就是兩個進程可以做完全相同的事,但如果初始參數或者傳入的變量不同,兩個進程也可以做不同的事。
一個進程調用fork()函數后,系統先給新的進程分配資源,例如存儲數據和代碼的空間。然后把原來的進程的所有值都復制到新的新進程中,只有少數值與原來的進程的值不同。相當於克隆了一個自己。
#include <stdio.h>
#include <unistd.h>
int main ()
{
int fpid;//the return of fork function
int count=0;
fpid=fork();
if (fpid < 0)
printf("error in fork!");
else if (fpid == 0) {
printf("i am the child process, my process id is %d\n",getpid());
count++;
}
else { //pid bigger than 0;
printf("i am the parent process, my process id is %d\n",getpid());
count++;
}
printf("result count: %d\n",count);
return 0;
}
返回結果為:
i am the child process, my process id is 7067 result count: 1 i am the parent process, my process id is 7066 result count: 1
fork雖然僅被調用了一次,但是卻可能有兩次返回,它的返回值有三種:
(1)在父進程中,fork返回值大於零的子進程id。
(2)在子進程中,fork返回值為零。
(3)fork進程創建失敗時返回負值。
fork出錯可能有兩種原因:
1)當前的進程數已經達到了系統規定的上限,這時errno的值被設置為EAGAIN。
2)系統內存不足,這時errno的值被設置為ENOMEM。
創建新進程成功后,系統中出現兩個基本完全相同的進程,這兩個進程執行沒有固定的先后順序,哪個進程先執行要看系統的進程調度策略。
二、循環中的fork函數
#include <stdio.h>
#include <unistd.h>
int main ()
{
int fpid;
int i;
for(i = 0; i < 3; i++)
{
fpid = fork();
if(fpid>0)
{
printf("loop %d: my process id is %d, my father process id is %d, fpid %d\n",
i, getpid(), getppid(), fpid);
}
else if(!fpid)
{
printf("loop %d: my process id is %d, my father process id is %d, fpid %d\n",
i, getpid(), getppid(), fpid);
}
}
return 0;
}
運行結果:
loop 0: my process id is 7281, my father process id is 7280, fpid 0 loop 1: my process id is 7282, my father process id is 7281, fpid 0 loop 1: my process id is 7281, my father process id is 7280, fpid 7282 loop 0: my process id is 7280, my father process id is 6968, fpid 7281 loop 1: my process id is 7285, my father process id is 7280, fpid 0 loop 1: my process id is 7280, my father process id is 6968, fpid 7285 loop 2: my process id is 7287, my father process id is 7280, fpid 0 loop 2: my process id is 7280, my father process id is 6968, fpid 7287 [root@localhost threadTest]# loop 2: my process id is 7284, my father process id is 7281, fpid 0 loop 2: my process id is 7281, my father process id is 1, fpid 7284 loop 2: my process id is 7286, my father process id is 7285, fpid 0 loop 2: my process id is 7285, my father process id is 1, fpid 7286 loop 2: my process id is 7283, my father process id is 7282, fpid 0 loop 2: my process id is 7282, my father process id is 1, fpid 7283
從結果可以看到當最上層的父進程死亡之后,未死亡的子進程會繼續運行到結束,但是此時他們的父進程id變為1。id為1的進程為init進程,init進程是系統中的一個特殊進程,通常程序文件為/sbin/init,在系統啟動時負責啟動各種系統服務,之后就負責清理子進程。只要有子進程終止,init就會調用wait函數清理它。
三、fork函數產生的死進程(僵屍進程)
我們所運行的程序,並不是都會結束的,有些程序(如服務器等)需要它一直執行下去。這些程序中如果調用到fork函數就有可能產生“僵屍進程”。
#include <stdio.h>
#include <unistd.h>
int main ()
{
int fpid;
int i;
for(i;; i++)
{
fpid = fork();
if(fpid>0)
{
printf("loop %d: my process id is %d, my father process id is %d, fpid %d\n",
i, getpid(), getppid(), fpid);
}
else if(!fpid)
{
printf("loop %d: my process id is %d, my father process id is %d, fpid %d\n",
i, getpid(), getppid(), fpid);
exit(0);
}
sleep(10);
}
return 0;
}
運行10秒后用ps -e命令可以看到結果如下:
[root@localhost root]# ps -e | grep test 22928 pts/5 00:00:00 test 22929 pts/5 00:00:00 test <defunct>
而且每隔10秒鍾,再運行一次ps -e命令,都可以看到多增加了一個test <defunct>進程。狀態欄為defunct的進程,就是所謂的“僵屍”進程。“僵屍進程”是一個已經死亡的進程,但是其在進程表中仍占了一個位置。因為進程表的容量有限,所以defunct進程不僅占用系統的內存資源,影響系統的性能,而且如果數量太多還會導致系統癱瘓。
當以fork()系統調用建立一個新的進程后,核心進程就會在進程表中給這個新進程分配一個進入點,然后將相關信息存儲在該進入點所對應的進程表內。這些信息中有一項是其父進程的識別碼。當這個進程走完了自己的生命周期后,它會執行exit()系統調用,此時原來進程表中的數據會被該進程的退出碼(exit code)、執行時所用的CPU時間等數據所取代,這些數據會一直保留到系統將它傳遞給它的父進程為止。由此可見,defunct進程的出現時間是在子進程終止后,但是父進程尚未讀取這些數據之前。
當父進程執行終止后,再用ps -e命令觀察時,我們會發現defunct進程也隨之消失。這是因為父進程終止后,init 進程會接管父進程留下的這些“孤兒進程”(orphan process),而這些“孤兒進程”執行完后,它在進程表中的進入點將被刪除。如果一個程序設計上有缺陷,就可能導致某個進程的父進程一直處於睡眠狀態或是陷入死循環,那么當該子進程執行結束后就變成了defunct進程,這個defunct 進程可能會一直留在系統中直到系統重新啟動。
僵屍進程無法調用kill清除,因為kill命令式用來終止進程的,而僵屍進程已經終止了。可以使用wait或者waitpid函數來清除僵屍進程。
四、用wait或waitpid避免fork函數產生僵屍進程
wait和waitpid的函數原型是:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
若調用成功則返回清理掉的子進程id,若調用出錯則返回-1。父進程調用wait或waitpid時可能會:
- 阻塞(如果它的所有子進程都還在運行)。
- 帶子進程的終止信息立即返回(如果一個子進程已終止,正等待父進程讀取其終止信息)。
- 出錯立即返回(如果它沒有任何子進程)。
這兩個函數的區別是:
- 如果父進程的所有子進程都還在運行,調用wait將使父進程阻塞,而調用waitpid時如果在options參數中指定WNOHANG可以使父進程不阻塞而立即返回0。
- wait等待第一個終止的子進程,而waitpid可以通過pid參數指定等待哪一個子進程。
可見,調用wait和waitpid不僅可以獲得子進程的終止信息,還可以使父進程阻塞等待子進程終止,起到進程間同步的作用。如果參數status不是空指針,則子進程的終止信息通過這個參數傳出,如果只是為了同步而不關心子進程的終止信息,可以將status參數指定為NULL。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main ()
{
int fpid;
int stat_val;
int i;
for(i;; i++)
{
fpid = fork();
if(fpid>0)
{
printf("loop %d: my process id is %d, my father process id is %d, fpid %d\n",
i, getpid(), getppid(), fpid);
}
else if(!fpid)
{
printf("loop %d: my process id is %d, my father process id is %d, fpid %d\n",
i, getpid(), getppid(), fpid);
exit(0);
}
waitpid(fpid, &stat_val,0);
if(WIFEXITED(stat_val))
printf("child exited with code %d\n", WEXITSTATUS(stat_val));
else if (WIFSIGNALED(stat_val))
printf("child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
sleep(10);
}
return 0;
}
運行結果:
loop 134514078: my process id is 31080, my father process id is 31079, fpid 0 loop 134514078: my process id is 31079, my father process id is 6968, fpid 31080 child exited with code 0 loop 134514079: my process id is 31089, my father process id is 31079, fpid 0 loop 134514079: my process id is 31079, my father process id is 6968, fpid 31089 child exited with code 0
這時在使用ps -e查看就不會產生僵屍進程了。但是會引起阻塞。以下是不會引起阻塞的代碼
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void sig_chld_handler(void) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main ()
{
int fpid;
int stat_val;
int i;
#ifdef __USE_GNU
signal(SIGCHLD, (sighandler_t )sig_chld_handler);
#endif
#ifdef __USE_BSD
signal(SIGCHLD, (sig_t )sig_chld_handler);
#endif
for(i;; i++)
{
fpid = fork();
if(fpid>0)
{
printf("loop %d: my process id is %d, my father process id is %d, fpid %d\n",
i, getpid(), getppid(), fpid);
}
else if(!fpid)
{
printf("loop %d: my process id is %d, my father process id is %d, fpid %d\n",
i, getpid(), getppid(), fpid);
exit(0);
}
}
return 0;
}
子進程的終止信息在一個int中包含了多個字段,用宏定義可以取出其中的每個字段:如果子進程是正常終止的,WIFEXITED取出的字段值非零,WEXITSTATUS取出的字段值就是子進程的退出狀態;如果子進程是收到信號而異常終止的,WIFSIGNALED取出的字段值非零,WTERMSIG取出的字段值就是信號的編號。