Linux 应用层开发入门(二十三)| 异步通知方式读取输入数据

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个核心步骤:

  1. 编写信号处理函数 :当驱动发送SIGIO信号时,内核会触发该函数执行,核心逻辑是读取驱动中的输入数据并处理。
  2. 注册信号处理函数 :通过signal()函数将SIGIO信号与自定义的处理函数绑定,建立 "信号-处理逻辑" 的映射关系。
  3. 打开输入设备节点 :以非阻塞模式打开目标输入设备(如/dev/input/event0),避免读取操作意外阻塞。
  4. 告知驱动进程ID :通过fcntl(fd, F_SETOWN, getpid())将应用进程ID告知驱动,让驱动知道该向哪个进程发信号。
  5. 使能异步通知功能 :通过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 关键注意事项

  1. 非阻塞模式 :打开设备时必须添加O_NONBLOCK,否则信号处理函数中的read可能阻塞,导致主循环卡死;
  2. 权限问题/dev/input/eventX默认仅root可访问,需使用sudo运行程序,或修改设备节点权限;
  3. 信号重入:信号处理函数应尽量简洁,避免复杂逻辑,防止信号嵌套触发;
  4. 资源释放:实际应用中需处理退出信号(如 SIGINT),在退出前关闭文件描述符。

6 总结

  • 异步通知的核心是SIGIO信号驱动,应用无需阻塞等待数据,由驱动主动触发处理逻辑;
  • 应用层实现异步通知需完成 5 个步骤:编写信号处理函数→注册信号→打开设备→告知进程 ID→使能 FASYNC 标志;
  • 输入设备的异步读取需结合input_event结构体解析数据,且必须以非阻塞模式打开设备避免死锁。
相关推荐
A小辣椒19 分钟前
TShark:基础知识
linux
AlfredZhao2 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao17 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈2 天前
Unix 与 Linux 异同小叙
linux·服务器·unix