
1 异步通知的核心概念
在Linux应用开发中,I/O操作的处理方式分为同步和异步两种:
- 同步I/O:应用程序发起读取/写入请求后,会阻塞等待操作完成,即 "你慢我等你";
- 异步通知:应用程序无需阻塞等待,驱动程序有数据时主动向应用发送信号,应用收到信号后再处理数据,即 "你忙你的,有情况通知我"。
1.1 异步通知的核心要素
异步通知的本质是 "信号驱动的 I/O",核心问题可归纳为7个关键点:
| 核心问题 | 答案 |
|---|---|
| 谁发信号? | 内核驱动程序 |
| 发什么信号? | SIGIO(表示有IO事件待处理) |
| 怎么发信号? | 内核提供专用函数接口 |
| 发给谁? | 目标应用程序(需告知进程ID) |
| 收到信号做什么? | 执行预先注册的信号处理函数 |
| 信号与函数如何关联? | 应用通过signal()注册映射关系 |
| 如何控制通知开关? | 设置文件描述符的FASYNC标志位 |
Linux系统中定义了数十种信号(内核头文件include/uapi/asm-generic/signal.h),异步I/O通知固定使用SIGIO信号,专门用于标识 "输入输出事件就绪"。
2 异步通知的应用层实现步骤
要实现基于异步通知的输入设备数据读取,应用程序需完成5个核心步骤:
- 编写信号处理函数 :当驱动发送
SIGIO信号时,内核会触发该函数执行,核心逻辑是读取驱动中的输入数据并处理。 - 注册信号处理函数 :通过
signal()函数将SIGIO信号与自定义的处理函数绑定,建立 "信号-处理逻辑" 的映射关系。 - 打开输入设备节点 :以非阻塞模式打开目标输入设备(如
/dev/input/event0),避免读取操作意外阻塞。 - 告知驱动进程ID :通过
fcntl(fd, F_SETOWN, getpid())将应用进程ID告知驱动,让驱动知道该向哪个进程发信号。 - 使能异步通知功能 :通过
fcntl获取文件描述符的当前标志位,添加FASYNC标志后重新设置,开启驱动的异步通知能力。
3 输入设备的异步通知实战
输入设备(如键盘、鼠标、触摸屏)在Linux系统中统一以/dev/input/eventX节点呈现,数据格式为input_event结构体,包含事件类型、事件码、事件值三个核心字段。
3.1 核心数据结构说明
cs
// 输入事件结构体(内核头文件linux/input.h定义)
struct input_event {
struct timeval time; // 事件发生时间
__u16 type; // 事件类型(如EV_KEY表示按键、EV_REL表示相对坐标)
__u16 code; // 事件码(如KEY_ENTER表示回车按键)
__s32 value; // 事件值(如1表示按下、0表示松开)
};
3.2 关键 API 说明
| 函数 / 宏 | 作用 |
|---|---|
signal(SIGIO, handler) |
注册SIGIO信号的处理函数 |
fcntl(fd, F_SETOWN, pid) |
设置信号接收进程ID |
fcntl(fd, F_GETFL) |
获取文件描述符的标志位 |
fcntl(fd, F_SETFL, flags) |
设置文件描述符的标志位 |
EVIOCGID |
IOCTL命令,获取输入设备ID信息 |
EVIOCGBIT |
IOCTL命令,获取设备支持的事件类型 |
4 完整代码
以下是基于异步通知读取输入设备数据的完整代码,可直接编译运行:
cs
#include <linux/input.h> // 输入设备相关结构体/宏定义
#include <sys/types.h> // 基本类型定义
#include <sys/stat.h> // 文件状态相关
#include <fcntl.h> // 文件控制(fcntl)相关
#include <sys/ioctl.h> // IOCTL相关
#include <stdio.h> // 标准输入输出
#include <string.h> // 字符串操作
#include <unistd.h> // 系统调用(sleep/getpid等)
#include <signal.h> // 信号相关
// 全局文件描述符,信号处理函数中需访问
int fd;
/**
* @brief SIGIO信号处理函数:读取输入设备事件并打印
* @param sig 接收到的信号值(此处固定为SIGIO)
*/
void my_sig_handler(int sig)
{
struct input_event event; // 输入事件结构体
// 循环读取所有就绪的事件(非阻塞模式,无数据时read返回-1)
while(read(fd, &event, sizeof(event)) == sizeof(event))
{
// 打印事件类型、事件码、事件值
printf("get event: type=0x%x , code=0x%x , value=0x%x\n",
event.type, event.code, event.value);
}
}
/**
* @brief 主函数:初始化异步通知并进入主循环
* @param argc 参数个数
* @param argv 参数列表:argv[1]为输入设备节点(如/dev/input/event0)
* @return 0-成功,-1-失败
*/
/* 运行方式:./05_input_read_fasync /dev/input/event0 */
int main(int argc, char **argv)
{
int err;
int len;
int i;
unsigned char byte;
int bit;
struct input_id id; // 输入设备ID结构体
unsigned int evbit[2]; // 存储设备支持的事件类型位图
unsigned int flags; // 文件描述符标志位
int count = 0; // 主循环计数器
// 事件类型名称映射表(简化版,仅列举常用类型)
char *ev_names[] = {
"EV_SYN", "EV_KEY", "EV_REL", "EV_ABS",
"EV_MSC", "EV_SW", "NULL", "NULL",
"NULL", "NULL", "NULL", "NULL",
"NULL", "NULL", "NULL", "NULL",
"NULL", "EV_LED", "EV_SND", "NULL",
"EV_REP", "EV_FF",
};
// 检查参数个数(必须传入设备节点路径)
if(argc != 2)
{
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
// 1. 注册SIGIO信号处理函数:绑定信号与处理逻辑
signal(SIGIO, my_sig_handler);
// 2. 打开输入设备:O_NONBLOCK非阻塞模式,避免read阻塞
fd = open(argv[1], O_RDWR|O_NONBLOCK);
if(fd < 0)
{
printf("open %s error\n", argv[1]);
return -1;
}
// 3. 获取输入设备ID信息(厂商、产品ID等)
err = ioctl(fd, EVIOCGID, &id);
if(err == 0)
{
printf("===== Device ID Info =====\n");
printf("bustype = 0x%x\n", id.bustype);
printf("vendor = 0x%x\n", id.vendor);
printf("product = 0x%x\n", id.product);
printf("version = 0x%x\n", id.version);
printf("==========================\n");
}
// 4. 获取设备支持的事件类型位图
len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
if(len > 0 && len <= sizeof(evbit))
{
printf("support ev type: ");
// 解析位图,打印支持的事件类型
for(i=0; i<len; i++)
{
byte = ((unsigned char *)evbit)[i];
for(bit=0 ; bit<8; bit++)
{
if(byte & (1<<bit))
{
// 通过映射表打印事件类型名称
printf("%s ", ev_names[i*8+bit]);
}
}
}
printf("\n");
}
// 5. 告知驱动:将SIGIO信号发送给当前进程(getpid()获取当前进程ID)
fcntl(fd, F_SETOWN, getpid());
// 6. 使能异步通知:添加FASYNC标志位
flags = fcntl(fd, F_GETFL); // 获取当前标志位
fcntl(fd, F_SETFL, flags | FASYNC); // 设置新标志位(保留原有,添加FASYNC)
// 主循环:模拟应用的其他业务逻辑
while(1)
{
printf("main loop count = %d\n", count++);
sleep(2); // 每2秒打印一次,证明主循环不阻塞
}
close(fd); // 实际运行中主循环不会退出,此处仅为代码完整性
return 0;
}
4.1 编译与运行
编译命令:
gcc 05_input_read_fasync.c -o 05_input_read_fasync
运行命令(需替换为实际的输入设备节点):
# 查看系统输入设备
ls /dev/input/
# 运行程序(需root权限,否则可能无访问权限)
sudo ./05_input_read_fasync /dev/input/event0
运行效果:
- 主循环每2秒打印一次
main loop count; - 当操作输入设备(如按键盘、移动鼠标)时,会触发
SIGIO信号,立即打印事件详情。
5 关键注意事项
- 非阻塞模式 :打开设备时必须添加
O_NONBLOCK,否则信号处理函数中的read可能阻塞,导致主循环卡死; - 权限问题 :
/dev/input/eventX默认仅root可访问,需使用sudo运行程序,或修改设备节点权限; - 信号重入:信号处理函数应尽量简洁,避免复杂逻辑,防止信号嵌套触发;
- 资源释放:实际应用中需处理退出信号(如 SIGINT),在退出前关闭文件描述符。
6 总结
- 异步通知的核心是
SIGIO信号驱动,应用无需阻塞等待数据,由驱动主动触发处理逻辑; - 应用层实现异步通知需完成 5 个步骤:编写信号处理函数→注册信号→打开设备→告知进程 ID→使能 FASYNC 标志;
- 输入设备的异步读取需结合
input_event结构体解析数据,且必须以非阻塞模式打开设备避免死锁。