当前位置:首页>Linux>嵌入式 Linux poll 机制讲解

嵌入式 Linux poll 机制讲解

  • 2026-10-21 13:27:52
嵌入式 Linux poll 机制讲解

一、场景引入

应用程序与驱动程序交互,较为重要的一种方式是POLL/超时方式。应用程序通过 poll/select 函数设置超时时间,超时或有数据时均返回。

类比生活中的场景是:妈妈打开房间门看小孩,发现小孩还在睡觉,于是陪小孩一起睡;但妈妈手上还有很多事,所以她说 “我可以陪你一会儿,但可以定个闹钟”。

二、poll 机制的应用层调用前提

在使用 poll 机制定 “闹钟” 时,打开文件时是否传入 O_NONBLOCK(非阻塞标志),没有任何影响。

具体操作是这样的:应用程序调用open函数后,需要先调用 poll 函数,再调用read函数。

三、poll 函数的参数与功能

调用 poll 函数时,需要传入一个数组,这个数组的作用是指定要监测的驱动:

  1. 首先要打开目标驱动,获取文件句柄;
  2. 将文件句柄存入数组,数组中每一项对应一个要监测的驱动;
  3. 同时需要指定数组的长度(即要监测的驱动数量)。

除此之外,还要指定监测的事件类型 —— 比如 “输入事件(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 操作套路一致,先看具体步骤:

  1. 应用程序先调用 open 函数打开驱动,这一步和之前没有差别,没什么特别需要说明的;
  2. 关键区别在于:打开驱动后,应用程序不直接调用 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. 被数据源唤醒(小孩醒了)
    :如果有按键按下,会触发中断,中断函数会把数据存入缓冲区,然后唤醒等待队列上的进程;
  2. 超时唤醒(闹钟响了)
    :如果休眠期间一直没有数据,等到设定的超时时间到了,进程会被 “闹钟” 唤醒。

我们分别看两种唤醒后的处理:

(1)超时唤醒的处理

假设是超时唤醒,进程被唤醒后会再次进入 for 循环:

  • 第二次调用驱动的 poll 函数,此时按键缓冲区仍然为空,驱动还是返回 0;
  • 内核判断已经超时,就会从 sys_poll 返回错误给应用程序。

这里能看到一个关键比例:应用程序调用 1 次 poll,内核会调用 2 次驱动的 poll 函数(第一次休眠前,第二次超时后),是 1:2 的关系。

(2)数据源唤醒的处理

假设休眠期间有按键按下,触发中断后:

  • 中断函数会把按键数据存入缓冲区,然后去等待队列唤醒进程(这里要注意:简单的驱动中断函数会直接唤醒等待队列,带消抖的驱动会通过定时器触发唤醒,但核心都是 “唤醒等待队列上的进程”);
  • 进程被唤醒后,再次进入 for 循环,调用驱动的 poll 函数;
  • 此时驱动检测到缓冲区有数据,返回 POLLIN;
  • 内核看到返回 POLLIN,就会立刻把结果返回给应用程序,不再休眠。

这种情况下,应用程序调用 1 次 poll,内核调用 2 次驱动 poll(第一次休眠前,第二次唤醒后),同样是 1:2 的关系;如果第一次调用驱动 poll 时就有数据,会直接返回 POLLIN,此时是 1:1 的关系。

六、poll 机制与 read 函数的配合使用

使用 poll 机制的核心目的是:在指定时间内监测驱动是否有数据,再决定是否调用 read 函数 —— 这就像 “妈妈定闹钟后,根据闹钟响的原因(小孩醒了 / 时间到了)决定下一步动作”。

具体配合逻辑如下:

  1. 应用程序先调用 poll 函数,根据返回结果判断状态:
  • 如果 poll 返回 POLLIN(有数据),就调用 read 函数读取数据;
  • 如果 poll 返回超时错误,就打印 “time out”;
  1. 当 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. 应用程序调用 1 次 poll,内核可能调用 1~2 次驱动 poll 函数;
  2. 第一次调用驱动 poll:先查询是否有数据,没数据就休眠;
  3. 休眠唤醒后(中断或超时),第二次调用驱动 poll:再次查询数据状态,有数据就返回,超时就返回错误;
  4. 最终实现 “定闹钟式监测”,既避免了阻塞休眠的无限等待,也避免了非阻塞的频繁查询,兼顾效率和灵活性。

最新文章

随机文章