目录
一、背景
内核版本:linux 4.19
poll和select的区别不大,区别在于可以监控的描述符数量,select默认为1024(由glibc进行限制,通过宏__FD_SETSIZE进行限制),而poll则没有这个限制。
此外还有ppoll这个系统调用,可以等待纳秒级别的信号,以及可以不被其他信号(非指定信号,比如crtl + c)中断。一个使用ppoll轮询的进程,如果把ppoll的第四个参数sigmask设置为SIGINT,那么ctrl + c是无法结束的,只能等到ppoll超时。
本篇文章主要是还是给自己当笔记用的,主要分析都作为注释写到代码中了。
二、驱动开发中poll的用法
用户空间应编写程序,伪代码如下:
#include "poll.h"
// 打开设备文件
int fd = open("/dev/xxx", O_RDWR);
// 创建 struct polld 结构体,并为其中的成员赋值
struct pollfd fds[1];
fds[0].fd = fd; // 指定文件描述
fds[0].events = POLLIN; // 有数据可以读的时候返回
ret = poll(fds, 1, 5000);
// 情况1,只传入了一个描述符,就无所谓了,返回了>0的值就说明ok了
// =0表示超时,< 0则失败
if (ret > 0) { // 如果poll有效,驱动给了返回值
ret = read(fd, &data, sizeof(data));
}
// 情况2,如果监听了多个描述符,要判断哪个设备发生了监听的事件
if (ret > 0) {
for(i=0; i< ARRAY_SIZE(fds); i++) { // 每个描述符都要判断
if(pollfds[i].revents & POLLIN) {
// 做对应的处理
}
}
}
驱动中应实现对应的poll:
unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
// g, 此函数最终会调用传入的pt->_qproc,也就是__pollwait(),把当前current加入到等待队列dev->r_wait中
// g, 随后由do_poll()进行scheduel切换进程,开启睡眠
// g, 直到在某处调用wake_up_interruptible(&dev->r_wait),唤醒该进程
// g, 唤醒进程的实际工作是在wait_event绑定的回调函数pollwake()中做的,涉及到default_wake_function()->..->try_to_wake_up()
poll_wait(filp, &dev->r_wait, wait); /* 将等待队列头添加到poll_table中 */
if(atomic_read(&dev->releasekey)) { /* 按键按下 */
mask = POLLIN | POLLRDNORM; /* 返回PLLIN */
}
return mask;
}
...
...
/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
.owner = THIS_MODULE,
.open = imx6uirq_open,
.read = imx6uirq_read,
.poll = imx6uirq_poll,
};
驱动的poll需要在某个设备事件(自己定义,可以是等待按键,也可以啥都不做直接返回也没问题,不过一般都是用来轮询数据有没有准备好)到来时,返回监听的事件,也就是POLLIN即可,就可以通知到用户层事件的发生,跳出poll的轮询。
三、poll()系统调用
关于系统调用(还在整理):Linux内核学习之 – ARMv8架构的系统调用
该系统调用的实现如下:
fs/select.c:
SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds,
int, timeout_msecs)
{
struct timespec64 end_time, *to = NULL;
int ret;
if (timeout_msecs >= 0) {
to = &end_time;
// g, 将参数timeout_msecs转换到结构struct timespec
poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC,
NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
}
ret = do_sys_poll(ufds, nfds, to);
// g, 当系统调用被其他信号中断时(此时并不是系统调用出错,而是被信号中断了)
if (ret == -EINTR) {
struct restart_block *restart_block;
restart_block = ¤t->restart_block;
restart_block->fn = do_restart_poll;
restart_block->poll.ufds = ufds;
restart_block->poll.nfds = nfds;
if (timeout_msecs >= 0) {
restart_block->poll.tv_sec = end_time.tv_sec;
restart_block->poll.tv_nsec = end_time.tv_nsec;
restart_block->poll.has_timeout = 1;
} else
restart_block->poll.has_timeout = 0;
// g, 返回了该错误码(ERESTART_RESTARTBLOCK),该错误码会使内核认为此次系统调用应该重启(不会返回到用户空间)
// g, 该restart_block会被存入current->restart_block中
// g, note 什么时候重启?我看好像是在系统调用退出执行do_notify_resume()->do_signal()时,若该信号设置了SA_RESTART,则会修改regs->pc,重新指向系统调用指令,也就是说退出后会重新执行一遍系统调用。
ret = -ERESTART_RESTARTBLOCK;
}
return ret;
}
该系统调用的执行过程可以分为三步:
- 转换用户传入的超时时间为struct timespec64
- 调用do_sys_poll()函数,这是处理poll的主函数
- 若在执行该系统调用时被其他信号打断,则设置重启操作。
第一步很好理解。第三步是系统调用重新执行相关的操作,涉及到系统调用退出时在entry.S中要执行的一个函数do_notify_resume(),该函数比较复杂,是否需要启动进程调度(need_reseched),是否重启系统调用,调试暂停(拦截内核的信号,暂停在这里并通知调试器)等都会在这里处理,暂时不进行分析,以后有空单独写一篇笔记。
真正起作用的是do_sys_poll()函数:
fs/select.c:
static int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
struct timespec64 *end_time)
{
struct poll_wqueues table;
int err = -EFAULT, fdcount, len, size;
/* Allocate small arguments on the stack to save memory and be
faster - use long to make sure the buffer is aligned properly
on 64 bit archs to avoid unaligned access */
long stack_pps[POLL_STACK_ALLOC/sizeof(long)]; // g, 256/sizeof(long) = 256 / 8 = 32,分配了一个long[32],可以认为是分配了一段内存(内核栈中分配的)
struct poll_list *const head = (struct poll_list *)stack_pps; // g, 一个struct poll_list的大小为:int + int, 是一个long
struct poll_list *walk = head;
unsigned long todo = nfds; // g, 用户空间调用poll函数的第二个参数,要监听的文件个数
if (nfds > rlimit(RLIMIT_NOFILE)) // g, 这里明显对poll()可以监控的文件数量做了一个限制,也就是进程可以open的最大文件数量,那为什么都说poll()没有数量限制呢?
return -EINVAL;
// g, 下面的拷贝过程可以概括为:
// 1. 创建一个struct poll_list链表
// 2. 链表中的每一个节点,都会保存一部分用户传入的struct poll_fd,每一个节点占用内存不得超过一个PAGE_SIZE(内核页)。若超过了PAGE_SIZE,则重新创建一个结点插入到链表中,并为其申请所需的内存
// 3. 链表的头结点除外,链表的头结点内存是在内核栈中分配的,只能分配long[32]大小。其余的结点内存分配都是在内核堆区分配的。
// 4. 经过拷贝之后,用户空间传入的所有信息(最重要的是struct poll_fd这个结构体的信息),都拷贝到了内核空间,并可以通过一个struct poll_list链表获取。
len = min_t(unsigned int, nfds, N_STACK_PPS); // g, 最后一个宏是判断刚才分配的那一段内存stack_pps[],最多能存放多少个struct poll_fd
for (;;) {
walk->next = NULL;
walk->len = len;
if (!len) // g, len为0了就跳出去
break;
// g, ufds是用户传入的第一个参数,也就是struct* pollfd
if (copy_from_user(walk->entries, ufds + nfds-todo, // g, 如果返回值不为0,也就是说有拷贝失败的字节数
sizeof(struct pollfd) * walk->len)) // g , 这个poll_list节点还可以放len个,就一次性拷贝len个
goto out_fds;
todo -= walk->len; // g, 还剩下多少
if (!todo) // g, 如果剩下的为0,也可以结束了
break;
len = min(todo, POLLFD_PER_PAGE); // g, 该宏定义了一个内核PAGE能存放多少个struct poll_fd。也就是说一个poll_list节点里面的所有内容,一定要放在一个内核页中,不要跨页
size = sizeof(struct poll_list) + sizeof(struct pollfd) * len;
walk = walk->next = kmalloc(size, GFP_KERNEL);
if (!walk) {
err = -ENOMEM;
goto out_fds;
}
}
// g, 会初始化一些内容:有一个比较重要:table->pt->qproc = __pollwait
poll_initwait(&table);
fdcount = do_poll(head, &table, end_time); // g, 调用do_poll 此处进入poll, 最终会调用驱动中的poll
poll_freewait(&table);
for (walk = head; walk; walk = walk->next) { // g, 结果拷贝给到用户内存
struct pollfd *fds = walk->entries;
int j;
for (j = 0; j < walk->len; j++, ufds++)
if (__put_user(fds[j].revents, &ufds->revents))
goto out_fds;
}
err = fdcount;
out_fds:
walk = head->next;
while (walk) {
struct poll_list *pos = walk;
walk = walk->next;
kfree(pos);
}
return err;
}
该函数也分为三步:
- 从用户空间拷贝信息(struct pollfd)到内核
- 初始化struct poll_wqueues,绑定一个函数指针到__pollwait,执行处理函数do_poll()
- 将内核信息(处理过后的struct pollfd)拷贝回用户空间
3.1 用户空间 -> 内核
主要是这一段代码:
fs/select.c:
static int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
struct timespec64 *end_time)
{
struct poll_wqueues table;
int err = -EFAULT, fdcount, len, size;
/* Allocate small arguments on the stack to save memory and be
faster - use long to make sure the buffer is aligned properly
on 64 bit archs to avoid unaligned access */
long stack_pps[POLL_STACK_ALLOC/sizeof(long)]; // g, 256/sizeof(long) = 256 / 8 = 32,分配了一个long[32],可以认为是分配了一段内存(内核栈中分配的)
struct poll_list *const head = (struct poll_list *)stack_pps; // g, 一个struct poll_list的大小为:int + int, 是一个long
struct poll_list *walk = head;
unsigned long todo = nfds; // g, 用户空间调用poll函数的第二个参数,要监听的文件个数
if (nfds > rlimit(RLIMIT_NOFILE)) // g, 这里明显对poll()可以监控的文件数量做了一个限制,也就是进程可以open的最大文件数量,那为什么都说poll()没有数量限制呢?
return -EINVAL;
// g, 下面的拷贝过程可以概括为:
// 1. 创建一个struct poll_list链表
// 2. 链表中的每一个节点,都会保存一部分用户传入的struct poll_fd,每一个节点占用内存不得超过一个PAGE_SIZE(内核页)。若超过了PAGE_SIZE,则重新创建一个结点插入到链表中,并为其申请所需的内存
// 3. 链表的头结点除外,链表的头结点内存是在内核栈中分配的,只能分配long[32]大小。其余的结点内存分配都是在内核堆区分配的。
// 4. 经过拷贝之后,用户空间传入的所有信息(最重要的是struct poll_fd这个结构体的信息),都拷贝到了内核空间,并可以通过一个struct poll_list链表获取。
len = min_t(unsigned int, nfds, N_STACK_PPS); // g, 最后一个宏是判断刚才分配的那一段内存stack_pps[],最多能存放多少个struct pollfd
for (;;) {
walk->next = NULL;
walk->len = len;
if (!len) // g, len为0了就跳出去
break;
// g, ufds是用户传入的第一个参数,也就是struct* pollfd
if (copy_from_user(walk->entries, ufds + nfds-todo, // g, 如果返回值不为0,也就是说有拷贝失败的字节数
sizeof(struct pollfd) * walk->len)) // g , 这个poll_list节点还可以放len个,就一次性拷贝len个
goto out_fds;
todo -= walk->len; // g, 还剩下多少
if (!todo) // g, 如果剩下的为0,也可以结束了
break;
len = min(todo, POLLFD_PER_PAGE); // g, 该宏定义了一个内核PAGE能存放多少个struct poll_fd。也就是说一个poll_list节点里面的所有内容,一定要放在一个内核页中,不要跨页
size = sizeof(struct poll_list) + sizeof(struct pollfd) * len;
walk = walk->next = kmalloc(size, GFP_KERNEL);
if (!walk) {
err = -ENOMEM;
goto out_fds;
}
}
...
...
这一段代码做了几件事情:
- 创建一个struct poll_list链表,首先创建一个头结点,以局部变量的方式创建,位于内核栈中
- 链表中的每一个节点,都会保存一部分用户传入的struct poll_fd,每一个节点占用内存不得超过一个PAGE_SIZE(内核页)。若超过了PAGE_SIZE,则重新创建一个struct poll_list结点插入到链表中,并为其申请所需的内存。
- 链表的头结点除外,链表的头结点内存是在内核栈中分配的,只能分配long[32]大小。其余的结点内存分配都是在内核堆区分配的。
- 经过拷贝之后,用户空间传入的所有信息(最重要的是struct poll_fd这个结构体的信息),都拷贝到了内核空间,并可以通过struct poll_list链表获取。每一个节点的entries域都会保存一个或多个struct pollfd
节点结构体定义如下:
fs/select.c:
struct poll_list {
struct poll_list *next; // g, 指向下一个节点
int len; // g, 表示当前节点保存了多少个struct pollfd
struct pollfd entries[0]; // g, 空数组,用多少填充多少
};
3.2 poll_initwait()与do_poll()
先来说一下poll_initwait()函数,该函数用来初始化一个struct poll_wqueues结构体:
fs/select.c:
void poll_initwait(struct poll_wqueues *pwq)
{
init_poll_funcptr(&pwq->pt, __pollwait); // g, 初始化pwq->pt->_qproc = __pollwait
pwq->polling_task = current; // g, 设置为current,后续会加入到等待队列中
pwq->triggered = 0;
pwq->error = 0;
pwq->table = NULL;
pwq->inline_index = 0;
}
--->
include/linux/poll.h:
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
pt->_qproc = qproc;
pt->_key = ~(__poll_t)0; /* all events enabled */
}
这里面涉及到一个关键函数:__pollwait(),后面需要通过该函数来把当前进程(pwq->polling_task = current;)加入到等待队列中,后面再说,现在先放着
接下来就会执行do_poll(head, &table, end_time),该函数实现如下:
fs/select.c
static int do_poll(struct poll_list *list, struct poll_wqueues *wait,
struct timespec64 *end_time)
{
poll_table* pt = &wait->pt; // g, wait在上一步已经进行了初始化
ktime_t expire, *to = NULL;
int timed_out = 0, count = 0;
u64 slack = 0;
__poll_t busy_flag = net_busy_loop_on() ? POLL_BUSY_LOOP : 0;
unsigned long busy_start = 0;
/* Optimise the no-wait case */
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
pt->_qproc = NULL;
timed_out = 1;
}
if (end_time && !timed_out)
slack = select_estimate_accuracy(end_time);
for (;;) {
struct poll_list *walk;
bool can_busy_loop = false;
// g, list也在外面进行了初始化,copy_from_user用户传入的struct pollfd,所有struct poll_ist组成了一个链表
// g, 接下来会遍历链表,遍历链表每一个节点,然后遍历每一个节点的pollfd数组
for (walk = list; walk != NULL; walk = walk->next) { // g, 遍历整个链表
struct pollfd * pfd, * pfd_end;
pfd = walk->entries;
pfd_end = pfd + walk->len;
for (; pfd != pfd_end; pfd++) { // g, 遍历链表每个poll_list节点中的所有struct pollfd
/*
* Fish for events. If we found one, record it
* and kill poll_table->_qproc, so we don't
* needlessly register any other waiters after
* this. They'll get immediately deregistered
* when we break out and return.
*/
// g, pt->_qproc在之前的一步已经被初始化为了__pollwait,内核的poll会调用到这个pt->_qproc(也就是__pollwait),把当前进程加入到某个等待队列中
// g, 如果有多个fd(也就是说有多个设备驱动),那么就应该是每一个设备驱动中都应创建一个自己的等待队列,然后把current加进去,然后无论哪个fd的条件满足了都会唤醒这个等待队列
// g, __pollwait中会为等待队列设置唤醒时回调func(),该函数设置为了pollwake(),最终会调用 __pollwake()。该函数会pwq->triggered = 0;
// g, 判断是否有期望的事件触发,也就是比较pfd->events与驱动返回的mask是否相等
// g, 如果有期望的事件发生,则会更新pfd->revents,也就是真实发生的事件
if (do_pollfd(pfd, pt, &can_busy_loop,
busy_flag)) {
count++;
pt->_qproc = NULL; // g, 如果有事件发生了,就不会再次加入到等待队列中了,这个函数指针不再是__pollwait了
/* found something, stop busy polling */
busy_flag = 0;
can_busy_loop = false; // g, 如果有期望事件触发了,停止loop,也就是停止轮询
}
}
}
/*
* All waiters have already been registered, so don't provide
* a poll_table->_qproc to them on the next loop iteration.
*/
pt->_qproc = NULL; // g, 整个poll的过程,对每个fd来说,只有一次加入等待队列然后scheduel的机会。之后要么等待被唤醒,要么超时
if (!count) { // g, count = 0,说明所有fd都没有期望的事件发生,则可以去响应其他信号
count = wait->error;
if (signal_pending(current)) // g, 仅检查当前进程是否有信号处理(不会在这里处理信号),返回不为0表示有信号需要处理,则需要退出当前的系统调用
count = -EINTR; // g, 表示系统调用被信号中断
}
// g, 如果count不是0,也就是说出现了事件(要么是驱动设备那里出现了事件,要么是有信号需要处理,不管哪种情况都需要退出系统调用),则不会再调用poll_schedule_timeout休眠了
// g, 也就是说,虽然我把在do_pollfd中已经把curret加入到了等待队列,但是我没有sechedule,所以不会休眠
if (count || timed_out)
break;
/* only if found POLL_BUSY_LOOP sockets && not out of time */
if (can_busy_loop && !need_resched()) { // g, 如果现在是busy并且没有设置过need_resched标志(内核进程需要调度时会显式的设置该标志)
if (!busy_start) {
busy_start = busy_loop_current_time();
continue;
}
if (!busy_loop_timeout(busy_start))
continue;
}
busy_flag = 0;
/*
* 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 = timespec64_to_ktime(*end_time);
to = &expire;
}
if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack)) // g, 休眠,等待被事件唤醒。
timed_out = 1; // g, 超时后设置该变量为1
}
return count;
}
该函数主要进行以下几个工作:
- 首先遍历所有struct pollfd
- 对每一个struct pollfd,都调用一次do_pollfd()函数,该函数会判断是否有事件发生,并且该函数会调用到设备驱动中实现的dev_poll()函数
- 如果遍历完所有pollfd,都没有检测到事件发生,会调用poll_schedule_timeout()函数阻塞,让出cpu
其中的关键就是do_pollfd()函数,其实现如下:
fs/select.c:
static inline __poll_t do_pollfd(struct pollfd *pollfd, poll_table *pwait,
bool *can_busy_poll,
__poll_t busy_flag)
{
int fd = pollfd->fd; // g, 这个pollfd->fd一定在用户空间open过了
__poll_t mask = 0, filter;
struct fd f;
if (fd < 0)
goto out;
mask = EPOLLNVAL;
f = fdget(fd); // g, 获取打开的fd对应的struct file(被强转成了struct fd, fd->file就是struct file)
if (!f.file)
goto out;
/* userland u16 ->events contains POLL... bitmap */
filter = demangle_poll(pollfd->events) | EPOLLERR | EPOLLHUP; // g, poll->events是用户空间传入的要求检测的事件,只关注这些事件
pwait->_key = filter | busy_flag;
// g, 最终会调用file->f_op->poll, open的时候file->f_op已经绑定了Inode->i_op了,也就是decice_create设备的时候绑定的操作集
// g, 最终由驱动层字符设备的ops返回mask
// g, 还有一点很重要,驱动中的poll,会调用poll_wait()->[pwait->_qproc()],也就是已经注册了的 __pollwait 函数
mask = vfs_poll(f.file, pwait);
if (mask & busy_flag)
*can_busy_poll = true;
mask &= filter; // g, 检测是否有指定的事件触发
fdput(f);
out:
/* ... and so does ->revents */
pollfd->revents = mangle_poll(mask); // g, 设置返回值,用户空间可以通过判断revents域判断哪个fd发生了事件
return mask;
}
该函数会调用虚拟文件系统的vfs_poll(),并最终调用到驱动中绑定的dev_poll()函数:
include/linux/poll.h:
static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt)
{
if (unlikely(!file->f_op->poll))
return DEFAULT_POLLMASK;
return file->f_op->poll(file, pt);
}
在这里,需要回顾一下在驱动中的dev_poll(),要做什么工作。回到文章开始,驱动中实现的dev_poll()函数如下:
...
...
struct imx6uirq_dev{
...
wait_queue_head_t r_wait; /* 读等待队列头 */
...
};
struct imx6uirq_dev imx6uirq; /* irq设备 */
...
...
void timer_function(unsigned long arg)
{
...
...
/* 唤醒进程 */
if(atomic_read(&dev->releasekey)) { /* 完成一次按键过程 */
/* wake_up(&dev->r_wait); */
wake_up_interruptible(&dev->r_wait); // 当某一个情况发生时,唤醒等待队列中的进程
}
...
...
}
...
...
unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
// g, 此函数最终会调用传入的pt->_qproc,也就是__pollwait(),把当前current加入到等待队列dev->r_wait中
// g, 随后由do_poll()进行scheduel切换进程,开启睡眠
// g, 直到在某处调用wake_up_interruptible(&dev->r_wait),唤醒该进程
// g, 唤醒进程的实际工作是在wait_event绑定的回调函数pollwake()中做的,涉及到default_wake_function()->..->try_to_wake_up()
poll_wait(filp, &dev->r_wait, wait); /* 将等待队列头添加到poll_table中 */
if(atomic_read(&dev->releasekey)) { /* 按键按下 */
mask = POLLIN | POLLRDNORM; /* 返回PLLIN */
}
return mask;
}
...
...
static int xx_init(void)
{
...
...
/* 初始化等待队列头 */
init_waitqueue_head(&imx6uirq.r_wait);
...
...
return 0;
}
module_init(xx_init);
驱动中做了什么工作呢?
- 创建并初始化一个等待队列头r_wait
- 调用poll_wait()函数,并传入等待队列头r_wait和do_pollfd()传入的poll_table
- 给do_pollfd()返回一个mask,所以说,事件发生与否,完全看驱动中的dev_poll()返回了什么mask
- 在合适的时机调用wake_up_interruptible(&dev->r_wait)唤醒该等待队列。
为什么驱动中要做这些工作?我猜这应该是poll系统调用约定俗成的。先不管poll_wait()函数做了什么,先看一下do_poll()在通过do_pollfd()调用了驱动程序中的dev_poll()之后,又做了什么工作:
fs/select.c
static int do_poll(struct poll_list *list, struct poll_wqueues *wait,
struct timespec64 *end_time)
{
...
..
for (;;) {
...
...
for (walk = list; walk != NULL; walk = walk->next) { // g, 遍历整个链表
struct pollfd * pfd, * pfd_end;
pfd = walk->entries;
pfd_end = pfd + walk->len;
for (; pfd != pfd_end; pfd++) { // g, 遍历链表每个poll_list节点中的所有struct pollfd
if (do_pollfd(pfd, pt, &can_busy_loop,
busy_flag)) {
count++;
pt->_qproc = NULL; // g, 如果有事件发生了,就不会再次加入到等待队列中了,这个函数指针不再是__pollwait了
/* found something, stop busy polling */
busy_flag = 0;
can_busy_loop = false; // g, 如果有期望事件触发了,停止loop,也就是停止轮询
}
}
}
...
...
if (end_time && !to) {
expire = timespec64_to_ktime(*end_time);
to = &expire;
}
if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack)) // g, 休眠,等待被事件唤醒。
timed_out = 1; // g, 超时后设置该变量为1
}
return count;
}
它调用poll_schedule_timeout()休眠了。既然休眠了,那肯定是每法自己唤醒自己的,只能由其他进程来唤醒自己了。那这一个环节具体是咋做的呢,关键在我们驱动函数dev_poll()调用的这个poll_wait()中。
3.3 __pollwait()与pollwake()
先看一下dev_poll中调用的poll_wait():
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && p->_qproc && wait_address)
p->_qproc(filp, wait_address, p);
}
可以看到,最终是调用了p->_qproc,而该指针已经指向了__pollwait():
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); // g, 又是一个空数组,每调用一次,返回pwq->inline_entries[index++]。如果inline_entries不够用了再kmalloc新的一页来存放struct poll_table_entry
if (!entry)
return;
entry->filp = get_file(filp);
entry->wait_address = wait_address;
entry->key = p->_key;
init_waitqueue_func_entry(&entry->wait, pollwake); // g, pollwake作为该等待队列唤醒时的回调函数。
entry->wait.private = pwq;
// g, 加入等待队列wait_address中,但是没有直接调用sechedule,所以还没有休眠。这个wait_address是驱动中定义的等待队列头
// g, 等驱动中某一个中断之类的来唤醒这个等待队列,唤醒时会执行绑定的函数,也就是pollwake(),最终会调用default_wake_function()->try_to_wake_up()
add_wait_queue(wait_address, &entry->wait);
}
这个函数,把pwq->inline_entries[index]->wait加入到我们驱动程序中注册的等待队列头。这样,我们就可以在其他地方来唤醒等待队列中的进程。基于此,我们看一下唤醒等待队列时执行的函数pollwake():
驱动程序中某事件就绪
->wake_up_interruptible(等待队列)
->__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
->__wake_up_common_lock()
->__wake_up_common()
->curr.func(curr, mode, wake_flags, key),
->curr.func就是等待队列项绑定的回调函数,对于__pollwait()中加入到该等待队列中的等待队列项来说,就是pollwake()
fs/select.c:
static int pollwake(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
struct poll_table_entry *entry;
entry = container_of(wait, struct poll_table_entry, wait);
if (key && !(key_to_poll(key) & entry->key))
return 0;
return __pollwake(wait, mode, sync, key);
}
...
...
static int __pollwake(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
struct poll_wqueues *pwq = wait->private;
DECLARE_WAITQUEUE(dummy_wait, pwq->polling_task);
/*
展开:
struct wait_queue_entry dummy_wait = {
.private = pwq->polling_task, // 早已被设置为了current
.func = default_wake_function,
.entry = { NULL, NULL } }
*/
smp_wmb();
pwq->triggered = 1;
return default_wake_function(&dummy_wait, mode, sync, key);
}
->default_wake_function()
->try_to_wake_up(pwq->polling_task, mode, wake_flags),try_to_wake_up会唤醒等待队列中进程,之前pwq->polling_task被设置为了current
当唤醒之后,do_poll()会从休眠处继续运行,也就是会从poll_schedule_timeout()处继续运行(实际上是从调用schedule()的返回处继续运行)。这里可以看一下这个休眠函数的实现过程:
static 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) // g, 如果triggered = 0,就要休眠。有个函数__pollwake会设置为1,该函数是wait队列唤醒时的回调函数中会调用
rc = schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS); // g, 使得当前进程休眠指定的时间范围,使用CLOCK_MONOTONIC计时系统。返回0表示超时,返回-EINTR表示到时之前被唤醒。用了高精度定时器,hrtime开头的都是高精度的。
// g,上一步就要休眠了,其中调用了schedule。被唤醒了的时候才会执行到下面这一步
__set_current_state(TASK_RUNNING);
/*
* Prepare for the next iteration.
*
* The following smp_store_mb() serves two purposes. First, it's
* the counterpart rmb of the wmb in pollwake() such that data
* written before wake up is always visible after wake up.
* Second, the full barrier guarantees that triggered clearing
* doesn't pass event check of the next iteration. Note that
* this problem doesn't exist for the first iteration as
* add_wait_queue() has full barrier semantics.
*/
smp_store_mb(pwq->triggered, 0);
return rc;
}
->schedule_hrtimeout_range_clock()
->schedule(),完成任务切换。
当被唤醒后,如果是因为超时被唤醒的,则进入到下一此的for循环仍然会对所有struct pollfd进行最后一次遍历,然后break;
如果不是因为超时被唤醒的,那就是我们的驱动程序主动唤醒的,说明驱动程序中准备好了该事件,那么在下次遍历中do_pollfd()再次调用到驱动中dev_poll()时应该是能拿到相应的mask的。要是拿不到,那就说明你这个驱动写的有问题,把人家唤醒了还不给人家return正确的mask。
四、关于try_to_wake_up()这个函数
这个函数涉及到linux的调度策略,这个函数会为要唤醒的进程选择一个合适的cpu,加入到它的ready队列中。如何选择合适的cpu就是一个问题。在看这个函数的时候发现了一种调度策略叫EAS调度,但是我看的4.19的内核没有这个调度策略,这个调度不是CFS的选进程策略,而是一个选核策略。5.0之后才添加了这个调度策略。
但是该函数除此之外,还有个地方值得研究一下,就是该函数中用了四个内存屏障,理解这四处内存屏障的用法对ARM cache一致性和内存屏障的作用的了解非常有帮助,后续有时间写一写一致性模型和EAS调度器相关的笔记。
try_to_wake_up()
->select_task_rq(struct task_struct *p, int cpu, int sd_flags, int wake_flags)
->p->sched_class->select_task_rq(),如果p是NORMAL类,那么就是它的sched_class就是CFS
->fair_sched_class,CFS的sched_class
->find_energy_efficient_cpu()
使用EAS的前提,是这个soc必须得是SMP架构。不过现在是个soc就是多核,只不过可能有的是异构。分析完几个系统调用之后就不再看linux 4.19了,后续再学习进程调度和内存管理的时候还是得看5.0的内核