↓ 跳过正文
  1. Posts/

当数字 1 键失灵之后:从 XWayland 假象到 Linux 内核输入子系统(evdev/uinput)

·4106 字·9 分钟

起因
#

昨晚把常用的 87 配列机械键盘拆开清洗了一遍。装回去之后发现主键盘区的数字 1 键彻底没反应了——按下去手感正常,但系统完全不认。

87 键盘砍掉了右侧小键盘区(Numpad),所以不存在"用小键盘的 1 凑合"这个选项。在 Linux 终端里,没有 1 意味着什么?#!/bin/bash 写不了,逻辑非 ! 打不出来(它是 Shift+1),-j1 传不了参,连 Ctrl+F1 切 TTY 都没戏。一个比 0 大、比 2 小的自然数,就这么从我的输入空间里消失了。

硬件层面短时间修不了,只能走软件改键。问题是拿哪个键来牺牲。

对比了一圈:CapsLock 我已经映射成 Escape 了(Vim 用户标配),左 Ctrl 和左 Alt 是各种快捷键的基石不能动,右 Shift 打大写字母时偶尔要用。最后选定了右 Alt 键(AltGr)——右手大拇指最顺手够到的位置,而且在我的日常工作流里,右 Alt 单独按下的场景几乎为零。

于是目标明确了:把右 Alt 映射成数字 1。

xev 能抓到包,xmodmap 却是哑巴
#

在 X11 下混了这么多年,改键的第一反应就是 xev + xmodmap。

先开 xev 确认右 Alt 的 keycode:

1xev -event keyboard

按下右 Alt,终端立刻打出了一段完整的 X11 事件日志:

1KeyRelease event, serial 28, synthetic NO, window 0x1400001,
2    root 0x350, subw 0x0, time 6230724, (40,33), root:(911,471),
3    state 0x8, keycode 108 (keysym 0xffea, Alt_R), same_screen YES,
4    XLookupString gives 0 bytes: 
5    XFilterEvent returns: False

keycode 108,keysym Alt_R,没问题。直接上 xmodmap:

1xmodmap -e "keycode 108 = 1 exclam 1 exclam"

执行完毕,没有报错。按右 Alt——没有任何反应。

我盯着终端看了几秒。没有 error,没有 warning,就是静默失败了。

第二次尝试——我注意到 xev 日志里的 state 0x8。这个 state 是 X11 的修饰符掩码,0x8 对应 Mod1Mask,说明 X Server 把 Alt_R 绑在了 Mod1 修饰符槽位上。修饰符键有个特殊行为:按下时 X Server 会把它当成快捷键前缀来处理,而不是直接发给应用。如果 Alt_R 仍然挂在 modifier 表里,映射可能被拦截了。

于是先解绑 modifier 再映射:

1xmodmap -e "remove mod1 = Alt_R"
2xmodmap -e "keycode 108 = 1 exclam 1 exclam"

依然没有任何效果。连报错都没有。

到这里我开始怀疑不是 xmodmap 语法的问题了。跑了一句:

1echo $XDG_SESSION_TYPE

输出:

1wayland

我之前那篇桌面工具换代记录里就提到过,主力桌面已经全面切到 Wayland 了。但下意识里,遇到改键这种事还是祭出了 X11 时代的老工具。

问题来了:既然跑的是 Wayland 会话,为什么 xev 能完美抓到键盘事件?如果 X11 的东西在 Wayland 下完全不工作,那 xev 不应该什么都收不到吗?

Wayland 下 XWayland 的沙箱边界
#

这个现象初看很矛盾,但仔细想就说得通了。

xev 是一个传统的 X11 Client。在 Wayland 会话下,它并不是直接跟 Wayland Compositor 对话,而是跑在一个叫 XWayland 的兼容层里。XWayland 本质上是一个特殊的 Wayland Client,同时也是一个"虚拟的 X Server"——它对 xev 这样的 X11 应用假装自己就是传统的 X Server,而自己作为 Wayland Client 从 Compositor 那里接收输入事件。

所以事件的流向是这样的:物理键盘产生硬件中断,内核通过 USB HID 驱动生成输入事件,libinput 读取后交给 Wayland Compositor。当 xev 的窗口获得焦点时,Compositor 把键盘事件转发给 XWayland 进程,XWayland 再用 X11 协议分发给 xev。整条链路走通了,所以 xev 能看到 keycode 108。

但 xmodmap 修改的是什么?它修改的是 XWayland 进程内部维护的那张虚拟键位映射表(Keymap)。在传统 X11 架构下,X Server 是输入的唯一入口,改了 X Server 的键表就等于改了全局输入。但在 Wayland 架构下,Compositor 才是输入事件的唯一权威来源。Compositor 通过 libinput 直接从内核读取硬件事件,然后按 wl_keyboard 协议分发给各个 Wayland Client(包括 XWayland)。

关键点在这里:Compositor 绝不会反过来去读取 XWayland 内部的键表。XWayland 只是 Compositor 的一个普通 Client,它内部的键位映射对 Compositor 来说是不可见的。xmodmap 改了一张没人读的表。

用一句话说:xev 之所以能工作,是因为它在被动接收 Compositor 转发过来的事件;xmodmap 之所以失败,是因为它试图修改一个对输入链路没有任何影响力的沙箱内部状态。

这个架构差异用图来看更直观:

X11 传统输入流 vs Wayland + XWayland 隔离输入流

左栏是传统 X11 架构:键盘事件进入 X Server 后,xmodmap 修改的键表直接作用于 X Server 的事件分发,所有 X Client 都能拿到改键后的结果。右栏是 Wayland 架构:Compositor 通过 libinput 垄断了硬件输入,XWayland 只是众多 Client 之一,它内部的 xmodmap 改键被完全隔离在沙箱里。

这实际上是 Wayland 的安全模型刻意为之的结果。X11 时代,任何一个 X Client 都可以通过 XGrabKey、XSendEvent 等调用全局截获或伪造键盘事件——这也是 keylogger 在 X11 下如此容易实现的原因。Wayland 的设计哲学是:每个 Client 只能看到自己窗口的输入事件,不能监听、拦截或篡改其他 Client 的输入。xmodmap 的全局改键能力,在这个安全模型下被封死了。

下沉到内核:evdev 和 uinput
#

既然 Display Server 层面因为安全隔离封死了全局改键,那只能往更底层找出路。

先理清 Linux 的输入事件链路:当我按下键盘上的一个物理按键,最底层的流程是——USB 控制器触发中断,内核的 USB HID 驱动解析 HID Report,将它翻译成标准的 input_event 结构体(包含 type=EV_KEY、code=KEY_RIGHTALT、value=1/0),然后通过内核的 input 子系统 暴露为 /dev/input/eventX 字符设备。这就是 evdev 接口。

用户态程序(比如 libinput)通过 read() 这个字符设备来消费键盘事件。libinput 读到 KEY_RIGHTALT 后交给 Compositor,Compositor 再分发给应用。

那如果我能在 libinput 读取之前,就把 KEY_RIGHTALT 替换成 KEY_1 呢?

这正是 keyd 的工作原理。它是一个用户态守护进程,做了两件事:

  1. 通过 ioctl(fd, EVIOCGRAB, 1) 对物理键盘的 /dev/input/eventX 执行独占抓取(Exclusive Grab)。一旦 Grab 成功,原始的键盘事件就不会再被其他进程(包括 libinput)读到——keyd 垄断了这个设备的事件流。

  2. keyd 内部有一个状态机,按照配置文件做键位重映射。重映射后的事件通过 /dev/uinput 接口(User-space Input)注入回内核。/dev/uinput 允许用户态程序在内核中注册一个全新的虚拟输入设备(名字叫 keyd virtual keyboard),然后往这个虚拟设备写入 input_event。内核的 input 子系统收到后,会把它当成一个真实键盘的事件来处理——生成新的 /dev/input/eventY,libinput 从这个虚拟设备读到的就是已经改键后的 KEY_1。

整个数据流是一个内核态-用户态-内核态的闭环:

keyd 基于 evdev/uinput 的数据流闭环

这个方案的工程优势很明显:对上层完全透明。不管是 Wayland 原生应用、XWayland 应用,还是 Ctrl+Alt+F3 切过去的纯 TTY 虚拟控制台,看到的都是虚拟键盘发出的 KEY_1。而且 Shift 修饰逻辑也天然继承——按 Shift + 右 Alt 会输出 !(因为内核层面传上去的就是 KEY_1,Shift 修饰在更上层处理)。

这比在 Display Server 层面做改键干净得多。keyd 工作在 libinput 之下、内核 input 子系统之上,是整条输入链路中最早的用户态介入点。

keyd 落地的两个暗坑
#

知道了原理,接下来就是装 keyd、写配置、启动服务。

Ubuntu/Debian 下直接 apt 安装:

1sudo apt install keyd

写了一份最简配置 /etc/keyd/default.conf:

1[main]
2rightalt = 1

重启服务:

1sudo systemctl restart keyd

按右 Alt——还是没反应。

查看 systemd 状态:

1systemctl status keyd
 1● keyd.service - key remapping daemon
 2     Loaded: loaded (/usr/lib/systemd/system/keyd.service; enabled; preset: enabled)
 3     Active: active (running) since Sun 2026-10-18 21:50:09 CST; 17s ago
 4       Docs: man:keyd.rvaiya(1)
 5   Main PID: 88423 (keyd.rvaiya)
 6     CGroup: /system.slice/keyd.service
 7             └─88423 /usr/bin/keyd.rvaiya
 8
 910月 18 21:50:09 pn51 systemd[1]: Started keyd.service - key remapping daemon.
1010月 18 21:50:09 pn51 keyd.rvaiya[88423]: CONFIG: parsing /etc/keyd/default.conf
1110月 18 21:50:09 pn51 keyd.rvaiya[88423]: Starting keyd v2.5.0 ()
1210月 18 21:50:09 pn51 keyd.rvaiya[88423]: DEVICE: ignoring 0fac:1ade:d2b36ae6  (keyd virtual pointer)
1310月 18 21:50:09 pn51 keyd.rvaiya[88423]: DEVICE: ignoring 04d9:4545:9a365209  (USB Keyboard)

服务是 active (running),配置也成功解析了。但两行日志直接说明了问题所在:

1DEVICE: ignoring 04d9:4545:9a365209  (USB Keyboard)

04d9:4545 就是我这把 Holtek USB 键盘的 Vendor:Product ID。keyd 明确打印了 ignoring——它看到了这个设备,但主动选择忽略它。

暗坑一:缺少 [ids] 导致设备被忽略
#

翻了一下 keyd 的文档才搞明白:keyd 的配置解析器要求显式声明一个 [ids] section,指定需要 Grab 的硬件设备 ID。如果配置文件里只有 [main] 而没有 [ids],keyd 出于安全防御策略——防止误锁死鼠标、电源键或其他未知外设——会默认忽略所有物理输入设备。

服务状态显示 active (running) 但不做任何事情,这个行为在系统工程里其实很常见:守护进程的"健康"和"有效"是两码事。进程活着不代表它在干活。

修复很简单,在配置文件顶部加上 [ids] section。用通配符 * 表示抓取所有键盘设备,或者精确绑定 04d9:4545:

1[ids]
2*
3
4[main]
5rightalt = 1

暗坑二:keyd 的二进制文件叫 keyd.rvaiya
#

改完配置准备用 keyd monitor 调试一下键码:

1sudo keyd monitor
1sudo: 'keyd': command not found

我愣了一下。刚刚 apt install keyd 装的包,systemd 服务也跑起来了,怎么二进制文件找不到?

回头看 systemd 日志里的线索——Main PID: 88423 (keyd.rvaiya) 和 Docs: man:keyd.rvaiya(1)。可执行文件名不是 keyd,而是 keyd.rvaiya。

在 Debian/Ubuntu 的 apt 仓库中,早期已经存在一个叫 onak 的软件包(OpenPGP 密钥服务器),它已经占用了 /usr/bin/keyd 这个文件名。Debian Policy Manual 要求不同软件包不能安装同名文件,所以 keyd 的打包维护者把二进制文件重命名为了 keyd.rvaiya——后缀来自 keyd 原作者的 GitHub ID(Raheman Vaiya, rvaiya)。

所以正确的调试命令是:

1sudo keyd.rvaiya monitor

这种命名空间冲突在 Debian 生态里不罕见,但如果不看 systemd 日志里的 Main PID 行,确实很难想到可执行文件名被改了。

最终配置
#

最终完整的 /etc/keyd/default.conf:

1[ids]
2*
3
4[main]
5rightalt = 1
6altgr = 1

rightalt 和 altgr 写了双保险——不同键盘布局下,右 Alt 可能被识别为 rightalt 或 altgr,两个都映射就不用操心具体是哪个。

重启服务使配置生效:

1sudo systemctl restart keyd
2systemctl status keyd

这次日志里不再有 ignoring,取而代之的是:

1DEVICE: adding 04d9:4545:9a365209  (USB Keyboard)

按下右 Alt,终端输出 1。按 Shift + 右 Alt,输出 !。切到 TTY 测试,同样生效。

本来只是洗坏了一个物理按键,排查链路却从 X11 的修饰符状态机、到 Wayland 的安全隔离模型、到 Linux 内核的 evdev/uinput 虚拟设备接口,最后还撞上了 Debian 软件包命名空间的历史包袱。每一层看起来都工作正常(xev 能抓包、xmodmap 不报错、keyd 服务 active),但实际效果为零——静默失败比显式报错更消耗排查精力,因为你不知道该从哪个环节开始怀疑。

折腾 Linux 系统最让我着迷的地方也正在于此:从一个具体的、琐碎的工程困境出发,一路往下剥,每剥一层都能看到一个设计决策背后的 trade-off——Wayland 用安全隔离换掉了 X11 的全局可控性,keyd 用内核级设备劫持换来了对上层的完全透明。

参考资料
#

相关文章

从倒排索引到 BPE:高性能系统为什么总在避免扫描全集

·2540 字·6 分钟
最近在系统学习 Search 和 Information Retrieval 的知识。在看倒排索引(Inverted Index)的时候,我越看越觉得这个结构非常眼熟。直到我想起前段时间写 CS336 作业里 BPE(Byte-Pair Encoding)分词算法的优化逻辑,我才突然意识到:原来这些看似完全不同领域的系统,底层其实都在共享同一种 pattern。

模型压缩之后,为什么推理反而变慢了:一次 CPU Serving 的性能优化实践

·2828 字·6 分钟
之前我负责过一个推荐系统深度学习 Serving 框架的功能开发与性能优化。 我们的系统主要跑在 CPU 上,以 TensorFlow 为主力 inference engine。随着模型迭代,尤其是 Embedding 层越来越大,模型导出、传输和上线变得非常慢,Serving 节点的内存成本也眼看着往上飙。