网络编制程序,从贯彻到应用

By admin in 美高梅手机版4858 on 2019年4月9日

近年来有诸如此类八个气象:笔者是3个很忙的大业主,作者有九16个手提式有线电话机,手提式有线电话机来消息了,我的文书就会告知我“高管,你的手提式有线电话机来信息了。”笔者很生气,笔者的秘书正是那样子,每便手提式无线电话机来新闻就只告诉本人来音讯了,COO尽快去看。可是他绝非把话说领会:到底是哪个手机来新闻啊!笔者可有玖几个手提式有线电话机啊!于是,笔者不得不多个3个有线电话去查看,来规定毕竟是哪多少个手提式有线电话机来消息了。那就是IO复用中select模型的缺陷!COO心想,假若书记能把来新闻的无绳电话机一贯获得作者桌子上就好了,那么自个儿的频率必然大增(那正是epoll模型)。

近年来有那样3个现象:作者是2个很忙的大业主,小编有915个手提式有线话机,手提式无线话机来音讯了,作者的秘书就会告诉自个儿“COO,你的手机来消息了。”笔者很生气,作者的书记正是那样子,每一回手提式有线话机来新闻就只告诉自身来音讯了,COO尽快去看。不过她绝非把话说知道:到底是哪个手提式有线电话机来音讯啊!小编可有九二12个手提式有线电话机啊!于是,作者只可以2个2个有线电话去查看,来明确到底是哪多少个手机来音信了。那就是IO复用中select模型的缺陷!总高管心想,借使书记能把来新闻的无绳电话机直接获得本身桌子上就好了,那么小编的作用肯定大增(那正是epoll模型)。

I/O多路复用是在二十多线程或多进度编制程序中常用技术。首假如由此select/epoll/poll多个函数协助的。在此首要对select和epoll函数详细介绍。

 

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

那大家先来计算一下select模型的弱项:

select函数

  • 该函数运维进程提示内核等待多少个事件中的任何四个产生,并唯有二个或三个事件发生或经历壹段钦点的光阴后才提示它。
  • 调用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,它报告内核等待所内定描述符中的其余1个妥贴可花多久。该参数有两种只怕:
      • 永久等待下去:仅在有1个叙述符准备好I/O时才回来,将其设为空指针
      • 等候1段固定时间:在有2个讲述符准备好I/O时重回,不过不超过由该参数所指向的timeval结构中钦命的秒数和阿秒数。
      • 一向不等待:检查描述符后随即重回,那就是轮询。为此,该参数必须指向3个timeval结构,但是当中的值必须安装为0
    • 多少个参数readset,writeset,exceptset钦赐大家要让内核测试读、写和充裕条件的讲述符。
    • 什么样给那多少个参数的每二个参数内定几个或多少个描述符值是2个安排上的题材。select使用描述符集,常常是三个整数数组,在那之中每一个整数中的每一人对应一个描述符。举例来说,借使使用30个人整数,那么该数组的首先个要素对应于描述符0~3一,第三个要素对应于描述符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?
      

      大家分配八个fd_set数据类型的描述符集,并用这几个宏设置或测试该集合中的每一人,也足以用C语言中的赋值语句把它赋值成此外多个叙述符集。
      留意:前边所谈论的各类描述符占用整数数组中的1人的章程只有是select函数的或是达成之壹。

    • maxfdp①参数钦命待测试的描述符个数,它的值是待测试的最大描述符加一。描述符0,一,贰,…,直到maxfdp壹 –
      1均被测试。

    • 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. 单个进程能够监视的文本讲述符的多少存在最大范围,日常是拾二4,当然能够变动数据,但出于select采纳轮询的不二等秘书诀扫描文件描述符,文件讲述符数量越来越多,质量越差;(在linux内核头文件中,有那样的定义:#define
    __FD_SETSIZE 1024)
  2. 基础 /
    用户空间内部存款和储蓄器拷贝难题,select须求复制大量的句柄数据结构,产生巨大的开发;
    select重返的是含有整个句柄的数组,应用程序需求遍历整个数组才能窥见什么样句柄爆发了轩然大波;
  3. select的接触格局是水平触发,应用程序假如未遂对2个曾经就绪的文书讲述符实行IO操作,那么将来每便select调用照旧会将那么些文件讲述符文告进程。

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

  • 满意下列八个原则之一的别样三个时,3个套接字准备好读:
    • 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当下大小。对于那样的套接字执行读操作不会堵塞并将重返1个大于0的值(也正是回来准备好读入的多少)。我们使用SO_RECVLOWAT套接字选项设置套接字的低水位标记。对于TCP和UDP套接字而言,其暗许值为1
    • 该连接的读半部关闭(也等于接到了FIN的TCP连接)。对那样的套接字的读操作将不打断并再次回到0(也正是重返EOF)
    • 该套接字时一个监听套接字且已做到的连接数不为0。
    • 其上有二个套接字错误待处理。对如此的套接字的读操作将不打断并重返-一(也正是回来贰个不当),同时把errno设置为适龄的谬误条件。这么些待处理错误也能够由此SO_E凯雷德RO奥迪Q7套接字选项调用getsockopt获取并免除。
  • 下列多个原则的别样1个满意时,一个套接字准备好写:
    • 该套接字发送缓冲区中的可用字节数大于等于套接字发送缓冲区低水位标记的当下高低,并且或该套接字已两次三番,也许该套接字不必要一连(如UDP套接字)。那象征1旦大家把那样的套接字设置成非阻塞的,写操作将不封堵并赶回一个正值(如由传输层接收的字节数)。咱们利用SO_SNDLOWAT套接字选项来安装该套接字的低水位标记。对于TCP和UDP而言,暗中同意值为2048
    • 该连接的写半部关闭。对这么的套接字的写操作将生出SIGPIPE实信号
    • 接纳非阻塞式connect套接字已创立连接,也许connect已经已破产告终
    • 其上有贰个套接字错误待处理。对那样的套接字的写操作将不封堵并赶回-一(也等于回来二个荒唐),同时把errno设置为适龄的荒谬条件。那几个待处理错误也能够由此SO_E猎豹CS六ROKoleos套接字选项调用getsockopt获取并清除。
  • 假若一个套接字存在带外数据依然仍居于带外标记,那么它有那么些条件待处理。
  • 留意:当某些套接字上发生错误时,它将由select标记为既可读又可写
  • 接过低水位标记和出殡和埋葬低水位标记的目的在于:允许采纳进度控制在select可读或可写条件在此以前有稍许数量可读或有多大空间可用于写。
  • 任何UDP套接字只要其发送低水位标记小于等于发送缓冲区大小(暗许应该总是那种关系)就总是可写的,那是因为UDP套接字不要求连接。

•    
当客户处理五个描述符时(例就如时处理交互式输入和互联网套接口) 

考虑一下之类场景:有⑩0万个客户端同时与一个服务器进度保持着TCP连接。而每权且刻,平日唯有几百上千个TCP连接是活泼的(事实上海大学部分情景都以那种场面)。怎样贯彻如此的高并发?

设想一下之类场景:有十0万个客户端同时与一个服务器进程保持着TCP连接。而每一整日,平日唯有几百上千个TCP连接是活跃的(事实上海南大学学部分情景都以那种气象)。如何兑现那样的高并发?

poll函数

  • 函数原型:

    #include<poll.h>
    
    int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
    
        返回:若有就绪描述符则为数目,若超时则为0,若出错则为-1
    
  • 首先个参数是指向三个构造数组首个因素的指针。每一种数组成分都以1个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成员中回到该描述符的状态。(每种描述符都有几个变量,三个为调用值,另三个为回到结果,从而幸免采纳值-结果参数。)

  • poll事件

美高梅手机版4858 1

•    
尽管1个TCP服务器既要处理监听套接口,又要拍卖已连接套接口 

总结总结一下,2个经过最多有十贰伍个文本描述符,那么大家须要开一千个进程来拍卖十0万个客户连接。假使大家选用select模型,这一千个经过里某壹段时间内唯有数个客户连接供给多少的收取,那么我们就不得不轮询10二多少个公文讲述符以分明毕竟是哪个客户有数量可读,想想假若一千个经过都有周围的作为,那系统能源消耗可有多大啊!

简言之总计一下,三个经过最多有十30个公文描述符,那么我们须求开1000个经过来拍卖十0万个客户连接。假若大家应用select模型,这一千个经过里某一段时间内唯有数个客户连接需求多少的收到,那么大家就只能轮询拾24个文件讲述符以分明究竟是哪位客户有多少可读,想想假如一千个经过都有接近的一坐一起,那系统财富消耗可有多大啊!

epoll函数

  • epoll是Linux特有的I/O复用函数。它在促成和选择上与select、poll有一点都不小的差异。
    • 第一,epoll使用1组函数来完结任务,而不是单个函数。
    • 说不上,epoll把用户关心的公文讲述符上的轩然大波放在内核里的2个事件表中,从而无须像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的相当慢运维12分重要。
    • data成员用于存款和储蓄用户数据,是3个联合体:

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

      其中5个分子用得最多的是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参数钦定)中复制到它的第2个参数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(艾德ge
      Trigger,边沿触发)方式。对于ET工作形式下的文本描述符,当epoll_wait检查评定到其上有事件产生并将此事件通报应用程序后,应用程序必须及时处理该事件,因为延续的epoll_wait调用将不再向应用程序公告那1轩然大波。
    • ET方式在十分的大程度上降低了同二个epoll事件被再一次触发的次数。由此功能要比LT形式高。
    • 每一个使用ET形式的公文讲述符都应该是非阻塞的。假若文件讲述符是阻塞的,那么读或写操作将会因为尚未继承的时光而一向处于阻塞状态(饥渴状态)
  • EPOLLONESHOT事件
    • 尽管使用ET方式,三个socket上的某部事件如故大概被触发多次。那在并发程序中引起一个难题。比如2个线程(或进程)在读取完某些socket上的数目后起首拍卖那个多少,而在数额的处理进程中该socket上又有新数据可读(EPOLLIN再一次被触发),此时其余四个线程被提醒来读取那个新的多少。于是应运而生了四个线程同时操作1个socket的外场。那本来不是大家期待的。大家期待的是3个socket连接在任一时半刻刻都只被2个线程处理。
    • 对此注册了EPOLLONESHOT事件的文本描述符,操作系统最多触发其上登记的贰个可读、可写或尤其事件,且只触发叁遍,除非大家使用epoll_ctl函数重置该文件讲述符上的EPOLLONESHOT事件。那样,当贰个线程在处理有些socket时,其余线程时不容许有时机操作该socket的。但转头思索,注册了EPOLLONESHOT事件的socket1旦被有些线程处理达成,该线程就应该马上重置那一个socket上的EPOLLONESHOT事件,以有限支持那么些socket下一次可读时,其EPOLLIN事件能被触发,进而让别的干活线程有机会继续处理这一个socket.

•    
假诺2个服务器即要处理TCP,又要拍卖UDP 

针对select模型的症结,epoll模型被建议来了!

本着select模型的弱项,epoll模型被提议来了!

•    
假设3个服务器要处理多少个劳务或多少个商讨 

epoll模型的帮助和益处

  • 支撑一个历程打开大数据的socket描述符
  • IO成效不随FD数目扩张而线性下落
  • 使用mmap加快内核与用户空间的信息传递

epoll模型的帮助和益处

  • 帮助三个进度打开大数目标socket描述符
  • IO功效不随FD数目增添而线性下落
  • 利用mmap加快内核与用户空间的新闻传递

select/poll/epoll差别

  1. poll重回的时候用户态须求轮询判断每一种描述符的情景,即便唯有三个叙述符就绪,也要遍历整个集合。假诺集合中活跃的讲述符很少,遍历进度的费用就会变得不小,而只要集合中多数的叙述符都是活泼的,遍历进度的支出又足以忽略。epoll的兑现中年老年是只遍历活跃的描述符,在外向描述符较少的情状下就会很有优势,在代码的解析进程中能够看出epoll的达成过于复杂并且其促成进度为达成线程安全需求联合处理(锁),如若大多数叙述符都以虎虎有生气的,遍历那一点分别相对于加锁来说早已不值1提了,epoll的频率只怕不及select或poll。
  2. 传参格局各异 
    • 帮助的最大描述符分化,根本原因是基本管理各类文件句柄的数据结构不一致,select能够处理的最大fd不可能超出FDSETSIZE,因为调用select传入的参数fd_set是2个位数组,数组大小就是FDSETSIZE私下认可为十二四,所以调用方式界定了并发量。Poll是应用一个数组传入的参数,未有最大范围。Epoll不需求每一趟都传出,因为会调用epoll_ctl添加。
    • 使用格局分歧,select调用每趟都以因为内核会对数组实行在线修改,应用程序下次调用select前不得不重置那八个fdset,而poll比他聪明点,将句柄与事件绑定在一块儿通过二个struct
      pollfd实现,再次回到时是通过其revets完毕,所以不必要重置该协会,直接传送就行,epoll不必要传递。
    • 援救的事件类型数分化:select应为未有将句柄与事件展开绑定,所以fd_set仅仅是个文本讲述符集合,由此要求多少个fd_set分别传入可读可写及尤其事件,这使得他不可能处理越多门类的事件,而poll选择的pollfd中event要求动用6三个bit,epoll采纳的
      epoll_event则要求92个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是线程安全的,内部提供了锁的爱抚,固然1个线程在epoll_wait的时候另三个线程epoll_ctl也没难题。
  6. 根本使用了slab机制,为epoll提供了长足的数据结构。
  7. Select和poll相当于epoll的LT格局,不协助ET情势,epoll帮助尤其该高速的ET格局 
    (ET和LT差异见下文)

 

epoll的三种工作方式

  • LT(level triggered,水平触发形式)是缺省的干活情势,并且同时帮忙block 和 non-block
    socket。在那种做法中,内核告诉您3个文本讲述符是不是安妥了,然后你能够对那几个就绪的fd实行IO操作。假使你不作任何操作,内核依旧会再三再四公告你的,所以,那种格局编制程序出荒谬也许要小一些。比如基本公告你个中二个fd能够读数据了,你赶紧去读。你依然懒懒散散,不去读那几个数额,下3遍巡回的时候基本发现你还没读刚才的数额,就又通知你尽快把刚刚的多少读了。那种机制得以相比较好的保管每一种数据用户都处理掉了。

  • ET(edge-triggered,边缘触发形式)是便捷工作章程,只支持no-block
    socket。在那种格局下,当描述符从未就绪变为就绪时,内核通过epoll告诉您。然后它会倘若你领悟文书讲述符已经就绪,并且不会再为那多少个文件讲述符发送越来越多的服服帖帖通告,等到下次有新的数量进来的时候才会再也起身妥当事件。简单的讲,便是水源文告过的作业不会再说第3次,数据错过没读,你本身担当。那种机制真正速度拉长了,不过危害相伴而行。

epoll的二种工作格局

  • LT(level triggered,水平触发形式)是缺省的办事方法,并且同时扶助block 和 non-block
    socket。在那种做法中,内核告诉您七个文件讲述符是不是稳妥了,然后您能够对这一个就绪的fd进行IO操作。假设你不作任何操作,内核照旧会一连通告你的,所以,那种方式编制程序出荒唐恐怕要小1些。比如基本通告你个中八个fd能够读数据了,你尽快去读。你仍然懒懒散散,不去读那一个数量,下贰次巡回的时候基本发现你还没读刚才的数量,就又公告你快速把刚刚的数目读了。那种机制能够相比好的担保每一种数据用户都处理掉了。

  • ET(edge-triggered,边缘触发格局)是火速工作格局,只支持no-block
    socket。在那种形式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会若是你驾驭文书讲述符已经就绪,并且不会再为那么些文件讲述符发送越多的安妥公告,等到下次有新的多寡进来的时候才会重复启程妥当事件。简单来讲,正是根本布告过的工作不会再说第一回,数据错过没读,你协调担负。那种体制真正速度进步了,不过危害相伴而行。

epoll工作原理

epoll_create

操作系统在运维时会登记3个evnetpollfs的文件系统,对应的file
operations只是达成了poll跟release操作,然后开端化一些数据结构,例如二个slab缓存,以便后边简化epitem和eppoll_entry对象的分红,
初阶化递归检查队列等。

制造1个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实行睡眠,若是超时可能被升迁,首先从友好起始化的等候队列删除
,然后开首拷贝资源给用户空间了。

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

再者逐一判断其是还是不是为水平触发,是的话再度插入到就绪链表。

   

现实贯彻由许多细节:
就算拷贝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,第三个参数是 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模型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,第3个参数是 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拷贝到三个一时半刻链表txlist,
然后如假若LT事件同时事件就绪的话fd被再次放回了rdllist。那么下次epoll_wait当然会又把rdllist里的fd拿来拷给用户了。举个例子。假若一个socket,只是connect,还未曾收发数据,那么它的poll事件掩码总是有POLLOUT的,每一趟调用epoll_wait总是回到POLLOUT事件,因为它的fd就接二连3被放回rdllist;假诺此时有人往这几个socket里写了一大堆数据,造成socket塞住,fd不会放回rdllist,epoll_wait将不会再再次来到用户POLLOUT事件。假使大家给那个socket加上EPOLLET,然后connect,没有收发数据,epoll_wait只会回来1回POLLOUT通告给用户(因为此fd不会再回去rdllist了),接下去的epoll_wait都不会有别的事件通报了。

 

美高梅手机版4858 ,注意地方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由不足写变为可写的时候,即由满状态变为不满状态的时候。

(二)
当有旧数据被发送走时,即buffer中待写的内容降少得时候。

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

 

对此LT格局则简单多了,除了上述操作为读了一条事件就绪就直接通告。

 

ET比LT高效的原由:

透过地方的剖析,可获得LT每趟都亟待处理rdlist,无疑向用户拷贝的数码变多,且epoll_wait循环也变多,质量自然降低了。

其余3只从用户角度想念,使用ET形式,它能够省事的处理EPOLLOUT事件,省去打开与关闭EPOLLOUT的epoll_ctl(EPOLL_CTL_MOD)调用。从而有希望让你的属性得到一定的晋级。例如你须求写出1M的多少,写出到socket
256k时,重回了EAGAIN,ET方式下,当再度epoll再次来到EPOLLOUT事件时,继续写出待写出的数据,当未有数据必要写出时,不处理直接略过即可。而LT格局则要求先开辟EPOLLOUT,当未有多少必要写出时,再关闭EPOLLOUT(不然会直接再次回到EPOLLOUT事件),而调用epoll_ctl是系统调用,要陷入内核并且必要操作加锁红黑树,总体来说,ET处理EPOLLOUT方便神速些,LT不不难遗漏事件、不易发生bug,若是server的响应平日较小,不会触发EPOLLOUT,那么适合采纳LT,例如redis等,那种情景下居然不供给关怀EPOLLOUT,流量丰富小的时候一直发送,假使发送不完在开始展览关切EPOLLOUT,发送完撤废关怀就行了,能够拓展多少的优化。而nginx作为高品质的通用服务器,网络流量能够跑满达到1G,那种情状下很不难触发EPOLLOUT,则利用ET。

参见搜狐

 

事实上选取:

当epoll工作在ET方式下时,对于读操作,假设read一遍未有读尽buffer中的数据,那么下次将得不到读就绪的打招呼,造成buffer中已有的数据无机会读出,除非有新的数码再一次抵达。对于写操作,首如若因为ET情势下fd平日为非阻塞导致的八个难点——怎么样确认保障将用户供给写的多寡写完。

要消除上述五个ET情势下的读写难点,大家必须贯彻:

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

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

 

行使那种办法自然要使每一种连接的套接字工作于非阻塞形式,因为读写须求一向读或写直到出错(对于读,当读到的其实字节数小于请求字节数时就能够告壹段落),而一旦您的文书讲述符假使不是非阻塞的,那这些平素读或直接写势必会在终极一次阻塞。那样就无法在堵塞在epoll_wait上了,造成任何文件讲述符的职责饿死。

由此也就常说“ET供给工作在非阻塞格局”,当然那并不能够表达ET无法做事在堵塞格局,而是工作在堵塞形式只怕在运作中会出现一些题材。

 

ET形式下的accept

   
思索那种情景:四个三番五次同时抵达,服务器的 TCP
就绪队列弹指间积累八个就绪

老是,由于是边缘触发形式,epoll
只会通报1遍,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
调用上,直到其余有个别客户建立三个新的接连完结。然则在此时期,服务器单纯地打断在accept
调用上(实际应该阻塞在select上),就绪队列中的别的描述符都得不到处理。

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

某些连接时,accept 调用可以即时重返 -1,
那时源自 Berkeley 的兑现会在基本中处理该事件,并不会将该事件通报给
epoll,而任何达成把 errno 设置为 ECONNABOCRUISERTED 或许 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

      
在1些监听事件和读取分离的景色中,比如说在主线程中监听,在子线程中接收数据并处理,那时候晤面世七个线程同时操作一个socket的规模,比如说主线程监听到事件交由线程1拍卖,还未处理完又有事件到达,主线程交由线程二甩卖,这就造成数据不均等,1般意况下需求在该文件讲述符上注册EPOLLONESHOT事件,操作系统最多触发其上注册的三个可读可写或尤其事件,且只触发2回,除非大家选取epoll_ctl函数重置该EPOLLONESHOT事件。反过来思量也1如既往,注册了该事件的线程处理完数据后必须重新注册,不然下次不会另行接触。参见《linux高质量服务器编制程序》九.③.4节

             
不过有3个败笔,那样的话会每一次都调用epoll_ctrl陷入内核,并且epoll为确认保证线程安全会使用了加锁红黑树,这样会严重影响属性,此时就要求换壹种思路,在应用层维护1个原子整数或称为flag来记录当前句柄是或不是有线程在拍卖,每一遍有事件来临得时候会检查这些原子整数,如若在拍卖就不会分配线程处理,不然会分配线程,那样就制止了深陷内核,使用epoll_data来储存这一个原子整数就行。

      
对于使用EPOLLSHOT格局来防护数据不1致既能够选择ET也得以运用LT,因为她防止了重新接触,可是使用原子整数的格局只好使用ET格局,他不是幸免再度接触,而是防止被多少个线程处理,在多少意况下也许总括的速度跟不上io涌来的快慢,便是无力回天及时接收缓冲区的情节,此时收受线程和主线程是分开的,假设选择LT的话主线程会平素触发事件,导致busy-loop。
而使用ET触发只有在事件来临得时候会触发,缓冲区有内容并不会触发,触发的次数就减少了,固然主线程依然恐怕空转(fd有事件来临,但已被线程处理,此时不要求处理,继续epoll_wait就好),但这么空转比屡次调用epoll_ctl的可能率小多了。

   
上边的化解措施相似完美,其实存在竞态的气象,倘使线程1检查flag为false,未有线程处理这么些socket,准备去接受处理的时候被调出CPU了,线程二获得cput后也同样发现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;

}

接下来再写一个简易的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客户端的动作是那样的:第3遍先发送字符串”ABCDE”过去服务器端,伍秒后,再发字符串”ABCDEFGHIJK”过去服务端,大家旁观一下ET方式的服务器和LT情势的服务器在读取数据的办法上究竟有啥界别。

ET模式

美高梅手机版4858 2

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

LT模式

美高梅手机版4858 3

LT方式现象分析:
同理,第三次收受字符串有丰盛的半空中接受,第二回接到字符串缓冲区空间欠缺,所以第2回收取时分了两遍来接受。同时也只顾到,只要你没有完全选取完上次的数目,内核就会一而再文告你去接收数据!所以事件触发的次数是二次。

带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;

}

接下来再写三个简便的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模式

美高梅手机版4858 4

ET形式现象分析:大家的服务器读缓冲区大小大家设置了拾。第贰遍收受字符串时,大家的缓冲区有充分的上空切受它,所以打字与印刷出内容”ABCDE”并且打字与印刷出”read
later”表示数据现已读完了。第一遍接到字符串时,大家的缓冲区空间不足以接收全体的字符,所以分了五遍收取。但是总触发次数仅为一次。

LT模式

美高梅手机版4858 5

LT形式现象分析:
同理,首次接受字符串有丰盛的半空中接受,第三遍收取字符串缓冲区空间欠缺,所以第二次收受时分了一次来接受。同时也留意到,只要您未曾完全选择完上次的数量,内核就会两次三番通告你去接收数据!所以事件触发的次数是一次。

epoll的误区

EPOLLONESHOT事件

即使大家使用ET格局,三个socket上的某部事件依然恐怕被触发数十次,这在并发程序中就会掀起部分难点。比如两个县城在读取完有个别socket上的数量后初阶拍卖那几个多少,而在多少的出来经过中该socket上又有新数据可读(EPOLLIN再一次被触发),此时另八个县份被唤醒来读取那个新数据。于是就应运而生了多个线程同时操作2个socket的规模。那当然不是大家所愿意的,大家愿意的是一个socket连接在任一时半刻刻都只被1个线程处理。那或多或少方可选拔EPOLLONESHOT事件达成。

对此注册了EPOLLONSHOT事件的文书描述符,操作系统最多触发其上登记的1个可读、可写只怕特别事件,且只触发一回,除非大家利用epoll_ctl函数重置该公文讲述符上注册的EPOLLONESHOT事件。那样,当贰个线程在拍卖有些socket时,其余线程是不容许有机会操作该socket的。但转头考虑,注册了EPOLLONESHOT事件的socket1旦被有个别线程处理完成,该线程就活该立刻重置那些socket上的EPOLLONESHOT事件,以管教那些socket下三回可读时,其EPOLLIN事件能被触发,进而让其余工作线程有机遇继续处理那几个socket。

下边是1个运用了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;
}

美高梅手机版4858 6

EPOLLONESHOT形式现象分析:大家继承使用方面的TCP客户端来测试,要求修改一下客户端的sleep时间改为3秒。工作流程就是:客户端第二回发送数据时服务器的接收缓冲区是有丰硕空间的,然后服务器的行事线程进入五秒的处理数量阶段;3秒后客户端继续发送新数据恢复生机,然而工作线程还在处理数据,不能霎时接受新的数目。二秒后,客户端该线程数据处理完了,早先接到新的数码。可以调查到,大家客户端只行使了同一个线程去处理同3个客户端的请求,符合预期。

EPOLLONESHOT事件

尽管大家应用ET形式,四个socket上的某部事件依然只怕被触发多次,那在并发程序中就会抓住部分题材。比如3个县城在读取完某些socket上的数额后开端拍卖那几个数据,而在数据的出来经过中该socket上又有新数据可读(EPOLLIN再度被触发),此时另一个县份被唤起来读取这一个新数据。于是就应运而生了多个线程同时操作三个socket的框框。那当然不是大家所期望的,大家愿意的是贰个socket连接在任权且刻都只被一个线程处理。那点足以动用EPOLLONESHOT事件完毕。

对此注册了EPOLLONSHOT事件的文件描述符,操作系统最多触发其上注册的3个可读、可写或许尤其事件,且只触发三次,除非大家利用epoll_ctl函数重置该公文讲述符上注册的EPOLLONESHOT事件。那样,当二个线程在拍卖有个别socket时,别的线程是不或然有空子操作该socket的。但转头考虑,注册了EPOLLONESHOT事件的socket壹旦被某些线程处理完成,该线程就应该立时重置那一个socket上的EPOLLONESHOT事件,以保证这一个socket下一次可读时,其EPOLLIN事件能被触发,进而让任何干活线程有时机继续处理这些socket。

上面是2个用到了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;
}

美高梅手机版4858 7

EPOLLONESHOT格局现象分析:大家两次三番运用方面包车型地铁TCP客户端来测试,须求修改一下客户端的sleep时间改为3秒。工作流程就是:客户端第一回发送数据时服务器的接收缓冲区是有丰盛空间的,然后服务器的办事线程进入五秒的处理数量阶段;3秒后客户端继续发送新数据恢复生机,但是工作线程还在处理数据,无法立时接受新的多少。2秒后,客户端该线程数据处理完了,初叶收受新的多寡。可以洞察到,大家客户端只行使了同二个线程去处理同多少个客户端的央求,符合预期。

1.  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)
来判断事件,就算事件产生了变化,就不在再次回到。

 

基本的守候队列:

基础为了帮忙对装备的堵塞访问,就供给统一筹划二个等待队列,等待队列中是一个个进度,当设备事件就绪后会唤起等待队列中的进度来消费事件。然则在动用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的线程安全难题

当2个线程阻塞在epoll_wait()上的时候,别的线程向里面添加新的文书讲述符是没问题的,倘若那个文件讲述符就绪的话,阻塞线程的epoll_wait()会被提示。可是借使正在监听的某文件讲述符被别的线程关闭的话详表现是未定义的。在多少
UNIX系统下,select会解除阻塞重返,而文件讲述符会被认为就绪,可是对那几个文件讲述符进行IO操作会战败(除非这些文件讲述符又被分配了),在Linux下,另三个线程关闭文件讲述符未有任何影响。但无论怎么着,应当尽可能壁面1个线程关闭另多个线程在监听的文本讲述符。

 

递归深度检查评定(ep_call_nested)

epoll本人也是文本,也可以被poll/select/epoll监视,如若epoll之间交互监视就有希望引致死循环。epoll的贯彻中,全数一点都不小概率发生递归调用的函数都由函数ep_call_nested实行打包,递归调用进程中出现死循环或递归过深就会打破死循环和递归调用直接回到。该函数的兑现依靠于一个表面包车型地铁全局链表nested_call_node(分裂的函数调用使用不一致的节点),每一遍调用可能发生递归的函数(nproc)就向链表中添加3个涵盖当前函数调用上下文ctx(进度,CPU,或epoll文件)和处理的对象标识cookie的节点,通过检查评定是还是不是有相同的节点就足以领会是不是产生了死循环,检查链表中同样上下文包括的节点个数就足以知道递归的纵深。参见参考二。

 

怎么须要创建一个文件系统:

壹是足以在基本维护一些音讯,这几个音信在频繁epoll_wait之间是保持的(保存的是eventpoll结构)第壹点是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
:   

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图
Copyright @ 2010-2019 美高梅手机版4858 版权所有