说在前面
- 环境: WSL、ubuntu16
- 参考: UNIX网络编程、linux manual page
- 目录:这里
僵尸进程
-
设置僵死(zombie)状态的目的是维护子进程的信息,以便父进程在以后某个时刻获取。这些信息包括子进程的进程ID、终止状态以及资源利用信息(CPU时间、内存使用量等等)。
-
如果一个进程终止,而该进程有子进程处于僵死状态,那么这些子进程将被init进程接管(即这些子进程的ppid-父进程ID被设置为1,init进程的ID)。init进程将清理这些僵尸进程(init进程将wait它们)。
wait, waitpid — wait for a child process to stop or terminate
-
在linux下,使用ps命令可以看到僵尸进程被添加了 标志。
处理僵尸进程
- 僵尸进程占用内核中的空间,不清理可能导致进程资源耗尽。
- 无论何时我们fork子进程都得wait它们,以防它们变成僵死进程。为此我们建立一个俘获SIGCHLD信号的信号处理函数,在函数体中我们调用wait。
// #include <signal.h> // #include <sys/wait.h> void sig_child(int signo) { pit_t pid; int stat; pid = wait(&stat); printf("child %d terminated\n", pid); /* 在信号处理函数中不建议使用printf这样的标准I/O函数 */ return; } typedef void Sigfunc(int); Sigfunc * Signal(int signo, Sigfunc *func) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if(signo == SIGALRM) { #ifdef SA_INTERRUPT //act.sa_flags |= SA_INTERRUPT; // 注释掉 #endif } else { #ifdef SA_RESTART //act.sa_flags |= SA_RESTART; // 注释掉 #endif } if(sigaction(signo, &act, &oact) < 0) return (SIG_ERR); return (oact.sa_handler); } int main(int argc, char **argv) { /* ...完整代码见源码... */ Listen(listenfd, LISTENQ); Signal(SIGCHLD, sig_child); for ( ; ; ) { clilen = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *) &cliaddr, &clilen); /* ...完整代码见源码... */ }
- wait函数使用见⟅UNIX网络编程⟆⦔wait和waitpid函数
- 安装信号处理函数只需要做一次即可。见⟅UNIX网络编程⟆⦔POSIX信号处理。
这里我们使用的是自定义的Signal函数,而不是系统提供的signal函数,因为系统提供的signal函数会设置SA_RESTART标志Signal(SIGCHLD, sig_child);
- 关于SA_RESTART标志
详细见⟅UNIX网络编程⟆⦔POSIX信号处理;在这里我们先将有关代码注释掉,即不设置SA_RESTART标志,这样内核将不会自动重启被中断的系统调用。
代码
编译
- 使用cmake。
cmake . make
运行
-
server
./server.out
-
client
./client.out 127.0.0.1
键入jio,回射jio。(该程序功能见⟅UNIX网络编程⟆⦔TCP客户/服务器程序实例(一))
-
client
键入Ctrl+D -
server
此时server的输出结果如下:
child 500 terminated
此行为sig_child函数的输出;
accept error: Interrupted system call
此行为Accept函数的输出。
-
解释
- Ctrl+D表示EOF。之后客户TCP发送FIN给服务器,服务器响应ACK。
- 收到客户的FIN导致服务器TCP返回一个EOF给函数readline,服务器子进程终止。(1、2两条具体见⟅UNIX网络编程⟆⦔TCP客户/服务器程序实例(二))
- 子进程终止,向父进程递交SIGCHLD信号;此时父进程阻塞于accept系统调用。
- 由于安装了SIGCHLD的信号处理函数,开始执行sig_child函数;在sig_child函数中,wait函数取到子进程的PID以及终止状态(stat),而后printf,最后返回。
- 由于该信号是在父进程阻塞于慢系统调用(accept)时由父进程捕获的,内核会使accept返回一个EINTR错误(表示被中断的系统调用),并且系统没有设置SA_RESTART标志(因此内核不会自动重启accept),另外我们也没有处理该错误(见accept的包裹函数Accept),因此父进程终止。
-
其它
《unix网络编程》一书中,它的栗子是在Solaris中编译运行的,其标准c函数库不会设置SA_RESTART标志,而我们的环境ubuntu中则会设置该标志,所以要达到书中效果需要注释掉那段代码(设置SA_RESTART标志)。
不同的系统signal函数的实现是不同的。 -
关于sig_child函数中的return语句
《unix网络编程》一书中,约定在信号处理函数中显示的给出return语句。即使对于返回值类型为void的信号处理函数而言,有无return效果一样。这么一来, 当某个系统调用被我们编写的某个信号处理函数中断时,我们就可以得知该系统调用具体是被哪个信号处理函数的哪个return语句中断的。(这个怎么理解?)
处理被中断的系统调用
-
我们用术语慢系统调用(slow system call)描述过accept函数,该术语也适用于那些可能永远阻塞的系统调用。
-
永远阻塞的系统调用是指调用有可能永远无法返回,多数网络支持函数都属于这一类。 举例来说,如果没有客户连接到服务器上,那么服务器的accept调用就没有返回的保证。其他慢系统调用的例子是对管道和终端设备的读和写。
例外:磁盘IO,它们一般都会返回到调用者(假设没有灾难性的硬件故障)。
-
适用于慢系统调用的基本规则是:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。
有些内核自动重启某些被中断的系统调用。
不过为了便于移植,当我们编写捕获信号的程序时(多数并发服务器捕获SIGCHLD),我们必须对慢系统调用返回EINTR有所准备。
(移植性问题是由早期使用的修饰词“可能”、“有些”和对POSIX的SA_ RESTART标志的支持是可选的这一事实造成的。 即使某个实现支持SA_ RESTART标志,也并非所有被中断系统调用都可以自动重启。) -
为了处理被中断的accept(以及可移植性),做出如下改动:
for ( ; ; ) { clilen = sizeof(cliaddr); if( (connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen) < 0) { if(errno ==EINTR) continue; else err_sys("accept error"); } /* ... */
这里调用的是accept而不是包裹函数Accept(这里为什么不将EINTR的处理放进包裹函数?)。
上述代码就是将被中断的系统调用重启,这对accept以及read、write、open、select之类的是适用的。但是对于connect来说,若其返回EINTR,我们不能再次调用,否则将立即返回错误。(若connect被一个捕获的信号中断,需要适用select函数来等待连接完成,暂未讨论)