Linux 键盘驱动程序

作者:Andries E. Brouwer

当您按下控制台键盘上的一个键时,相应的字符不会像从串行端口输入一样简单地添加到 tty (通用终端处理) 输入缓冲区中。在内核知道正确的字符是什么之前,需要进行大量的处理。只有在处理之后,处理所有交互式终端设备的通用 tty 代码才能接管。

粗略地说,情况是这样的:键盘产生扫描码,扫描码被组装成键码(每个键一个唯一的代码),然后键码使用内核键映射转换为 tty 输入字符。之后,就像任何其他终端一样,正常的 `stty' 处理就会发生。

首先是扫描码

通常的 PC 键盘能够产生三组扫描码。向端口 0x60 写入 0xf0,然后写入 1、2 或 3 将使键盘进入扫描码模式 1、2 或 3。写入 0xf0,然后写入 0 查询模式,将导致键盘返回扫描码字节 0x43、0x41 或 0x3f。(孩子们,不要在家里尝试。如果您不非常小心,您最终会陷入一种只能通过重启才能解决的情况——而且 control-alt-delete 将无法正确关闭计算机。有关详细信息,请参阅随附的 kbd_cmd.c 列表。)

扫描码模式 2 是默认模式。在这种模式下,按键通常会产生一个值 s,范围为 0x01-0x5f,而相应的按键释放会产生 s+0x80。在扫描码模式 3 中,唯一产生扫描码的按键释放是两个 Shift 键以及左 Ctrl 和 Alt 键;对于所有其他键,仅记录按键。产生的扫描码主要与扫描码模式 2 的扫描码相同。

在扫描码模式 1 中,大多数按键释放产生的值与扫描码模式 2 中的值相同,但对于按键,则存在完全不同的、不相关的值。细节有些混乱。

程序可以通过以下方式请求原始扫描码

ioctl(0, KDSKBMODE, K_RAW);

例如,Xdosemusvgadoomshowkey -s 这样做。默认键码转换模式通过以下方式恢复

ioctl(0, KDSKBMODE, K_XLATE);

有关如何从 shell 提示符退出原始扫描码模式的一些建议,请参阅键盘 FAQ(在 kbd-0.90.tar.gz 中)。(在 shell 提示符下,命令 kbd_mode [-s|-k|-a|-u] 将键盘模式设置为扫描码模式、键码模式、转换(“ASCII”)模式和 Unicode 模式。但是,当键盘处于原始扫描码模式时,很难键入此命令。)

扫描码到键码

如果键和扫描码之间存在一对一的对应关系,生活将会变得轻松。(实际上在扫描码模式 3 中是这样,但这对于 Linux 来说是不够的,因为 X 需要按键和按键释放事件。)

但实际上,单次按键可以产生最多六个扫描码的序列,内核必须解析扫描码流并将其转换为一系列按键和按键释放事件。为此,每个键都提供了一个唯一的键码 k,范围为 1-127,按键 k 会产生键码 k,而释放它会产生键码 k+128。原则上,键码的分配是任意的(并且与 X 使用的键码无关),但目前,对于那些在 0x01-0x58 范围内产生单个扫描码的键,键码等于扫描码。

解析的工作原理是

  • 识别 Pause 键产生的特殊序列 0xe1 0x1d 0x45 0xe1 0x9d 0xc5

  • 丢弃键盘插入的任何虚假的 Shift-down 和 Shift-up 代码,以使内核相信您按下了 Shift 以撤消 NumLock 的效果

  • 识别扫描码对 0xe0 s

  • 识别单个扫描码 s。

由于 s 可以取 127 个值(0 是键盘错误条件,高位表示按下/释放),这意味着解析可能导致 1+127+126=254 个不同的键码。但是,目前键码被限制在 1-127 的范围内,我们需要稍微努力才能使事情适应。(毫无疑问,键码迟早会是整数而不是 7 位量,并且键映射将是稀疏的,但目前我们可以避免这种情况——因为据我所知,没有实际的 PC 键盘超过 127 个键。)因此,有一些小表将键码分配给扫描码对 0xe0 s 或范围在 0x59-0x7f 内的单个扫描码。在默认设置中,一切都适用于当前大多数键盘,但如果您有一些奇怪的键盘,您可以使用 KDSETKEYCODE ioctl 填写这些表中的条目,使内核识别原本无法识别的键;请参阅 setkeycodes(8)。

有两个键是不寻常的,因为它们的键码不是恒定的,而是取决于修饰符。当与任一 Alt 键组合使用时,PrintScrn 键将产生键码 84,否则产生键码 99。当与任一 Ctrl 键组合使用时,Pause 键将产生键码 101,否则产生键码 119。(这有历史原因,但可能会改变,以便为其他目的释放键码 99 和 119。)

目前,没有办法告诉 X 关于奇怪的键(盘)。最简单的解决方案是让 X 使用键码而不是扫描码,这样关于奇怪的键及其产生的扫描码的信息就位于一个地方。

程序可以通过执行以下操作来请求获取键码

ioctl(0, KDSKBMODE,
K_MEDIUMRAW);

例如,showkey 这样做。警告:KDSETKEYCODE ioctl 和 K_MEDIUMRAW 键盘模式的功能细节将来可能会发生变化。

键映射

键码通过在适当的键映射上查找来转换为键符号。有八个可能的修饰符(shift 键),当前活动的修饰符和锁的组合决定了使用的键映射。

因此,发生的事情大致是

int shift_final = shift_state ^ kbd->lockstate;
 ushort *key_map = key_maps[shift_final];
 keysym = key_map[keycode];

这八个修饰符被称为 Shift、AltGr、Control、Alt、ShiftL、ShiftR、CtrlL 和 CtrlR。这些标签没有内在含义,修饰符可以用于任意目的,除了 Shift 修饰符的键映射决定了 CapsLock 的动作(并且 Shift 键部分抑制键盘应用程序模式)。默认情况下,Shift 绑定到两个 Shift 键和 Control 键,Alt 和 AltGr 绑定到左 Alt 键和右 Alt 键。其余四个修饰符在默认内核中未绑定。X 能够区分 ShiftL 和 ShiftR 等。

因此,有 256 个可能的键映射——用于普通符号、Shift+符号、Ctrl+AltL+Shift+符号等。通常,并非所有键映射都会被分配(具有三个以上修饰符的组合相当不寻常),实际上默认内核仅分配 7 个键映射,即普通、Shift、AltR、Ctrl、Ctrl+Shift、AltL 和 Ctrl+AltL 映射。您可以通过使用 loadkeys(1) 填写它们的一些条目来分配更多键映射。

键 # 符号

键符号是 shorts,即它们由两个字节组成。在 Unicode 模式下,这个 short 只是返回的 16 位值——或者,更准确地说,返回的字节字符串是这个 Unicode 字符的 UTF-8 表示。键盘通过以下方式进入 Unicode 模式

ioctl(0, KDSKBMODE, K_UNICODE);

当不在 Unicode 模式下时,高位字节被视为类型,低位字节被视为值,我们执行

type = KTYP(keysym);
 (*key_handler[type])(keysym & 0xff, up_flag);

类型从数组 key_handler 中选择一个函数

static k_hand key_handler[16] = {
     do_self, do_fn, do_spec, do_pad, do_dead,
     do_cons, do_cur, do_shift, do_meta, do_ascii,
     do_lock, do_lowercase, do_ignore, do_ignore,
     do_ignore, do_ignore
 };
  1. do_self,通常用于普通键,仅返回给定值,在可能处理挂起的死音符之后。

  2. do_fn,通常用于功能键,返回字符串 func_table[value]。可以使用 loadkeys(1) 分配字符串。

  3. do_spec 用于特殊操作,不一定与字符输入相关。它执行 spec_fn_table[value]();,其中

    static void_fnp spec_fn_table[] = {
    do_null, enter, show_ptregs, show_mem,
    show_state, send_intr, lastcons, caps_toggle,
    num, hold, scroll_forw, scroll_back, boot_it,
    caps_on, compose, SAK, decr_console,
    incr_console, spawn_console, bare_num
     };
    

    关联的动作(及其默认键绑定)是

    • Return (Enter):返回 CR,如果设置了 VC_CRLF 模式,则也返回 LF。可以通过向控制台发送 ESC [ 20 hESC [ 20 l 来设置/清除 CRLF 模式。

    • Show_Registers (AltR-ScrollLock):打印 CPU 寄存器的内容。

    • Show_Memory (Shift-ScrollLock):打印当前内存使用情况。

    • Show_State (Ctrl-ScrollLock):打印进程树。

    • Break (Ctrl-Break):向当前 tty 发送 Break。

    • Last_Console (Alt-PrintScrn):切换到上次使用的控制台。

    • Caps_Lock (CapsLock):切换 CapsLock 设置。

    • Num_Lock (NumLock):在键盘应用程序模式下:返回 ESC O P;否则,切换 NumLock 设置。可以通过向控制台发送 ESC = 或 ESC > 来设置/清除键盘应用程序模式。(另请参阅下面的 Bare_Num_Lock。)

    • Scroll_Lock (ScrollLock):停止/启动 tty——大致相当于 ^S/^Q

    • Scroll_Forward (Shift-PageDown):向下滚动控制台。

    • Scroll_Backward (Shift-PageUp):向上滚动控制台。这两个功能是通过使用显卡上的内存来实现的,并且仅提供非常有限的回滚功能。此外,当您切换虚拟控制台时,所有回滚信息都会丢失。因此,对于真正的回滚,请使用类似 screen 的程序。

    • Boot (Ctrl-AltL-Del):重启。如果您按下 Ctrl-AltL-Del(或 loadkeys 分配了 keysym Boot 的任何键),则机器会立即重启(不同步),或者向 init 发送 SIGINT。前一种行为是默认行为。默认行为可以由 root 用户使用系统调用 reboot(): 更改;请参阅 ctrlaltdel(8)init(8)

      某些版本的 init 更改了默认值。当 init 接收到 SIGINT 时会发生什么取决于所使用的 init 版本——通常它将由 /etc/inittab 中的 pf(代表 powerfail)条目决定,这意味着您可以运行任意程序。在当前内核中,Ctrl-AltR-Del 不再默认分配给 Boot,只有 Ctrl-AltL-Del 是。

      有时,当 init 在磁盘等待中挂起(并且同步不可能)时,说 ctrlaltdel hard 可能很有用,这可能允许您强制重启而无需断电或按下复位按钮。

    • aps_On (none):设置 CapsLock。

    • ompose (Ctrl-.):启动组合序列。以下两个字符将被组合。这是获得您很少需要的重音字符的好方法。例如,Ctrl-.><,><c 将产生 c-cedilla,而 Ctrl-.&gt:<a><e 将产生丹麦字母 æ。哪些组合组合成什么字符;将显示 dumpkeys(1),loadkeys(1) 将设置组合。

    • SAK (none):安全注意键。这应该杀死与当前 tty 相关的所有进程,并将 tty 重置为已知的默认状态。它尚未完全实现——键盘/控制台重置应该如何处理字体和键映射尚不清楚。最简单的解决方案是向某个受信任的守护程序发送信号,并让它根据需要重置键盘和控制台。通过这种方式,我们获得了与下面的 Spawn_Console 函数密切相关的东西。

    • Decr_Console (AltL-LeftArrow):切换到循环顺序中当前控制台之前的虚拟控制台。

    • Incr_Console (AltL-RightArrow):切换到循环顺序中当前控制台之后的虚拟控制台。

    • Spawn_Console 或 KeyboardSignal (AltL-UpArrow):向指定进程发送指定信号。我使用它来向 init 发出信号,它应该为我创建一个新的虚拟控制台。

    • Bare_Num_Lock (Shift-NumLock):切换 NumLock 设置(与键盘应用程序模式无关)。

    只要 init 和 loadkeys 的新版本尚未发布,您就可以通过使用 loadkeys 并启动程序 spawn_console 来进行尝试

    % loadkeys >> EOF
     alt keycode 103 = 0x0212
     EOF
     % spawn_console &
    

    当然,如果您将此放入 /etc/rc.local 中,您可能希望启动 getty 而不是 bash

  4. do_pad,通常用于小键盘键。在键盘应用程序模式下,这会产生一些三字符字符串 ESC O X(其中 X 取决于键),前提是没有同时按下 Shift 键。否则,当 NumLock 开启时,我们会得到键上打印的符号(0123456789.+ -/* 和 CR)。

    最后,如果 NumLock 未开启,则四个箭头键产生 ESC [ X (其中 X=ABCDp,当不在光标键模式下时,否则为 ESC O X),而其余键被视为功能键,并产生关联的字符串。对于中间键(小键盘-5),我们发现四种可能性

    • 在键盘应用程序模式下(未按 Shift 键),ESC O u

    • 在键盘应用程序模式下,按 Shift 键,未开启 NumLock,ESC O G

    • 否则,未开启 NumLock,ESC [ G

    • 但开启 NumLock,为 5。

    如果您认为这不必要地复杂,我同意。它是 VT100 和 DOS 键盘行为的混乱组合。但是,到目前为止,更改建议遇到了太大的阻力。

  5. do_dead 用于“死键”,它为后面的键提供音符。默认情况下,没有死键。可以定义产生死音符抑音符、锐音符、扬抑符、颚化符或分音符的键。死键如何与后面的键组合使用上面讨论的组合机制指定。

  6. do_cons 用于切换控制台。默认情况下,组合键 (Ctrl-)AltL-Fn 切换到虚拟控制台 n,其中 n 的范围为 1-12,AltR-Fn 切换到控制台 n+12,对于相同的 n

  7. do_cur 处理光标键。根据光标键模式,您可以获得 ESC [ XESC O X (其中 XA、B、CD 之一)。(可以通过向控制台发送 ESC [ ? 1 hESC [ ? 1 l 来设置或清除光标键模式。)

  8. do_shift 维护 shift 状态(修饰键的按下/释放状态)。

  9. do_meta 通常用于与 AltL 组合的普通键。如果键盘处于元模式,这将产生一对 ESC x;否则,将产生 x | 0x80,其中 x 是两种情况下按下的键。(您可以使用小型实用程序 setmetamode(1) 设置或清除元模式。)

  10. do_ascii 用于构造给定的代码:按下 AltL,在小键盘上键入十进制代码,然后释放 AltL。这将产生具有给定代码的字符。在 Unicode 模式下,同样的方法适用于十六进制:按下 AltR,在小键盘上键入十六进制代码,可以使用普通的 a、b、c、d、e 和 f 键,然后释放 AltR。这将产生具有给定代码的 Unicode 符号。

  11. do_lock 切换相应修饰键锁的状态。(回想一下我们上面看到的行:shift_final = shift_state ^ kbd-<lockstate。)因此,如果您的西里尔字母键在与 AltR 的组合下,您可以使用 AltR 与其他键一起仅获得一些西里尔字母符号,但如果您计划键入较长的西里尔字母文本,则应键入 AltGr_Lock。(请注意,我在这里称为 AltR 的右 Alt 键通常被称为 AltGr。)

  12. do_lowercase 用于处理 CapsLock。请注意,CapsLock 与 ShiftLock 不同。使用 ShiftLock,数字 4 将变为美元符号(对于默认键盘布局),但 CapsLock 仅影响小写字母,并将它们变为相应的大写字母。类型 11 等效于类型 0,并添加了符号可能受 CapsLock 影响的信息(并且结果字符是从按下 Shift 键产生的字符)。

如前所述,几乎所有这些都可以通过使用 loadkeys(1) 动态更改。当前状态由 dumpkeys(1) 转储。已知符号的列表由 dumpkeys -l 提供。与各种键关联的键码可以使用 showkey(1) 找到。这些以及许多其他用于键盘和控制台的实用程序可以在 ftp.funet.fi 及其镜像站点上的 kbd-0.90.tar.gz 中找到。

使用 loadkeys

使用 loadkeys 将 BackSpace 键产生的代码从 Delete 更改为 BackSpace

% loadkeys
keycode 14 = BackSpace

将字符串 “emacs\n” 分配给功能键 F12,将 “rm *~\n” 分配给 Shift-F12(键码 88 是使用 showkey 找到的;F66 是一个随机未使用的功能键符号)

% loadkeys
 keycode 88 = F12
 shift keycode 88 = F66
 string F12 = "emacs\n"
 string F66 = "rm *~\n"

创建将 | 和 S 组合成 $ 的组合

% loadkeys
 compose '|' 'S' to '$'
 compose 'S' '|' to '$'

重置为某些默认状态

% loadkeys -d
续篇

在上述处理之后,获得的字符被放入原始 tty 队列中。根据 tty 的模式,它们将被处理并转移到熟 tty 队列。(不要将 stty 知道的原始模式与上面讨论的原始扫描码模式混淆。)最后,应用程序在执行 getchar(); 时将获得它们。

Andries Brouwer,aeb@cwi.nl,在过去的 20 年左右的时间里,将 Unix 用于各种数学、语言和娱乐目的。有些人可能因为 hack 的第一个网络版本而认识他。

加载 Disqus 评论