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

来源:互联网 发布:淘宝女包品牌排行 编辑:程序博客网 时间:2024/06/05 11:12
原理分析来自网上,代码和数据结构图由本人梳理分析

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

所有的系统调用,基于都可以在它的名字前加上“sys_”前缀,这就是它在内核中对应的函数。比如系统调用openreadwritepoll,与之对应的内核函数为:sys_opensys_readsys_writesys_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个条件之一(count0,超时、有信号等待处理)
     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_initwaitdo_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_initwaitpoll_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)超时后,会重复23动作,直到应用程序的poll调用传入的时间到达。 
0 0
原创粉丝点击