4. Linux内核阻塞与非阻塞IO¶
在Linux内核驱动开发中,IO操作是连接用户空间与内核空间、硬件设备的核心环节, 其中阻塞IO与非阻塞IO是两种最基础、最常用的IO模型。无论是字符设备、块设备还是网络设备的驱动开发,都离不开对这两种IO模型的理解与应用。 阻塞IO凭借其简洁的实现逻辑、低CPU占用率,广泛应用于对实时性要求不高、无需频繁轮询的场景; 非阻塞IO则通过避免进程阻塞,提升了程序的并发能力,适用于需要同时处理多个设备、对响应速度有一定要求的场景。
4.1. 阻塞与非阻塞IO概念¶
IO操作的核心本质是“数据交互”,即用户空间与内核空间、内核空间与硬件设备之间的数据传输。 阻塞与非阻塞IO的核心区别在于IO操作无法立即完成时,进程的处理方式不同——是暂停等待(阻塞),还是立即返回(非阻塞)。
4.1.1. 阻塞IO¶
阻塞IO(Block IO)是最直观、最常用的IO模型。当进程发起IO请求后,如果当前无法完成数据传输,进程会主动放弃CPU使用权,进入休眠状态, 直到IO条件满足,内核才会唤醒该进程,继续完成IO操作。
在阻塞IO模型中,进程的休眠是“主动且高效”的,不会占用CPU资源,仅在IO就绪时被唤醒,适合对CPU资源敏感、无需频繁处理IO事件的场景, 例如按键检测、串口数据接收等。
阻塞IO的核心特点:进程等待IO就绪期间,处于休眠状态,不消耗CPU;实现逻辑简单,无需额外的轮询机制;IO完成后,进程能及时响应。
4.1.2. 非阻塞IO¶
非阻塞IO(Non-Block IO)与阻塞IO相反,当进程发起IO请求后,无论IO条件是否满足,内核都会立即返回结果: 如果IO就绪,直接完成数据传输并返回成功;如果IO未就绪,不阻塞进程,而是返回一个错误标识,告知进程“当前无法完成IO操作”。
进程收到错误标识后,可选择继续发起IO请求(轮询),或去处理其他任务,待后续再次检测IO状态。 为了避免轮询导致的CPU占用过高,非阻塞IO通常会与poll/select/epoll等机制配合使用,实现高效的IO事件监听。
非阻塞IO的核心特点:进程不阻塞,始终处于运行状态;IO未就绪时立即返回错误,需配合轮询或事件监听机制; 适合需要同时处理多个IO设备、对响应实时性有要求的场景,例如多串口通信、网络数据收发等。
4.1.3. IO就绪与阻塞条件¶
无论是阻塞IO还是非阻塞IO,“IO就绪”都是核心触发条件。IO就绪通常指:硬件设备已准备好数据、 内核缓冲区有可用空间、资源已释放。
阻塞条件则是IO未就绪,此时阻塞IO会让进程休眠,非阻塞IO则立即返回错误。 内核通过“等待队列”管理休眠的进程,当IO就绪时,唤醒等待队列中的进程,完成IO操作。
4.2. 阻塞与非阻塞IO执行流程¶
4.2.3. 两种流程对比说明¶
阻塞IO的流程核心是“休眠等待”,无需进程主动轮询,CPU利用率高,但进程只能专注于单个IO操作,无法处理其他任务; 非阻塞IO配合poll机制的流程核心是“事件监听”,进程可在IO未就绪时处理其他任务,并发能力强,且通过休眠避免了轮询导致的CPU浪费。
4.3. 阻塞IO常用函数¶
4.3.1. 等待队列头初始化¶
4.3.1.1. init_waitqueue_head函数¶
init_waitqueue_head函数用于初始化一个等待队列头,用于管理休眠的进程,是阻塞IO的基础。 等待队列头是内核中用于组织休眠进程的链表结构,所有因IO未就绪而休眠的进程,都会被加入到对应的等待队列中。
函数原型:
1 | void init_waitqueue_head(wait_queue_head_t *wq);
|
参数说明:
wq:等待队列头指针。
4.3.2. 可中断阻塞等待及唤醒¶
4.3.2.1. wait_event_interruptible函数¶
wait_event_interruptible函数用于让当前进程进入 可中断 的休眠状态,等待指定的阻塞条件满足。 进程休眠期间,可被信号中断,中断后返回错误标识,适合大多数阻塞IO场景。
函数原型:
1 | int wait_event_interruptible(wait_queue_head_t wq, int condition);
|
参数说明:
wq:等待队列头,进程会被加入到该等待队列中休眠。
condition:阻塞条件,当condition为真时,进程被唤醒;为假时,进程保持休眠。
返回值:0表示进程被正常唤醒;-ERESTARTSYS表示进程被信号中断,需重新发起IO请求
4.3.2.2. wake_up_interruptible函数¶
wake_up_interruptible函数用于唤醒等待队列中所有处于可中断休眠状态的进程,忽略不可中断休眠的进程,通知进程IO条件已满足,可继续执行IO操作。
函数原型:
1 | void wake_up_interruptible(wait_queue_head_t *wq);
|
参数说明:
wq为等待队列头指针,指向需要唤醒的等待队列。
4.3.3. 不可中断阻塞等待及唤醒¶
4.3.3.1. wait_event函数¶
wait_event函数用于让当前进程进入 不可中断 的休眠状态,等待阻塞条件满足。 进程休眠期间,无法被信号中断,仅在阻塞条件满足时被唤醒,适合对IO稳定性要求极高、不允许被中断的场景。
函数原型:
1 | void wait_event(wait_queue_head_t wq, int condition);
|
参数说明:
wq:等待队列头,进程会被加入到该等待队列中休眠。
condition:阻塞条件,当condition为真时,进程被唤醒;为假时,进程保持休眠。
4.3.3.2. wake_up函数¶
wake_up函数用于唤醒等待队列中所有进程,包括不可中断休眠的进程和可中断休眠的进程。
函数原型:
1 | wake_up(wait_queue_head_t *wq);
|
参数说明:
wq为等待队列头指针,指向需要唤醒的等待队列。
4.4. 非阻塞IO常用函数¶
4.4.1. poll函数¶
驱动层实现的poll回调函数,用于响应用户空间的poll请求,检测IO状态,返回就绪的IO事件(如可读、可写)。 是连接用户空间poll调用与内核IO状态检测的核心函数。
函数原型:
1 | unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait);
|
参数说明:
filp:文件结构体指针,指向当前打开的设备文件。
wait:poll_table_struct类型指针,用于将进程加入到等待队列中,实现进程休眠。
返回值:返回就绪的IO事件掩码,常用掩码如下:
POLLIN:表示有数据可读。
POLLOUT:表示有空间可写。
POLLERR:表示IO操作出错。
POLLRDNORM:表示有普通数据可读,与POLLIN功能类似,常用在字符设备中。
4.4.2. poll_wait函数¶
poll_wait函数用于将当前进程加入到指定的等待队列中,配合poll函数实现进程休眠,监听IO就绪事件。 该函数仅将进程加入等待队列,不阻塞进程,阻塞由内核根据poll函数的返回值决定。
函数原型:
1 | void poll_wait(struct file *filp, wait_queue_head_t *wq, struct poll_table_struct *wait);
|
参数说明:
filp:文件结构体指针,指向当前设备文件。
wq:等待队列头指针,进程将被加入到该等待队列中。
wait:poll_table_struct类型指针,与poll函数的wait参数一致,用于关联进程与等待队列。
4.5. 阻塞IO实验¶
本实验在Linux中断子系统实验基础上进行修改,设备树插件完全一致,驱动仅说明修改部分,重复部分不作说明。
阻塞IO实现:通过等待队列实现,当按键未按下时,应用程序read操作阻塞,按键按下后,驱动唤醒进程,返回按键状态。
本章的示例代码目录为: linux_driver/20_blockio
4.5.1. 驱动代码详解¶
核心数据结构定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /* 定义 LED 字符设备结构体 */
struct led_chrdev {
/* 字符设备结构体 */
struct cdev dev;
/* 自旋锁 */
spinlock_t spinlock;
/* LED 状态 */
int led_state;
/* LED 的 GPIO 描述符 */
struct gpio_desc *led_gpio;
/* 按钮的 GPIO 描述符 */
struct gpio_desc *button_gpio;
/* 消抖定时器 */
struct timer_list debounce_timer;
/* 中断号 */
int irq;
/* 阻塞 IO 等待队列头 */
wait_queue_head_t wq;
/* 按键标志,表示数据是否就绪 */
int button_pressed;
/* 按键状态,供应用层读取 */
int button_state;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
|
第18行:阻塞IO等待队列头,用于管理阻塞的进程;
第20行:阻塞IO触发开关,标记按键是否按下,控制进程阻塞与唤醒;
第22行:按键状态,0-按下,1-未按下,供应用层读取。
驱动初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | static int pdrv_led_probe(struct platform_device *pdev)
{
// 内存分配,从设备树获取资源,字符设备注册(省略重复代码)
/* 初始化自旋锁 */
spin_lock_init(&led_cdev->spinlock);
/* 初始化消抖定时器 */
timer_setup(&led_cdev->debounce_timer, button_debounce_callback, 0);
/* 默认关闭 LED */
led_cdev->led_state = 0;
/* 初始化等待队列头 */
init_waitqueue_head(&led_cdev->wq);
/* 初始化按键按下标志为0,表示未按下 */
led_cdev->button_pressed = 0;
/* 初始化按键状态为1,表示高电平 */
led_cdev->button_state = 1;
/* 获取按键GPIO对应的中断号 */
led_cdev->irq = gpiod_to_irq(led_cdev->button_gpio);
if (led_cdev->irq < 0) {
ret = led_cdev->irq;
/* 打印获取中断号失败 */
printk(KERN_ERR "Failed to get button IRQ: %d\n", ret);
/* 跳转到错误处理标签 */
goto device_err;
}
/* 申请中断,下降沿触发 */
ret = devm_request_irq(&pdev->dev, led_cdev->irq, button_irq_handler,
IRQF_TRIGGER_FALLING,
"button_irq", led_cdev);
if (ret < 0) {
/* 打印申请中断失败 */
printk(KERN_ERR "Failed to request IRQ: %d\n", ret);
/* 跳转到错误处理标签 */
goto device_err;
}
return 0;
// 错误处理(省略重复代码)
}
|
第15行:初始化等待队列头;
第18行:初始化按键按下标志为0,表示未按下,读取会进入阻塞休眠。
第21行:初始化按键状态为1,按键上拉,默认处于高电平。
消抖定时器回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | static void button_debounce_callback(struct timer_list *t)
{
/* 反向找到包含这个定时器的设备结构体 led_chrdev 指针 */
struct led_chrdev *led_cdev = from_timer(led_cdev, t, debounce_timer);
/* 用于保存中断状态信息 */
unsigned long flags;
/* 获取自旋锁并保存中断状态 */
spin_lock_irqsave(&led_cdev->spinlock, flags);
/* 读取稳定后的按键电平 */
led_cdev->button_state = gpiod_get_value(led_cdev->button_gpio);
/* 低电平表示按键稳定按下 */
if(led_cdev->button_state == 0){
/* 翻转LED电平状态 */
gpiod_set_value(led_cdev->led_gpio, !gpiod_get_value(led_cdev->led_gpio));
/* 设置按下标志 */
led_cdev->button_pressed = 1;
/* 唤醒等待队列:通知应用层有数据可读 */
wake_up_interruptible(&led_cdev->wq);
}
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&led_cdev->spinlock, flags);
/* 按键按下打印日志 */
if(led_cdev->button_state == 0){
printk(KERN_INFO "按键已按下,LED状态翻转\n");
}
}
|
按键按下时,翻转LED状态,设置button_pressed为1,通过wake_up_interruptible唤醒等待队列中的应用层进程。
阻塞IO实现
read函数是阻塞IO的核心实现,应用层调用read时,若按键未按下(button_pressed=0),进程进入阻塞休眠状态; 按键按下后,被等待队列唤醒,读取按键状态,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | static ssize_t pdrv_led_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
/* 从文件私有数据获取设备结构体 */
struct led_chrdev *led_cdev = filp->private_data;
/* 保存中断状态,保护临界区 */
unsigned long flags;
/* 临时变量,用于存储读取的按键状态 */
int btn_state;
/* 阻塞条件:按键未按下时,应用层read会阻塞 */
if (!led_cdev->button_pressed) {
/* 进入阻塞等待,等待被唤醒,然后判断button_pressed是否1 */
if (wait_event_interruptible(led_cdev->wq, led_cdev->button_pressed)) {
/* 被信号中断,返回错误 */
return -ERESTARTSYS;
}
}
/* 自旋锁保护 */
spin_lock_irqsave(&led_cdev->spinlock, flags);
/* 置位为0,准备下一次进入阻塞等待 */
led_cdev->button_pressed = 0;
/* 读取当前按键电平 */
btn_state = led_cdev->button_state;
/* 释放自旋锁 */
spin_unlock_irqrestore(&led_cdev->spinlock, flags);
/* 如果不使用临时变量加锁保护,copy_to_user直接返回led_cdev->button_state,
可能会因为copy_to_user睡眠,如果此时定时器中断触发修改button_state,
数据一致性无法保证 */
/* 将按键状态拷贝到用户空间,应用层读int类型,对应sizeof(int) */
if (copy_to_user(buf, &btn_state, sizeof(int))) {
/* 拷贝失败,返回错误 */
return -EFAULT;
}
/* 返回实际拷贝的字节数 */
return sizeof(int);
}
|
阻塞IO实现逻辑:
阻塞条件判断:若button_pressed为0(按键未按下),调用wait_event_interruptible函数,将进程放入等待队列wq,进入可中断休眠状态,放弃CPU资源;
唤醒机制:按键按下后,定时器回调函数设置button_pressed为1,并调用wake_up_interruptible唤醒等待队列中的进程,判断button_pressed是否为真;
数据交互:进程唤醒后,通过copy_to_user将按键状态(btn_state)拷贝到用户空间,供应用层读取;
重置标志:通过自旋锁保护,将button_pressed置为0,为下一次阻塞等待做准备;
4.5.2. 应用代码详解¶
应用代码比较简单,仅用来读取按键状态,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd, ret;
int button_state;
if(argc != 2) {
printf("Usage: ./blockio_app /dev/blockio\n");
return -1;
}
/* 只读打开 */
fd = open(argv[1], O_RDONLY);
if(0 > fd) {
printf("ERROR: %s file open failed!\n", argv[1]);
return -1;
}
for ( ; ; ) {
/* 阻塞读取 */
read(fd, &button_state, sizeof(int));
printf("button_state = %d\n",button_state);
}
close(fd);
return 0;
}
|
4.5.3. Makefile说明¶
本节实验使用的Makefile如下所示,编写该Makefile时,只需要根据实际情况修改变量KERNEL_DIR、obj-m和test_app即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #指定内核路径,可以是相对路径或绝对路径
KERNEL_DIR=../../kernel/
#KERNEL_DIR=/home/guest/LubanCat_Linux_rk356x_SDK/kernel/
#指定目标架构为arm64
ARCH=arm64
#指定交叉编译工具链的前缀
CROSS_COMPILE=aarch64-linux-gnu-
#导出为环境变量
export ARCH CROSS_COMPILE
#指定要编译的内核模块目标文件
obj-m := blockio.o
test_app = blockio_app
#all :默认目标,执行时会编译驱动模块
#$(MAKE) :调用make工具
#-C $(KERNEL_DIR) :指定的内核源码目录
#M=$(CURDIR) :模块的源码位于当前目录
#modules :编译模块
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
$(CROSS_COMPILE)gcc -o $(test_app) $(test_app).c
.PHONE:clean
#清理编译生成的文件
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
rm $(test_app)
|
4.5.4. 编译设备树和驱动¶
此部分和Linux中断子系统实验完全一致不作过多说明。
编译得到设备树插件lubancat-led-overlay.dtb、驱动模块blockio.ko和应用程序blockio_app。
4.5.5. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后会发现系统心跳灯默认没有闪烁, 是因为我们使用设备树插件关闭了leds节点,释放了引脚。
使用以下命令加载驱动和运行应用程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #加载驱动
sudo insmod blockio.ko
#信息输出如下
[ 290.386588] led platform driver init
[ 290.387339] led platform driver probe
[ 290.387504] major=236, minor=0
#运行应用程序
sudo ./blockio_app /dev/blockio
#打开新终端或应用程序后台运行,查看程序CPU占用率
ps aux | grep blockio_app
#信息输出如下,
root 52697 0.0 0.0 1788 420 ttyFIQ0 S 04:45 0:00 ./blockio_app /dev/blockio
#52697:进程ID
#第一个0.0:CPU使用率0
#第二个0.0:内存使用率0
|
应用程序中循环部分没有加任何延时,但CPU使用率为0,说明程序进入阻塞休眠。
使用杜邦线一端连接按键引脚,另一端多次连接和断开连接GND引脚模拟按键按下和松开,信息打印如下:
1 2 3 4 5 6 7 | [ 306.822824] pdrv_led open
[ 314.431509] 按键已按下,LED状态翻转
button_state = 0
[ 314.541511] 按键已按下,LED状态翻转
button_state = 0
[ 316.618298] 按键已按下,LED状态翻转
button_state = 0
|
从打印信息可以看到,按下按键,应用程序被唤醒,打印按键按下状态,阻塞IO功能正常。
4.5.6. 实验注意事项¶
应用层运行时,需确保设备节点“/dev/blockio”存在,若提示“Permission denied”,使用sudo获取权限;
驱动卸载前,需先终止对应的应用程序,避免出现“设备正忙”错误。
4.6. 非阻塞IO实验¶
本实验在阻塞IO实验基础上进行修改,增加poll函数适配应用层事件监听,设备树插件完全一致,驱动仅说明修改部分,重复部分不作说明。
本章的示例代码目录为: linux_driver/21_noblockio
4.6.1. 驱动代码详解¶
非阻塞IO实现
read函数增加非阻塞IO支持,通过判断文件描述符的O_NONBLOCK标志, 实现“事件未就绪时立即返回错误,事件就绪时读取数据”的非阻塞逻辑,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | static ssize_t pdrv_led_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
/* 从文件私有数据获取设备结构体 */
struct led_chrdev *led_cdev = filp->private_data;
/* 保存中断状态,保护临界区 */
unsigned long flags;
/* 临时变量,用于存储读取的按键状态 */
int btn_state;
/* 非阻塞模式:无按键按下直接返回-EAGAIN */
if ((filp->f_flags & O_NONBLOCK) && !led_cdev->button_pressed)
return -EAGAIN;
if (!led_cdev->button_pressed) {
/* 阻塞模式:进入阻塞等待,等待被唤醒,然后判断button_pressed是否1 */
if (wait_event_interruptible(led_cdev->wq, led_cdev->button_pressed)) {
/* 被信号中断,返回错误 */
return -ERESTARTSYS;
}
}
/* 自旋锁保护 */
spin_lock_irqsave(&led_cdev->spinlock, flags);
/* 置位为0,准备下一次进入阻塞等待 */
led_cdev->button_pressed = 0;
/* 读取当前按键电平 */
btn_state = led_cdev->button_state;
/* 释放自旋锁 */
spin_unlock_irqrestore(&led_cdev->spinlock, flags);
/* 如果不使用临时变量加锁保护,copy_to_user直接返回led_cdev->button_state,
可能会因为copy_to_user睡眠,如果此时定时器中断触发修改button_state,
数据一致性无法保证 */
/* 将按键状态拷贝到用户空间,应用层读int类型,对应sizeof(int) */
if (copy_to_user(buf, &btn_state, sizeof(int))) {
/* 拷贝失败,返回错误 */
return -EFAULT;
}
/* 返回实际拷贝的字节数 */
return sizeof(int);
}
|
非阻塞IO实现逻辑:
非阻塞模式判断:通过filp->f_flags & O_NONBLOCK判断应用层是否以非阻塞模式打开设备节点;若为非阻塞模式且按键未按下(button_pressed=0),直接返回-EAGAIN错误,告知应用层“无数据可读,可再次尝试”;
兼容阻塞模式:若未设置O_NONBLOCK标志,逻辑与blockio.c一致,进程进入阻塞休眠,直至按键按下被唤醒;
数据交互与标志重置:事件就绪后,将按键状态拷贝至用户空间,重置button_pressed为0,为下一次事件触发做准备。
poll函数
poll函数用于适配应用层poll调用,监听按键事件(POLLIN),实现“应用层无阻塞等待事件”的机制, 避免应用层频繁调用read函数轮询,提升系统效率,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /* 定义字符设备的文件操作结构体 */
static struct file_operations pdrv_led_fops = {
.owner = THIS_MODULE,
.open = pdrv_led_open,
.release = pdrv_led_release,
.write = pdrv_led_write,
.read = pdrv_led_read,
.poll = pdrv_led_poll,
};
static unsigned int pdrv_led_poll(struct file *filp, struct poll_table_struct *wait)
{
struct led_chrdev *led_cdev = filp->private_data;
unsigned int mask = 0;
/* 将等待队列加入poll的等待集合 */
poll_wait(filp, &led_cdev->wq, wait);
/* 自旋锁保护 */
spin_lock_irq(&led_cdev->spinlock);
/* 按键按下时,返回可读事件POLLIN */
if (led_cdev->button_pressed) {
mask |= POLLIN | POLLRDNORM;
}
/* 自旋锁释放 */
spin_unlock_irq(&led_cdev->spinlock);
return mask;
}
|
poll函数逻辑:
poll_wait函数:将应用层进程加入等待队列wq,不阻塞进程,仅完成“等待队列注册”,使进程能够被事件唤醒;
事件判断:判断button_pressed是否为1(按键按下),若为1,设置mask为POLLIN(可读事件);
返回值mask:告知应用层当前设备的事件状态,POLLIN表示有数据可读,无事件则返回0;
4.6.2. 应用代码详解¶
应用程序通过poll调用监听设备事件,实现非阻塞等待,与驱动的poll函数、非阻塞read函数对应,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | #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>
int main(int argc, char *argv[])
{
int fd, ret;
int button_state;
struct pollfd fds[1]; // 定义 pollfd 结构体数组
if(argc != 2) {
printf("Usage: ./blockio_app /dev/blockio\n");
return -1;
}
// O_NONBLOCK非阻塞打开
fd = open(argv[1], O_NONBLOCK);
if(0 > fd) {
printf("ERROR: %s file open failed!\n", argv[1]);
return -1;
}
// 初始化 pollfd 结构体
fds[0].fd = fd; // 指定要监视的文件描述符
fds[0].events = POLLIN; // 指定要监视的事件为可读
for ( ; ; ) {
// 调用 poll 函数等待事件发生
ret = poll(fds, 1, -1); // -1 表示无限等待
if (ret < 0) {
perror("poll");
break;
} else if (ret == 0) {
// 超时,这里不会发生,因为设置了无限等待
printf("Poll timeout\n");
} else {
// 检查是否有可读事件发生
if (fds[0].revents & POLLIN) {
// 读取按键状态
ret = read(fd, &button_state, sizeof(int));
if (ret < 0) {
perror("read");
break;
}
printf("button_state = %d\n", button_state);
}
}
}
close(fd);
return 0;
}
|
逻辑说明:
设备打开:以O_NONBLOCK标志打开设备节点,明确告知驱动以非阻塞模式交互;
poll结构体初始化:struct pollfd结构体用于配置监听参数,fd指定监听的设备文件描述符,events设置为POLLIN(监听可读事件),与驱动poll函数返回的事件类型对应。
poll事件监听:调用poll函数,参数-1表示无限等待,直至有POLLIN事件触发;poll函数会阻塞等待事件,而非应用层主动轮询,既实现非阻塞IO,又提升系统效率。
数据读取:当poll函数检测到POLLIN事件(按键按下),调用read函数读取按键状态;此时button_pressed=1,内核read函数不会返回-EAGAIN,可直接读取数据并打印。
4.6.3. 编译设备树和驱动¶
此部分和阻塞IO实验完全一致不作过多说明。
编译得到设备树插件lubancat-led-overlay.dtb、驱动模块noblockio.ko和应用程序noblockio_app。
4.6.4. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后会发现系统心跳灯默认没有闪烁, 是因为我们使用设备树插件关闭了leds节点,释放了引脚。
使用以下命令加载驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #加载驱动
sudo insmod noblockio.ko
#信息输出如下
[ 235.399876] led platform driver init
[ 235.400457] led platform driver probe
[ 235.400603] major=236, minor=0
#运行应用程序
sudo ./noblockio_app /dev/noblockio
#打开新终端或应用程序后台运行,查看程序CPU占用率
ps aux | grep noblockio_app
#信息输出如下,
root 20981 0.0 0.0 1788 424 ttyFIQ0 S+ 17:21 0:00 ./noblockio_app /dev/noblockio
#20981:进程ID
#第一个0.0:CPU使用率0
#第二个0.0:内存使用率0
|
应用程序循环未加延时但CPU使用率为0,原因是循环中调用了poll(fds, 1, -1),-1表示无限等待,此时进程会挂在等待队列(驱动中的wq)进入可中断阻塞休眠状态,不再占用CPU资源。
使用杜邦线一端连接按键引脚,另一端多次连接和断开连接GND引脚模拟按键按下和松开,信息打印如下:
1 2 3 4 5 6 7 | [ 565.982410] pdrv_led open
[ 570.619617] 按键已按下,LED状态翻转
button_state = 0
[ 572.229723] 按键已按下,LED状态翻转
button_state = 0
[ 573.036543] 按键已按下,LED状态翻转
button_state = 0
|
从打印信息可以看到,按下按键,应用程序被唤醒,打印按键按下状态,非阻塞IO功能正常。
4.6.5. 实验注意事项¶
非阻塞模式验证:应用层必须以O_NONBLOCK标志打开设备节点,否则read函数会默认进入阻塞模式,无法验证非阻塞IO功能;
poll函数使用:应用程序中pollfd结构体的events必须设置为POLLIN,否则无法监听到驱动触发的可读事件;
资源释放:驱动卸载前,需确保应用程序已停止,避免驱动无法正常卸载。