问题:由上一篇文章分析可知,若客户端进程终止,则服务器子进程会变成僵尸进程。原因是由于服务器子进程退出后,服务器父进程未获取子进程的状态信息,导致子进程变成僵尸进程。
解决方法:由于子进程退出会产生SIGCHLD信号给父进程,所以添加一个对该信号的捕获函数,在该函数内获取子进程的终止状态,从而避免产生僵尸进程。
程序:
信号处理函数:
void sigfunc(int signo)
{
pid_t pid;
int status;
pid = wait(&status);
printf("child %d terminated\n", pid);
return;
}
引发的问题:子进程终止时,父进程是阻塞与accept()系统调用的。父进程得到子进程的终止信号后,去执行信号处理函数,则accept()系统调用就被中断,称为“
被中断的系统调用”,accept是
慢系统调用,即调用有可能永远无法返回。
当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。而有些内核可以自动重启动被中断的系统调用。因此,当父进程执行完信号处理函数返回时,accept()可能由于未被自动重启动而产生错误,即errno=EINTR。
解决办法:accept()返回时,若产生错误,查看错误原因。若errno=EINTR,则可知是由于被中断的系统调用未自动重启动导致产生错误。
程序:
accept()调用后添加一下代码:
if (confd == -1)
{
if (errno = EINTR)
continue ;
else
err_exit("accept");
}
多个连接带来的问题:
在客户端进程上发起5个连接:
client:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#define err_exit(m)\
{\
perror(m);\
exit(EXIT_FAILURE);\
}
#define SERV_PORT 9877
#define BUFSIZE 4096
//客户端具体操作函数
void str_cli(FILE *fp, int sockfd);
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("argument error\n");
exit(0);
}
int sockfd[5];
struct sockaddr_in servaddr;
int status;
int i = 0;
for ( i = 0; i < 5; i++)
{
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);//将点分十进制IP地址转化为网络字节序的二进制地址
sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd[i] == -1)
err_exit("socket");
status = connect(sockfd[i], (struct sockaddr *)&servaddr, sizeof(servaddr));//连接服务器
if (status == -1)
err_exit("connect");
}
str_cli(stdin, sockfd[0]);
exit(0);
}
void str_cli(FILE *fp, int sockfd)
{
printf("connection success! \n");
char send[BUFSIZE], recive[BUFSIZE];
while (fgets(send, BUFSIZE, fp) != NULL)
{
write(sockfd, send, strlen(send));
read(sockfd, recive, BUFSIZE);
fputs(recive, stdout);
bzero(recive, BUFSIZE);
}
}
运行服务器程序,运行客户端程序,接着终止客户端进程。最后发现有4个僵尸进程:
原因:在客户端建立5个连接后,在服务器端则会fork5个子进程。终止客户端进程后,客户端进程打开的描述符由内核关闭,从而客户端发送5个FIN给服务器,导致服务器的5个子进程全部终止,产生了5个SIGCHLD信号。而unix信号是不排队的,所以只处理了第一个子进程,剩下的4个子进程变为僵尸进程。
解决办法:服务器的信号处理函数中循环使用waitpid,而wait会导致进程阻塞,所以不能用wait。
程序:
server.c:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
//出错函数
#define err_exit(m)\
{\
perror(m);\
exit(EXIT_FAILURE);\
}
//最大连接数
#define LISTENQ 1024
//服务器端口号
#define SERV_PORT 9877
//接收和发送的缓冲区大小
#define BUFSIZE 4096
//处理客户端请求函数
void str_echo(int confd);
//子进程退出产生的SIGCHLD信号的处理函数
void sigfunc(int signo);
int main(int argc, char **argv)
{
int confd, listenfd;
struct sockaddr_in cliaddr, servaddr;
pid_t childpid;
socklen_t clilen;
int status;
char buff[BUFSIZE];
//设置协议地址结构内容
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
listenfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if (listenfd == -1)
err_exit("socket");
status = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));//将协议、IP地址、端口绑定到套接字
if (status == -1)
err_exit("bind");
status = listen(listenfd, LISTENQ);//使套接字变为监听套接字
if (status == -1)
err_exit("listen");
signal(SIGCHLD, sigfunc);//子进程退出的信号处理
while (1)
{
clilen = sizeof(cliaddr);//这一步最容易忘记
confd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);//等待连接完成
if (confd == -1)
{
if (errno = EINTR)
continue ;
else
err_exit("accept");
}
if ((childpid = fork()) == 0)//并发服务器,fork一个子进程来处理客户端请求
{
printf("connection from %s, port %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
ntohs(cliaddr.sin_port));
close(listenfd);//子进程不需要监听套接字
str_echo(confd);//子进程处理客户端请求
close(confd);//处理结束,关闭连接套接字
exit(0);//处理结束,关闭子进程
}
close(confd);//父进程不需要连接套接字
}
}
void str_echo(int confd)
{
ssize_t n;
char buf[BUFSIZE];
while ((n = read(confd, buf, BUFSIZE)) > 0)
write(confd, buf, n);
}
void sigfunc(int signo)
{
pid_t pid;
int status;
while ( (pid = waitpid(-1, &status, WNOHANG) > 0))
printf("child %d terminated\n", pid);
return;
}
所以在网络编程时要注意2种情况
1.当fork子进程时,一定要捕获SIGCHLD信号,并使用waitpid正确编写信号处理函数
2.当捕获信号时,必须处理被中断的系统调用