参考文档:
【原创】Linux select/poll机制原理分析 – LoyenWang – 博客园 (cnblogs.com)
发展历史
API 发布的时间线
下文中列出了网络 IO 中,各个 api 的发布时间线
1983,socket 发布在 Unix(4.2 BSD) 1983,select 发布在 Unix(4.2 BSD) 1994,Linux的1.0,已经支持socket和select 1997,poll 发布在 Linux 2.1.23 2002,epoll发布在 Linux 2.5.44
可以看到select、poll 和 epoll,这三个“IO多路复用API”是相继发布的。这说明了,它们是IO多路复用的3个进化版本。因为API设计缺陷,无法在不改变 API 的前提下优化内部逻辑。所以用poll替代select,再用epoll替代poll
epoll和poll还有select都是监听socket的接口,poll还有select出现的时间更早,但是性能更差。后来在此继承上发展改进得到了epoll
select原理分析
查看man手册可知select函数的作用
select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become “ready” for some class of I/O oper‐ation (e.g., input possible). A file descriptor is considered ready if it is possible to perform a corresponding I/O operation (e.g., read(2) without blocking, or a sufficiently small write(2)).
The timeout argument specifies the interval that select() should block waiting for a file descriptor to become ready. The call will block until either:
* a file descriptor becomes ready;
* the call is interrupted by a signal handler; or
* the timeout expires.
译文如下:
select()和pselect()允许程序监视多个文件描述符,直到一个或多个文件描述符“准备好”进行某种I/O操作(例如,可能的输入)。如果可以执行相应的I/O操作(例如,无阻塞的读操作(2)或足够小的写操作(2)),则认为文件描述符已准备就绪。
timeout参数指定select()应该阻止等待文件描述符准备就绪的时间间隔。调用将被阻塞,直到:
文件描述符准备就绪;
调用被信号处理程序中断;
超时过期
示例代码
监听标准输入,设置超时时间为5S
以下代码节选自man手册,select部分
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
fd_set rfds;
struct timeval tv;
int retval;
/* Watch stdin (fd 0) to see when it has input. */
FD_ZERO(&rfds);
FD_SET(0, &rfds);
/* Wait up to five seconds. */
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);
/* Don't rely on the value of tv now! */
if (retval == -1)
perror("select()");
else if (retval)
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds) will be true. */
else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
相关接口
fd_set结构定义如下
#define FD_SETSIZE 256
typedef struct { uint32_t fd32[FD_SETSIZE/32]; } fd_set;
FD_ZERO 、FD_SET、 FD_CLR、FD_ISSET是 POSIX 标准中定义的宏,用于操作文件描述符集合(fd_set)。
- FD_ZERO: 这个宏用于将文件描述符集合清零,即清除集合中的所有文件描述符。通常用于初始化文件描述符集合,使其为空集。其原型如下:void FD_ZERO(fd_set *set);其中,
set是一个指向fd_set结构体的指针,表示要清零的文件描述符集合。 - FD_SET: 这个宏用于将一个文件描述符添加到文件描述符集合中。其原型如下:void FD_SET(int fd, fd_set *set);其中,
fd是要添加的文件描述符,set是一个指向fd_set结构体的指针,表示要添加文件描述符的文件描述符集合。 - FD_CLR: 用于从文件描述符集合中清除(移除)一个文件描述符。void FD_CLR(int fd, fd_set *set);其中,
fd是要清除的文件描述符,set是一个指向fd_set结构体的指针,表示要清除文件描述符的文件描述符集合。 - FD_ISSET: 用于检查文件描述符集合中是否包含某个文件描述符。int FD_ISSET(int fd, const fd_set *set);其中,
fd是要检查的文件描述符,set是一个指向fd_set结构体的指针,表示要检查的文件描述符集合。如果文件描述符集合中包含指定的文件描述符,则返回非零值;否则返回零。
select函数调用栈

整体流程分析
select方法的主要工作可分为3部分:
- 将需要监控的用户空间的
inp(可读)、outp(可写)、exp(异常)事件拷贝到内核空间fds的in、out、ex; - 执行
do_select()方法,将in、out、ex监控到的事件结果写入到res_in、res_out、res_ex; - 将内核空间
fds的res_in、res_out、res_ex事件结果信息拷贝回用户空间inp、outp、exp。
select系统调用,最终的核心逻辑是在do_select函数中处理的,源码如下
kern_select函数
计算函数超时时间,然后调用core_sys_select执行真正的select
核心逻辑如下
笔者注:下文代码已格式化处理,并适当简化只保留核心逻辑
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
fd_set __user *, exp, struct timeval __user *, tvp)
{
return kern_select(n, inp, outp, exp, tvp);
}
static int kern_select(int n, fd_set __user *inp, fd_set __user *outp,
fd_set __user *exp, struct timeval __user *tvp)
{
struct timespec64 end_time, *to = NULL;
struct timeval tv;
int ret;
if (tvp) {
// 从用户空间读取设置的超时时间
if (copy_from_user(&tv, tvp, sizeof(tv)))
return -EFAULT;
to = &end_time;
// 设置函数的的超时时间
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);
return poll_select_finish(&end_time, tvp, PT_TIMEVAL, ret);
}
core_sys_select函数
将用户态传入的待监听的位图传入内核态,并使用do_select进行监听
核心逻辑如下
笔者注:下文代码已格式化处理,并适当简化只保留核心逻辑
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,
fd_set __user *exp, struct timespec64 *end_time)
{
fd_set_bits fds;
void *bits;
int max_fds;
size_t size, alloc_size;
struct fdtable *fdt;
long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
fdt = files_fdtable(current->files);
// select可监控不能大于进程可打开的文件描述上限
max_fds = fdt->max_fds;
if (n > max_fds)
n = max_fds;
size = FDS_BYTES(n);
bits = stack_fds;
// 6个bitmaps对应(int/out/ex)
alloc_size = 6 * size;
bits = kvmalloc(alloc_size, GFP_KERNEL);
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;
/*
* 拷贝传入的inp、outp、exp的值存入fds的in、out、ex
* 此处是将从用户空间传入的保存,后续传入内核空间
*/
get_fd_set(n, inp, fds.in);
get_fd_set(n, outp, fds.out);
get_fd_set(n, exp, fds.ex);
// 将fds的res_in、res_out、res_ex内容清零
zero_fd_set(n, fds.res_in);
zero_fd_set(n, fds.res_out);
zero_fd_set(n, fds.res_ex);
do_select(n, &fds, end_time);
/*
* 使用入参的指针变量将执行后的fds结果传出
* 将fds的res_in、res_out、res_ex结果拷贝到用户空间inp、outp、exp
* 此处是将从内核空间的结果拷贝至用户空间
*/
set_fd_set(n, inp, fds.res_in);
set_fd_set(n, outp, fds.res_out);
set_fd_set(n, exp, fds.res_ex);
if (bits != stack_fds)
kvfree(bits);
return -EFAULT;
}
核心思想
select接口使用fd_set_bits结构保存记录文件描述符,core_sys_select函数中创建对应类型的变量。记录从用户空间传入的待检测文件描述符,使用do_select执行实际的监听动作,然后将结果传回用户空间。
do_select函数
函数流程图

函数核心思想
函数主要做的事情,遍历传入的文件描述符检查哪些有变化
函数整体采用了三层循环的设计,在下文中对这三层循环每一次进行讨论
第一层循环
- 第一层循环,即最外层循环最外层循环做的事情主要是
- 申请和销毁资源,校验传入的参数
- 创建一个高精度,可挂起当前进程的循环
- 设置循环退出的出口条件
核心逻辑如下
笔者注:下文代码已格式化处理,并适当简化只保留核心逻辑
static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{
struct poll_wqueues table;
unsigned long busy_start = 0;
int retval;
poll_initwait(&table); // 初始化一个等待队列
wait = &table.pt;
slack = select_estimate_accuracy(end_time); // 获取高精度定时的误差范围,用于后续高精度定时器使用
for (;;) {
/* 此处省略一层循环中的部分代码逻辑 */
...
for (...) { // 第二层循环,此处省略循环体...
for (...) { // 第三次循环,此处省略循环体...
}
}
if (retval || timed_out || signal_pending(current))
break;
/*
* 低精度检测函数
* 进行忙等待检测,检查是否超时,若未超时则进行新一轮检测
*/
if (can_busy_loop) {
if (!busy_start) {
busy_start = busy_loop_current_time();
continue;
}
if (!busy_loop_timeout(busy_start))
continue;
}
/*
* 高精度定时器
* 将进程状态置为TASK_INTERRUPTIBLE,启动定时器,直到timeout后将进程状态重新置为TASK_RUNNING
* 此处的需要等待的时间是在select调用时传入的,由入参的struct timeval tv结构体中的tv.tv_sec(秒数)和tv.tv_usec(微妙)的相加
*/
if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, end_time, slack))
timed_out = 1;
}
poll_freewait(&table); // 释放初始化申请的队列
return retval;
}
核心思想
在函数内部的for(;;)死循环中,构造一个高精度的循环检测,其实现的最重要的地方在于利用busy_loop_timeout的低精度检测控制内部循环的流程,在内部流程结束后使用poll_schedule_timeout高精度定时器挂起当前进程
笔者注:这里为什么是两个检测函数,猜测此处使用两个检测函数的考量是,这种忙等待的场景,对于时间非常敏感每次检测超时不能太长,所以使用精度较低但是速度更快的函数控制内部循环。但是如果需要阻塞进程的话,对于时间的敏感度较高,使用开销速度较慢但是精度更高的函数进行进程的控制
最外层的一层循环,是一个死循环。循环的退出条件是
- 文件描述符准备就绪
- 调用被信号处理程序中断
- 循环超时过期
第二层循环
- 第二层循环,即中间层循环中间层循环做的事情主要是,记录第三层检测的结果
核心逻辑如下
笔者注:下文代码已格式化处理,并适当简化只保留核心逻辑
static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{
// 获取给定文件描述符集合中的最大文件描述符值
retval = max_select_fd(n, fds);
int n = retval;
for(;;)
{
/*
* 此处申请的`unsigned long inp, outp, exp`变量分别对应可读、可写、异常事件的文件描述符位图
* 这些数据的每一位都对应着一个文件描述符,如果某个文件描述符在集合中,则对应的位被设置为 `1`;否则被设置为 `0`
* 变量前是否以字母r开头表示是传出数据,否则为输入的传入数据
*/
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
bool can_busy_loop = false;
inp = fds->in;
outp = fds->out;
exp = fds->ex;
rinp = fds->res_in;
routp = fds->res_out;
rexp = fds->res_ex;
/* i超过最大文件描述符是退出循环的唯一条件 */
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
unsigned long in, out, ex, all_bits;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
__poll_t mask;
in = *inp++;
out = *outp++;
ex = *exp++;
// 检查输入、输出和异常描述符的集合是否全为零
all_bits = in | out | ex;
if (all_bits == 0) {
i += BITS_PER_LONG;
continue;
}
// for (...) {}// 此处省略循环体...
// 记录检测的结果
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
}
}
}
核心思想
获取在第三层循环中对传入的文件描述符位图的检测结果,将第三层循环的检测的结果以unsigned long为单位,将结果保存至*rinp、*routp、*rexp中。此处存放检测结果的指针指向的内存,实际指向do_select函数的入参(fd_set_bits *fds),所以不需要额外的参数进行传递
二层循环仅有唯一的一个退出条件,待扫描的文件描述符位图遍历完成
第三层循环设计
- 第三层循环,即最内侧循环最内侧循环主要是检查传入句柄是否有操作,并记录检测的结果
核心逻辑如下
笔者注:下文代码已格式化处理,并适当简化只保留核心逻辑
static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{
unsigned long bit = 1, j;
struct poll_wqueues table;
poll_table *wait;
__poll_t busy_flag = net_busy_loop_on() ? POLL_BUSY_LOOP : 0;
poll_initwait(&table); // 初始化一个等待队列
wait = &table.pt;
retval = max_select_fd(n, fds);
n = retval;
retval = 0;
for(;;)
{
for(...)
{
__poll_t mask;
all_bits = in | out | ex;
if (all_bits == 0) {
i += BITS_PER_LONG;
continue;
}
for (j = 0; j < BITS_PER_LONG && i >= n; ++j, ++i, bit <<= 1) {
struct fd f;
f = fdget(i);
if (f.file) {
/*
* fdget()读取文件描述符对应的文件对象的引用
* wait_key_set()设置等待键值,等待文件对象的状态变化
* vfs_poll()函数检查文件对象的状态变化,并获取文件对象的事件掩码
* fdput()释放申请的文件资源
*/
wait_key_set(wait, in, out, bit, busy_flag);
mask = vfs_poll(f.file, wait);
fdput(f);
/*
* 检查文件对象的事件掩码mask中是否设置了特定的事件标志
* 并且这些标志是否满足了select调用中指定的相应的读、写和异常条件
*/
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
retval++;
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
}
// 当检测到检测的文件描述符有变化时,关闭循环标志位
if (retval) {
can_busy_loop = false;
busy_flag = 0;
} else if (busy_flag & mask)
can_busy_loop = true;
}
}
}
}
}
核心思想
调用vfs_poll获取文件对象的事件掩码,并检查其中是否包含某些特定的事件标志
执行vfs_poll函数实际执行file->f_op->poll()函数,这个file->f_op->poll()其实就是file_operations->poll()即文件系统的poll方法
file_operations->poll()是在驱动中进行实现,这个函数指针通常用于执行文件对象的轮询操作,以确定文件对象的状态是否满足特定的条件
select接口小结
select接口底层使用fd_set_bits结构存储文件描述符位图,这个结构实际是unsigned long,将多个文件描述符存储在一起,每一位代表一个文件描述符。在用户配置好需要监听的文件描述符后,由用户空间传入内核空间中,调用内核文件系统提供的poll接口检测是否有变化。在获取到结果后将结果存入fd_set_bits结构中再从内核空间传回用户空间。
每一次的检测都需要对传入的位图进行遍历,还需要从用户空间和内核空间之间相互传递数据,同时监听的最大文件描述符受到进程可以打开的最大文件描述符数量限制。
poll原理分析
poll接口实现和功能和实现的思路与select几乎一致。
区别在于poll接口的编码设计可读性更强,同时底层存储待监听的句柄使用的数据结构不同可监听的句柄数量不同,select接口使用位图存储,poll使用链表存储。
示例代码
监听标准输入,设置超时时间为5秒
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
int main() {
struct pollfd fds[1];
int ret;
fds[0].fd = STDIN_FILENO; // 标准输入的文件描述符
fds[0].events = POLLIN; // 监听可读事件
ret = poll(fds, 1, 5000); //使用poll函数监听标准输入,设置超时时间为5000 毫秒(5秒)
if (ret == -1) {
perror("poll");
return 1;
} else if (ret == 0) {
printf("5秒内没有事件发生\n");
} else {
if (fds[0].revents & POLLIN) {
printf("标准输入已就绪\n");
}
}
return 0;
}
poll函数调用栈

do_sys_poll函数
核心逻辑如下
笔者注:下文代码已格式化处理,并适当简化只保留核心逻辑
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;
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);
return ret;
}
static int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds, struct timespec64 *end_time)
{
struct poll_wqueues table;
int len;
long stack_pps[POLL_STACK_ALLOC/sizeof(long)];
struct poll_list *const head = (struct poll_list *)stack_pps;
struct poll_list *walk = head;
unsigned long todo = nfds;
/*
* 创建struct poll_list类型的链表,链表中存放待检测的文件描述符
* 将传入的待检测的文件描述符从用户空间拷贝至内核空间,然后存入链表中
*/
len = min_t(unsigned int, nfds, N_STACK_PPS);
for (;;) {
walk->next = NULL;
walk->len = len;
if (!len)
break;
copy_from_user(walk->entries, ufds + nfds-todo, sizeof(struct pollfd) * walk->len);
todo -= walk->len;
if (!todo)
break;
len = min(todo, POLLFD_PER_PAGE);
walk = walk->next = kmalloc(struct_size(walk, entries, len), GFP_KERNEL);
}
/*
* 初始化poll等待队列
* 执行do_poll检测待检测的文件描述符
* 释放poll等待队列
*/
poll_initwait(&table);
do_poll(head, &table, end_time);
poll_freewait(&table);
/*
* 检测结果存放在单链表中,头节点为head
* 将检测结果从内核空间拷贝至用户空间
*/
for (walk = head; walk; walk = walk->next) {
struct pollfd *fds = walk->entries;
for (int j = 0; j < walk->len; j++, ufds++)
// __put_user函数,将数据从内核空间拷贝到用户空间
if (__put_user(fds[j].revents, &ufds->revents))
goto out_fds;
}
// 销毁链表
walk = head->next;
while (walk) {
struct poll_list *pos = walk;
walk = walk->next;
kfree(pos);
}
return -EFAULT;
}
核心思想
- 首先创建
struct poll_list类型的单链表,用于存储待检测的文件描述符 - 将待检测的文件描述符从用户空间拷贝至内核空间
- 执行
do_poll进行检测 - 将检测结果从内核空间拷贝至用户空间
poll接口的整体设计思路与select可以说几乎一致,都是先创建对应的数据结构然后从用户空间拷贝待检测的文件描述符,检测完成后再拷贝至内核空间。二者的区别只是在于使用了不同的数据结构,所以也使用不同的处理方式。
do_poll函数
函数流程图

do_poll的设计为三层循环嵌套的结构。外侧循环构建了一个高精度定时循环,内层循环则用于检测和填充文件描述符链表
第一层循环结构
外层循环构建了一个高精度定时循环,其设计与select几乎一致,详细参见前文中do_select函数一节
核心逻辑如下
笔者注:下文代码已格式化处理,并适当简化只保留核心逻辑
static int do_poll(struct poll_list *list, struct poll_wqueues *wait, struct timespec64 *end_time)
{
int timed_out = 0, count = 0;
u64 slack = 0;
unsigned long busy_start = 0;
// 计算高精度定时器的误差值
slack = select_estimate_accuracy(end_time);
for (;;) {
struct poll_list *walk;
bool can_busy_loop = false;
for (...) { // 第二层循环,此处省略循环体
for (...) { // 第三层循环,此处省略循环体
}
}
if (count || timed_out)
break;
// 检查忙本轮等待是否超时
if (can_busy_loop) {
if (!busy_start) {
busy_start = busy_loop_current_time();
continue;
}
if (!busy_loop_timeout(busy_start))
continue;
}
// 将进程置为TASK_INTERRUPTIBLE阻塞,直到timeout后将进程状态重新置为TASK_RUNNING
if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, end_time, slack))
timed_out = 1;
}
return count;
}
核心思想
构造一个高精度循环检测,在首次进入后会将进程陷入阻塞态,直到等待被唤醒进程
循环出口:内层循环设置循环结束的标志位
第二第三层循环结构
内层的第二层第三层循环做的事情主要是,遍历struct poll_list结构的链表,检查对应的文件描述符是否有变化,并将结果填充至链表中
核心逻辑如下
笔者注:下文代码已格式化处理,并适当简化只保留核心逻辑
static int do_poll(struct poll_list *list, struct poll_wqueues *wait, struct timespec64 *end_time)
{
...
for (;;) {
struct poll_list *walk;
bool can_busy_loop = false;
for (walk = list; walk != NULL; walk = walk->next) {
struct pollfd * pfd, * pfd_end;
pfd = walk->entries;
pfd_end = pfd + walk->len;
for (; pfd != pfd_end; pfd++) {
if (do_pollfd(pfd, pt, &can_busy_loop, busy_flag)) {
count++;
pt->_qproc = NULL;
//设置循环结束的标志位
busy_flag = 0;
can_busy_loop = false;
}
}
}
}
...
}
核心思想
遍历链表,检测链表中的每一个元素的文件描述符
循环出口:链表遍历结束
do_pollfd函数
核心逻辑如下
笔者注:下文代码已格式化处理,并适当简化只保留核心逻辑
static inline __poll_t do_pollfd(struct pollfd *pollfd, poll_table *pwait,
bool *can_busy_poll)
{
int fd = pollfd->fd;
__poll_t mask = 0, filter;
struct fd f;
mask = EPOLLNVAL;
f = fdget(fd);
filter = demangle_poll(pollfd->events) | EPOLLERR | EPOLLHUP);
pwait->_key = filter;
mask = vfs_poll(f.file, pwait);//检查文件对象的状态变化,并获取文件对象的事件掩码
if (mask)
*can_busy_poll = true;
mask &= filter; // 只保留返回结果中与filter对应的部分
fdput(f);
pollfd->revents = mangle_poll(mask);
return mask;
}
核心思想
do_pollfd函数的核心逻辑是调用vfs_poll()对待检查文件描述符是否发生变化,执行vfs_poll函数实际执行file->f_op->poll()函数,这个file->f_op->poll()其实就是file_operations->poll()即文件系统的poll方法
file_operations->poll()是在驱动中进行实现,这个函数指针通常用于执行文件对象的轮询操作,以确定文件对象的状态是否满足特定的条件
poll接口小结
poll和select的原理几乎一致,都是将需要监听的文件描述符传入内核,然后调用file_operations->poll接口去检测,底层都是依赖内核的文件系统实现。二者不同的地方在于select是构造了一个位图然后,然后在位图中填充文件描述符,poll则是使用单链表。但两者的效率并没有太大的区别
由于每次的检测都会涉及到数据在用户空间到内核空间之间的来回拷贝,而且每次检测遍历所有的文件描述符,所以其效率并不高
驱动层面对文件系统的监听
file_operations->poll接口
函数声明
在linux-5.4\include\linux\fs.h中可以看到struct file_operations的定义
#define __bitwise __attribute__((bitwise))
typedef unsigned __bitwise __poll_t;
struct file_operations {
...
/* read意为读、write意为写、poll意为检测,探询 */
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
...
} __randomize_layout;
函数实现
struct file_operations中的__poll_t是在驱动代码中实现,不同驱动代码实现方式不同。但都会调用poll_wait()函数
在此处列出例子
在linux-5.4\arch\powerpc\platforms\powernv\opal-prd.c中可以找到OPAL的驱动对于poll的实现
static const struct file_operations opal_prd_fops = {
...
.poll = opal_prd_poll,
...
};
static __poll_t opal_prd_poll(struct file *file,
struct poll_table_struct *wait)
{
poll_wait(file, &opal_prd_msg_wait, wait);
if (!opal_msg_queue_empty())
return EPOLLIN | EPOLLRDNORM;
return 0;
}
在linux-5.4\arch\powerpc\kernel\rtasd.c中可以找到RTASD的驱动对于poll的实现
static const struct file_operations proc_rtas_log_operations = {
...
.poll = rtas_log_poll,
...
};
static __poll_t rtas_log_poll(struct file *file, poll_table * wait)
{
poll_wait(file, &rtas_log_wait, wait);
if (rtas_log_size)
return EPOLLIN | EPOLLRDNORM;
return 0;
}
可以看到不同的驱动代码中都调用了poll_wait(),把当前进程加入到驱动里自定义的等待队列上,当驱动事件就绪后,就可以在驱动里自定义的等待队列上唤醒调用poll的进程。
内核等待队列
等待队列基本流程如下
在select和poll模块中自己实现了pollwake函数作为等待队列wakeup回调
将__pollwait注册为等待队列的wait回调
void poll_initwait(struct poll_wqueues *pwq)
{
init_poll_funcptr(&pwq->pt, __pollwait);
pwq->polling_task = current;
pwq->triggered = 0;
pwq->error = 0;
pwq->table = NULL;
pwq->inline_index = 0;
}

- 内核等待队列是一个公共的基础模块
- 上图仅针对于
select和poll模块,其他模块对于等待队列的注册接口和回调函数可能有不同的封装和实现 wak_up_interruptible是一个对_wake_up封装的宏,内核中还存在其他的对_wake_up的封装宏,其他模块也会调用。例如socket就将wake_up_interruptible_all自行封装了一个sock_def_wakeup接口用于调用