
🎬 渡水无言 :个人主页渡水无言
❄专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》
❄专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏》
❄专栏传送门 :《产品测评专栏》
⭐️流水不争先,争的是滔滔不绝
📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生
| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生
在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连
目录
[3.2、测试 APP 代码](#3.2、测试 APP 代码)
[4.1、编译驱动程序和测试 APP](#4.1、编译驱动程序和测试 APP)
前言
在Linux驱动开发中,IO访问分为阻塞和非阻塞两种方式。上一篇博客我们实现了阻塞IO(应用程序等待设备就绪,期间会休眠,不占用CPU),本篇将基于I.MX6ULL开发板,在阻塞IO基础上改造,实现非阻塞IO,重点讲解非阻塞IO的驱动实现、poll/select函数的使用,以及完整的实验验证流程。
一、前置准备
硬件:I.MX6ULL开发板(KEY0按键)
内核:Linux 4.1.15(适配I.MX6ULL)
工具:VSCode、交叉编译器(arm-linux-gnueabihf-gcc)
前置知识:掌握阻塞IO原理、按键中断驱动、file_operations结构体
二、硬件原理图分析
按键 KEY0 的原理图如下:

图中可以看出,按键 KEY0 是连接到 I.MX6U 的 UART1_CTS 这个 IO 上的,KEY0接了一个 10K 的上拉电阻,因此 KEY0 没有按下的时候 UART1_CTS 应该是高电平,当 KEY0按下以后 UART1_CTS 就是低电平。
三、实验程序编写
通过按键中断触发IO就绪,驱动层支持非阻塞读取,应用层分别用poll和select两种方式实现非阻塞访问,对比阻塞IO,理解非阻塞IO"轮询但低CPU占用"的核心优势。
实验目录结构:
15_noblockio/ # 实验根目录
├── noblockio.c # 非阻塞IO驱动程序
├── noblockioApp.c # 非阻塞IO测试APP
└── Makefile # 编译脚本
3.1、驱动程序编写(noblockio.c)
驱动核心修改点:
在read函数中增加非阻塞判断(通过f_flags & O_NONBLOCK),无数据时返回-EAGAIN
实现poll函数,将等待队列添加到poll_table,检测设备就绪状态
完善file_operations结构体,添加poll成员映射
cpp
1 #include <linux/types.h>
2 #include <linux/kernel.h>
3 #include <linux/delay.h>
4 #include <linux/ide.h>
5 #include <linux/init.h>
6 #include <linux/module.h>
7 #include <linux/errno.h>
8 #include <linux/gpio.h>
9 #include <linux/cdev.h>
10 #include <linux/device.h>
11 #include <linux/of.h>
12 #include <linux/of_address.h>
13 #include <linux/of_gpio.h>
14 #include <linux/semaphore.h>
15 #include <linux/timer.h>
16 #include <linux/of_irq.h>
17 #include <linux/irq.h>
18 #include <linux/wait.h>
19 #include <linux/poll.h>
20 #include <asm/mach/map.h>
21 #include <asm/uaccess.h>
22 #include <asm/io.h>
23 /***************************************************************
24 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
25 文件名 : noblock.c
26 作者 : duan
27 版本 : V1.0
28 描述 : 非阻塞IO访问
29 其他 : 无
30 ***************************************************************/
31 #define IMX6UIRQ_CNT 1 /* 设备号个数 */
32 #define IMX6UIRQ_NAME "noblockio" /* 名字 */
33 #define KEY0VALUE 0X01 /* KEY0按键值 */
34 #define INVAKEY 0XFF /* 无效的按键值 */
35 #define KEY_NUM 1 /* 按键数量 */
36
37 /* 中断IO描述结构体 */
38 struct irq_keydesc {
39 int gpio; /* gpio */
40 int irqnum; /* 中断号 */
41 unsigned char value; /* 按键对应的键值 */
42 char name[10]; /* 名字 */
43 irqreturn_t (*handler)(int, void *); /* 中断服务函数 */
44 };
45
46 /* imx6uirq设备结构体 */
47 struct imx6uirq_dev{
48 dev_t devid; /* 设备号 */
49 struct cdev cdev; /* cdev */
50 struct class *class; /* 类 */
51 struct device *device; /* 设备 */
52 int major; /* 主设备号 */
53 int minor; /* 次设备号 */
54 struct device_node *nd; /* 设备节点 */
55 atomic_t keyvalue; /* 有效的按键键值 */
56 atomic_t releasekey; /* 标记是否完成一次完成的按键,包括按下和释放 */
57 struct timer_list timer;/* 定义一个定时器*/
58 struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键init述数组 */
59 unsigned char curkeynum; /* 当前init按键号 */
60
61 wait_queue_head_t r_wait; /* 读等待队列头 */
62 };
63
64 struct imx6uirq_dev imx6uirq; /* irq设备 */
65
66 /* @description : 中断服务函数,开启定时器
67 * 定时器用于按键消抖。
68 * @param - irq : 中断号
69 * @param - dev_id : 设备结构。
70 * @return : 中断执行结果
71 */
72 static irqreturn_t key0_handler(int irq, void *dev_id)
73 {
74 struct imx6uirq_dev *dev = (struct imx6uirq_dev*)dev_id;
75
76 dev->curkeynum = 0;
77 dev->timer.data = (volatile long)dev_id;
78 mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); /* 10ms定时 */
79 return IRQ_RETVAL(IRQ_HANDLED);
80 }
81
82 /* @description : 定时器服务函数,用于按键消抖,定时器到了以后
83 * 再次读取按键值,如果按键还是处于按下状态就表示按键有效。
84 * @param - arg : 设备结构变量
85 * @return : 无
86 */
87 void timer_function(unsigned long arg)
88 {
89 unsigned char value;
90 unsigned char num;
91 struct irq_keydesc *keydesc;
92 struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
93
94 num = dev->curkeynum;
95 keydesc = &dev->irqkeydesc[num];
96
97 value = gpio_get_value(keydesc->gpio); /* 读取IO值 */
98 if(value == 0){ /* 按下按键 */
99 atomic_set(&dev->keyvalue, keydesc->value);
100 }
101 else{ /* 按键松开 */
102 atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
103 atomic_set(&dev->releasekey, 1); /* 标记松开按键,即完成一次完整的按键过程 */
104 }
105
106 /* 唤醒进程 */
107 if(atomic_read(&dev->releasekey)) { /* 完成一次按键过程 */
108 /* wake_up(&dev->r_wait); */
109 wake_up_interruptible(&dev->r_wait);
110 }
111 }
112
113 /*
114 * @description : 按键IO初始化
115 * @param : 无
116 * @return : 无
117 */
118 static int keyio_init(void)
119 {
120 unsigned char i = 0;
121 char name[10];
122 int ret = 0;
123
124 imx6uirq.nd = of_find_node_by_path("/key");
125 if (imx6uirq.nd== NULL){
126 printk("key node not find!\r\n");
127 return -EINVAL;
128 }
129
130 /* 提取GPIO */
131 for (i = 0; i < KEY_NUM; i++) {
132 imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);
133 if (imx6uirq.irqkeydesc[i].gpio < 0) {
134 printk("can't get key%d\r\n", i);
135 }
136 }
137
138 /* 初始化key所使用的IO,并且设置成中断模式 */
139 for (i = 0; i < KEY_NUM; i++) {
140 memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(name)); /* 缓冲区清零 */
141 sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i); /* 组合名字 */
142 gpio_request(imx6uirq.irqkeydesc[i].gpio, name);
143 gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);
144 imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);
145 #if 0
146 imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
147 #endif
148 }
149
150 /* 申请中断 */
151 imx6uirq.irqkeydesc[0].handler = key0_handler;
152 imx6uirq.irqkeydesc[0].value = KEY0VALUE;
153
154 for (i = 0; i < KEY_NUM; i++) {
155 ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler,
156 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);
157 if(ret < 0){
158 printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);
159 return -EFAULT;
160 }
161 }
162
163 /* 创建定时器 */
164 init_timer(&imx6uirq.timer);
165 imx6uirq.timer.function = timer_function;
166
167 /* 初始化等待队列头 */
168 init_waitqueue_head(&imx6uirq.r_wait);
169 return 0;
170 }
171
172 /*
173 * @description : 打开设备
174 * @param - inode : 传递给驱动的inode
175 * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
176 * 一般在open的时候将private_data指向设备结构体。
177 * @return : 0 成功;其他 失败
178 */
179 static int imx6uirq_open(struct inode *inode, struct file *filp)
180 {
181 filp->private_data = &imx6uirq; /* 设置私有数据 */
182 return 0;
183 }
184
185 /*
186 * @description : 从设备读取数据
187 * @param - filp : 要打开的设备文件(文件描述符)
188 * @param - buf : 返回给用户空间的数据缓冲区
189 * @param - cnt : 要读取的数据长度
190 * @param - offt : 相对于文件首地址的偏移
191 * @return : 读取的字节数,如果为负值,表示读取失败
192 */
193 static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
194 {
195 int ret = 0;
196 unsigned char keyvalue = 0;
197 unsigned char releasekey = 0;
198 struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
199
200 if (filp->f_flags & O_NONBLOCK) { /* 非阻塞访问 */
201 if(atomic_read(&dev->releasekey) == 0) /* 没有按键按下,返回-EAGAIN */
202 return -EAGAIN;
203 } else { /* 阻塞访问 */
204 /* 加入等待队列,等待被唤醒,也就是有按键按下 */
205 ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey));
206 if (ret) {
207 goto wait_error;
208 }
209 }
210
211 keyvalue = atomic_read(&dev->keyvalue);
212 releasekey = atomic_read(&dev->releasekey);
213
214 if (releasekey) { /* 有按键按下 */
215 if (keyvalue & 0x80) {
216 keyvalue &= ~0x80;
217 ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
218 } else {
219 goto data_error;
220 }
221 atomic_set(&dev->releasekey, 0);/* 按下标志清零 */
222 } else {
223 goto data_error;
224 }
225 return 0;
226
227 wait_error:
228 return ret;
229 data_error:
230 return -EINVAL;
231 }
232
233 /*
234 * @description : poll函数,用于处理非阻塞访问
235 * @param - filp : 要打开的设备文件(文件描述符)
236 * @param - wait : 等待列表(poll_table)
237 * @return : 设备或者资源状态,
238 */
239 unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
240 {
241 unsigned int mask = 0;
242 struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
243
244 poll_wait(filp, &dev->r_wait, wait); /* 将等待队列头添加到poll_table中 */
245
246 if(atomic_read(&dev->releasekey)) { /* 按键按下 */
247 mask = POLLIN | POLLRDNORM; /* 返回PLLIN */
248 }
249 return mask;
250 }
251
252 /* 设备操作函数 */
253 static struct file_operations imx6uirq_fops = {
254 .owner = THIS_MODULE,
255 .open = imx6uirq_open,
256 .read = imx6uirq_read,
257 .poll = imx6uirq_poll,
258 };
259
260 /*
261 * @description : 驱动入口函数
262 * @param : 无
263 * @return : 无
264 */
265 static int __init imx6uirq_init(void)
266 {
267 /* 1、构建设备号 */
268 if (imx6uirq.major) {
269 imx6uirq.devid = MKDEV(imx6uirq.major, 0);
270 register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
271 } else {
272 alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
273 imx6uirq.major = MAJOR(imx6uirq.devid);
274 imx6uirq.minor = MINOR(imx6uirq.devid);
275 }
276
277 /* 2、注册字符设备 */
278 cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
279 cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);
280
281 /* 3、创建类 */
282 imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
283 if (IS_ERR(imx6uirq.class)) {
284 return PTR_ERR(imx6uirq.class);
285 }
286
287 /* 4、创建设备 */
288 imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
289 if (IS_ERR(imx6uirq.device)) {
290 return PTR_ERR(imx6uirq.device);
291 }
292
293 /* 5、始化按键 */
294 atomic_set(&imx6uirq.keyvalue, INVAKEY);
295 atomic_set(&imx6uirq.releasekey, 0);
296 keyio_init();
297 return 0;
298 }
299
300 /*
301 * @description : 驱动出口函数
302 * @param : 无
303 * @return : 无
304 */
305 static void __exit imx6uirq_exit(void)
306 {
307 unsigned i = 0;
308 /* 删除定时器 */
309 del_timer_sync(&imx6uirq.timer); /* 删除定时器 */
310
311 /* 释放中断 */
312 for (i = 0; i < KEY_NUM; i++) {
313 free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
314 gpio_free(imx6uirq.irqkeydesc[i].gpio);
315 }
316 cdev_del(&imx6uirq.cdev);
317 unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
318 device_destroy(imx6uirq.class, imx6uirq.devid);
319 class_destroy(imx6uirq.class);
320 }
321
322 module_init(imx6uirq_init);
323 module_exit(imx6uirq_exit);
324 MODULE_LICENSE("duan");
修改设备文件名字为"noblockio",当驱动程序加载成功以后就会在根文件系统中出现一个名为"/dev/noblockio"的文件。
第200~202行,判断是否为非阻塞式读取访问,如果是的话就判断按键是否有效,也就是
判断一下有没有按键按下,如果没有的话就返回-EAGAIN。
第239~250行,imx6uirg_poll 函数就是file_operations 驱动操作集中的poll 函数,当应用程序调用select或者poll函数的时候imx6uirg poll函数就会执行。
第244行调用poll wait 函数将等待队列头添加到poll_table中
第246~248行判断按键是否有效,如果按键有效的话就向应用程序返回POLLIN这个事件,表示有数据可以读取。
第257行,设置file_operations 的poll成员变量为imx6uirg_poll。
3.2、测试 APP 代码
应用层实现两种非阻塞访问方式:poll函数和select函数,可根据需求注释/启用对应代码,核心逻辑是"轮询检测设备就绪,有数据则读取,无数据则超时返回,不阻塞主线程"。
新建名为 noblockioApp.c 测试 APP 文件,然后在其中输入如下所示内容:
cpp
1 #include "stdio.h"
2 #include "unistd.h"
3 #include "sys/types.h"
4 #include "sys/stat.h"
5 #include "fcntl.h"
6 #include "stdlib.h"
7 #include "string.h"
8 #include "poll.h" // poll函数头文件
9 #include "sys/select.h"// select函数头文件
10 #include "sys/time.h"
11
12 /***************************************************************
13 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
14 文件名 : noblockioApp.c
17 描述 : 非阻塞IO测试APP(支持poll/select两种方式)
18 使用方法 :./noblockioApp /dev/noblockio & (后台运行)
20 ***************************************************************/
21
22 int main(int argc, char *argv[])
23 {
24 int fd;
25 int ret = 0;
26 char *filename;
27 struct pollfd fds; // poll函数所需结构体
28 fd_set readfds; // select函数所需文件描述符集
29 struct timeval timeout; // select超时时间
30 unsigned char data; // 存储读取的按键值
31
32 /* 检查参数是否正确 */
33 if (argc != 2) {
34 printf("Error Usage!\r\n");
35 printf("Usage: %s /dev/noblockio\r\n", argv[0]);
36 return -1;
37 }
38
39 filename = argv[1];
40 /* 以非阻塞方式打开设备(O_NONBLOCK标志) */
41 fd = open(filename, O_RDWR | O_NONBLOCK);
42 if (fd < 0) {
43 printf("Can't open file %s\r\n", filename);
44 return -1;
45 }
46
47 /* 方式1:使用poll函数实现非阻塞访问(注释则启用select方式) */
48 #if 1
49 fds.fd = fd; // 要检测的文件描述符
50 fds.events = POLLIN; // 检测"有数据可读取"事件
51
52 while (1) {
53 /* poll函数:参数(文件描述符集,数量,超时时间ms) */
54 ret = poll(&fds, 1, 500); // 超时时间500ms,超时返回0
55 switch (ret) {
56 case 0: // 超时(无按键按下)
57 // 可添加自定义超时处理逻辑(如打印提示)
58 // printf("poll timeout...\r\n");
59 break;
60 case -1: // 错误
61 printf("poll error!\r\n");
62 break;
63 default: // 有数据可读取(按键按下)
64 if (fds.revents & POLLIN) { // 确认是POLLIN事件
65 ret = read(fd, &data, sizeof(data));
66 if (ret < 0) {
67 printf("read error!\r\n");
68 } else {
69 if (data) { // 读取到有效按键值
70 printf("key value = %d\r\n", data);
71 }
72 }
73 }
74 break;
75 }
76 }
77 #endif
78
79 /* 方式2:使用select函数实现非阻塞访问(启用需注释上面的poll代码) */
80 #if 0
81 while (1) {
82 FD_ZERO(&readfds); // 清空文件描述符集
83 FD_SET(fd, &readfds); // 将fd添加到读描述符集
84
85 /* 设置超时时间:500ms(tv_sec=0,tv_usec=500000) */
86 timeout.tv_sec = 0;
87 timeout.tv_usec = 500000;
88
89 /* select函数:参数(最大fd+1,读集,写集,异常集,超时时间) */
90 ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
91 switch (ret) {
92 case 0: // 超时
93 // printf("select timeout...\r\n");
94 break;
95 case -1: // 错误
96 printf("select error!\r\n");
97 break;
98 default: // 有数据可读取
99 if (FD_ISSET(fd, &readfds)) { // 确认fd有数据
100 ret = read(fd, &data, sizeof(data));
101 if (ret < 0) {
102 printf("read error!\r\n");
103 } else {
104 if (data) {
105 printf("key value=%d\r\n", data);
106 }
107 }
108 }
109 break;
110 }
111 }
112 #endif
113
114 /* 关闭文件(实际不会执行,因为while循环是死循环) */
115 close(fd);
116 return ret;
117 }
第41行:以O_NONBLOCK标志打开设备,告知驱动"应用层采用非阻塞访问"。
第49~76行:poll方式实现,通过poll函数轮询设备,超时时间500ms,有数据则读取按键值,无数据则超时继续轮询。
第81~111行:select方式实现,通过select函数检测读描述符集,超时时间500ms,逻辑与poll一致,只是API不同。
两种方式二选一,推荐使用poll(代码更简洁),可根据项目需求选择。
四、运行测试
4.1、编译驱动程序和测试 APP
编写 Makefile 文件,本次实验的 Makefile 文件和中断实验之前的实验基本一样,只是将 obj-m 变量的值改为noblockio.o,Makefile 内容如下所示:
cpp
KERNELDIR := /home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2
CURRENT_PATH := $(shell pwd)
obj-m := noblockio.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第 4 行,设置 obj-m 变量的值为noblockio.o。
输入如下命令编译出驱动模块文件:
cpp
make -j32
编译成功以后就会生成一个名为"noblockio.ko"的驱动模块文件。
编译测试 APP
输入如下命令编译测试noblockioApp.c这个测试程序:
cpp
arm-linux-gnueabihf-gcc noblockioApp.c -o noblockioApp
编译成功以后就会生成noblockioApp这个应用程序。
5.2、运行测试
将上一小节编译出来的noblockio.ko 和noblockioApp这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中。
cpp
sudo cp noblockio.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
cpp
sudo cp noblockioApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
进入到目录 lib/modules/4.1.15 中,输入如下命令加载noblockio.ko 驱动模块:
cpp
depmod //第一次加载驱动的时候需要运行此命令
modprobe noblockio.ko //加载驱动
驱动加载成功以后使用如下命令打开noblockioApp这个测试 APP,并且以后台模式运行
cpp
./noblockioApp /dev/noblockio &
按下开发板上的 KEY0 键,终端就会输出按键值,如下图所示:

当按下 KEY0 按键以后noblockioApp 这个测试 APP 就会打印出按键值。输入"top"命令,
查看 noblockioApp这个应用 APP 的 CPU 使用率,如下图所示:

从上图 可以看出,采用非阻塞方式读处理以后, noblockioApp 的 CPU 占用率也低至 0.0%,和之前的 blockioApp 一样,这里的 0.0% 并不是说 noblockioApp 这个应用程序。
不使用 CPU 了,只是因为使用率太小了,而图中只能显示出小数点后一位,因此就显示成了 0.0%。
如果要"杀掉"处于后台运行模式的 noblockioApp 这个应用程序,可以上一期博客 的方法。
总结
本次实验完成了Linux非阻塞IO的驱动开发和测试。
适用场景:非阻塞IO适合需要同时处理多个设备、不允许长时间阻塞的场景(如串口通信、按键检测等)。
