分类: LINUX
2009-02-17 20:39:08
Unix网络编程中服务端子进程终止时的信号处理问题。
在《unix网络编程》中有这样一个例子:
服务端:
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 12345
#define LISTENQ 1024
int main(int argc, char ** argv) {
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("socket error!\n");
exit(1);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {
printf("bind error!\n");
exit(1);
} else {
printf("bind successfully!\n");
}
if (listen(listenfd, LISTENQ) != 0) {
printf("listen error!\n");
exit(1);
} else {
printf("listen successfully!\n");
}
for(;;) {
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen)) < 0) {
printf("accept error!\n");
}
if ( (childpid = fork()) == 0) {
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}
客户端:
#include
#include
#include
#include
#include
#include
int main(int argc, char ** argv) {
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2) {
printf("error!\n");
exit(1);
}
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("socket error!\n");
exit(1);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(12345);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
printf("connect error!\n");
exit(1);
}
str_cli(stdin, sockfd);
exit(0);
}
这个程序有个问题:fork子进程后没有捕获SIGCHLD信号。
在服务器子进程终止的时候,会给父进程发送一个SIGCHLD信号。但是我们在代码中并没有对其进行处理,则内核对该信号的缺省操作是被忽略。于是子进程遍进入僵死状态,我们可以用一个小程序来验证一下:(fork.c)
#include
int main(int argc, char ** argv) {
int pid;
if ( (pid = fork()) == 0) {
// do nothing ...
} else {
// sleep
sleep(10000);
}
}
编译:
-bash-3.2$ gcc -o forktest fork.c
运行程序之前用ps查看一下:
-bash-3.2$ ps
PID TTY TIME CMD
运行程序:
-bash-3.2$ ./forktest &
[1] 4869
然后用ps查看一下:
-bash-3.2$ ps
PID TTY TIME CMD
可见子进程进入僵死状态,我们可以看的更加清楚一些:
-bash-3.2$ ps -t pts/29 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
4869
4870
4882
13968
状态Z就代表僵死状态。
《unix网络编程》中的信号处理函数是自己写的Signal函数,他对sigaction函数进行了包装:
1 #include "unp.h"
2 Sigfunc *
3 Signal (int signo, Sigfunc *func)
4 {
5 struct sigaction act, oact;
6 act.sa_handler = func;
7 sigemptyset (&act.sa_mask);
8 act.sa_flags = 0;
9 if (signo == SIGALRM) {
10 #ifdef SA_INTERRUPT
11 act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
12 #endif
13 } else {
14 #ifdef SA_RESTART
15 act.sa_flags |= SA_RESTART; /* SVR4, 4.4BSD */
16 #endif
17 }
18 if (sigaction (signo, &act, &oact) < 0)
19 return (SIG_ERR);
20 return (oact.sa_handler);
21 }
注意第15行这里的SA_RESTART标志,如果设置,有相应信号中断的系统调用将有内核自动重启,但是并非所有系统都支持这个标志,所以要有一个通用的解决办法。
在本例中父进程阻塞与accept系统调用,子进程发送的SIGCHLD信号在此时捕获,则在accept中可能会返回一个EINTR错误(被中断的系统调用)。有些内核自动重启某些被中断的系统调用,不过为了移植性,当我们编写程序时,我们必须对EINTR错误有所准备。
为了处理中断的accept函数,我们将代码改写如下:
for ( ; ; ) {
clilen = sizeof (cliaddr);
if ( (connfd = accept (listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for () */
else
err_sys ("accept error");
}
这段代码会就是自己重启被中断的系统调用。
还有一个问题需要注意的是在写信号处理函数的时候获得已终止子进程进程好的方法要用waitpid而不是wait,因为当有多个子进程在信号处理函数前终止,那么wait只会捕获到第一个终止的子进程。正确的做法是使用waitpid,在一个循环内调用waitpid以获得所有已终止子进程的状态,而且必须制定WNOHANG选项,意思是在有尚未终止的子进程在运行是不要阻塞。
完整的代码如下:
信号处理函数:
1 #include "unp.h"
2 void
3 sig_chld(int signo)
4 {
5 pid_t pid;
6 int stat;
7 while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
8 printf("child %d terminated\n", pid);
9 return;
10 }
服务器端代码:
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 12345
#define LISTENQ 1024
int main(int argc, char ** argv) {
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("socket error!\n");
exit(1);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {
printf("bind error!\n");
exit(1);
} else {
printf("bind successfully!\n");
}
if (listen(listenfd, LISTENQ) != 0) {
printf("listen error!\n");
exit(1);
} else {
printf("listen successfully!\n");
}
for(;;) {
clilen = sizeof(cliaddr);
if ( (connfd = accept (listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
pringf("accept error\n");
}
if ( (childpid = fork()) == 0) {
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}
总结:
在进行并发程序编写是要注意下面问题:
1. 当fork子进程时,必须对SIGCHLD信号进行处理
2. 当捕获信号时,必须处理被中断的系统调用
3. 信号处理函数必须用waitpid而不是wait