不過進程的退出並沒有把所有資源釋放,保留一一些資源,
比如進程的PID依然被占用,不可被分配,來看看僵屍進程依舊占有的資源:進程控制塊task_struct ,內核棧等。這些資源不釋放是為了提供一些重要信息,比如進程為何退出,退出碼是多少,收到信號退出還是正常退出等,像墓志銘一樣總結僵屍進程的一生,一般是由父進程收集子進程的死亡信息。
清除僵屍進程有2種方法:
父進程調用wait函數,父進程退出,init進程為子進程收屍
如何防止僵屍進程的產生
將子進程死亡發送的SIGCHLD的處理函數設置為SIG_IGN或者在調用sigaction函數時設置SA_NOCLDWAIT標志位。這2者都會告訴子進程,父進程很絕情,不會為子進程收屍。反正一旦這2者有一個設定了,autoreap標志位將設置為true,子進程發現autoreap為True,子進程掛了將不會進入僵屍狀態,而是調用release_task函數自行了斷.
等待子進程之wait()
include <sys/wait.h>
pid_t wait(int *status);
成功時,返回已退出子進程的進程ID;失敗時,則返回-1並設置errno。
注意父子進程是兩個進程,子進程退出和父進程調用wait()函數來獲取子進程的退出狀態在時間上是獨立的事件,因此會出現以下兩種情況:
·
子進程先退出,父進程后調用wait()函數。
·父進程先調用wait()函數,子進程后退出。
第一種情況,子進程幾乎已經銷毀了自己所有的資源,只留下少量的信息等待父進程來“收屍”。當父進程調用wait()函數的時候,苦守寒窯十八載的子進程終於等到了父進程來“收屍”,這種情況下,父進程獲取到子進程的狀態信息,wait函數立刻返回。
對於第二種情況,父進程先調用wait()函數,調用時並無子進程退出,該函數調用就會陷入阻塞狀態,直到某個子進程退出。
wait()函數等待的是任意一個子進程,任何一個子進程退出,都可以讓其返回。當多個子進程都處於僵屍狀態,wait()函數獲取到其中一個子進程的信息后立刻返回。
由於wait()函數不會接受pid_t類型的入參,所以它無法明確地等待特定的子進程。
一個進程如何等待所有的子進程退出呢?
wait()函數返回有三種可能性:
·等到了子進程退出,獲取其退出信息,返回子進程的進程ID。
·等待過程中,收到了信號,信號打斷了系統調用,並且注冊信號處理函數時並沒有設置SA_RESTART標志位,系統調用不會被重啟,wait()函數返回-1,並且將errno設置為EINTR。
·已經成功地等待了所有子進程,沒有子進程的退出信息需要接收,在這種情況下,wait()函數返回-1,errno為ECHILD。
《Linux/Unix系統編程手冊》給出下面的代碼來等待所有子進程的退出:
while((childPid = wait(NULL)) != -1)
continue;
if(errno !=ECHILD)
errExit("wait");
這種方法並不完全,因為這里忽略了wait()函數被信號中斷這種情況,如果wait()函數被信號中斷,上面的代碼並不能成功地等待所有子進程退出。
若將上面的wait()函數封裝一下,使其在信號中斷后,自動重啟wait就完備了。代碼如下:
pid_t r_wait(int *stat_loc)
{
int retval;
while(((retval = wait(stat_loc)) == -1 &&
(errno == EINTR))//被信號打斷 EINTR;
return retval;
}
while((childPid = r_wait(NULL)) != -1)
continue;
If(errno != ECHILD)
{
/*some error happened*/
}
如果父進程調用wait()函數時,已經有多個子進程退出且都處於僵屍狀態,那么哪一個子進程會被先處理是不一定的(標准並未規定處理的順序)。
通過上面的討論,可以看出wait()函數存在一定的局限性:
不能等待特定的子進程。如果進程存在多個子進程,而它只想獲取某個子進程的退出狀態,並不關心其他子進程的退出狀態,此時wait()只能一一等待,通過查看返回值來判斷是否為關心的子進程。
·
如果不存在子進程退出,wait()只能阻塞。有些時候,僅僅是想嘗試獲取退出子進程的退出狀態,如果不存在子進程退出就立刻返回,不需要阻塞等待,類似於trywait的概念。wait()函數沒有提供trywait的接口。
·wait()函數只能發現子進程的終止事件,如果子進程因某信號而停止,或者停止的子進程收到SIGCONT信號又恢復執行,這些事件 wait()函數是無法獲知的。換言之,wait()能夠探知子進程的死亡,卻不能探知子進程的昏迷(暫停),也無法探知子進程從昏迷中蘇醒(恢復執行)。
由於上述三個缺點的存在,所以Linux又引入了waitpid()函數
等待子進程之waitpid()
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
先說說waitpid()與wait()函數相同的地方:
·返回值的含義相同,都是終止子進程或因信號停止或因信號恢復而執行的子進程的進程ID。
·status的含義相同,都是用來記錄子進程的相關事件,后面一節將會詳細介紹。
接下來介紹waitpid()函數特有的功能。
其第一個參數是pid_t類型,有了此值,不難看出waitpid函數肯定具備了精確打擊的能力。waitpid函數可以明確指定要等待哪一個子進程的退出(以及停止和恢復執行)。事實上,擴展的功能不僅僅如此:
·pid>0:表示等待進程ID為pid的子進程,也就是上文提到的精確打擊的對象。
·pid=0:表示等待與調用進程同一個進程組的任意子進程;因為子進程可以設置自己的進程組,所以某些子進程不一定和父進程歸屬於同一個進程組,這樣的子進程,waitpid函數就毫不關心了。
·pid=-1:表示等待任意子進程,同wait類似。waitpid(-1,&status,0)與wait(&status)完全等價。
·pid<-1:等待所有子進程中,進程組ID與pid絕對值相等的所有子進程。
內核之中,wait函數和waitpid函數調用的都是wait4系統調用。下面是wait4系統調用的實現。函數的中間部分,根據pid的正負或是否為0和-1來定義wait_opts類型的變量wo,后面會根據wo來控制到底關心哪些進程的事件。
SYSCALL_DEFINE4(wait4, pid_t, upid, int __user *, stat_addr,
int, options, struct rusage __user *, ru)
{
struct wait_opts wo;
struct pid *pid = NULL;
enum pid_type type;
long ret;
if (options & ~(WNOHANG|WUNTRACED|WCONTINUED|
__WNOTHREAD|__WCLONE|__WALL))
return -EINVAL;
if (upid == -1)
type = PIDTYPE_MAX; /*任意子進程*/
else if (upid < 0) { //
等待所有子進程中,進程組ID與pid絕對值相等的所有子進程type = PIDTYPE_PGID;
pid = find_get_pid(-upid);
} else if (upid == 0) { //
表示等待與調用進程同一個進程組的任意子進程type = PIDTYPE_PGID;
pid = get_task_pid(current, PIDTYPE_PGID);
} else /* upid > 0 */ { //等待pid值的進程
type = PIDTYPE_PID;
pid = find_get_pid(upid);
}
wo.wo_type = type;
wo.wo_pid = pid;
wo.wo_flags = options | WEXITED;
wo.wo_info = NULL;
wo.wo_stat = stat_addr;
wo.wo_rusage = ru;
ret = do_wait(&wo);
put_pid(pid);
/* avoid REGPARM breakage on x86: */
asmlinkage_protect(4, ret, upid, stat_addr, options, ru);
return ret;
}
可以看到
,內核的do_wait函數會根據wait_opts類型的wo變量來控制到底在等待哪些子進程的狀態。
當前進程中的每一個線程(在內核層面,線程就是進程,每個線程都有獨立的task_struct),都會遍歷其子進程。在內核中,task_struct中的children成員變量是個鏈表頭,該進程的所有子進程都會鏈入該鏈表,遍歷起來比較方便。代碼如下:
static int do_wait_thread(struct wait_opts *wo, struct task_struct *tsk)
{
struct task_struct *p;
list_for_each_entry(p, &tsk->children, sibling) {
/*遍歷進程所有的子進程*/
int ret = wait_consider_task(wo, 0, p);
if (ret)
return ret;
}
return 0;
}
但是我們並不一定關心所有的子進程。當wait()函數或waitpid()函數的第一個參數pid等於-1的時候,表示任意子進程我們都關心。但是如果是waitpid()函數的其他情況,則表示我們只關心其中的某些子進程或某個子進程。內核需要對所有的子進程進行過濾,找到關心的子進程。這個過濾的環節是在內核的eligible_pid函數中完成的。
/* 當waitpid的第一個參數為-1時, wo->wo_type 賦值為PIDTYPE_MAX
* 其他三種情況task_pid_type(p, wo->wo_type)== wo->wo_pid檢驗
* 或者檢查pid是否相等, 或者檢查進程組ID是否等於指定值
*/
static int eligible_pid(struct wait_opts *wo, struct task_struct *p)
{
return wo->wo_type == PIDTYPE_MAX ||
task_pid_type(p, wo->wo_type) == wo->wo_pid; //
其他三種情況task_pid_type(p, wo->wo_type)== wo->wo_pid檢驗}
waitpid函數的第三個參數options是一個位掩碼(bit mask),可以同時存在多個標志。當options沒有設置任何標志位時,其行為與wait類似,即阻塞等待與pid匹配的子進程退出。
options的標志位可以是如下標志位的組合:
·WUNTRACE:除了關心終止子進程的信息,也關心那些因信號而停止的子進程信息。
·WCONTINUED:除了關心終止子進程的信息,也關心那些因收到信號而恢復執行的子進程的狀態信息。
·WNOHANG:指定的子進程並未發生狀態變化,立刻返回,不會阻塞。這種情況下返回值是0。如果調用進程並沒有與pid匹配的子進程,則返回-1,並設置errno為ECHILD,根據返回值和errno可以區分這兩種情況。
傳統的wait函數只關注子進程的終止,而waitpid函數則可以通過前兩個標志位來檢測子進程的停止和從停止中恢復這兩個事件。
講到這里,需要解釋一下什么是“使進程停止”,什么是“使進程繼續”,以及為什么需要這些。設想如下的場景,正在某機器上編譯一個大型項目,編譯過程需要消耗很多CPU資源和磁盤I/O資源,並且耗時很久。如果我暫時需要用機器做其他事情,雖然可能只需要占用幾分鍾時間。但這會使這幾分鍾內的用戶體驗非常糟糕,那怎么辦?當然,殺掉編譯進程是一個選擇,但是這個方案並不好。因為編譯耗時很久,貿然殺死進程,你將不得不從頭編譯起。這時候,我們需要的僅僅是讓編譯大型工程的進程停下來,把CPU資源和I/O資源讓給我,讓我從容地做自己想做的事情,幾分鍾后,我用完了,讓編譯的進程繼續工作就行了。
Linux提供了SIGSTOP(信號值19)和SIGCONT(信號值18)兩個信號,來完成暫停和恢復的動作,可以通過執行kill-SIGSTOP或kill-19來暫停一個進程的執行,通過執行kill-SIGCONT或kill-18來讓一個暫停的進程恢復執行。
waitpid()函數可以通過WUNTRACE標志位關注停止的事件,如果有子進程收到信號處於暫停狀態,waitpid就可以返回。
同樣的道理,通過WCONTINUED標志位可以關注恢復執行的事件,如果有子進程收到SIGCONT信號而恢復執行,waitpid就可以返回。
但是上述兩個事件和子進程的終止事件是並列的關系,waitpid成功返回的時候,可能是等到了子進程的終止事件,也可能是等到了暫停或恢復執行的事件。這需要通過status的值來區分。
那么,現在應該分析status的值了。
4.7.4 等待子進程之等待狀態值
無論是wait()函數還是waitpid()函數,都有一個status變量。這個變量是一個int型指針。可以傳遞NULL,表示不關心子進程的狀態信息。如果不為空,則根據填充的status值,可以獲取到子進程的很多信息,如圖4-12所示。

根據圖4-12可知,直接根據status值可以獲得進程的退出方式,但是為了保證可移植性,不應該直接解析status值來獲取退出狀態。因此系統提供了相應的宏(macro),用來解析返回值。下面分別介紹各種情況。
1.進程是正常退出的
有兩個宏與正常退出相關,見表4-4。
表4-4 與進程正常退出相關的宏

2.進程收到信號,導致退出
有三個宏與這種情況相關,見表4-5。
表4-5 與進程收到信號導致退出相關的宏

3.進程收到信號,被停止
有兩個宏與這種情況相關,見表4-6。

之所以需要WSTOPSIG宏來返回導致子進程停止的信號值,是因為不只一個信號可以導致子進程停止:SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU,都可以使進程停止。
4.子進程恢復執行
有一個宏與這種情況相關,見表4-7。
表4-7 與子進程恢復執行相關的宏

為何沒有返回使子進程恢復的信號值的宏?原因是只有SIGCONT信號能夠使子進程從停止狀態中恢復過來。如果子進程恢復執行,只可能是收到了SIGCONT信號,所以不需要宏來取信號的值。
下面給出了判斷子進程終止的示例代碼。等待子進程暫停或恢復執行的情況,可以根據下面的示例代碼自行實現。
void print_wait_exit(int status)
{
printf("status = %d\n",status);
if(WIFEXITED(status))
{
printf("normal termination,exit status = %d\n",WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))
{
printf("abnormal termination,signal number =%d%s\n",WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status)?"core file generated" : "");
#else
"");
#endif
}
}
進程退出和等待的內核實現
Linux引入多線程之后,為了支持進程的所有線程能夠整體退出,內核引入了exit_group系統調用。對於進程而言,無論是調用exit()函數、_exit()函數還是在main函數中return,最終都會調用exit_group系統調用。
對於單線程的進程,從do_exit_group直接調用do_exit就退出了。但是對於多線程的進程,
如果某一個線程調用了exit_group系統調用,那么該線程在調用do_exit之前,會通過zap_other_threads函數,給每一個兄弟線程掛上一個SIGKILL信號。內核在嘗試遞送信號給兄弟進程時(通過get_signal_to_deliver函數),會在掛起信號中發現SIGKILL信號。內核會直接調用do_group_exit函數讓該線程也退出(如圖4-13所示)。這個過程在第3章中已經詳細分析過了。

在do_exit函數中,進程會釋放幾乎所有的資源(文件、共享內存、信號量等)。該進程並不甘心,因為它還有兩樁心願未了:
·作為父進程,它可能還有子進程,進程退出以后,將來誰為它的子進程收屍”。
·作為子進程,它需要通知它的父進程來為自己“收屍”。
這兩件事情是由exit_notify來負責完成的,具體來說forget_original_parent函數和do_notify_parent函數各自負責一件事,如表4-9所示。

forget_original_parent(),多么“悲傷”的函數名。顧名思義,該函數用來給自己的子進程安排新的父進程。
給自己的子進程安排新的父進程,細分下來,是兩件事情:
1)為子進程尋找新的父進程。
2)將子進程的父進程設置為第1)步中找到的新的父親。
為子進程尋找父進程,是由find_new_reaper()函數完成的。如果退出的進程是多線程進程,則可以將子進程托付給自己的兄弟線程。如果沒有這樣的線程,就“托孤”給init進程。
static void forget_original_parent(struct task_struct *father)
{
struct task_struct *p, *n, *reaper;
LIST_HEAD(dead_children);
write_lock_irq(&tasklist_lock);
/*
* Note that exit_ptrace() and find_new_reaper() might
* drop tasklist_lock and reacquire it.
*/
exit_ptrace(father);
reaper = find_new_reaper(father);
list_for_each_entry_safe(p, n, &father->children, sibling) {
struct task_struct *t = p;
do {
t->real_parent = reaper;
if (t->parent == father) {
BUG_ON(t->ptrace);
t->parent = t->real_parent;
}
/*內核提供了機制, 允許父進程退出時向子進程發送信號*/
if (t->pdeath_signal)
group_send_sig_info(t->pdeath_signal,
SEND_SIG_NOINFO, t);
} while_each_thread(p, t);
reparent_leader(father, p, &dead_children);
}
write_unlock_irq(&tasklist_lock);
BUG_ON(!list_empty(&father->children));
list_for_each_entry_safe(p, n, &dead_children, sibling) {
list_del_init(&p->sibling);
release_task(p);
}
}
這部分代碼比較容易引起困擾的是下面這行,我們都知道,子進程“死”的時候,會向父進程發送信號SIGCHLD,Linux也提供了一種機制,允許父進程“死”的時候向子進程發送信號。
if (t->pdeath_signal)
group_send_sig_info(t->pdeath_signal,
SEND_SIG_NOINFO, t);
讀者可以通過man prctl,查看PR_SET_PDEATHSIG標志位部分。如果應用程序通過prctl函數設置了父進程“死”時要向子進程發送信號,就會執行到這部分內核代碼,以通知其子進程。
接下來是第二樁未了的心願:想辦法通知父進程為自己“收屍”。
對於單線程的程序來說完成這樁心願比較簡單,但是
多線程的情況就復雜些。只有線程組的主線程才有資格通知父進程,線程組的其他線程終止的時候,不需要通知父進程,也沒必要保留最后的資源並陷入僵屍態,直接調用release_task函數釋放所有資源就好。
為什么要這樣設計?細細想來,這么做是合理的。父進程創建子進程時,只有子進程的主線程是父進程親自創建出來的,是父進程的親生兒子,父進程也只關心它,至於子進程調用pthread_create產生的其他線程,父進程壓根就不關心。
由於父進程只認子進程的主線程,所以在線程組中,主線程一定要挺住。在用戶層面,可以調用pthread_exit讓主線程先“死”,但是在內核態中,主線程的task_struct一定要挺住,哪怕變成僵屍,也不能釋放資源。
生命在於“折騰”,如果主線程率先退出了,而其他線程還在正常工作,內核又將如何處理?
else if (thread_group_leader(tsk)) {
/*線程組組長只有在全部線程都已退出的情況下,
*才能調用do_notify_parent通知父進程*/
autoreap = thread_group_empty(tsk) && //必須全部退出才會
do_notify_parent(tsk, tsk->exit_signal);
} else {
/*如果是線程組的非組長線程, 可以立即調用release_task,
*釋放殘余的資源, 因為通知父進程這件事和它沒有關系*/
autoreap = true;
}
上面的代碼給出了答案,如果退出的進程是線程組的主線程,但是線程組中還有其他線程尚未終止(thread_group_empty函數返回false),那么autoreaper就等於false,也就不會調用do_notify_parent向父進程發送信號了。
因為子進程的線程組中有其他線程還活着,因此子進程的主線程退出時不能通知父進程,錯過了調用do_notify_parent的機會,那么父進程如何才能知曉子進程已經退出了呢?答案會在最后一個線程退出時揭曉。此答案就藏在內核的release_task函數中:
leader = p->group_leader;
//不是主線程 自己是最后一個線程 主線程除以僵屍狀態
if (leader != p && thread_group_empty(leader) && leader->exit_state == EXIT_ZOMBIE) {
zap_leader = do_notify_parent(leader, leader->exit_signal);//像父進程發送信號函數
if (zap_leader)
leader->exit_state = EXIT_DEAD;
}
當線程組的最后一個線程退出時,如果發現:
·該線程不是線程組的主線程。
·線程組的主線程已經退出,且處於僵屍狀態。
·自己是最后一個線程。
同時滿足這三個條件的時候,該子進程就需要冒充線程組的組長,即以子進程的主線程的身份來通知父進程。
上面討論了一種比較少見又比較折騰的場景,正常的多線程編程應該不會如此安排。對於多線程的進程,一般情況下會等所有其他線程退出后,主線程才退出。這時,主線程會在exit_notify函數中發現自己是組長,線程組里所有成員均已退出,然后它調用do_notify_parent函數來通知父進程。
無論怎樣,子進程都走到了do_notify_parent函數這一步。該函數是完成父子進程之間互動的主要函數。
//子進程的主要線程pcb,退出信號
bool do_notify_parent(struct task_struct *tsk, int sig)
{
struct siginfo info;
unsigned long flags;
struct sighand_struct *psig;
bool autoreap = false;
BUG_ON(sig == -1);
/* do_notify_parent_cldstop should have been called instead. */
BUG_ON(task_is_stopped_or_traced(tsk));
BUG_ON(!tsk->ptrace &&
(tsk->group_leader != tsk || !thread_group_empty(tsk)));
if (sig != SIGCHLD) {
/*
* This is only possible if parent == real_parent.
* Check if it has changed security domain.
*/
if (tsk->parent_exec_id != tsk->parent->self_exec_id)
sig = SIGCHLD;
}
info.si_signo = sig;
info.si_errno = 0;
rcu_read_lock();
info.si_pid = task_pid_nr_ns(tsk, tsk->parent->nsproxy->pid_ns);
info.si_uid = __task_cred(tsk)->uid;
rcu_read_unlock();
info.si_utime = cputime_to_clock_t(cputime_add(tsk->utime,
tsk->signal->utime));
info.si_stime = cputime_to_clock_t(cputime_add(tsk->stime,
tsk->signal->stime));
info.si_status = tsk->exit_code & 0x7f;
if (tsk->exit_code & 0x80)
info.si_code = CLD_DUMPED;
else if (tsk->exit_code & 0x7f)
info.si_code = CLD_KILLED;
else {
info.si_code = CLD_EXITED;
info.si_status = tsk->exit_code >> 8;
}
psig = tsk->parent->sighand;
spin_lock_irqsave(&psig->siglock, flags)
//是SIGCHLD信號 但父進程的信號處理函數設置為SIG_IGN或者flag設置為SA_NOCLDWAIT位
if (!tsk->ptrace && sig == SIGCHLD &&
(psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN ||(psig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDWAIT))) {
autoreap = true;//設置為true,表示父進程不關心自己的退出信息,將會調用release_task函數,釋放殘余的資源,自行了斷,子進程也就不會進入僵屍狀態了。
if (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN)
sig = 0;
}
/*子進程向父進程發送信號*/
if (valid_signal(sig) && sig)
__group_send_sig_info(sig, &info, tsk->parent);
/* 子進程嘗試喚醒父進程, 如果父進程正在等待其終止 */
__wake_up_parent(tsk, tsk->parent);
spin_unlock_irqrestore(&psig->siglock, flags);
return autoreap;
}
父子進程之間的互動有兩種方式:
·子進程向父進程發送信號SIGCHLD。
·子進程喚醒父進程。
對於這兩種方法,我們分別展開討論。
1.父子進程互動之SIGCHLD信號
父進程可能並不知道子進程是何時退出的,如果調用wait函數等待子進程退出,又會導致父進程陷入阻塞,無法執行其他任務。那有沒有一種辦法,讓子進程退出的時候,異步通知到父進程呢?答案是肯定的。當子進程退出時,會向父進程發送SIGCHLD信號。
父進程收到該信號,默認行為是置之不理。在這種情況下,子進程就會陷入僵屍狀態,而這又會浪費系統資源,該狀態會維持到父進程退出,子進程被init進程接管,init進程會等待僵屍進程,使僵屍進程釋放資源。
如果父進程不太關心子進程的退出事件,聽之任之可不是好辦法,可以采取以下辦法:
·父進程調用signal函數或sigaction函數,將SIGCHLD信號的處理函數設置為SIG_IGN。
·父進程調用sigaction函數,設置標志位時置上SA_NOCLDWAIT位(如果不關心子進程的暫停和恢復執行,則置上SA_NOCLDSTOP位)
從
內核代碼來看,如果父進程的SIGCHLD的信號處理函數為SIG_IGN或sa_flags中被置上了SA_NOCLDWAIT位,子進程運行到此處時就知道了,父進程並不關心自己的退出信息,do_notify_parent函數就會返回true。在外層的exit_notify函數發現返回值是true,就會調用release_task函數,釋放殘余的資源,自行了斷,子進程也就不會進入僵屍狀態了。
為SIGCHLD寫信號處理函數並不簡單,原因是SIGCHLD是傳統的不可靠信號。信號處理函數執行期間,會將引發調用的信號暫時阻塞(除非顯式地指定了SA_NODEFER標志位),在這期間收到的SIGCHLD之類的傳統信號,都不會排隊。因此,如果在處理SIGCHLD信號時,有多個子進程退出,產生了多個SIGCHLD信號,但父進程只能收到一個。如果在信號處理函數中,只調用一次wait或waitpid,則會造成某些僵屍進程成為漏網之魚。
正確的寫法是,信號處理函數內,帶着NOHANG標志位循環調用waitpid。如果返回值大於0,則表示不斷等待子進程退出,返回0則表示當前沒有僵屍子進程,返回-1則表示出錯,最大的可能就是errno等於ECHLD,表示所有子進程都已退出。
while(waitpid(-1,&status,WNOHANG) > 0)
{
/*此處處理返回信息*/
continue;
}
信號處理函數中的waitpid可能會失敗,從而改變全局的errno的值,當主程序檢查errno時,就有可能發生沖突,所以進入信號處理函數前要現保存errno到本地變量,信號處理函數退出前,再恢復errno。
2.父子進程互動之等待隊列
上一種方法可以稱之為信號通知。另一種情況是父進程調用wait主動等待。如果父進程調用wait陷入阻塞,那么子進程退出時,又該如何及時喚醒父進程呢?
前面提到了,子進程會調用__wake_up_parent函數,來及時喚醒父進程。事實上,前提條件是父進程確實在等待子進程的退出。如果父進程並沒有調用wait系列函數等待子進程的退出,那么,等待隊列為空,子進程的__wake_up_parent對父進程並無任何影響。
void __wake_up_parent(struct task_struct *p, struct task_struct *parent)
{ //等待隊列頭
__wake_up_sync_key(&parent->signal->wait_chldexit,
TASK_INTERRUPTIBLE, 1, p);
}
父進程的進程描述符的signal結構體中有wait_childexit變量,這個變量是等待隊列頭。父進程調用wait系列函數時,會創建一個wait_opts結構體,並把該結構體掛入等待隊列中。
static long do_wait(struct wait_opts *wo)
{
struct task_struct *tsk;
int retval;
trace_sched_process_wait(wo->wo_pid);
/*掛入等待隊列*/
init_waitqueue_func_entry(&wo->child_wait, child_wait_callback);
wo->child_wait.private = current;
add_wait_queue(¤t->signal->wait_chldexit, &wo->child_wait);
repeat:
/**/
wo->notask_error = -ECHILD;
if ((wo->wo_type < PIDTYPE_MAX) &&
(!wo->wo_pid || hlist_empty(&wo->wo_pid->tasks[wo->wo_type])))
goto notask;
set_current_state(TASK_INTERRUPTIBLE);//父進程設置自己為此狀態
read_lock(&tasklist_lock);
tsk = current;
do {
retval = do_wait_thread(wo, tsk);
if (retval)
goto end;
retval = ptrace_do_wait(wo, tsk);
if (retval)
goto end;
if (wo->wo_flags & __WNOTHREAD)
break;
} while_each_thread(current, tsk);
read_unlock(&tasklist_lock);
/*找了一圈, 沒有找到滿足等待條件的的子進程, 下一步的行為將取決於WNOHANG標志位
*如果將WNOHANG標志位置位, 則表示不等了, 直接退出,
*如果沒有置位, 則讓出CPU, 醒來后繼續再找一圈*/
notask:
retval = wo->notask_error;
if (!retval && !(wo->wo_flags & WNOHANG)) {
retval = -ERESTARTSYS;
if (!signal_pending(current)) {
schedule();
goto repeat;
}
}
end:
__set_current_state(TASK_RUNNING);//找到了滿足條件的子進程設置為此狀態
remove_wait_queue(¤t->signal->wait_chldexit, &wo->child_wait);
return retval;
}
tsk = current;
父進程先把自己設置成TASK_INTERRUPTIBLE狀態,然后開始尋找滿足等待條件的子進程。如果找到了,則將自己重置成TASK_RUNNING狀態,歡樂返回;如果沒找到,就要根據WNOHANG標志位來決定等不等待子進程。如果沒有WNOHANG標志位,那么,父進程就會讓出CPU資源,等待別人將它喚醒。
回到另一頭,子進程退出的時候,會調用__wake_up_parent,喚醒父進程,父進程醒來以后,回到repeat,再次掃描。這樣做,子進程的退出就能及時通知到父進程,從而使父進程的wait系列函數可以及時返回。