linux poll机制分析(二)
一、回顾
在linux poll机制使用(一)写了个实现poll机制的简单例子。在驱动模块中需要实现struct file_operations
的.poll
成员。在驱动模块中xxx_poll函数
的的作用是将当前进程添加到等待队列中;然后判断事件是否发生,发生则返回POLLIN | POLLRDNORM
,否则返回0(可以看看上一章的例子);接下来分析一下 linux 内核中 poll 机制的实现。
二、poll机制分析
1、系统调用
当应用层调用poll函数时,linux发生系统调用(
系统调用入口CALL(sys_poll)
),程序从应用空间切换到内核空间,然后执行sys_poll
函数(sys_poll函数在fs\select.c
文件中)。sys_poll
函数的作用是 根据时间计算滴答频率数,然后调用do_sys_poll
函数。代码块如下:
/* ufds:应用层传递过来的struct pollfd结构体数组
* nfds:应用层传递过来的struct pollfd结构体个数
* timeout_msecs:超时时间*/
asmlinkage long sys_poll(struct pollfd __user *ufds, unsigned int nfds,long timeout_msecs)
{
s64 timeout_jiffies;
if (timeout_msecs > 0) {
/* 大于0,根据时间计算滴答频率数 */
timeout_jiffies = msecs_to_jiffies(timeout_msecs);
} else {
/* Infinite (< 0) or no (0) timeout */
timeout_jiffies = timeout_msecs;
}
/* 然后调用do_sys_poll函数*/
return do_sys_poll(ufds, nfds, &timeout_jiffies);
}
2、do_sys_poll函数
do_sys_poll
函数在fs\select.c
文件中;do_sys_poll
函数的主要作用初始化table(table->pt->qproc = __pollwait)
,然后初始化poll列表
(poll列表的作用是存放用户空间的struct pollfd
)接着调用do_poll
函数。下面的代码块省略了部分源代码。代码块如下:
int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds, s64 *timeout)
{
struct poll_wqueues table;
struct poll_list *head;
struct poll_list *walk;
.....//省略了部分源代码
if (nfds > current->signal->rlim[RLIMIT_NOFILE].rlim_cur)
return -EINVAL;
/* 初始化 table */
/* table->pt->qproc = __pollwait
* __pollwait 主要将当前进程挂到等待队列中,这里只是做初始化而已。
* 在驱动程序里调用的 poll_wait(file, &button_waitq, wait); 函数里面最终
* 调用的就是table->pt->qproc()函数,也就是调用__pollwait()函数。
*/
poll_initwait(&table);
.....//省略部分源代码(省略的源代码其实就是分配空间和初始化head列表)
/* do_poll函数的参数说明:
*
* nfds: 应用层传递过来的struct pollfd结构体个数
* head: poll列表头,列表里面包含应用层传进来的struct pollfd数组的信息
* table: table->pt->qproc指向__pollwait函数(主要将当前进程挂到等待队列中)
* timeout: 等待超时
*/
/* 调用do_poll函数 */
fdcount = do_poll(nfds, head, &table, timeout);
.....//省略部分源代码(这部分的源代码是将revents(返回的事件) copy 到应用层)
return err;
}
3、do_poll函数
do_poll
函数在fs\select.c
文件中。do_poll
函数的作用主要是设置当前进程的任务状态,然后遍历poll列表、调用do_pollfd
函数,然后根据实际情况是否将当前进程休眠或者唤醒当前进程,代码块如下:
//参数说明
/*nfds: 应用层传递过来的struct pollfd结构体个数
*list:poll列表头,列表里面包含应用层传进来的struct pollfd数组的信息
*wait:wait->pt->qproc指向__pollwait函数(主要将当前进程挂到等待队列中)
*timeout:等待超时时间
*/
static int do_poll(unsigned int nfds, struct poll_list *list,
struct poll_wqueues *wait, s64 *timeout)
{
int count = 0;
poll_table* pt = &wait->pt;
/* Optimise the no-wait case */
if (!(*timeout))
pt = NULL;
/* 执行poll的时候有一个大循环 */
for (;;) {
struct poll_list *walk;
long __timeout;
/* 设置当前的任务状态为可中断休眠 */
set_current_state(TASK_INTERRUPTIBLE);
/* 遍历poll列表 */
for (walk = list; walk != NULL; walk = walk->next) {
struct pollfd * pfd, * pfd_end;
/* 获取头部的地址 */
pfd = walk->entries;
/* 获取尾部指针 */
pfd_end = pfd + walk->len;
/* 遍历struct pollfd结构体数组 */
for (; pfd != pfd_end; pfd++) {
/*------------------------------------------------------------------*/
/* do_pollfd其实就是调用开发者所写的xxx_poll函数了(里面调用的的是struct file_operations .poll函数)
* do_pollfd函数下面的代码块分析
*/
/* 执行完do_pollfd后,这个进程就被挂到等待队列里面了。
* 这里仅仅是挂到等待队列而已,进程的休眠是在下面
* 执行 schedule_timeout 函数的时候休眠的
*/
if (do_pollfd(pfd, pt)) {
/* 如果执行驱动程序的poll返回的是非0值 则count++ */
/* 表名设备有数据要返回给应用程序 */
count++;
pt = NULL;
}
/*------------------------------------------------------------------*/
}
}
pt = NULL;
/* 结束大循环的条件有三个:
* count: 表示设备有数据返回给应用程序
* timeout: 等待超时时间到
* signal_pending(current): 当前进程接收到信号
*/
if (count || !*timeout || signal_pending(current))
break;
/* 发生错误退出 */
count = wait->error;
if (count)
break;
if (*timeout < 0) {
/* Wait indefinitely */
__timeout = MAX_SCHEDULE_TIMEOUT;
} else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT-1)) {
/*
* Wait for longer than MAX_SCHEDULE_TIMEOUT. Do it in
* a loop
*/
__timeout = MAX_SCHEDULE_TIMEOUT - 1;
*timeout -= __timeout;
} else {
__timeout = *timeout;
*timeout = 0;
}
/* 执行到这里之后 timeout 的值已经为零,下一次循环就会超时返回 */
/* 这里让当前进程休眠一会
* 唤醒进程的处理休眠的时间到之外,还可以由驱动程序唤醒
*/
__timeout = schedule_timeout(__timeout);
if (*timeout >= 0)
*timeout += __timeout;
}
/* 将当前进程设置为就绪状态,等待CPU调度 */
__set_current_state(TASK_RUNNING);
return count;
}
4、do_pollfd
do_pollfd
函数在fs\select.c
文件中,函数的作用就是,根据文件描述符找到struct file
结构体,然后调用驱动程序的poll函数
//参数说明
/* pollfd: struct pollfd结构体
* pwait:pwait->qproc指向__pollwait函数(主要将当前进程挂到等待队列中)
*/
static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait)
{
unsigned int mask;
int fd;
mask = 0;
fd = pollfd->fd;
if (fd >= 0) {
int fput_needed;
struct file * file;
/* 根据文件描述符,获取file结构
* file结构是每打开一个文件就会有一个file
*/
file = fget_light(fd, &fput_needed);
mask = POLLNVAL;
if (file != NULL) {
mask = DEFAULT_POLLMASK;
if (file->f_op && file->f_op->poll)
/* 调用驱动里面的poll函数,这个函数是驱动开发者添加的poll函数 */
/* 如果驱动有数据让应用程序读的话,就返回非0, 否侧返回0 */
mask = file->f_op->poll(file, pwait);
/* Mask out unneeded events. */
/* 根据应用层传递的events,来屏蔽不需要的事件 */
mask &= pollfd->events | POLLERR | POLLHUP;
fput_light(file, fput_needed);
}
}
pollfd->revents = mask;
/* 设备有数据可读则返回非0 */
/* 否则返回0 */
return mask;
}
三、总结
- poll > sys_poll > do_sys_poll > poll_initwait,poll_initwait函数注册一下回调函数__pollwait,它就是我们的驱动程序执行poll_wait时,真正被调用的函数。
- 接下来执行file->f_op->poll,即我们驱动程序里自己实现的poll函数
它会调用poll_wait把自己挂入某个队列,这个队列也是我们的驱动自己定义的;
它还判断一下设备是否就绪- 如果设备未就绪,do_sys_poll里会让进程休眠一定时间
- 进程被唤醒的条件有两个 :1、“一定时间”到了 ;2、二是被驱动程序唤醒。驱动程序发现条件就绪时,就把“某个队列”上挂着的进程唤醒,这个队列,就是前面通过poll_wait把本进程挂过去的队列。
- 如果驱动程序没有去唤醒进程,那么chedule_timeout(__timeou)超时后,会重复2、3动作,直到应用程序的poll调用传入的时间到达。