增加信號處理的回射服務器

問題:由上一篇文章分析可知,若客戶端進程終止,則服務器子進程會變成殭屍進程。原因是由於服務器子進程退出後,服務器父進程未獲取子進程的狀態信息,導致子進程變成殭屍進程。

解決方法:由於子進程退出會產生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.當捕獲信號時,必須處理被中斷的系統調用



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章