在unix网络编程中的TCP客户/服务器程序示例中第一个示例是使用多进程的阻塞I/O设计的服务器,这里把要注意的地方做一下笔记。
服务器端源码:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/time.h> #include <time.h> #include <netinet/in.h> #include <errno.h> #include <arpa/inet.h> #include <string.h> #include <signal.h> void sig_chld(int signo) { pid_t pid; int stat; while( (pid = waitpid(-1, &stat, WNOHANG)) > 0 ) { printf("child %d terminated\n", pid); } return ; } void str_echo(int sockfd) { ssize_t n; char buff[50000]; again: while( (n = read(sockfd, buff ,sizeof(buff))) > 0 ) { write(sockfd, buff, n); } if(n <0 && errno == EINTR) goto again; else if(n < 0) perror("read error"); } int main() { int listenfd, connfd; pid_t childpid; socklen_t client; struct sockaddr_in clientaddr; struct sockaddr_in serveraddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); //bzero(&serveraddr, sizeof(serveraddr)); memset(&serveraddr, 0, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(3333); serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)); listen(listenfd, 5); signal(SIGCHLD, sig_chld); for( ; ; ) { client = sizeof(clientaddr); if( (connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &client)) < 0 ) { if(errno == EINTR) continue; else perror("accept error"); } if( (childpid = fork()) == 0 ) { close(listenfd); str_echo(connfd); close(connfd); exit(0); } close(connfd); } }
问题一:子进程回收问题
服务器在每接到客户端的连接请求而创立连接后,总会创建一个子进程去执行与客户端的交互,而父进程继续监听客户端的连接请求。在子进程执行完毕退出后,如果父进程没有对其回收,则子进程就变成了僵尸进程,是的资源无法释放。
父进程回收子进程资源的方式是调用wait或waitpid函数。但是如果直接在父进程的循环中直接使用wait函数则会使父进程阻塞在该函数处,使得无法继续接收客户端连接请求。waitpid函数可以设置为非阻塞模式,但是当父进程阻塞在accept函数处时,无法回收此时结束的子进程。
解决方法是利用信号处理函数。在子进程退出时,内核会向子进程的父进程发送SIGCHLD信号,父进程只要捕获该信号并调用信号处理函数处理sig_chld()函数即可。在sig_chld设计中使用的是waitpid函数而不是wait函数,是因为wait函数不能设置为非阻塞的。当多个子进程几乎同时退出时,父进程在捕获到其中一个信号并在执行信号处理函数过程中,其他的SIGCHLD信号将会被忽略,这样会产生大量的僵尸进程,因此sig_chld()函数使用了wile循环以回收多个同时结束的子进程。
void sig_chld(int signo) { pid_t pid; int stat; while( (pid = waitpid(-1, &stat, WNOHANG)) > 0 ) { printf("child %d terminated\n", pid); } return ; }
WNOHANG设置waitpid函数是非阻塞的,如果函数调用时没用退出的子进程,将返回0,循环将结束,程序继续执行中断开始的地方。如果有多个子进程退出,则循环会多次执行waitpid函数,这样避免产生僵尸进程。
在使用signal函数捕获到信号时会产生中断。如果父进程正阻塞在accept函数处时,中断可能会使accept函数返回-1,并将errno设置为EINTR。这里涉及到了慢系统调用的基本规则:当阻塞于某个慢系统调用的一个进程捕获了某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。有些内核自动重启某些被中断的系统调用,有些则需要自己编写程序重启该系统调用。
for( ; ; ) { client = sizeof(clientaddr); if( (connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &client)) < 0 ) { if(errno == EINTR) continue; else perror("accept error"); }
能够重启的系统调用有read、 write、select、open等; connect函数不能重启,要重新创建套接字。