第 62 章 终端 (Terminals)
核心结论
-
终端驱动的两个输入模式:默认 canonical mode 行缓冲 + 行编辑(NL/EOF/ERASE/KILL 等生效);noncanonical mode 按字符返回给应用(vi / less 必备)。
-
termios 结构是终端属性配置中心:
c_iflag/c_oflag/c_cflag/c_lflag四个位掩码 +c_cc[NCCS]特殊字符数组;tcgetattr取出、tcsetattr(fd, optional_actions, &tp)推回(TCSANOW/TCSADRAIN/TCSAFLUSH控制时序)。 -
特殊字符驱动的信号生成:
C(INTR→SIGINT)、\(QUIT→SIGQUIT)、Z(SUSP→SIGTSTP)、D(EOF→read 返回 0)、^U(KILL→擦行);由ISIG/ICANON/IEXTEN控制;调试时常需禁用。 -
noncanonical 的 MIN/TIME 控制 read 何时返回:(MIN=0,TIME=0) polling;(MIN>0,TIME=0) blocking;(MIN=0,TIME>0) 读超时;(MIN>0,TIME>0) interbyte 超时——这是 escape sequence 解码(方向键)的关键。
-
「cooked / cbreak / raw」是历史模式:在 POSIX termios 上要手动组合 flags;TLPI
ttySetCbreak/ttySetRaw用ISIG/ICANON/IEXTEN/ECHO/OPOST等位的清零 / 设置模拟。 -
窗口大小 / 终端识别:SIGWINCH +
ioctl TIOCGWINSZ(winsize 结构);isatty/ttyname判定 fd 是否终端 / 取设备名(/dev/pts/N或/dev/ttyN)。
|
本章主旨
本章聚焦「程序如何与终端驱动对话」——background 分支进程组 / 中断字符 / 行编辑 / 行缓冲/非缓冲/raw 的对应 flags。本书主要关注软件终端(xterm / pty),硬件串行只作概述。读者需理解:(1) 如何用 tcgetattr/tcsetattr 安全修改 termios;(2) canonical vs noncanonical 的语义差别与何时切换;(3) MIN/TIME 何时选哪种组合;(4) 程序退出 / SIGINT / SIGTSTP 时如何还原终端属性(避免留下乱套的 shell)。 |
一、核心概念
本章围绕 6 个核心概念:termios 与 tcgetattr/tcsetattr、special 字符、c_iflag/c_oflag/c_cflag/c_lflag flag、canonical vs noncanonical、MIN/TIME 模式、cooked/cbreak/raw 历史模拟。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
termios 结构 |
|
§62.2; |
特殊字符表 (c_cc) |
INTR(^\) / QUIT(^\) / ERASE(^?)/ KILL(^U)/ EOF(^D)/ START/STOP/XON-XOFF(Q/S)/ SUSP(^Z)/ EOL/EOL2/ LNEXT(^V)/ WERASE/ REPRINT。由 |
§62.4;用 |
c_iflag / c_oflag / c_cflag / c_lflag flag 组 |
c_iflag 控制输入(如 ICRNL 把 CR 映射成 NL);c_oflag 控制输出(OPOST 启用输出处理、ONLCR 把 NL 映射为 CR-NL);c_cflag 控制硬件(CS8=8bit 字长、PARENB 奇偶等);c_lflag 控制本地行为(ICANON/ISIG/ECHO/IEXTEN/TOSTOP)。 |
§62.5;表 62-2 给全部 flag 默认值;修改前需 tcgetattr 备份。 |
Canonical vs Noncanonical I/O |
Canonical = 行缓冲 + 行编辑 + 特殊字符解释; |
§62.6;测试 echo 是默认 canonical;vi、ssh、read -n 1 都改非 canonical。 |
MIN / TIME 参数 |
决定 noncanonical read 何时返回:(0,0)=poll;(>0,0)=block until ≥ MIN;(0,>0)=timeout 至少 1 byte 或 0;(>0,>0)=interbyte timeout 解 escape sequence。 |
§62.6.2;VMIN/VTIME 常量可能在一些 UNIX 上和 VEOF/VEOL 共用,需保存原 termios 防恢复时丢 EOF 默认。 |
cooked / cbreak / raw 模式 |
历史 UNIX 模式,POSIX 上要组合 flag;cbreak=非 canonical+关 echo+留 INTR/QUIT/SUSP;raw=关一切(ICANON/ISIG/IEXTEN/ECHO/OPOST/ICRNL/IXON…)。 |
§62.6.3;TLPI Listing 62-3 |
二、详细笔记
62.1 终端驱动概览
What:每个终端(含 pty)对应一个字符设备及终端驱动;驱动维护 input/output 两个队列(line-discipline 在两者之间),可分别处于 canonical mode 与 noncanonical mode。
Why:理解「输入先被 line discipline 处理」是后续 termios 行为的前提——^D 触发 EOF、^C 触发 SIGINT 等都是驱动层而非应用层的事件。
How:
| 元素 | 默认模式 | 改后模式 |
|---|---|---|
Line discipline |
N_TTY 实现 canonical mode |
|
输入队列 |
canonical:按行聚合 |
noncanonical:按字符 |
输出队列 |
output processing 默认 (OPOST) 开启 |
关闭 OPOST 时透传 |
队列上限 |
Linux 内核:4096 bytes(不查 MAX_INPUT) |
sysconf 给出 255 但未实际使用 |
echo |
默认 ECHO on |
password 输入前应关 |
When:(1) 写读 stdin 行处理程序——理解 canonical 缓冲;(2) 写字符实时处理程序——切 noncanonical;(3) 调试 SUSP/EOF 行为——查 c_cc + 触发 flag。
Example:ioctl(fd, FIONREAD, &cnt) 取输入队列未读字节数(非 SUSv3)。
62.2 tcgetattr / tcsetattr 与 termios
What:tcgetattr(fd, &tp) 把当前终端属性读到 struct termios;tcsetattr(fd, optional_actions, &tp) 把结构回写;optional_actions 三选一。
Why:所有 termios 修改都走这条 API;ioctl 接口老且类型不安全,POSIX 1.0 引入 tcgetattr/tcsetattr 统一接口。
How:
| when | 时机 | 典型用途 |
|---|---|---|
TCSANOW |
立即生效 |
修改只影响输入的处理 |
TCSADRAIN |
等当前输出排干 |
修改影响输出(如去掉 OPOST) |
TCSAFLUSH |
同 TCSADRAIN + 清输入队列 |
修改前要丢弃待输入(如关 echo 时) |
经典循环:tcgetattr → 备份到 struct termios save = tp; → 位运算修改字段 → tcsetattr;退出前用 save 恢复。
When:(1) 想关 echo → tp.c_lflag &= ~ECHO,TCSAFLUSH;(2) 想清 type-ahead → TCSAFLUSH;(3) 改 baud rate → cfsetispeed/cfsetospeed + tcsetattr(TCSANOW)。
Example:TLPI Listing 62-2 关闭 echo 读取密码风格:
// 摘自《The Linux Programming Interface》 第 62 章 Listing 62-2
#include <termios.h>
#include "tlpi_hdr.h"
#define BUF_SIZE 100
int main(int argc, char *argv[]) {
struct termios tp, save;
char buf[BUF_SIZE];
if (tcgetattr(STDIN_FILENO, &tp) == -1) errExit("tcgetattr");
save = tp;
tp.c_lflag &= ~ECHO;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1) errExit("tcsetattr");
printf("Enter text: ");
fflush(stdout);
if (fgets(buf, BUF_SIZE, stdin) == NULL)
printf("Got end-of-file/error on fgets()\n");
else
printf("\nRead: %s", buf);
if (tcsetattr(STDIN_FILENO, TCSANOW, &save) == -1) errExit("tcsetattr");
exit(EXIT_SUCCESS);
}
|
注意事项
|
62.3 stty 命令
What:stty 是 tcgetattr/tcsetattr 的命令行版本——stty -a 看全部属性、stty sane 重置、stty intr ^L 改 INTR。
Why:(1) 调试程序改坏了 terminal 用 Control-J stty sane Control-J 恢复(^J 是 ASCII 10,真 newline,shell 处理);(2) 看 min/time 当前值。
How:stty -F /dev/ttyN 可读 / 改另一终端;不同 UNIX 上行为略不同;bash 内置命令行编辑会改 termios,--noediting 绕过。
When:(1) 程序崩溃后终端乱 → stty sane;(2) 想看当前 ICANON 设置 → stty -a;(3) 想改 INTR → stty intr ^L。
Example:发现按 ^C 不能终止程序时,stty -a | grep intr 看是否被改了。
62.4 终端特殊字符
What:c_cc[] 数组存特殊字符值,每个常量 VINTR/VEOF/VKILL/… 是数组下标。Linux 下 VMIN/VTIME 与 VEOF/VEOL 不重叠(SUSv3 允许重叠)。
Why:理解驱动把哪些字符变信号 / 编辑操作,避免在「按下能被识别为键」的字符上做字符串解析。
How(关键摘要):
| 字符 | 默认值 | 行为 | 依赖 flag |
|---|---|---|---|
INTR |
^C |
发 SIGINT 给前台进程组 |
ISIG |
QUIT |
^\ |
发 SIGQUIT |
ISIG |
SUSP |
^Z |
发 SIGTSTP |
ISIG |
EOF |
^D |
行首时 read 返回 0 |
ICANON |
ERASE |
^? |
删一个字符(按 backspace 等价物) |
ICANON |
KILL |
^U |
删整行 |
ICANON |
WERASE |
^W |
删一个 word |
ICANON+IEXTEN |
LNEXT |
^V |
literal next,下一字符按字面量 |
ICANON+IEXTEN |
REPRINT |
^R |
重画未完成的输入行 |
ICANON+IEXTEN+ECHO |
START/STOP |
Q/S |
software flow control |
IXON |
EOL/EOL2 |
undef |
额外的行定界符 |
ICANON [/ IEXTEN] |
fpathconf(fd, _PC_VDISABLE) 返回某常量(常 = 0)禁用某特殊字符。
When:(1) 想禁用 INTR(不响应 ^C)→ tp.c_cc[VINTR] = fpathconf(STDIN_FILENO, _PC_VDISABLE);;(2) telnet 风格协议用 EOL2 做内嵌转义字符。
Example:TLPI Listing 62-1 new_intr.c——改 INTR 为其它字符;用于教学(如换成 ^L 让学生验证)。
62.5 终端 flag (c_iflag/c_oflag/c_cflag/c_lflag)
What:表 62-2 列出全部 flag;按字段分四组。
Why:(1) ECHO 控制密码输入;(2) ICANON 切 canonical/noncanonical;(3) OPOST 关输出处理;(4) IXON 关 software flow control;(5) ICRNL、ONLCR 控制 CR ↔ NL 转换。
How(必记常用):
| Flag | 字段 | 默认 | 用途 |
|---|---|---|---|
ICANON |
c_lflag |
on |
canonical 模式 |
ECHO |
c_lflag |
on |
echo 输入字符 |
ISIG |
c_lflag |
on |
INTR/QUIT/SUSP 触发信号 |
IEXTEN |
c_lflag |
on |
WERASE/REPRINT/LNEXT 等扩展处理 |
OPOST |
c_oflag |
on |
输出后处理(NL→CR-NL 等) |
ICRNL |
c_iflag |
on |
输入 CR→NL |
INLCR |
c_iflag |
off |
输入 NL→CR |
IGNCR |
c_iflag |
off |
输入 CR 丢弃 |
ONLCR |
c_oflag |
on |
输出 NL→CR-NL |
IXON |
c_iflag |
on |
START/STOP software flow control |
TOSTOP |
c_lflag |
off |
后台输出触发 SIGTTOU |
ISTRIP |
c_iflag |
off |
剥除 bit 8 |
IXANY |
c_iflag |
off |
任何字符恢复输出 |
CS8 |
c_cflag |
on |
8-bit 字长 |
PARENB |
c_cflag |
off |
奇偶校验 |
HUPCL |
c_cflag |
on |
最后 close 时 hangup modem |
When:(1) 关 echo 读密码 → c_lflag &= ~ECHO;(2) 完全国际化 → OPOST | ICRNL | ONLCR 都关;(3) 写 password / passphrase prompt → 通常加 TCSAFLUSH 清 type-ahead。
Example:TLPI Listing 62-2 仅关闭 ECHO(保留 ISIG 让 Ctrl-C 还能终止)。
62.6 I/O 模式:canonical / noncanonical / cooked / cbreak / raw
What:三种语义模式 + 两种现代术语对应:
-
canonical mode(ICANON 置位):行缓冲 + 行编辑。
-
noncanonical mode(ICANON 清零):字符即返回。
-
cooked = canonical + 全开处理(ICRNL、OCRNL、echo、ISIG 等默认全开)。
-
cbreak = noncanonical + 关 echo + 留 ISIG(仍响应 C/Z/^\)——TLPI Listing 62-3 实现。
-
raw = noncanonical + 关 ISIG/IEXTEN/ECHO/OPOST/ICRNL 等一切——「数据流原样穿过驱动」。
Why:(1) 编辑器(vi)需要 raw / cbreak 实时响应按键;(2) shell / cat 用 cooked 完事;(3) curses 应用用 cbreak。
How:MIN/TIME 决定 noncanonical 何时返回:
| MIN | TIME | read 行为 |
|---|---|---|
0 |
0 |
poll:立即返回(≥ 1 byte 返字节数;0 返 0) |
> 0 |
0 |
block:直到 MIN 字节可用 |
0 |
> 0 |
timeout:1 byte 或 TIME 0.1s 到即返回;0 返 0 |
> 0 |
> 0 |
interbyte timeout:第一字节触发计时器,间 > TIME/10 s 时返 |
TLPI Listing 62-3 给 cbreak / raw 函数:
// 摘自《The Linux Programming Interface》 第 62 章 Listing 62-3
#include <termios.h>
#include "tty_functions.h"
int ttySetCbreak(int fd, struct termios *prevTermios) {
struct termios t;
if (tcgetattr(fd, &t) == -1) return -1;
if (prevTermios != NULL) *prevTermios = t;
t.c_lflag &= ~(ICANON | ECHO);
t.c_lflag |= ISIG;
t.c_iflag &= ~ICRNL;
t.c_cc[VMIN] = 1; /* block 直到 1 byte */
t.c_cc[VTIME] = 0;
if (tcsetattr(fd, TCSAFLUSH, &t) == -1) return -1;
return 0;
}
int ttySetRaw(int fd, struct termios *prevTermios) {
struct termios t;
if (tcgetattr(fd, &t) == -1) return -1;
if (prevTermios != NULL) *prevTermios = t;
t.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO);
t.c_iflag &= ~(BRKINT | ICRNL | IGNBRK | IGNCR | INLCR
| INPCK | ISTRIP | IXON | PARMRK);
t.c_oflag &= ~OPOST;
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
if (tcsetattr(fd, TCSAFLUSH, &t) == -1) return -1;
return 0;
}
When:(1) 写 readline 风格输入 → canonical;(2) vi 实现 → raw;(3) pager (less) → cbreak;(4) 想解析方向键 → 用 MIN>0+TIME>0 interbyte timeout。
Example:TLPI Listing 62-4 test_tty_functions.c——退出 / SIGINT / SIGTSTP 都恢复原始 termios(userTermios);TUCH 的 tstpHandler 切回原 termios 后 raise SIGTSTP、再保存恢复。
62.7 线路速率 (cfgetispeed / cfsetispeed)
What:cfgetispeed/cfsetispeed 取 / 设输入速率;cfgetospeed/cfsetospeed 取 / 设输出速率;操作 termios 内的 speed_t(实际上藏在 c_cflag 的 CBAUD/CBAUDEX mask)。
Why:与老式调制解调器 / 终端打交道时;现代 USB / 串口多 115200 baud。
How:B300/B1200/B2400/B9600/B38400/B115200 为常用符号常量;cfsetispeed(0) = 「随 cfsetospeed」。
When:(1) 串口 9600 / 38400 / 115200 切换 → 调 cfsetospeed + tcsetattr(TCSANOW);(2) 现代 Linux 上很少直接用到。
Example:cfsetospeed(&tp, B38400); tcsetattr(fd, TCSANOW, &tp);
62.8 线路控制 (tcsendbreak / tcdrain / tcflush / tcflow)
What:四个 POSIX 线路控制函数——send BREAK、drain 等发送完成、flush 输入/输出队列、suspend/resume 数据流(software flow control)。
Why:(1) tcdrain 在程序退出前保证输出排干(避免数据丢失);(2) tcflush(fd, TCIFLUSH) 清用户 type-ahead(密码输入前);(3) tcflow 配合 terminal 速度匹配。
How(tcflush 队列选择):
| queue_selector | 含义 |
|---|---|
TCIFLUSH |
flush 输入队列 |
TCOFLUSH |
flush 输出队列 |
TCIOFLUSH |
flush 双队列 |
tcflow:
| action | 含义 |
|---|---|
TCOOFF |
suspend 输出 |
TCOON |
resume 输出 |
TCIOFF |
发 STOP 给对端(IXON 生效时) |
TCION |
发 START 给对端 |
When:(1) password 提示前 → tcflush(STDIN_FILENO, TCIFLUSH) 清 type-ahead;(2) 程序退出前 → tcdrain(STDOUT_FILENO);(3) 想停终端输出 → tcflow(STDOUT_FILENO, TCOOFF)。
Example:TLPI 没给独立示例;这些函数多用于串口编程(modem 旧应用)。
62.9 窗口大小:SIGWINCH + TIOCGWINSZ
What:终端驱动记录 struct winsize {ws_row, ws_col, ws_xpixel, ws_ypixel};resize 事件向 foreground process group 发 SIGWINCH;程序通过 ioctl TIOCGWINSZ 读取。
Why:curses / vim 等全屏程序须监听 resize 并重画;不解 SIGWINCH 会导致窗大小变化后「垃圾显示」。
How:
#include <sys/ioctl.h>
struct winsize ws;
ioctl(STDIN_FILENO, TIOCGWINSZ, &ws); /* 读 */
ws.ws_row = 40; ws.ws_col = 100;
ioctl(STDIN_FILENO, TIOCSWINSZ, &ws); /* 写(触发 SIGWINCH) */
注意:TIOCSWINSZ 不改变真实终端窗大小,只更新驱动记录并通知前台进程组;实际窗大小由 window manager / 终端模拟器决定。
When:(1) curses / ncurses 应用 resize → 必须处理 SIGWINCH;(2) demo 见 TLPI Listing 62-5。
Example:
// 摘自《The Linux Programming Interface》 第 62 章 Listing 62-5 (片段)
static void sigwinchHandler(int sig) {}
int main(int argc, char *argv[]) {
struct winsize ws;
sigaction(SIGWINCH, ...); /* sigwinchHandler */
for (;;) {
pause();
ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);
printf("Caught SIGWINCH, new window size: %d rows * %d columns\n",
ws.ws_row, ws.ws_col);
}
}
62.10 终端识别:isatty / ttyname
What:isatty(fd) 判断 fd 是否终端;ttyname(fd) 返回设备名(/dev/pts/0、/dev/tty1),查 /dev 与 /dev/pts 找到匹配 st_rdev 的入口。
Why:(1) 程序判断输入 / 输出是否终端(vim 决定 behavior、diff 输出颜色);(2) 调试:日志里贴终端名。
How:ttyname_r 是可重入版。
When:(1) 编辑器等决定是否交互模式 → isatty(STDIN_FILENO);(2) 日志系统识别 session → ttyname(STDERR_FILENO);(3) tty(1) 是命令行版。
三、关键图表
|
关键系统调用 / 函数
|
|
termios 关键 flag
|
|
MIN/TIME 组合
|
四、思维导图
mindmap
root((第 62 章 终端))
终端驱动
输入输出队列
line discipline
canonical default
termios 结构
c_iflag oflag
c_cflag lflag
c_cc 数组
tcgetattr tcsetattr
TCSANOW
TCSADRAIN
TCSAFLUSH
标准流程
特殊字符
INTR QUIT SUSP
EOF ERASE KILL
LNEXT REPRINT
WERASE START STOP
flags
ICANON ECHO ISIG
OPOST ICRNL ONLCR
IXON TOSTOP
c_cflag 硬件
输入模式
canonical 非 canonical
cooked cbreak raw
MIN TIME 组合
线路控制
cfget cfset speed
tcdrain tcflush
tcflow tcsendbreak
窗口大小
SIGWINCH
TIOCGWINSZ
winsize
终端识别
isatty
ttyname
/dev/pts /dev
五、重点与易错点
-
「修改 termios」标准流程:tcgetattr → 修改 → tcsetattr——直接赋值给单个字段可能损坏 termios(其它字段保持不变);务必先读完整结构。
-
不要忘记 atexit / exit handler 还原 termios——程序意外崩溃后 shell 仍持有乱套的 termios;TLPI test_tty 用 SIGINT/SIGTERM/SIGTSTP 各自 handler 恢复。
-
TCSAFLUSH 在改 input 相关 flag 时最安全——避免 type-ahead 被错误解释(如关 echo 时还遗留字符)。
-
pseudoserver 改 termios 前必须 tcgetattr 保存原值——不要假设启动时是 canonical;必须保存 user 设置。
-
Canonical 模式 read 一次最多返一行——若 read 缓冲区比一行大,后续 read 才返剩余;要按行分帧。
-
EOF (^D) 在行首导致 read 返 0;在行中只 flush 当前已输入部分,再读继续输入;与 fgets 类似行为。
-
Noncanonical + MIN/TIME 选择决定交互模型——vim 用 MIN=1 TIME=0 单字符阻塞;less / more 可用 MIN=0 TIME>0 读超时;escape sequence 用 MIN>0 TIME>0 interbyte timeout。
-
VMIN/VTIME 在 SUSv3 允许和 VEOF/VEOL 重叠——切回 canonical 模式前用保存的 termios 恢复;TLPI 警告 (Linux 上不重叠,但写 portable code 必须备份)。
-
ICANON = 0 后 ISIG 通常仍开——cbreak 模式;raw 模式才关 ISIG;vi 等需要 ISIG 让 Ctrl-C 还能终止程序。
-
OPOST 关 = no output processing——NL 不会自动变 CR-NL,光标不会下行;终端输出会有问题,不要随便关。
-
IXON software flow control 可能干扰 binary 协议——ftp/ssh 会改 mod;binary raw socket 时关 IXON。
-
SIGWINCH 默认被忽略——程序必须显式 install handler 才能感知 resize。
-
isatty 在 piped stdin 上返 0——许多交互程序 (
vim,less,top) 通过此判定;测试时不要 pipe 输入否则 program 进入 non-interactive 模式。 -
ttyname 返回静态分配字符串——多线程下用 ttyname_r;否则要复制。
-
stty 修改不跨 stty 会话持久——stty 改只是本次 shell;要永久生效写到 shell 启动脚本。
-
跨章衔接:第 64 章 pty 借用一切 termios 概念;第 34 章 job control(SIGTSTP/SIGTTOU)依赖 ISIG/TOSTOP;第 8 章密码输入 getpass() 是用 TCSAFLUSH 关 echo 的范例;第 60-61 章 fork-per-client server 也常涉及「进程在 background 时终端行为」(SIGTTOU)。