详细说说select poll epoll

(以下内容来自网络和自己的总结,再次感谢网络中的大神们提供的见解)


在探索select poll  epoll之前我们首先要知道什么叫多路复用:

下来探索一下为什么会用到多路复用:

首先我们看看一个客户端请求服务器的完整过程。首先,请求过来,要建立连接,然后再接收数据,接收数据后,再发送数据。

具体到系统底层,就是读写事件,而当读写事件没有准备好时,必然不可操作,如果不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,那就只能等了,等事件准备好了,你再继续吧。阻塞调用会进入内核等待,cpu就会让出去给其他进程使用了,你可能会说那么加进程数呀,当读写事件十分多的时候会创建很多的进程,此时进程的上下文切换会占用过多的cpu资源。有人会说那么用线程,其实线程的上下文切换也会占用过多资源,而且还会引入线程之间同步和互斥的问题,因为线程之间看到的是同一块内存资源。


所以我么就会思考能不能用一个进程来查看很多的IO事件,比如每一个人都在钓鱼每一个鱼上钩都比做是一个事件发生的话,那么一百个事件发生你可以让一百个人在那里一人拿一个鱼竿进行钓鱼,你自己负责进行鱼的收集。此时如果没有鱼上钩,那一百个人就在那阻塞等待,你自己为了收鱼也在空闲着。这里的你自己可以比作CPU,一般个人可以比作多个进程,此时如果不是所有鱼都上钩,你就十分空闲 其他人也在那拿着鱼竿空闲等待着,如果同时有多个鱼上钩了,多个人会像你汇报,此时汇报的顺序问题就是形成混乱。此时我们可以进行一下改进,比如专门找一个人拿着许多鱼竿,当一个鱼竿上的鱼上钩以后再拉起鱼竿,这样节约了人力,还解决了问题。


下来讲一个真实的故事吧:

假设你是一个机场的空管, 你需要管理到你机场的所有的航线, 包括进港,出港, 有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。

你会怎么做?
最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机, 从进港,接客,排位,出港,航线监控,直至交接给下一个空港,全程监控。
那么问题就来了:
    很快你就发现空管塔里面聚集起来一大票的空管员,交通稍微繁忙一点,新的空管员就已经挤不进来了。
    空管员之间需要协调,屋子里面就1, 2个人的时候还好,几十号人以后 ,基本上就成菜市场了。
    空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。 (线程之间资源共享)

现实上我们的空管同时管几十架飞机稀松平常的事情, 他们怎么做的呢?
他们用这个东西


这个东西叫flight progress strip. 每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。


这个东西现在还没有淘汰哦,只是变成电子的了而已。。
是不是觉得一下子效率高了很多,一个空管塔里可以调度的航线可以是前一种方法的几倍到几十倍。
如果你把每一个航线当成一个Sock(I/O 流), 空管当成你的服务端Sock管理代码的话.
第一种方法就是最传统的多进程并发模型 (每进来一个新的I/O流会分配一个新的进程管理。)
第二种方法就是I/O多路复用 (单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。)


其实“I/O多路复用”这个坑爹翻译可能是这个概念在中文里面如此难理解的原因。所谓的I/O多路复用在英文中其实叫 I/O multiplexing. 如果你搜索multiplexing啥意思,基本上都会出这个图:


于是大部分人都直接联想到"一根网线,多个sock复用" 这个概念,包括上面的几个回答, 其实不管你用多进程还是I/O多路复用, 网线都只有一根好伐。多个Sock复用一根网线这个功能是在内核+驱动层实现的。

重要的事情再说一遍: I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。

在同一个进程里面, 通过拨开关的方式,来同时传输多个I/O流, (学过EE的人现在可以站出来义正严辞说这个叫“时分复用”了)。


讲了这么多故事下来到我们重点的select  poll  epoll 了

select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个鬼存在,其实是他们出现是有先后顺序的。

I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。

select 被实现以后,很快就暴露出了很多问题。
select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍(其实select支持不了几万个),这个无 谓的开销就颇有海天盛筵的豪气了。
select 只能监视1024个链接, 这个跟草榴没啥关系哦,linux 定义在头文件中的,参见FD_SETSIZE。
select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.
“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
霸不霸气


于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如
poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。
poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了,所以行走江湖,还是小心为妙。
其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。

但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。

于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.

epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
epoll 现在是线程安全的。
epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。

可是epoll 有个致命的缺点。只有linux支持。(这个缺点还是缺点吗,主人,你开心就好) 比如BSD上面对应的实现是kqueue。


 PS: 上面所有这些比较分析,都建立在大并发下面,如果你的并发数太少,用哪个,其实都没有区别。

   于Linux网络接口来说,你只有一个网卡,却需要同时处理N个链接,这里就需要Multiplexing;

上升到软件层面,也就是题主关心的几个系统调用,就是IO(网络socket包括在内)需要one to many的时候提供一种multiplexing机制。

简单的说下自己的理解
io分为磁盘io和网络io,这里说的是网络io。我们知道计算机之间传输数据是流传输。一台计算机网络io只会有一个。

这里说单进程
在最基本的c/s demo中,send/recv就是在一条io通道收发数据,这就是基本的网络io,但是这种操作是不能“填满”io的,也就是说大部分io资源你没有用,仅仅有一个io操作,当然你可以开多进程或多线程,代价可想而知

此时出现了io多路复用,自己的话翻译一下,复用网络io从而有多个io操作能在网络io中执行。
linux下网络io使用socket套接字来通信,普通io模型只能监听一个socket,而io多路复用可同时监听多个socket

io多路复用避免阻塞在io上(其实select poll epoll 都是阻塞的只是可以设置超时),原本为多进程或多线程来接收多个连接的消息变为单进程或单线程保存多个socket的状态后轮询处理(epoll进行了改进,后面会说)


实现io多路复用需要函数来支持,就是linux下的select/poll,epoll以及win下 iocp 和 BSD 的 kqueue


select:
下面是select的函数接口:
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
 
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。内部使用三个位图实现

调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。

当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。



poll:
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */

};
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。内部使用的是一个结构体的链表实现的

和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。


epoll:
epoll的接口如下:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *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 */             
};

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int epoll:
epoll的接口如下:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *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 */             
};

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

主要是epoll_create,epoll_ctl和epoll_wait三个函数。epoll_create函数创建epoll文件描述符,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。返回是epoll描述符。-1表示创建失败。epoll_ctl 控制对指定描述符fd执行op操作,event是与fd关联的监听事件。op操作有三种:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。epoll_wait 等待epfd上的io事件,最多返回maxevents个事件。


主要是epoll_create,epoll_ctl和epoll_wait三个函数。

epoll_create函数创建epoll文件描述符,(linux2.6之后)参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。返回是epoll描述符。-1表示创建失败。

epoll_ctl 控制对指定描述符fd执行op操作,event是与fd关联的监听事件。op操作有三种:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。

epoll_wait 等待epfd上的io事件,最多返回maxevents个事件,timeout超时时间。


epoll为什么高效(相比select)

1、  仅从上面的调用方式就可以看出epoll比select/poll的一个优势:select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。

所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。

 2、 此外,内核使用了slab机制,为epoll提供了快速的数据结构:
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。

当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

3、 epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。

这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。

所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。

那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。

 

如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。

1.执行epoll_create时,创建了红黑树和就绪list链表。

2.执行epoll_ctl时,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。
3.执行epoll_wait时立刻返回准备就绪链表里的数据即可。


EPOLL事件有两种模型:
Edge Triggered (ET)
Level Triggered (LT)

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。可见ET模式很大程度上降低了一个epoll事件被重复触发的次数,一次ET效率比LT模式高。但是如果一直不读取ET模式下的数据可能会导致数据的丢失。

注意:每个使用ET模式的文件描述符都要是非阻塞的,如果文件描述符是阻塞的,那么读或者写会因为后续没有时间一直处于阻塞状态(饥饿状态)。


LT模式 和ET模式(电平触发和边沿触发)
EPOLLIN事件 :(read)  (关心的是接受缓冲区是否有数据)
内核中socket接受缓冲区空                低电平
内核中socket接受缓冲区不空            高电平

EPOLLOUT事件:(write) (关心的是发送缓冲区有没有空余位置)
内核中的socket发送缓冲区满              低电平
内核中的socket发送缓冲区不满          高电平


LT电平触发
高电平触发

ET边缘触发
低-----------》高   触发
高-----------》低   触发

最后说一说epoll的线程安全:

epoll的红黑树由一个互斥量保护,ready list是自旋锁保护的。

ready list涉及到add\mod\wait
红黑树涉及到add\mod\del

1 等待队列实现原理
1.1 功能介绍
  进程有多种状态,当进程做好准备后,它就处于就绪状态(TASK_RUNNING),放入运行队列,等待内核调度器来调度。当然,同一时刻可能有多个进程 进入就绪状态,但是却可能只有1个CPU是空闲的,所以最后能不能在CPU上运行,还要取决于优先级等多种因素。当进程进行外部设备的IO等待操作时,由 于外部设备的操作速度一般是非常慢的,所以进程会从就绪状态变为等待状态(休眠),进入等待队列,把CPU让给其它进程。直到IO操作完成,内核“唤醒” 等待的进程,于是进程再度从等待状态变为就绪状态。
  在用户态,进程进行IO操作时,可以有多种处理方式,如阻塞式IO,非阻塞式IO,多路复用(select/poll/epoll),AIO(aio_read/aio_write)等等。这些操作在内核态都要用到等待队列。
1.2 相关的结构体
typedef struct __wait_queue  wait_queue_t;
struct __wait_queue
{
    unsigned int flags;
    #define WQ_FLAG_EXCLUSIVE 0x01
    struct task_struct * task; //等待队列节点对应的进程
    wait_queue_func_t func;   //等待队列的回调函数,在进程被唤醒
    struct list_head task_list;
};
这个是等待队列的节点,在很多等待队列里,这个func函数指针默认为空函数。
但是,在select/poll/epoll函数中,这个func函数指针不为空,并且扮演着重要的角色。

struct __wait_queue_head
{
    spinlock_t lock;
    struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
这个是等待队列的头部。其中task_list里有指向下一个节点的指针。为了保证对等待队列的操作是原子的,还需要一个自旋锁lock。

这里需要提一下内核队列中被广泛使用的结构体struct list_head。
struct list_head
{
    struct list_head *next, *prev;
};
       
1.3 实现原理
可以看到,等待队列的核心是一个list_head组成的双向链表。
其中,第一个节点是队列的头,类型为wait_queue_head_t,里面包含了一个list_head类型的成员task_list。
接下去的每个节点类型为 wait_queue_t,里面也有一个list_head类型的成员task_list,并且有个指针指向等待的进程。通过这种方式,内核组织了一个等待队列。

那么,这个等待队列怎样与一个事件关联呢?
在内核中,进程在文件操作等事件上的等待,一定 会有一个对应的等待队列的结构体与之对应。例如,等待管道的文件操作(在内核看来,管道也是一种文件)的进程都放在管道对应 inode.i_pipe->wait这个等待队列中。这样,如果管道文件操作完成,就可以很方便地通过 inode.i_pipe->wait唤醒等待的进程。
在大部分情况下(如系统调用read),当前进程等待IO操作的完成,只要在内核堆栈中分配一个wait_queue_t的结构体,然后初始化,把task指向当前进程的task_struct,然后调用add_wait_queue()放入等待队列即可。
但是,在select/poll中,由于系统调 用要监视多个文件描述符的操作,因此要把当前进程放入多个文件的等待队列,并且要分配多个wait_queue_t结构体。这时候,在堆栈上分配是不合适 的。因为内核堆栈很小。所以要通过动态分配的方式来分配wait_queue_t结构体。除了在一些结构体里直接定义等待队列的头部,内核的信号量机制也 大量使用了等待队列。信号量是为了进行进程同步而引入的。与自旋锁不同的是,当一个进程无法获得信号量时,它会把自己放到这个信号量的等待队列中,转变为 等待状态。当其它进程释放信号量时,会唤醒等待的进程。

epoll 关键结构体:
struct ep_pqueue
{
    poll_table pt;
    struct epitem *epi;
};
这个结构体类似于select/poll中的struct poll_wqueues。由于epoll需要在内核态保存大量信息,所以光光一个回调函数指针已经不能满足要求,所以在这里引入了一个新的结构体struct epitem。
 
struct epitem
{
    struct rb_node rbn;
    红黑树,用来保存eventpoll
 
    struct list_head rdllink;
    双向链表,用来保存已经完成的eventpoll
 
    struct epoll_filefd ffd;
    这个结构体对应的被监听的文件描述符信息
 
    int nwait;
    poll操作中事件的个数
 
    struct list_head pwqlist;
    双向链表,保存着被监视文件的等待队列,功能类似于select/poll中的poll_table
 
    struct eventpoll *ep;
    指向eventpoll,多个epitem对应一个eventpoll
 
    struct epoll_event event;
    记录发生的事件和对应的fd
 
    atomic_t usecnt;
    引用计数
 
    struct list_head fllink;
    双向链表,用来链接被监视的文件描述符对应的struct file。因为file里有f_ep_link,
    用来保存所有监视这个文件的epoll节点
 
    struct list_head txlink;
    双向链表,用来保存传输队列
 
    unsigned int revents;
    文件描述符的状态,在收集和传输时用来锁住空的事件集合
};
该结构体用来保存与epoll节点关联的多个文件描述符,保存的方式是使用红黑树实现的hash表。至于为什么要保存,下文有详细解释。它与被监听的文件描述符一一对应。
struct eventpoll
{
    spinlock_t lock;
    读写锁
 
    struct mutex mtx;
    读写信号量
 
    wait_queue_head_t wq;
    wait_queue_head_t poll_wait;
 
    struct list_head rdllist;
    已经完成的操作事件的队列。
 
    struct rb_root rbr;
    保存epoll监视的文件描述符
    struct epitem *ovflist;
    struct user_struct *user;
};
这个结构体保存了epoll文件描述符的扩展信息,它被保存在file结构体的private_data中。它与epoll文件节点一一对应。通常一个epoll文件节点对应多个被监视的文件描述符。所以一个eventpoll结构体会对应多个epitem结构体。
那么,epoll中的等待事件放在哪里呢?见下面
 
struct eppoll_entry
{
    struct list_head llink;
    void *base;
    wait_queue_t wait;
    wait_queue_head_t *whead;
};
与select/poll的struct poll_table_entry相比,epoll的表示等待队列节点的结构体只是稍有不同,
与struct poll_table_entry比较一下。
struct poll_table_entry
{
    struct file * filp;
    wait_queue_t wait;
    wait_queue_head_t * wait_address;
};
由于epitem对应一个被监视的文件,所以通过base可以方便地得到被监视的文件信息。又因为一个文件可能有多个事件发生,所以用llink链接这些事件。

相关内核代码:

fs/eventpoll.c

判断一个tcp套接字上是否有激活事件:net/ipv4/tcp.c:tcp_poll函数


每个epollfd在内核中有一个对应的eventpoll结构对象.
其中关键的成员是一个readylist(eventpoll:rdllist)
和一棵红黑树(eventpoll:rbr).
eventpoll的红黑树中.红黑树的作用是使用者调用EPOLL_MOD的时候可以快速找到fd对应的epitem。
epoll_ctl的功能是实现一系列操作,如把文件与 eventpollfs文件系统的inode节点关联起来。这里要介绍一下eventpoll结构体,它保存在file->f_private中, 记录了eventpollfs文件系统的inode节点的重要信息,其中成员rbr保存了该epoll文件节点监视的所有文件描述符。组织的方式是一棵红 黑树,这种结构体在查找节点时非常高效。首先它调用ep_find()从eventpoll中的红黑树获得epitem结构体。然后根据op参数的不同而 选择不同的操作。如果op为EPOLL_CTL_ADD,那么正常情况下epitem是不可能在eventpoll的红黑树中找到的,所以调用 ep_insert创建一个epitem结构体并插入到对应的红黑树中。
       ep_insert()首先分配一个epitem对象,对它初始化后,把它放入对应的红黑树。此外,这个函数还要作一个操作,就是把当前进程放入对应文件操作的等待队列。这一步是由下面的代码完成的。
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
......
revents = tfile->f_op->poll(tfile, &epq.pt);
 
函数先调用init_poll_funcptr注册了一个回调函数ep_ptable_queue_proc,ep_ptable_queue_proc函数会在调用f_op->poll时被执行。
 900 static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
 901                  poll_table *pt)
 902 {
 903     struct epitem *epi = ep_item_from_epqueue(pt);
 904     struct eppoll_entry *pwq;
 905
 906     if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
 907         init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
 908         pwq->whead = whead;
 909         pwq->base = epi;
 910         add_wait_queue(whead, &pwq->wait);
 911         list_add_tail(&pwq->llink, &epi->pwqlist);
 912         epi->nwait++;
 913     } else {
 914        
 915         epi->nwait = -1;
 916     }
 
 917 }
该函数分配一个epoll等待队列结点eppoll_entry: 一方面把它挂到文件操作的等待队列中,另一方面把它挂到epitem的队列中。此外,它还注册了一个等待队列的回调函数 ep_poll_callback。当文件操作完成,唤醒当前进程之前,会调用ep_poll_callback(),把eventpoll放到 epitem的完成队列中(注释:通过查看代码,此处应该是把epitem放到eventpoll的完成队列,只有这样才能在epoll_wait()中只要看eventpoll的完成队列即可得到所有的完成文件描述符),并唤醒等待进程。
如果在执行f_op->poll以后,发现被监视的文件操作已经完成了,那么把它放在完成队列中了,并立即把等待操作的那些进程唤醒。
 919     if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
 920         return -ENOMEM;

 963     ep_rbtree_insert(ep, epi);

调用epoll_wait的时候,将readylist中的epitem出列,将触发的事件拷贝到用户空间.之后判断epitem是否需
要重新添加回readylist.

epitem重新添加到readylist必须满足下列条件:

1) epitem上有用户关注的事件触发.

2) epitem被设置为水平触发模式(如果一个epitem被设置为边界触发则这个epitem不会被重新添加到readylist
中,在什么时候重新添加到readylist请继续往下看).

注意,如果epitem被设置为EPOLLONESHOT模式,则当这个epitem上的事件拷贝到用户空间之后,会将
这个epitem上的关注事件清空(只是关注事件被清空,并没有从epoll中删除,要删除必须对那个描述符调用
EPOLL_DEL),也就是说即使这个epitem上有触发事件,但是因为没有用户关注的事件所以不会被重新添加到
readylist中.


epitem被添加到readylist中的各种情况(当一个epitem被添加到readylist如果有线程阻塞在epoll_wait中,那
个线程会被唤醒):

1)对一个fd调用EPOLL_ADD,如果这个fd上有用户关注的激活事件,则这个fd会被添加到readylist.

2)对一个fd调用EPOLL_MOD改变关注的事件,如果新增加了一个关注事件且对应的fd上有相应的事件激活,
则这个fd会被添加到readylist.

3)当一个fd上有事件触发时(例如一个socket上有外来的数据)会调用ep_poll_callback(见eventpoll::ep_ptable_queue_proc),
如果触发的事件是用户关注的事件,则这个fd会被添加到readylist中.

了解了epoll的执行过程之后,可以回答一个在使用边界触发时常见的疑问.在一个fd被设置为边界触发的情况下,
调用read/write,如何正确的判断那个fd已经没有数据可读/不再可写.epoll文档中的建议是直到触发EAGAIN
错误.而实际上只要你请求字节数小于read/write的返回值就可以确定那个fd上已经没有数据可读/不再可写.
最后用一个epollfd监听另一个epollfd也是合法的,epoll通过调用eventpoll::ep_eventpoll_poll来判断一个
epollfd上是否有触发的事件(只能是读事件).


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值