1、概念
1、孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那么那些子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)所收養,從而保證每個進程都會有一個父進程。而Init進程會自動wait其子進程,因此被Init接管的所有進程都不會變成僵屍進程。
補充:孤兒進程是沒有父進程的進程,孤兒進程這個重任就落到了init進程身上。每當出現一個孤兒進程的時候,內核就把孤兒進程的父進程設置為init,而init進程會循環地wait()它的已經退出的子進程。這樣,當一個孤兒進程結束了其生命周期的時候,init進程就會處理它的一切善后工作。因此孤兒進程並不會有什么危害。
2、僵屍進程:一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那么子進程的進程描述符仍然保存在系統中,這種進程稱之為僵屍進程。當用ps命令觀察進程的執行狀態時,看到這些進程的狀態欄為defunct。僵屍進程是一個早已死亡的進程,但在進程表(processs table)中仍占了一個位置(slot)。
補充(內核):一個進程終止后,內核會釋放終止進程(調用了exit系統調用)所使用的所有存儲區,關閉所有打開的文件等。但內核為每一個終止子進程保存了一定量的信息,設置僵死狀態來維護子進程的信息,以便父進程在以后某個時候獲取,這些信息至少包括進程ID,進程的終止狀態,以及該進程使用的CPU時間,所以當終止子進程的父進程調用wait或waitpid時就可以得到這些信息。任何一個子進程(init除外)在exit后並非馬上就消失,而是留下一個稱外僵屍進程的數據結構,等待父進程處理。這是每個子進程都必需經歷的階段。另外子進程退出的時候會向其父進程發送一個SIGCHLD信號。
嚴格來說,僵屍進程並不是問題的根源,罪魁禍首是產生出大量僵屍進程的那個父進程。因此,把產生大量僵屍進程的那個父進程kill掉(通過kill發送SIGTERM或者SIGKILL信號)之后,它產生的僵死進程就變成了孤兒進程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們占用的系統進程表中的資源。
2、如何避免僵屍進程?
(1)通過signal(SIGCHLD, SIG_IGN)通知內核對子進程的結束不關心,由內核回收。如果不想讓父進程掛起,可以在父進程中加入一條語句:signal(SIGCHLD,SIG_IGN);表示父進程忽略SIGCHLD信號,該信號是子進程退出的時候向父進程發送的。
(2)父進程調用wait/waitpid等函數等待子進程結束,如果尚無子進程退出wait會導致父進程阻塞。waitpid可以通過傳遞WNOHANG使父進程不阻塞立即返回。
(3)如果父進程很忙可以用signal注冊信號處理函數,在信號處理函數調用wait/waitpid等待子進程退出。
(4)通過兩次調用fork。父進程首先調用fork創建一個子進程然后waitpid等待子進程退出,子進程再fork一個孫進程后退出。這樣子進程退出后會被父進程等待回收,而對於孫子進程其父進程已經退出所以孫進程成為一個孤兒進程,孤兒進程由init進程接管,孫進程結束后,init會等待回收。
第一種方法忽略SIGCHLD信號,這常用於並發服務器的一個技巧,因為並發服務器常常fork很多子進程,子進程終結之后需要服務器進程去wait清理資源。如果將此信號的處理方式設為忽略,可讓內核把僵屍子進程轉交給init進程去處理,省去了大量僵屍進程占用系統資源。
3、wait()、waitpid()
wait()
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status)
(1)子進程結束時,系統向其父進程發送SIGCHILD信號;
(2)父進程調用wait函數后阻塞;
(3)父進程被SIGCHILD信號喚醒,然后去回收僵屍子進程;
(4)父子進程之間是異步的,SIGCHILD信號機制就是為了解決父子進程之間的異步通信問題,讓父進程可以及時的去回收僵屍子進程。
(5)若父進程沒有任何子進程則wait返回錯誤。
錯誤代碼:(更多errno含義見cat /usr/include/asm-generic/errno-base.h和/usr/include/asm-generic/errno.h)
(1)ECHILD:沒有等待的子進程;
(2)EINTR:未抓住信號,或該信號未設置,或未找到該信號。
#include <errno.h> int waitreturn; waitreturn = wait(&val); if(waitreturn == -1) { printf("errno:%d\n", errno); }
得到status信息:
由於status信息被存放在一個整數的不同二進制位中,不同平台有不同定義,所以用常規的方法讀取會非常麻煩,人們就設計了一套專門的宏(macro)來完成這項工作,最常用的有:
(1)WIFEXITED(status) 這個宏用來指出子進程是否為正常退出(不是信號導致的退出),如果是,它會返回一個非零值。參數status是wait參數指針指向的整數。
(2)WEXITSTATUS(status) 當WIFEXITED返回非零值時,可以用這個宏來提取子進程的返回值,如果子進程調用exit(5)退出,WEXITSTATUS(status) 就會返回5;如果子進程調用exit(7),WEXITSTATUS(status)就會返回7。請注意,如果進程不是正常退出的,也就是說, WIFEXITED返回0,這個值就毫無意義。
其余:
(1)WIFSTOPPED/WSTOPSIG:當子進程是因為被一個信號暫停而返回時則WIFSTOPPED(status)為真,在這種情況下WSTOPSIG(status)返回這個暫停子進程信號的編號。
(2)WIFCONTINUED:當一個暫停的子進程被信號SIGCONT喚醒而返回狀態,則WIFCONTINUED(status)為真,否則為假。
(3)WIFSIGNALED/WTERMSIG/WCOREDUMP:當程序異常終止時WIFSIGNALED(staus)為真,這種情況下WTERMSIG(status)返回終止進程的信號編號。並且程序異常終止時產生了core文件的話,則WCOREDUMP(status)為真,否則為假。
waitpid()
函數原型:
#include <sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
作用:
系統調用waitpid是wait的封裝,waitpid只是多出了兩個可由用戶控制的參數pid和options,為編程提供了靈活性。
參數:
(1)pid
pid>0時,只等待進程ID等於pid的子進程,不管其它已經有多少子進程運行結束退出了,只要指定的子進程還沒有結束,waitpid就會一直等下去
pid=-1時,等待任何一個子進程退出,沒有任何限制,此時waitpid和wait的作用一模一樣
pid=0時,等待同一個進程組中的任何子進程,如果子進程已經加入了別的進程組,waitpid不會對它做任何理睬
pid<-1時,等待一個指定進程組中的任何子進程,這個進程組的ID等於pid的絕對值
(2)options
如果使用了WNOHANG參數,即使沒有子進程退出,它也會立即返回,不會像wait那樣永遠等下去
如果使用了WUNTRACED參數,則子進程進入暫停則馬上返回,但結束狀態不予以理會
Linux中只支持WNOHANG和WUNTRACED兩個選項,這是兩個常數,可以用"|"運算符把它們連接起來使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
如果不想使用它們,也可以把options設為0,如:ret=waitpid(-1,NULL,0);
返回值:
(1)當正常返回的時候waitpid返回收集到的子進程的進程ID;
(2)如果設置了選項WNOHANG,而調用中waitpid發現沒有已退出的子進程可收集,則返回0;
(3)如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在;
(4)當pid所指示的子進程不存在,或此進程存在,但不是調用進程的子進程,waitpid就會出錯返回,這時errno被設置為ECHILD。
得到status信息(和上面一樣):
4、實驗
以下實驗了多種情況,用於理解父進程wait多個子進程
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
if(fork() == 0)
{
printf("1--This is the child process. pid =%d\n", getpid());
sleep(3);
printf("%d exit\n", getpid());
exit(0);
}
if(fork() == 0)
{
printf("2--This is the child process. pid =%d\n", getpid());
sleep(10);
printf("%d exit\n", getpid());
exit(0);
}
if(fork() == 0)
{
printf("3--This is the child process. pid =%d\n", getpid());
sleep(17);
printf("%d exit\n", getpid());
exit(0);
}
// while(1)
// {
// pid = wait(NULL);
// printf("parent:%d,return of wait:%d\n", getpid(), pid);
// if(pid == -1)
// {
// break;
// }
// }
//循環wait每一個子進程輸出,最后返回值是-1表示沒有子進程了,該父進程也退出
//while(wait(NULL) != -1);
//wait最后一個子進程退出后,該父進程退出
//printf("parent:%d,return of wait:%d\n", getpid(), wait(NULL));
//wait第一個返回的進程后,該父進程就返回,其他未返回進程變孤兒進程,由init進程接管
// while(wait(NULL) != -1)
// {
// printf("parent:%d,return of wait:%d\n", getpid(), wait(NULL));
// }
//while里wait第一個退出的子進程,printf里wait第二個子進程輸出,while再wait第三個子進程,最后printf里是-1,表明已經沒有子進程,父進程退出
signal(SIGCHLD,SIG_IGN);//忽略子進程exit,內核直接轉交init處理defunct,這樣就算父進程沒退出也不會有僵屍進程
sleep(20);//父進程沒有wait任何子進程,沒有設置SIG_IGN,且還在運行中時子進程就退出了,此時所有子進程變僵屍進程,直到父進程退出后,子進程被init進程接管處理
printf("exit\n");
return 0;
}
總結:
調用一個wait,則第一個子進程返回后該父進程也返回,那其他子進程還在運行,為何沒有變成僵屍進程,而是直接由init接管
-----父進程返回了,所以其他子進程變孤兒進程,直接由init接管,所以ps –ef | grep defunct沒有僵屍進程
如果父進程fork多個子進程,子進程退出后,該父進程還在運行中,但沒wait任何子進程,則此時的子進程會變僵屍進程,直到父進程結束,然后由init進程接管
-----父進程不wait阻塞,而且在子進程結束后還在運行。此時為了不產生僵屍進程,可以在父進程中設置signal(SIGCHLD,SIG_IGN); 使得父進程忽略子進程退出,把子進程直接交由init接管
5、父進程退出(正常/異常退出)讓子進程也退出
C語言。直接在子進程中加:prctl(PR_SET_PDEATHSIG, SIGHUP);
python。在子進程中:
import signal
import prctl
prctl.set_pdeathsig(signal.SIGHUP)
需要下載對應python-prctl:
1、ubuntu: apt-get install build-essential libcap-dev pip install python-prctl
2、centos: yum install gcc glibc-devel libcap-devel easy_install python-prctl
在 Linux 中,進程可以要求內核在父進程退出的時候給自己發信號。如上在子進程中使用prctl系統調用,父進程掛了后,子進程就會收到SIGHUP信號,系統對SIGHUP信號的默認處理是終止收到該信號的進程。所以若程序中沒有捕捉該信號,當收到該信號時,進程就會退出。
(1)關於linux信號
各種信號的說明參見:signal
linux下:
ctrl-c 發送 SIGINT 信號給前台進程組中的所有進程。常用於終止正在運行的程序。
ctrl-z 發送 SIGTSTP 信號給前台進程組中的所有進程。常用於掛起一個進程,暫停執行,放入后台,可通過jobs顯示當前暫停的進程,使用fg讓最后一個進程在前台運行(fg %N使第N個到前台,bg %N到后台)。
ctrl-d 不是發送信號,而是表示一個特殊的二進制值,表示 EOF。退出當前shell,相當於exit命令。
ctrl-\ 發送 SIGQUIT 信號給前台進程組中的所有進程,終止前台進程並生成 core 文件。
Key Function
Ctrl-c Kill foreground process
Ctrl-z Suspend foreground process
Ctrl-d Terminate input, or exit shell
Ctrl-s Suspend output
Ctrl-q Resume output
Ctrl-o Discard output
Ctrl-l Clear screen
(2)關於prctl
a.使用:
int prctl ( int option,unsigned long arg2,unsigned long arg3,unsigned long arg4,unsigned long arg5 )
option可選(各種options含義):
PR_SET_PDEATHSIG :arg2作為處理器信號pdeath被輸入,正如其名,如果父進程不能再用,進程接受這個信號。
PR_SET_NAME :把參數arg2作為調用進程的進程名字。(SinceLinux 2.6.11)(此方法改變了task_struct的comm。現象是:/proc/$pid/status和pstree -p都變為新名字,/proc/$pid/cmdline和ps -aux |grep $pid沒變)(其他修改進程名的方法)
b.內核源碼:
SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3, unsigned long, arg4, unsigned long, arg5) { struct task_struct *me = current; unsigned char comm[sizeof(me->comm)]; long error; error = security_task_prctl(option, arg2, arg3, arg4, arg5); if (error != -ENOSYS) return error; error = 0; switch (option) { case PR_SET_PDEATHSIG: if (!valid_signal(arg2)) { error = -EINVAL; break; } me->pdeath_signal = arg2;//父進程dies,傳遞給子進程的signal break; ... case PR_SET_NAME: comm[sizeof(me->comm) - 1] = 0; if (strncpy_from_user(comm, (char __user *)arg2, sizeof(me->comm) - 1) < 0) return -EFAULT; set_task_comm(me, comm);//修改task_struct->comm proc_comm_connector(me); break; ... }
概念參考:
https://www.cnblogs.com/wuchanming/p/4020463.html
https://www.cnblogs.com/Anker/p/3271773.html
https://blog.csdn.net/astrotycoon/article/details/41172389