从select引起的bug聊聊多路复用二 poll

一 前言

select最多支持1024个连接,且连接的文件描述符的最大不能超过1024个,如果程序打开了很多文件,或用了2MB这种大页内存,可能会导致打开的文件超过1024,从而使unix socket 产生莫名其妙的问题,poll这套IO多路复用机制和select的用法很像,采用链表而不是采用位图的方式,突破了1024个套接字的限制,本文就是用poll重新实现前篇的功能。

二 poll

2.1 poll的API说明

poll函数原型:


int poll(struct pollfd *fds, unsigned long nfds, int timeout); 
   
返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1

struct pollfd定义如下:

struct pollfd {
    int    fd;       /* file descriptor */
    short  events;   /* events to look for */
    short  revents;  /* events returned */
 };

fd 文件描述符;
events 要检测的事件类型;
revents 指的是返回事件类型,可以设置多个;
timeout 设置为负数表示永久等待; 如果是0 ,表示不阻塞立刻返回; 如果大于0的数值表示 poll 调用方等待指定的毫秒数后返回。
比select的优点是不用每次都设置文件描述符。

可读事件类型:

#define POLLIN     0x0001    /* any readable data available */
#define POLLPRI    0x0002    /* OOB/Urgent readable data */
#define POLLRDNORM 0x0040    /* non-OOB/URG data available */
#define POLLRDBAND 0x0080    /* OOB/Urgent readable data */

可写事件类型:


#define POLLOUT    0x0004    /* file descriptor is writeable */
#define POLLWRNORM POLLOUT   /* no write type differentiation */
#define POLLWRBAND 0x0100    /* OOB/Urgent data can be written */

错误事件类型定义:


#define POLLERR    0x0008    /* 一些错误发送 */
#define POLLHUP    0x0010    /* 描述符挂起*/
#define POLLNVAL   0x0020    /* 请求的事件无效*/

2.2 采用poll改成服务器代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define CLIENT_SIZE 100
#define SOCK_FILE "command.socket"
#define TOO_MANY "Too many client."

typedef struct unix_socket_infos_ {
  int socket;
  struct pollfd event_sets[CLIENT_SIZE];
  struct sockaddr_un client_addr;
} unix_socket_infos_t;

static int create_unix_socket(unix_socket_infos_t *this) {
  struct sockaddr_un addr;
  addr.sun_family = AF_UNIX;
  strncpy(addr.sun_path, SOCK_FILE, sizeof(addr.sun_path));
  addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
  int len = strlen(addr.sun_path) + sizeof(addr.sun_family) + 1;

  int listen_socket = socket(AF_UNIX, SOCK_STREAM, 0);
  if (listen_socket == -1) {
    perror("create socket error.\n");
    return -1;
  }
  int on = 1;
  /* set reuse option */
  int ret = setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&on,
                       sizeof(on));
  unlink(SOCK_FILE);
  /* bind socket */
  ret = bind(listen_socket, (struct sockaddr *)&addr, len);
  if (ret == -1) {
    perror("bind error.\n");
    return -1;
  }
  printf("start to listen\n");
  ret = listen(listen_socket, 1);
  if (ret == -1) {
    perror("listen error\n");
    return -1;
  }
  ret = chmod(SOCK_FILE, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
  if (ret == -1) {
    perror("chmod error\n");
    return -1;
  }
  this->socket = listen_socket;
  return 1;
}

static int close_client(unix_socket_infos_t *this, int index) {
  int client = this->event_sets[index].fd;
  close(client);
  this->event_sets[index].fd = -1;
}

static int deal_client(unix_socket_infos_t *this, int index) {
  char buffer[1024] = {0};
  int ret = recv(this->event_sets[index].fd, buffer, sizeof(buffer) - 1, 0);
  if (ret <= 0) {
    if (ret == 0) {
      printf("lost connect.\n");
    } else {
      printf("recv error:%s \n", strerror(errno));
    }
    close_client(this, index);
    return -1;
  }
  if (ret < sizeof(buffer) - 2) {
    buffer[ret] = '\n';
    buffer[ret + 1] = 0;
  }
  fprintf(stderr, "client[%d]:%s", this->event_sets[index].fd, buffer);
  ret = send(this->event_sets[index].fd, buffer, strlen(buffer), MSG_NOSIGNAL);
  if (ret < 0) {
    perror("send error:");
  } else {
    fprintf(stderr, "server:%s", buffer);
  }
  return 1;
}

static int accept_client(unix_socket_infos_t *this ) {
  socklen_t len = sizeof(this->client_addr);
  char buffer[1024] = {0};
  int client = accept(this->socket, (struct sockaddr *)&(this->client_addr), &len);
  printf("client to comming:%d\n", client);
  if (client < 0) {
    perror("accept error\n");
    return -1;
  }
  memset(buffer, 0x0, 1024);
  int ret = recv(client, buffer, sizeof(buffer) - 1, 0);
  if (ret < 0) {
    perror("recv error\n");
    return -1;
  }
  if (ret < sizeof(buffer) - 2) {
    buffer[ret] = '\n';
    buffer[ret + 1] = 0;
  }
  fprintf(stderr, "client[%d][first]:%s", client, buffer);
  ret = send(client, buffer, strlen(buffer), MSG_NOSIGNAL);
  if (ret < 0) {
    perror("send error\n");
  } else {
    fprintf(stderr, "server[first]:%s", buffer);
  }
  int is_set = 0;
  for (int i = 0; i < CLIENT_SIZE; i++) {
    if (this->event_sets[i].fd < 0) {
      this->event_sets[i].fd = client;
      this->event_sets[i].events =  POLLRDNORM;
      is_set = 1;
      break;
    }
  }
  if (is_set == 0) {
    fputs(TOO_MANY, stdout);
    close(client);
    return -1;
  }
  return 1;
}

static int run_poll(unix_socket_infos_t *this) {
  struct timeval tv;
  int ret;
  tv.tv_sec = 0;
  tv.tv_usec = 200 * 1000;
  int ready_number;

  
  this->event_sets[0].fd = this->socket;
  this->event_sets[0].events = POLLRDNORM;

  while (1) {
    if ((ready_number = poll(this->event_sets, CLIENT_SIZE, -1)) < 0) {
      perror("poll error.");
    }
    if (this->event_sets[0].revents & POLLRDNORM) {
      accept_client(this);
      // 只有一个准备好的文件描述符
      if (--ready_number <= 0)
        continue;
    }
    for (int i = 1; i < CLIENT_SIZE; i++) {
      int socket_fd;
      if ((socket_fd = this->event_sets[i].fd) < 0) {
           continue;
      }

      if (this->event_sets[i].revents & (POLLRDNORM | POLLERR)) {
          deal_client(this, i);
          if (--ready_number <= 0)
            break;
      }

    }
  }
}

int main(int argc, char **argv) {
  unix_socket_infos_t unix_socket_infos;
  for (int i = 0; i < CLIENT_SIZE; i++) {
    unix_socket_infos.event_sets[i].fd = -1;
  }
  int ret = create_unix_socket(&unix_socket_infos);
  printf("start to loop\n");
  run_poll(&unix_socket_infos);
  return 0;
}

代码比select更简单,而且没有1024的限制。
poll 相比select 不用每次都重新设置监听的文件描述符,将事件和文件描述符分开,所以不用很麻烦。
内核代码里面除了将select的位图改成链表外,其他的大差不差,同样是循环调用文件描述符的对应文件的poll方法,如下:

mask = f.file->f_op->poll(f.file, pwait);

将获取的mask设置到返回的事件中。代码和select一样,都在select.c中。

三 套接字是否要设置为非阻塞模式

在上面的代码中,我们没有对监听的socket 做特殊的设置,这就可能存在问题,举个简单的例子,
如果客户端和服务器端连接后,客户端发送RST报文给服务器端,服务器端内核里面将全连接从全连接的队列中删除,而服务器端从poll中返回,如果不能及时的调用accept,导致全连接队列的客户端连接又恰好被内核删除了,导致再调用accept不能返回。

所以一般建议将监听的socket设置为非阻塞模式:

fcntl(fd, F_SETFL, O_NONBLOCK);

其实其他的客户端来的socket的监听,用阻塞模式仍然可能存在问题,像write,如果写的数据多,而对方读的慢导致发送窗口为0而阻塞,单线程被阻塞了,没有在poll函数中,从而如果有其他客户端来连接的会,就需要等待,从而影响性能(这个没试验出来,缓冲区够大)。

你可能感兴趣的:(从select引起的bug聊聊多路复用二 poll)