到目前为止,进程只能输出,没有办法接收用户输入。SYS_READ 形同虚设,键盘按了也没反应。
这一章实现 TTY——Unix 对终端设备的最初抽象,让进程能以行为单位从串口读取输入。
TTY 是什么
TTY 原本是 teletypewriter(电传打字机)的缩写。在 Unix 里,它泛指"终端"——一个字符设备,既能接收键盘输入,又能输出文字。
现代 Linux 里的 /dev/tty、/dev/pts/0 都是 TTY 的后代。我们这里用串口 COM1 模拟一个最简单的 TTY。
整体架构
用户键盘输入
↓ 串口硬件触发 IRQ4
isr_handler → tty_recv(c) ← 回显 + 写入 ring buffer
↓ 遇到 '\n'
line_ready = 1
↓ 进程 syscall SYS_READ(fd=0)
tty_read(buf, len) ← 把一行搬到用户 buf
↓
用户程序处理输入
三个关键组件:
- Ring Buffer:固定大小的环形缓冲区,存放还未被读走的字符
- 行规程(line discipline):积累字符,遇
\n才通知"行就绪" - IRQ4 中断处理:从串口读一个字节,交给 TTY
Ring Buffer
环形缓冲区是一个固定数组,加上读指针和写指针:
[ _ _ _ _ h e l l o \n _ _ ]
↑ ↑
rx_read rx_write
rx_len = 6
写字符:rx_buf[rx_write] = c; rx_write = (rx_write + 1) % TTY_BUF_SIZE; rx_len++
读字符:c = rx_buf[rx_read]; rx_read = (rx_read + 1) % TTY_BUF_SIZE; rx_len--
写指针追上读指针就满了,满了直接丢弃新字符。读指针追上写指针就空了。两个指针永远在 [0, TTY_BUF_SIZE) 内循环,不需要移动数据。
串口中断
PC 的 COM1 串口连在 IRQ4 上。接收到一个字符时,PIC 发中断,CPU 跳到中断处理函数。
tty_init() 做两件事:
outb(0x3F8 + 1, 0x01); // COM1 IER:打开接收中断
pic_unmask(4); // PIC:放行 IRQ4
isr_handler 里新增:
} else if (irq == 4) {
char c = (char)isr_inb(0x3F8);
tty_recv(c);
}
从 COM1 数据寄存器(0x3F8)读出字符,交给 tty_recv。
行规程
tty_recv 很简单,三步走:
void tty_recv(char c) {
if (c == '\r') c = '\n'; // 回车统一转换
serial_putchar(c); // 回显:让用户看到自己输入的字符
if (rx_len < TTY_BUF_SIZE) {
rx_buf[rx_write] = c;
rx_write = (rx_write + 1) % TTY_BUF_SIZE;
rx_len++;
}
if (c == '\n') line_ready = 1;
}
tty_read 只在 line_ready 时才复制数据:
int tty_read(void *buf, uint32_t len) {
if (!line_ready) return 0;
// 复制直到遇到 '\n' 或读完 len 个字节
// 复制完后重新检查缓冲区中是否还有下一个 '\n'
}
这就是"行规程"最核心的语义:用户程序以行为单位读取,每次 read 返回一整行(包含 \n)。
系统调用改动
SYS_WRITE:从原来的"直接 serial_print(ptr)“改成"调 tty_write(ptr, len)",新增了长度参数,这样可以输出包含 \0 的二进制数据。
SYS_READ:fd=0 特殊处理,走 tty_read;其他 fd 走原来的 VFS 路径。
case SYS_READ: {
if (rfd == 0) {
rn = tty_read(kbuf, len);
} else {
rn = fd_read(current->fd_table, rfd, kbuf, len);
}
...
}
测试
用户程序(hello.asm):
_start:
; 输出提示
mov rax, SYS_WRITE
lea rdi, [rel msg_prompt]
mov rsi, msg_prompt_len
syscall
.read_loop:
; 自旋等待一行输入
mov rax, SYS_READ
mov rdi, 0 ; fd=0 (stdin/tty)
lea rsi, [rel buf]
mov rdx, 64
syscall
cmp rax, 0
jle .read_loop
; 回显
mov rbx, rax
mov rax, SYS_WRITE
lea rdi, [rel msg_echo]
mov rsi, msg_echo_len
syscall
mov rax, SYS_WRITE
lea rdi, [rel buf]
mov rsi, rbx
syscall
运行效果:
tty> type something (end with Enter):
hello
you typed: hello
用户输入 hello 并回车,每个字符都被立即回显,按下回车后 tty_read 返回,程序打印 “you typed: hello”。
总结
| 组件 | 职责 |
|---|---|
tty_init |
开串口接收中断 + pic_unmask(4) |
tty_recv |
IRQ4 回调:回显 + 写 ring buffer |
tty_read |
syscall 路径:读一行 |
tty_write |
syscall 路径:输出到串口 |
TTY 的核心是中断驱动输入 + 行规程缓冲。字符来了不阻塞进程,进程 read 时也不轮询硬件,而是检查"行就绪"标志。这是异步 I/O 最朴素的形态。
下一章:select/poll,让进程同时等多个 fd。