ARM Linux 驱动开发篇:阻塞与非阻塞IO详解(含等待队列+poll机制)--- Ubuntu20.04

张开发
2026/5/22 3:12:24 15 分钟阅读
ARM Linux 驱动开发篇:阻塞与非阻塞IO详解(含等待队列+poll机制)--- Ubuntu20.04
渡水无言个人主页渡水无言❄专栏传送门《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》❄专栏传送门《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏》❄专栏传送门《产品测评专栏》⭐️流水不争先争的是滔滔不绝博主简介第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生在这里主要分享自己学习的linux嵌入式领域知识有分享错误或者不足的地方欢迎大佬指导也欢迎各位大佬互相三连​目录前言一、阻塞和非阻塞 IO 核心概念二、阻塞 IO 实现核心等待队列2.1 等待队列头wait_queue_head_t2.2、等待队列项wait_queue_t2.3、将队列项添加/移除等待队列头2.4、等待唤醒2.5、等待事件2.6、阻塞 IO 实现步骤驱动侧三、非阻塞 IO 的核心实现轮询机制3.1 三种轮询 API 对比3.2、select 函数3.3、poll 函数3.4、epoll 函数高并发场景四、Linux 驱动下的 poll 操作函数总结前言在 Linux 驱动开发中设备数据的到来往往是随机且不可预知的如果应用程序一直轮询查询设备状态会造成极高的 CPU 占用甚至影响系统运行。因此合理处理数据的等待与读取方式至关重要本期博客我们就来学习 Linux 下两种经典的设备访问模式 ——阻塞 IO 与非阻塞 IO并通过等待队列和 poll 机制实现高效、低功耗的驱动设计。一、阻塞和非阻塞 IO 核心概念博客中的IOInput/Output并非单片机 GPIO 引脚而是应用程序对驱动设备的输入 / 输出操作如读取按键值、写入数据到外设。在 Linux 驱动开发中阻塞和非阻塞 IO 是设备访问的两种核心模式直接决定驱动的 CPU 占用率与响应效率。当 应用程序对设备驱动进行操作的时候如果不能获取到设备资源那么阻塞式 IO就会将应用程序对应的线程挂起直到设备资源可以获取为止。下图就是阻塞式IO典型用途应用程序调用read函数从设备中读取数据当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒然后从设备中读取数据返回给应用程序。应用程序可以使用如下所示示例代码来实现阻塞访问int fd; int data 0; fd open(/dev/xxx_dev, O_RDWR); /* 阻塞方式打开 */ ret read(fd, data, sizeof(data)); /* 读取数据 */定义文件描述符 fd 和数据变量 data。调用 open 以读写模式O_RDWR打开设备文件 /dev/xxx_dev默认采用阻塞方式。调用 read 从设备中读取 sizeof(data) 字节的数据到 data 变量中并将返回值存入 ret。对于设备驱动文件的默认读取方式就是阻塞式的。对于非阻塞IO应用程序对应的线程不会挂起它要么一直轮询等待直到设备资源可以使用要么就直接放弃如下图所示应用程序使用非阻塞访问方式从设备读取数据当设备不可用或数据未准备好的时候会立即向内核返回一个错误码表示数据读取失败。应用程序会再次重新读取数据这样一直往复循环直到数据读取成功。如果应用程序要采用非阻塞的方式来访问驱动设备文件可以使用如下所示代码int fd; int data 0; fd open(/dev/xxx_dev, O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */ ret read(fd, data, sizeof(data)); /* 读取数据 */第4行使用open函数打开“/dev/xxx_dev”设备文件的时候添加了参数“O_NONBLOCK”表示以非阻塞方式打开设备这样从设备中读取数据的时候就是非阻塞方式的了。模式触发方式进程状态CPU 占用适用场景阻塞 IO资源就绪自动唤醒休眠态低单设备、低并发数据采集非阻塞 IO主动轮询 / 事件监听运行态高多设备、高并发、实时响应二、阻塞 IO 实现核心等待队列阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态这样可以将CPU 资源让出来。但是当设备文件可以操作的时候就必须唤醒进程一般在中断函数里面完成唤醒工作。Linux内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作。2.1 等待队列头wait_queue_head_t等待队列的 “容器”管理所有等待该事件的进程。定义于include/linux/wait.hstruct __wait_queue_head { spinlock_t lock; // 自旋锁保护队列操作的原子性 struct list_head task_list; // 等待队列项链表存储所有休眠进程 }; typedef struct __wait_queue_head wait_queue_head_t;定义好等待队列头以后需要初始化动态初始化void init_waitqueue_head(wait_queue_head_t *q) 静态初始化DECLARE_WAIT_QUEUE_HEAD(name)一次性定义并初始化2.2、等待队列项wait_queue_t等待队列头就是一个等待队列的头部每个访问设备的进程都是一个队列项当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。定义于include/linux/wait.hstruct __wait_queue { unsigned int flags; // 队列项标志 void *private; // 私有数据通常指向当前进程 wait_queue_func_t func; // 唤醒函数 struct list_head task_list; // 挂接到等待队列头的链表节点 }; typedef struct __wait_queue wait_queue_t;使用宏DECLARE_WAITQUEUE定义并初始化一个等待队列项宏的内容如下DECLARE_WAITQUEUE(name, tsk)name就是等待队列项的名字。tsk表示这个等待队列项属于哪个任务(进程)一般设置为 current表示当前进程。因 此 这个宏就是给当前正在运行的进程创建并初始化了一个等待队列项。2.3、将队列项添加/移除等待队列头当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中 只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可。等待队列项添加API函数如下add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)q等待队列项要加入的等待队列头。wait要加入的等待队列项。返回值无。等待队列项移除API函数如下remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)q要删除的等待队列项所处的等待队列头。wait要删除的等待队列项。返回值无。2.4、等待唤醒当设备可以使用的时候就要唤醒进入休眠态的进程唤醒可以使用如下两个函数void wake_up(wait_queue_head_t *q) void wake_up_interruptible(wait_queue_head_t *q)参数q就是要唤醒的等待队列头这两个函数会将这个等待队列头中的所有进程都唤醒。API 函数功能适用场景wake_up(wait_queue_head_t *q)唤醒队列中所有可中断 / 不可中断进程通用唤醒场景wake_up_interruptible(wait_queue_head_t *q)仅唤醒可被信号中断的进程驱动中断 / 事件触发唤醒推荐2.5、等待事件除了主动唤醒以外也可以设置等待队列等待某个事件当这个事件满足以后就自动唤醒等待队列中的进程和等待事件有关的 API函数如下表所示函数原型功能说明wait_event(wq, condition)等待以wq为等待队列头的等待队列被唤醒前提是condition条件必须满足为真否则一直阻塞。此函数会将进程设置为TASK_UNINTERRUPTIBLE状态wait_event_timeout(wq, condition, timeout)功能和wait_event类似但是此函数可以添加超时时间以jiffies为单位。此函数有返回值如果返回 0 表示超时时间到且condition为假返回 1 表示condition为真即条件满足wait_event_interruptible(wq, condition)与wait_event函数类似但是此函数将进程设置为TASK_INTERRUPTIBLE即可以被信号打断wait_event_interruptible_timeout(wq, condition, timeout)与wait_event_timeout函数类似此函数也将进程设置为TASK_INTERRUPTIBLE可以被信号打断2.6、阻塞 IO 实现步骤驱动侧以按键驱动为例阻塞 IO 实现需完成 4 步设备结构体中添加等待队列头用于管理按键数据就绪的休眠进程初始化等待队列头在驱动初始化函数中调用 init_waitqueue_head读函数中实现阻塞逻辑无数据时将进程加入等待队列并休眠有数据时直接读取事件触发时唤醒进程在按键中断 / 定时器处理函数中判断数据有效后调用 wake_up_interruptible 唤醒。三、非阻塞 IO 的核心实现轮询机制当应用程序以非阻塞方式O_NONBLOCK访问设备时无法通过阻塞休眠等待数据需通过轮询Polling主动查询设备状态。Linux 内核提供了三种经典轮询 APIselect、poll、epoll应用程序通过这些 API 监听设备是否可读 / 可写避免空轮询占用 CPU。3.1 三种轮询 API 对比API最大监听 FD 数效率适用场景select1024默认随 FD 数量增加效率下降少量 FD 监听、简单场景poll无限制同select但无 FD 数量限制中等数量 FD 监听epoll无限制高并发场景下效率极高大规模并发服务器、网络编程3.2、select 函数select是最基础的轮询 API通过文件描述符集合fd_set监听读、写、异常三类事件。函数原型int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);参数与返回值nfds监听的三类文件描述符集合中最大文件描述符 1readfds/writefds/exceptfds分别监听读、写、异常事件的文件描述符集合fd_set 类型timeout超时时间struct timeval 类型tv_sec 秒 tv_usec 微秒NULL 表示无限等待返回值0 表示就绪 FD 数量0 表示超时-1 表示错误。比如我们现在要从一个设备文件中读取数据那么就可以定义一个fd_set变量这个变量要传递给参数 readfds。当我们定义好一个fd_set变量以后可以使用如下所示几个宏进行操作void FD_ZERO(fd_set *set); // 清空集合 void FD_SET(int fd, fd_set *set); // 添加 FD 到集合 void FD_CLR(int fd, fd_set *set); // 从集合移除 FD int FD_ISSET(int fd, fd_set *set); // 判断 FD 是否就绪使用select函数对某个设备驱动文件进行读非阻塞访问的操作示例void main(void) { int ret, fd; fd_set readfds; struct timeval timeout; // 非阻塞方式打开设备 fd open(/dev/blockio, O_RDWR | O_NONBLOCK); FD_ZERO(readfds); // 清空读集合 FD_SET(fd, readfds); // 将设备 FD 加入读集合 // 设置超时时间500ms timeout.tv_sec 0; timeout.tv_usec 500000; // 轮询监听读事件 ret select(fd 1, readfds, NULL, NULL, timeout); switch (ret) { case 0: // 超时 printf(timeout!\r\n); break; case -1: // 错误 printf(error!\r\n); break; default: // 有数据可读 if (FD_ISSET(fd, readfds)) { int key; read(fd, key, sizeof(key)); // 读取按键值 printf(key: %d\r\n, key); } break; } }3.3、poll 函数poll是select的改进版无最大FD数量限制通过struct pollfd数组监听事件使用更灵活。函数原型int poll(struct pollfd *fds, nfds_t nfds, int timeout);函数参数和返回值含义如下fds要监视的文件描述符集合以及要监视的事件,为一个数组数组元素都是结构体pollfd类型的pollfd结构体如下所示struct pollfd { int fd; // 要监听的文件描述符 short events; // 要监听的事件如 POLLIN short revents; // 内核返回的就绪事件由内核填充 };可监视的事件类型如下所示POLLIN 有数据可以读取。 POLLPRI 有紧急的数据需要读取。 POLLOUT 可以写数据。 POLLERR 指定的文件描述符发生错误。 POLLHUP 指定的文件描述符挂起。 POLLNVAL 无效的请求。 POLLRDNORM 等同于 POLLIN使用poll函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示void main(void) { int ret, fd; struct pollfd fds; // 非阻塞方式打开设备 fd open(/dev/blockio, O_RDWR | O_NONBLOCK); // 构造 pollfd 结构体 fds.fd fd; fds.events POLLIN; // 监听读事件 // 轮询监听超时 500ms ret poll(fds, 1, 500); if (ret 0) { // 数据有效 if (fds.revents POLLIN) { int key; read(fd, key, sizeof(key)); printf(key: %d\r\n, key); } } else if (ret 0) { // 超时 printf(timeout!\r\n); } else if (ret 0) { // 错误 printf(error!\r\n); } }3.4、epoll 函数高并发场景epoll是为大规模并发设计的轮询 API通过事件通知机制避免遍历所有 FD效率远高于select/poll常用于网络编程。应用程序需要先使用 epoll_create函数创建一个epoll句柄epoll_create函数原型如下int epoll_create(int size)函数参数和返回值含义如下size从Linux2.6.8开始此参数已经没有意义了随便填写一个大于0的值就可以。返回值epoll句柄如果为-1的话表示创建失败。epoll句柄创建成功以后。使用epoll_ctl函数向其中添加要监视的文件描述符以及监视的事件epoll_ctl函数原型如下所示int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)函数参数和返回值含义如下epfd要操作的 epoll 句柄也就是使用 epoll_create 函数创建的 epoll 句柄。op表示要对 epfd(epoll 句柄)进行的操作可以设置为EPOLL_CTL_ADD 向 epfd 添加文件参数 fd 表示的描述符。 EPOLL_CTL_MOD 修改参数 fd 的 event 事件。 EPOLL_CTL_DEL 从 epfd 中删除 fd 描述符。fd要监视的文件描述符。event要监视的事件类型。为epoll_event结构体类型指针。可选的事件如下所示EPOLLIN 有数据可以读取。 EPOLLOUT 可以写数据。 EPOLLPRI 有紧急的数据需要读取。 EPOLLERR 指定的文件描述符发生错误。 EPOLLHUP 指定的文件描述符挂起。 EPOLLET 设置 epoll 为边沿触发默认触发模式为水平触发。 EPOLLONESHOT 一次性的监视当监视完成以后还需要再次监视某个 fd那么就需要将 fd 重新添加到 epoll 里面。注意上面这些事件可以进行“或”操作也就是说可以设置监视多个事件。返回值0成功-1失败并且设置errno的值为相应的错误码。一切都设置好以后应用程序就可以通过epoll_wait函数来等待事件的发生类似select函数。epoll_wait函数原型如下所示int epoll_wait( int epfd, struct epoll_event *events, int maxevents, int timeout)函数参数和返回值含义如下epfd要等待的epoll。events指向epoll_event结构体的数组当有事件发生的时候Linux内核会填写events调用者可以根据 events判断发生了哪些事件。maxeventsevents数组大小必须大于0。timeout超时时间单位为ms。返回值0超时-1错误其他值准备就绪的文件描述符数量。四、Linux驱动下的poll操作函数当应用程序调用select/poll函数来对驱动程序进行非阻塞访问的时候驱动程序file_operations操作集中的poll函数就会执行。所以驱动程序的编写者需要提供对应的poll函数。poll函数原型如下所示unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)参数与返回值filp要打开的设备文件waitpoll 表格需传递给 poll_wait 函数返回值设备就绪事件掩码如 POLLIN 表示可读。可返回的资源如下POLLIN 有数据可以读取。POLLPRI 有紧急的数据需要读取。POLLOUT 可以写数据。POLLERR 指定的文件描述符发生错误。POLLHUP 指定的文件描述符挂起。POLLNVAL 无效的请求。POLLRDNORM等同于POLLIN普通数据可读我们需要在驱动程序的poll函数中调用poll_wait函数poll_wait函数不会引起阻塞只是将应用程序添加到 poll_table中poll_wait函数原型如下void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);功能将应用程序进程加入等待队列不阻塞仅完成监听注册注意poll_wait 不会返回就绪事件需手动检查设备状态并返回事件掩码。总结本文详细讲解了 Linux 驱动开发中阻塞与非阻塞 IO 的核心概念、实现机制及实际应用。

更多文章