09 IO多路复用 Select/Poll/Epoll介绍

在linux的世界里,一切皆文件。每一个进程都拥有一张文件描述符的表,指向文件、socket、硬件还有一些操作系统对象。

典型的拥有很多IO源的系统都会有一个初始化的阶段,然后进入待机模式——等待客户端请求并且响应。

最简单的解决方案就是为每一个客户端创建一个线程(或者进程),一直阻塞直到请求发送或者写入了响应。这种模式在客户端数量很小的时候可以工作,但是我们想要扩展到成千上万的客户端,为每个客户端创建线程(或者进程)是一个很糟糕的主意。

IO多路复用

问题的解决方案是使用内核机制去轮询一系列的文件描述符。在linux系统下,主要有以下三种选择:

  • select(2)
  • poll(2)
  • epoll

以上三种方法的思想都是一致的,新建一系列的文件描述符,告诉内核你对每一个描述符的操作,然后使用线程去阻塞一个函数调用直到至少有一个文件描述符请求的操作是可用的。

Select系统调用

select()系统调用提供了一种实现同步IO多路复用的方法。

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

一个对select()的调用会阻塞直到指定的文件描述符已经准备进行IO了,或者指定的超时时间到了。

监控的文件集合分成三类:

  • readfds文件描述符集合是设置为监控数据是否可读的。
  • writefds文件描述符集合是设置为监控写入数据是否完成而没有阻塞。
  • exceptsfds设置为监控是否有异常发生或者带外(out-of-band)数据可用(一般只用于sockets)。

监控的集合可以为NULL,这种情况下select不会监控对应事件。

在一个成功的返回,有且只有已经准备好IO的对象会被添加至对应集合.
样例:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <wait.h>
#include <signal.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
 
#define MAXBUF 256
 
void (void)
{
  sleep(2);
  char msg[MAXBUF];
  struct sockaddr_in addr = {0};
  int n, sockfd,num=1;
  srandom(getpid());
  
  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去检测最大的fd数量。

主循环创建了一个所有文件描述符的集合,调用select然后检查哪个文件描述符已经准备被读取了。为了简单起见,我们不加任何的错误检测。

返回后,select只改变那些文件描述符已经就绪的集合,因此我们需要在每一次迭代构建当前的集合。

我们需要告诉select所有集合中文件描述符(以下简称为fd)的最高数量的原因是由于fd_set的内部实现机制。每一个fd是被一个bit声明的,因此fd_set是一个32个整型的数组(32*32=1024bit)。这个函数检测所有bit去观察是否其集合已经到达了最大值。这意味着如果我们有五个fd,但是其最高的数值是900,这个函数会检测从0到900的所有bit去找到需要监控的fd。另外select有一个POSIX实现——pselectpselect使在等待的时候使用一个掩码。

Select 总结

  • 我们需要在每一次调用之前构造每一个集合。
  • select函数需要检测到最高位往前的任何bit——O(n)。
  • 我们需要遍历整个fd集合去检测指定集合是否有返回。
  • select的主要优势就是可移植性强,几乎所有的类unix系统都支持它。

Poll 系统调用

select函数的低效的三个位掩码fd集合不一样,poll提供了一个单独的n个pollfd的结构体数组,函数声明如下:

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

pollfd结构体的对于不同的事件和事件的返回有不同的成员,所以我们不需要每一次都构建它。

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

对于每一个fd构造一个pollfd的对象然后填充其要求的事件。在poll返回之后检测事件变量的值。

用poll去改变上面的例子:

for (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);
    }
 }
}

像我们用selcet做的那样,我们需要检测每一个pollfd对象去观察它的fd是否已经准备好了,但是我们不需要每一次迭代都构建集合。

Poll vs Select

  • poll()不要求用户计算fd的最高值+1。
  • poll()在处理大数值的fd时更高效。想象一下,当你通过select只监听一个fd,但它的数值是900的时候,你需要检测每一个集合的从0到900的每一个bit。
  • select的fd集合是静态的大小。
  • 使用selcect,fd集合会在返回的时候重新构建,因此随后的调用都需要重新初始化它。而poll系统调用将输入和输出分离,允许数组在没有改变的情况下复用。
  • selecttimeout参数在返回的时候是为定义的,可移植的代码需要重新初始化它。pselect没有这个问题。
  • select移植性更好,有一些类unix系统不支持poll

Epoll系统调用

当我们使用selectpoll的时候,我们在用户空间处理所有的事情,然后我们每一个调用都发送fd集合然后等待。为了增加另外的socket,我们需要增加它到集合里并且重新调用select/poll。

epoll系统调用帮助我们创建并管理在内核里的context,我们将任务分成三步。

  • 使用epoll_create创建一个在内核的context。
  • 使用epoll_ctl从内核中增加或移除fd。
  • 使用epoll_wait等待内核中的事件。

让我们把上面的例子改为epoll实现的:

struct epoll_event events[5];
  int epfd = epoll_create(10);
  ...
  ...
  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);
    
    for(i=0;i<nfds;i++) {
        memset(buffer,0,MAXBUF);
        read(events[i].data.fd, buffer, MAXBUF);
        puts(buffer);
    }
  }

我们首先创建了一个context(参数会被忽略但是必须是正数)。当一个客户端连接时,我们创建一个epoll_event对象然后把它添加到context,然后死循环里我们只等待context。

Epoll vs select/poll

  • 我们能够在等待的时候添加或者移除fd。
  • epoll_wait只会返回已经就绪的对象。
  • epoll有更好的性能——O(1) vs O(n)。
  • epoll可以水平触发和边缘触发。
  • epoll是linux特有的所以不可移植。

转载自:大专栏 英文版

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

推荐阅读更多精彩内容