终端与终端 I/O — termios、行纪律与规范模式
终端与终端 I/O — termios、行纪律与规范模式
这一章讲什么: 前面每章都在和终端打交道,却没人解释 它为什么那样行为。为什么程序必须等你敲回车才看得到你 打的字?为什么输密码时字符不显示?为什么 CTRL-C 是中断 而不是个字符?答案全在一个软件组件——行纪律——和 它的配置接口 termios。vi、emacs、top 的本质就是「退出 规范模式,把终端改装成自己想要的样子」。本章教你自己动手改。
1. 顶层全景:一次击键的旅程
键盘 → 设备驱动 → 输入队列(容量 N_TTY_BUF_SIZE=4096)
→ 行纪律:回显(开 ECHO 就抄一份到输出队列)?
编辑字符处理(erase=^?、kill=^U、werase=^W)?
特殊字符(INTR=^C → SIGINT、susp=^Z → SIGTSTP)?
规范模式(ICANON):攒够一行(CR/NL)才交给 read
→ read 缓冲区 → 进程
进程 write → 输出队列 → 行纪律输出处理(opost/onlcr:NL→CR+NL)
→ 设备驱动 → 屏幕
配置接口:stty(命令行看/改)→ termios 结构(程序里改)图说:你敲的每个字都先过行纪律这一关——它决定什么显示、 什么编辑、什么变成信号、什么时候放行。 结构上分两层: 设备驱动(搬运字符,和 xterm 打交道)+ 行纪律 (tty_ldisc,管处理;输入/输出队列都在它的管辖下)1。
2. 规范模式:为什么非要回车
开场实验 upcopychars:逐字符 read+回显的程序,敲 abcde
屏幕毫无反应,回车才整行蹦出来——C 库缓冲不背锅(用的
write/read),是终端攒行。好处:行编辑(abcde 退两格
再 xyz → 程序收到 abcxyz);echo 由内核做(SSH 卡顿时
打字不显示,就是远端的回显慢了);特殊字符直接变信号2。
这套「行是输入基本单位」的默认行为叫规范模式
(canonical mode);vi 那种不要任何处理的模式叫raw3。
两个容量数字:输入队列 4096 字节(内核源码
N_TTY_BUF_SIZE 4096,实测用 16,384 字符粘贴法验证:
超过即丢);MAX_INPUT = 255(POSIX 规定的规范行最大长度,
pathconf(ttyname(0), _PC_MAX_INPUT) 可查)4。
3. stty:仪表盘先认一遍
stty -a 全览,三种行:符号值(cs8=8 位、tab0 等延迟位——
Linux 已忽略)、开关(icanon、echo、前面带 - 为关)、
变量 = 值(rows/columns、intr=^C、erase=^?...)。六个类别:
特殊字符 / 特殊设置(min、time)/ 输入(i 开头)/ 输出(o 开头)/
控制 / 本地5。救命三键:stty sane( sane 恢复默认)、
reset、回显关了盲打时 CTRL-J 当回车6。
两个必做实验:icrnl(输入 CR→NL)——关掉后回车只光标
回行首不换行,得 CTRL-J 才能提交;onlcr(输出 NL→CR+NL)
——关掉后 \n 只剩换行不回行首,cat 的输出一路向右跑
出屏幕7。这一进一出两个开关解释了终端「魔法」的全部。
4. termios:程序里改装终端
一张 man 页管全部:tcgetattr(fd, &t) 读、tcsetattr(fd, TCSANOW, &t) 写。结构五个成员8:
struct termios {
tcflag_t c_iflag; /* 输入模式:ICRNL、IXON… */
tcflag_t c_oflag; /* 输出模式:OPOST、ONLCR… */
tcflag_t c_cflag; /* 控制模式:波特率、校验、流控 */
tcflag_t c_lflag; /* 本地模式:ISIG、ICANON、ECHO… */
cc_t c_cc[NCCS]; /* 特殊字符:VEOF、VINTR、VMIN、VTIME… */
};本地三位总开关(本章的主角):
| 开关 | 关掉后 |
|---|---|
ICANON | 退出规范模式:输入不再攒行,read 立刻拿到字符;VMIN/VTIME 接管 |
ECHO | 回显关闭——密码输入的原理 |
ISIG | CTRL-C/CTRL-\ 不再发信号,只是普通字符 |
非规范模式的 read 语义由 VMIN/VTIME 决定(c_cc 数组里): VMIN=1/VTIME=0 = 每个字符立刻返回(vi 的做法); VMIN=0/VTIME=n = 最多等 n/10 秒,超时也返(「等输入但 别傻等」);VMIN=n/VTIME=0 = 攒够 n 个才返。一个反直觉点: VMIN/VTIME 与 VEOF/VEOL 共用下标,因为规范和非规范 互斥,同一格装谁不冲突9。
辅助件:isatty(fd) 判是否终端、ttyname(fd) 拿设备路径、
cfmakeraw() 一键全脱(raw 模式)、ioctl 的 TIOCGWINSZ
拿窗口行列(resize 时收 SIGWINCH)。第 19 章的 ncurses 就是
把这些封装成库。
改 ECHO 的最小闭环(toggle_echo,十几行):isatty 拦住重定向
→ tcgetattr 读 → 查 c_lflag & ECHO 反着改 →
tcsetattr(TCSANOW) 立刻写回。运行一次关回显,再运行一次
开回来10。它的加长版是模拟登录:输密码前关掉 ECHO、
读完立刻还原——「用完必还原」这条纪律的出处,登录程序世世代代
这么写11。
ioctl:装不进通用模型的,归它
ioctl = input/output control:1979 年 System 7 的万能钥匙,
最初管终端与串口,后来什么设备都往里塞。POSIX 嫌它难用,把
终端控制单独抽成 termios API 收进标准,其余设备控制留在
ioctl、不进标准(glibc 一直带)12。两条承重事实:
- 多数 termios 函数底层最终都是一次 ioctl 调用——你在 第 12 章用 TIOCGWINSZ 拿窗口尺寸,用的就是它;
- 通用 I/O 模型装不下的设备专属杂活归它:换终端字体、 磁带倒带、弹出光盘、网络接口路由表……一个变参函数, 请求码的含义散在各设备自己的 man 页里12。
5. 项目:spl_stty——表驱动的 stty 克隆
解决什么问题: 命令行上的开关名是小写的(icrnl、erase),
termios 里的掩码是大写宏(ICRNL、VERASE)——两张皮;
且 stty 的形态有六种:摘要、size/speed、-a 全览、
[-]switch 开关、delay-const 延迟位、var value 赋值13。
硬写 if-else 链的话,几十个开关没法治。
承重决策:五张同构表。 定义一个 {int mask; char *name;}
的 maskmap 结构,为 c_iflag/c_oflag/c_cflag/c_lflag 各建
一张数组、c_cc 再建一张,每行一对「宏 ↔ 名字」,哨兵
{-1, NULL} 收尾(遍历到 mask 为负即停)。波特率同理,一张
{B9600, "9600"} 的 baudmap——只是 speed_t 无符号,哨兵
改用不可能的 9999;输出延迟位再配一张 dlymask_map13。
同构的代价立即兑现:「打印某个 flagset 的当前值」与「修改
任一开关或变量」各自只需要一套函数——遍历数组、按名字查到
掩码、在 termios 结构副本上置位或清位,tcsetattr(TCSANOW)
写回。数据流一行讲完:命令行词 → 表里查名 → 得掩码 → 改副本
→ 写回驱动14。
如果错,会错在: ① 以为 c_cc 那张表和其余四张一个玩法—— VERASE 存的是字符不是位,改它要动
c_cc[VERASE]的值; ② 改完忘了 tcsetattr——termios 是「读副本、改副本、整包写回」 的协议,只改内存里的结构,驱动纹丝不动14。
6. 判断/边界/可带走
作者的判断: 「终端 I/O 是操作系统里最乱的部分」原话开头 ——历史、BSD/SysV 分裂、缺标准三因;学习路径「先懂驱动结构 再编程」是本章骨架15。spl_stty 的表驱动是本章最实用的 范式:开关名与掩码两张皮,靠同构表 + 哨兵收拢成两套函数14。 边界: 伪终端(pty,伪终端对 master/slave)只在网络登录处点到;波特率/校验/硬件流控 (c_cflag)整组略过;termios 的 TCSADRAIN/TCSAFLUSH 动作 区别未展开;ioctl 的请求码清单散在各设备 man 页,原书只立 了定位没枚举12。可带走的: ① 终端 = 设备驱动 + 行纪律, 队列在行纪律;② 规范模式攒行 + 回显 + 特殊字符;③ 输入 队列 4096/MAX_INPUT 255;④ stty 三形式六类别 + sane; ⑤ icrnl/onlcr 一进一出;⑥ termios 五成员 + 三总开关 + VMIN/VTIME 三组搭配;⑦ cfmakeraw/isatty/TIOCGWINSZ; ⑧ 改终端属性 = 读副本→改副本→tcsetattr 写回,用完必还原; ⑨ ioctl 的定位:termios 装不下的设备专属操作,且 termios 底层大多是它;⑩ 表驱动收拢「名字 ↔ 掩码」两张皮。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 攒行实验与解释 | Chapter 18: Terminals and Terminal I/O | text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:124(搜「nothing happens」) · :149(搜「fundamental unit」) |
| 终端驱动两层结构 | 同上 | :197(搜「two subcomponents」) · :212(搜「input queue」) |
| 队列容量 4096/255 | 同上 | :253(搜「MAX_INPUT」) · :312(搜「N_TTY_BUF_SIZE」) |
| stty 输出三形式/六类 | 同上 | :357(搜「three kinds」) · :444(搜「Categories」) |
| sane/reset 救命 | 同上 | :394(搜「reset」) |
| icrnl/onlcr 实验 | 同上 | :514(搜「icrnl switch」) · :598(搜「onlcr switch」) |
| termios 结构与 API | 同上 | :651(搜「tcgetattr」) · :673(搜「struct termios」) |
| c_lflag 三位开关 | 同上 | :748(搜「Table 18-3」) |
| VMIN/VTIME | 同上 | :802(搜「VMIN」) |
| toggle_echo/模拟登录 | 同上 | :900(搜「toggles the terminal’s echo state」) · :1016(搜「simulated login program」) |
| spl_stty 用法/表驱动 | 同上 | :1032(搜「Writing an spl_stty Command」) · :1051(搜「Print a brief summary」) · :1089(搜「typedef struct _maskmap」) · :1129(搜「mask member is negative」) |
| spl_stty 波特率/打印 | 同上 | :1130(搜「facilitate writing a single」) · :1137(搜「typedef struct _baudmap」) · :1257(搜「Functions That Display Attributes」) |
| ioctl 定位 | 同上 | :1681(搜「The ioctl() System Call」) · :1706(搜「ultimately make a call to ioctl」) |
Footnotes
-
出处:「Chapter 18: Terminals and Terminal I/O」第 194–222 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:197,搜「two subcomponents」) · :212(搜「input queue」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 141–183 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:149,搜「fundamental unit」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 184–192 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:191,搜「raw mode」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 251–313 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:253,搜「MAX_INPUT」) · :312(搜「N_TTY_BUF_SIZE」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 356–508 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:357,搜「three kinds」) · :444(搜「Categories」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 394–398 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:394,搜「reset」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 510–614 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:514,搜「icrnl switch」) · :598(搜「onlcr switch」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 641–691 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:651,搜「tcgetattr」) · :673(搜「struct termios」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 781–817 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:802,搜「VMIN」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 898–928 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:900,搜「toggles the terminal’s echo state」) · :910(搜「Do not redirect standard input」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 930–1031 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:930,搜「A Program that Changes and Restores」) · :953(搜「Set the echo bit in tt」) · :1016(搜「simulated login program」)。 ↩ -
出处:「Chapter 18: Terminals and Terminal I/O」第 1681–1760 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:1681,搜「The ioctl() System Call」) · :1684(搜「input/output control」) · :1689(搜「difficult to use」) · :1706(搜「ultimately make a call to ioctl」)。 ↩ ↩2 ↩3 -
出处:「Chapter 18: Terminals and Terminal I/O」第 1032–1130 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:1032,搜「Writing an spl_stty Command」) · :1051(搜「Print a brief summary」) · :1089(搜「typedef struct _maskmap」) · :1100(搜「const maskmap input_flags」) · :1129(搜「mask member is negative」)。 ↩ ↩2 -
出处:「Chapter 18: Terminals and Terminal I/O」第 1130–1310 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:1130,搜「facilitate writing a single」) · :1137(搜「typedef struct _baudmap」) · :1147(搜「NO_SUCH_BAUD」) · :1257(搜「Functions That Display Attributes」)。 ↩ ↩2 ↩3 -
出处:「Chapter 18: Terminals and Terminal I/O」第 52–79 段(
text/24-ch18-chapter-18-terminals-and-terminal-i-o.txt:52,搜「messiest」)。 ↩