暗无天日

=============>DarkSun的个人博客

Linux 输入栈全景解析:从硬件按键到屏幕响应

前言

本文整理自 The Input Stack on Linux — An End-To-End Architecture Overview(作者:Patrick Louis),是对 Linux 输入设备栈的一次系统性学习(很多内容不是很懂,依赖AI补充知识点,不确定正确率)。

当我们按下键盘上的一个键,到屏幕上出现对应的字符,中间经历了怎样的旅程?这篇文章试图回答这个问题。

Linux 的输入处理大致分为三层:

  • **内核层**:硬件、总线、输入核心子系统(input core)
  • **暴露层**:evdev 事件抽象、devtmpfs 设备节点、sysfs 属性、procfs 调试接口
  • **用户空间层**:udev 设备管理、libinput 事件处理库、XKB 键盘映射、X11/Wayland 图形栈

让我们逐层深入。

内核的 Input Core

input core 是什么

input core 是内核中负责管理输入设备及其事件的核心子系统,代码位于 drivers/input/input.c 。它提供了:

  • 设备的分配与注册( input_allocate_device, input_register_device
  • 事件的分发机制:驱动通过 input_event 推送事件,input core 以扇出(fan-out)方式转发给所有注册的 handler
  • 默认的 evdev handler 会将事件标准化后暴露到用户空间的 /dev/input/eventX

简单来说,input core 充当了硬件驱动和用户空间之间的中间层,实现了一种发布-订阅模式。

事件分发流程

驱动
  → input_event()
    → input core (遍历 input_handler_list)
      → handler.events() (如 evdev)
        → handler 遍历其 input_handle 链表
          → 通过字符设备 /dev/input/eventX 暴露给用户空间

[注] handler 和 handle 的命名很容易混淆:

  • input_handler=:事件处理模块(如 evdev、kbd、joydev),注册在 input core 上,实现 =events 回调
  • input_handle=:内核中的一个绑定结构,连接一个 =input_dev 和一个 =input_handler=,表示"这个设备被这个 handler 处理"。一个 handler 可以有多个 handle(对应多个设备)

用户空间的程序通过打开 handler 创建的字符设备文件来读取事件。

关键数据结构

  • struct input_dev: 代表一个输入设备,包含设备名称、能力位图(支持哪些事件类型)等信息
  • struct input_handler: 事件处理接口,核心是 events 回调函数,负责接收并转发事件
  • struct input_handle: 连接一个 input_dev 和一个 input_handler 的桥梁,每个 handle 代表一组设备-处理器的绑定关系

设备拓扑路径

一个 USB 键盘在内核中的路径可能像这样:

/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/0003:046D:C31C.0003/input/input6/event3

从左到右解读:

  1. pci0000:00/0000:00:14.0 — PCI 总线上的 USB 主控制器
  2. usb1/1-1/1-1:1.0 — USB 总线和端口层级
  3. 0003:046D:C31C.0003 — HID 设备节点(总线 0003=USB HID,厂商 046D=Logitech)
  4. input/input6 — input 子系统设备
  5. event3 — evdev 接口,对应的字符设备 /dev/input/event3

内核对象层级

内核通过以下对象类型来管理系统中的设备:

对象 说明
bus 总线,设备连接的基础
device 物理或逻辑设备
driver 与设备关联的驱动程序
class 按功能分类的设备类别(如 input、disk)
subsystem 系统结构视图,我们关心的是 input 子系统

MODALIAS 与热插拔

内核通过 MODALIAS 机制实现驱动自动加载:

  1. 设备接入时,内核构造 MODALIAS 字符串(包含厂商 ID、产品 ID 等)
  2. 通过 uevent(基于 netlink)发送到用户空间
  3. udev 收到后调用 modprobe 加载对应模块

查看设备的 MODALIAS:

cat /sys/class/input/event3/device/id/modalias
# 或通过 udevadm
udevadm info --query=property /dev/input/event3 | grep MODALIAS

sysfs 文件系统

sysfs 是内核对象在用户空间的文件系统映射,通常挂载在 =/sys/=。

对于输入设备,重要的目录包括:

目录 说明
/sys/devices/ 设备的层级结构,反映真实的硬件拓扑
/sys/class/input/ 所有 input 类设备的符号链接
/sys/bus/ 按总线类型组织的设备视图
/sys/module/ 已加载的内核模块信息

实用命令:

# 查看设备的层级关系
udevadm info --tree

# 查看 input 设备的能力
cat /sys/class/input/event3/device/capabilities/ev

# 通过 procfs 查看所有输入设备
cat /proc/bus/input/devices

cat /proc/bus/input/devices 是非常有用的调试命令,它会列出所有输入设备及其绑定的 handler(如 event3、kbd 等)。

HID — 人机接口设备

HID(Human Interface Device)是当今最重要的输入/输出标准协议。大多数新设备(鼠标、键盘、麦克风等)都通过 USB、I2C、Bluetooth 等传输层使用 HID 协议。

HID 的核心思想:设备先通过 Report Descriptor 描述自己的能力(有哪些按钮、轴等),然后按照约定的 Report 格式发送/接收数据。这意味着不需要为每种新设备写专门的驱动。

HID Report Descriptor 示例

一个鼠标的 Report Descriptor 可能描述:

Usage Page (Button)     ; 按钮页面
  Usage Minimum (1)     ; 按钮 1
  Usage Maximum (5)     ; 按钮 5
  Logical Minimum (0)
  Logical Maximum (1)
  Report Count (5)      ; 5 个按钮
  Report Size (1)       ; 每个按钮 1 bit
  Input (Data, Variable, Absolute)  ; 绝对值

  Report Count (1)      ; 填充
  Report Size (3)
  Input (Constant)      ; 常量,被忽略

Usage Page (Generic Desktop)
  Usage (X)
  Usage (Y)
  Logical Minimum (-127)
  Logical Maximum (127)
  Report Size (8)
  Report Count (2)
  Input (Data, Variable, Relative)  ; 相对值

HID 调试工具

# 解码 HID Report Descriptor
hid-decode /sys/bus/hid/devices/0003:046D:C31C.0003/report_descriptor

# 从 hidraw 设备读取原始 HID 数据
cat /dev/hidraw0 | hexdump -C

HID Core 子系统

HID Core 管理设备的生命周期,解析 Report Descriptor,然后将 Report 分发给注册在 HID 总线上的驱动。最重要的是 =hid-input=,它会将 HID Report 翻译为 evdev 事件,并注册 input 设备。

evdev — 事件设备

evdev 是 input core 的默认 handler,它将各种输入设备的事件标准化后暴露给用户空间。

evdev 事件格式

struct input_event {
    struct timeval time;  // 时间戳
    __u16 type;           // 事件类型
    __u16 code;           // 事件代码
    __s32 value;          // 事件值
};

常见事件类型:

类型 说明
EV_KEY 按键和按钮
EV_REL 相对事件(鼠标移动、滚轮)
EV_ABS 绝对事件(触摸屏坐标)
EV_SYN 同步事件,标记一帧事件的结束

一个触摸板的 evdev 帧示例:

type: EV_ABS  code: ABS_X         value: 1234
type: EV_ABS  code: ABS_Y         value: 5678
type: EV_ABS  code: ABS_PRESSURE  value: 50
type: EV_SYN  code: SYN_REPORT    value: 0

设备能力

每个输入设备在注册时会声明自己的能力(capabilities),可通过 sysfs 查看:

ls /sys/class/input/input6/capabilities/
# ev  key  rel  abs  led  sw  ff  msc  snd

更方便的方式是使用 =libinput record=,它会以人类可读的格式展示设备的所有信息。

evdev 调试工具

# 使用 evtest 查看事件
sudo evtest /dev/input/event3

# 使用 libinput record 记录事件
sudo libinput record /dev/input/event3

# 使用 evemu 录制和回放
sudo evemu-record /dev/input/event3
sudo evemu-play /dev/input/event3 < recording.txt

udev 与 hwdb

udev 的职责

udev(systemd-udevd)是用户空间的动态设备管理器,它监听内核的 uevent 并执行相应操作:

  • 根据 MODALIAS 加载内核模块
  • 设置设备节点的访问权限
  • 为设备附加属性
  • 创建符号链接,使设备名称更可预测
  • 通过规则系统在设备插拔时执行自定义操作

udev 规则基础

规则文件存放在:

  • /usr/lib/udev/rules.d/ (系统默认)
  • /etc/udev/rules.d/ (管理员自定义,优先级最高)

一个简单的规则示例:当特定键盘插入或拔出时执行脚本:

ACTION=="add|remove", SUBSYSTEM=="input", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", RUN+="/path/to/script.sh"

常用的匹配键:=KERNEL=, SUBSYSTEM, ACTION, ATTR{...}, ENV{...}

常用的赋值键:=SYMLINK=, ENV{...}, RUN+, MODE, OWNER, GROUP

udevadm 常用命令

# 查看设备的属性层级
udevadm info -a /dev/input/event3

# 实时监控 uevent
udevadm monitor

# 测试规则匹配
udevadm test /dev/input/event3

# 查看所有已加载的规则
systemd-analyze cat-config udev/rules.d

hwdb — 硬件数据库

hwdb 是 udev 的硬件属性查找表,以 .hwdb 文件定义,编译为 hwdb.bin 用于快速检索。

更新 hwdb:

sudo systemd-hwdb update
sudo udevadm trigger

libinput

libinput 是现代 Linux 图形环境的统一输入处理库,被 Xorg(通过驱动)和 Wayland 合成器广泛使用。它封装了 udev 和 evdev,提供设备检测、事件处理、以及各种输入处理的抽象。

为什么需要 libinput

  1. **统一性**:避免 X11 各个 xf86 输入驱动中的重复逻辑
  2. **处理硬件缺陷**:许多设备的固件报告错误的 HID 描述符,libinput 通过 quirks 机制修正
  3. **复杂输入处理**:触摸板多点触控追踪、按钮防抖、手势识别等

libinput 的主要功能

  • 按钮防抖(debouncing)
  • 触摸板软件按钮(clickpad)
  • 触摸板压力检测
  • 手掌和拇指检测
  • 滚动(两指滚动、边缘滚动)
  • 点击行为(tap-to-click)
  • 手势(滑动、捏合)
  • 指针加速度

Seat 的概念

Seat 是与用户会话关联的输入设备集合。大多数机器只有一个 seat,但在多座位场景下一个机器可以有多个用户同时使用不同的输入设备。

seat 通过 udev 的 ENV{ID_SEAT} 属性分配,可通过 loginctl 管理。

libinput 调试工具

# 列出所有设备及其配置
libinput list-devices

# 调试所有事件
libinput debug-events

# 图形化调试触摸板
libinput debug-gui

# 录制和回放设备事件(用于 bug 报告)
libinput record /dev/input/event3
libinput replay recording.yaml

键盘处理细节

扫描码到键码

键盘处理涉及三层映射:

Scancode (硬件原始码) → Keycode (内核事件码) → Keysym (用户空间符号)
  • Scancode=:硬件产生的原始值,如 =0x1E
  • =Keycode=:内核映射后的事件码,如 =KEY_A=(定义在 =input-event-codes.h=)
  • =Keysym=:用户空间的符号,如 "a" 或 "A"

键码映射可通过 hwdb 在运行时修改:

# /etc/udev/hwdb.d/61-custom-keyboard.hwdb
evdev:input:b0003v046DpC31C*
 KEYBOARD_KEY_70039=leftalt   # 将 CapsLock 映射为左 Alt

控制台键盘

在文本控制台中,kbd handler(=drivers/tty/vt/keyboard.c=)负责处理键盘输入,它通过 TTY 和 line discipline 机制工作。相关工具:

# 查看扫描码
showkey --scancodes

# 设置控制台键盘布局
loadkeys us

# 查看当前键盘模式
kbd_mode

# 设置键盘重复率
kbdrate

XKB — 键盘布局系统

XKB(X Keyboard)是处理 keycode 到 keysym 映射的用户空间库和配置系统。虽然名字中有 "X",但它也被 Wayland 和各种图形工具包使用。

XKB 的核心概念:

概念 说明
RMLVO Rules-Model-Layout-Variant-Options,用户友好的配置
KcCGST Keycodes-Compat-Geometry-Symbols-Types,内部组件
Levels 按键的不同层级(如按 Shift 时切换到 Level 2)
Groups 完全切换键盘布局(如切换到日语布局)
Modifiers 修饰键状态(Shift、Ctrl、Alt 等)

[注] XKB 的 keycode = evdev keycode + 8,这是一个历史遗留的偏移量。

RMLVO 到 KcCGST 的解析过程

用户选择 RMLVO(如 layout=us, variant=dvorak),XKB 通过 rules 文件将其解析为 KcCGST 组件,编译成完整的 keymap。

# 查看当前 RMLVO 解析结果
xkbcli compile-keymap --layout us --variant dvorak

# 列出所有可用的布局
xkbcli list

# 交互式调试按键
xkbcli interactive-evdev

# 查看如何输入特定字符
xkbcli how-to-type --keysym "eacute"

Compose 键

XKB 的 Compose 功能允许通过组合键输入特殊字符,配置文件位于:

# 系统 Compose 配置
/usr/share/X11/locale/*/Compose

# 用户自定义
~/.XCompose

示例:=AltGr+'= + eé

指针设备细节

触摸板类型

类型 说明
Clickpad 整个触摸板可按下,无独立按钮(INPUT_PROP_BUTTONPAD)
Forcepad 类似 clickpad,通过压力检测代替物理按钮
Trackpoint ThinkPad 中间的小红帽摇杆

多点触控(MT)

支持多点触控的触摸板通过 evdev 的 slot 和 tracking ID 机制追踪每个手指:

slot 0: tracking_id=1  ABS_MT_POSITION_X=100  ABS_MT_POSITION_Y=200
slot 1: tracking_id=2  ABS_MT_POSITION_X=300  ABS_MT_POSITION_Y=400
slot 0: tracking_id=-1  (手指抬起)

指针加速

libinput 支持三种加速配置:

  • =adaptive=(默认):根据速度动态调整
  • =flat=:1:1 线性映射
  • 自定义配置

游戏手柄

游戏手柄不经过 evdev 和 libinput,而是使用 joydev handler,通过 /dev/input/jsN 暴露 js_event 格式的事件。

# 测试游戏手柄
jstest /dev/input/js0

# 校准
jscal /dev/input/js0

上层图形栈:X11 与 Wayland

X11 输入栈

在 X11 中:

  1. X Server 通过 xf86-input-libinput 驱动使用 libinput
  2. 配置通过 /usr/share/X11/xorg.conf.d/ 中的配置片段
  3. 客户端通过 X 协议接收 XInput 事件
  4. 键盘映射由 X Server 内部维护,通过 XKB 配置
# 列出 X Server 识别的输入设备
xinput --list

# 查看设备属性
xinput --list-props <device_id>

# 监控 X 事件
xev

# 设置 XKB 布局
setxkbmap -layout us -variant dvorak

Wayland 输入栈

在 Wayland 中:

  1. 合成器直接使用 libinput,无中间驱动层
  2. 客户端必须显式注册监听器来接收事件(更安全)
  3. 事件只转发给当前获得焦点的客户端
  4. 输入设备列表在协议中不暴露(合成器内部管理)
  5. 配置方式因合成器而异
# GNOME Wayland 配置示例
gsettings get org.gnome.desktop.input-sources xkb-options
gsettings set org.gnome.desktop.peripherals.touchpad tap-to-click true

# wlroots 合成器通过环境变量设置 XKB
export XKB_DEFAULT_LAYOUT=us
export XKB_DEFAULT_VARIANT=dvorak

Wayland 与 X11 的关键差异

方面 X11 Wayland
事件广播 全局广播(任何客户端可监听) 只发给焦点客户端
输入注入 XTEST 扩展可直接注入事件 需要 libei + portal 权限控制
键盘配置 X Server 统一管理 各合成器自行实现
设备列举 xinput 可列出 协议不暴露,需 libinput list-devices

虚拟输入与自动化

uinput — 用户空间输入模拟

uinput 模块允许在用户空间创建虚拟输入设备:

#include <linux/uinput.h>

// 创建虚拟设备
int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
ioctl(fd, UI_SET_EVBIT, EV_KEY);
ioctl(fd, UI_SET_KEYBIT, KEY_A);

struct uinput_setup setup = {.id.bustype = BUS_USB, .name = "virtual"};
ioctl(fd, UI_DEV_SETUP, &setup);
ioctl(fd, UI_DEV_CREATE, 0);

// 发送事件
struct input_event ev = {.type = EV_KEY, .code = KEY_A, .value = 1};
write(fd, &ev, sizeof(ev));

XTEST — X11 测试扩展

XTEST 允许在 X11 中直接注入键盘和鼠标事件,无需 root 权限:

# 使用 xdotool 模拟输入
xdotool key a
xdotool mousemove 100 100 click 1

libei — Wayland 的模拟输入

Wayland 通过 libei(Emulated Input)库来处理虚拟输入,架构如下:

客户端(libei) → xdg-desktop-portal(dbus) → 合成器(EIS侧) → 内部虚拟设备

这种方式下,合成器可以控制哪些客户端被允许模拟输入,提供更好的安全性。

相关工具:=ydotool=(基于 uinput)、=wtype=(基于 Wayland 虚拟键盘协议)

输入法框架

输入法(Input Method)用于输入键盘上没有的字符(如中文、日文等)。

IMF 与 IME

  • **IMF**(Input Method Framework):输入法框架,负责管理输入法切换、与工具包集成
  • **IME**(Input Method Engine):具体的输入法引擎,负责将按键序列转换为文字

当今两大主流 IMF:

  • **IBus**(GNOME/GTK 默认)
  • **Fcitx5**(KDE/Qt 默认)

切换输入法框架的环境变量:

# GTK 应用
export GTK_IM_MODULE=ibus    # 或 fcitx

# Qt 应用
export QT_IM_MODULE=ibus     # 或 fcitx

# X 应用
export XMODIFIERS=@im=ibus   # 或 @im=fcitx

输入法的处理流程:

按键事件 → XKB(keycode→keysym) → 工具包 IM 模块 → IMF → IME(候选词) → 提交文本

总结

从按下键盘到屏幕响应,事件经过了这样的旅程:

硬件 → 总线驱动 → HID 驱动 → Input Core → evdev handler → /dev/input/eventX
  → udev(权限/属性) → libinput(事件处理) → 合成器/X Server
  → XKB(键盘映射) → 工具包 → 应用程序

每一层都有自己的职责,也都有相应的调试工具。理解这个栈对于排查输入相关问题、自定义输入行为都非常有帮助。

调试工具速查表

层级 工具
硬件/总线 lsusb -v, lspci -vn, hwinfo --short
HID hid-decode, usbhid-dump, hid-tools
evdev evtest, evemu-record, libinput record
sysfs cat /proc/bus/input/devices, udevadm info
udev udevadm monitor, udevadm test, udevadm info
libinput libinput list-devices, libinput debug-events, libinput debug-gui
XKB xkbcli list, xkbcli interactive-evdev, xkbcli how-to-type
X11 xinput, xev, setxkbmap, xdotool
Wayland wev, gsettings, loginctl seat-status
游戏手柄 jstest, jscal
Linux : 输入设备 : 内核 : udev : libinput : XKB