--- title: 阻塞IO与poll机制 tags: [Linux驱动, 阻塞IO, poll, select, 嵌入式, 等待队列, 异步通知] created: 2026-09-16 updated: 2026-09-17 pdf_ref: - "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第五十二章 Linux阻塞和非阻塞IO实验" - "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第五十三章 异步通知实验" --- # 阻塞IO与poll机制 > 💡 **关联知识**: [[Linux+C+C++技术体系梳理/2. Linux系统编程/04-IO多路复用]] | [[03-Linux驱动开发核心/04-并发同步与原子操作]] | [[03-Linux驱动开发核心/05-中断与定时器]] ## 一、阻塞与非阻塞IO概述 ### 1.1 什么是IO 这里的"IO"并非单片机中的GPIO引脚,而是**Input/Output(输入/输出)**,指应用程序对驱动设备的读写操作。当应用程序对设备驱动进行操作时,可能无法立即获取到设备资源。 ### 1.2 阻塞IO(Blocking IO) **定义**:当应用程序调用`read()`或`write()`时,如果设备不可用或数据未准备好,进程会进入**休眠状态**(阻塞),直到设备可用后被唤醒,再完成数据读写。 ```c // 阻塞方式打开设备 int fd = open("/dev/xxx_dev", O_RDWR); // 默认阻塞模式 ret = read(fd, &data, sizeof(data)); // 无数据时进程休眠 ``` **特点**: - 进程在等待期间不占用CPU - 需要在驱动中实现唤醒机制(通常在中断中唤醒) - CPU资源利用率高 ### 1.3 非阻塞IO(Non-blocking IO) **定义**:当设备不可用或数据未准备好时,`read()`/`write()`会立即返回一个错误码(`-EAGAIN`或`-EWOULDBLOCK`),应用程序可以选择重试或放弃。 ```c // 非阻塞方式打开设备 int fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); // 添加O_NONBLOCK ret = read(fd, &data, sizeof(data)); // 无数据时立即返回错误 ``` **特点**: - 进程不会休眠,持续轮询设备状态 - CPU占用率高(忙等待) - 适合对响应时间要求高的场景 ### 1.4 阻塞vs非阻塞对比 ```mermaid graph TD A[应用程序调用read] --> B{设备是否可用?} B -->|是| C[读取数据并返回] B -->|否| D{IO模式?} D -->|阻塞IO| E[进程进入休眠态] E --> F[等待设备就绪] F --> G[被唤醒] G --> C D -->|非阻塞IO| H[立即返回错误码] H --> I[应用程序重试或放弃] ``` ### 1.5 应用程序如何设置阻塞/非阻塞 | 方式 | 代码示例 | 说明 | | ------------ | ----------------------------------------- | ------------------------ | | 阻塞(默认) | `open("/dev/xxx", O_RDWR)` | 默认行为,无`O_NONBLOCK` | | 非阻塞 | `open("/dev/xxx", O_RDWR \| O_NONBLOCK)` | 添加`O_NONBLOCK`标志 | | 运行时切换 | `fcntl(fd, F_SETFL, flags \| O_NONBLOCK)` | 动态设置非阻塞 | --- ## 二、等待队列(Wait Queue) ### 2.1 等待队列原理 等待队列是Linux内核实现阻塞进程唤醒的机制。当设备不可用时,将进程添加到等待队列中使其休眠;当设备可用时(如中断发生),从队列中唤醒进程。 ```mermaid graph LR A[进程A] -->|加入| C[等待队列头] B[进程B] -->|加入| C D[中断/事件] -->|唤醒| C C -->|唤醒| E[进程A继续执行] C -->|唤醒| F[进程B继续执行] ``` ### 2.2 等待队列头(wait_queue_head_t) 等待队列头是队列的入口,定义在`include/linux/wait.h`: ```c struct __wait_queue_head { spinlock_t lock; // 自旋锁,保护队列 struct list_head task_list; // 链表,存放等待队列项 }; typedef struct __wait_queue_head wait_queue_head_t; ``` **初始化方法**: ```c // 方法1:动态初始化 wait_queue_head_t my_wq; init_waitqueue_head(&my_wq); // 方法2:静态初始化(推荐) DECLARE_WAIT_QUEUE_HEAD(my_wq); ``` ### 2.3 等待队列项(wait_queue_t) 每个等待的进程对应一个队列项: ```c struct __wait_queue { unsigned int flags; // 标志位 void *private; // 私有数据,通常指向task_struct wait_queue_func_t func; // 唤醒回调函数 struct list_head task_list; // 链表节点 }; typedef struct __wait_queue wait_queue_t; ``` **定义并初始化队列项**: ```c // 定义并初始化一个等待队列项,关联当前进程 DECLARE_WAITQUEUE(wait, current); ``` ### 2.4 添加/移除等待队列 ```c // 将等待队列项添加到队列头 void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); // 从队列头移除等待队列项 void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); ``` ### 2.5 唤醒函数 ```c // 唤醒所有等待进程(包括TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE) void wake_up(wait_queue_head_t *q); // 只唤醒TASK_INTERRUPTIBLE状态的进程(可被信号打断) void wake_up_interruptible(wait_queue_head_t *q); ``` **使用场景**: - `wake_up()`:唤醒所有类型进程 - `wake_up_interruptible()`:推荐使用,进程可被信号中断 ### 2.6 等待事件API | 函数 | 描述 | | ---------------------------------------------------------- | -------------------------------------------- | | `wait_event(wq, condition)` | 无条件等待,进程状态为`TASK_UNINTERRUPTIBLE` | | `wait_event_timeout(wq, condition, timeout)` | 带超时等待,超时后自动唤醒 | | `wait_event_interruptible(wq, condition)` | 可中断等待,进程状态为`TASK_INTERRUPTIBLE` | | `wait_event_interruptible_timeout(wq, condition, timeout)` | 可中断+超时 | **使用示例**: ```c // 等待releasekey有效(按键按下) wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); ``` ### 2.7 等待队列使用流程 ```mermaid sequenceDiagram participant App as 应用程序 participant Driver as 驱动 participant IRQ as 中断 App->>Driver: read(fd, buf, cnt) Driver->>Driver: DECLARE_WAITQUEUE(wait, current) Driver->>Driver: add_wait_queue(&r_wait, &wait) Driver->>Driver: __set_current_state(TASK_INTERRUPTIBLE) Driver->>Driver: schedule() [进程休眠] Note over IRQ: 设备就绪(如按键按下) IRQ->>Driver: 中断处理函数 Driver->>Driver: wake_up_interruptible(&r_wait) Driver->>App: 进程被唤醒,继续执行 Driver->>Driver: remove_wait_queue(&r_wait, &wait) Driver->>App: 返回数据 ``` --- ## 三、poll机制 ### 3.1 poll机制原理 当应用程序以非阻塞方式访问设备时,驱动需要提供**轮询(poll)**机制。应用程序通过`select()`、`poll()`或`epoll()`查询设备是否可操作,驱动中的`poll()`函数会被执行。 ```mermaid graph TD A[应用程序] -->|select/poll/epoll| B[内核] B -->|调用| C[驱动poll函数] C -->|返回事件| B B -->|通知| A A -->|数据可读/可写| D[read/write] ``` ### 3.2 select函数 ```c int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` **参数说明**: - `nfds`:最大文件描述符+1 - `readfds`:监视读事件的文件描述符集合 - `writefds`:监视写事件的文件描述符集合 - `exceptfds`:监视异常事件的文件描述符集合 - `timeout`:超时时间,NULL表示无限等待 **fd_set操作宏**: ```c FD_ZERO(&readfds); // 清空集合 FD_SET(fd, &readfds); // 添加fd到集合 FD_CLR(fd, &readfds); // 从集合删除fd FD_ISSET(fd, &readfds); // 测试fd是否在集合中 ``` ### 3.3 poll函数 ```c int poll(struct pollfd *fds, nfds_t nfds, int timeout); ``` **pollfd结构体**: ```c struct pollfd { int fd; // 文件描述符 short events; // 请求的事件(输入) short revents; // 返回的事件(输出) }; ``` **事件标志**: | 事件 | 描述 | | ------------ | ---------------------------- | | `POLLIN` | 有数据可以读取 | | `POLLPRI` | 有紧急数据需要读取 | | `POLLOUT` | 可以写数据 | | `POLLERR` | 发生错误 | | `POLLHUP` | 文件描述符挂起 | | `POLLNVAL` | 无效的请求 | | `POLLRDNORM` | 等同于`POLLIN`,普通数据可读 | ### 3.4 epoll机制 epoll是为处理大并发设计的,适合文件描述符数量多的场景: ```c // 创建epoll句柄 int epoll_create(int size); // 控制epoll(添加/修改/删除监视) int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 等待事件 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); ``` **epoll事件**: | 事件 | 描述 | | -------------- | ------------------------ | | `EPOLLIN` | 有数据可以读取 | | `EPOLLOUT` | 可以写数据 | | `EPOLLPRI` | 有紧急数据需要读取 | | `EPOLLERR` | 发生错误 | | `EPOLLHUP` | 挂起 | | `EPOLLET` | 边沿触发(默认水平触发) | | `EPOLLONESHOT` | 一次性监视 | ### 3.5 驱动中的poll函数 ```c unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait); ``` **实现要点**: 1. 调用`poll_wait()`将进程添加到等待队列 2. 根据设备状态返回相应事件 ```c static unsigned int xxx_poll(struct file *filp, poll_table *wait) { unsigned int mask = 0; struct xxx_dev *dev = filp->private_data; // 将进程添加到等待队列(不阻塞) poll_wait(filp, &dev->r_wait, wait); // 根据设备状态设置返回事件 if (atomic_read(&dev->data_ready)) { mask |= POLLIN | POLLRDNORM; // 可读 } if (!atomic_read(&dev->buffer_full)) { mask |= POLLOUT | POLLWRNORM; // 可写 } return mask; } ``` ### 3.6 poll_wait函数 ```c void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p); ``` **作用**:将当前进程添加到`poll_table`对应的等待队列中,**不会引起阻塞**。 ### 3.7 poll机制流程图 ```mermaid sequenceDiagram participant App as 应用程序 participant Kernel as 内核 participant Driver as 驱动 App->>Kernel: select/poll/epoll Kernel->>Driver: 调用file_operations.poll Driver->>Driver: poll_wait()添加到等待队列 Driver->>Kernel: 返回事件掩码 Kernel->>App: 返回就绪的fd alt 有数据可读 App->>Kernel: read(fd, buf, cnt) Kernel->>Driver: 调用read函数 Driver->>App: 返回数据 else 超时 App->>App: 处理超时 end ``` --- ## 四、异步通知(Fasync) ### 4.1 异步通知原理 异步通知类似于硬件中断,驱动主动向应用程序发送信号(如`SIGIO`),通知数据就绪。应用程序无需轮询,等待信号即可。 ```mermaid graph LR A[驱动程序] -->|kill_fasync| B[发送SIGIO信号] B --> C[应用程序] C -->|信号处理函数| D[读取数据] ``` ### 4.2 fasync_struct结构体 ```c struct fasync_struct { spinlock_t fa_lock; // 自旋锁 int magic; // 魔数 int fa_fd; // 文件描述符 struct fasync_struct *fa_next; // 链表下一个节点 struct file *fa_file; // 关联的文件 struct rcu_head fa_rcu; // RCU回调 }; ``` **在设备结构体中定义**: ```c struct xxx_dev { // ... 其他成员 struct fasync_struct *async_queue; // 异步通知队列 }; ``` ### 4.3 驱动中的fasync函数 ```c int (*fasync)(int fd, struct file *filp, int on); ``` **实现示例**: ```c static int xxx_fasync(int fd, struct file *filp, int on) { struct xxx_dev *dev = filp->private_data; // 调用fasync_helper初始化fasync_struct if (fasync_helper(fd, filp, on, &dev->async_queue) < 0) return -EIO; return 0; } // 在file_operations中注册 static struct file_operations xxx_ops = { .fasync = xxx_fasync, .release = xxx_release, // 关闭时释放 }; ``` ### 4.4 释放fasync_struct ```c static int xxx_release(struct inode *inode, struct file *filp) { // 调用fasync函数,on=0表示删除异步通知 return xxx_fasync(-1, filp, 0); } ``` ### 4.5 kill_fasync函数 ```c void kill_fasync(struct fasync_struct **fp, int sig, int band); ``` **参数**: - `fp`:fasync_struct指针 - `sig`:要发送的信号(如`SIGIO`) - `band`:可读设为`POLL_IN`,可写设为`POLL_OUT` **使用示例**: ```c // 在中断处理或定时器中发送信号 if (atomic_read(&dev->releasekey)) { if (dev->async_queue) kill_fasync(&dev->async_queue, SIGIO, POLL_IN); } ``` ### 4.6 应用程序处理异步通知 **三步设置**: ```c // 1. 注册信号处理函数 signal(SIGIO, sigio_signal_func); // 2. 将进程号告诉内核 fcntl(fd, F_SETOWN, getpid()); // 3. 开启异步通知 int flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | FASYNC); ``` **信号处理函数示例**: ```c static void sigio_signal_func(int signum) { int err; unsigned int keyvalue; err = read(fd, &keyvalue, sizeof(keyvalue)); if (err < 0) { // 读取错误处理 } else { printf("SIGIO signal! key value=%d\r\n", keyvalue); } } ``` ### 4.7 异步通知流程图 ```mermaid sequenceDiagram participant App as 应用程序 participant Driver as 驱动 participant IRQ as 中断/定时器 App->>Driver: open("/dev/xxx") App->>App: signal(SIGIO, handler) App->>App: fcntl(F_SETOWN, getpid()) App->>App: fcntl(F_SETFL, FASYNC) Driver->>Driver: fasync()被调用 Note over IRQ: 设备就绪 IRQ->>Driver: 中断处理函数 Driver->>App: kill_fasync(SIGIO) Driver->>App: 信号处理函数执行 App->>Driver: read(fd, buf, cnt) Driver->>App: 返回数据 ``` --- ## 五、完整源码分析 ### 5.1 阻塞IO实验源码(blockio.c) ```c #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define IMX6UIRQ_CNT 1 #define IMX6UIRQ_NAME "blockio" #define KEY0VALUE 0X01 #define INVAKEY 0XFF #define KEY_NUM 1 struct irq_keydesc { int gpio; int irqnum; unsigned char value; char name[10]; irqreturn_t (*handler)(int, void *); }; struct imx6uirq_dev { dev_t devid; struct cdev cdev; struct class *class; struct device *device; int major; int minor; struct device_node *nd; atomic_t keyvalue; atomic_t releasekey; struct timer_list timer; struct irq_keydesc irqkeydesc[KEY_NUM]; unsigned char curkeynum; wait_queue_head_t r_wait; /* 读等待队列头 */ }; struct imx6uirq_dev imx6uirq; /* 中断服务函数,开启定时器消抖 */ static irqreturn_t key0_handler(int irq, void *dev_id) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id; dev->curkeynum = 0; dev->timer.data = (volatile long)dev_id; mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); return IRQ_RETVAL(IRQ_HANDLED); } /* 定时器服务函数,按键消抖 */ void timer_function(unsigned long arg) { unsigned char value; unsigned char num; struct irq_keydesc *keydesc; struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg; num = dev->curkeynum; keydesc = &dev->irqkeydesc[num]; value = gpio_get_value(keydesc->gpio); if (value == 0) { /* 按下按键 */ atomic_set(&dev->keyvalue, keydesc->value); } else { /* 按键松开 */ atomic_set(&dev->keyvalue, 0x80 | keydesc->value); atomic_set(&dev->releasekey, 1); } /* 唤醒进程 */ if (atomic_read(&dev->releasekey)) { wake_up_interruptible(&dev->r_wait); } } /* 按键IO初始化 */ static int keyio_init(void) { // ... GPIO和中断初始化代码 ... /* 初始化等待队列头 */ init_waitqueue_head(&imx6uirq.r_wait); return 0; } /* 从设备读取数据(阻塞方式,原书示例代码 52.2.2.1) */ static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { int ret = 0; unsigned char keyvalue = 0; unsigned char releasekey = 0; struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data; #if 0 /* 加入等待队列,等待被唤醒,也就是有按键按下 */ ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); if (ret) { goto wait_error; } #endif DECLARE_WAITQUEUE(wait, current); /* 定义一个等待队列 */ if (atomic_read(&dev->releasekey) == 0) { /* 没有按键按下 */ add_wait_queue(&dev->r_wait, &wait); /* 添加到等待队列头 */ __set_current_state(TASK_INTERRUPTIBLE); /* 设置任务状态 */ schedule(); /* 进行一次任务切换 */ if (signal_pending(current)) { /* 判断是否为信号引起的唤醒 */ ret = -ERESTARTSYS; goto wait_error; } __set_current_state(TASK_RUNNING); /* 设置为运行状态 */ remove_wait_queue(&dev->r_wait, &wait); /* 将等待队列移除 */ } keyvalue = atomic_read(&dev->keyvalue); releasekey = atomic_read(&dev->releasekey); if (releasekey) { if (keyvalue & 0x80) { /* 松开按键 */ keyvalue &= ~0x80; ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue)); } else { goto data_error; } } else { goto data_error; } return 0; wait_error: set_current_state(TASK_RUNNING); /* 设置任务为运行态 */ remove_wait_queue(&dev->r_wait, &wait); /* 将等待队列移除 */ return ret; data_error: return -EINVAL; } static struct file_operations imx6uirq_fops = { .owner = THIS_MODULE, .open = imx6uirq_open, .read = imx6uirq_read, }; ``` ### 5.2 非阻塞IO+poll实验源码(noblockio.c) ```c /* 读取函数(支持阻塞和非阻塞) */ static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { int ret = 0; unsigned char keyvalue = 0; unsigned char releasekey = 0; struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data; if (filp->f_flags & O_NONBLOCK) { /* 非阻塞访问 */ if (atomic_read(&dev->releasekey) == 0) return -EAGAIN; } else { /* 阻塞访问 */ ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); if (ret) { goto wait_error; } } keyvalue = atomic_read(&dev->keyvalue); releasekey = atomic_read(&dev->releasekey); if (releasekey) { if (keyvalue & 0x80) { keyvalue &= ~0x80; ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue)); } else { goto data_error; } } else { goto data_error; } return 0; wait_error: return ret; data_error: return -EINVAL; } /* poll函数 */ static unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait) { unsigned int mask = 0; struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data; poll_wait(filp, &dev->r_wait, wait); if (atomic_read(&dev->releasekey)) { /* 按键按下 */ mask = POLLIN | POLLRDNORM; /* 返回POLLIN */ } return mask; } static struct file_operations imx6uirq_fops = { .owner = THIS_MODULE, .open = imx6uirq_open, .read = imx6uirq_read, .poll = imx6uirq_poll, }; ``` > 阻塞实验的测试 APP 直接复用第 51 章的 `imx6uirqApp.c`(重命名为 `blockioApp.c`,内容不修改)。 #### 5.2.1 非阻塞测试APP(noblockioApp.c,原书示例代码 52.3.3.2) ```c #include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" #include "poll.h" #include "sys/select.h" #include "sys/time.h" #include "linux/ioctl.h" int main(int argc, char *argv[]) { int fd; int ret = 0; char *filename; struct pollfd fds; fd_set readfds; struct timeval timeout; unsigned char data; if (argc != 2) { printf("Error Usage!\r\n"); return -1; } filename = argv[1]; fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞访问 */ if (fd < 0) { printf("Can't open file %s\r\n", filename); return -1; } #if 0 /* 使用 poll 函数实现非阻塞访问 */ fds.fd = fd; fds.events = POLLIN; while (1) { ret = poll(&fds, 1, 500); if (ret) { /* 数据有效 */ ret = read(fd, &data, sizeof(data)); if (ret < 0) { /* 读取错误 */ } else { if (data) printf("key value = %d \r\n", data); } } else if (ret == 0) { /* 超时 */ /* 用户自定义超时处理 */ } else if (ret < 0) { /* 错误 */ /* 用户自定义错误处理 */ } } #endif /* 使用 select 函数实现非阻塞访问(原书默认开启) */ while (1) { FD_ZERO(&readfds); FD_SET(fd, &readfds); /* 构造超时时间 */ timeout.tv_sec = 0; timeout.tv_usec = 500000; /* 500ms */ ret = select(fd + 1, &readfds, NULL, NULL, &timeout); switch (ret) { case 0: /* 超时 */ break; case -1: /* 错误 */ break; default: /* 可以读取数据 */ if (FD_ISSET(fd, &readfds)) { ret = read(fd, &data, sizeof(data)); if (ret < 0) { /* 读取错误 */ } else { if (data) printf("key value=%d\r\n", data); } } break; } } close(fd); return ret; } ``` > 原书说明:`#if 0` 段用 `poll` 函数轮询,默认开启的 `while` 段用 `select` 函数实现非阻塞访问;想用 poll 时把 `#if 0` 改成 `#if 1` 即可。 ### 5.3 异步通知实验源码(asyncnoti.c) ```c /* 设备结构体 */ struct imx6uirq_dev { // ... 其他成员 struct fasync_struct *async_queue; /* 异步相关结构体 */ }; /* fasync函数 */ static int imx6uirq_fasync(int fd, struct file *filp, int on) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data; return fasync_helper(fd, filp, on, &dev->async_queue); } /* release函数 */ static int imx6uirq_release(struct inode *inode, struct file *filp) { return imx6uirq_fasync(-1, filp, 0); } /* 定时器服务函数中发送信号 */ void timer_function(unsigned long arg) { // ... 按键消抖处理 ... if (atomic_read(&dev->releasekey)) { if (dev->async_queue) kill_fasync(&dev->async_queue, SIGIO, POLL_IN); } } static struct file_operations imx6uirq_fops = { .owner = THIS_MODULE, .open = imx6uirq_open, .read = imx6uirq_read, .poll = imx6uirq_poll, .fasync = imx6uirq_fasync, .release = imx6uirq_release, }; ``` ### 5.4 异步通知测试APP(asyncnotiApp.c) ```c #include #include #include #include static int fd = 0; static void sigio_signal_func(int signum) { int err = 0; unsigned int keyvalue = 0; err = read(fd, &keyvalue, sizeof(keyvalue)); if (err < 0) { printf("Read error!\r\n"); } else { printf("SIGIO signal! key value=%d\r\n", keyvalue); } } int main(int argc, char *argv[]) { int flags = 0; char *filename; if (argc != 2) { printf("Error Usage!\r\n"); return -1; } filename = argv[1]; fd = open(filename, O_RDWR); if (fd < 0) { printf("Can't open file %s\r\n", filename); return -1; } signal(SIGIO, sigio_signal_func); fcntl(fd, F_SETOWN, getpid()); flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | FASYNC); while (1) { sleep(2); } close(fd); return 0; } ``` --- ## 六、实验验证 ### 6.1 编译方法 **驱动编译(Makefile)**: ```makefile KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek CURRENT_PATH := $(shell pwd) obj-m := blockio.o # 或 noblockio.o, asyncnoti.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean ``` **编译命令**: ```bash # 编译驱动模块 make -j32 # 交叉编译测试APP arm-linux-gnueabihf-gcc blockioApp.c -o blockioApp arm-linux-gnueabihf-gcc noblockioApp.c -o noblockioApp arm-linux-gnueabihf-gcc asyncnotiApp.c -o asyncnotiApp ``` ### 6.2 测试步骤 ```bash # 1. 拷贝文件到开发板 cp blockio.ko blockioApp /rootfs/lib/modules/4.1.15/ # 2. 加载驱动 cd /lib/modules/4.1.15 depmod modprobe blockio.ko # 3. 后台运行测试APP ./blockioApp /dev/blockio & # 4. 按下KEY0按键测试 # 观察输出按键值 # 5. 查看CPU使用率 top # 不加阻塞处理时 imx6uirqApp 的 CPU 占用率高达 99.6%,加入阻塞访问后降到 0.0% # 6. 关闭后台程序(先用 ps 查看 PID) ps kill -9 149 # 149 为示例 PID ``` **测试对比(原书 52.2.3 / 52.3.3)**:三个实验的设备节点分别为 `/dev/blockio`、`/dev/noblockio`、`/dev/asyncnoti`;前两个驱动加载后用 `./xxxApp /dev/xxx &` 后台运行,按下 KEY0 即打印按键值,CPU 占用率均低至 0.0%。 ### 6.3 非阻塞IO测试 ```bash # 使用select方式测试 ./noblockioApp /dev/noblockio & # 或使用poll方式(修改源码中#if 0为#if 1) ./noblockioApp /dev/noblockio & ``` ### 6.4 异步通知测试 ```bash # 加载异步通知驱动 modprobe asyncnoti.ko # 运行测试APP ./asyncnotiApp /dev/asyncnoti # 按下KEY0按键,观察输出 # SIGIO signal! key value=1 ``` --- ## 七、IO模型对比 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 | 模型 | 机制 | CPU占用 | 响应延迟 | 实现复杂度 | 适用场景 | | ---------- | ----------------- | ------- | -------- | ---------- | ------------ | | 阻塞IO | 等待队列 | 低 | 高 | 简单 | 低频设备 | | 非阻塞IO | 轮询 | 高 | 低 | 简单 | 实时性要求高 | | IO多路复用 | select/poll/epoll | 中 | 中 | 中等 | 多设备监控 | | 异步通知 | 信号(SIGIO) | 低 | 低 | 复杂 | 事件驱动 | **选择建议**: - 单设备、低频操作:阻塞IO - 多设备、需要并发:IO多路复用(epoll) - 事件驱动、低延迟:异步通知 - 实时性要求极高:非阻塞IO + 轮询 --- ## 八、跨平台对比 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 | 特性 | IMX6ULL (Linux) | STM32 (FreeRTOS) | RK3568 (Linux) | | -------- | ----------------- | ------------------ | ----------------- | | 阻塞IO | 等待队列 | 任务通知/信号量 | 等待队列 | | poll机制 | select/poll/epoll | 无原生支持 | select/poll/epoll | | 异步通知 | SIGIO信号 | 无原生支持 | SIGIO信号 | | 中断机制 | request_irq | xTaskNotifyFromISR | request_irq | | 任务调度 | 内核调度器 | FreeRTOS调度器 | 内核调度器 | **嵌入式差异**: - **Linux系统**:完整的IO模型支持,等待队列、poll、异步通知 - **RTOS系统**:通常使用任务通知、事件组、信号量实现类似功能 - **裸机系统**:轮询+中断,无阻塞机制 --- ## 九、面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 题目1:阻塞IO和非阻塞IO有什么区别? **考察点**:IO模型理解 **参考答案**: - **阻塞IO**:进程在设备不可用时进入休眠状态,不占用CPU,等待设备就绪后被唤醒 - **非阻塞IO**:进程在设备不可用时立即返回错误码(`-EAGAIN`),应用程序需要重试 - **核心区别**:阻塞IO让出CPU,非阻塞IO占用CPU轮询 - **应用场景**:阻塞IO适合低频操作,非阻塞IO适合实时性要求高的场景 ### 题目2:如何实现设备的可poll功能? **考察点**:poll机制实现 **参考答案**: 1. 在驱动中实现`file_operations`的`poll`函数 2. 在`poll`函数中调用`poll_wait()`将进程添加到等待队列 3. 根据设备状态设置返回事件掩码(`POLLIN`/`POLLOUT`等) 4. 用户空间使用`select()`/`poll()`/`epoll()`监控 5. 当设备就绪时,驱动返回相应事件,用户空间进行读写操作 ### 题目3:等待队列的使用流程是什么? **考察点**:等待队列机制 **参考答案**: 1. **定义并初始化等待队列头**:`DECLARE_WAIT_QUEUE_HEAD(r_wait)`或`init_waitqueue_head()` 2. **在read函数中等待**: - 定义等待队列项:`DECLARE_WAITQUEUE(wait, current)` - 添加到队列:`add_wait_queue(&r_wait, &wait)` - 设置进程状态:`__set_current_state(TASK_INTERRUPTIBLE)` - 让出CPU:`schedule()` 3. **在中断中唤醒**:`wake_up_interruptible(&r_wait)` 4. **清理工作**:`remove_wait_queue(&r_wait, &wait)` ### 题目4:异步通知和信号有什么关系? **考察点**:异步通知机制 **参考答案**: - **异步通知基于信号机制**:驱动通过`kill_fasync()`向应用程序发送信号(如`SIGIO`) - **应用程序处理**: 1. 使用`signal()`注册信号处理函数 2. 使用`fcntl(F_SETOWN)`将进程号告诉内核 3. 使用`fcntl(F_SETFL, FASYNC)`开启异步通知 - **优势**:无需轮询,驱动主动通知,CPU占用低 - **限制**:仅支持`SIGIO`等特定信号,不适合高频事件 ### 题目5:select、poll、epoll有什么区别? **考察点**:IO多路复用对比 **参考答案**: | 特性 | select | poll | epoll | | -------- | ---------------- | ---------- | --------------- | | 最大fd数 | 1024(FD_SETSIZE) | 无限制 | 无限制 | | 数据结构 | fd_set位图 | pollfd数组 | 红黑树+就绪链表 | | 触发方式 | 水平触发 | 水平触发 | 水平/边沿触发 | | 效率 | O(n)遍历 | O(n)遍历 | O(1)事件回调 | | 内核实现 | 遍历所有fd | 遍历所有fd | 回调通知就绪fd | | 适用场景 | 少量fd | 中等fd | 大量fd(高并发) | **选择建议**: - 少量fd(<1024):select - 中等fd:poll - 大量fd、高并发:epoll --- **代码来源**: Linux驱动例程 14_blockio, 15_noblockio, 16_asyncnoti **最后更新**: 2026-09-17 **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第52章 Linux阻塞和非阻塞IO实验、第53章 异步通知实验