[个人推荐] Linux poll机制分析(基于内核3.10.0)

原理分析来自网上,代码和数据结构图由本人梳理分析

阅读vhost的时候,发现使用了大量的等待队列和poll,这里温故而知新一下。
注:wait_queue_t是等待在wait_queue_head_t队列中的等待元素

所有的系统调用,基于都可以在它的名字前加上“ sys_ ”前缀,这就是它在内核中对应的函数。比如系统调用 open read write poll ,与之对应的内核函数为: sys_open sys_read sys_write sys_poll

一、内核框架:

1. 对于系统调用pollselect,它们对应的内核函数都是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
{
     /*设定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;
}

它对超时参数稍作处理后,直接调用 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)
{
........
     /*初始化poll_wqueues变量table,设置添加队列方法为__pollwait*/
     poll_initwait(&table);

........
     /*
     * 在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) - 六六哥 - 六六哥的博客
 
注: 参考本图可以很容易理解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 */
}

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)
{
........
     /*
     * 在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;
     } 

     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;
}

可见,它就是调用我们的驱动程序里注册的 poll 函数。

二、驱动程序:
驱动程序里与
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);
}

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
   一是上面说的“一定时间”到了,
   二是被驱动程序唤醒。驱动程序发现条件就绪时,就把“某个队列”上挂着的进程唤醒,这个队列,就是前面通过 poll_wait 把本进程挂过去的队列。

5.  如果驱动程序没有去唤醒进程,那么 poll_schedule_timeout (to) 超时后,会重复 2 3 动作,直到应用程序的 poll 调用传入的时间到达。  
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值