博客园  :: 首页  :: 联系 :: 管理

select 实现分析 –2 【整理】

Posted on 2013-05-09 21:43  Apprentice89  阅读(8187)  评论(3编辑  收藏  举报

select 实现分析 –2 【整理】

 

l  select相关的结构体

比较重要的结构体由四个:struct poll_wqueuesstruct poll_table_pagestruct poll_table_entrystruct poll_table_struct

 

每一个调用select()系统调用的应用进程都会存在一个struct poll_wqueues结构体,用来统一辅佐实现这个进程中所有待监测的fd的轮询工作,后面所有的工作和都这个结构体有关,所以它非常重要。

 

struct poll_wqueues {

       poll_table pt;

       struct poll_table_page *table;

       struct task_struct *polling_task; //保存当前调用select的用户进程struct task_struct结构体

       int triggered;            // 当前用户进程被唤醒后置成1,以免该进程接着进睡眠

       int error;                 // 错误码

       int inline_index;        // 数组inline_entries的引用下标

       struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];

};

 

实际上结构体poll_wqueues内嵌的poll_table_entry数组inline_entries[] 的大小是有限的,如果空间不够用,后续会动态申请物理内存页以链表的形式挂载poll_wqueues.table上统一管理。接下来的两个结构体就和这项内容密切相关:

 

struct poll_table_page { // 申请的物理页都会将起始地址强制转换成该结构体指针

       struct poll_table_page   *next;      // 指向下一个申请的物理页

       struct poll_table_entry  *entry;     // 指向entries[]中首个待分配(空的) poll_table_entry地址

       struct poll_table_entry  entries[0]; // page页后面剩余的空间都是待分配的poll_table_entry结构体

};

 

 

对每一个fd调用fop->poll() => poll_wait() => __pollwait()都会先从poll_wqueues.inline_entries[]中分配一个poll_table_entry结构体,直到该数组用完才会分配物理页挂在链表指针poll_wqueues.table上然后才会分配一个poll_table_entry结构体poll_get_entry函数)。

poll_table_entry具体用处:函数__pollwait声明如下:

static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);

 

该函数调用时需要3个参数,第一个是特定fd对应的file结构体指针,第二个就是特定fd对应的硬件驱动程序中的等待队列头指针,第3个是调用select()的应用进程中poll_wqueues结构体的poll_table该进程监测的所有fd调用fop->poll函数都用这一个poll_table结构体

 

struct poll_table_entry {

       struct file     *filp;                 // 指向特定fd对应的file结构体;

       unsigned long   key;                   // 等待特定fd对应硬件设备的事件掩码,如POLLIN POLLOUTPOLLERR;

       wait_queue_t    wait;                  // 代表调用select()的应用进程,等待在fd对应设备的特定事件 (读或者写)的等待队列头上,的等待队列项;

       wait_queue_head_t   *wait_address;     // 设备驱动程序中特定事件的等待队列头(该fd执行fop->poll,需要等待时在哪等,所以叫等待地址);

};

 

总结几点:

1.  特定的硬件设备驱动程序的事件等待队列头是有限个数的,通常是有读事件和写事件的等待队列头;

2.  而一个调用了select()的应用进程只存在一个poll_wqueues结构体;

3.  该应用程序可以有多个fd在进行同时监测其各自的事件发生,但该应用进程中每一个fd有多少个poll_table_entry存在,那就取决于fd对应的驱动程序中有几个事件等待队列头了,也就是说,通常驱动程序的poll函数中需要对每一个事件的等待队列头调用poll_wait()函数。比如,如果有读写两个等待队列头,那么就在这个应用进程中存在两个poll_table_entry结构体,在这两个事件的等待队列头中分别将两个等待队列项加入;

4.  如果有多个应用进程使用select()方式同时在访问同一个硬件设备,此时硬件驱动程序中加入等待队列头中的等待队列项对每一个应用程序来说都是相同数量的一个事件等待队列头一个,数量取决于事件等待队列头的个数

 

 

do_select函数中,遍历所有nfd,对每一个fd调用对应驱动程序中的poll函数。

驱动程序中poll一般具有如下形式:

static unsigned int XXX_poll(struct file *filp, poll_table *wait)

{

   unsigned int mask = 0;

   struct XXX_dev *dev = filp->private_data;

   ...

   poll_wait(filp, &dev->r_wait, wait);

   poll_wait(filp ,&dev->w_wait, wait);

   if(CAN_READ)//读就绪

   {

      mask |= POLLIN | POLLRDNORM;

   }

   if(CAN_WRITE)//写就绪

   {

      mask |= POLLOUT | POLLRDNORM;

   }

   ...

   return mask;

}

 

以字符设备evdev为例(文件drivers/input/evdev.c

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

{

       struct evdev_client *client = file->private_data;

       struct evdev *evdev = client->evdev;

       poll_wait(file, &evdev->wait, wait);

       return ((client->head == client->tail) ? 0 : (POLLIN | POLLRDNORM)) | (evdev->exist ? 0 : (POLLHUP | POLLERR));

}

 

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

{

       if (p && wait_address)

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

}

 

其中wait_address是驱动程序需要提供的等待队列头,来容纳后续等待该硬件设备就绪的进程对应的等待队列项。

 

typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);

 

 

typedef struct poll_table_struct {

       poll_queue_proc qproc;

       unsigned long key;

} poll_table;

 

fop->poll()函数的poll_table参数是从哪里传进来的?好生阅读过代码就可以发现,do_select()函数中存在一个结构体struct poll_wqueues,其内嵌了一个poll_table的结构体,所以在后面的大循环中依次调用各个fdfop->poll()传递的poll_table参数都是poll_wqueues.poll_table

poll_table结构体的定义其实蛮简单,就一个函数指针,一个key值。这个函数指针在整个select过程中一直不变,而key则会根据不同的fd的监测要求而变化。

qproc函数初始化在函数do_select() –> poll_initwait() -> init_poll_funcptr(&pwq->pt, __pollwait)中实现,回调函数就是__pollwait()

 

int do_select(int n, fd_set_bits *fds, struct timespec *end_time)

{

       struct poll_wqueues table;

       ...

       poll_initwait(&table);

       ...

}

 

 

void poll_initwait(struct poll_wqueues *pwq)

{

       init_poll_funcptr(&pwq->pt, __pollwait);

       ...

}

 

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

{

       pt->qproc = qproc;

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

}

 

/* Add a new entry */

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

struct poll_table_entry *entry = poll_get_entry(pwq);

if (!entry)

return;

get_file(filp);

entry->filp = filp;      // 保存对应的file结构体

entry->wait_address = wait_address;  // 保存来自设备驱动程序的等待队列头

entry->key = p->key;  // 保存对该fd关心的事件掩码

init_waitqueue_func_entry(&entry->wait, pollwake);// 初始化等待队列项,pollwake是唤醒该等待队列项时候调用的函数

entry->wait.private = pwq; // poll_wqueues作为该等待队列项的私有数据,后面使用

add_wait_queue(wait_address, &entry->wait);// 将该等待队列项添加到从驱动程序中传递过来的等待队列头中去。

}

 

驱动程序在得知设备有IO事件时(通常是该设备上IO事件中断),会调用wakeupwakeup –> __wake_up_common -> curr->func(pollwake)

static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)

{

       struct poll_table_entry *entry;

       entry = container_of(wait, struct poll_table_entry, wait);// 取得poll_table_entry结构体指针

       if (key && !((unsigned long)key & entry->key))/*这里的条件判断至关重要,避免应用进程被误唤醒,什么意思?*/

              return 0;

       return __pollwake(wait, mode, sync, key);

}

 

pollwake调用__pollwake,最终调用default_wake_function

 

static int __pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)

{

struct poll_wqueues *pwq = wait->private;

DECLARE_WAITQUEUE(dummy_wait, pwq->polling_task);

smp_wmb();

pwq->triggered = 1; // select()用户进程只要有被唤醒过,就不可能再次进入睡眠,因为这个标志在睡眠的时候有用

return default_wake_function(&dummy_wait, mode, sync, key); // 默认通用的唤醒函数

}

 

最终唤醒调用select的进程,在do_select函数的schedule_timeout函数之后继续执行(继续for(;;),也即从新检查每一个fd是否有事件发生),此次检查会发现设备的该IO事件,于是select返回用户层。

 

结合这两节的内容,select的实现结构图如下:

 

 

 

参考:

http://blog.csdn.net/lizhiguo0532/article/details/6568969

http://blog.csdn.net/lizhiguo0532/article/details/6568968

推荐阅读该博客上的博文。