众所周知,进程通常不是凭空独立的出现的,在类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
来维系的。这也是程序员可以控制的关系。