现行反革命有诸如此类3个场景:我是1个很忙的大业主,小编有九十几个手提式有线电话机,手提式有线电话机来音信了,作者的文书就会报告自身“总监,你的手提式有线电话机来新闻了。”作者很生气,我的文书就是那样子,每便手提式有线电话机来音信就只报告小编来音讯了,主任尽快去看。可是她一向不把话说领会:到底是哪位手提式有线电话机来新闻啊!小编可有九十七个手提式有线电话机啊!于是,小编不得不一个一个手提式有线电话机去查看,来规定毕竟是哪多少个手提式有线电电话机来音讯了。那正是IO复用中select模型的瑕疵!CEO心想,如果书记能把来消息的手提式有线电话机直接获得自家桌子上就好了,那么作者的频率必然大增(那便是epoll模型)。

今昔有如此1个景色:小编是贰个很忙的大业主,作者有九21个手提式有线电话机,手提式无线电话机来新闻了,作者的书记就会告诉本人“首席营业官,你的无绳电话机来音信了。”笔者很恼火,笔者的书记正是那样子,每便手提式有线电话机来音讯就只报告我来音讯了,高管尽快去看。不过她绝非把话说精晓:到底是哪些手机来消息啊!作者可有九14个手提式有线电电话机啊!于是,小编只得1个多个有线电话去查看,来鲜明到底是哪多少个手提式有线电话机来消息了。那正是IO复用中select模型的症结!老板心想,假诺书记能把来新闻的无绳电话机间接得到本人桌子上就好了,那么作者的功用肯定大增(那就是epoll模型)。

I/O多路复用是在102线程或多进程编制程序中常用技术。首借使透过select/epoll/poll多少个函数援助的。在此关键对select和epoll函数详细介绍。

 

那大家先来总计一下select模型的老毛病:

那大家先来总计一下select模型的缺陷:

select函数

  • epoll全面授课,网络编制程序。该函数运维进度提示内核等待三个事件中的任何一个爆发,并只有2个或七个事件发生或经历1段钦赐的时间后才提示它。
  • 调用select告知内核查怎么描述符(就读、写或尤其条件)感兴趣以及等待多久。大家感兴趣的叙说符不囿于于套接字,任何描述符都能够动用select来测试。
  • 函数原型:

    #include<sys/select.h>
    #include<sys/time.h>
    
    int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set
                *exceptset, const struct timeval *timeout);
                返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
    
    • 最终贰个参数timeout,它报告内核等待所钦定描述符中的其余一个稳当可花多久。该参数有两种大概:
      • 永久等待下去:仅在有3个叙述符准备好I/O时才回去,将其设为空指针
      • 等候壹段固定时间:在有叁个描述符准备好I/O时重临,可是不超越由该参数所指向的timeval结构中内定的秒数和阿秒数。
      • 常有不等待:检查描述符后马上赶回,这正是轮询。为此,该参数必须指向三个timeval结构,不过里面包车型客车值必须安装为0
    • 多少个参数readset,writeset,exceptset钦赐大家要让内核测试读、写和尤其条件的叙说符。
    • 怎么着给那多少个参数的每二个参数钦赐1个或八个描述符值是2个企划上的题材。select使用描述符集,平日是1个平头数组,当中各类整数中的每壹位对应叁个描述符。举例来说,若是使用33人整数,那么该数组的率先个要素对应于描述符0~3一,第2个元素对应于描述符3二~陆三,以此类推。全体这几个完成细节都与应用程序非亲非故,它们隐藏在名称叫fd_set的数据类型和以下多个宏中:

      void FD_ZERO(fd_set *fdset);    //clear all bits in fdset
      void FD_SET(int fd, fd_set *fdset);   //turn on the bit for fd in fdset
      void FD_CLR(int fd, fd_set *fdset);  //turn off the bit for fd in fdset
      int FD_ISSET(int fd, fd_set *fdset);  //is the bit for fd on in fdset?
      

      我们分配3个fd_set数据类型的叙述符集,并用这几个宏设置或测试该集合中的每1位,也能够用C语言中的赋值语句把它赋值成其它3个讲述符集。
      专注:前边所研商的种种描述符占用整数数组中的一人的艺术唯有是select函数的大概达成之一。

    • maxfdp1参数内定待测试的叙述符个数,它的值是待测试的最大描述符加一。描述符0,一,2,…,直到maxfdp1 –
      一均被测试。

    • select函数修改由指针readset,writeset和exceptset所针对的讲述符集,因此那四个参数都以值-结果参数。该函数再次来到后,我们运用FD_ISSET宏来测试fd_set数据类型中的描述符。描述符集内别的与未妥当描述符所对应的位再次来到时均清成0.为此,每一遍重复调用select函数时,大家都得重复把拥有描述符集内所关心的位均置为一

多路复用的适用场面

  1. 单个进度能够监视的文件讲述符的数目存在最大范围,经常是拾二肆,当然能够变动数据,但鉴于select采取轮询的措施扫描文件描述符,文件讲述符数量愈来愈多,质量越差;(在linux内核头文件中,有那样的概念:#define
    __FD_SETSIZE 1024)
  2. 根本 /
    用户空间内部存款和储蓄器拷贝难点,select需求复制大批量的句柄数据结构,发生巨大的支付;
    select再次来到的是带有整个句柄的数组,应用程序供给遍历整个数组才能发现什么样句柄产生了轩然大波;
  3. select的接触格局是程度触发,应用程序借使未有做到对一个早就就绪的文件讲述符进行IO操作,那么之后每一遍select调用依旧会将这个文件讲述符文告进度。
  1. 单个进度能够监视的文件讲述符的数量存在最大范围,经常是1024,当然能够更改数据,但出于select采取轮询的法子扫描文件描述符,文件讲述符数量越来越多,质量越差;(在linux内核头文件中,有如此的定义:#define
    __FD_SETSIZE 1024)
  2. 根本 /
    用户空间内存拷贝难题,select要求复制大批量的句柄数据结构,发生巨大的开销;
    select重回的是带有整个句柄的数组,应用程序供给遍历整个数组才能窥见什么样句柄产生了事件;
  3. select的接触情势是程度触发,应用程序假如未有形成对一个业已就绪的文件讲述符举办IO操作,那么之后每回select调用如故会将这么些文件讲述符文告进程。

select重回套接字的“就绪”条件

  • 满意下列八个标准之壹的其它二个时,3个套接字准备好读:
    • 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的脚下高低。对于这么的套接字执行读操作不会卡住并将回到三个大于0的值(也正是重临准备好读入的数码)。大家应用SO_RECVLOWAT套接字选项设置套接字的低水位标记。对于TCP和UDP套接字而言,其私下认可值为1
    • 该连接的读半部关闭(相当于吸收了FIN的TCP连接)。对如此的套接字的读操作将不封堵并再次来到0(也便是重返EOF)
    • 该套接字时一个监听套接字且已做到的连接数不为0。
    • 其上有一个套接字错误待处理。对这么的套接字的读操作将不打断并回到-一(也便是回来三个谬误),同时把errno设置为方便的不当条件。那么些待处理错误也足以因而SO_E帕杰罗ROENVISION套接字选项调用getsockopt获取并消除。
  • 下列七个条件的任何三个满意时,二个套接字准备好写:
    • 该套接字发送缓冲区中的可用字节数大于等于套接字发送缓冲区低水位标记的此时此刻高低,并且或该套接字已接二连三,可能该套接字不供给一连(如UDP套接字)。这表示如若大家把如此的套接字设置成非阻塞的,写操作将不封堵并返回三个正在(如由传输层接收的字节数)。大家选用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和UDP而言,暗许值为2048
    • 该连接的写半部关闭。对这么的套接字的写操作将时有产生SIGPIPE时限信号
    • 使用非阻塞式connect套接字已确立连接,大概connect已经已破产告终
    • 其上有2个套接字错误待处理。对这么的套接字的写操作将不打断并回到-一(也正是回到多少个张冠李戴),同时把errno设置为适当的荒谬条件。那个待处理错误也得以经过SO_E奥迪Q5RO瑞鹰套接字选项调用getsockopt获取并免去。
  • 若是一个套接字存在带外数据也许仍处在带外标记,那么它有至极条件待处理。
  • 小心:当某些套接字上发生错误时,它将由select标记为既可读又可写
  • 接受低水位标记和出殡和埋葬低水位标记的意在:允许利用进度控制在select可读或可写条件以前有多少多少可读或有多大空间可用来写。
  • 任何UDP套接字只要其发送低水位标记小于等于发送缓冲区大小(私下认可应该总是那种关涉)就延续可写的,那是因为UDP套接字不须求连接。

•    
当客户处理四个描述符时(例就像是时处理交互式输入和网络套接口) 

思虑一下之类场景:有⑩0万个客户端同时与多个服务器进度保持着TCP连接。而每一时半刻时,常常唯有几百上千个TCP连接是生动活泼的(事实上海大学部分景观都以那种景观)。怎样落到实处那样的高并发?

思考一下之类场景:有100万个客户端同时与2个服务器进程保持着TCP连接。而每一时半刻时,经常唯有几百上千个TCP连接是活泼的(事实上海大学部分景色都以这种意况)。怎么着落到实处如此的高并发?

poll函数

  • 函数原型:

    #include<poll.h>
    
    int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
    
        返回:若有就绪描述符则为数目,若超时则为0,若出错则为-1
    
  • 先是个参数是指向一个结构数组第一个因素的指针。种种数组元素都以3个pollfd结构,用于内定测试有些给定描述符fd的尺码。

    struct pollfd{
        int fd;    //descriptor to check
        short event;  //events of interest on fd
        short revents;  //events that occurred on fd
    };
    

    要测试的尺度由events成员钦赐,函数在相应的revents成员中回到该描述符的意况。(每一种描述符都有三个变量,3个为调用值,另三个为回去结果,从而防止选用值-结果参数。)

  • poll事件

澳门金沙国际 1

•    
倘诺一个TCP服务器既要处理监听套接口,又要拍卖已连接套接口 

简短总结一下,二个经过最多有十二多少个文件描述符,那么大家须求开一千个经过来拍卖十0万个客户连接。倘诺大家利用select模型,这一千个经过里某壹段时间内唯有数个客户连接须要多少的吸收,那么我们就不得不轮询十二十六个公文讲述符以鲜明究竟是哪个客户有数量可读,想想假诺一千个经过都有像样的行事,那系统能源消耗可有多大啊!

简单来讲计算一下,2个历程最多有10二七个公文描述符,那么我们必要开一千个进程来拍卖100万个客户连接。如若我们应用select模型,这一千个进度里某壹段时间内只有数个客户连接须要多少的吸收接纳,那么大家就只能轮询10二十三个文本讲述符以分明毕竟是哪位客户有数据可读,想想借使一千个经过都有周边的行为,这系统能源消耗可有多大呀!

epoll函数

  • epoll是Linux特有的I/O复用函数。它在达成和行使上与select、poll有非常的大的距离。
    • 首先,epoll使用一组函数来形成职责,而不是单个函数。
    • 扶助,epoll把用户关切的文本讲述符上的风浪放在内核里的一个轩然大波表中,从而无须像select和poll那样每一趟调用都要再度传入文件讲述符集或事件集。
    • 但epoll要求动用三个附加的文书描述符,来唯一标识内核中的这么些事件表
  • epoll文件讲述符使用如下方式创建:

    #include<sys/epoll.h>
    
    int epoll_create(int size);
    

    size参数完全不起成效,只是给基础1个提示,告诉它事件表必要多大。该函数重回的文本讲述符将用作其余兼具epoll系统调用的首先个参数,以钦定要访问的根本事件表。

  • 下边包车型大巴函数用来操作epoll的基石事件表:

    #include<sys/epoll.h>
    
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    
        返回:若成功返回0,失败返回-1,并置errno
    

    fd参数是要操作的文件描述符,op参数则钦点操作类型。操作类型有以下3类:

    • EPOLL_CTL_ADD, 过去的事情件表中注册fd上的风浪
    • EPOLL_CTL_MOD, 修改fd上的登记事件
    • EPOLL_CTL_DEL, 删除fd上的挂号事件
  • event钦定事件,它是epoll_event结构指针类型,epoll_event的概念如下:

    strcut epoll_event{
        __uint32_t events;    //epoll事件
        epoll_data_t data;    //用户数据
    };
    
    • 里面,events成员描述事件类型。epoll帮助的轩然大波类型同poll基本相同。表示epoll事件类型的宏在poll对应的宏前添加”E”,比如epoll的数量可读事件是EPOLLIN。
    • epoll有五个附加的轩然大波类型——EPOLLET和EPOLLONESHOT。它们对于epoll的便快捷运输维不能缺少。
    • data成员用于存款和储蓄用户数据,是三个联合体:

      typedef union epoll_data{
          void *ptr;
          int fd;
          uint32_t u32;
          uint64_t u64;
      }epoll_data_t;
      

      里面五个成员用得最多的是fd,它内定事件所从属的目的文件讲述符。

  • epoll系列系统调用的要紧接口是epoll_wait函数,它在壹段超时时间内守候壹组文件讲述符上的轩然大波,其原型如下:

    #include<sys/epoll.h>
    
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
        返回:若成功返回就绪的文件描述符个数,失败时返回-1,并置errnoo
    
  • maxevents参数钦定最多监听多少个事件,它必须大于0

  • event_wait函数假使检查评定到事件,就将兼具就绪事件从水源事件表(由epfd参数钦命)中复制到它的第一个参数events指向的数组中。那么些数组只用于输出epoll_wait检验到的服服帖帖事件,而不像select和poll的数组参数那样既用于传入用户注册的风云,又用于出口基础检查实验到的伏贴事件。这就十分的大地进步了应用程序索引就绪文件讲述符的作用。

  • 下边代码给出 poll和epoll在行使上的异样:

    //如何索引poll返回的就绪文件描述符
    int ret = poll(fds, MAX_EVENT_NUMBER, -1);
    //必须遍历所有已注册文件描述符并找到其中的就绪者
    for(int i = 0; i < MAX_EVENT_NUMBER; ++i){
        if(fds[i].revents & POLLIN)  //判断第 i 个文件描述符是否就绪
        {
            int sockfd = fds[i].fd;
            //处理sockfd
        }
    }
    
    //如何索引epoll返回的文件描述符
    int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
    //仅遍历就绪的ret个文件描述符
    for(int i = 0; i < ret; ++i){
        int sockfd = events[i].data.fd;
        //sockfd肯定就绪,直接处理
    }
  • LT和ET模式
    • LT(Level
      Trigger,电平触发)格局:是暗中认可工作方式,在那种形式下的epoll约等于三个频率较高的poll。当epoll_wait检查实验到其上有事件产生并将此事件通报应用程序后,应用程序能够不比时处理该事件。那样,当应用程序下一遍调用epoll_wait时,epoll_wait还会重复向应用程序公告此事件。
    • ET(Edge
      Trigger,边沿触发)方式。对于ET工作格局下的公文描述符,当epoll_wait检查评定到其上有事件产生并将此事件通报应用程序后,应用程序必须及时处理该事件,因为三番五次的epoll_wait调用将不再向应用程序公告这一事件。
    • ET格局在十分大程度上下滑了同二个epoll事件被再次触发的次数。由此功效要比LT格局高。
    • 各类使用ET方式的文书讲述符都应该是非阻塞的。要是文件讲述符是阻塞的,那么读或写操作将会因为未有继续的年月而平昔处于阻塞状态(饥渴状态)
  • EPOLLONESHOT事件
    • 不怕使用ET形式,2个socket上的某部事件依旧或许被触发多次。那在并发程序中挑起三个题材。比如2个线程(或进度)在读取完某些socket上的数码后开首拍卖这个数据,而在数码的处理进程中该socket上又有新数据可读(EPOLLIN再一次被触发),此时其它二个线程被提拔来读取那一个新的数据。于是出现了三个线程同时操作一个socket的排场。那自然不是我们愿意的。我们目的在于的是二个socket连接在任暂时刻都只被2个线程处理。
    • 对于注册了EPOLLONESHOT事件的文本描述符,操作系统最多触发其上注册的一个可读、可写或尤其事件,且只触发一遍,除非大家接纳epoll_ctl函数重置该文件讲述符上的EPOLLONESHOT事件。那样,当贰个线程在处理有些socket时,别的线程时不容许有机遇操作该socket的。但转头考虑,注册了EPOLLONESHOT事件的socket壹旦被某些线程处理达成,该线程就活该马上重置这么些socket上的EPOLLONESHOT事件,以管教那些socket下3次可读时,其EPOLLIN事件能被触发,进而让其余工作线程有时机继续处理那几个socket.

•    
尽管多个服务器即要处理TCP,又要拍卖UDP 

本着select模型的老毛病,epoll模型被建议来了!

针对select模型的短处,epoll模型被建议来了!

•    
要是1个服务器要拍卖多少个服务或多个商讨 

epoll模型的长处

  • 支撑叁个进程打开大数指标socket描述符
  • IO成效不随FD数目扩充而线性降低
  • 接纳mmap加快内核与用户空间的消息传递

epoll模型的长处

  • 支撑二个历程打开大数量的socket描述符
  • IO效用不随FD数目增添而线性降低
  • 行使mmap加速内核与用户空间的新闻传递

select/poll/epoll差别

  1. poll再次回到的时候用户态须求轮询判断每种描述符的气象,纵然唯有3个描述符就绪,也要遍历整个集合。借使集合中活跃的叙说符很少,遍历进程的支出就会变得非常的大,而只要集合中多数的描述符都以虎虎有生气的,遍历进程的开支又足以忽略。epoll的兑现中年老年是只遍历活跃的描述符,在外向描述符较少的事态下就会很有优势,在代码的解析进度中得以看看epoll的完毕过于复杂并且其促成进程为促成线程安全须要一起处理(锁),要是大多数讲述符都以生动活泼的,遍历那一点分别绝对于加锁来说早已不值壹提了,epoll的频率恐怕不比select或poll。
  2. 传参情势各异 
    • 扶助的最大描述符分歧,根本原因是基本管理种种文件句柄的数据结构分化,select能够处理的最大fd不能够超出FDSETSIZE,因为调用select传入的参数fd_set是四个位数组,数组大小正是FDSETSIZE暗中认可为十24,所以调用格局界定了并发量。Poll是使用3个数组传入的参数,未有最大范围。Epoll不须要每回都流传,因为会调用epoll_ctl添加。
    • 动用方式不一致,select调用每趟都以因为内核会对数组举行在线修改,应用程序下次调用select前不得不重置这八个fdset,而poll比他聪明点,将句柄与事件绑定在协同通过一个struct
      pollfd完毕,重临时是透过其revets完毕,所以不须要重置该组织,直接传送就行,epoll不需求传递。
    • 援助的风云类型数分歧:select应为未有将句柄与事件开始展览绑定,所以fd_set仅仅是个公文讲述符集合,由此供给多个fd_set分别传入可读可写及至极事件,那使得她不可能处理越多花色的轩然大波,而poll选用的pollfd中event须要使用6十六个bit,epoll选择的
      epoll_event则须要玖五个bit,辅助越多的风浪类型。
  3. poll每一趟供给从用户态将全体的句柄复制到内核态,假设以万计的句柄会导致每趟都要copy几十几百KB的内部存款和储蓄器到内核态,十分的低效。使用epoll时您只须要调用epoll_ctl事先添加到对应红黑树,真正用epoll_wait时不用传递socket句柄给基础,节省了拷贝开支。
  4. 基础完成上:轮流动调查用全部fd对应的poll(把current挂到各样fd对应的装备等待队列上),等到有事件发生的时候会通报他,在调用停止后,又把进度从各样等待队列中剔除。在 epoll_wait时,把current轮流的进入fd对应的配备等待队列,在配备等待队列醒来时调用四个回调函数(当然,这就须要“唤醒回调”机制),把发滋事件的fd归入叁个链表,然后回到那几个链表上的fd。
  5. Select
    不是线程安全的,epoll是线程安全的,内部提供了锁的保险,就算叁个线程在epoll_wait的时候另1个线程epoll_ctl也没难题。
  6. 水源使用了slab机制,为epoll提供了高效的数据结构。
  7. Select和poll相当于epoll的LT形式,不接济ET形式,epoll帮衬特别该高速的ET情势 
    (ET和LT差异见下文)

 

epoll的三种工作形式

  • LT(level triggered,水平触发方式)是缺省的劳作方式,并且同时支持block 和 non-block
    socket。在那种做法中,内核告诉你一个文件讲述符是或不是妥贴了,然后您能够对这些就绪的fd进行IO操作。如若你不作任何操作,内核照旧会一而再公告你的,所以,这种形式编制程序出荒唐大概要小1些。比如基本布告你其中多少个fd能够读数据了,你急速去读。你要么懒懒散散,不去读这些数目,下3回巡回的时候根本发现你还没读刚才的数量,就又布告你急忙把刚刚的数目读了。那种体制能够对比好的承接保险每种数据用户都处理掉了。

  • ET(edge-triggered,边缘触发情势)是高效工作措施,只帮衬no-block
    socket。在那种情势下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会倘诺你明白文书讲述符已经就绪,并且不会再为那多少个文件讲述符发送越多的服服帖帖公告,等到下次有新的多少进来的时候才会再也起身妥当事件。简单来讲,便是根本布告过的政工不会再说第二次,数据错过没读,你协调负责。那种机制真正速度拉长了,可是危机相伴而行。

epoll的三种工作方式

  • LT(level triggered,水平触发形式)是缺省的办事办法,并且同时支持block 和 non-block
    socket。在那种做法中,内核告诉您2个文本讲述符是或不是妥帖了,然后你能够对那些就绪的fd实行IO操作。假使您不作任何操作,内核照旧会继续公告你的,所以,那种格局编制程序出错误也许要小一些。比如基本公告你个中1个fd能够读数据了,你尽快去读。你依旧懒懒散散,不去读那几个数量,下壹次巡回的时候基本发现你还没读刚才的多寡,就又布告你飞速把刚刚的数据读了。那种体制能够相比较好的担保每种数据用户都处理掉了。

  • ET(edge-triggered,边缘触发方式)是全速工作方法,只协助no-block
    socket。在那种情势下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会若是你理解文书讲述符已经就绪,并且不会再为那多少个文件讲述符发送越多的稳当布告,等到下次有新的数额进来的时候才会重复起身伏贴事件。简单的讲,正是基础文告过的事体不会再说第一回,数据错过没读,你协调负担。那种体制真正速度拉长了,不过风险相伴而行。

epoll工作规律

epoll_create

操作系统在运行时会登记三个evnetpollfs的文件系统,对应的file
operations只是完毕了poll跟release操作,然后伊始化1些数据结构,例如贰个slab缓存,以便后边简化epitem和eppoll_entry对象的分红,
早先化递归检查队列等。

创造八个eventpoll对象,
里边有用户音信,是还是不是root,最大监听fd数目,等待队列,就绪链表,红黑树的头结点等,并且创建三个fd
即epollfd,,

而eventpoll对象保存在struct
file结构的private指针中,为便于从fd获得eventpoll对象,并赶回。

 

epoll_ctl

将epoll_event结构拷贝到内核空间中;

再即是判断参加的fd是不是匡助poll结构;

并且从epfd->file->privatedata获取event_poll对象,依据op区分是丰盛,删除照旧修改;

首先在eventpoll结构中的红黑树查找是或不是曾经存在了相呼应的fd,没找到就帮助插入操作,不然报重复的错误;

相呼应的修改,删除相比较不难就不啰嗦了

布置时会实行上锁。

 

插入操作时,会创制二个与fd对应的epitem结构,并且开始化相关成员,比如保留监听的fd跟file结构等等的,

 

末尾调用加入的fd的file
operation->poll函数(最终会调用poll_wait操作)用于来将近日经过注册到设备的等候队列:在其内传递poll_table变量调用poll_wait,poll_table会提供五个函数指针,事实上调用的便是那个函数指针指向的指标,该函数正是将近年来拓展挂在设备的等待队列中,并钦点设备事件就绪时的回调函数callback,该callback的落成正是将该epitem放在rdlist链表中。

 

谈到底将epitem结构丰富到红黑树中

 

epoll_wait

计量睡眠时间(即使有),判断eventpoll对象的链表是还是不是为空,不为空那就工作,不睡觉,并且初叶化三个等候队列,把团结挂上去,设置本人的长河景况为可睡眠状态。判断是或不是有非功率信号到来(有的话一向被暂停醒来),若是啥事都未曾那就调用schedule_timeout进行睡眠,如果超时或然被提醒,首先从本身初阶化的等候队列删除
,然后开始拷贝能源给用户空间了。

拷贝财富则是先把就绪事件链表转移到中间链表,然后逐1回历拷贝到用户空间。

并且逐一判断其是或不是为水平触发,是的话再一次插入到就绪链表。

   

切实实现由众多细节:
假如拷贝rdlist进度中又有事件就绪了如何做,要是epollfd被另三个epoll监听会不会循环唤醒,lt什么日期会从rdlist中剔除等,见下文
!

epoll模型API

#include <sys/epoll.h> 

/* 创建一个epoll的句柄,size用来告诉内核需要监听的数目一共有多大。当创建好epoll句柄后,
它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。*/
int epoll_create(int size);  

/*epoll的事件注册函数*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

/*等待事件的到来,如果检测到事件,就将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  

epoll的风浪注册函数epoll_ctl,第2个参数是 epoll_create()
的再次回到值,第3个参数表示动作,使用如下多个宏来表示:

POLL_CTL_ADD    //注册新的fd到epfd中;
EPOLL_CTL_MOD    //修改已经注册的fd的监听事件;
EPOLL_CTL_DEL    //从epfd中删除一个fd;

struct epoll_event 结构如下:

typedef union epoll_data
{
    void        *ptr;
    int          fd;
    __uint32_t   u32;
    __uint64_t   u64;
} epoll_data_t;

struct epoll_event 
{
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

epoll_event结构体中的events 能够是以下多少个宏的聚合:

EPOLLIN     //表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT    //表示对应的文件描述符可以写;
EPOLLPRI    //表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR    //表示对应的文件描述符发生错误;
EPOLLHUP    //表示对应的文件描述符被挂断;
EPOLLET     //将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT//只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

epoll模型API

#include <sys/epoll.h> 

/* 创建一个epoll的句柄,size用来告诉内核需要监听的数目一共有多大。当创建好epoll句柄后,
它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。*/
int epoll_create(int size);  

/*epoll的事件注册函数*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

/*等待事件的到来,如果检测到事件,就将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  

epoll的风云注册函数epoll_ctl,第三个参数是 epoll_create()
的重返值,第一个参数表示动作,使用如下八个宏来表示:

POLL_CTL_ADD    //注册新的fd到epfd中;
EPOLL_CTL_MOD    //修改已经注册的fd的监听事件;
EPOLL_CTL_DEL    //从epfd中删除一个fd;

struct epoll_event 结构如下:

typedef union epoll_data
{
    void        *ptr;
    int          fd;
    __uint32_t   u32;
    __uint64_t   u64;
} epoll_data_t;

struct epoll_event 
{
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

epoll_event结构体中的events 能够是以下多少个宏的汇集:

EPOLLIN     //表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT    //表示对应的文件描述符可以写;
EPOLLPRI    //表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR    //表示对应的文件描述符发生错误;
EPOLLHUP    //表示对应的文件描述符被挂断;
EPOLLET     //将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT//只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

EPOll的ET与LT

根本完成:

只是在从rdlist中回到的时候有分别,内核首先会将rdlist拷贝到3个方今链表txlist,
然后如要是LT事件同时事件就绪的话fd被再次放回了rdllist。那么下次epoll_wait当然会又把rdllist里的fd拿来拷给用户了。举个例子。借使3个socket,只是connect,还并未有收发数据,那么它的poll事件掩码总是有POLLOUT的,每一回调用epoll_wait总是回到POLLOUT事件,因为它的fd就接连被放回rdllist;要是此时有人往这么些socket里写了一大堆数据,造成socket塞住,fd不会放回rdllist,epoll_wait将不会再回去用户POLLOUT事件。要是我们给那几个socket加上EPOLLET,然后connect,未有收发数据,epoll_wait只会重返叁遍POLLOUT文告给用户(因为此fd不会再回来rdllist了),接下去的epoll_wait都不会有其余事件通报了。

 

注意地点LT
fd拷贝回rdlist并不是向用户处理完事后发出的,而是向用户拷贝完现在一向复制到rdlist中,那么只要用户消费那几个事件使事件不服帖了如何做,比如说本来是可读的,重返给用户,用户读到不可读结束,继续调用epoll_wait
重回rdlist,则发现不足读,事实上每一趟回来在此之前会以NULL继续调用poll,判断事件是不是变动,平日调用poll会传递个poll_table变量,就开始展览添加到等待队列中,而此时不要求加上,只是一口咬住不放一下气象,假使rdlist中状态变化了,就不会给用户再次回到了。

 

接触格局:

据悉对二种到场rdlist途径的分析,能够得出ET格局下被提示(重回就绪)的标准化为:

对此读取操作:

(一)
当buffer由不得读状态变为可读的时候,即由空变为不空的时候。

(2)
当有新数据到达时,即buffer中的待读内容变多的时候。

(三)
当buffer中有数量可读(即buffer不空)且用户对相应fd进行epoll_mod
IN事件时

 

对此写操作:

(一)
当buffer由不足写变为可写的时候,即由满状态成为不满状态的时候。

(2)
当有旧数据被发送走时,即buffer中待写的剧情减少得时候。

(三)
当buffer中有可写空间(即buffer不满)且用户对相应fd实行epoll_mod
OUT事件时

 

对此LT情势则简单多了,除了上述操作为读了一条事件就绪就径直文告。

 

ET比LT高效的来头:

透过地点的解析,可获得LT每一遍都需求处理rdlist,无疑向用户拷贝的数额变多,且epoll_wait循环也变多,品质自然下落了。

除此以外一面从用户角度思量,使用ET形式,它能够省事的拍卖EPOLLOUT事件,省去打开与关闭EPOLLOUT的epoll_ctl(EPOLL_CTL_MOD)调用。从而有十分的大概率让您的本性获得肯定的晋升。例如你需求写出1M的多寡,写出到socket
25陆k时,重临了EAGAIN,ET方式下,当再一次epoll重返EPOLLOUT事件时,继续写出待写出的数量,当未有多少必要写出时,不处理直接略过即可。而LT形式则须要先开辟EPOLLOUT,当未有数量供给写出时,再关闭EPOLLOUT(不然会一贯重回EPOLLOUT事件),而调用epoll_ctl是系统调用,要陷入内核并且须求操作加锁红黑树,总体来说,ET处理EPOLLOUT方便快速些,LT不不难遗漏事件、不易爆发bug,要是server的响应经常较小,不会触发EPOLLOUT,那么适合选拔LT,例如redis等,那种意况下仍旧不要求关爱EPOLLOUT,流量丰盛小的时候一直发送,要是发送不完在开始展览关切EPOLLOUT,发送完打消关心就行了,能够开展多少的优化。而nginx作为高品质的通用服务器,网络流量能够跑满达到壹G,那种景观下很简单触发EPOLLOUT,则运用ET。

参见新浪

 

骨子里运用:

当epoll工作在ET方式下时,对于读操作,尽管read贰遍没有读尽buffer中的数据,那么下次将得不到读就绪的通知,造成buffer中已有个别数据无机会读出,除非有新的数据重复抵达。对于写操作,主假若因为ET情势下fd平日为非阻塞导致的2个标题——怎么样保证将用户要求写的数码写完。

要消除上述七个ET方式下的读写难点,大家必须贯彻:

a.
对于读,只要buffer中还有多少就一向读;

b.
对于写,只要buffer还有空间且用户请求写的数目还未写完,就直接写。

 

行使那种格时局须要使各类连接的套接字工作于非阻塞情势,因为读写须求一向读或写直到出错(对于读,当读到的实际字节数小于请求字节数时就足以告壹段落),而壹旦你的文本讲述符假若不是非阻塞的,那那么些一贯读或直接写势必会在结尾2遍阻塞。那样就不能在堵塞在epoll_wait上了,造成任何文件讲述符的任务饿死。

因此也就常说“ET供给工作在非阻塞情势”,当然那并不能够表明ET不可能干活在堵塞形式,而是工作在堵塞情势只怕在运转中会出现有的难点。

 

ET情势下的accept

   
考虑那种景色:三个两次三番同时到达,服务器的 TCP
就绪队列瞬间积累多少个就绪

连天,由于是边缘触发情势,epoll
只会通报一回,accept 只处理三个三番五次,导致 TCP
就绪队列中多余的连年都得不随地理。

     消除办法是用 while 循环抱住 accept
调用,处理完 TCP
就绪队列中的全部连接后再脱离循环。怎样掌握是不是处理完就绪队列中的全数连接呢?
accept  重返 -一 并且 errno 设置为 EAGAIN 就象征全体连接都处理完。

的正确性利用情势为:

while ((conn_sock =
accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen))
> 0) {  

    handle_client(conn_sock);  

}  

if (conn_sock == -1) {  

     if (errno != EAGAIN && errno !=
ECONNABORTED   

            && errno != EPROTO && errno !=
EINTR)   

        perror(“accept”);  

}

增加:服务端使用多路转接技术(select,poll,epoll等)时,accept应工作在非阻塞情势。

原因:假诺accept工作在堵塞形式,思量那种景况:
TCP 连接被客户端夭亡,即在服务器调用 accept
在此以前(此时select等早已回来连接到达读就绪),客户端主动发送 汉兰达ST
终止连接,导致刚刚建立的接连从妥贴队列中移出,假若套接口被设置成阻塞形式,服务器就会向来不通在
accept
调用上,直到其余有个别客户建立2个新的连天完结。不过在此时期,服务器单纯地打断在accept
调用上(实际应该阻塞在select上),就绪队列中的别的描述符都得不随地理。

    化解办法是把监听套接口设置为非阻塞,
当客户在服务器调用 accept 以前暂停

有个别连接时,accept 调用能够立即回到 -一,
那时源自 Berkeley 的实现会在基础中处理该事件,并不会将该事件通报给
epoll,而别的达成把 errno 设置为 ECONNABO奥迪Q7TED 可能 EPROTO
错误,我们理应忽视那七个错误。(具体可参看UNP v一 p36三)

 

epoll的3个大致利用范例

#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>



#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

void setnonblocking(int sock)
{
    int opts;
    opts=fcntl(sock,F_GETFL);
    if(opts<0)
    {
        perror("fcntl(sock,GETFL)");
        exit(1);
    }
    opts = opts|O_NONBLOCK;
    if(fcntl(sock,F_SETFL,opts)<0)
    {
        perror("fcntl(sock,SETFL,opts)");
        exit(1);
    }
}

int main(int argc, char* argv[])
{
    int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;


    if ( 2 == argc )
    {
        if( (portnumber = atoi(argv[1])) < 0 )
        {
            fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
            return 1;
        }
    }
    else
    {
        fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
        return 1;
    }



    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件

    struct epoll_event ev,events[20];
    //生成用于处理accept的epoll专用的文件描述符

    epfd=epoll_create(256);
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    //把socket设置为非阻塞方式

    //setnonblocking(listenfd);

    //设置与要处理的事件相关的文件描述符

    ev.data.fd=listenfd;
    //设置要处理的事件类型

    ev.events=EPOLLIN|EPOLLET;
    //ev.events=EPOLLIN;

    //注册epoll事件

    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);

    serveraddr.sin_port=htons(portnumber);
    bind(listenfd,(struct sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    for ( ; ; ) {
        //等待epoll事件的发生

        nfds=epoll_wait(epfd,events,20,500);
        //处理所发生的所有事件

        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。

            {
                connfd = accept(listenfd,(struct sockaddr *)&clientaddr, &clilen);
                if(connfd<0){
                    perror("connfd<0");
                    exit(1);
                }
                //setnonblocking(connfd);

                char *str = inet_ntoa(clientaddr.sin_addr);
                printf("accapt a connection from\n ");
                //设置用于读操作的文件描述符

                ev.data.fd=connfd;
                //设置用于注测的读操作事件

                ev.events=EPOLLIN|EPOLLET;
                //ev.events=EPOLLIN;

                //注册ev

                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
            else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。

            {
                printf("EPOLLIN\n");
                if ( (sockfd = events[i].data.fd) < 0)
                    continue;
                if ( (n = read(sockfd, line, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) {
                        close(sockfd);
                        events[i].data.fd = -1;
                    } else
                        printf("readline error\n");
                } else if (n == 0) {
                    close(sockfd);
                    events[i].data.fd = -1;
                }
                if(n<MAXLINE-2)
                    line[n] = '\0';

                //设置用于写操作的文件描述符

                ev.data.fd=sockfd;
                //设置用于注测的写操作事件

                ev.events=EPOLLOUT|EPOLLET;
                //修改sockfd上要处理的事件为EPOLLOUT

                //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

            }
            else if(events[i].events&EPOLLOUT) // 如果有数据发送

            {
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                //设置用于读操作的文件描述符

                ev.data.fd=sockfd;
                //设置用于注测的读操作事件

                ev.events=EPOLLIN|EPOLLET;
                //修改sockfd上要处理的事件为EPOLIN

                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            }
        }
    }
    return 0;
}

epoll的三个简单易行利用范例

#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>



#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

void setnonblocking(int sock)
{
    int opts;
    opts=fcntl(sock,F_GETFL);
    if(opts<0)
    {
        perror("fcntl(sock,GETFL)");
        exit(1);
    }
    opts = opts|O_NONBLOCK;
    if(fcntl(sock,F_SETFL,opts)<0)
    {
        perror("fcntl(sock,SETFL,opts)");
        exit(1);
    }
}

int main(int argc, char* argv[])
{
    int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;


    if ( 2 == argc )
    {
        if( (portnumber = atoi(argv[1])) < 0 )
        {
            fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
            return 1;
        }
    }
    else
    {
        fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
        return 1;
    }



    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件

    struct epoll_event ev,events[20];
    //生成用于处理accept的epoll专用的文件描述符

    epfd=epoll_create(256);
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    //把socket设置为非阻塞方式

    //setnonblocking(listenfd);

    //设置与要处理的事件相关的文件描述符

    ev.data.fd=listenfd;
    //设置要处理的事件类型

    ev.events=EPOLLIN|EPOLLET;
    //ev.events=EPOLLIN;

    //注册epoll事件

    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);

    serveraddr.sin_port=htons(portnumber);
    bind(listenfd,(struct sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    for ( ; ; ) {
        //等待epoll事件的发生

        nfds=epoll_wait(epfd,events,20,500);
        //处理所发生的所有事件

        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。

            {
                connfd = accept(listenfd,(struct sockaddr *)&clientaddr, &clilen);
                if(connfd<0){
                    perror("connfd<0");
                    exit(1);
                }
                //setnonblocking(connfd);

                char *str = inet_ntoa(clientaddr.sin_addr);
                printf("accapt a connection from\n ");
                //设置用于读操作的文件描述符

                ev.data.fd=connfd;
                //设置用于注测的读操作事件

                ev.events=EPOLLIN|EPOLLET;
                //ev.events=EPOLLIN;

                //注册ev

                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
            else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。

            {
                printf("EPOLLIN\n");
                if ( (sockfd = events[i].data.fd) < 0)
                    continue;
                if ( (n = read(sockfd, line, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) {
                        close(sockfd);
                        events[i].data.fd = -1;
                    } else
                        printf("readline error\n");
                } else if (n == 0) {
                    close(sockfd);
                    events[i].data.fd = -1;
                }
                if(n<MAXLINE-2)
                    line[n] = '\0';

                //设置用于写操作的文件描述符

                ev.data.fd=sockfd;
                //设置用于注测的写操作事件

                ev.events=EPOLLOUT|EPOLLET;
                //修改sockfd上要处理的事件为EPOLLOUT

                //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

            }
            else if(events[i].events&EPOLLOUT) // 如果有数据发送

            {
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                //设置用于读操作的文件描述符

                ev.data.fd=sockfd;
                //设置用于注测的读操作事件

                ev.events=EPOLLIN|EPOLLET;
                //修改sockfd上要处理的事件为EPOLIN

                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            }
        }
    }
    return 0;
}

EPOLlONSHOT

      
在有个别监听事件和读取分离的场景中,比如说在主线程中监听,在子线程中接收数据并处理,那时候会出现多个线程同时操作多少个socket的局面,比如说主线程监听到事件交由线程一处理,还未处理完又有事件到达,主线程交由线程二拍卖,那就招致数据不1致,壹般意况下需求在该文件讲述符上注册EPOLLONESHOT事件,操作系统最多触发其上注册的三个可读可写或尤其事件,且只触发二次,除非大家采纳epoll_ctl函数重置该EPOLLONESHOT事件。反过来思虑也同样,注册了该事件的线程处理完数据后务必重新注册,不然下次不会重新接触。参见《linux高品质服务器编制程序》玖.三.四节

             
可是有1个通病,那样的话会每一趟都调用epoll_ctrl陷入内核,并且epoll为保证线程安全会使用了加锁红黑树,这样会严重影响属性,此时就必要换一种思路,在应用层维护三个原子整数或称为flag来记录当前句柄是还是不是有线程在拍卖,每一趟有事件来临得时候会检查这一个原子整数,假使在处理就不会分配线程处理,否则会分配线程,那样就制止了深陷内核,使用epoll_data来囤积那个原子整数就行。

      
对于使用EPOLLSHOT格局来预防数据不雷同既能够应用ET也足以利用LT,因为她防止了再次接触,然而利用原子整数的方法只可以选择ET情势,他不是提防再度接触,而是防止被七个线程处理,在某些景况下只怕总结的快慢跟不上io涌来的进程,正是不可能即时接收缓冲区的内容,此时接受线程和主线程是分离的,尽管应用LT的话主线程会一向触发事件,导致busy-loop。
而使用ET触发唯有在事变来临得时候会接触,缓冲区有内容并不会接触,触发的次数就变少了,即使主线程依旧可能空转(fd有事件来临,但已被线程处理,此时不供给处理,继续epoll_wait就好),但这么空转比屡次调用epoll_ctl的可能率小多了。

   
上边的化解办法①般完美,其实存在竞态的意况,假如线程1检查flag为false,未有线程处理那么些socket,准备去接受处理的时候被调出CPU了,线程二获得cput后也1如既往发现flag为false,
就去接手socket来拍卖,此时假如线程1接续取得CPU,就会继续执行,接管socket,那样就会时有爆发二个socket被七个线程处理的情景。

 

带ET和LT双格局的epoll服务器

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <errno.h>
#include <stdbool.h>


#define MAX_EVENT_NUMBER 1024  //event的最大数量
#define BUFFER_SIZE 10      //缓冲区大小
#define ENABLE_ET  1       //是否启用ET模式

/* 将文件描述符设置为非拥塞的  */
int SetNonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

/* 将文件描述符fd上的EPOLLIN注册到epoll_fd指示的epoll内核事件表中,参数enable_et指定是否对fd启用et模式 */
void AddFd(int epoll_fd, int fd, bool enable_et)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN; //注册该fd是可读的
    if(enable_et)
    {
        event.events |= EPOLLET;
    }

    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);  //向epoll内核事件表注册该fd
    SetNonblocking(fd);
}

/*  LT工作模式特点:稳健但效率低 */
void lt_process(struct epoll_event* events, int number, int epoll_fd, int listen_fd)
{
    char buf[BUFFER_SIZE];
    int i;
    for(i = 0; i < number; i++) //number: 就绪的事件数目
    {
        int sockfd = events[i].data.fd;
        if(sockfd == listen_fd)  //如果是listen的文件描述符,表明有新的客户连接到来
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof(client_address);
            int connfd = accept(listen_fd, (struct sockaddr*)&client_address, &client_addrlength);
            AddFd(epoll_fd, connfd, false);  //将新的客户连接fd注册到epoll事件表,使用lt模式
        }
        else if(events[i].events & EPOLLIN) //有客户端数据可读
        {
            // 只要缓冲区的数据还没读完,这段代码就会被触发。这就是LT模式的特点:反复通知,直至处理完成
            printf("lt mode: event trigger once!\n");
            memset(buf, 0, BUFFER_SIZE);
            int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
            if(ret <= 0)  //读完数据了,记得关闭fd
            {
                close(sockfd);
                continue;
            }
            printf("get %d bytes of content: %s\n", ret, buf);

        }
        else
        {
            printf("something unexpected happened!\n");
        }
    }
}

/* ET工作模式特点:高效但潜在危险 */
void et_process(struct epoll_event* events, int number, int epoll_fd, int listen_fd)
{
    char buf[BUFFER_SIZE];
    int i;
    for(i = 0; i < number; i++)
    {
        int sockfd = events[i].data.fd;
        if(sockfd == listen_fd)
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof(client_address);
            int connfd = accept(listen_fd, (struct sockaddr*)&client_address, &client_addrlength);
            AddFd(epoll_fd, connfd, true);  //使用et模式
        }
        else if(events[i].events & EPOLLIN)
        {
            /* 这段代码不会被重复触发,所以我么循环读取数据,以确保把socket读缓存的所有数据读出。这就是我们消除ET模式潜在危险的手段 */

            printf("et mode: event trigger once!\n");
            while(1)
            {
                memset(buf, 0, BUFFER_SIZE);
                int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
                if(ret < 0)
                {
                    /* 对于非拥塞的IO,下面的条件成立表示数据已经全部读取完毕,此后epoll就能再次触发sockfd上的EPOLLIN事件,以驱动下一次读操作 */

                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                    {
                        printf("read later!\n");
                        break;
                    }

                    close(sockfd);
                    break;
                }
                else if(ret == 0)
                {
                    close(sockfd);
                }
                else //没读完,继续循环读取
                {
                    printf("get %d bytes of content: %s\n", ret, buf);
                }
            }
        }
        else
        {
            printf("something unexpected happened!\n");
        }
    }
}


int main(int argc, char* argv[])
{
    if(argc <= 2)
    {
        printf("usage:  ip_address + port_number\n");
        return -1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    int ret = -1;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
    if(listen_fd < 0)
    {
        printf("fail to create socket!\n");
        return -1;
    }

    ret = bind(listen_fd, (struct sockaddr*)&address, sizeof(address));
    if(ret == -1)
    {
        printf("fail to bind socket!\n");
        return -1;
    }

    ret = listen(listen_fd, 5);
    if(ret == -1)
    {
        printf("fail to listen socket!\n");
        return -1;
    }

    struct epoll_event events[MAX_EVENT_NUMBER];
    int epoll_fd = epoll_create(5);  //事件表大小为5
    if(epoll_fd == -1)
    {
        printf("fail to create epoll!\n");
        return -1;
    }

    AddFd(epoll_fd, listen_fd, true); //使用ET模式epoll,将listen文件描述符加入事件表

    while(1)
    {
        int ret = epoll_wait(epoll_fd, events, MAX_EVENT_NUMBER, -1);
        if(ret < 0)
        {
            printf("epoll failure!\n");
            break;
        }

        if(ENABLE_ET)
        {
            et_process(events, ret, epoll_fd, listen_fd);
        }
        else
        {
            lt_process(events, ret, epoll_fd, listen_fd);  
        }

    }

    close(listen_fd);
    return 0;

}

接下来再写3个简约的TCP客户端来测试一下:

//客户端
#include <sys/types.h> 
#include <sys/socket.h> 
#include <stdio.h> 
#include <netinet/in.h> 
#include <arpa/inet.h> 
#include <unistd.h> 
#include <stdlib.h>
#include <sys/time.h>

int main() 
{ 
    int client_sockfd; 
    int len; 
    struct sockaddr_in address;//服务器端网络地址结构体 
     int result; 
    char str1[] = "ABCDE"; 
    char str2[] = "ABCDEFGHIJK"; 
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket 
    address.sin_family = AF_INET; 
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(8888); 
    len = sizeof(address); 
    result = connect(client_sockfd, (struct sockaddr *)&address, len); 
    if(result == -1) 
    { 
         perror("oops: client2"); 
         exit(1); 
    } 
    //第一次读写
    write(client_sockfd, str1, sizeof(str1)); 

    sleep(5);

    //第二次读写
    write(client_sockfd, str2, sizeof(str2)); 


    close(client_sockfd); 

    return 0; 
}

TCP客户端的动作是那般的:第一遍首发送字符串”ABCDE”过去服务器端,五秒后,再发字符串”ABCDEFGHIJK”过去服务端,大家观望一下ET格局的服务器和LT方式的服务器在读取数据的主意上到底有如何界别。

ET模式

澳门金沙国际 2

ET方式现象分析:大家的服务器读缓冲区大小咱们设置了十。第贰次收受字符串时,大家的缓冲区有丰盛的长空中接力受它,所以打字与印刷出内容”ABCDE”并且打印出”read
later”表示数据已经读完了。第二次收受字符串时,大家的缓冲区空间不足以接收全数的字符,所以分了三次收到。不过总触发次数仅为三遍。

LT模式

澳门金沙国际 3

LT方式现象分析:
同理,第二遍收受字符串有丰硕的上空切受,第一遍接受字符串缓冲区空间欠缺,所以第一遍接到时分了两回来接受。同时也注意到,只要您未有完全接受完上次的数目,内核就会一而再公告你去接收数据!所以事件触发的次数是壹遍。

澳门金沙国际,带ET和LT双情势的epoll服务器

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <errno.h>
#include <stdbool.h>


#define MAX_EVENT_NUMBER 1024  //event的最大数量
#define BUFFER_SIZE 10      //缓冲区大小
#define ENABLE_ET  1       //是否启用ET模式

/* 将文件描述符设置为非拥塞的  */
int SetNonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

/* 将文件描述符fd上的EPOLLIN注册到epoll_fd指示的epoll内核事件表中,参数enable_et指定是否对fd启用et模式 */
void AddFd(int epoll_fd, int fd, bool enable_et)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN; //注册该fd是可读的
    if(enable_et)
    {
        event.events |= EPOLLET;
    }

    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);  //向epoll内核事件表注册该fd
    SetNonblocking(fd);
}

/*  LT工作模式特点:稳健但效率低 */
void lt_process(struct epoll_event* events, int number, int epoll_fd, int listen_fd)
{
    char buf[BUFFER_SIZE];
    int i;
    for(i = 0; i < number; i++) //number: 就绪的事件数目
    {
        int sockfd = events[i].data.fd;
        if(sockfd == listen_fd)  //如果是listen的文件描述符,表明有新的客户连接到来
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof(client_address);
            int connfd = accept(listen_fd, (struct sockaddr*)&client_address, &client_addrlength);
            AddFd(epoll_fd, connfd, false);  //将新的客户连接fd注册到epoll事件表,使用lt模式
        }
        else if(events[i].events & EPOLLIN) //有客户端数据可读
        {
            // 只要缓冲区的数据还没读完,这段代码就会被触发。这就是LT模式的特点:反复通知,直至处理完成
            printf("lt mode: event trigger once!\n");
            memset(buf, 0, BUFFER_SIZE);
            int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
            if(ret <= 0)  //读完数据了,记得关闭fd
            {
                close(sockfd);
                continue;
            }
            printf("get %d bytes of content: %s\n", ret, buf);

        }
        else
        {
            printf("something unexpected happened!\n");
        }
    }
}

/* ET工作模式特点:高效但潜在危险 */
void et_process(struct epoll_event* events, int number, int epoll_fd, int listen_fd)
{
    char buf[BUFFER_SIZE];
    int i;
    for(i = 0; i < number; i++)
    {
        int sockfd = events[i].data.fd;
        if(sockfd == listen_fd)
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof(client_address);
            int connfd = accept(listen_fd, (struct sockaddr*)&client_address, &client_addrlength);
            AddFd(epoll_fd, connfd, true);  //使用et模式
        }
        else if(events[i].events & EPOLLIN)
        {
            /* 这段代码不会被重复触发,所以我么循环读取数据,以确保把socket读缓存的所有数据读出。这就是我们消除ET模式潜在危险的手段 */

            printf("et mode: event trigger once!\n");
            while(1)
            {
                memset(buf, 0, BUFFER_SIZE);
                int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
                if(ret < 0)
                {
                    /* 对于非拥塞的IO,下面的条件成立表示数据已经全部读取完毕,此后epoll就能再次触发sockfd上的EPOLLIN事件,以驱动下一次读操作 */

                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                    {
                        printf("read later!\n");
                        break;
                    }

                    close(sockfd);
                    break;
                }
                else if(ret == 0)
                {
                    close(sockfd);
                }
                else //没读完,继续循环读取
                {
                    printf("get %d bytes of content: %s\n", ret, buf);
                }
            }
        }
        else
        {
            printf("something unexpected happened!\n");
        }
    }
}


int main(int argc, char* argv[])
{
    if(argc <= 2)
    {
        printf("usage:  ip_address + port_number\n");
        return -1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    int ret = -1;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
    if(listen_fd < 0)
    {
        printf("fail to create socket!\n");
        return -1;
    }

    ret = bind(listen_fd, (struct sockaddr*)&address, sizeof(address));
    if(ret == -1)
    {
        printf("fail to bind socket!\n");
        return -1;
    }

    ret = listen(listen_fd, 5);
    if(ret == -1)
    {
        printf("fail to listen socket!\n");
        return -1;
    }

    struct epoll_event events[MAX_EVENT_NUMBER];
    int epoll_fd = epoll_create(5);  //事件表大小为5
    if(epoll_fd == -1)
    {
        printf("fail to create epoll!\n");
        return -1;
    }

    AddFd(epoll_fd, listen_fd, true); //使用ET模式epoll,将listen文件描述符加入事件表

    while(1)
    {
        int ret = epoll_wait(epoll_fd, events, MAX_EVENT_NUMBER, -1);
        if(ret < 0)
        {
            printf("epoll failure!\n");
            break;
        }

        if(ENABLE_ET)
        {
            et_process(events, ret, epoll_fd, listen_fd);
        }
        else
        {
            lt_process(events, ret, epoll_fd, listen_fd);  
        }

    }

    close(listen_fd);
    return 0;

}

然后再写3个简易的TCP客户端来测试一下:

//客户端
#include <sys/types.h> 
#include <sys/socket.h> 
#include <stdio.h> 
#include <netinet/in.h> 
#include <arpa/inet.h> 
#include <unistd.h> 
#include <stdlib.h>
#include <sys/time.h>

int main() 
{ 
    int client_sockfd; 
    int len; 
    struct sockaddr_in address;//服务器端网络地址结构体 
     int result; 
    char str1[] = "ABCDE"; 
    char str2[] = "ABCDEFGHIJK"; 
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket 
    address.sin_family = AF_INET; 
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(8888); 
    len = sizeof(address); 
    result = connect(client_sockfd, (struct sockaddr *)&address, len); 
    if(result == -1) 
    { 
         perror("oops: client2"); 
         exit(1); 
    } 
    //第一次读写
    write(client_sockfd, str1, sizeof(str1)); 

    sleep(5);

    //第二次读写
    write(client_sockfd, str2, sizeof(str2)); 


    close(client_sockfd); 

    return 0; 
}

TCP客户端的动作是如此的:第一次首发送字符串”ABCDE”过去服务器端,伍秒后,再发字符串”ABCDEFGHIJK”过去服务端,大家着眼一下ET情势的服务器和LT情势的服务器在读取数据的点子上毕竟有哪些不一致。

ET模式

澳门金沙国际 4

ET格局现象分析:大家的服务器读缓冲区大小咱们设置了十。第一次收受字符串时,大家的缓冲区有丰盛的上空切受它,所以打字与印刷出内容”ABCDE”并且打字与印刷出”read
later”表示数据已经读完了。第二回收取字符串时,大家的缓冲区空间不足以接收全体的字符,所以分了三遍接受。但是总触发次数仅为叁遍。

LT模式

澳门金沙国际 5

LT格局现象分析:
同理,第叁次接受字符串有丰裕的上空中接力受,第贰回接受字符串缓冲区空间不足,所以第三回收到时分了一次来经受。同时也只顾到,只要你未有完全接受完上次的多少,内核就会持续通告你去接收数据!所以事件触发的次数是二回。

epoll的误区

EPOLLONESHOT事件

固然大家使用ET情势,1个socket上的某些事件照旧恐怕被触发数次,那在并发程序中就会吸引部分难点。比如贰个县城在读取完某些socket上的多寡后初步次拍卖卖这么些多少,而在数额的出来经过中该socket上又有新数据可读(EPOLLIN再一次被触发),此时另三个县份被唤醒来读取那几个新数据。于是就出现了多个线程同时操作一个socket的局面。那自然不是大家所愿意的,大家目的在于的是1个socket连接在任权且刻都只被二个线程处理。那或多或少方可选取EPOLLONESHOT事件达成。

对于注册了EPOLLONSHOT事件的文本描述符,操作系统最多触发其上注册的三个可读、可写也许格外事件,且只触发1次,除非我们接纳epoll_ctl函数重置该文件讲述符上注册的EPOLLONESHOT事件。那样,当2个线程在处理有个别socket时,别的线程是不容许有空子操作该socket的。但转头考虑,注册了EPOLLONESHOT事件的socket一旦被有些线程处理达成,该线程就相应及时重置那一个socket上的EPOLLONESHOT事件,以确认保障那个socket下2次可读时,其EPOLLIN事件能被触发,进而让任何干活线程有时机继续处理那一个socket。

下边是一个应用了EPOLLONESHOT的epoll服务器

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <stdbool.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10

struct fds
{
    int epollfd;
    int sockfd;
};

int SetNonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

void AddFd(int epollfd, int fd, bool oneshot)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    if(oneshot)
    {
        event.events |= EPOLLONESHOT;
    }

    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    SetNonblocking(fd);
}

/*重置fd上的事件,这操作以后,尽管fd上的EPOLLONESHOT事件被注册,但是操作系统仍然会触发fd上的EPOLLIN事件,且只触发一次*/
void reset_oneshot(int epollfd, int fd)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

/*工作线程*/
void* worker(void* arg)
{
    int sockfd = ((struct fds*)arg)->sockfd;
    int epollfd = ((struct fds*)arg)->epollfd;
    printf("start new thread to receive data on fd: %d\n", sockfd);
    char buf[BUFFER_SIZE];
    memset(buf, 0, BUFFER_SIZE);

    while(1)
    {
        int ret = recv(sockfd, buf,BUFFER_SIZE-1, 0);
        if(ret == 0)
        {
            close(sockfd);
            printf("foreigner closed the connection\n");
            break;
        }
        else if(ret < 0)
        {
            if(errno = EAGAIN)
            {
                reset_oneshot(epollfd, sockfd);
                printf("read later\n");
                break;
            }
        }
        else
        {
            printf("get content: %s\n", buf);
            //休眠5秒,模拟数据处理过程
            printf("worker working...\n");
            sleep(5);
        }
    }
    printf("end thread receiving data on fd: %d\n", sockfd);
}

int main(int argc, char* argv[])
{
    if(argc <= 2)
    {
        printf("usage: ip_address + port_number\n");
        return -1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    int ret = -1;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    if(listenfd < 0)
    {
        printf("fail to create socket!\n");
        return -1;
    }

    ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
    if(ret == -1)
    {
        printf("fail to bind socket!\n");
        return -1;
    }

    ret = listen(listenfd, 5);
    if(ret == -1)
    {
        printf("fail to listen socket\n");
        return -1;
    }

    struct epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    if(epollfd == -1)
    {
        printf("fail to create epoll\n");
        return -1;
    }

    //注意,监听socket listenfd上是不能注册EPOLLONESHOT事件的,否则应用程序只能处理一个客户连接!因为后续的客户连接请求将不再触发listenfd的EPOLLIN事件
    AddFd(epollfd, listenfd, false);


    while(1)
    {
        int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);  //永久等待
        if(ret < 0)
        {
            printf("epoll failure!\n");
            break;
        }

        int i;
        for(i = 0; i < ret; i++)
        {
            int sockfd = events[i].data.fd;
            if(sockfd == listenfd)
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof(client_address);
                int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
                //对每个非监听文件描述符都注册EPOLLONESHOT事件
                AddFd(epollfd, connfd, true);
            }
            else if(events[i].events & EPOLLIN)
            {
                pthread_t thread;
                struct fds fds_for_new_worker;
                fds_for_new_worker.epollfd = epollfd;
                fds_for_new_worker.sockfd = events[i].data.fd;
                /*新启动一个工作线程为sockfd服务*/
                pthread_create(&thread, NULL, worker, &fds_for_new_worker);

            }
            else
            {
                printf("something unexpected happened!\n");
            }
        }
    }

    close(listenfd);

    return 0;
}

澳门金沙国际 6

EPOLLONESHOT格局现象分析:大家再三再四采纳方面包车型客车TCP客户端来测试,需求修改一下客户端的sleep时间改为3秒。工作流程便是:客户端第三回发送数据时服务器的接收缓冲区是有丰盛空间的,然后服务器的行事线程进入伍秒的处理数量阶段;3秒后客户端继续发送新数据恢复生机,然而工作线程还在处理数据,不能立时接受新的多寡。二秒后,客户端该线程数据处理完了,开首收取新的数量。能够洞察到,大家客户端只使用了同二个线程去处理同多个客户端的央浼,符合预期。

EPOLLONESHOT事件

不怕大家接纳ET形式,三个socket上的有些事件可能只怕被触发多次,那在并发程序中就会引发部分难点。比如1个县城在读取完有些socket上的数额后起头拍卖这个多少,而在数额的出来经过中该socket上又有新数据可读(EPOLLIN再度被触发),此时另1个县份被提醒来读取那么些新数据。于是就应运而生了八个线程同时操作多个socket的范围。那当然不是我们所期望的,我们盼望的是贰个socket连接在任目前刻都只被3个线程处理。那或多或少得以行使EPOLLONESHOT事件达成。

对于注册了EPOLLONSHOT事件的文书描述符,操作系统最多触发其上注册的贰个可读、可写只怕相当事件,且只触发2回,除非大家应用epoll_ctl函数重置该文件讲述符上注册的EPOLLONESHOT事件。那样,当3个线程在处理有个别socket时,其余线程是不容许有时机操作该socket的。但转头思量,注册了EPOLLONESHOT事件的socket1旦被某些线程处理完成,该线程就应该马上重置那么些socket上的EPOLLONESHOT事件,以保障这些socket下2回可读时,其EPOLLIN事件能被触发,进而让别的干活线程有机遇继续处理那几个socket。

上边是七个选取了EPOLLONESHOT的epoll服务器

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <stdbool.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10

struct fds
{
    int epollfd;
    int sockfd;
};

int SetNonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

void AddFd(int epollfd, int fd, bool oneshot)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    if(oneshot)
    {
        event.events |= EPOLLONESHOT;
    }

    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    SetNonblocking(fd);
}

/*重置fd上的事件,这操作以后,尽管fd上的EPOLLONESHOT事件被注册,但是操作系统仍然会触发fd上的EPOLLIN事件,且只触发一次*/
void reset_oneshot(int epollfd, int fd)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

/*工作线程*/
void* worker(void* arg)
{
    int sockfd = ((struct fds*)arg)->sockfd;
    int epollfd = ((struct fds*)arg)->epollfd;
    printf("start new thread to receive data on fd: %d\n", sockfd);
    char buf[BUFFER_SIZE];
    memset(buf, 0, BUFFER_SIZE);

    while(1)
    {
        int ret = recv(sockfd, buf,BUFFER_SIZE-1, 0);
        if(ret == 0)
        {
            close(sockfd);
            printf("foreigner closed the connection\n");
            break;
        }
        else if(ret < 0)
        {
            if(errno = EAGAIN)
            {
                reset_oneshot(epollfd, sockfd);
                printf("read later\n");
                break;
            }
        }
        else
        {
            printf("get content: %s\n", buf);
            //休眠5秒,模拟数据处理过程
            printf("worker working...\n");
            sleep(5);
        }
    }
    printf("end thread receiving data on fd: %d\n", sockfd);
}

int main(int argc, char* argv[])
{
    if(argc <= 2)
    {
        printf("usage: ip_address + port_number\n");
        return -1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    int ret = -1;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    if(listenfd < 0)
    {
        printf("fail to create socket!\n");
        return -1;
    }

    ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
    if(ret == -1)
    {
        printf("fail to bind socket!\n");
        return -1;
    }

    ret = listen(listenfd, 5);
    if(ret == -1)
    {
        printf("fail to listen socket\n");
        return -1;
    }

    struct epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    if(epollfd == -1)
    {
        printf("fail to create epoll\n");
        return -1;
    }

    //注意,监听socket listenfd上是不能注册EPOLLONESHOT事件的,否则应用程序只能处理一个客户连接!因为后续的客户连接请求将不再触发listenfd的EPOLLIN事件
    AddFd(epollfd, listenfd, false);


    while(1)
    {
        int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);  //永久等待
        if(ret < 0)
        {
            printf("epoll failure!\n");
            break;
        }

        int i;
        for(i = 0; i < ret; i++)
        {
            int sockfd = events[i].data.fd;
            if(sockfd == listenfd)
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof(client_address);
                int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
                //对每个非监听文件描述符都注册EPOLLONESHOT事件
                AddFd(epollfd, connfd, true);
            }
            else if(events[i].events & EPOLLIN)
            {
                pthread_t thread;
                struct fds fds_for_new_worker;
                fds_for_new_worker.epollfd = epollfd;
                fds_for_new_worker.sockfd = events[i].data.fd;
                /*新启动一个工作线程为sockfd服务*/
                pthread_create(&thread, NULL, worker, &fds_for_new_worker);

            }
            else
            {
                printf("something unexpected happened!\n");
            }
        }
    }

    close(listenfd);

    return 0;
}

澳门金沙国际 7

EPOLLONESHOT方式现象分析:大家继续应用方面包车型大巴TCP客户端来测试,须要修改一下客户端的sleep时间改为3秒。工作流程正是:客户端第2回发送数据时服务器的接收缓冲区是有丰裕空间的,然后服务器的劳作线程进入5秒的处理数据阶段;3秒后客户端继续发送新数据恢复生机,然而工作线程还在处理多少,无法立刻接受新的数码。二秒后,客户端该线程数据处理完了,起首收受新的多少。能够调查到,我们客户端只行使了同一个线程去处理同3个客户端的请求,符合预期。

壹.  epoll ET格局只帮忙非阻塞句柄?

其实也支撑阻塞句柄,只可是依照使用的应用意况,壹般只适合非阻塞使用,参见上文“EPOLL
ET与LT的莫过于行使”

 二.  epoll的共享内部存款和储蓄器?

epoll相对于select高效是因为从基本拷贝就绪文件讲述符的时候用了共享内部存款和储蓄器?
那是卓殊的,完毕的时候只是用了运用了copy_from_user跟__put_user实行基本跟用户虚拟空间数据交互,并从未共享内部存储器的api。

题目回顾

epoll供给再度op->poll的案由

因为等待队列中的有事件后会唤醒全部的历程,恐怕部分经过位于对头把事件消费后就径直删除了这一个事件,前面包车型客车进程唤醒后也许再未有事件消费了,所以需求重新判断poll,如若事件还在则投入rdlist中。当然消费完事件后不肯定会去除,等待队列中得以因此flag选项设置消费的法子。

 

epoll每一遍都将txlist中的LT事件不等用户消费就径直再次回到给rdlist,那么在用户消费了该事件后,导致事件不妥善,再一次调用epoll_wait,epoll_wait还会回到rdlist吗?

不会再次重临,因为在回去就绪列表在此以前会还调用三回revents
= epi->ffd.file->f_op->poll(epi->ffd.file, NULL)
来判断事件,如若事件发生了变通,就不在再次来到。

 

水源的等待队列:

水源为了补助对设备的不通访问,就要求统筹1个守候队列,等待队列中是一个个经过,当设备事件就绪后会提醒等待队列中的进程来消费事件。可是在使用select监听非阻塞的句柄时候,那几个队列不是用来落到实处非阻塞,而是实现情况的等候,即等待某些可读可写事件产生后通报监听的过程

 

水源的 poll技术就是为着poll/select设计的?

各种设备的驱动为了帮助操作系统虚拟文件系统对其的运用须求提供一多级函数,比如说read,write等,其中poll正是内部四个函数,为了select,poll达成,用来查询设备是还是不是可读或可写,或是不是处在某种特殊景况。

 

eventPoll的多少个连串

evnetpoll中有四个等待队列,

wait_queue_head_t wq;

wait_queue_head_t poll_wait;

前端用于调用epoll_wait()时,
大家就是”睡”在了这几个等待队列上…

后者用于那一个用于epollfd本事被poll的时候…
也等于说epollfd被别的epoll监视,调用其file->poll() 时。

对于本epoll监视的句柄有新闻的时候会向wq消息队列进行wakeup,同时对于poll_wait也会开始展览wakeup

 

eventPollfs实现的file opetion

只兑现了poll和realse,由于epoll自己也是文件系统,其讲述符也得以被poll/select/epoll监视,因而要求贯彻poll方法,具体正是ep_eventpoll_poll方法,他里头贯彻是将监听当前epollfd的线程插入到本身的poll_wait队列中,判断本人接听的句柄是不是有事件发生,假诺局地话供给将音讯再次回到给监听epollfd的epoll_wait,
具体方法是接下来扫描就绪的文书列表, 调用各样文件上的poll 检验是或不是真的就绪,
然后复制到用户空间,可是文件列表中有希望有epoll文件,
调用poll的时候有不小可能率会爆发递归,  所以用ep_call_nested 包装一下,
幸免死循环和过深的调用。具体参见难题递归深度检验(ep_call_nested)

 

epoll的线程安全题材

当3个线程阻塞在epoll_wait()上的时候,别的线程向里面添加新的文书讲述符是没难点的,假诺那个文件讲述符就绪的话,阻塞线程的epoll_wait()会被提示。可是借使正在监听的某文件讲述符被别的线程关闭的话详表现是未定义的。在多少
UNIX系统下,select会解除阻塞再次回到,而文件讲述符会被认为就绪,不过对这几个文件讲述符实行IO操作会退步(除非那些文件讲述符又被分配了),在Linux下,另3个线程关闭文件讲述符未有任何影响。但好歹,应当尽恐怕壁面3个线程关闭另三个线程在监听的文本讲述符。

 

递归深度检查测试(ep_call_nested)

epoll本人也是文本,也足以被poll/select/epoll监视,假设epoll之间交互监视就有非常大大概引致死循环。epoll的落到实处中,全体望产生递归调用的函数都由函数ep_call_nested进行包装,递归调用进度中冒出死循环或递归过深就会打破死循环和递归调用直接再次回到。该函数的贯彻依靠于二个表面包车型大巴大局链表nested_call_node(差别的函数调用使用差别的节点),每一次调用大概产生递归的函数(nproc)就向链表中添加八个涵盖当前函数调用上下文ctx(进度,CPU,或epoll文件)和处理的指标标识cookie的节点,通过检查评定是不是有同样的节点就足以了然是还是不是产生了死循环,检查链表中千篇一律上下文包涵的节点个数就能够掌握递归的吃水。参见参考贰。

 

何以供给创立三个文件系统:

一是能够在根本维护一些新闻,那几个音讯在数13回epoll_wait之间是涵养的(保存的是eventpoll结构)第2点是epoll本人也足以被poll/epoll

 

五个回调函数

Epoll向等待队列有三个函数交互,分别是调用对应装备的poll函数,在poll函数中调用ep_ptable_queue_proc函数,将如今经过插入到等候队列,钦命ep_poll_callback为提醒时的回调函数。Ep_poll_callback完毕将方今的句柄复制到rdlist并wakeup,eventpoll的wq等待队列。

参考文献:

 

  

教学了基石阻塞与非阻塞和poll机制, 并分析了select的兑现格局

授课poll的完毕,也为下篇博客做铺垫

Epoll的贯彻,是自个儿的入门博客

很全面,很系统,讲解了poll机制和select/poll/epoll实现

任课poll机制,相关性十分的小,不过对基础的等候队列了解有匡助

牛客网注释

应用封神之八种!哈哈

2018-6-3 append: 

select / poll / epoll: practical difference for system architects
:   

 

相关文章