Linux 0.11源码深度解析:kernel/chr_drv/tty_io.c —— 终端I/O的控制中枢与行规约引擎
一、文件概述:用户与内核的交互桥梁
tty_io.c 位于/kernel/chr_drv目录,是Linux 0.11中终端(Terminal/TTY)输入输出的核心实现。在1991年的命令行时代,终端是用户与计算机交互的唯一窗口。这个文件负责管理键盘输入的回显(Echo)、行编辑(Line Discipline)、作业控制(Job Control),以及将字符数据在用户进程、内核缓冲区、串行硬件之间高效流转。
1.1 历史背景:Teletype的遗产
"TTY"一词源自Teletype(电传打字机),即早期Unix系统使用的物理终端设备。Linux 0.11继承了Unix的TTY抽象,将键盘、显示器、串口统一建模为字符设备/dev/tty*。即使在现代Linux中,当你打开终端模拟器(如GNOME Terminal)时,内核仍在沿用这套TTY架构。
1.2 核心职责
tty_io.c扮演着多重关键角色:
缓冲管理层:维护读/写环形缓冲区,平衡低速I/O与高速CPU的速度差异。
行规约层(Line Discipline):处理特殊字符(回车、换行、退格、Ctrl+C),实现基本的行编辑。
作业控制层:管理前台/后台进程组,处理终端信号(SIGINT, SIGTSTP)。
设备抽象层:为上层VFS提供统一的
read/write接口,向下屏蔽键盘/串口硬件差异。
二、关键数据结构:TTY缓冲与控制
2.1 TTY缓冲区结构(tty_queue)
这是TTY驱动的核心数据结构,用于缓存输入输出字符:
struct tty_queue { unsigned long data; /* 缓冲区内存地址 */ unsigned long head; /* 写指针(生产者) */ unsigned long tail; /* 读指针(消费者) */ struct wait_queue * proc_list; /* 等待进程队列 */ unsigned long flags; /* 状态标志 */ };缓冲策略:
输入缓冲:键盘敲击的字符先存入队列,待进程读取。
输出缓冲:进程写入的字符先存入队列,再由中断例程发送到显示器。
2.2 TTY设备表(tty_table)
内核维护一个全局TTY设备数组:
struct tty_struct tty_table[3]; /* 控制台 + 2个串口 */设备分配:
tty_table[0]:控制台(Console),对应
/dev/tty0tty_table[1]:第一个串口(Serial Port 1)
tty_table[2]:第二个串口(Serial Port 2)
2.3 终端控制结构(tty_struct)
struct tty_struct { struct termios termios; /* 终端属性(波特率、模式等) */ int pgrp; /* 前台进程组ID */ int stopped; /* 终端是否停止 */ struct tty_queue *read_q; /* 读队列(原始输入) */ struct tty_queue *write_q; /* 写队列 */ struct tty_queue *secondary; /* 规范模式队列(经过行处理) */ };termios结构:包含控制终端行为的标志位,如回显(ECHO)、规范模式(CANON)、信号使能(ISIG)等。
三、核心函数深度解析
3.1 读系统调用:tty_read()
这是TTY设备的读操作实现,供VFS调用:
int tty_read(struct tty_struct *tty, char *buf, int nr) { int c, n = 0; unsigned long flags; // 1. 检查终端状态 if (tty->stopped) return -EIO; // 2. 循环读取字符 while (n < nr) { // 等待队列中有数据可用 if (EMPTY(tty->secondary)) { if (n > 0) // 已有数据则立即返回(非阻塞) break; // 无数据则睡眠等待 sleep_if_empty(&tty->secondary->proc_list); continue; } // 3. 从队列获取字符 c = GETCH(tty->secondary); // 4. 特殊字符处理(EOF, NL, CR) if (c == EOF_CHAR(tty) && L_CANON(tty)) { // EOF字符(Ctrl+D),返回已读数据 break; } if (c == '\n' && L_CANON(tty)) { // 换行符,结束一行 PUTCH(c, buf++); n++; break; } // 5. 拷贝到用户空间 PUTCH(c, buf++); n++; } // 6. 唤醒可能的写进程 wake_up(&tty->write_q->proc_list); return n; }关键逻辑:在规范模式(Canonical Mode)下,读取以行为单位,遇换行或EOF才返回;在原始模式下,有多少字符读多少字符。
3.2 写系统调用:tty_write()
int tty_write(struct tty_struct *tty, char *buf, int nr) { int n = 0; // 1. 检查终端状态 if (tty->stopped) return -EIO; // 2. 循环写入字符到写队列 while (n < nr) { if (FULL(tty->write_q)) break; // 队列满,返回(后续由中断处理) char c = GETCH(buf++); // 3. 特殊字符转换(如LF转CR-LF) if (c == '\n' && O_POSTNL(tty)) PUTCH('\r', tty->write_q); // 4. 写入队列 PUTCH(c, tty->write_q); n++; } // 5. 启动传输(触发硬件中断发送数据) tty->write(tty); return n; }输出转换:根据终端设置,自动将Unix风格的换行(\n)转换为DOS/终端需要的回车换行(\r\n)。
3.3 行规约核心:con_write()与键盘处理
这是控制台输出的核心,处理VT100转义序列和屏幕显示:
static void con_write(struct tty_struct *tty) { int column = tty->column; int row = tty->row; while (!EMPTY(tty->write_q)) { int c = GETCH(tty->write_q); // 1. 转义序列处理(如光标移动、清屏) if (tty->escape) { handle_escape(c, tty); continue; } // 2. 特殊控制字符 if (c == '\033') { // ESC tty->escape = 1; continue; } if (c == '\b') { // Backspace column--; if (column < 0) column = 0; continue; } if (c == '\t') { // Tab column = (column + 8) & ~7; continue; } // 3. 普通字符显示 if (c >= ' ') { // 计算显存地址 unsigned short *pos = screen + row * SCREEN_COLS + column; *pos = (tty->color << 8) | c; column++; } // 4. 换行与滚动 if (column >= SCREEN_COLS || c == '\n') { column = 0; row++; if (row >= SCREEN_ROWS) { scroll_screen(tty); row = SCREEN_ROWS - 1; } } } // 5. 更新光标位置 move_cursor(row, column); tty->row = row; tty->column = column; }显存操作:直接向物理地址0xB8000写入字符和属性,实现极速显示。
3.4 键盘中断处理:keyboard_interrupt()
键盘输入通过硬件中断进入系统:
void keyboard_interrupt(int irq, struct pt_regs *regs) { unsigned char scancode; // 1. 读取扫描码 scancode = inb(0x60); // 2. 转换扫描码为ASCII(处理Shift、CapsLock等) char c = translate_scancode(scancode); // 3. 处理特殊组合键 if (c == 0) { // 控制键 if (scancode == CTRL_C_PRESSED) { // Ctrl+C:向前台进程组发送SIGINT kill_pg(-tty->pgrp, SIGINT); return; } if (scancode == CTRL_Z_PRESSED) { // Ctrl+Z:暂停前台进程组 kill_pg(-tty->pgrp, SIGTSTP); return; } } // 4. 回显(Echo)处理 if (L_ECHO(tty)) { PUTCH(c, tty->write_q); tty->write(tty); } // 5. 输入放入队列 PUTCH(c, tty->read_q); // 6. 行规约处理(规范模式下的行编辑) if (L_CANON(tty)) { if (c == '\n' || c == EOF_CHAR(tty)) { // 行结束:将数据从读队列复制到辅助队列 copy_to_secondary(tty); // 唤醒等待读取的进程 wake_up(&tty->secondary->proc_list); } } else { // 原始模式:直接唤醒 wake_up(&tty->read_q->proc_list); } }作业控制:Ctrl+C和Ctrl+Z在这里被转换为信号,发送给整个前台进程组。
四、行规约:规范模式 vs 原始模式
4.1 规范模式(Cooked Mode)
这是默认模式,提供丰富的行编辑功能:
行缓冲:输入以行为单位提交给进程。
行编辑:支持退格(Backspace)删除字符。
特殊字符:
Ctrl+C中断进程,Ctrl+D发送EOF。回显:键盘输入同时在屏幕上显示。
4.2 原始模式(Raw Mode)
用于编辑器(如vi)等需要精细控制的场景:
即时输入:字符一到就传递给进程,无需等待回车。
无回显:程序自行控制显示内容。
无特殊处理:所有字符(包括Ctrl+C)都原样传递给程序。
模式切换:通过ioctl()修改termios结构中的标志位实现。
五、作业控制与进程组
5.1 前台进程组
每个TTY有一个前台进程组ID(pgrp):
只有前台进程组的进程可以从终端读取输入。
终端产生的信号(SIGINT, SIGTSTP)只发送给前台进程组。
后台进程组尝试读取终端时会被暂停(SIGTTIN)。
5.2 终端信号
SIGINT (Ctrl+C):中断前台进程组。
SIGTSTP (Ctrl+Z):暂停前台进程组。
SIGQUIT (Ctrl+):产生核心转储并终止。
SIGWINCH:终端窗口大小改变(0.11未实现)。
六、设计哲学与历史局限
6.1 Unix终端架构的经典实现
tty_io.c体现了Unix"模块化分层"的设计:
上层:VFS接口,统一文件操作语义。
中层:行规约,处理数据转换和编辑。
下层:硬件驱动,与具体设备交互。
6.2 局限性
缓冲区大小固定:队列大小固定,不支持动态调整。
编码支持有限:仅支持ASCII,无Unicode支持。
终端类型单一:仅支持基本VT100功能,无颜色、无鼠标支持。
串口支持简单:串口驱动缺乏流控、奇偶校验等高级特性。
6.3 与现代Linux对比
特性 | Linux 0.11 | 现代Linux |
|---|---|---|
终端数量 | 固定3个 | 动态创建(PTY),支持数百个 |
行规约 | 简单实现 | 可插拔行规约模块(N_TTY, N_PPP) |
编码 | ASCII | UTF-8,宽字符支持 |
图形 | 文本模式 | 帧缓冲,终端模拟器 |
作业控制 | 基本支持 | 完整的会话和进程组管理 |
七、总结:命令行的守护者
tty_io.c 是Linux 0.11人机交互的神经中枢。它不只是简单地搬运字符,而是:
编辑者:在规范模式下提供行编辑能力,让命令行输入更友好。
仲裁者:通过作业控制管理前后台进程,实现多任务协作。
翻译官:在ASCII字符、扫描码、转义序列之间进行转换。
同步器:通过缓冲区和等待队列,协调慢速终端与快速CPU的节奏。
虽然现代Linux的TTY子系统已演变为更复杂的PTY和伪终端架构,但其核心逻辑——行规约处理、作业控制、终端属性——依然深深植根于tty_io.c奠定的基础之上。每当我们打开终端窗口输入命令时,都在与这套30年前的架构进行着跨越时空的对话。
