select usage and implementation in kernel简单介绍了select的内核实现,file_operations poll function介绍了file_operations中的 poll函数。在看完 select(poll)系统调用实现解析(一)文章后,发现还是需要介绍下detail。本文内容源于该系列文章。

PS:本文不适合阅读,但是当结合代码看时效果明显。

1. 为什么要实现 file_operation结构体的poll函数?

上层要能使用select()和poll()系统调用来监测某个设备文件描述符,那么就必须实现这个设备驱动程序中struct file_operation结构体的poll函数,为什么?

因为这两个系统调用最终都会调用驱动程序中的poll函数来初始化一个等待队列项, 然后将其加入到驱动程序中的等待队列头,这样就可以在硬件可读写的时候wake up这个等待队列头,然后等待硬件设备可读写事件的进程都将被唤醒。(这个等待队列头可以包含多个等待队列项,这些不同的等待队列项是由不同的应用程序调用select或者poll来监测同一个硬件设备的时候调用file_operation的poll函数初始化填充的)。

2. select系统调用

select()系统调用代码
调用顺序如下:

1
2
3
4
sys_select()
core_sys_select()
do_select()
fop->poll()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp, 
fd_set __user *, exp, struct timeval __user *, tvp)
{
       struct timespec end_time, *to = NULL;
       struct timeval tv;
       int ret; 
       if (tvp) {// 如果超时值非NULL
              if (copy_from_user(&tv, tvp, sizeof(tv)))   // 从用户空间取数据到内核空间
                     return -EFAULT;
              to = &end_time;
              // 得到timespec格式的未来超时时间
              if (poll_select_set_timeout(to,
                            tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
                            (tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
                     return -EINVAL;
       }
       ret = core_sys_select(n, inp, outp, exp, to);             // 关键函数
       ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);
       /*如果有超时值, 并拷贝离超时时刻还剩的时间到用户空间的timeval中*/

       return ret;             // 返回就绪的文件描述符的个数
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp, 
fd_set __user *exp, struct timespec *end_time)
{
       fd_set_bits fds;
 /**
       typedef struct {
              unsigned long *in, *out, *ex;
              unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;
这个结构体中定义的全是指针,这些指针都是用来指向描述符集合的。
**/

       void *bits;
       int ret, max_fds;
       unsigned int size;
       struct fdtable *fdt;
       /* Allocate small arguments on the stack to save memory and be faster */
       long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
       ret = -EINVAL;
       if (n < 0)
              goto out_nofds;
       /* max_fds can increase, so grab it once to avoid race */
       rcu_read_lock();
       fdt = files_fdtable(current->files); // RCU ref, 获取当前进程的文件描述符表
       max_fds = fdt->max_fds;
       rcu_read_unlock();
       if (n > max_fds)// 如果传入的n大于当前进程最大的文件描述符,给予修正
              n = max_fds;
             
       /*
        * We need 6 bitmaps (in/out/ex for both incoming and outgoing),
        * since we used fdset we need to allocate memory in units of
        * long-words.
        */
       size = FDS_BYTES(n);
       // 以一个文件描述符占一bit来计算,传递进来的这些fd_set需要用掉多少个字
       bits = stack_fds;
       if (size > sizeof(stack_fds) / 6) {
              // 除6,为什么?因为每个文件描述符需要6个bitmaps
              /* Not enough space in on-stack array; must use kmalloc */
              ret = -ENOMEM;
              bits = kmalloc(6 * size, GFP_KERNEL); // stack中分配的太小,直接kmalloc
              if (!bits)
                     goto out_nofds;
       }

       // 这里就可以明显看出struct fd_set_bits结构体的用处了。
       fds.in      = bits;
       fds.out     = bits +   size;
       fds.ex      = bits + 2*size;
       fds.res_in  = bits + 3*size;
       fds.res_out = bits + 4*size;
       fds.res_ex  = bits + 5*size;

       // get_fd_set仅仅调用copy_from_user从用户空间拷贝了fd_set
       if ((ret = get_fd_set(n, inp, fds.in)) ||
           (ret = get_fd_set(n, outp, fds.out)) ||
           (ret = get_fd_set(n, exp, fds.ex)))
              goto out;
       zero_fd_set(n, fds.res_in);  // 对这些存放返回状态的字段清0
       zero_fd_set(n, fds.res_out);
       zero_fd_set(n, fds.res_ex);

       ret = do_select(n, &fds, end_time);    // 关键函数,完成主要的工作
 
       if (ret < 0)             // 有错误
              goto out;
       if (!ret) {              // 超时返回,无设备就绪
              ret = -ERESTARTNOHAND;
              if (signal_pending(current))
                     goto out;
              ret = 0;
       }

       // 把结果集,拷贝回用户空间
       if (set_fd_set(n, inp, fds.res_in) ||
           set_fd_set(n, outp, fds.res_out) ||
           set_fd_set(n, exp, fds.res_ex))
              ret = -EFAULT;
             
out:
       if (bits != stack_fds)
              kfree(bits);     // 如果有申请空间,那么释放fds对应的空间
out_nofds:
       return ret;                    // 返回就绪的文件描述符的个数
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
       ktime_t expire, *to = NULL;
       struct poll_wqueues table;
       poll_table *wait;
       int retval, i, timed_out = 0;
       unsigned long slack = 0;
       rcu_read_lock();

       // 根据已经设置好的fd位图检查用户打开的fd, 要求对应fd必须打开, 并且返回最大的fd。
       retval = max_select_fd(n, fds);
       rcu_read_unlock();
       if (retval < 0)
              return retval;
       n = retval;

       // 一些重要的初始化:
       // poll_wqueues.poll_table.qproc函数指针初始化,该函数是驱动程序中poll函数实
       // 现中必须要调用的poll_wait()中使用的函数。
       poll_initwait(&table);
       wait = &table.pt;
       if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
              wait = NULL;
              timed_out = 1;     // 如果系统调用带进来的超时时间为0,那么设置
                                          // timed_out = 1,表示不阻塞,直接返回。
       }

       if (end_time && !timed_out)
              slack = estimate_accuracy(end_time); // 超时时间转换
             
       retval = 0;
       for (;;) {
              unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
              inp = fds->in; outp = fds->out; exp = fds->ex;
              rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
             
              // 所有n个fd的循环
              for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
                     unsigned long in, out, ex, all_bits, bit = 1, mask, j;
                     unsigned long res_in = 0, res_out = 0, res_ex = 0;
                     const struct file_operations *f_op = NULL;
                     struct file *file = NULL;

                     // 先取出当前循环周期中的32个文件描述符对应的bitmaps
                     in = *inp++; out = *outp++; ex = *exp++;
                     all_bits = in | out | ex;  // 组合一下,有的fd可能只监测读,或者写,
                     //或者e rr,或者同时都监测
                     if (all_bits == 0) {  // 这32个描述符没有任何状态被监测,就跳入
// 下一个32个fd的循环中
                            i += __NFDBITS; //每32个文件描述符一个循环,正好一个long型数
                            continue;
                     }

                     // 本次32个fd的循环中有需要监测的状态存在
                     for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {// 初始bit = 1
                            int fput_needed;
                            if (i >= n)      // i用来检测是否超出了最大待监测的fd
                                   break;
                            if (!(bit & all_bits))
                                   continue; // bit每次循环后左移一位的作用在这
                                   // 跳过没有状态监测的fd
                            file = fget_light(i, &fput_needed); // 得到file结构指针,并增加
// 引用计数字段f_count
                            if (file) {        // 如果file存在
                                   f_op = file->f_op;
                                   mask = DEFAULT_POLLMASK;
                                   if (f_op && f_op->poll) {
                                          wait_key_set(wait, in, out, bit);
                                          // 设置当前fd待监测的事件掩码
                                          mask = (*f_op->poll)(file, wait);
/*
调用驱动程序中的poll函数,
以evdev驱动中的evdev_poll()为例该函数会调用函数poll_wait(file, &evdev->wait, wait),
继续调用__pollwait()回调来分配一个poll_table_entry结构体,
该结构体有一个内嵌的等待队列项,
设置好wake时调用的回调函数后将其添加到驱动程序中的等待队列头中。
*/
                                   }
                                   fput_light(file, fput_needed);
                                   // 释放file结构指针,实际就是减小他的一个引用计数字段f_count。

                                   // mask是每一个fop->poll()程序返回的设备状态掩码。
                                   if ((mask & POLLIN_SET) && (in & bit)) {
                                          res_in |= bit;         // fd对应的设备可读
                                          retval++;
                                          wait = NULL;       // 后续有用,避免重复执行__pollwait()
                                   }
                                   if ((mask & POLLOUT_SET) && (out & bit)) {
                                          res_out |= bit;              // fd对应的设备可写
                                          retval++;
                                          wait = NULL;
                                   }
                                   if ((mask & POLLEX_SET) && (ex & bit)) {
                                          res_ex |= bit;
                                          retval++;
                                          wait = NULL;
                                   }
                            }
                     }

                     // 根据poll的结果写回到输出位图里,返回给上级函数
                     if (res_in)
                            *rinp = res_in;
                     if (res_out)
                            *routp = res_out;
                     if (res_ex)
                            *rexp = res_ex;

                     /*
                            这里的目的纯粹是为了增加一个抢占点。
                            在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),
cond_resched是空操作。
                     */
                     cond_resched();
              }
              wait = NULL// 后续有用,避免重复执行__pollwait()
              if (retval || timed_out || signal_pending(current))
                     break;
              if (table.error) {
                     retval = table.error;
                     break;
              }
              /*跳出这个大循环的条件有: 有设备就绪或有异常(retval!=0), 超时(timed_out

              = 1), 或者有中止信号出现*/

              /*
               * If this is the first loop and we have a timeout
               * given, then we convert to ktime_t and set the to
               * pointer to the expiry value.
               */

              if (end_time && !to) {
                     expire = timespec_to_ktime(*end_time);
                     to = &expire;
              }

              // 第一次循环中,当前用户进程从这里进入休眠,
              //上面传下来的超时时间只是为了用在睡眠超时这里而已
              // 超时,poll_schedule_timeout()返回0;被唤醒时返回-EINTR
              if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
                                      to, slack))
                     timed_out = 1; /* 超时后,将其设置成1,方便后面退出循环返回到上层 */
       }

       // 清理各个驱动程序的等待队列头,同时释放掉所有空出来的page页(poll_table_entry)

       poll_freewait(&table);

       return retval; // 返回就绪的文件描述符的个数
}

3. 重要结构体之间关系

比较重要的结构体由四个:struct poll_wqueues、struct poll_table_page、struct poll_table_entry、struct poll_table_struct。

3.1 结构体关系

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

1
2
3
4
5
6
7
8
9
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上统一管理。接下来的两个结构体就和这项内容密切相关:

1
2
3
4
5
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调用

1
2
3
fop->poll() 
poll_wait()
__pollwait()

都会先从poll_wqueues. inline_entries[]中分配一个poll_table_entry结构体,直到该数组用完才会分配物理页挂在链表指针poll_wqueues.table上,然后才会分配一个poll_table_entry结构体。具体用来做什么?这里先简单说说,__pollwait()函数调用时需要3个参数,第一个是特定fd对应的file结构体指针,第二个就是特定fd对应的硬件驱动程序中的等待队列头指针,第3个是调用select()的应用进程中poll_wqueues结构体的poll_table项(该进程监测的所有fd,调用fop->poll函数时都用这一个poll_table结构体)。

1
2
3
4
5
6
7
8
struct poll_table_entry {
       struct file *filp;            // 指向特定fd对应的file结构体;
       unsigned long key;            // 等待特定fd对应硬件设备的事件掩码,如POLLIN、
//  POLLOUT、POLLERR;
       wait_queue_t wait;           // 代表调用select()的应用进程,等待在fd对应设备的特定事件
//  (读或者写)的等待队列头上,的等待队列项;
       wait_queue_head_t *wait_address; // 设备驱动程序中特定事件的等待队列头;
};

总结一下几点:

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

3.2 注意项

对于第3点中,如果驱动程序中有多个事件等待队列头,那么在这种情况下,写设备驱动程序时就要特别小心了,特别是设备有事件就绪,然后唤醒等待队列头中所有应用进程的时候,需要使用另外的宏。

在这之前看一看__pollwait()函数中填充poll_table_entry结构体时注册的唤醒回调函数pollwake()。

1
2
3
4
5
6
7
8
9
10
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);
}

驱动程序中存在多个事件的等待队列头,并且应用程序中只监测了该硬件的某几项事件,比如,驱动中有读写等待队列头,但应用程序只监测读事件的发生。这种情况下,写驱动程序时候,如果唤醒函数用法不当,就会引起误唤醒的情况。
先来看一看我们熟知的一些唤醒函数吧!

1
2
#define wake_up(x)                    __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_interruptible(x)      __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

注意到这个key了吗?通常我们调用唤醒函数时key为NULL,很容易看出,如果我们在这种情况下,使用上面两种唤醒函数,那么key && !((unsigned long)key & entry->key)的判断条件一直都会是假,也就是说,只要设备的几类事件之一有发生,不管应用程序中是否对其有监测,都会在这里顺利通过,将应用程序唤醒,唤醒后,重新调用一遍fop->poll(注意:第一次和第二次调用该函数时少做了一件事,后面代码详解)函数,得到设备事件掩码。假如恰好在这次唤醒后的一轮调用fop->poll()函数的循环中,没有其他硬件设备就绪,那么可想而知,从源码上看,do_select()会直接返回0。

1
2
3
4
5
6
// mask是每一个fop->poll()程序返回的设备状态掩码。
if ((mask & POLLIN_SET) && (in & bit)) {
       res_in |= bit;         // fd对应的设备可读
       retval++;
       wait = NULL;              // 后续有用,避免重复执行__pollwait()
}

(in & bit)这个条件就是用来确认用户程序有没有让你监测该事件的, 如果没有retval仍然是0,基于前面的假设,那么do_select()返回给上层的也是0。那又假如应用程序中调用select()的时候没有传入超时值,那岂不是和事实不相符合吗?没有传递超时值,那么select()函数会一直阻塞直到至少有1个fd的状态就绪。

所以在这种情况下,设备驱动中唤醒函数需要用另外的一组:

1
2
3
4
5
#define wake_up_poll(x, m)                            /
       __wake_up(x, TASK_NORMAL, 1, (void *) (m))
      
#define wake_up_interruptible_poll(x, m)               /
       __wake_up(x, TASK_INTERRUPTIBLE, 1, (void *) (m))

上述的m值,应该和设备发生的事件相符合。设置poll_table_entry结构体key项的函数是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
#define POLLEX_SET (POLLPRI)

static inline void wait_key_set(poll_table *wait, unsigned long in,
                            unsigned long out, unsigned long bit)
{
       if (wait) {
              wait->key = POLLEX_SET;
              if (in & bit)
                     wait->key |= POLLIN_SET;
              if (out & bit)
                     wait->key |= POLLOUT_SET;
       }
}

这里的m值,可以参考上面的宏来设置,注意传递的不是key的指针,而就是其值本身,只不过在wake_up_poll()到pollwake()的传递过程中是将其转换成指针的。

如果唤醒函数使用后面一组的话,再加上合理设置key值,我相信pollwake()函数中的if一定会严格把关,不让应用程序没有监测的事件唤醒应用进程,从而避免了发生误唤醒。

4. fop->poll()

fop->poll()函数就是file_operations结构体中的poll函数指针项,该函数相信很多人都知道怎么写,网上大把的文章介绍其模板,但是为什么要那么写,而且它做了什么具体的事情?本小节来揭开其神秘面纱,先贴一个模板上来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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(...)//读就绪
    {
          mask |= POLLIN | POLLRDNORM;
     }
    if(...)//写就绪
    {
          mask |= POLLOUT | POLLRDNORM;
     }
    ..
    return mask;
}

这个poll_wait()函数所做的工作挺简单,就是添加一个等待队列项到poll_wait ()函数传递进去的第二个参数,其代表的是驱动程序中的特定事件的等待队列头。

下面以字符设备evdev为例,文件drivers/input/evdev.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 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是驱动程序需要提供的等待队列头,来容纳后续等待该硬件设备就绪的进程对应的等待队列项。关键结构体poll_table, 这个结构体名字也取的不好,什么table?其实其中没有table的一丁点概念,容易让人误解呀!

1
2
3
4
5
6
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的结构体,所以在后面的大循环中依次调用各个fd的fop->poll()传递的poll_table参数都是poll_wqueues.poll_table。

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

qproc函数初始化在函数

1
2
3
do_select()
poll_initwait()
init_poll_funcptr(&pwq->pt, __pollwait)

中实现,回调函数就是__pollwait()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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);
       // 将该等待队列项添加到从驱动程序中传递过来的等待队列头中去。
}

该函数首先通过container_of宏来得到结构体poll_wqueues的地址,然后调用poll_get_entry()函数来获得一个poll_table_entry结构体,这个结构体是用来连接驱动和应用进程的关键结构体,其实联系很简单,这个结构体中内嵌了一个等待队列项wait_queue_t,和一个等待队列头 wait_queue_head_t,它就是驱动程序中定义的等待队列头,应用进程就是在这里保存了每一个硬件设备驱动程序中的等待队列头(当然每一个fd都有一个poll_table_entry结构体)。

很容易想到的是,如果这个设备在别的应用程序中也有使用,又恰好别的应用进程中也是用select()来访问该硬件设备,那么在另外一个应用进程的同一个地方也会调用同样的函数来初始化一个poll_table_entry结构体,然后将这个结构体中内嵌的等待队列项添加到同一份驱动程序的等待队列头中。此后,如果设备就绪了,那么驱动程序中将会唤醒这个对于等待队列头中所有的等待队列项(也就是等待在该设备上的所有应用进程,所有等待的应用进程将会得到同一份数据)。

上面语句保存了一个应用程序select一个fd的硬件设备时最全的信息,方便在设备就绪的时候容易得到对应的数据。这里的entry->key值就是为了防止误唤醒而准备的。设置这个key值的地方在函数do_select()中。如下:

1
2
3
4
5
6
7
8
if (file) {
       f_op = file->f_op;
       mask = DEFAULT_POLLMASK;
       if (f_op && f_op->poll) {
              wait_key_set(wait, in, out, bit);   
mask = (*f_op->poll)(file, wait);
       }
}

fop->poll()函数的返回值都是有规定的,例如函数evdev_poll()中的返回值:

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

会根据驱动程序中特定的buffer队列标志,来返回设备状态。这里的判断条件是读循环buffer的头尾指针是否相等:client->head == client->tail。

5. poll_wait()函数在select()睡眠前后调用的差异

1
2
3
4
5
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);
}

这里有一个if条件判断,如果驱动程序中没有提供等待队列头wait_address,那么将不会往下执行p->qproc,也就是不会将当前应用进程的等待队列项添加进驱动程序中对应的等待队列头中。

如果select()中调用fop->poll()时传递进来的poll_table是NULL,通常情况下,只要在应用层传递进来的超时时间结构体值不为0,哪怕这个结构体指针你传递NULL,那么在函数do_select()中第一次睡眠之前的那次所有fd的大循环中,调用fop->poll()函数传递的poll_table是绝对不会为NULL的。但是第一次睡眠唤醒之后的又一次所有fd的大循环中,再次调用fop->poll()函数时,此时传递的poll_table是NULL,可想而知,这一次只是检查fop->poll()的返回状态值而已。如果从上层调用select时传递的超时值结构体赋值成0,那么do_select()函数的只会调用一次所有fd的大循环,之后不再进入睡眠,直接返回0给上层,基本上这种情况是没有得到任何有用的状态。

为了避免应用进程被唤醒之后再次调用pollwait()的时,重复地调用函数__pollwait(),在传递poll_table结构体指针的时候,在睡眠之前保证其为有效地址,而在唤醒之后保证传入的poll_table地址是NULL,因为在唤醒之后,再次调用fop->poll()的作用只是为了再次检查设备的事件状态而已。具体详见代码。

6. 唤醒应用进程

注意事项中已经讨论过驱动程序唤醒进程的一点注意项,但这里再次介绍睡眠唤醒的整个流程。
睡眠时调用函数poll_schedule_timeout()来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
int poll_schedule_timeout(struct poll_wqueues *pwq, int state,
ktime_t *expires, unsigned long slack)
{
       int rc = -EINTR;
       set_current_state(state);
       if (!pwq->triggered)  // 这个triggered在什么时候被置1的呢?只要有一个fd
// 对应的设备将当前应用进程唤醒后将会把它设置成1
              rc = schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS);
       __set_current_state(TASK_RUNNING);
  
       set_mb(pwq->triggered, 0);
       return rc;
}

唤醒的话会调用函数pollwake():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 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);
       if (key && !((unsigned long)key & entry->key))
              return 0;
       return __pollwake(wait, mode, sync, key);
}

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);
       /*
        * Although this function is called under waitqueue lock, LOCK
        * doesn't imply write barrier and the users expect write
        * barrier semantics on wakeup functions.  The following
        * smp_wmb() is equivalent to smp_wmb() in try_to_wake_up()
        * and is paired with set_mb() in poll_schedule_timeout.
        */
       smp_wmb();
       pwq->triggered = 1;
       // select()用户进程只要有被唤醒过,就不可能再次进入睡眠,因为这个标志在睡眠的时候有用          
       return default_wake_function(&dummy_wait, mode, sync, key);
       // 默认通用的唤醒函数
}


参考资料:

  1. select(poll)系统调用实现解析(一)

  2. select(poll)系统调用实现解析(二)

  3. select(poll)系统调用实现解析(三)