Some details about select implementation
文章目录
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
4sys_select()
core_sys_select()
do_select()
fop->poll()
1 | SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp, |
1 | int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp, |
1 | int do_select(int n, fd_set_bits *fds, struct timespec *end_time) |
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
9struct 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
5struct 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
3fop->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
8struct 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; // 设备驱动程序中特定事件的等待队列头;
};
总结一下几点:
- 特定的硬件设备驱动程序的事件等待队列头是有限个数的,通常有读事件和写事件的等待队列头;
- 一个调用了select()的应用进程只存在一个poll_wqueues结构体;
- 应用程序可以有多个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
10static 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
注意到这个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
__wake_up(x, TASK_NORMAL, 1, (void *) (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
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
19static 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.c1
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
6typedef 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
3do_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
38int 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
8if (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
2return ((client->head == client->tail) ? 0 : (POLLIN | POLLRDNORM)) |
(evdev->exist ? 0 : (POLLHUP | POLLERR));
会根据驱动程序中特定的buffer队列标志,来返回设备状态。这里的判断条件是读循环buffer的头尾指针是否相等:client->head == client->tail。
5. poll_wait()函数在select()睡眠前后调用的差异
1 | static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *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
13int 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);
// 默认通用的唤醒函数
}
参考资料: