select/poll/epool linux多路复用解析

Linux(实际上市Unix)的一个基本概念是Unix/Linux中一切都是文件。每个进程都有一个指向文件,套接字,设备或其他操作系统对象的文件描述符

与许多IO源一起工作的典型系统都要经历一个初始化阶段,然后进入某种待机模式------等待客户端发送请求并响应


image.png

linux下有3种方案轮训一组文件描述符.

  • select
  • poll
  • epoll

Select系统调用

select()系统调用提供了一种实现同步多路复用 I/O 的机制。

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

对 select()的调用会阻塞,直到给定的文件描述符准备好执行 I/O 操作,或者到达了可选的指定超时时间。

被监视的文件描述符被分成三组:

readfds 集合中列出的文件描述符将被监视,以查看数据是否可用于读取;
writefds 集合中列出的文件描述符将被监视,以查看写入操作是否会在没有阻塞的情况下完成;
exceptfds 集合中的文件描述符将被监视,以查看是否发生了异常,或者带外数据是否可用(这些状态仅适用于套接字)。

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
 
#define MAXBUF 256
 
void child_process(void)
{
  sleep(2);
  char msg[MAXBUF];
  struct sockaddr_in addr = {0};
  int n, sockfd,num=1;
  srandom(getpid());
  /* Create socket and connect to server */
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = inet_addr("127.0.0.1");
 
  connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
 
  printf("child {%d} connected \n", getpid());
  while(1){
        int sl = (random() % 10 ) +  1;
        num++;
        sleep(sl);
    sprintf (msg, "Test message %d from client %d", num, getpid());
    n = write(sockfd, msg, strlen(msg));    /* Send message */
  }
 
}
 
int main()
{
  char buffer[MAXBUF];
  int fds[5];
  struct sockaddr_in addr;
  struct sockaddr_in client;
  int addrlen, n,i,max=0;;
  int sockfd, commfd;
  fd_set rset;
  for(i=0;i<5;i++)
  {
    if(fork() == 0)
    {
        child_process();
        exit(0);
    }
  }
 
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  memset(&addr, 0, sizeof (addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = INADDR_ANY;
  bind(sockfd,(struct sockaddr*)&addr ,sizeof(addr));
  listen (sockfd, 5); 
 
  for (i=0;i<5;i++) 
  {
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    fds[i] = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    if(fds[i] > max)
        max = fds[i];
  }
  
  while(1){
    FD_ZERO(&rset);
    for (i = 0; i< 5; i++ ) {
        FD_SET(fds[i],&rset);
    }
 
    puts("round again");
    select(max+1, &rset, NULL, NULL, NULL);
 
    for(i=0;i<5;i++) {
        if (FD_ISSET(fds[i], &rset)){
            memset(buffer,0,MAXBUF);
            read(fds[i], buffer, MAXBUF);
            puts(buffer);
        }
    }   
  }
  return 0;
}

我们从创建 5 个子进程开始,每个进程连接到服务器并将消息发送到服务器,服务器进程使用accept(2) 为每个客户端创建不同的文件描述符,select(2) 中的第一个参数应该是三个集合中编号最大的文件描述符,再加上 1,就可以知道最大的文件描述符编号。

主无限循环创建一组所有文件描述符,调用 select 和 on 返回检查哪个文件描述符已准备好读取,为了简单起见,没有添加错误检查。

返回时,选择将该集合更改为仅包含准备好的文件描述符,因此我们需要在每次迭代中重新构建集合。

Select – summary:

  • 我们需要在每次调用之前构建每组集合;
  • 这个函数检查任何 bit 到更高的数字 —— O(n);
  • 我们需要遍历文件描述符来检查它是否存在于从 select() 返回的集合中;
  • select 的主要优点在于它的可移植性 —— 每个类 unix 操作系统的都有。

Poll 系统调用

不像 select() 低效的三个基于位掩码的文件描述符集合,poll() 采用了一个 nfds pollfd 结构的单个数组,函数原型更简单:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

pollfd 结构对事件和返回事件有不同的字段,所以我们不需要每次都创建它:

struct pollfd {
      int fd;
      short events; 
      short revents;
};

修改上面的例子:


#include <errno.h>
#include <netinet/in.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAXBUF 256

void child_process(void)
{
  sleep(2);
  char msg[MAXBUF];
  struct sockaddr_in addr = {0};
  int n, sockfd, num = 1;
  srandom(getpid());
  /* Create socket and connect to server */
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = inet_addr("127.0.0.1");

  connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));

  printf("child {%d} connected \n", getpid());
  while (1)
  {
    int sl = (random() % 10) + 1;
    num++;
    sleep(sl);
    sprintf(msg, "Test message %d from client %d", num, getpid());
    n = write(sockfd, msg, strlen(msg)); /* Send message */
  }
}

int main()
{
  char buffer[MAXBUF];
  int fds[5];
  struct sockaddr_in addr;
  struct sockaddr_in client;
  int addrlen, n, i, max = 0;
  ;
  int sockfd, commfd;
  fd_set rset;
  for (i = 0; i < 5; i++)
  {
    if (fork() == 0)
    {
      child_process();
      exit(0);
    }
  }

  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = INADDR_ANY;
  bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
  listen(sockfd, 5);

  struct pollfd pollfds[5];
  for (int i = 0; i < 5; i++)
  {
    memset(&client, 0, sizeof(client));
    addrlen = sizeof(client);
    pollfds[i].fd = accept(sockfd, (struct sockaddr *)&client, &addrlen);
    pollfds[i].events = POLLIN;
  }
  sleep(1);
  while (1)
  {
    puts("round again");
    poll(pollfds, 5, 50000);

    for (i = 0; i < 5; i++)
    {
      if (pollfds[i].revents & POLLIN)
      {
        pollfds[i].revents = 0;
        memset(buffer, 0, MAXBUF);
        read(pollfds[i].fd, buffer, MAXBUF);
        puts(buffer);
      }
    }
  }
  return 0;
}

就像使用 select 所做的那样,我们需要检查每个 pollfd 对象,看看它的文件描述符是否准备好,但不需要在每次迭代时构建集合。

Poll vs Select

poll() 不要求用户计算编号最高的文件描述符 +1 的值;
poll() 对于大值文件描述符更有效。假设我们通过 select() 方法监视一个值为 900 的单个文件描述符 —— 内核将不得不检查传入集合的每个值的每一位,直到第 900 位;
select() 的文件描述符集合是静态大小的;
使用 select(),文件描述符集合会在返回时重建,因此每个后续调用都必须重新初始化它们。 poll() 系统调用将输入(events 字段)与输出(revents 字段)分隔开,允许在不更改的情况下重新使用该数组。
返回时,select() 的 timeout 参数未定义。 可移植性代码需要重新初始化它,这不是pselect() 的问题;
select() 更具可移植性,因为某些 Unix 系统不支持 poll()。

Epoll 系统调用

在使用 select 和 poll 时,我们管理用户空间上的所有内容,并在每个调用上发送集合以等待,要添加另一个套接字,我们需要将它添加到集合中并再次调用 select/poll。

Epoll* 系统调用帮助我们创建和管理内核中的上下文,我们将任务分为 3 个步骤:

  • 使用 epoll_create 在内核中创建一个上下文;
  • 使用 epoll_ctl 向/从上下文添加/移除文件描述符;
  • 使用 epoll_wait 等待上下文中的事件。

将上面的示例改用 epoll:


#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/epoll.h>
#define MAXBUF 256

void child_process(void)
{
  sleep(2);
  char msg[MAXBUF];
  struct sockaddr_in addr = {0};
  int n, sockfd, num = 1;
  srandom(getpid());
  /* Create socket and connect to server */
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = inet_addr("127.0.0.1");

  connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));

  printf("child {%d} connected \n", getpid());
  while (1)
  {
    int sl = (random() % 10) + 1;
    num++;
    sleep(sl);
    sprintf(msg, "Test message %d from client %d", num, getpid());
    n = write(sockfd, msg, strlen(msg)); /* Send message */
  }
}

int main()
{
  char buffer[MAXBUF];
  struct sockaddr_in addr;
  struct sockaddr_in client;
  int addrlen, n, i, max = 0;
  ;
  int sockfd, commfd;
  fd_set rset;
  for (i = 0; i < 5; i++)
  {
    if (fork() == 0)
    {
      child_process();
      exit(0);
    }
  }

  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = INADDR_ANY;
  bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
  listen(sockfd, 5);

struct epoll_event events[5];
int epfd = epoll_create(10);
int nfds;

for (i=0;i<5;i++) 
  {
    static struct epoll_event ev;
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    ev.data.fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); 
  }
  
  while(1){
    puts("round again");
    nfds = epoll_wait(epfd, events, 5, 10000);
    printf("ngfs = {%d}  \n", nfds);
    for(i=0;i<nfds;i++) {
            memset(buffer,0,MAXBUF);
            read(events[i].data.fd, buffer, MAXBUF);
            puts(buffer);
    }
  }

  return 0;
}

Epoll vs Select/Poll

  • 我们可以在等待时添加或删除文件描述符;
  • epoll_wait 仅返回具有准备文件描述符的对象;
  • epoll 有更好的性能 —— O(1) 而不是O(n);
  • epoll 可以表现为级别触发或边缘触发(请参见手册页);
  • epoll 是 Linux 特有的,因此可移植性一般。

参考:
https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll/#.XwMCHJMzZTY

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352