Skip to content

第一百四十九章:输入设备

合道期

涉及内核源码:

林小源来到设备山脉的一处峡谷入口。峡谷两侧的岩壁上嵌满了各种各样的接口——有的像键盘的插槽,有的像鼠标的圆孔,有的像触摸屏的扁平凹槽。

峡谷中央站着一个穿着白色制服的年轻人,他的胸口别着一枚徽章,上面刻着 "input" 三个字母。他的面前有一个巨大的事件分拣台,台面上堆满了各种形状的小令牌。

"欢迎来到输入子系统。"年轻人拿起一枚令牌,令牌上刻着 type、code、value 三个字段,"我是 input 核心。所有用户交互的入口——键盘、鼠标、触摸屏——都通过我到达用户空间。"

他随手把令牌扔进分拣台的一个凹槽里。令牌顺着凹槽滑下去,经过几道分拣门,最终落入了标记着 "EV_KEY" 的收集箱。

"这枚令牌是什么?"林小源问。

"按键事件。type 是 EV_KEY,code 是 KEY_A,value 是 1——表示 A 键被按下了。"input 核心又拿起一枚令牌,"这是鼠标事件——type 是 EV_REL,code 是 REL_X,value 是 120——鼠标向右移动了 120 个单位。"

林小源注意到分拣台旁边有一条传送带,传送带的末端连接着一个文件节点——/dev/input/event0。

"用户空间的程序读取这个文件,就能拿到所有的输入事件,"input 核心说,"不管是键盘、鼠标还是触摸屏,格式都一样:time、type、code、value。四个字段,统一接口。"

林小源点了点头——所有的人机交互,归根结底都要从这个入口进来。


c
/*
 * 输入设备子系统:
 *
 *   硬件设备 → 驱动 → input 核心 → 事件处理 → 用户空间
 *
 * 输入事件:
 *   struct input_event {
 *     struct timeval time;
 *     __u16 type;
 *     __u16 code;
 *     __s32 value;
 *   };
 *
 * 事件类型:
 *   EV_KEY — 按键事件
 *   EV_REL — 相对移动(鼠标)
 *   EV_ABS — 绝对位置(触摸屏)
 *
 * 设备注册:
 *   input_register_device(dev)
 *
 * 事件上报:
 *   input_report_key(dev, KEY_A, 1)
 *   input_sync(dev)
 */

printf("=== 输入设备 — 用户交互的入口 ===\n\n");

printf("输入设备子系统:\n\n");
printf("  硬件设备 → 驱动 → input 核心\n");
printf("  → 事件处理 → 用户空间\n\n");

printf("--- 输入事件 ---\n");
printf("struct input_event {\n");
printf("  struct timeval time;  // 时间戳\n");
printf("  __u16 type;           // 事件类型\n");
printf("  __u16 code;           // 事件代码\n");
printf("  __s32 value;          // 值\n");
printf("};\n\n");

printf("--- 事件类型 ---\n");
printf("EV_KEY: 按键事件\n");
printf("  KEY_A, KEY_B, ...\n");
printf("  BTN_LEFT, BTN_RIGHT\n\n");
printf("EV_REL: 相对移动\n");
printf("  REL_X, REL_Y\n");
printf("  鼠标移动\n\n");
printf("EV_ABS: 绝对位置\n");
printf("  ABS_X, ABS_Y\n");
printf("  触摸屏\n\n");

printf("--- 设备注册 ---\n");
printf("struct input_dev *dev = input_allocate_device();\n");
printf("dev->name = \"my_keyboard\";\n");
printf("set_bit(EV_KEY, dev->evbit);\n");
printf("set_bit(KEY_A, dev->keybit);\n");
printf("input_register_device(dev);\n\n");

printf("--- 事件上报 ---\n");
printf("input_report_key(dev, KEY_A, 1);  // 按下\n");
printf("input_sync(dev);                   // 同步\n");
printf("input_report_key(dev, KEY_A, 0);  // 释放\n");
printf("input_sync(dev);                   // 同步\n\n");

printf("--- /dev/input/ ---\n");
printf("/dev/input/event0 — 键盘\n");
printf("/dev/input/event1 — 鼠标\n");
printf("/dev/input/event2 — 触摸屏\n\n");

printf("--- evtest ---\n");
printf("evtest /dev/input/event0\n");
printf("  显示输入事件\n");

峡谷深处传来一阵急促的敲击声。林小源循声走去,看到一个戴着指套的工匠正飞快地敲打着一块电路板。每敲一下,一枚新的令牌就从电路板上弹出来,滑入分拣台。

"这是键盘驱动,"input 核心走过来解释,"他每检测到一次按键,就调用 input_report_key() 上报一枚令牌。但光上报还不够——他必须在最后调用 input_sync(),告诉接收端:这一批事件结束了,你可以处理了。"

林小源拿起两枚连续的令牌。第一枚是 KEY_A 按下(value=1),第二枚是 KEY_A 释放(value=0)。两枚令牌之间夹着一枚 sync 标记。

"为什么不直接发一个 'A 键按下' 的消息?"林小源问。

input 核心摇头:"因为一次物理动作可能产生多个事件。比如触摸屏——手指按下是一个 ABS_X + ABS_Y + ABS_PRESSURE 的组合,你必须用 sync 把它们标记为一组。没有 sync,接收端就不知道什么时候该处理。"

林小源看到传送带上的令牌按照严格的顺序排列:事件、事件、sync,事件、事件、sync。每个 sync 就像一个句号,把一组相关的事件分隔成完整的消息。

远处,一个触摸屏驱动正在同时上报十个手指的位置——每个手指三个坐标(X、Y、压力),加上一个 sync。三十枚令牌在传送带上排成整齐的一列。

他忽然明白,sync 就是事件通信里的标点符号——没有它,接收端根本不知道一句话在哪里结束。

林小源走出峡谷,来到一片开阔的平原。平原上竖着三根巨大的柱子,柱子上分别刻着 "键盘"、"鼠标"、"触摸屏"。

三根柱子的底部汇聚到同一个基座上——基座上刻着 "input 子系统" 的铭文。

"这就是统一接口的价值,"input 核心站在基座旁说,"键盘是离散的按键事件,鼠标是连续的相对移动,触摸屏是绝对坐标——硬件完全不同,但到了我这里,全部变成同一个结构体:input_event。"

林小源试着从 /dev/input/event0 读取事件。他读到的第一条消息是一个 EV_KEY 事件——KEY_ENTER 被按下。第二条是 EV_REL 事件——鼠标移动了 5 个像素。两种完全不同的硬件,输出的格式一模一样。

"用户空间的程序不需要知道底层是键盘还是鼠标,"input 核心说,"它只管读 /dev/input/eventX,解析 type、code、value 就行了。这就是抽象的力量。"

一个 evtest 工具的化身从远处走来,拿着一个放大镜在传送带旁观察:"你看,每枚令牌都有时间戳——精确到微秒。调试的时候,你可以用我来逐条查看事件,看看硬件到底在发什么。"

林小源接过放大镜,看到令牌背面刻着精确的时间戳。每一枚令牌都记录着它产生的时刻——这让调试变得简单而精确。

键盘、鼠标、触摸屏,硬件千差万别,但输出格式一模一样——这就是抽象的价值。


道藏笔记

内核启示

输入子系统的设计思路很清晰:不管什么硬件,统一输出 input_event 结构体,包含时间戳、类型、代码、值四个字段。EV_KEY 管按键,EV_REL 管鼠标这类相对移动,EV_ABS 管触摸屏这类绝对坐标。驱动通过 input_report_key() 或 input_report_rel() 上报事件,最后必须调用 input_sync() 标记一组事件结束——因为一次物理动作可能产生多个事件,没有 sync 接收端就不知道什么时候该处理。所有事件最终都通过 /dev/input/eventX 到达用户空间,程序只需要解析 type、code、value 就行,完全不用关心底层是什么硬件。


破关试炼

输入设备之试

输入子系统向上报告按键、鼠标等事件时,本章使用的上报函数是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道