原理分析来自网上,代码和数据结构图由本人梳理分析
阅读vhost的时候,发现使用了大量的等待队列和poll,这里温故而知新一下。
注:wait_queue_t是等待在wait_queue_head_t队列中的等待元素
一、内核框架:
1. 对于系统调用poll或select,它们对应的内核函数都是sys_poll
。分析
sys_poll
,即可理解
poll
机制。
sys_poll
函数位于
fs/select.c
文件中,代码如下:
SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds, int, timeout_msecs) //就是sys_poll
{
/*
* 初始化poll_wqueues,设置poll_table的处理函数qproc为__pollwait,
* 这里的__pollwait函数将在驱动的poll方法中通过调用poll_wait函数来执行。
* poll_wait会执行p->_qproc,也就是__pollwait,将当前task加入等待队列后阻塞住
*/
init_poll_funcptr(&pwq->pt, __pollwait);
}
* 在for死循环中,首先轮询整条poll_list。在所有等待poll的操作中寻找是否有已满足条件的操作,有则跳出循环
*/
for (;;) {
for (walk = list; walk != NULL; walk = walk->next) {
for (; pfd != pfd_end; pfd++) {
/*
* 在do_pollfd函数中通过mask = file->f_op->poll来调用驱动中的poll方法,并且获得驱动中操作poll后的mask值。将当前任务加入到等待队列中
* 但是注意此时任务还没有block挂起,直到后面执行poll_schedule_timeout才挂起
*/
if (do_pollfd(pfd, pt, &can_busy_loop, busy_flag)) {
可见,它就是调用我们的驱动程序里注册的
poll
函数。
二、驱动程序:
驱动程序里与 poll 相关的地方有两处:
p->qproc
就是
__pollwait
函数,从它的代码可知,它只是把当前进程挂入我们驱动程序里定义的一个队列里而已。它的代码如下:
/*
* 此函数就是完成添加队列的工作。
* 把当前进程挂入我们驱动程序里定义的一个队列里
*/
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
{
struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);
/*取得一个空隙的entry*/
struct poll_table_entry *entry = poll_get_entry(pwq);
if (!entry)
return;
/*更新entry的信息,entry的wait元素的方法更新为pollwake*/
entry->filp = get_file(filp);
entry->wait_address = wait_address;
entry->key = p->_key;
init_waitqueue_func_entry(&entry->wait, pollwake);
entry->wait.private = pwq;
/*将当前实体wait加入到等待队列wait_address中*/
add_wait_queue(wait_address, &entry->wait);
}
执行到驱动程序的 poll_wait 函数时, 进程并没有休眠,我们的驱动程序里实现的poll函数是不会引起休眠的 。让进程进入休眠,是前面分析的 do_sys_poll 函数的 “poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack) ”。
poll_wait 只是把本进程挂入某个队列,应用程序调用 poll > sys_poll > do_sys_poll > poll_initwait , do_poll > do_pollfd > 我们自己写的 poll 函数加入到等待队列后,再调用 poll_schedule_timeout 进入休眠。如果我们的驱动程序发现情况就绪,可以把这个队列上挂着的进程唤醒。可见, poll_wait 的作用,只是为了让驱动程序能找到要唤醒的进程。即使不用 poll_wait ,我们的程序也有机会被唤醒: poll_schedule_timeout (to) ,只是休眠 to中指定的 这段时间。
现在来总结一下 poll 机制:
1. poll > sys_poll > do_sys_poll > poll_initwait , poll_initwait 函数注册一下回调函数 __pollwait ,它就是我们的驱动程序执行 poll_wait 时,真正被调用的函数。用来将当前任务加入等待队列中
2. 接下来执行 file->f_op->poll ,即我们驱动程序里自己实现的 poll 函数
它会调用 poll_wait 把自己挂入某个队列,这个队列也是我们的驱动自己定义的;
它还判断一下设备是否就绪。
3. 如果设备未就绪, do_sys_poll 里会让进程休眠一定时间
4. 进程被唤醒的条件有 2 :
SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds, int, timeout_msecs) //就是sys_poll
{
/*设定timeout*/
if (timeout_msecs >= 0) {
to = &end_time;
poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC, NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
}
//关键实现
ret = do_sys_poll(ufds, nfds, to);
......
return ret;
}
if (timeout_msecs >= 0) {
to = &end_time;
poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC, NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
}
//关键实现
ret = do_sys_poll(ufds, nfds, to);
......
return ret;
}
它对超时参数稍作处理后,直接调用
do_sys_poll
。
2.do_sys_poll 函数也位于位于 fs/select.c 文件中,我们忽略其他代码:
int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds, struct timespec *end_time)
2.do_sys_poll 函数也位于位于 fs/select.c 文件中,我们忽略其他代码:
int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds, struct timespec *end_time)
{
........
/*初始化poll_wqueues变量table,设置添加队列方法为__pollwait*/
poll_initwait(&table);
........
/*
* 在do_poll函数中有一个for的死循环,退出条件为count或者timeout为非零。
* count为非零标识do_pollfd函数返回的mask为真,timeout表示定时时间到。
*/
* 在do_poll函数中有一个for的死循环,退出条件为count或者timeout为非零。
* count为非零标识do_pollfd函数返回的mask为真,timeout表示定时时间到。
*/
fdcount = do_poll(nfds, head, &table, end_time);
........
}
poll_initwait
函数非常简单,它初始化一个
poll_wqueues
变量
table
:
![Linux poll机制分析(基于内核3.10.0) - 六六哥 - 六六哥的博客 Linux poll机制分析(基于内核3.10.0) - 六六哥 - 六六哥的博客](http://img1.ph.126.net/k_uema8DKv24e1Ra36XSag==/6630472929909443788.png)
注: 参考本图可以很容易理解poll_get_entry /* * 从poll_wqueues返回一个空闲的poll_table_entry */ static struct poll_table_entry *poll_get_entry(struct poll_wqueues *p) { struct poll_table_page *table = p->table; /* * 首先试图从内部已分配的inline_entries中返回一个poll_table_entry */ if (p->inline_index < N_INLINE_POLL_ENTRIES) return p->inline_entries + p->inline_index++; /* * 如果inline的没有,就分配页面,从页面中的poll_table_page的entries中取得entry * 在还没有poll_table_page或者poll_table_page中的entry已经用满的情况下,分配新的table */ if (!table || POLL_TABLE_FULL(table)) { struct poll_table_page *new_table; /*分配新的table*/ new_table = (struct poll_table_page *) __get_free_page(GFP_KERNEL); if (!new_table) { p->error = -ENOMEM; return NULL; } /*执行第一个entry,并且将table上链*/ new_table->entry = new_table->entries; new_table->next = table; p->table = new_table; /*更新table的指针*/ table = new_table; } /* * 更新并返回entries中下一个新的entry的地址 * 此时table和entry分别执行最新的table和table中最新的空闲entry */ return table->entry++; } |
/*
* 初始化poll_wqueues,设置poll_table的处理函数qproc为__pollwait,
* 这里的__pollwait函数将在驱动的poll方法中通过调用poll_wait函数来执行。
* poll_wait会执行p->_qproc,也就是__pollwait,将当前task加入等待队列后阻塞住
*/
void poll_initwait(struct poll_wqueues *pwq)
{
init_poll_funcptr(&pwq->pt, __pollwait);
}
/*初始化poll_table的处理函数_qproc为指定的处理函数,select认为__pollwait*/
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
pt->_qproc = qproc;
pt->_key = ~0UL; /* all events enabled */
}
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
pt->_qproc = qproc;
pt->_key = ~0UL; /* all events enabled */
}
即
table->pt->qproc = __pollwait
,
__pollwait
将在驱动的
poll
函数里用到。(通过驱动的poll方法,调用poll_wait,poll_wait调用qproc方法,也就调用到了__poll_wait)
3.do_poll 函数位于 fs/select.c 文件中,代码如下:
static int do_poll(unsigned int nfds, struct poll_list *list, struct poll_wqueues *wait, struct timespec *end_time)
3.do_poll 函数位于 fs/select.c 文件中,代码如下:
static int do_poll(unsigned int nfds, struct poll_list *list, struct poll_wqueues *wait, struct timespec *end_time)
{
........
/*
* 在for死循环中,首先轮询整条poll_list。在所有等待poll的操作中寻找是否有已满足条件的操作,有则跳出循环
*/
for (;;) {
for (walk = list; walk != NULL; walk = walk->next) {
for (; pfd != pfd_end; pfd++) {
/*
* 在do_pollfd函数中通过mask = file->f_op->poll来调用驱动中的poll方法,并且获得驱动中操作poll后的mask值。将当前任务加入到等待队列中
* 但是注意此时任务还没有block挂起,直到后面执行poll_schedule_timeout才挂起
*/
if (do_pollfd(pfd, pt, &can_busy_loop, busy_flag)) {
......
}
}
}
}
}
}
if (count || timed_out)
break;
/*
* 执行poll_schedule_timeout(), 执行完此函数后,进程进入休眠,直到被wake_up或者休眠时间到。
*/
if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack))
timed_out = 1;
① 这是个循环,它退出的条件为:
a. 3 个条件之一 (count 非 0 ,超时、有信号等待处理 )
count 不为 0 表示 do_pollfd 至少有一个成功。
b. 发生错误
② 重点在 do_pollfd 函数,后面再分析
③ 让本进程休眠一段时间,注意:应用程序执行 poll 调用后,如果①②的条件不满足,进程就会进入休眠。那么,谁唤醒呢?除了休眠到指定时间被系统唤醒外,还可以被驱动程序唤醒──记住这点,这就是为什么驱动的 poll 里要调用 poll_wait 的原因,后面分析。
4. do_pollfd 函数位于 fs/select.c 文件中,代码如下:
static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait, bool *can_busy_poll, unsigned int busy_flag)
{
if (fd >= 0) {
if (f.file) {
if (f.file->f_op && f.file->f_op->poll) {
/*
* 即调用驱动中的poll函数(在写驱动程序时编写poll函数)
* 通过file->f_op->poll来调用驱动中的poll方法,而驱动中的poll方法会调用poll_wait函数
* poll_wait函数调用poll_initwait函数注册的__pollwait函数
* (驱动函数的poll的实现方法中,一般都会调用这个poll_wait,如dvb_audio_poll(), bttv_poll())
break;
/*
* 执行poll_schedule_timeout(), 执行完此函数后,进程进入休眠,直到被wake_up或者休眠时间到。
*/
if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack))
timed_out = 1;
}
return count;
}
分析其中的代码,可以发现,它的作用如下:
① 这是个循环,它退出的条件为:
a. 3 个条件之一 (count 非 0 ,超时、有信号等待处理 )
count 不为 0 表示 do_pollfd 至少有一个成功。
b. 发生错误
② 重点在 do_pollfd 函数,后面再分析
③ 让本进程休眠一段时间,注意:应用程序执行 poll 调用后,如果①②的条件不满足,进程就会进入休眠。那么,谁唤醒呢?除了休眠到指定时间被系统唤醒外,还可以被驱动程序唤醒──记住这点,这就是为什么驱动的 poll 里要调用 poll_wait 的原因,后面分析。
4. do_pollfd 函数位于 fs/select.c 文件中,代码如下:
static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait, bool *can_busy_poll, unsigned int busy_flag)
{
if (fd >= 0) {
if (f.file) {
if (f.file->f_op && f.file->f_op->poll) {
/*
* 即调用驱动中的poll函数(在写驱动程序时编写poll函数)
* 通过file->f_op->poll来调用驱动中的poll方法,而驱动中的poll方法会调用poll_wait函数
* poll_wait函数调用poll_initwait函数注册的__pollwait函数
* (驱动函数的poll的实现方法中,一般都会调用这个poll_wait,如dvb_audio_poll(), bttv_poll())
* 注意,这里本任务不会休眠,只是加入到了wait_queue中,需要后面调用schedule()才会休眠
*/
mask = f.file->f_op->poll(f.file, pwait);
}
}
}
pollfd->revents = mask;
return mask;
}
*/
mask = f.file->f_op->poll(f.file, pwait);
}
}
}
pollfd->revents = mask;
return mask;
}
二、驱动程序:
驱动程序里与 poll 相关的地方有两处:
一是构造
file_operation
结构时,要定义自己的
poll
函数。
二是通过
poll_wait
来调用上面说到的
__pollwait
函数,
pollwait
的代码如下:
/*
* 通过调用poll_table注册的_qproc函数,来实现将任务加入等待队列,select中为__pollwait
*/
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);
}
* 通过调用poll_table注册的_qproc函数,来实现将任务加入等待队列,select中为__pollwait
*/
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);
}
/*
* 此函数就是完成添加队列的工作。
* 把当前进程挂入我们驱动程序里定义的一个队列里
*/
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
{
struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);
/*取得一个空隙的entry*/
struct poll_table_entry *entry = poll_get_entry(pwq);
if (!entry)
return;
/*更新entry的信息,entry的wait元素的方法更新为pollwake*/
entry->filp = get_file(filp);
entry->wait_address = wait_address;
entry->key = p->_key;
init_waitqueue_func_entry(&entry->wait, pollwake);
entry->wait.private = pwq;
/*将当前实体wait加入到等待队列wait_address中*/
add_wait_queue(wait_address, &entry->wait);
}
执行到驱动程序的 poll_wait 函数时, 进程并没有休眠,我们的驱动程序里实现的poll函数是不会引起休眠的 。让进程进入休眠,是前面分析的 do_sys_poll 函数的 “poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack) ”。
poll_wait 只是把本进程挂入某个队列,应用程序调用 poll > sys_poll > do_sys_poll > poll_initwait , do_poll > do_pollfd > 我们自己写的 poll 函数加入到等待队列后,再调用 poll_schedule_timeout 进入休眠。如果我们的驱动程序发现情况就绪,可以把这个队列上挂着的进程唤醒。可见, poll_wait 的作用,只是为了让驱动程序能找到要唤醒的进程。即使不用 poll_wait ,我们的程序也有机会被唤醒: poll_schedule_timeout (to) ,只是休眠 to中指定的 这段时间。
现在来总结一下 poll 机制:
1. poll > sys_poll > do_sys_poll > poll_initwait , poll_initwait 函数注册一下回调函数 __pollwait ,它就是我们的驱动程序执行 poll_wait 时,真正被调用的函数。用来将当前任务加入等待队列中
2. 接下来执行 file->f_op->poll ,即我们驱动程序里自己实现的 poll 函数
它会调用 poll_wait 把自己挂入某个队列,这个队列也是我们的驱动自己定义的;
它还判断一下设备是否就绪。
3. 如果设备未就绪, do_sys_poll 里会让进程休眠一定时间
4. 进程被唤醒的条件有 2 :
一是上面说的“一定时间”到了,
二是被驱动程序唤醒。驱动程序发现条件就绪时,就把“某个队列”上挂着的进程唤醒,这个队列,就是前面通过
poll_wait
把本进程挂过去的队列。
5. 如果驱动程序没有去唤醒进程,那么 poll_schedule_timeout (to) 超时后,会重复 2 、 3 动作,直到应用程序的 poll 调用传入的时间到达。
5. 如果驱动程序没有去唤醒进程,那么 poll_schedule_timeout (to) 超时后,会重复 2 、 3 动作,直到应用程序的 poll 调用传入的时间到达。