Skip to content

导引:点亮什么、为什么、设计图

这一章我们要点亮什么

一件最直观的事:敲键盘,屏幕上就出现对应的字符。a 键出 a,Shift+aA,回车换行,退格往回抹。整个链路是:你按下键 → 键盘控制器发 IRQ1 → 我们的 handler 读扫描码、解码、入队 → 主循环从队列里取出来、回显到 013 搭好的 console。

这件小事背后,其实点亮了好几个「第一次」:

这是内核第一个真正的输入设备。之前所有的交互都是单向的——内核往外吐串口、吐屏幕。从这一章起,外面能往里塞东西了。

这是第一个非时钟中断的真 handler。回想 011:我们装了一整套 PIC + IDT,IRQ0(时钟)挂了真 handler,但 IRQ1 到 IRQ15 全是那个「发个 EOI 就把数据丢掉」的 default handler。键盘挂上 IRQ1,是这套中断体系第一次为一个真正的外部设备干活。

它还顺带示范了一个在内核里反复出现的结构:中断里生产、主循环里消费的环形缓冲区。中断 handler 要快、不能阻塞,所以它只负责「把数据收下来塞进队列」;真正怎么处理这些数据(回显、将来交给 shell),是主循环的事。这个「ISR 入队 + 轮询出队」的分工,后面还会在很多地方见到。

为什么现在需要它

先说为什么是现在。013 让屏幕能显示了,可一个只能显示、不能接收输入的系统,离「能用」还差得远。你能让它说话,却没法回应它。键盘是最自然的第一个输入设备——每个 PC 都有,PS/2 接口简单(不需要 USB 那套复杂的枚举),而且它的中断 IRQ1 我们在 011 装中断体系时就已经 remap 好了、给它留了 vector 0x21,只是当时挂的是个空 handler。现在把真 handler 挂上去,水到渠成。

再往前看一步:键盘回显是后面所有用户态交互的预演。再过几个 tag,我们要做一个 shell,shell 的本质就是「读键盘输入 → 解析命令 → 输出结果」。而「读键盘输入」这一步,正是这一章搭好的 Keyboard::poll。所以这一章不仅是「让键盘响」,它还在给 shell 铺最底下那层路:一个能取出解码后按键事件的接口。

还有一笔小账要还。011 的时候,PIT 的 handler 里有一句每秒打印一次 [TICK] uptime: Ns 的输出,当时用它验证时钟在走。可现在键盘要回显了,如果每秒还有一行 [TICK] 刷进来,你敲键的回显就会被它搅得乱七八糟。所以这一章顺手把那句 tick 打印删掉了——它的历史使命(验证时钟)已经完成,现在该让位给真正的交互输出。这是个很典型的「调试代码用完就撤」的小动作。

设计图

整个键盘子系统,核心是一个生产者/消费者的环形缓冲区,夹在「中断」和「主循环」这两个异步的世界之间:

展开代码 (共 29 行)收起代码
text
   按键物理动作


   PS/2 键盘控制器 (i8042)
        │  把扫描码放进输出缓冲, 拉 IRQ1

   ┌─────────────────────────────────────────┐
   │  CPU 收到中断, 跳到 irq1_stub (vector 0x21) │
   │  → keyboard_irq1_handler (C 桥)           │
   │  → Keyboard::irq1_handler(frame):         │  ← 中断上下文, 要快
   │      sc = io_inb(0x60)                    │
   │      解码 make/break + modifier + ASCII    │
   │      enqueue(KeyEvent)  ──────┐           │
   │      PIC::send_eoi(1)         │           │
   └───────────────────────────────┼───────────┘

                          ┌─────────────────┐
                          │  ring buffer     │  ← 64 项, 满则丢
                          │  queue_[64]      │
                          └────────┬─────────┘
                                   ▼  主循环 (进程上下文)
   ┌─────────────────────────────────────────┐
   │  while (1) {                              │
   │      hlt;          // 睡到下一个中断      │
   │      while (Keyboard::poll(ev))           │  ← 慢慢消费
   │          if (ev.pressed && ev.ascii)       │
   │              console.putc(ev.ascii);      │
   │  }                                        │
   └─────────────────────────────────────────┘

PS/2 控制器只有两个端口要记:0x60 是数据口(读它拿扫描码、写它发命令数据),0x64 是状态/命令口(读它看状态、写它发控制器命令)。handler 每次从 0x60 读一个字节,这个字节就是全部的输入。

035_multi_terminal-45-gf25de18 · f25de18 · 2026-08-04