20251202 - Linux输入子系统支持的操作机制

在 Linux 中,一切皆文件。/dev/input/eventX 也是文件,所以它完美继承了 Linux VFS(虚拟文件系统)提供的所有标准 I/O 操作方式。

对于嵌入式应用(尤其是 GUI 应用或游戏),选择合适的 I/O 方式决定了系统的响应速度和 CPU 占用率。

以下是这四种机制的详细介绍及代码套路:


1. 阻塞 I/O (Blocking I/O) ------ 默认方式

这是最简单、最符合直觉的方式。

  • 机制:

    当你调用 read() 读取输入设备时,如果没有按键按下(没有数据),你的程序会"睡着"(被内核挂起),停在 read 这一行不动。

    直到有按键按下,内核唤醒你的进程,read 函数才返回数据,程序继续往下跑。

  • 应用场景:

    简单的测试程序,或者专门处理输入的线程。

  • 优缺点

    • 优点:代码简单,不占用 CPU(进程在休眠)。
    • 缺点:程序会卡死,无法处理其他任务(除非多线程)。

代码示例:

c 复制代码
// 1. 以默认方式打开(阻塞)
int fd = open("/dev/input/event0", O_RDONLY);

struct input_event ev;

while (1) {
    // 2. 读数据
    // 如果没数据,程序就卡在这里睡觉,CPU占用率为0
    read(fd, &ev, sizeof(ev)); 
    
    // 3. 醒来后打印
    printf("Type: %d, Code: %d, Value: %d\n", ev.type, ev.code, ev.value);
}

2. 非阻塞 I/O (Non-Blocking I/O) ------ 忙轮询

  • 机制:

    当你调用 read() 时,如果有数据就读走;如果没有数据,read 立刻返回一个错误(通常是 -1),并且设置错误码 errno 为 EAGAIN。程序不会睡觉。

  • 如何开启:

    在 open 时加上 O_NONBLOCK 标志。

  • 应用场景:

    极少单独使用。如果放在 while(1) 里死循环读取,会导致 CPU 占用率飙升到 100%(因为你在不停地问"有数据吗?没有。有数据吗?没有...")。通常配合其他逻辑使用。

代码示例:

c 复制代码
// 1. 加上 O_NONBLOCK 标志
int fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK);

struct input_event ev;

while (1) {
    // 2. 尝试读取
    int len = read(fd, &ev, sizeof(ev));
    
    if (len == sizeof(ev)) {
        // 读到了数据,处理...
        printf("Get event!\n");
    } else {
        // 没读到数据,立刻返回了
        // 这里可以做点别的事,比如刷新屏幕动画
        // 但如果不加 sleep,CPU会满载
    }
}

3. I/O 多路复用 (POLL / SELECT) ------ 最常用

这是嵌入式 Linux 开发中最推荐、最主流的方式。

  • 机制:

    你就像雇了一个保安(poll 或 select 函数)。你把所有要监控的文件(触摸屏、网络socket、串口)都交给保安。

    然后你告诉保安:"我要去睡觉了,这些设备里只要有一个有动静,你就把我叫醒。"

  • 底层原理:

    进程调用 poll 进入休眠。当输入子系统产生中断,驱动层会唤醒等待队列,poll 函数返回,告诉你哪个文件有数据了。

  • 应用场景:

    Qt 的事件循环、Android 的 InputReader、任何复杂的 GUI 系统。

代码示例 (使用 poll):

c 复制代码
#include <poll.h>

int fd = open("/dev/input/event0", O_RDONLY); // 注意:这里还是阻塞或非阻塞模式打开都可以,通常非阻塞

struct pollfd fds[1];
fds[0].fd = fd;
fds[0].events = POLLIN; // 只有当"有数据可读"时才叫醒我

while (1) {
    // 1. 调用 poll,超时时间设为 5000ms (5秒)
    // 程序在这里睡觉,直到有事件或者超时
    int ret = poll(fds, 1, 5000);

    if (ret == 0) {
        printf("超时了,5秒内没人按键\n");
    } else if (ret > 0) {
        // 2. 只有当 poll 返回 > 0,才去 read,保证一定能读到,不会阻塞
        if (fds[0].revents & POLLIN) {
            struct input_event ev;
            read(fd, &ev, sizeof(ev));
            printf("按键按下!\n");
        }
    }
}

4. 异步通知 (Asynchronous Notification) ------ 信号驱动

这是一种"反客为主"的机制。类似于软件中断

  • 机制:

    应用程序不需要主动去读,也不需要睡觉等待。而是注册一个信号处理函数(比如处理 SIGIO 信号)。

    应用程序正常干别的事。当驱动层有数据时,内核会给进程发送一个 SIGIO 信号,进程被迫暂停当前工作,跳转到信号处理函数去执行。

  • 应用场景:

    需要对输入极其敏感,或者不想使用多线程/Poll循环的场景。但由于信号处理函数里不能做复杂操作(尤其是不能有阻塞操作),实际上在复杂 GUI 中用得不多。

配置步骤 (比较繁琐):

  1. 注册信号处理函数 (signalsigaction)。
  2. 设置文件的拥有者 (fcntl(fd, F_SETOWN, getpid())),告诉内核信号发给谁。
  3. 开启异步通知标志 (fcntl(fd, F_SETFL, flags | FASYNC)).

代码示例:

c 复制代码
#include <signal.h>
#include <fcntl.h>

int fd;

// 信号处理函数:当有按键时,内核会自动调用这个函数
void my_signal_handler(int signum)
{
    struct input_event ev;
    // 在这里读取数据
    while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
        printf("异步通知:收到按键!code=%d\n", ev.code);
    }
}

int main()
{
    fd = open("/dev/input/event0", O_RDONLY);

    // 1. 注册信号
    signal(SIGIO, my_signal_handler);

    // 2. 设置当前进程为文件的所有者,接收信号
    fcntl(fd, F_SETOWN, getpid());

    // 3. 获取当前标志并添加 FASYNC 标志
    int flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | FASYNC);

    while (1) {
        // 主程序可以做任何事,不需要理会按键
        // 比如打印 '.' 模拟正在处理繁重的任务
        printf(".");
        sleep(1); 
    }
    return 0;
}

总结对比

机制 描述 比喻 CPU 占用 响应速度 推荐指数
阻塞 死等 在门口一直站着等快递,直到快递来 低 (睡觉) ⭐⭐⭐ (简单任务)
非阻塞 轮询 每秒开门看一次快递来了没 极高 (空转) 取决于轮询间隔 ⭐ (不推荐单独用)
POLL 多路复用 让保安看着门口,快递来了叫醒我 低 (睡觉) ⭐⭐⭐⭐⭐ (最推荐)
异步 信号 我去打游戏,快递员到了打我电话 最快 ⭐⭐ (逻辑复杂)

对于你的学习阶段:

  1. 先写阻塞 版本,理解 struct input_event
  2. 一定要掌握 poll 版本,因为这是实现一个真正的嵌入式程序(比如同时处理串口指令和触摸屏操作)的基础。
相关推荐
polarislove02142 小时前
5.7W25Q64 实验(上)-嵌入式铁头山羊STM32笔记
笔记·stm32·嵌入式硬件
d111111111d2 小时前
STM32-HAL库学习,初识HAL库
笔记·stm32·单片机·嵌入式硬件·学习
d111111111d16 小时前
STM32外设基地址与寄存器偏移地址的深度解析
笔记·stm32·单片机·嵌入式硬件·学习
快乐的划水a16 小时前
nanoMODBUS 库
stm32
无聊到发博客的菜鸟16 小时前
使用STM32对SD卡进行性能测试
stm32·单片机·rtos·sd卡·fatfs
许商17 小时前
【stm32】cmake脚本(一)
stm32·单片机·嵌入式硬件
polarislove021417 小时前
8.1 时钟树-嵌入式铁头山羊STM32笔记
笔记·stm32·嵌入式硬件
风行男孩18 小时前
stm32基础学习——OLED显示屏的基本使用
stm32·嵌入式硬件·学习
养一回月亮!18 小时前
FreeRTOS任务延迟:vTaskDelay与vTaskDelayUntil的深度对比
stm32·单片机·嵌入式硬件