linux中epoll原理,Linux下select&poll&epoll的实现原理(一)

最近简单看了一把 linux-3.10.25 kernel中select/poll/epoll这个几个IO事件检测API的实现。此处做一些记录。

其基本的原理是相同的,流程如下

先依次调用fd对应的struct file.f_op->poll()方法(如果有提供实现的话),尝试检查每个提供待检测IO的fd是否已经有IO事件就绪

如果已经有IO事件就绪,则直接所收集到的IO事件返回,本次调用结束

如果暂时没有IO事件就绪,则根据所给定的超时参数,选择性地进入等待

如果超时参数指示不等待,则本次调用结束,无IO事件返回

如果超时参数指示等待(等待一段时间或持续等待),则将当前select/poll/epoll的调用任务挂起

当所检测的fd任何一个有新的IO事件发生时,会将上述的处于等待的任务唤醒。任务被唤醒之后,重新执行1中的IO事件收集过程,将此时收集到的IO事件返回,本次的调用过程结束。

可以看出流程并不复杂,本文按照上述流程,先对select/poll的实现做进一步分析,epoll的实现要复杂一些,另外做叙述。

上述比较关键的地方是

1. 起初在无IO事件时,调用任务在被挂起之后,fd上有IO事件发生时,如何将挂起的任务唤醒?

接下来一poll()的实现过程,介绍这一过程的实现原理。

要实现唤醒过程,比较关键的步骤为

在上述初次调用 struct file.f_op->poll() 时,除了fd对应的 struct file 指针之外,还需要传入一个 poll_table 类型的指针。 该 poll_table 具体是在 poll() 的实现过程中定义提供的。

在各个fd的struct file.f_op->poll()方法中,需要对应调用一下poll_wait()

其中关键

struct file.f_op->poll() 方法的作用是既是直接查询fd上的IO事件

而 poll_wait() 既是实现根据需要将 poll() 的调用任务,选择性的添加到fd的IO事件等待队列中。

注意此处是“选择性的添加”,为何如此说,得看 poll_wait() 的实现,其是一个inline函数,如下

67c6b99d6dba48cdad528788ce0ccc1b.jpg

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)

{

if (p && p->_qproc &&wait_address)

p->_qproc(filp, wait_address, p);

}

67c6b99d6dba48cdad528788ce0ccc1b.jpg

定义在 include/linux/poll.h 中。

可以看出,当调用 struct file.f_op->poll() 时,poll_table 的 _qproc 成员有定义时,poll_wait() 的调用才有实际的效果。

而前面已经说了, “poll_table 具体是在 poll() 的实现过程中定义提供的”,具体是 poll_initwait(&table) 调用对 poll_table 进行初始化的。

再进一步看一下 poll_initwait() 实现,如下

67c6b99d6dba48cdad528788ce0ccc1b.jpg

1 void poll_initwait(struct poll_wqueues *pwq)

2 {

3 init_poll_funcptr(&pwq->pt, __pollwait);

4 pwq->polling_task =current;

5 pwq->triggered = 0;

6 pwq->error = 0;

7 pwq->table =NULL;

8 pwq->inline_index = 0;

9 }

67c6b99d6dba48cdad528788ce0ccc1b.jpg

其中

1 static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)

2 {

3 pt->_qproc =qproc;

4 pt->_key = ~0UL; /*all events enabled */

5 }

也就是说初次遍历 poll 给定的诸fd,直至初始收集到IO事件,都会对 __pollwait() 调用一把。

实际上也正是在

__pollwait() 中实现将 poll() 的调用任务添加到 fd

的等待队列中,并且指定了唤醒执行过程中的回调;并且初次对所有的fd调用过 struct file.f_op->poll() 之后,会将

poll_table._qproc 只为NULL,故而后续重新,调用 struct file.f_op->poll()

时不会重复的将调用线程添加到fd的等待队列两种。在 poll() 的实现中,该回调函数是 pollwake(),具体有 pollwake()

调用 kernel 的唤醒API(default_wake_function())将 poll() 的调用任务唤醒。

(由于之前具体是在 poll() 的实现中进行挂起的,所以自然唤醒过程也应该放在 poll() 的实现中了)

上面这些内容是介绍了 poll() 这一侧为等待唤醒,所做的准备工作。接下来看看,当fd上有IO时间就绪时,是如何将那些对自身等待的任务唤醒的。

此处以 eventfd 的实现为例,其实现较为简单,方便叙述。

先简单说一下 eventfd 的特性,其内部维持了一个64位的计数器。当该计数器大于0时,fd上有可读事件;当该计数器值小于 ULLONG_MAX 时,有可写实现

看其实现代码知道,其具体对该计数值更新过程发生在 eventfd_ctx_read() / eventfd_write() 中。

eventfd_ctx_read()操作之后,该内部计数器值减小,结尾如下代码中片段会对的内部 wait_queue 中记录的等待任务进行唤醒操作。

1 if (likely(res == 0)) {

2 eventfd_ctx_do_read(ctx, cnt);

3 if (waitqueue_active(&ctx->wqh))

4 wake_up_locked_poll(&ctx->wqh, POLLOUT);

5 }

而在 eventfd_write() 操作完毕后,内部计数器值增大,结尾如下代码片段会对对的内部 wait_queue 中记录的等待任务进行唤醒操作。

1 if (likely(res > 0)) {

2 ctx->count +=ucnt;

3 if (waitqueue_active(&ctx->wqh))

4 wake_up_locked_poll(&ctx->wqh, POLLIN);

5 }

至此, 完整的唤醒通知过程也就介绍完成了。

select() 实现过程与 poll() 完全一致,不同的待检测的fd和结果事件的返回方式不同而已。poll() 内部是使用链表进行记录,而 select() 是使用的bit位序列进行记录的而已。

详细请看 fs/select.c 中看 poll() 和 select() 的实现代码。

epoll() 的事件检查方式与 poll() / select() 类似,一个明显的差别是各个 fd 是注册到 epoll 内部进行记录管理的,而 poll/ select 需要每次从用户态拷贝到 kernel 中,

且 epoll 内部对 fd 的记录是用rbtree,并且还有自己自己的一些独有的特点,这些内容将在另外的文章中进行描述。本文到此为止

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值