在 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 中用得不多。
配置步骤 (比较繁琐):
- 注册信号处理函数 (
signal或sigaction)。 - 设置文件的拥有者 (
fcntl(fd, F_SETOWN, getpid())),告诉内核信号发给谁。 - 开启异步通知标志 (
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 | 多路复用 | 让保安看着门口,快递来了叫醒我 | 低 (睡觉) | 快 | ⭐⭐⭐⭐⭐ (最推荐) |
| 异步 | 信号 | 我去打游戏,快递员到了打我电话 | 低 | 最快 | ⭐⭐ (逻辑复杂) |
对于你的学习阶段:
- 先写阻塞 版本,理解
struct input_event。 - 一定要掌握
poll版本,因为这是实现一个真正的嵌入式程序(比如同时处理串口指令和触摸屏操作)的基础。