Select & Epoll原理

预备知识

等待队列

等待队列有一个等待队列头,其他加入这个等待队列的需要加在这个头上。

需要加入等待队列的话,可以调用封装好的sleep_on(wait_queue_head_t *q)。这个sleep_on()函数其中调用了__add_wait_queue(q, &wait),然后调用了schedule_timeout()去调度进程并让当前进程睡眠,最后调用__remove_wait_queue(q, &wait)。即睡眠回来的进程把自己从等待队列头上去掉,然后往下执行。

对一个等待队列头调用wake_up(wait_queue_head_t *q)的话,就是去队列中调用挂在等待队列的实体注册的回调函数。wake_up_all(wait_queue_head_t *q)则是遍历这个等待队列,调用所有的实体的回调函数。默认的回调函数是default_wake_function(),这个函数实际上就是去调用try_to_wake_up()唤醒等待队列上的进程。然后注册到等待队列上的进程启动,并回到sleep_on(),并且调用__remove_wait_queue(q, &wait)。

一般说来,当设备有响应的时候,会触发一个硬中断。在硬中断处理函数中会调用其wake_up()或者wake_up_all()函数。

static unsigned int poll(struct file* file, poll_table* wait)

一个设备的f_op->poll()函数干两件事。

一、调用poll_wait()。一般情况下就是poll_wait(file, &dev->r_wait, p)和poll_wait(file, &dev->w_wait, p)。其中dev->r_wait和dev->w_wait就是相应设备的等待队列的队列头。

二、对设备读写状态进行检查,有就给mask置位,最后返回mask。

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

f_op->poll()调用的poll_wait()一般只做一件事,就是判定p和wait_address是否为null,如果都不是,那么调用p->qproc(filp, wait_address, p)。一般来说,这个pqroc()函数的行为是1.设置等待队列被唤醒之后所执行的函数,2.把实体加入到相应设备的等待队列中。

Select

结构

struct poll_wqueues来表达一次select()。这个结构里面最重要的一项是struct poll_table pt。poll_wqueues里面还有struct poll_table_entry的链表和数组。

struct poll_table_entry表示一个需要监听的事件,并且是挂到驱动设备的一个或多个等待队列的实体。

函数

select()最后要调用do_select()。do_select()做如下几件事。

一、定义一个poll_wqueues并调用poll_initwait(&table)初始化这个poll_wqueues。poll_initwait(&table)中最关键的初始化操作是init_poll_funcptr(&pwq->pt, __pollwait)。即设置table.pt.qproc = __pollwait。

二、死循环。首先,遍历。遍历所有用户需要监听的fd,对于每个fd:1.调用mask = (*f_op->poll)(file, retval ? NULL : wait)。其中wait就是我们的table.pt。2.查看mask是否置位,来改变返回的位图和retval。其次,检查。检查(retval的值 | timeout | 当前进程是否信号要处理),如果满足任意一个,那么退出死循环。都不满足的话,调用schedule_timeout()让出cpu并且睡眠。

三、调用poll_freewait(&table),这个函数:释放poll_wqueues,并且移除设备的等待队列上的所有实体。然后返回retval。

__pollwait()函数和我们上面说的一样。这个函数的行为是设置等待队列被唤醒之后所执行的函数为pollwake()(实际上这个函数本质上就是default_wake_function())并且加入到fd相应设备的等待队列中

注意对poll()的调用参数。如果select()发现retval不为0的话,就不执行__pollwait()了,而只是去检查设备的状态。

Epoll

结构

struct eventpoll来表达一次epoll()。这个结构体里面有红黑树的根rb_root rbr,有一个链表list_head rdllist,还有一个链表struct epitem* ovflist,有一个等待队列wait_queue_head_t wq

struct epitem代表一个用户注册的实例,其中有一个红黑树节点和一个链表节点,还有用户空间的数据。

struct epoll_entry代表一个需要加入设备等待队列的实体,其中有一个base指针指向epitem。

函数

epoll_create()调用系统调用sys_epoll_create()。这个系统调用继续调用sys_epoll_create1(0)。即这个create1()函数舍弃了用户传来的size变量。这个sys_epoll_create1()函数接着调用ep_alloc()进行初始化结构(红黑树等),然后调用anon_inode_getfd()函数创建一个匿名的文件,然后调用fd_install()给文件安排一个fd。

epoll_ctl()调用系统调用sys_epoll_ctl()。我们主要讨论插入的情况。sys_epoll_ctl()先判定这个要插入的fd是否已经在红黑树中,如果存在了的话就不插入了,而是返回一个错误。如果不存在,可以正常插入,那么调用ep_insert()函数。

ep_insert()函数做的事情如下。

一、调用init_poll_funcptr()函数注册ep_ptable_queue_proc()函数。

二、把这个fd初始化一个struct epitem并且加入到一个红黑树中。

三、首先,调用当前交给epoll_ctl()的fd的revents = poll(tfile, &epq.pt)。检查设备是否满足条件。

四、如果设备有响应,且这个fd没加入eventpoll的队列中的话,就直接加入到eventpoll的队列中。否则什么也不干。

这个ep_ptable_queue_proc()函数设置设备的等待队列的callback为ep_poll_callback(),并且把epoll_entry加入fd对应的设备的等待队列

ep_poll_callback()做的事情就是,1.判断是不是有事件发生,如果没有就退出。2.unlikely判断ovflist能不能加入(ep_scan_ready_list()是否正在执行,是否正在向用户空间传数据),如果能的话就把epitem加入到ovflist中。3.如果不能加入ovflist,那么判断当前epitem是否在rdllist中,如果不在那么就把响应的epitem加入到struct eventpoll->rdllist中。4.如果ep->wq上有等待实体,那么唤醒ep->wq等待队列。

对于epoll_wait()函数,epoll_wait()调用ep_poll()函数。ep_poll()也是一个死循环,死循环里做以下事情。

一、检查eventpoll->rdllist里有没有元素,如果没有的话就1.把current加到eventpoll的等待队列里(ep->wq)。2.执行一个死循环,死循环里检查(链表是否是空 | timeout | 当前进程是否信号要处理),如果满足任意一个,那么退出死循环。死循环里还调用schedule_timeout()重新调度让出cpu并睡眠。3.调用__remove_wait_queue(&ep->wq, &wait)。

二、调用ep_send_events()把元素传递给内核空间。如果还没获取到,那么继续死循环,直到获取到为止。

ep_send_events()调用ep_scan_ready_list()。ep_scan_ready_list()做以下几件事。1.置位ovflist声明其可访问。2.调用ep_send_events_proc()。3.在ep_send_events_proc()执行的过程中,有可能会有新的epitem加入到eventpoll->ovflist中,所以要遍历ovflist,把在ovflist中的,在rdllist中没有的epitem加入到rdllist中。4.复位ovflist声明其不可访问。5.如果rdllist不为空(LT水平触发 | 传输rdllist出现错误 | 新的ovflist加入rdllist)那么如果ep->wq上有等待实体,则唤醒ep->wq。

ep_send_events_proc()这个函数遍历eventpoll->rdllist。对其中每一项,1.移除rddlist。2.调用f_op->poll(epi->ffd.file, NULL)。即,这个时候不再调用ep_ptable_queue_proc(),只是单纯的去检查,不改变设备的等待队列,所以也就不用从队列里移除或者再添加。2.如果通过了(确实有响应),那么就传递给用户空间数据,如果传输失败再把当前epitem加回到eventpoll->rdllist。3.传递完,检查events的标志,如果是ONESHOT那么把标志全部清零(除了PRIVATE_BITS);如果是LT水平触发,那么就把这个epitem再添加到eventpoll->rdllist中;ET触发什么都不干。

实际流程

Select直接获取

Select先设置wait = __pollwait,然后去遍历rset,分别调用poll(file, retval ? NULL : wait)。假如执行到第3个fd,发现设备有回应,即retval不为0了。那么对于前3个fd,poll分别注册了它们的pollwait回调函数并且加入到了等待队列头当中。然后到了第4个,retval已经不为0了,以后的poll只执行检查。执行完,位图和retval也都设置好了,这时死循环break,那么把前3个fd从设备的等待队列头中摘除,然后高高兴兴地返回retval就完事。

Select睡眠获取

Select先设置wait = __pollwait,然后去遍历rset,分别调用poll(file, retval ? NULL : wait)。然而没有任何fd有poll的相应,那么当前进程就去睡眠。如果有设备响应了任何一个fd,调用了wake_up(&head)。wake_up(&head)去调用default_wake_funtion()。这时在Select中睡眠的进程被唤醒,从死循环头部开始执行,以下流程就类似于Select的直接获取,不直接说了。但是要注意,如果是第3个fd开始响应的,那么前3个fd会被挂在等待队列头两次。不过最后都会被一起移除的。

Epoll直接获取

epoll_create()先初始化红黑树、创建文件并返回epoll的描述符。

epoll_ctl()判断这个注册的fd没在红黑树中,那么调用ep_insert()。ep_insert()设置wait = ep_ptable_queue_proc,然后把epitem加到红黑树中,然后调用poll(file, wait),设置callback为ep_poll_callback,然后把entry加入到设备的等待队列头中。这时检查有没有响应,有响应直接加入到rdllist中。

epoll_wait()检查rdllist是否为空。发现rdllist不为空之后,直接通过ep_send_events()返回rdllist。

Epoll睡眠获取

上述epoll_wait()函数发现rdllist是空的,那么就去睡觉。当设备有响应的时候,硬件设备执行wake_up(&dev->r_wait)。这个wake_up()调用等待的实体的ep_poll_callback(),这个ep_poll_callback()函数把准备好的epitem加入到struct eventpoll->rdllist或ovflist中,并且调用wake_up(&ep->wq)。这个wake_up()调用default_wake_function(),唤醒current进程。进程回到epoll_wait(),把current移除等待队列ep->wq,然后调用ep_send_events()。

你可能感兴趣的:(Select & Epoll原理)