上一章 busybox sh 成功显示了 / # 提示符,但 shell 拿不到任何输入——因为 tty_read 是忙轮询的,没有字符就直接返回 0,shell 以为收到 EOF,立刻退出。这一章实现真正的阻塞式 TTY,让 shell 能等待用户输入。
之前的问题
之前的 tty_read 大概是这样:
int tty_read(char *buf, int len) {
// 轮询串口状态寄存器
if (!(inb(0x3F8 + 5) & 1)) return 0; // 没数据就返回 0
*buf = inb(0x3F8);
return 1;
}
这有两个问题:
- 没有输入时返回 0,上层程序(shell)以为是 EOF
- 如果上层在循环里调这个,CPU 100% 占用
正确做法:没有输入时挂起进程,等串口中断来了再唤醒。
串口中断(IRQ4)
COM1 对应 IRQ4,接在 PIC 主片的 IR4 引脚。要用串口中断,需要两步:
1. 在 PIC 上 unmask IRQ4:
// PIC 主片 IMR 寄存器:0 表示开放,1 表示屏蔽
uint8_t mask = inb(0x21);
mask &= ~(1 << 4); // 清除 bit4,开放 IRQ4
outb(0x21, mask);
2. 开启串口的接收中断:
outb(0x3F8 + 1, 0x01); // IER:bit0 = 接收数据可用中断
中断处理函数在 IRQ4 触发时读取接收到的字符:
void irq4_handler(void) {
while (inb(0x3F8 + 5) & 1) { // LSR bit0:数据就绪
char c = inb(0x3F8);
tty_input(c); // 送入行缓冲
}
pic_send_eoi(4);
}
行缓冲(canonical mode)
TTY 的 canonical mode:用户输入不是一个字符一个字符地交给进程,而是按回车键之后才把整行交出去。这样用户可以在按回车前修改输入(退格)。
行缓冲实现:
static char linebuf[256];
static int linebuf_len = 0;
static int line_ready = 0; // 是否有完整行
void tty_input(char c) {
if (c == '\r' || c == '\n') {
linebuf[linebuf_len++] = '\n';
line_ready = 1;
tty_write("\r\n", 2); // 回显换行
} else if (c == 0x7F) { // 退格
if (linebuf_len > 0) {
linebuf_len--;
tty_write("\b \b", 3); // 终端退格:光标左移、空格覆盖、再左移
}
} else if (c == 0x03) { // Ctrl+C
signal_send(fg_pid, SIGINT);
} else if (c == 0x04) { // Ctrl+D(EOF)
line_ready = 1; // 空行,read 返回 0
} else {
linebuf[linebuf_len++] = c;
tty_write(&c, 1); // 回显
}
}
回显(echo)是 TTY 的基本功能:收到字符后立即通过串口写回终端,用户才能看到自己在打什么。
阻塞等待:sti + hlt
tty_read 等待行缓冲就绪的方式:
int tty_read(char *buf, int len) {
// 等待完整行
while (!line_ready) {
sti(); // 开中断
hlt(); // 挂起 CPU,等中断唤醒
cli(); // 恢复关中断(内核态约定)
}
// 取出一行
int n = linebuf_len < len ? linebuf_len : len;
memcpy(buf, linebuf, n);
linebuf_len = 0;
line_ready = 0;
return n;
}
sti 开中断,hlt 让 CPU 停下来等中断。串口 IRQ4 触发后,CPU 从 hlt 中醒来,执行中断处理程序,调用 tty_input 把字符存入行缓冲,EOI 之后返回 hlt 的下一条指令,循环检查 line_ready。
如果按下回车,line_ready = 1,while 循环退出,读取并返回这一行。
这里有个细节:PIT 定时器(100Hz)也会让 hlt 醒来,这是好事——调度器可以趁机切换到其他进程,不会让 CPU 完全空转。
Ctrl+D 的 EOF 语义
Ctrl+D 在 canonical mode 下的含义:刷出当前行缓冲,如果行缓冲为空则返回 0(EOF)。
实现:收到 0x04 时设置 line_ready = 1,读出时如果 n == 0,上层程序收到 read() 返回 0,解读为 EOF。
// tty_read 返回 0 → read() 返回 0 → shell 解读为 EOF,正常退出
最终效果
/ # echo hello
hello
/ # ls
bin etc
/ #
busybox sh 能正常回显输入,退格可用,按回车执行命令,Ctrl+C 发送 SIGINT,Ctrl+D 退出 shell。shell 不再退出了——tty_read 真正挂起,等用户输入。
小结
阻塞式 TTY 的核心是三件事:
串口中断:不再轮询,IRQ4 触发时读字符写入行缓冲。需要在 PIC 上 unmask IRQ4,并开启串口接收中断。
行缓冲:收集字符直到回车,统一交给进程。同时处理退格(\b \b)和特殊键(Ctrl+C、Ctrl+D)。
sti + hlt:内核态等待的标准姿势。开中断,然后挂起;中断来了,CPU 自动醒来继续执行。PIT 中断会打断 hlt,调度器可以趁机运行其他进程,CPU 利用率从 100% 降到接近 0%。