一、场景引入
应用程序与驱动程序交互,较为重要的一种方式是POLL/超时方式。应用程序通过 poll/select 函数设置超时时间,超时或有数据时均返回。
类比生活中的场景是:妈妈打开房间门看小孩,发现小孩还在睡觉,于是陪小孩一起睡;但妈妈手上还有很多事,所以她说 “我可以陪你一会儿,但可以定个闹钟”。
二、poll 机制的应用层调用前提
在使用 poll 机制定 “闹钟” 时,打开文件时是否传入 O_NONBLOCK(非阻塞标志),没有任何影响。
具体操作是这样的:应用程序调用open函数后,需要先调用 poll 函数,再调用read函数。
三、poll 函数的参数与功能
调用 poll 函数时,需要传入一个数组,这个数组的作用是指定要监测的驱动:
- 将文件句柄存入数组,数组中每一项对应一个要监测的驱动;
除此之外,还要指定监测的事件类型 —— 比如 “输入事件(POLLIN)”,意思是 “当驱动有输入数据时,poll 函数就返回”。
我们先把应用程序的调用流程梳理清楚,后面再讲底层机制。这里要明确,poll 机制核心是 “定闹钟式监测”。
intmain(int argc, char **argv)
{
int fd;
int val;
structpollfdfds[1];
int timeout_ms = 5000;
int ret;
/* 1. 判断参数 */
if (argc != 2)
{
printf("Usage: %s <dev>\n", argv[0]);
return-1;
}
/* 2. 打开文件 */
fd = open(argv[1], O_RDWR);
if (fd == -1)
{
printf("can not open file %s\n", argv[1]);
return-1;
}
fds[0].fd = fd;
fds[0].events = POLLIN;
while (1)
{
/* 3. 读文件 */
ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN))
{
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
else
{
printf("timeout\n");
}
}
close(fd);
return0;
}
四、应用层与驱动层的 poll 函数对应关系
应用程序调用 poll 函数的整体流程,和之前的 open 操作套路一致,先看具体步骤:
- 应用程序先调用 open 函数打开驱动,这一步和之前没有差别,没什么特别需要说明的;
- 关键区别在于:打开驱动后,应用程序不直接调用 read 函数。因为如果直接读,要么是阻塞方式(没数据就一直休眠),要么是非阻塞方式(没数据就立刻返回错误),而我们需要的是 “定闹钟” 的效果,所以必须调用 poll 函数。
这里要注意一个对应关系:应用程序调用 poll 函数,最终会进入驱动程序的 poll 函数 —— 也就是说,驱动程序里必须实现一个 poll 函数,和应用层的 poll 函数对应。
我们来看驱动层的 poll 函数,它的实现非常简单:核心逻辑是调用 poll_wait 函数,然后判断当前是否有数据 —— 没数据就返回 0,有数据就返回 POLLIN(表示有输入事件),其他状态(如 POLLNORMAL)暂时不用关注。
staticunsignedintgpio_key_drv_poll(struct file *fp, poll_table * wait)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
/* 定义自己的file_operations结构体 */
staticstructfile_operationsgpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
};
五、内核 poll 的循环逻辑与休眠机制
要彻底理解 poll 机制,需要深入内核的处理逻辑:
1. 内核 poll 的核心循环
应用程序调用一次 poll 函数,会进入内核的 sys_poll 系统调用,而内核里有一个 for 循环,关键逻辑如下:
- 第一次循环:内核会调用驱动程序的 poll 函数,同时将当前进程(任务)挂到对应的等待队列上;
- 驱动 poll 函数返回当前状态:假设此时按键缓冲区为空,驱动 poll 返回 0;
- 注意:第一次返回 0 时,内核不会立刻把结果返回给用户空间,而是会让进程休眠一段时间 —— 这个 “一段时间” 就是我们设定的超时时间,也就是 “闹钟的时长”。下面是伪代码。
sys_poll()
{
for(;;)
{
ret = drv_poll();
if(ret || time_out_flag)
{
return;
}
else
{
sleep(time_out);
}
}
}
2. 休眠的唤醒条件
进程在休眠过程中,有两种唤醒方式,对应 “妈妈被唤醒” 的两种场景:
- 被数据源唤醒(小孩醒了):如果有按键按下,会触发中断,中断函数会把数据存入缓冲区,然后唤醒等待队列上的进程;
- 超时唤醒(闹钟响了):如果休眠期间一直没有数据,等到设定的超时时间到了,进程会被 “闹钟” 唤醒。
我们分别看两种唤醒后的处理:
(1)超时唤醒的处理
假设是超时唤醒,进程被唤醒后会再次进入 for 循环:
- 第二次调用驱动的 poll 函数,此时按键缓冲区仍然为空,驱动还是返回 0;
- 内核判断已经超时,就会从 sys_poll 返回错误给应用程序。
这里能看到一个关键比例:应用程序调用 1 次 poll,内核会调用 2 次驱动的 poll 函数(第一次休眠前,第二次超时后),是 1:2 的关系。
(2)数据源唤醒的处理
假设休眠期间有按键按下,触发中断后:
- 中断函数会把按键数据存入缓冲区,然后去等待队列唤醒进程(这里要注意:简单的驱动中断函数会直接唤醒等待队列,带消抖的驱动会通过定时器触发唤醒,但核心都是 “唤醒等待队列上的进程”);
- 进程被唤醒后,再次进入 for 循环,调用驱动的 poll 函数;
- 内核看到返回 POLLIN,就会立刻把结果返回给应用程序,不再休眠。
这种情况下,应用程序调用 1 次 poll,内核调用 2 次驱动 poll(第一次休眠前,第二次唤醒后),同样是 1:2 的关系;如果第一次调用驱动 poll 时就有数据,会直接返回 POLLIN,此时是 1:1 的关系。
六、poll 机制与 read 函数的配合使用
使用 poll 机制的核心目的是:在指定时间内监测驱动是否有数据,再决定是否调用 read 函数 —— 这就像 “妈妈定闹钟后,根据闹钟响的原因(小孩醒了 / 时间到了)决定下一步动作”。
具体配合逻辑如下:
- 应用程序先调用 poll 函数,根据返回结果判断状态:
- 如果 poll 返回 POLLIN(有数据),就调用 read 函数读取数据;
- 如果 poll 返回超时错误,就打印 “time out”;
- 当 poll 返回 POLLIN 时,调用 read 函数读取数据的过程,和之前讲的驱动 read 函数逻辑一致:
- 此时缓冲区肯定有数据,read 函数会直接把数据拷贝到用户空间,不会进入休眠(因为 poll 已经确认有数据了);
- 不会走 “没数据休眠” 的分支,直接执行数据拷贝并返回。
while (1)
{
/* 3. 读文件 */
ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN))
{
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
else
{
printf("timeout\n");
}
}
七、poll 机制的核心特点总结
poll 机制比之前的 “open+read” 稍复杂,复杂点在于内核的 for 循环逻辑:
- 应用程序调用 1 次 poll,内核可能调用 1~2 次驱动 poll 函数;
- 第一次调用驱动 poll:先查询是否有数据,没数据就休眠;
- 休眠唤醒后(中断或超时),第二次调用驱动 poll:再次查询数据状态,有数据就返回,超时就返回错误;
- 最终实现 “定闹钟式监测”,既避免了阻塞休眠的无限等待,也避免了非阻塞的频繁查询,兼顾效率和灵活性。