Linux 30 Linux poll和epoll的使用

poll

poll函数与select函数差不多
函数原型:

#include <poll.h>
int poll(struct pollfd fd[], nfds_t nfds, int timeout);
struct pollfd
{
    int fd; // 文件描述符
    short event;// 请求的事件
    short revent;// 返回的事件
}

每个pollfd结构体指定了一个被监视的文件描述符。第一个参数是一个数组,即poll函数可以监视多个文件描述符。每个结构体的events是监视该文件描述符的事件掩码,由用户来设置。revents是文件描述符的操作结果事件,内核在调用返回时设置。events中请求的任何事件都可能在revents中返回。合法的事件如下:

后三个只能作为描述字的返回结果存储在revents中,而不能作为测试条件用于events中。

这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。使用poll()和select()不一样,你不需要显式地请求异常情况报告。
POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。

例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。

第二个参数nfds:要监视的描述符的数目。

timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds参数超出PLIMIT_NOFILE值。
ENOMEM:可用内存不足,无法完成请求。

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <poll.h>

#define MAX_BUFFER_SIZE 1024
#define IN_FILES 3
#define TIME_DELAY 60*5
#define MAX(a,b) ((a>b)?(a):(b))

int main(int argc ,char **argv)
{
    struct pollfd fds[IN_FILES];
    char buf[MAX_BUFFER_SIZE];
    int i,res,real_read, maxfd;

	/* 打开文件 */
    fds[0].fd = 0;
    if((fds[1].fd=open("data1",O_RDONLY|O_NONBLOCK)) < 0)
    {
        fprintf(stderr,"open data1 error:%s",strerror(errno));
        return 1;
    }
    if((fds[2].fd=open("data2",O_RDONLY|O_NONBLOCK)) < 0)
    {
        fprintf(stderr,"open data2 error:%s",strerror(errno));
        return 1;
    }

	/* 设置请求的事件POLLIN */
    for (i = 0; i < IN_FILES; i++)
    {
        fds[i].events = POLLIN;
    }

	/* 可读 */
    while(fds[0].events || fds[1].events || fds[2].events)
    {
		/* poll返回结构体中revents不为0的文件描述符个数 */
        if (poll(fds, IN_FILES, TIME_DELAY) <= 0)
        {
            printf("Poll error\n");
            return 1;
        }
        for (i = 0; i < IN_FILES; i++)
        {
			/* 检查revents中的标志,对应于文件描述符请求的events */
            if (fds[i].revents)
            {
                memset(buf, 0, MAX_BUFFER_SIZE);
                real_read = read(fds[i].fd, buf, MAX_BUFFER_SIZE);
                if (real_read < 0)
                {
                    if (errno != EAGAIN)
                    {
                        return 1;
                    }
                }
                else if (!real_read)
                {
                    close(fds[i].fd);
                    fds[i].events = 0;
                }
                else
                {
                    if (i == 0)
                    {
                        if ((buf[0] == 'q') || (buf[0] == 'Q'))
                        {
                            return 1;
                        }
                    }
                    else
                    {
                        buf[real_read] = '\0';
                        printf("%s", buf);
                    }
                }
            }
        }
    }

    exit(0);
}

epoll

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

优点
支持一个进程打开大数目的socket描述符

select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译服务器代码,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大。

IO效率不随FD数目增加而线性下降

传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是“活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对“活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个“伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

使用mmap加速内核与用户空间的消息传递

这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核与用户空间mmap同一块内存实现的。

内核微调

这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小— 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包个数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。

epoll有2种工作方式:LT和ET

LT(level triggered)
是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET (edge-triggered)
是高速工作方式,只支持non-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。

epoll_create 创建一个epoll对象,一般epollfd = epoll_create()

epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件
比如
epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注册缓冲区非空事件,即有数据流入
epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//注册缓冲区非满事件,即流可以被写入
epoll_wait(epollfd,...)等待直到注册的事件发生

前言
I/O多路复用有很多种实现。在linux上,2.4内核前主要是select和poll,自Linux 2.6内核正式引入epoll以来,epoll已经成为了目前实现高性能网络服务器的必备技术。尽管他们的使用方法不尽相同,但是本质上却没有什么区别。
select的缺陷
高并发的核心解决方案是1个线程处理所有连接的“等待消息准备好”,这一点上epoll和select是无争议的。但select预估错误了一件事,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。select的使用方法是这样的:
返回的活跃连接 = = select(全部待监控的连接)。 什么时候会调用select方法呢?在你认为需要找出有报文到达的活跃连接时,就应该调用。所以,调用select在高并发时是会被频繁调用的。这样,这个频繁调用的方法就很有必要看看它是否有效率,因为,它的轻微效率损失都会被“频繁”二字所放大。它有效率损失吗?显而易见,全部待监控连接是数以十万计的,返回的只是数百个活跃连接,这本身就是无效率的表现。被放大后就会发现,处理并发上万个连接时,select就完全力不从心了。
此外,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数。
其次,内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时。看到这里,您可能要要问了,你为什么不提poll?笔者认为select与poll在内部机制方面并没有太大的差异。相比于select机制,poll只是取消了最大监控文件描述符数限制,并没有从根本上解决select存在的问题。
当并发连接为较小时,select与epoll似乎并无多少差距。可是当并发连接上来以后,select就显得力不从心了。
epoll高效的奥秘
epoll精巧的使用了3个方法来实现select方法要做的事:
1.新建epoll描述符==epoll_create()
2.epoll_ctrl(epoll描述符,添加或者删除所有待监控的连接)
3.返回的活跃连接 ==epoll_wait( epoll描述符 )

与select相比,epoll分清了频繁调用和不频繁调用的操作。例如,epoll_ctrl是不太频繁调用的,而epoll_wait是非常频繁调用的。这时,epoll_wait却几乎没有入参,这比select的效率高出一大截,而且,它也不会随着并发连接的增加使得入参越发多起来,导致内核执行效率下降。

要深刻理解epoll,首先得了解epoll的三大关键要素:mmap、红黑树、链表。

epoll是通过内核与用户空间mmap同一块内存实现的。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到epoll监听的句柄,效率高。

红黑树将存储epoll所监听的套接字。上面mmap出来的内存如何保存epoll所监听的套接字,必然也得有一套数据结构,epoll在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度O(logN)。


                 ----------------------------------------------------------------------------------
                 |   struct eventpoll                                                             |
                 |  {                                                                             |
                 |      spin_lock_t lock;            //对本数据结构的访问                           |
                 |       struct mutex mtx;            //防止使用时被删除                            |
                 |       wait_queue_head_t wq;        //sys_epoll_wait() 使用的等待队列             |
                 |       wait_queue_head_t poll_wait; //file->poll()使用的等待队列                  |
                 |       struct list_head rdllist;    //事件满足条件的链表                           |
                 |       struct rb_root rbr;          //用于管理所有fd的红黑树                       |
                 |       struct epitem *ovflist;      //将事件到达的fd进行链接起来发送至用户空间       |
                 |   }                                                                            |
                 ----------------------------------------------------------------------------------
                          ||
                          ||
                          ||
                          \/ 
                 -----------------------------------------------------------------------------------------
                 |   struct epitem                                                                       |
                 |   {                                                                                   |
                 |       struct rb_node rbn;            //用于主结构管理的红黑树                           |
                 |       struct list_head rdllink;      //事件就绪队列                                    |
                 |       struct epitem *next;           //用于主结构体中的链表                             |
                 |       struct epoll_filefd ffd;       //每个fd生成的一个结构         (BLACK)             |
                 |       int nwait;                                                                      |
                 |       struct list_head pwqlist;     //poll等待队列                                     |
                 |       struct eventpoll *ep;         //该项属于哪个主结构体                              |
                 |       struct list_head fllink;      //链接fd对应的file链表                             |
                 |       struct epoll_event event;     //注册的感兴趣的事件,也就是用户空间的epoll_event      |
                 |   }                                                                                   |
                 ----------------------------------------------------------------------------------------- 
                          //             \\
                         //               \\
                        //                 \\
                       \_                   _/
-----------------------------------      -------------------------------------
| struct epitem                   |      |  struct epitem                    |
| {                               |      |  {                                |
|     struct rb_node rbn;         |      |      struct rb_node rbn;          |
|     struct list_head rdllink;   |      |      struct list_head rdllink;    |
|     struct epitem *next;        |      |      struct epitem *next;         |
|     struct epoll_filefd ffd;    |      |      struct epoll_filefd ffd;     |
|     int nwait;                  |      |      int nwait;                   | 
|     struct list_head pwqlist;   |      |      struct list_head pwqlist;    |
|     struct eventpoll *ep;       |      |      struct eventpoll *ep;        |
|     struct list_head fllink;    |      |      struct list_head fllink;     |
|     struct epoll_event event;   |      |      struct epoll_event event;    |
| }                               |      |  }                                |
|           (RED)                 |      |              (RED)                |
-----------------------------------      -------------------------------------

添加以及返回事件
通过epoll_ctl函数添加进来的事件都会被放在红黑树的某个节点内,所以,重复添加是没有用的。当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,这个回调函数其实就所把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。


        ---------      ---------      ---------      ---------
        |   |   | ---> |   |   | ---> |   |   | ---> |   |   |
        |   |   | <--- |   |   | <--- |   |   | <--- |   |   |
        ---------      ---------      ---------      ---------
         /\
         |   红黑树中每个节点都是基于epitem结构中的rdllink成员
         |
         |                                                          --------------
         |                                                          | eventpoll  |
         |                                                          |------------|
         |                                                          |  lock      |
         |                                                          |  mtx       |
         |                                                          |  wq        |
         |                                                          |  poll_wait |
         |--------------------------------------------------------- |  rdllist   |
                            --------------------------------------- |  rbr       |
                            |                                       |  ovflist   |
                            |                                       --------------
                            |
                            |
                            |
                           \/
                                                                    --------------
                            O                                       | epitem     |
                           / \                                      |------------|
                         /    \                                     |  rbn       |
                        o      o                                    |  rdllink   |
                       / \    / \                                   |  next      |
                     /    \  /   \                                  |  ffd       |
                    o     o o     o                                 |  nwait     |
                                                                    |  pwqlist   |
                                                                    |  ep        |
          红黑树中每个节点都是基于epitem结构中的rbn成员                |  fllink    |
                                                                    |  event     |
                                                                    --------------

epoll_wait的工作流程:
epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。
文件fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。
ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。
ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。
ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对用的poll方法。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。

系统调用SelectPollEpoll
事件集合用哦过户通过3个参数分别传入感兴趣的可读,可写及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件,这使得用户每次调用select都要重置这3个参数统一处理所有事件类型,因此只需要一个事件集参数。用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无需反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件
描述符的时间复杂度O(n)O(n)O(1)
最大支持文件描述符数一般有最大值限制6553565535
工作模式LTLT支持ET高效模式
内核实现和工作效率采用轮询方式检测就绪事件,时间复杂度:O(n)采用轮询方式检测就绪事件,时间复杂度:O(n)采用回调方式检测就绪事件,时间复杂度:O(1)

行文至此,想必各位都应该已经明了为什么epoll会成为Linux平台下实现高性能网络服务器的首选I/O复用调用。
需要注意的是:epoll并不是在所有的应用场景都会比select和poll高很多。尤其是当活动连接比较多的时候,回调函数被触发得过于频繁的时候,epoll的效率也会受到显著影响!所以,epoll特别适用于连接数量多,但活动连接较少的情况。

Epoll的使用

文件描述符的创建
#include <sys/epoll.h>
int epoll_create ( int size );

在epoll早期的实现中,对于监控文件描述符的组织并不是使用红黑树,而是hash表。这里的size实际上已经没有意义。

注册监控事件
#include <sys/epoll.h>
int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );

函数说明:
fd:要操作的文件描述符
op:指定操作类型
操作类型:
EPOLL_CTL_ADD:往事件表中注册fd上的事件
EPOLL_CTL_MOD:修改fd上的注册事件
EPOLL_CTL_DEL:删除fd上的注册事件
event:指定事件,它是epoll_event结构指针类型
epoll_event定义:

struct epoll_event
{
    __unit32_t events;    // epoll事件
    epoll_data_t data;     // 用户数据 
};

结构体说明:
events:描述事件类型,和poll支持的事件类型基本相同(两个额外的事件:EPOLLET和EPOLLONESHOT,高效运作的关键)
data成员:存储用户数据

typedef union epoll_data
{
    void* ptr;              //指定与fd相关的用户数据 
    int fd;                 //指定事件所从属的目标文件描述符 
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
epoll_wait函数
#include <sys/epoll.h>
int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );

函数说明:
返回:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno
timeout:指定epoll的超时时间,单位是毫秒。当timeout为-1是,epoll_wait调用将永远阻塞,直到某个时间发生。当timeout为0时,epoll_wait调用将立即返回。
maxevents:指定最多监听多少个事件
events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。

EPOLLONESHOT事件

使用场合:
一个线程在读取完某个socket上的数据后开始处理这些数据,而数据的处理过程中该socket又有新数据可读,此时另外一个线程被唤醒来读取这些新的数据。
于是,就出现了两个线程同时操作一个socket的局面。可以使用epoll的EPOLLONESHOT事件实现一个socket连接在任一时刻都被一个线程处理。
作用:
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多出发其上注册的一个可读,可写或异常事件,且只能触发一次。
使用:
注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个sockt。
效果:
尽管一个socket在不同事件可能被不同的线程处理,但同一时刻肯定只有一个线程在为它服务,这就保证了连接的完整性,从而避免了很多可能的竞态条件。

LT与ET模式

彻底学会使用epoll(一)——ET模式实现分析

示例1:从stdin中写入数据到buffer,ET模式
/* epoll_stdin.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
    epfd = epoll_create(1); //只需要监听一个描述符——标准输入
    ev.data.fd = STDIN_FILENO;
    ev.events = EPOLLIN|EPOLLET; //监听读状态同时设置ET模式
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件
    for(;;)
    {
        nfds = epoll_wait(epfd, events, 5, -1);
        for(int i = 0; i < nfds; i++)
        {
            if(events[i].data.fd==STDIN_FILENO)
                printf("welcome to epoll's word!\n");

        }
    }
}

当用户输入一组字符,这组字符被送入buffer,字符停留在buffer中,又因为buffer由空变为不空,所以ET返回读就绪,输出”welcome to epoll’s world!”。
之后程序再次执行epoll_wait,此时虽然buffer中有内容可读,但是根据我们上节的分析,ET并不返回就绪,导致epoll_wait阻塞。(底层原因是ET下就绪fd的epitem只被放入rdlist一次)。
用户再次输入一组字符,导致buffer中的内容增多,根据我们上节的分析这将导致fd状态的改变,是对应的epitem再次加入rdlist,从而使epoll_wait返回读就绪,再次输出“Welcome to epoll’s world!”。

将代码中的ev.events = EPOLLIN|EPOLLET;改为ev.events = EPOLLIN;//默认为LT模式

示例2:从stdin中写入数据到buffer,LT模式
/* epoll_stdin.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
    epfd = epoll_create(1); //只需要监听一个描述符——标准输入
    ev.data.fd = STDIN_FILENO;
    ev.events = EPOLLIN; //监听读状态
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件
    for(;;)
    {
        nfds = epoll_wait(epfd, events, 5, -1);
        for(int i = 0; i < nfds; i++)
        {
            if(events[i].data.fd==STDIN_FILENO)
                printf("welcome to epoll's word!\n");

        }
    }
}

程序陷入死循环,因为用户输入任意数据后,数据被送入buffer且没有被读出,所以LT模式下每次epoll_wait都认为buffer可读返回读就绪。导致每次都会输出”welcome to epoll’s world!”。

以上两段程序,虽然buffer有数据可读,但是没有读出来,数据依然存在buffer中。

示例3:从stdin中写入数据到buffer,LT模式,并在读就绪之后将buf读取出来
/* epoll_stdin.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
    epfd = epoll_create(1); //只需要监听一个描述符——标准输入
    ev.data.fd = STDIN_FILENO;
    ev.events = EPOLLIN; //监听读状态
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件
    for(;;)
    {
        nfds = epoll_wait(epfd, events, 5, -1);
        for(int i = 0; i < nfds; i++)
        {
            if(events[i].data.fd==STDIN_FILENO)
			{
				char buf[1024] = {0};
				read(STDIN_FILENO, buf, sizeof(buf));
                printf("welcome to epoll's word! buf:%s\n", buf);
			}
        }
    }
}

本程序依然使用LT模式,但是每次epoll_wait返回读就绪的时候我们都将buffer(缓冲)中的内容read出来,所以导致buffer再次清空,下次调用epoll_wait就会阻塞。所以能够实现我们所想要的功能——当用户从控制台有任何输入操作时,输出”welcome to epoll’s world!”

示例4:从stdin中写入数据到buffer,ET模式,在读就绪之后修改注册事件IN
/* epoll_stdin.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
    epfd = epoll_create(1); //只需要监听一个描述符——标准输入
    ev.data.fd = STDIN_FILENO;
    ev.events = EPOLLIN|EPOLLET; //监听读状态同时设置ET模式
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件
    for(;;)
    {
        nfds = epoll_wait(epfd, events, 5, -1);
        for(int i = 0; i < nfds; i++)
        {
            if(events[i].data.fd==STDIN_FILENO)
			{
                printf("welcome to epoll's word!\n");
    			ev.data.fd = STDIN_FILENO;
    			ev.events = EPOLLIN|EPOLLET; //监听读状态同时设置ET模式
				epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev); //注册epoll事件(ADD无效)
			}
        }
    }
}

程序依然使用ET,但是每次读就绪后都主动的再次MOD IN事件,我们发现程序再次出现死循环,也就是每次返回读就绪。但是注意,如果我们将MOD改为ADD,将不会产生任何影响。别忘了每次ADD一个描述符都会在epitem组成的红黑树中添加一个项,我们之前已经ADD过一次,再次ADD将阻止添加,所以在次调用ADD IN事件不会有任何影响。

示例5:监听标准输出(写状态),ET模式
/* epoll_stdin.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
    epfd = epoll_create(1); //只需要监听一个描述符——标准输出
    ev.data.fd = STDOUT_FILENO;
    ev.events = EPOLLOUT|EPOLLET; //监听写状态同时设置ET模式
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件
    for(;;)
    {
        nfds = epoll_wait(epfd, events, 5, -1);
        for(int i = 0; i < nfds; i++)
        {
            if(events[i].data.fd==STDOUT_FILENO)
                printf("welcome to epoll's word!\n");

        }
    }
}

这个程序的功能是只要标准输出写就绪,就输出“welcome to epoll’s world”。我们发现这将是一个死循环。下面具体分析一下这个程序的执行过程:
首先初始buffer为空,buffer中有空间可写,这时无论是ET还是LT都会将对应的epitem加入rdlist,导致epoll_wait就返回写就绪。
程序想标准输出输出”welcome to epoll’s world”和换行符,因为标准输出为控制台的时候缓冲是“行缓冲”,所以换行符导致buffer中的内容清空,这就对应第二节中ET模式下写就绪的第二种情况——当有旧数据被发送走时,即buffer中待写的内容变少得时候会触发fd状态的改变。所以下次epoll_wait会返回写就绪。如此循环往复。

示例6:监听标准输出(写状态),ET模式,不输出换行符\n
/* epoll_stdin.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
    epfd = epoll_create(1); //只需要监听一个描述符——标准输出
    ev.data.fd = STDOUT_FILENO;
    ev.events = EPOLLOUT|EPOLLET; //监听写状态同时设置ET模式
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件
    for(;;)
    {
        nfds = epoll_wait(epfd, events, 5, -1);
        for(int i = 0; i < nfds; i++)
        {
            if(events[i].data.fd==STDOUT_FILENO)
                printf("welcome to epoll's word!");

        }
    }
}

将输出语句的printf的换行符移除。我们看到程序成挂起状态。因为第一次epoll_wait返回写就绪后,程序向标准输出的buffer中写入“welcome to epoll’s world!”,但是因为没有输出换行,所以buffer中的内容一直存在,下次epoll_wait的时候,虽然有写空间但是ET模式下不再返回写就绪。回忆第一节关于ET的实现,这种情况原因就是第一次buffer为空,导致epitem加入rdlist,返回一次就绪后移除此epitem,之后虽然buffer仍然可写,但是由于对应epitem已经不再rdlist中,就不会对其就绪fd的events的在检测了。

示例7:监听标准输出(写状态),LT模式,不输出换行符\n
/* epoll_stdin.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
    epfd = epoll_create(1); //只需要监听一个描述符——标准输出
    ev.data.fd = STDOUT_FILENO;
    ev.events = EPOLLOUT; //监听写状态同时设置ET模式
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件
    for(;;)
    {
        nfds = epoll_wait(epfd, events, 5, -1);
        for(int i = 0; i < nfds; i++)
        {
            if(events[i].data.fd==STDOUT_FILENO)
                printf("welcome to epoll's word!");

        }
    }
}

结果:

e to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's word!welcome to epoll's wo

程序再次死循环。这时候原因已经很清楚了,因为当向buffer写入”welcome to epoll’s world!”后,虽然buffer没有输出清空,但是LT模式下只有buffer有写空间就返回写就绪,所以会一直输出”welcome to epoll’s world!”,当buffer满的时候,buffer会自动刷清输出,同样会造成epoll_wait返回写就绪。

示例8:监听标准输出(写状态),ET模式,不输出换行符\n,在写就绪之后修改注册事件OUT
/* epoll_stdin.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
    epfd = epoll_create(1); //只需要监听一个描述符——标准输出
    ev.data.fd = STDOUT_FILENO;
    ev.events = EPOLLOUT|EPOLLET; //监听写状态同时设置ET模式
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件
    for(;;)
    {
        nfds = epoll_wait(epfd, events, 5, -1);
        for(int i = 0; i < nfds; i++)
        {
            if(events[i].data.fd==STDOUT_FILENO)
			{
                printf("welcome to epoll's word!");
    			ev.data.fd = STDOUT_FILENO;
    			ev.events = EPOLLOUT|EPOLLET; //监听读状态同时设置ET模式
				epoll_ctl(epfd, EPOLL_CTL_MOD, STDOUT_FILENO, &ev); //注册epoll事件(ADD无效)
			}
        }
    }
}

在每次向标准输出的buffer输出”welcome to epoll’s world!”后,重新MOD OUT事件。所以相当于每次都会返回就绪,导致程序循环输出。

经过前面的案例分析,我们已经了解到,当epoll工作在ET模式下时,对于读操作,如果read一次没有读尽buffer中的数据,那么下次将得不到读就绪的通知,造成buffer中已有的数据无机会读出,除非有新的数据再次到达。对于写操作,主要是因为ET模式下fd通常为非阻塞造成的一个问题——如何保证将用户要求写的数据写完。

要解决上述两个ET模式下的读写问题,我们必须实现:
对于读,只要buffer中还有数据就一直读;
对于写,只要buffer还有空间且用户请求写的数据还未写完,就一直写。

ET模式下的accept问题
请思考以下一种场景:在某一时刻,有多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理。在这种情形下,我们应该如何有效的处理呢?

解决的方法是:解决办法是用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept 返回 -1 并且 errno 设置为 EAGAIN 就表示所有连接都处理完。

ET模式为什么要设置在非阻塞模式下工作
因为ET模式下的读写需要一直读或写直到出错(对于读,当读到的实际字节数小于请求字节数时就可以停止),而如果你的文件描述符如果不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。这样就不能在阻塞在epoll_wait上了,造成其他文件描述符的任务饥饿。

LT:水平触发
效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。

ET:边缘触发
效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。

从本质上讲:与LT相比,ET模型是通过减少系统调用来达到提高并行效率的。

Linux下的I/O复用与epoll详解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值