眾所周知,進程通常不是憑空獨立的出現的,在類Unix系統中,所有的其他進程都是從 進程0 fork
出來的,每個進程都會擁有多個子進程。那么,想要弄清楚父進程和子進程的關系,我們首先要了解 fork
究竟經歷了什么過程。
fork
我們可以看一看fork的官方文檔。
$man fork
Linux下將會看到:
FORK(2) Linux Programmer's Manual FORK(2)
NAME
fork - create a child process
SYNOPSIS
#include <unistd.h>
pid_t fork(void);
DESCRIPTION
fork() creates a new process by duplicating the calling process. The new
process is referred to as the child process. The calling process is referred
to as the parent process.
The child process and the parent process run in separate memory spaces. At
the time of fork() both memory spaces have the same content. Memory writes,
file mappings (mmap(2)), and unmappings (munmap(2)) performed by one of the
processes do not affect the other.
The child process is an exact duplicate of the parent process except for the
following points:
* The child has its own unique process ID, and this PID does not match the ID
of any existing process group (setpgid(2)).
* The child's parent process ID is the same as the parent's process ID.
* The child does not inherit its parent's memory locks (mlock(2), mlock‐
all(2)).
* Process resource utilizations (getrusage(2)) and CPU time counters
(times(2)) are reset to zero in the child.
* The child's set of pending signals is initially empty (sigpending(2)).
* The child does not inherit semaphore adjustments from its parent
(semop(2)).
* The child does not inherit process-associated record locks from its parent
(fcntl(2)). (On the other hand, it does inherit fcntl(2) open file
description locks and flock(2) locks from its parent.)
* The child does not inherit timers from its parent (setitimer(2), alarm(2),
timer_create(2)).
* The child does not inherit outstanding asynchronous I/O operations from its
parent (aio_read(3), aio_write(3)), nor does it inherit any asynchronous
I/O contexts from its parent (see io_setup(2)).
而在Unix環境下則會看到:
FORK(2) BSD System Calls Manual FORK(2)
NAME
fork -- create a new process
SYNOPSIS
#include <unistd.h>
pid_t
fork(void);
DESCRIPTION
fork() causes creation of a new process. The new process (child process)
is an exact copy of the calling process (parent process) except for the
following:
o The child process has a unique process ID.
o The child process has a different parent process ID (i.e., the
process ID of the parent process).
o The child process has its own copy of the parent's descriptors.
These descriptors reference the same underlying objects, so
that, for instance, file pointers in file objects are shared
between the child and the parent, so that an lseek(2) on a
descriptor in the child process can affect a subsequent read or
write by the parent. This descriptor copying is also used by
the shell to establish standard input and output for newly cre-
ated processes as well as to set up pipes.
o The child processes resource utilizations are set to 0; see
setrlimit(2).
可以看到基本內容大同小異,簡單進行翻譯一下:調用fork
會創建一個當前進程的精確副本進程,這個被創建出的副本進程被稱作子進程而調用fork
的進程則稱為父進程。既然被稱作精確副本,看來子進程和父進程是相同的,其實也不盡然。
比如,子進程將會擁有一個自己的進程標識符也就是所謂的pid
。同時,根據父進程的不同,子進程的父進程id也不一樣。
由于現代操作系統的寫時復制機制,即使我們知道每個進程都擁有自己獨立的地址空間,其實指向的物理內存是和父進程相同的(代碼段,數據段,堆棧都指向父親的物理空間),只有子進程修改了其中的某個值時(通常會先調度運行子進程),才會給子進程分配新的物理內存,并把根據情況把新的值或原來的值復制給子進程的內存。
由此可見,父子進程其實有相當的獨立性,并不會相互影響。
那么既然沒有相互影響,那么父子進程會不會有依賴關系了呢?比如,關閉了父進程,子進程還會存在嗎?
既然提出了這個問題,正如手術刀既能治病救人也能殺人滅口,想要了解殺死進程之后發生什么,首先要了解的是我們殺死進程的刀——kill
。
kill
首先自然是先查看kill
的手冊:
$man kill
只摘取linux環境下的內容:
KILL(1) User Commands KILL(1)
NAME
kill - send a signal to a process
SYNOPSIS
kill [options] <pid> [...]
kill
聽起來是殺死什么東西的意思,但手冊上卻寫著它只是發送一個信號給進程。其實這個信號指的是Linux標準信號。
Linux標準信號
信號是進程間通信的形式,Linux支持64種標準信號。而其中32種是傳統Unix的信號。可以通過
$kill -l
來查看。
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS
其中可能與關閉進程有關的信號是INT
、QUIT
、TERM
和KILL
。讓我們來一一分析一下。
- SIGINT:其實我們平時在使用終端運行某個軟件的時候,如果這個軟件會持續運行而不再顯示shell提示符,那么我們通常關閉這個程序是使用^C(就是Ctrl+C),其實就是向當前運行的進程發送了一個SIGINT。通知前臺進程組停止進程。也就是會中斷前臺運行的所有進程。
- SIGQUIT:與
SIGINT
其實相似,通過終端鍵入^\來發送,相當于錯誤信號,會讓進程產生core文件。 - SIGTERM:當我們不加任何參數直接調用
kill
時,則會發送這個信號給進程,這個關閉請求并非強制,它會被阻塞,通常會等待一個程序正常退出。 - SIGKILL:這個就厲害了,這也是為什么關不掉一個程序,網上通常會教你
kill -9
,它發送一個強制的關閉信號,不可被忽略。但正是因為這樣,程序難以進行自我清理,而且會產生僵尸進程。
很顯然,由于前三者關閉進程的“人性化”導致出問題的情況及其有限,接下來我們將要對這個兇殘的kill -9
做做文章。
在繼續討論之前,我們先來補充一點知識:
僵尸進程和孤兒進程
進程和現實與眾不同的是,進程的世界通常是“白發人送黑發人“,父進程在調用子進程之后,通常會在子進程結束之后進行一些后續處理。
但我們知道,父進程的運行和子進程的結束通常是不可能同時進行的,那父進程也不可能知道子進程是如何結束的,那么,父進程如何為子進程”收尸“呢。
原來每個進程在結束自己之前通常會調用exit()
命令,資源即使早就全部釋放了,但進程號,運行時間,退出狀態卻會因此命令而保留,等到父進程調用了waitpid()
時,才會釋放這些內容。如果父進程不調用waitpid()
,則子進程的信息永遠不會釋放,這就是所謂的僵尸進程。
除非,父進程在子進程exit
之前就已經關閉,子進程便不會變為僵尸進程,這是因為,每次一個進程結束時,系統都會自動掃描一下這個進程的子進程,如果這個進程有子進程,此時這些子進程被稱作孤兒進程,便會把這些進程轉交給init
接管。這些子進程結束后,自然init
作為”繼父“進程會以某種機制waitpid()
(收尸)的。
讓我們先來構造一個父子關系程序。
#include <stdio.h>
#include <unistd.h>
int main (void) {
pid_t pid;
int count = 0;
pid = fork ();
if (pid < 0) printf("Error!\n");
else if (pid == 0) {
printf("I'm a child\n");
while(1);
exit (0);
}
else {
printf("I'm the father, and my son's pid is %d \n",pid);
while(1);
}
exit(0);
}
可見,這個C程序調用了fork()
來創建了一個子程序。父子程序都用一個死循環來卡住.
運行效果如下:
I'm the father, and my son's pid is 40211
I'm a child
使用^C可以看到,發送了SIGINT
信號,關閉了父進程和子進程。這是由于SIGINT
將會發送給所有依賴于當前終端的進程,自然前臺所有的進程都關閉了。
重新運行,并使用$ps -ef
觀看進程列表。
此時使用$pstree -p [父進程的pid]
則會看到父子進程的關系。此時無論向父進程發送任何退出信號,都會讓子進程變為孤兒進程。
那么,怎樣才能讓我的子進程隨著父進程愉快地結束呢?
讓我們改寫一下代碼:
#include <signal.h>
#include <sys/prctl.h>
#include <stdio.h>
#include <unistd.h>
int main (void) {
pid_t pid;
int count = 0;
pid = fork ();
if (pid < 0) printf("Error!\n");
else if (pid == 0) {
printf("I'm a child\n");
while(1) {
prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);
}
exit (0);
}
else {
printf("I'm the father, and my son's pid is %d \n",pid);
while(1) {
}
}
exit(0);
}
這時,殺死父進程,子進程會向自己發送一個SIGKILL
信號,從而父子進程都被關閉了。可見父子進程之間的生命相依聯系,就是通過prctl
來維系的。這也是程序員可以控制的關系。