linux select/epoll

一、Unix/Linux网络IO模型
在经典的Unix网络编程中,总结出了5种不同的网络IO模型,分别是阻塞式IO,非阻塞IO, IO多路复用,信号驱动IO,以及异步IO模型。

  1. 阻塞式IO


    image.png
  2. 非阻塞式IO


    image.png
  3. IO多路复用


    image.png
  4. 信号驱动式IO


    image.png
  5. 异步IO模型


    image.png

二、为什么IO多路复用应用最广泛
理论上来说异步IO模型性能更好,但是目前阶段在linux平台下,操作系统底层并没有真正实现完全异步IO,当然有可能在未来版本中会支持
而对于信号驱动IO,因为信号没有附加信息,如果一个信号源有多种产生信号的原因,信号接受者就无法区分,但是TCP协议里的事件类型有多种(read, write, accept),另外一个原因是如果基于java语言开发的话,似乎还不支持信号驱动处理。
然后是非阻塞IO, 这个虽然避免了IO阻塞,但是需要不断的主动轮询,浪费CPU资源,效率不高
阻塞式IO的场景一般对每个连接分配一个线程,但是当连接数太大的情况下(比如c10k,c100k),系统不可能创建这么多的线程。(或许协程在某种程度能改善这个问题)
所以最终一比较,至少在linux平台下,目前主流的方案大多基于IO多路复用技术。

linux平台提供的主要的IO多路复用技术有select, poll, epoll,主要目的是为了能让一个或少量的select线程(或reactor线程),来管理多个连接,本质上是基于一个真实生产环境中的特性,比如虽然存在几万个连接,但是在某一时间范围内,有数据可读或者可写的socket并不会很多,既active的连接不会很多。当然如果在一个极端环境下面,比如是一个高速的局域网,并且每个client连接都会一直不断的发送数据,既每个连接都可以看成active的连接,那么基于IO多路复用技术未必是最佳方案。

三、select
linux系统提供select函数来实现多路复用输入/输出模型,select系统调用是用来让我们的应用程序监视多个文件句柄(包括socket文件句柄)的状态变化,程序会在select这里等待,直到被监视的文件句柄有一个或多个发生了状态改变如可读或可写

API原型为:

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

参数说明:
n 一般为最大文件描述符 + 1, 既STDIN_FILENO + 1 该值会有limit限制,一般为1024,所以生产环境基本用epoll,kqueue等代替
readfds 监视是否有可读的文件描述符集合
writefds 监视是否有可写的文件描述符集合
exceptfds 监视是否有异常情况发生的文件描述符集合
timeout 超时时间,如果在某一段时间内依然没有相应事件触发,则会阻塞直到timeout时间过期 timeout.tv_sec单位为秒, timeout.tv_usec单位为微秒

四、poll
poll算select的加强版,但基本原理跟select类似,暂不赘述,后续在补充

五、epoll
由于select和poll系统调用存在以下几个问题,Linux内核2.6环境新增Event Poll的方式。

  1. select/poll每次检查的时候是通过遍历所有的文件描述符(fd), 尤其是对于网络scoket而言,大部分存在这么一个特性,既某一个时间点里,只有很少一部分的socket是“活跃”状态,如果每次都是遍历所有的网络文件描述符的话,当文件描述符变大之后,性能就会随着连接数变大之后线型下降

  2. select存在最大文件描述符的限制,具体取决于常量FD_SETSIZE,默认大小为1024, 不能满足大量的客户端连接.

反观epoll,则改进了以上不足的地方

  1. 在内核实现中epoll是根据每个fd上面的callback函数实现的。可以做到只有“活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会

  2. epoll没有对fd描述符有限制,理论上取决于系统内存大小, 可以通过命令 cat /proc/sys/fs/file-max查看,大概1G内存可以创建10w个连接

  3. epoll的具体实现使用mmap加速内核与用户空间的消息传递,进一步提高性能

epoll实际包含3个系统调用组成,分别为epoll_create(), epoll_ctl(), epoll_wait()

  1. epoll_create用于创建epoll的实例,其中参数size只要大于0即可,内核会动态获取大小,函数返回epoll本身的描述符
int epoll_create(int size);   
  1. epoll_ctl用于添加,修改,删除要监听的event事件
    参数op为EPOLL_CTL_ADD代表添加,EPOLL_CTL_MOD代表修改,EPOLL_CTL_DEL代表删除
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);    
  1. epoll_wait用于监视等待是否有IO事件发生,直到timeout过期
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);   

epoll中存在两种工作模式 LT 和 ET
二者的差异在于 level-trigger (LT) 模式下只要某个 socket 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 socket;而 edge-trigger (ET) 模式下只有某个 socket 从 unreadable 变为 readable 或从unwritable 变为 writable 时,epoll_wait 才会返回该 socket。

如下两个示意图:

  1. 从socket读数据:


    image.png
  2. 从socket写数据:


    image.png

所以, 在epoll的ET模式下, 正确的读写方式为:
读: 只要可读, 就一直读, 直到返回0, 或者 errno = EAGAIN
写: 只要可写, 就一直写, 直到数据发送完, 或者 errno = EAGAIN

六、epoll demo server端

//epoll_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
 
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);
 
int main(int argc, char *argv[])
{
   int serv_sock, clnt_sock;
   struct sockaddr_in serv_adr, clnt_adr;
   socklen_t adr_sz;
   int str_len, i;
   char buf[BUF_SIZE];
 
   struct epoll_event *ep_events;
   struct epoll_event event;
   int epfd, event_cnt;
 
   if(argc!=2) {
      printf("Usage : %s <port>\n", argv[0]);
      exit(1);
   }
 
   serv_sock=socket(PF_INET, SOCK_STREAM, 0);
   memset(&serv_adr, 0, sizeof(serv_adr));
   serv_adr.sin_family=AF_INET;
   serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
   serv_adr.sin_port=htons(atoi(argv[1]));
    
   if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
      error_handling("bind() error");
   if(listen(serv_sock, 5)==-1)
      error_handling("listen() error");
 
   epfd=epoll_create(EPOLL_SIZE);
   ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
 
   event.events=EPOLLIN;
   event.data.fd=serv_sock;  
   epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
 
   while(1)
   {
      event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
      if(event_cnt==-1)
      {
         puts("epoll_wait() error");
         free(ep_events);
         break;
      }
 
      for(i=0; i<event_cnt; i++)
      {
         if(ep_events[i].data.fd==serv_sock)
         {
            adr_sz=sizeof(clnt_adr);
            clnt_sock=
               accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
            event.events=EPOLLIN;
            event.data.fd=clnt_sock;
            epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
            printf("connected client: %d \n", clnt_sock);
         }
         else
         {
               str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
               if(str_len==0)    // close request!
               {
                  epoll_ctl(
                     epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                  close(ep_events[i].data.fd);
                  printf("closed client: %d \n", ep_events[i].data.fd);
               }
               else
               {
                  write(ep_events[i].data.fd, buf, str_len);    // echo!
               }
    
         }
      }
   }
   close(serv_sock);
   close(epfd);
   return 0;
}
 
void error_handling(char *buf)
{
   fputs(buf, stderr);
   fputc('\n', stderr);
   exit(1);
}

七、epoll demo client端

//epoll_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 1024
void error_handling(char *message);
 
int main(int argc, char *argv[])
{
   int sock;
   char message[BUF_SIZE];
   int str_len;
   struct sockaddr_in serv_adr;
 
   if(argc!=3) {
      printf("Usage : %s <IP> <port>\n", argv[0]);
      exit(1);
   }
    
   sock=socket(PF_INET, SOCK_STREAM, 0);  
   if(sock==-1)
      error_handling("socket() error");
    
   memset(&serv_adr, 0, sizeof(serv_adr));
   serv_adr.sin_family=AF_INET;
   serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
   serv_adr.sin_port=htons(atoi(argv[2]));
    
   if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
      error_handling("connect() error!");
   else
      puts("Connected...........");
    
   while(1)
   {
      fputs("Input message(Q to quit): ", stdout);
      fgets(message, BUF_SIZE, stdin);
       
      if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
         break;
 
      write(sock, message, strlen(message));
      str_len=read(sock, message, BUF_SIZE-1);
      message[str_len]=0;
      printf("Message from server: %s", message);
   }
    
   close(sock);
   return 0;
}
 
void error_handling(char *message)
{
   fputs(message, stderr);
   fputc('\n', stderr);
   exit(1);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269