7.并发服务器 以及 与之对应的客户端二

7.并发服务器 以及 与之对应的客户端2

之前程序存在着一个不确定性的因素。之前服务器代码中。子进程在死亡后,会向父进程发送SIGCHLD信号。这个信号会被父进程捕获,然后父进程调用wait函数对死亡子进程处理防止其变成僵尸进程(僵尸进程会一直占用内存资源在它被回收之前。如果父进程在死亡之前没有回收僵尸进程,那么在unix下,僵尸进程会被init进程托管并回收。无主进程都会被托管给init)。

 

执行之前的代码,然后执行ps -aux可以看到不会有服务器派生的子进程成为僵死进程,但如果将客户端代码改成

[root@liumengli net]# cat echo_client2.c
#include "/programe/net/head.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"

int main(int argc, char ** argv) {
        int sockfd[5], i;
        struct sockaddr_in serv_socket;

        for(i = 0; i < 5; i++) {
                sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);
                bzero(&serv_socket, sizeof(serv_socket));
                serv_socket.sin_family = AF_INET;
                serv_socket.sin_port = htons(atoi(argv[1]));
                inet_pton(AF_INET, "192.168.1.235", &serv_socket.sin_addr);
                connect(sockfd[i], (struct sockaddr *)&serv_socket, sizeof(serv_socket));
        }
        getchar();
        exit(0);
}

 

这个代码也很简单,只是一次对服务器进行了5次链接,而由此导致服务器会产生5个子进程为其服务,当我们在客户端按下回车,终止客户端进程后。再到服务器上输入ps -aux会看到有4个僵尸进程(一般是4个)。产生的原因是因为:unix不会对信号排队。

 

所谓信号不排队,在这里是指,当子进程SIGHCLD到达时,父进程开始响应这个信号,并作出处理。同时会关闭对SIGCHLD的响应,在处理过程中到达的SIGHCLD信号不会被响应,但也不会排队等待,信号会被丢失。

 

之前5个SIGCHLD信号几乎是同时到达,其中之一个被响应,而其它信号则会丢失,从而导致只调用了一次处理函数,剩下的进程就会变成僵尸进程。

 

要想解决这个问题,首先要看wait函数和waitpid这2者之间的区别。(当然也可以从信号不排队处着手,但这貌似难度很大)。

 

pid_t wait(int * statloc)

返回终止进程的pid和状态,状态存在于statloc中,可以通过sys/wait.h定义的宏操作获取。当父进程调用wait时候,如果所有的子进程都在运行,则父进程会被阻塞(当然,这里我们是在收到SIGCHLD信号后才调用,所以不会被阻塞)。如果没有子进程则wait会出错并立即返回。

我们再次分析之前的服务器情况,当子进程SIGCHLD信号到达时候,父进程开始调用my_op函数开始处理僵尸子进程。但此时共有5个子进程死亡,并有5个SIGCHLD到达,其它4个信号丢失,wait只是处理其中一个子进程从而有4个僵尸进程遗留。

 

当我们采用waitpid则可以用适当的手段解决这类问题。

pid_t waitpid(pid_t pid,  int * statloc, int options)

返回终止的子进程的pid。参数pid == -1时候等待任意子进程,pid > 0或pid < 0等待与pid绝对值相同的子进程。pid == 0等待其组ID和调用者组ID相同的任一进程。statloc同上。options ==  WNOHANG若由PID等待的子进程并不立即可用,waitpid不等待,立即返回0.另一个不多做解释。

 

当我们把my_op函数改成

void my_op(int signum, siginfo_t * info, void * myact) {
        pid_t pid;
        int stat;

        while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
                printf("child %d terminated\n", pid);

        return;
}

就可以有效避免因信号不排队到导致的某些僵尸子进程无法回收的不可预料因素。当然信号丢失的情况仍在发生,但我们关心的是僵尸子进程的回收,这里只是通过其它办法回收了僵尸子进程。

分析这段函数,当这个函数被调用时候,也就是收到了SIGCHLD信号,也就是有子进程死亡。不管是多个还是1个信号到达。执行到循环体,就会将所有僵尸子进程回收后,退出。当然,极端情况当循环体执行完后,所有僵尸进程回收结束,进入return时候,此时也有可能有SIGCHLD到达,这并不要紧,因为即使这次没有回收,等下次有子进程发出SIGCHLD信号时,上次遗留的依然会被清除。

 

其实此时服务器代码还可以改成,不加载信号处理代码,直接将循环体写到创建子进程的循环内部,当然不能写到子进程代码里面。当然此时,所有子进程死亡不会引起waitpid被调用,而是当下次子进程创建时候,遇到这段代码,则上次死亡的僵尸进程会被回收。这种方式没有加载信号处理的方式有效。