1 input子系统
input 子系统是 Linux 对输入设备提供的统一驱动框架。如按键、键盘、触摸屏和鼠标等输入设备的驱动方式是类似的,当出现按键、触摸等操作时,硬件产生中断,然后 CPU 直接读取引脚电平,或通过 SPI、I2C 等通讯方式从设备的寄存器读取具体的按键值或触摸坐标,然后把这些信息提交给内核。使用 input 子系统 驱动的输入设备可以通过统一的数据结构提交给内核,该数据结构包括输入的时间、类型、代号以及具体的键值或坐标,而内核则通过 /dev/input 目录下的文件接口传递给用户空间。
2 查看输入设备(Shell)
查看设备节点:
bash
# ls /dev/input/
by-path event0 event1 event2 event3 event4 mice mouse0
查看设备详情:
bash
# cat /proc/bus/input/devices
I: Bus=0003 Vendor=046d Product=c092 Version=0111
N: Name="Logitech G102 LIGHTSYNC Gaming Mouse"
P: Phys=usb-fd840000.usb-1/input0
S: Sysfs=/devices/platform/fd840000.usb/usb4/4-1/4-1:1.0/0003:046D:C092.0001/input/input6
U: Uniq=207C31705742
H: Handlers=mouse1 event5 dmcfreq
...
寻找 Name 字段,找到想要控制的设备,记下设备对应的 Handlers (例如 event5),编写程序需要用到。
3 输入读取(C程序)
无论底层硬件是什么,应用层读取到的数据都是统一的结构体 input_event 。
c
struct input_event {
struct timeval time; // 时间戳
__u16 type; // 事件类型 (按键? 相对位移? 绝对位移?)
__u16 code; // 事件代码 (哪个键? X轴还是Y轴?)
__s32 value; // 事件值 (按下/松开? 移动了多少?)
};
完整代码:
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/input.h> // 定义了 input_event 和 EV_KEY 等宏
#include <time.h>
int main(int argc, char **argv)
{
int fd;
struct input_event ev;
// 1. 检查参数
if (argc != 2)
{
printf("Usage: %s <device_path>\n", argv[0]);
printf("Example: %s /dev/input/event0\n", argv[0]);
return -1;
}
// 2. 打开设备节点 (阻塞模式)
fd = open(argv[1], O_RDONLY);
if (fd < 0)
{
perror("open device");
return -1;
}
printf("Reading input events from %s...\n", argv[1]);
// 3. 循环读取事件
while (1)
{
// read 会阻塞,直到有事件发生(按下按键、移动鼠标)
int len = read(fd, &ev, sizeof(ev));
if (len == sizeof(ev))
{
struct tm *tm_info;
char time_fmt[64];
time_t raw_time = ev.time.tv_sec; // 获取秒数
tm_info = localtime(&raw_time); // 转为本地时间结构体
strftime(time_fmt, sizeof(time_fmt), "%Y-%m-%d %H:%M:%S", tm_info); // 格式化为字符串
printf("[%s.%03ld] ", time_fmt, ev.time.tv_usec/1000);
// 解析事件类型
switch (ev.type)
{
case EV_SYN: // 同步事件 (0x00),它像一个句号,用于分隔不同时刻的事件
printf("Type: EV_SYN (Sync)\n");
break;
case EV_KEY: // 按键事件 (0x01)
printf("Type: EV_KEY, Code: %d, Value: %d (%s)\n",
ev.code, ev.value,
ev.value == 1 ? "Press" : (ev.value == 0 ? "Release" : "Repeat"));
break;
case EV_REL: // 相对位移 (0x02) - 鼠标
printf("Type: EV_REL, Code: %d (%s), Value: %d\n",
ev.code,
ev.code == REL_X ? "X" : (ev.code == REL_Y ? "Y" : "Other"),
ev.value);
break;
case EV_ABS: // 绝对位移 (0x03) - 触摸屏
printf("Type: EV_ABS, Code: %d, Value: %d\n", ev.code, ev.value);
break;
default:
printf("Type: 0x%x, Code: %d, Value: %d\n", ev.type, ev.code, ev.value);
}
}
}
close(fd);
return 0;
}
4 输入读取(IO 多路复用)
由于read事件文件操作会阻塞,那么采用这种方式就无法同时检测两个输入设备了,这种时候可以通过select或poll等IO多路复用的操作达成目的。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/input.h>
#include <poll.h>
#include <time.h>
#include <errno.h>
int main(int argc, char **argv)
{
int fd;
struct input_event ev;
struct pollfd fds[1]; // 定义 poll 结构体数组(虽然这里只监听 1 个)
int ret;
if (argc != 2)
{
printf("Usage: %s <device_path>\n", argv[0]);
printf("Example: %s /dev/input/event0\n", argv[0]);
return -1;
}
// 1. 打开设备
fd = open(argv[1], O_RDONLY);
if (fd < 0)
{
perror("open device");
return -1;
}
// 2. 配置 poll 监听结构体
fds[0].fd = fd; // 监听哪个文件?
fds[0].events = POLLIN; // 监听什么事件? POLLIN 表示"有数据可读"
printf("Listening on %s (Timeout: 5s)...\n", argv[1]);
while (1)
{
// 3. 调用 poll (核心)
// 参数: 结构体数组, 数组大小, 超时时间(毫秒)
// 5000ms = 5秒
ret = poll(fds, 1, 5000);
if (ret == 0)
{
// === 情况 A: 超时 (5秒内没人动鼠标) ===
printf("Timeout! (I am still alive, can do other jobs...)\n");
}
else if (ret < 0)
{
// === 情况 B: 出错 ===
perror("poll error");
break;
}
else
{
// === 情况 C: 有事件发生 (ret > 0) ===
// 判断是否是我们监听的那个文件发生了 POLLIN 事件
if (fds[0].revents & POLLIN)
{
// 此时再调用 read 不会阻塞,因为 poll 保证了有数据可读
int len = read(fd, &ev, sizeof(ev));
if (len == sizeof(ev))
{
// 时间格式化
struct tm *tm_info;
char time_fmt[64];
time_t raw_time = ev.time.tv_sec;
tm_info = localtime(&raw_time);
strftime(time_fmt, sizeof(time_fmt), "%H:%M:%S", tm_info);
// 打印关键信息
printf("[%s] Type: %d, Code: %d, Value: %d\n",
time_fmt, ev.type, ev.code, ev.value);
}
}
}
}
close(fd);
return 0;
}