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结构体解析数据,且必须以非阻塞模式打开设备避免死锁。
相关推荐
czxyvX2 小时前
005-Linux基础开发工具
linux
Linux运维技术栈2 小时前
jumpserver堡垒机从 CentOS 7 迁移至 Rocky Linux 9 实战指南
linux·运维·服务器·centos·rocky
wsad05322 小时前
CentOS 7 Minimal 常用软件工具安装指南
linux·运维·centos
开开心心就好3 小时前
轻松加密文件生成exe,无需原程序解密
linux·运维·服务器·windows·pdf·harmonyos·1024程序员节
济6173 小时前
ARM Linux 驱动开发篇----字符设备驱动开发(6)---测试chrdevbase 字符设备驱动开发实验--- Ubuntu20.04
linux·运维·驱动开发
小程同学>o<3 小时前
Linux 应用层开发入门(二十二)| poll_select方式读取输入数据
linux·嵌入式软件·地瓜机器人·atomgit·linux应用层开发·openloong开源社区·开源新春集福
求索小沈3 小时前
linux 录屏软件安装--obs
linux·运维·服务器
承渊政道4 小时前
Linux系统学习【深入剖析Git的原理和使用(上)】
linux·服务器·git·学习
开开心心就好4 小时前
高效U盘容量检测工具,一键辨真假,防假货
linux·运维·服务器·线性代数·安全·抽象代数·1024程序员节