Linux客户/服务器程序设计范式——阿帕奇服务器(多进程)
引言
本文会写一个并发服务器(concurrent server)程序,它为每个客户请求fork出一个子进程。
注意
1. 信号处理问题
对于相同信号,按信号的先后顺序依次处理。可能会产生的问题是,正在处理sig1信号时,又来了2个或更多的sig1信号,此sig1时只会在处理完原来的sig1信号后,再处理1个sig1信号。因此对于相同信号,会产生信号掉包的问题。 一个儿子退了之后,程序在处理handler(),如果此时又退了两个儿子,那么必然有一个儿子的资源回收不到,称为僵尸进程。
对于不同信号,优先处理后者,处理完后者在回头处理上一个。例如正在处理sig1时,来了sig2,则会先处理sig2,等处理完sig2后,回过头继续处理sig1。
2. 解决方案
在注册的信号处理函数中,使用循环,并在循环中用waitpid来回收子进程资源。只要进入信号处理函数,那么该信号处理函数就可以把所有fork出的儿子的资源都回收掉。注意:waitpid要设置成非阻塞模式,不然当进入循环后,如果没有子进程退出时,会阻塞在信号处理函数中。
3. wait与waitpid
wait一定是阻塞模式。因此在信号处理函数中,用while1{wait(NULL)}有问题,如果进入信号处理函数后,只要有儿子不退,就会一直阻塞在这里。
waitpid可以是阻塞模式,也可以是非阻塞模式。
4. select与accept
两者在收到信号时,均会返回-1。
对于系统默认不处理的信号,程序收不到,两者当然也不会返回-1。换句话说,SIGCHLD是系统默认不处理的信号,如果不对其注册信号处理函数,当子进程退出时,select与accept也是不会返回-1的。
代码
server.c
7 #include "my_socket.h" 8 #include <sys/wait.h> 9 #include <signal.h> 10 #include <errno.h> 11 #define MY_IP "192.168.1.100" 12 #define MY_PORT 8888 13 #define SIZE 192 14 #define MSG_SIZE (SIZE - 4) 15 extern int errno ; 16 typedef struct tag_mag 17 { 18 int msg_len;//记录msg_buf的真实大小 19 char msg_buf[MSG_SIZE];//msg_buf所占空间为188byte 20 }MSG, *pMSG; 21 22 void my_handle(int num) 23 { 24 /*waitpid参数: 25 * -1表示回收每一个儿子 26 * NULL表示不关心子进程的exit的返回值 27 * WNOHANG:wait no hang 非阻塞模式 28 *waitpid返回值: 29 * -1表示没有创建任何儿子 30 * 0表示没有儿子退出 31 *大于0表示有儿子退出 32 * */ 33 while(waitpid(-1, NULL, WNOHANG ) > 0) ; 34 } 35 36 int main(int argc, char* argv[]) 37 { 38 int fd_listen , fd_client ; 39 signal(SIGCHLD, my_handle); 40 my_socket(&fd_listen, MY_TCP, MY_IP ,MY_PORT); 41 my_listen(fd_listen, 10); 42 43 while( fd_client = accept(fd_listen, NULL, NULL)) 44 { 45 /* 只要不是程序默认忽略的信号,accept都能收到,并返回-1 */ 46 if(fd_client == -1) 47 { 48 if(errno == EINTR) 49 { 50 continue ; 51 }else 52 { 53 break ; //break退出后,父亲就退了。下来儿子会由init接管。 54 } 55 }else 56 { 57 if(fork() == 0) //fork儿子用于与客户端通信 58 { 59 MSG recv_msg ; 60 int recvn; 61 while(1 ) 62 { 63 memset(&recv_msg, 0, sizeof(MSG)); 64 /*在my_socket.c中,my_recv接收的长度 与 my_send 发送的长度必须是精确值 65 * my_recv中填的长度小于等于实际要收的,是可以的,大于的话就永远退不出循环了*/ 66 my_recv(&recvn, fd_client, &recv_msg, 4); 67 if(recvn == 0) //当对面客户端退出(关闭socket),系统调用recv的返回值为0 68 { 69 break ; 70 }else 71 { 72 my_recv(NULL,fd_client, &recv_msg.msg_buf, recv_msg.msg_len); 73 my_send(NULL, fd_client, &recv_msg, 4 + recv_msg.msg_len); 74 75 } 76 } 77 close(fd_client); 78 exit(0); 79 } 80 close(fd_client); 81 } 82 } 83 return 0 ; 84 }
client.c
1 #include "my_socket.h" 2 #define MY_IP "192.168.1.100" 3 #define MY_PORT 6666 4 #define SER_IP "192.168.1.100" 5 #define SER_PORT 8888 6 #define SIZE 192 7 #define MSG_SIZE (SIZE - 4) 8 typedef struct tag_mag 9 { 10 int msg_len ; 11 char msg_buf[MSG_SIZE];//188 12 }MSG, *pMSG; 13 int main(int argc, char* argv[]) 14 { 15 int sfd ; 16 my_socket(&sfd, MY_TCP, MY_IP, MY_PORT); 17 my_connect(sfd, SER_IP, SER_PORT); 18 MSG my_msg ; 19 while(memset(&my_msg, 0, sizeof(MSG)), fgets(my_msg.msg_buf, MSG_SIZE, stdin)!= NULL) 20 { 21 my_msg.msg_len = strlen(my_msg.msg_buf); 22 my_send(NULL, sfd, &my_msg, 4 + my_msg.msg_len ); 23 memset(&my_msg, 0, sizeof(MSG)); 24 my_recv(NULL, sfd, &my_msg, 4); 25 my_recv(NULL, sfd, &my_msg.msg_buf, my_msg.msg_len); 26 printf("recv from server : %s \n", my_msg.msg_buf); 27 28 } 29 close(sfd); 30 31 }
注意:本代码由于client.c中绑定了端口,因此只能连一个客户端,读者可以自己从命令行中输入端口号或者让系统自行分配。
本范式的缺陷在于,服务端在每次收到一个客户端连接请求后,才会fork儿子进行处理,fork有时间开销。更好的方法是,服务端提前fork好儿子,即进程池。