Linux TCP 服务器编程(三):信号处理

【版权声明:转载请保留出处:blog.csdn.net/gentleliu。邮箱:shallnew*163.com】

上一节说到我们不愿意留存僵死进程,它会占用内核空间,最终可能导致资源耗尽。所以我们无论何时fork子进程时都要wait他们,以防止变成僵死进程。因此我们建立一个处理SIGCHLD信号的处理函数。

我们可以调用wait和waitpid函数来处理已经终止的子进程:

  #include<sys/types.h>
  #include<sys/wait.h>
  pid_t wait (int * status);
  pid_t waitpid(pid_t pid,int * status,int options);

这两个函数返回已经终止子进程的ID号,参数status也是返回的子进程终止状态,不关心终止状态的话可以将其设为空。

  在调用wait的进程没有已经终止的子进程时,但有一个或多个子进程在运行时,那么wait函数将阻塞到现有子进程第一个终止为止。

  今天我们来点复杂的例子---计算服务器。(哇噻!好复杂,该不会是大型并行计算服务器吧?)别着急,是这样的,我们让客户端从标准输入输入一个数字然后发送到服务器,服务器收到之后计算出这个数字的平方后在返回给客户端。

  就这样子啦!(什么?这个啊?我还以为。。。)其实我是想用简单的例子来说明要讲的问题,这样重点更为突出,更易让人明白,因为我们不是讲某个具体大型服务器编程,在讲基础性的东东。本来用简单的例子就可以说明问题干吗还要整的那么复杂吗?搞的别人把时间都花在复杂的例子上面,到最后捡了芝麻丢了西瓜,一不小心又废话了!!

修改服务器程序如下:

  #include <stdio.h>
  #include <unistd.h>
  #include <errno.h>
  #include <string.h>
  #include <sys/types.h>
  #include <sys/socket.h>
  #include <stdlib.h>
  #include <arpa/inet.h>
  #include <netinet/in.h>
  #include <sys/wait.h>
  #include <signal.h>
  
  #include "server.h"
  #include "ly_debug.h"
  
  void accept_handle(int connfd, struct sockaddr_in peeraddr);
  void sig_chld();
  
  int main(int argc, const char *argv[])
  {
      int                 sockfd, connfd;
      struct sockaddr_in  seraddr, peeraddr;
      socklen_t           addrlen;
  
      COMPILE_TIME_PRINT;
  
      if (argc != 2) {
          printf("Usage: %s <port>\n", argv[0]);
          return -1;
      }
      sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (sockfd < 0) {
          LY_ERR("socket: %s\n", strerror(errno));
          return -1;
      }
  
      seraddr.sin_family = AF_INET;
      seraddr.sin_port = ntohs(atoi(argv[1]));
      seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
  
      if (bind(sockfd, (struct sockaddr *)&seraddr, sizeof(struct sockaddr))) {
          LY_ERR("bind: %s\n", strerror(errno));
          return -1;
      }
  
      if (listen(sockfd, MAX_CONN) < 0) {
          LY_ERR("listen: %s\n", strerror(errno));
          return -1;
      }
  
      addrlen = sizeof(struct sockaddr);
      for (;;) {
          connfd = accept(sockfd, (struct sockaddr *)(&peeraddr), &addrlen);
          if (connfd < 0) {
              LY_ERR("accept: %s\n", strerror(errno));
              continue;
          }
          
          if (0 == fork()) {
              close(sockfd);
              accept_handle(connfd, peeraddr);
              close(connfd);
              exit(0);
          }
          close(connfd);
          printf("Child %d terminated\n", wait(NULL));
      }
      return 0;
  }
  
  void accept_handle(int connfd, struct sockaddr_in peeraddr)
  {
      char            buf[32];
      int             val;
  
      LY_IFO("Receive request from %s, port %d\n",
              inet_ntop(AF_INET, &peeraddr.sin_addr, buf, sizeof(buf)),
              ntohs(peeraddr.sin_port));
  
      memset(buf, 0, sizeof(buf));
      if (recv(connfd, buf, sizeof(buf), 0) < 0) {
          LY_ERR("recv: %s\n", strerror(errno));
          return;
      } 
  
      val = atoi(buf);
      val *= val;
      snprintf(buf, sizeof(buf), "%d", val);
      if (send(connfd, buf, sizeof(buf), 0) < 0) {
          LY_ERR("send: %s\n", strerror(errno));
          return;
      }
  }

客户端程序如下:
  #include <stdio.h>
  #include <unistd.h>
  #include <errno.h>
  #include <string.h>
  #include <sys/types.h>
  #include <sys/socket.h>
  #include <stdlib.h>
  #include <netinet/in.h>
  #include <arpa/inet.h>
  
  #include "ly_debug.h"
  
  int main(int argc, const char *argv[])
  {
      int                 sockfd, ret;
      struct sockaddr_in  seraddr;
      char                buf[32];
  
      COMPILE_TIME_PRINT;
  
      if (argc != 3) {
          LY_PRT("Usage: %s <ip> <port>\n", argv[0]);
          return -1;
      }
      if (inet_pton(AF_INET, argv[1], &seraddr.sin_addr) < 0) {
          LY_ERR("inet_pton: %s\n", strerror(errno));
          return -1;
      }
  
      sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (sockfd < 0) {
          LY_ERR("socket: %s\n", strerror(errno));
          return -1;
      }
  
      seraddr.sin_family = AF_INET;
      seraddr.sin_port = htons(atoi(argv[2]));
  
      if (connect(sockfd, (struct sockaddr *)(&seraddr), 
                  sizeof(struct sockaddr)) < 0) {
          LY_ERR("connect: %s\n", strerror(errno));
          return -1;
      }
  
      fprintf(stdout, "Input a number: ");
      if (NULL == fgets(buf, sizeof(buf), stdin)) {
          LY_ERR("fgets failed!\n");
          return -1;
      }
      if (send(sockfd, buf, sizeof(buf), 0) < 0) {
          LY_ERR("send: %s\n", strerror(errno));
      }
  
      memset(buf, 0, sizeof(buf));
      ret = recv(sockfd, buf, sizeof(buf), 0);
      //ret = read(sockfd, buf, sizeof(buf));
      if (ret < 0) {
          LY_ERR("recv: %s\n", strerror(errno));
          return -1;
      } else if (ret > 0) {
          LY_PRT("Result: %s\n", buf);
      } else {
          LY_PRT("Peer shutdown!\n");
      }
  
      close(sockfd);
      return 0;
  }

编译后服务器先运行,然后先后运行三个客户端,,且不忙向服务器发送数字,发现运行第二个和第三个客户端时,服务器未打印welcome信息,直到服务器处理完第一个客户端请求之后才打印第二个客户端的欢迎信息,而第三个客户的欢迎信息也在服务器处理完第二个客户请求后才打印。此时ps查看一下进程信息,没有发现僵尸进程。但是这样作肯定不行,因为之前说了wait函数将阻塞到现有子进程第一个终止为止,这样的服务器和我们最开始写的迭代服务器一个效率了。

  那怎么办呢?别着急,我们还有个waitpid函数呢,那这个函数和wait函数有什么不同呢,这里关键在waitpid的第三个参数。先说第一个参数,pid是允许我们指定想等待的子进程ID ,-1表示等待第一个终止的进程。第三个参数Options允许我们指定附加选项,最常用的是WNOHANG,他告知内核在没有终止子进程时不要阻塞,那这不正是我们需要的吗?于是立马改代码,将服务器中for循环修改如下:

  for (;;) {
          connfd = accept(sockfd, (struct sockaddr *)(&peeraddr), &addrlen);
          if (connfd < 0) {
              LY_ERR("accept: %s\n", strerror(errno));
              continue;
          }
          
          if (0 == fork()) {
              close(sockfd);
              accept_handle(connfd, peeraddr);
              close(connfd);
              exit(0);
          }
          close(connfd);
          //printf("Child %d terminated\n", wait(NULL));
          printf("Child %d terminated\n", waitpid(-1, NULL, WNOHANG));
      }
如上编译运行, 马上发现结果不对,服务器由于未等待子进程终止直接返回导致当子进程退出时父进程未处理子进程而导致僵尸进程出现了,如下图:

所以,我们专门最好写个函数来处理SIGCHLD 信号,这个函数如下:

  void sig_chld()
  {
      pid_t   pid;
  
      while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
          printf("Child %d terminated\n", pid);
      }
  }

于是我们今天完整的服务器代码出来了:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>

#include "server.h"
#include "ly_debug.h"

void accept_handle(int connfd, struct sockaddr_in peeraddr);
void sig_chld();

int main(int argc, const char *argv[])
{
    int                 sockfd, connfd;
    struct sockaddr_in  seraddr, peeraddr;
    socklen_t           addrlen;

    COMPILE_TIME_PRINT;

    if (argc != 2) {
        printf("Usage: %s <port>\n", argv[0]);
        return -1;
    }
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        LY_ERR("socket: %s\n", strerror(errno));
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = ntohs(atoi(argv[1]));
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(sockfd, (struct sockaddr *)&seraddr, sizeof(struct sockaddr))) {
        LY_ERR("bind: %s\n", strerror(errno));
        return -1;
    }

    if (listen(sockfd, MAX_CONN) < 0) {
        LY_ERR("listen: %s\n", strerror(errno));
        return -1;
    }

    signal(SIGCHLD, sig_chld);

    addrlen = sizeof(struct sockaddr);
    for (;;) {
        connfd = accept(sockfd, (struct sockaddr *)(&peeraddr), &addrlen);
        if (connfd < 0) {
            LY_ERR("accept: %s\n", strerror(errno));
            continue;
        }
        
        if (0 == fork()) {
            close(sockfd);
            accept_handle(connfd, peeraddr);
            close(connfd);
            exit(0);
        }
        close(connfd);
        //printf("Child %d terminated\n", wait(NULL));
        //printf("Child %d terminated\n", waitpid(-1, NULL, WNOHANG));
    }
    return 0;
}

  void accept_handle(int connfd, struct sockaddr_in peeraddr)
  {
      char            buf[32];
      int             val;
  
      LY_IFO("Receive request from %s, port %d\n",
              inet_ntop(AF_INET, &peeraddr.sin_addr, buf, sizeof(buf)),
              ntohs(peeraddr.sin_port));
  
      memset(buf, 0, sizeof(buf));
      if (recv(connfd, buf, sizeof(buf), 0) < 0) {
          LY_ERR("recv: %s\n", strerror(errno));
          return;
      } 
  
      val = atoi(buf);
      val *= val;
      snprintf(buf, sizeof(buf), "%d", val);
      if (send(connfd, buf, sizeof(buf), 0) < 0) {
          LY_ERR("send: %s\n", strerror(errno));
          return;
      }
  }

void sig_chld()
{
    pid_t   pid;

    while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
        printf("Child %d terminated\n", pid);
    }
}

如上运行,结果如预期一样,并行处理,没有阻塞,没有僵尸进程。

Linux TCP 服务器编程(三):信号处理_第1张图片

下面我们做这样一个测试,运行一个服务器,然后运行一个客户端,客户端阻塞与标准输入,此时kill掉服务进程,看看会发生什么。发现有不对头的地方,有木有啊!

服务器已经被kill之后,客户端由于阻塞与标准输入,不能马上知道,只有当客户端输入后回车才发现对端已经不在了。这个问题涉及到i/o复用,在下一节我们继续。

你可能感兴趣的:(编程,linux,tcp,服务器,并行处理)