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.1. 阻塞IO执行流程

阻塞IO的执行流程主要分为“请求发起-进程休眠-IO就绪-进程唤醒-IO完成”五个步骤,具体流程如下:

../_images/subsystem_blockio_noblockio_0.jpg

4.2.2. 非阻塞IO执行流程

非阻塞IO单独使用时,需进程轮询检测IO状态,效率较低,实际开发中通常与poll机制配合,实现高效的IO事件监听,流程如下:

../_images/subsystem_blockio_noblockio_1.jpg

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. 驱动代码详解

核心数据结构定义

核心数据结构定义(位于linux_driver/20_blockio/blockio.c)
 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-未按下,供应用层读取。

驱动初始化

驱动初始化(位于linux_driver/20_blockio/blockio.c)
 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,按键上拉,默认处于高电平。

消抖定时器回调函数

消抖定时器回调函数(位于linux_driver/20_blockio/blockio.c)
 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),进程进入阻塞休眠状态; 按键按下后,被等待队列唤醒,读取按键状态,源码如下:

pdrv_led_read函数(位于linux_driver/20_blockio/blockio.c)
 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实现逻辑:

  1. 阻塞条件判断:若button_pressed为0(按键未按下),调用wait_event_interruptible函数,将进程放入等待队列wq,进入可中断休眠状态,放弃CPU资源;

  2. 唤醒机制:按键按下后,定时器回调函数设置button_pressed为1,并调用wake_up_interruptible唤醒等待队列中的进程,判断button_pressed是否为真;

  3. 数据交互:进程唤醒后,通过copy_to_user将按键状态(btn_state)拷贝到用户空间,供应用层读取;

  4. 重置标志:通过自旋锁保护,将button_pressed置为0,为下一次阻塞等待做准备;

4.5.2. 应用代码详解

应用代码比较简单,仅用来读取按键状态,源码如下:

blockio_app.c(位于linux_driver/20_blockio/blockio_app.c)
 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即可。

Makefile(位于linux_driver/20_blockio/Makefile)
 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标志, 实现“事件未就绪时立即返回错误,事件就绪时读取数据”的非阻塞逻辑,源码如下:

pdrv_led_read函数(位于linux_driver/21_noblockio/noblockio.c)
 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实现逻辑:

  1. 非阻塞模式判断:通过filp->f_flags & O_NONBLOCK判断应用层是否以非阻塞模式打开设备节点;若为非阻塞模式且按键未按下(button_pressed=0),直接返回-EAGAIN错误,告知应用层“无数据可读,可再次尝试”;

  2. 兼容阻塞模式:若未设置O_NONBLOCK标志,逻辑与blockio.c一致,进程进入阻塞休眠,直至按键按下被唤醒;

  3. 数据交互与标志重置:事件就绪后,将按键状态拷贝至用户空间,重置button_pressed为0,为下一次事件触发做准备。

poll函数

poll函数用于适配应用层poll调用,监听按键事件(POLLIN),实现“应用层无阻塞等待事件”的机制, 避免应用层频繁调用read函数轮询,提升系统效率,源码如下:

poll函数(位于linux_driver/21_noblockio/noblockio.c)
 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函数对应,源码如下:

noblockio_app.c(位于linux_driver/21_noblockio/noblockio_app.c)
 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,否则无法监听到驱动触发的可读事件;

  • 资源释放:驱动卸载前,需确保应用程序已停止,避免驱动无法正常卸载。