1. poll 机制
本篇主要是进行 poll 内核实现,调用流程的分析。
1.1 sys_poll 分析
应用程序调用 poll() 后,经过 SWI 的处理后,最终会进入内核的 sys_poll() 函数:
asmlinkage long sys_poll(struct pollfd __user *ufds, unsigned int nfds,
long timeout_msecs)
这个函数有两个部分:
-
第一部是将应用程序传递下来的时间(poll的第三个参数timespec),从毫秒单位转为节拍单位。
s64 timeout_jiffies; if (timeout_msecs > 0) { #if HZ > 1000 /* We can only overflow if HZ > 1000 */ if (timeout_msecs / 1000 > (s64)0x7fffffffffffffffULL / (s64)HZ) timeout_jiffies = -1; else #endif timeout_jiffies = msecs_to_jiffies(timeout_msecs); } else { /* Infinite (< 0) or no (0) timeout */ timeout_jiffies = timeout_msecs; }
-
第二部分是调用
do_sys_poll(ufds, nfds, &timeout_jiffies);
1.2 do_sys_poll 分析
do_sys_poll() 完成的工作有:
-
将用户空间的传递下来的 pollfds 链表拷贝到内核空间中,并把每一个 pollfd 封装成一个 poll_list 对象。将所有的 poll_list 对象,串联成一个单链表。
首先会尝试把所有的 poll_list 都存储在 do_sys_poll() 的函数栈中;
如果函数栈无法容纳全部 poll_list,则会在堆中开辟新空间,用来存储多出来的 poll_list;
-
创建一个 struct poll_wqueues table 对象。
struct poll_wqueues { poll_table pt; struct poll_table_page * table; int error; int inline_index; struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES]; };
调用 poll_initwait() 函数,初始化该对象。poll_initwait() 会调用
init_poll_funcptr(&pwq->pt, __pollwait);
初始化 poll_table;static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc) { pt->qproc = qproc; }
最终在 poll_table 中保存一个指向 __pollwait 的指针函数。
-
接着调用 do_poll();
1.3 do_poll 分析
接下来对 do_poll() 进行详细分析:
static int do_poll(unsigned int nfds, struct poll_list *list,
struct poll_wqueues *wait, s64 *timeout)
{
/* ...省略... */
for (;;) {
/* ...省略...*/
for (walk = list; walk != NULL; walk = walk->next) {
/* ...省略...*/
for (; pfd != pfd_end; pfd++) {
/*
* Fish for events. If we found one, record it
* and kill the poll_table, so we don't
* needlessly register any other waiters after
* this. They'll get immediately deregistered
* when we break out and return.
*/
if (do_pollfd(pfd, pt)) {
/* 如果驱动程序的poll返回非零,说明有事件发生,那么count++ */
count++;
pt = NULL;
}
}
}
/* 被唤醒的pfd个数大于零、或超时、或接受到了信号退出 */
if (count || !*timeout || signal_pending(current))
break;
/* ...省略...*/
/* 进入休眠,
* 如果休眠结束仍然没有事件发生,会再次执行一次for循环
* 当在此到达上面的条件时,超时退出。
*/
__timeout = schedule_timeout(__timeout);
/* ...省略...*/
return count; /* 有事件的fd个数 */
}
do_poll() 的核心代码,是对每一个 poll_list 对象,都对其调用一次 do_pollfd();
do_pollfd() 的定义如下:
static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait)
{
/* ...省略... */
if (fd >= 0) {
/* ...省略... */
if (file != NULL) {
/* ...省略... */
file = fget_light(fd, &fput_needed);
if (file->f_op && file->f_op->poll)
mask = file->f_op->poll(file, pwait);
/* Mask out unneeded events. */
mask &= pollfd->events | POLLERR | POLLHUP;
/* ...省略... */
}
该函数完成的工作是通过 pollfd 中的 fd,获取到文件描述所对应的文件 file 对象。
接着通过文件对象的 file_operations 操作结构体,调用 file_operations 的 .poll 成员,从而访问到驱动的实现的 .poll 函数;
1.4 驱动程序的 .poll 函数
以 s3c2440 为例,一般驱动程序中会调用 poll_wait 函数;
static unsigned s3c2440_buttons_poll(struct file *file, poll_table *wait)
{
unsigned int mask = 0;
poll_wait(file, &button_waitq, wait);
if (ev_press)
mask |= POLLIN | POLLRDBAND;
return mask;
}
poll_wait 的原型定义如下:
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);
}
p->qproc 就是我们前面提到的 __pollwait 函数。
__pollwait 的原型如下:
static void __pollwait(struct file *filp,
wait_queue_head_t *wait_address,
poll_table *p)
{
struct poll_table_entry *entry = poll_get_entry(p); /* 获取entry */
if (!entry)
return;
get_file(filp);
entry->filp = filp;
entry->wait_address = wait_address;
/* 将当前进程挂载到等待队列上 */
init_waitqueue_entry(&entry->wait, current);
add_wait_queue(wait_address, &entry->wait);
}
-
获取entry。entry的定义如下:
struct poll_table_entry { struct file * filp; wait_queue_t wait; wait_queue_head_t * wait_address; };
-
设置当前进程,对 filp 这个文件“感兴趣”。后续,等这个文件有可操作事件发生时,会唤醒当前进程进行操作;
-
add_wait_queue() 将当前进程挂载到 wait_address 等待队列上;
完成了上面的操作后,当前进程就被挂载到所有感兴趣的文件描述符的等待队列上。
接着 do_poll 会调用 schedule_timeout 使进程进入休眠状态;
当文件描述符上有事件发生时,进程就会被唤醒,从而再次执行一次 do_poll 中的 for 循环,这时 s3c2440_buttons_poll 中就根据 ev_press 的值,设置 mask 为大于零,从而使得 do_pollfd 返回 count 非零。
2. 补充说明节拍
通过 msecs_to_jiffies()可以将毫秒时间转换为以节拍做单位。
poll的timespec将其转为节拍,内核就可以在当前节拍(base)基础上poll的节拍作为offset,算出一个poll超时时的节拍,当节拍次数达到这个base+offset时,就可以判断poll时间超时,就会触发应用程序poll的超时返回。
在Linux内核中使用全局变量 jiffies 记录自系统启动以来产生的节拍的总次数。内核启动后会将 jiffies 初始化为零,此后,每次时钟中断处理程序都会增加该变量的值。而一秒内时钟中断的次数会等于 HZ 数,所以 jiffies 一秒内增加的值就为 HZ。
如果系统运行时间以秒为单位计算,那么也就是等于 $ 系统运行的总时间 = jiffies / HZ $