Linux 阻塞和非阻塞 I/O 简明指南
目录
声明
1. 阻塞和非阻塞简介
2. 等待队列
2.1 等待队列头
2.2 等待队列项
2.3 将队列项添加/移除等待队列头
2.4 等待唤醒
2.5 等待事件
3. 轮询
3.1 select函数
3.2 poll函数
3.3 epoll函数
4. Linux 驱动下的 poll 操作函数
声明
本博客所记录的关于正点原子i.MX6ULL开发板的学习笔记,(内容参照正点原子I.MX6U嵌入式linux驱动开发指南,可在正点原子官方获取正点原子Linux开发板 — 正点原子资料下载中心 1.0.0 文档),旨在如实记录我在学校学习该开发板过程中所遭遇的各类问题以及详细的解决办法。其初衷纯粹是为了个人知识梳理、学习总结以及日后回顾查阅方便,同时也期望能为同样在学习这款开发板的同学或爱好者提供一些解决问题的思路和参考。我尽力保证内容的准确性和可靠性,但由于个人知识水平和实践经验有限,若存在错误或不严谨之处,恳请各位读者批评指正。
责任声明:虽然我力求提供有效的问题解决办法,但由于开发板使用环境、硬件差异、软件版本等多种因素的影响,我的笔记内容不一定适用于所有情况。对于因参考本笔记而导致的任何直接或间接损失,我不承担任何法律责任。使用本笔记内容的读者应自行承担相关风险,并在必要时寻求专业技术支持。
1. 阻塞和非阻塞简介
当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO (IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作)就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。阻塞式 IO 如图所示:
应用程序调用 read 函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。
非阻塞 IO 如图所示:
应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。
2. 等待队列
2.1 等待队列头
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。 Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t 表示, wait_queue_head_t 结构体定义在文件 include/linux/wait.h 中,结构体内容如下所示:
// 定义一个等待队列头结构体
// 等待队列头用于管理等待队列,等待队列常用于实现进程的睡眠和唤醒操作
struct __wait_queue_head {// 自旋锁,用于保护等待队列相关的操作// 自旋锁是一种用于多处理器环境下的锁机制,当一个线程获取自旋锁时,如果锁已被其他线程持有,该线程会不断循环尝试获取锁,而不是进入睡眠状态spinlock_t lock; // 任务列表,用于存储等待在该等待队列上的任务(进程)// 这里使用链表来管理等待的任务,每个等待的任务以节点的形式存储在该链表中struct list_head task_list;
};// 为结构体 __wait_queue_head 定义一个别名 wait_queue_head_t
// 这样在后续使用时可以更方便地声明该结构体类型的变量
typedef struct __wait_queue_head wait_queue_head_t;
定义好等待队列头以后 使用 init_waitqueue_head 函数初始化等待队列头,函数原型如下:
void init_waitqueue_head(wait_queue_head_t *q)
参数 q 就是要初始化的等待队列头。
也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化
2.2 等待队列项
等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait_queue_t 表示等待队列项,结构体内容如下:
// 等待队列结构体
struct __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 , 在 Linux 内 核 中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。 因 此 宏DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。
2.3 将队列项添加/移除等待队列头
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,
等待队列项添加 API 函数如下:
void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
函数参数和返回值含义如下:
q: 等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
返回值:无。
等待队列项移除 API 函数如下:
void 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 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。
2.5 等待事件
当这个事件满足以后就自动唤醒等待队列中的进程,和等待事件有关的 API 函数如表 52.1.2.1 所示:
3. 轮询
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。 poll、 epoll 和 select 可以用于处理轮询,应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行。
3.1 select函数
select 函数原型:
// nfds 表示被检查的描述符集合中最大描述符加1
// 即它用于指定需要检查的文件描述符的范围,范围是从0到nfds - 1的文件描述符都会被检查
int select(int nfds, // readfds 是一个指向fd_set类型的指针,fd_set 是一个用于存储文件描述符集合的类型// readfds 用于指定需要检查可读性的文件描述符集合,集合中的文件描述符会被检查是否有数据可读fd_set *readfds, // writefds 是一个指向fd_set类型的指针// writefds 用于指定需要检查可写性的文件描述符集合,集合中的文件描述符会被检查是否可写fd_set *writefds, // exceptfds 是一个指向fd_set类型的指针// exceptfds 用于指定需要检查异常情况的文件描述符集合,集合中的文件描述符会被检查是否有异常发生fd_set *exceptfds, // timeout 是一个指向struct timeval类型的指针// struct timeval 是一个结构体,包含秒和微秒两个成员,用于表示时间// timeout 用于设置select函数的超时时间,如果设置为NULL,则select函数会一直阻塞直到有文件描述符满足条件或发生错误;如果设置了具体的时间值,select函数会在超时时间到达时返回struct timeval *timeout)
{// 函数体部分省略,实际的select函数实现会根据传入的参数检查文件描述符集合// 并返回满足条件的文件描述符数量,或者在超时或发生错误时返回相应的值
}
select 函数的返回值有以下几种情况:
大于 0:表示有文件描述符满足了可读、可写或异常的条件,返回值是满足条件的文件描述符的数量。
等于 0:表示在指定的超时时间内没有文件描述符满足条件(当 timeout 不为 NULL 时)。
小于 0:表示发生了错误,具体的错误类型可以通过 errno 变量获取
nfds: 所要监视的这三类文件描述集合中, 最大文件描述符加 1。
readfds、 writefds 和 exceptfds:这三个指针指向描述符集合,这三个参数指明了关心哪些描述符、需要满足哪些条件等等,这三个参数都是 fd_set 类型的, fd_set 类型变量的每一个位都代表了一个文件描述符。 readfds 用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么 seclect 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时。可以将 readfs设置为 NULL,表示不关心任何文件的读变化。 writefds 和 readfs 类似,只是 writefs 用于监视这些文件是否可以进行写操作。 exceptfds 用于监视这些文件的异常。
当我们定义好一个 fd_set 变量以后可以使用如下所示几个宏进行操作:
void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)
FD_ZERO 用于将 fd_set 变量的所有位都清零。
FD_SET 用于将 fd_set 变量的某个位置 1,也就是向 fd_set 添加一个文件描述符,参数 fd 就是要加入的文件描述符。
FD_CLR 用于将 fd_se变量的某个位清零,也就是将一个文件描述符从 fd_set 中删除,参数 fd 就是要删除的文件描述符。
FD_ISSET 用于测试一个文件是否属于某个集合,参数 fd 就是要判断的文件描述符
timeout:超时时间,当我们调用 select 函数等待某些文件描述符可以设置超时时间,超时时间使用结构体 timeval 表示,结构体定义如下所示:
// 定义一个表示时间值的结构体 timeval
struct timeval {// tv_sec 成员表示秒数,是一个长整型数据// 用于存储时间值中的秒部分long tv_sec; /* 秒 */// tv_usec 成员表示微秒数,也是一个长整型数据// 用于存储时间值中的微秒部分,微秒是秒的百万分之一long tv_usec; /* 微秒 */
};
当 timeout 为 NULL 的时候就表示无限期的等待。
返回值: 0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作; -1,发生错误;其他值,可以进行操作的文件描述符个数。
使用 select 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fcntl.h>// 主函数
void main(void)
{int ret, fd; // 定义变量 ret 用于存储 select 函数的返回值,fd 用于存储文件描述符fd_set readfds; // 定义文件描述符集 readfds,用于存储需要监视的读操作文件描述符struct timeval timeout; // 定义超时结构体 timeout,用于设置 select 函数的超时时间// 以读写且非阻塞的方式打开文件 "dev_xxx",将返回的文件描述符存储在 fd 中fd = open("dev_xxx", O_RDWR | O_NONBLOCK); // 清空文件描述符集 readfds,将所有位都设置为 0FD_ZERO(&readfds); // 将文件描述符 fd 添加到文件描述符集 readfds 中,表示要监视 fd 的读操作FD_SET(fd, &readfds); // 设置超时结构体的秒数为 0timeout.tv_sec = 0; // 设置超时结构体的微秒数为 500000,即 500 毫秒timeout.tv_usec = 500000; // 调用 select 函数,监视 readfds 集合中的文件描述符的读操作,// 第二个参数为 NULL 表示不监视写操作,第三个参数为 NULL 表示不监视异常情况,// 最后一个参数为 timeout 表示设置的超时时间,将返回满足条件的文件描述符数量或错误值ret = select(fd + 1, &readfds, NULL, NULL, &timeout); // 根据 select 函数的返回值进行处理switch (ret) {case 0: // 如果返回值为 0,表示超时printf("timeout!\r\n");break;case -1: // 如果返回值为 -1,表示发生错误printf("error!\r\n");break;default: // 其他情况表示有文件描述符满足可读条件if(FD_ISSET(fd, &readfds)) { // 检查文件描述符 fd 是否在 readfds 集合中且满足可读条件// 这里可以添加使用 read 函数读取数据的代码,目前仅作为示例,实际使用中需要根据需求编写读取数据的逻辑// read(fd, buffer, sizeof(buffer)); // 假设 buffer 是用于存储读取数据的缓冲区}break;}
}
注:在单个线程中, select 函数能够监视的文件描述符数量有最大的限制,一般为 1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用 poll 函数。
3.2 poll函数
poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制, Linux 应用程序中 poll 函数原型如下所示:
int poll(struct pollfd *fds,nfds_t nfds,int timeout)
fds: 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体 pollfd类型的, pollfd 结构体如下所示:
// pollfd 结构体用于描述一个被监视的文件描述符及其相关事件
struct pollfd {// 文件描述符:需要监视的目标文件描述符// 设置为 -1 时表示忽略该条目(events 字段会被忽略,revents 会被置零)int fd; // 请求的事件:bitmask,指定要监视的事件类型// 常用事件标志包括:// POLLIN - 有数据可读(包括普通数据和优先带外数据)// POLLOUT - 可以写入数据(不会阻塞)// POLLPRI - 有紧急数据可读(如 TCP 带外数据)// POLLERR - 发生错误(仅在 revents 中返回,不可设置)// POLLHUP - 发生挂起(如管道关闭、连接断开)// POLLNVAL - 文件描述符无效(如未打开)short events; // 返回的事件:bitmask,由内核填充,指示实际发生的事件// 包含与 events 相同的标志位,另外还可能包含:// POLLERR - 文件描述符发生错误// POLLHUP - 文件描述符被挂起// POLLNVAL - 文件描述符无效short revents;
};
fd 是要监视的文件描述符,如果 fd 无效的话那么 events 监视事件也就无效,并且 revents
返回 0。 events 是要监视的事件。返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,也就是发生事件或错误的文件描述符数量; 0,超时; -1,发生错误,并且设置 errno 为错误类型。
使用 poll 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>int main(void)
{int ret;int fd; /* 要监视的文件描述符 */struct pollfd fds; /* 定义pollfd结构体,用于poll系统调用 */// 以读写和非阻塞模式打开指定文件// 非阻塞模式意味着read/write操作不会阻塞进程fd = open("filename", O_RDWR | O_NONBLOCK); if (fd < 0) {perror("open failed");return -1;}/* 配置pollfd结构体 */fds.fd = fd; /* 设置要监视的文件描述符 */fds.events = POLLIN; /* 监视POLLIN事件:表示有数据可读 *//* 可选事件还包括:POLLOUT(可写)、POLLERR(错误)等 *//* 调用poll函数监视文件描述符 */// 参数1:指向pollfd数组的指针(这里只有一个元素)// 参数2:数组中元素的数量// 参数3:超时时间(毫秒),-1表示无限等待,0表示立即返回ret = poll(&fds, 1, 500); /* 轮询文件是否有可读数据,超时时间500ms *//* 处理poll返回值 */if (ret > 0) { /* 有事件发生 */if (fds.revents & POLLIN) { /* 检查是否是可读事件 *//* 读取数据的代码 */char buffer[1024];int bytes = read(fd, buffer, sizeof(buffer));if (bytes > 0) {printf("Read %d bytes\n", bytes);/* 处理读取到的数据 */} else if (bytes == 0) {printf("EOF detected\n");} else {perror("read error");}} else if (fds.revents & POLLERR) { /* 检查是否有错误发生 */printf("Poll error on fd %d\n", fd);}} else if (ret == 0) { /* 超时,没有事件发生 */printf("Poll timeout after 500ms\n");} else { /* 错误处理 */perror("poll failed");close(fd);return -1;}close(fd); /* 关闭文件描述符 */return 0;
}
3.3 epoll函数
selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。
epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。应用程序需要先使用 epoll_create 函数创建一个 epoll 句柄, epoll_create 函数原型如下:
int epoll_create(int size)
size: 从 Linux2.6.8 开始此参数已经没有意义了,随便填写一个大于 0 的值就可以。
返回值: epoll 句柄,如果为-1 的话表示创建失败。
epoll 句柄创建成功以后使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事件, epoll_ctl 函数原型如下所示:
/*** epoll_ctl - 控制epoll实例,添加、修改或删除监视的文件描述符* @epfd: epoll实例的文件描述符(由epoll_create创建)* @op: 操作类型,可选值:* - EPOLL_CTL_ADD: 向epoll实例中添加新的监视文件描述符* - EPOLL_CTL_MOD: 修改已存在的监视文件描述符的事件掩码* - EPOLL_CTL_DEL: 从epoll实例中删除监视的文件描述符* @fd: 要操作的目标文件描述符(如socket、管道等)* @event: 指向epoll_event结构体的指针,指定要监视的事件类型和关联数据* 当op为EPOLL_CTL_DEL时,event参数可以为NULL* * 返回值:* 成功时返回0,失败时返回-1并设置errno*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数 struct epoll_event *event
struct epoll_event {uint32_t events; /* 要监视的事件类型(位掩码) */epoll_data_t data; /* 与文件描述符关联的用户数据 */
};typedef union epoll_data {void *ptr; /* 指向用户自定义数据的指针 */int fd; /* 文件描述符 */uint32_t u32; /* 32位整数 */uint64_t u64; /* 64位整数 */
} epoll_data_t;
常用事件类型:
常用事件类型(events 字段):
EPOLLIN - 有数据可读(包括普通数据和优先带外数据)
EPOLLOUT - 可以写入数据(不会阻塞)
EPOLLPRI - 有紧急数据可读(如 TCP 带外数据)
EPOLLERR - 文件描述符发生错误(自动触发,无需设置)
EPOLLHUP - 文件描述符被挂起(自动触发,无需设置)
EPOLLET - 设置为边沿触发模式(Edge Triggered,默认是水平触发)
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 判断发生了哪些事件。
maxevents: events 数组大小,必须大于 0。
timeout: 超时时间,单位为 ms。
返回值: 0,超时; -1,错误;其他值,准备就绪的文件描述符数量。
epoll 更多的是用在大规模的并发服务器上,因为在这种场合下 select 和 poll 并不适合。当
设计到的文件描述符(fd)比较少的时候就适合用 selcet 和 poll。
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>int main() {// 创建epoll实例,参数size已被弃用但必须为正数(内核2.6.8后被忽略)// 返回值是一个文件描述符,用于后续的epoll操作int epfd = epoll_create(1);if (epfd == -1) {perror("epoll_create");exit(EXIT_FAILURE);}// 创建TCP套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket");close(epfd); // 清理已创建的资源exit(EXIT_FAILURE);}// 配置服务器地址(实际应用中需要bind、listen等操作)struct sockaddr_in server_addr = {.sin_family = AF_INET,.sin_port = htons(8080),.sin_addr.s_addr = INADDR_ANY};// 绑定套接字(示例中省略错误处理)if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(sockfd);close(epfd);exit(EXIT_FAILURE);}// 监听连接(示例中省略错误处理)if (listen(sockfd, 5) == -1) {perror("listen");close(sockfd);close(epfd);exit(EXIT_FAILURE);}// 设置epoll_event结构体,配置监听事件struct epoll_event ev;// 监视可读事件并使用边沿触发模式// 边沿触发模式要求:// 1. 必须使用非阻塞I/O// 2. 必须处理完所有数据(否则不会再次触发)ev.events = EPOLLIN | EPOLLET;// 存储与事件关联的数据(可在epoll_wait返回时获取)ev.data.fd = sockfd;// 将监听套接字添加到epoll实例中if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {perror("epoll_ctl: add");close(sockfd);close(epfd);exit(EXIT_FAILURE);}// 后续可能需要修改监听事件(例如添加可写事件)// 注意:修改事件时需重新设置整个ev.events字段ev.events = EPOLLIN | EPOLLOUT | EPOLLET; // 同时监听可读和可写事件if (epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev) == -1) {perror("epoll_ctl: mod");close(sockfd);close(epfd);exit(EXIT_FAILURE);}// 当不再需要监听某个文件描述符时,从epoll实例中删除// 第三个参数为需要删除的文件描述符// 第四个参数在删除时可以为NULL(内核不使用该参数)if (epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL) == -1) {perror("epoll_ctl: del");close(sockfd);close(epfd);exit(EXIT_FAILURE);}// 关闭资源close(sockfd);close(epfd);return 0;
}
4. Linux 驱动下的 poll 操作函数
当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations 操作集中的 poll 函数就会执行。所以驱动程序的编写者需要提供对应的 poll 函数, poll 函数原型如下所示:
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
函数参数和返回值含义如下: filp: 要打开的设备文件(文件描述符)。
wait: 结构体 poll_table_struct 类型指针, 由应用程序传递进来的。一般将此参数传递给poll_wait 函数。
返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下:
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)
参数 wait_address 是要添加到 poll_table 中的等待队列头,参数 p 就是 poll_table,就是file_operations 中 poll 函数的 wait 参数。
使用示例:
/*** my_driver_poll - 实现设备驱动的轮询方法,用于I/O多路复用* @filp: 指向被操作文件的结构体指针,包含文件状态和私有数据* @wait: 用于注册等待队列的poll_table结构体指针,由内核提供* * 返回值:* 位掩码,表示当前设备的就绪状态(POLLIN/POLLOUT等)* * 功能说明:* 1. 注册等待队列,使当前进程可以在设备状态变化时被唤醒* 2. 检查设备的读写缓冲区状态,设置相应的就绪标志* 3. 立即返回当前状态,不阻塞进程*/
static unsigned int my_driver_poll(struct file *filp, struct poll_table_struct *wait)
{// 从文件的私有数据中获取设备结构体实例struct my_device *dev = filp->private_data;unsigned int mask = 0; // 初始化就绪状态掩码/* 注册等待队列 - 这是实现非阻塞轮询的关键 */// 将当前进程添加到读操作的等待队列中// 当设备有新数据可读时,会唤醒该队列中的进程poll_wait(filp, &dev->read_wait_queue, wait);// 将当前进程添加到写操作的等待队列中// 当设备有足够空间可写入时,会唤醒该队列中的进程poll_wait(filp, &dev->write_wait_queue, wait);/* 检查读缓冲区状态 - 决定是否返回可读标志 */// 如果接收缓冲区不为空(有数据可读)if (!list_empty(&dev->rx_buffer)) {// 设置可读标志:// POLLIN - 有数据可读(包括普通数据和优先数据)// POLLRDNORM - 有普通数据可读(与POLLIN类似,具体取决于设备类型)mask |= POLLIN | POLLRDNORM;}/* 检查写缓冲区状态 - 决定是否返回可写标志 */// 如果发送缓冲区有足够空间(大于最小写入空间阈值)if (dev->tx_buffer_space > MIN_WRITE_SPACE) {// 设置可写标志:// POLLOUT - 可以写入数据(不会阻塞)// POLLWRNORM - 可以写入普通数据(与POLLOUT类似,具体取决于设备类型)mask |= POLLOUT | POLLWRNORM;}// 返回当前设备的就绪状态掩码// 内核会根据这个掩码通知用户空间程序哪些操作可以立即执行return mask;
}