尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

终端设备控制实战:termios、ICANON与原始模式完全指南

发布时间:2026/9/29 14:54:10

资讯中心
01
ARTICLE

终端设备控制实战:termios、ICANON与原始模式完全指南

终端设备控制实战:termios、ICANON与原始模式完全指南
简介操作系统实验7终端设备的控制实验报告面向操作系统课程学习者与Linux内核爱好者围绕修改Linux 0.11终端设备处理代码展开帮助深入理解键盘中断、扫描码映射以及设备管理的基本原理。报告完整记录了通过F12开关变量控制键盘输入与字符显示的全过程修改F12入口函数反转跳转至处理函数并在con_write中将输出字符统一替换为“*”同时涵盖编译运行、效果测试和关键问题讨论。包内仅含1个docx文档共876KB内容包含实验目的、分步操作详解、实验结果与结论并针对向文件输出字符是否应一并过滤等扩展问题进行了思考。资源已有151人学习下载适合正在完成操作系统终端设备实验或希望掌握Linux 0.11设备管理流程的读者参考。1. 终端设备控制为什么现代操作系统里还留着一个“打字机”抽象物理终端已经消失但 ssh 会话、终端模拟器、串口调试线甚至 docker attach 背后的输入输出走的仍是同一套抽象TTY 子系统。“终端设备的控制”是操作系统实验 7 的经典方向练三件事用 termios 修改终端属性、用非规范模式接管按键、用信号与 ioctl 感知屏幕变化。这套内容看起来古董却是理解“操作系统如何管理外设”的最小样本驱动之上有行规程行规程之上有系统调用系统调用之上才是我们手里的 read()。很多人写命令行工具时被“按回车才生效”“方向键变乱码”卡住往往不是逻辑错而是终端还停在规范模式。这篇笔记适合正在做操作系统实验的学生也适合想在 Linux 上写键盘交互工具却被终端行为绊倒的工程师。2. 终端设备的控制入口先用 stty 观察终端再上手 termios2.1 终端设备不是普通文件tty 文件、行规程与控制终端终端设备控制的第一课不是写代码而是先看清手里抓的到底是什么文件。在 Linux 里终端设备对应 /dev/tty* 这一族设备文件普通文件、管道、socket 都没有 termios 属性只有“真终端”才有。判断方法很简单在 bash 里执行tty能看到当前会话绑定的控制终端路径写程序时用 isatty(STDIN_FILENO)返回 0 就说明输入源不是终端。$ tty /dev/pts/3 $ stty -a speed 38400 baud; rows 48; columns 186; line 0; intr ^C; quit ^\; erase ^?; kill ^U; eof ^D; ...stty -a 会把当前终端的全部配置列出来波特率、行列数、特殊控制字符、ICANON/ECHO/ISIG 等一长串开关。这里值得留个印象你按下的 Ctrl-C 在到达进程之前先被终端行规程识别再被转成 SIGINT 信号。这个过程不是内核直接做的而是 TTY 子系统中叫“行规程”的那一层干的。行规程管理字符缓冲、行编辑、回显和信号产生而 termios 的操作对象正是行规程的可配置参数。理解了这层关系后面所有 tcsetattr 行为就都能解释。命令行工具里stty 和 termios 是同一个开关的两面stty 改的是 shell 所在终端当前的参数程序里 tcgetattr/tcsetattr 改的是自己手里那个 fd 指向的终端。很多实验代码的 bug 就是只改不读也没保存旧值退出后终端状态被改得乱七八糟。操作系统的任务里“设备管理”这一块看起来抽象终端设备控制就是最有手感的最小切片。还有一个常见误解/dev/tty 和 /dev/pts/3 的区别是什么/dev/tty 是“当前进程的控制终端”的代称无论标准输入输出被重定向到哪里open(/dev/tty) 永远打开会话的控制终端。想强制和用户键盘对话的程序比如 ssh 的密码提示就靠它绕开重定向。实验代码里未必用到但面试和期末考试都喜欢拿这个点出题。2.2 tcgetattr 与 tcsetattr读取配置与写回配置的两个系统调用读配置用 tcgetattr写配置用 tcsetattr。一个最小可编译的读取程序只需要十几行#include stdio.h #include termios.h #include unistd.h int main(void) { struct termios t; if (tcgetattr(STDIN_FILENO, t) ! 0) { perror(tcgetattr); return 1; } printf(ICANON: %s\n, (t.c_lflag ICANON) ? on : off); printf(ECHO: %s\n, (t.c_lflag ECHO) ? on : off); printf(ISIG: %s\n, (t.c_lflag ISIG) ? on : off); return 0; }这段代码只做一件事把当前终端的三个关键开关打印出来。c_lflag 是本地模式标志ICANON 决定规范模式ECHO 决定回显ISIG 决定是否把 Ctrl-C/Ctrl-Z 变成信号。tcgetattr 失败的原因只有一个——fd 指向的不是终端。很多新手用./demo input.txt重定向输入来测试就在这卡住报错 Inappropriate ioctl for device。编译命令是gcc -o probe probe.c不需要任何链接库因为 termios 函数属于 libc这一点比很多外设控制接口友好得多。tcsetattr 的第二个参数有三个候选值这是真正需要记忆的参数行为使用场景TCSANOW立即生效绝大多数实验代码改完马上想要效果TCSADRAIN等输出缓冲区的数据送完再改修改输出相关参数避免字符流被截断TCSAFLUSH清空输入输出缓冲区再改丢弃用户之前按错的按键让状态干净地切换写回配置有个隐含原则不要直接在 old 上改。通常写法是先struct termios fresh old;再改 fresh最后 tcsetattr这样 old 还留着退出时能恢复。这个“保存-修改-恢复”的三段式是终端设备控制里最值得养成的肌肉记忆。很多翻车现场都源于省了第一步后面把 old 覆盖了恢复时无从下手。另外struct termios 里的标志位不是随便摆的。c_iflag 管理输入映射比如 ICRNL 会把回车符\r转成换行符\nIXON 管理软件流控c_oflag 管理输出后处理OPOST 开着时内核会把 \n 展开成 \r\n。这一整条管线就是操作系统的“设备处理”在字符层的体现。2.3 ICANON、ECHO、ISIG决定终端“性格”的三个比特这三个开关单独拎出来讲是因为它们组合直接决定实验代码怎么写。ICANON 开着终端处于规范模式输入先进行缓冲退格、删除这类行编辑键由行规程处理直到按回车read() 才返回整行数据。ICANON 关掉进入非规范模式每个按键立刻交给进程read() 能逐字节读到原始输入。方向键、游戏键位、串口交互都要用非规范模式。ECHO 控制回显开着按键会立刻显示在屏幕上关掉按下什么都不会显示这正是口令输入需要的。顺带注意 stty -a 里的erase ^?是行规程自己的编辑键只要 ICANON 还开着这个擦除功能就一直有效一旦进入非规范模式退格键就只是一个值为 0x7f 的字节得程序自己处理。ISIG 控制“输入字符转信号”开着Ctrl-C 变成 SIGINT、Ctrl-\ 变成 SIGQUIT、Ctrl-Z 变成 SIGTSTP根本不会进入 read()关了Ctrl-C 就只是一个值为 0x03 的普通字符。想在程序里完全接管键盘通常会关它代价是 Ctrl-C 杀不掉进程必须自己设计退出口。三个位的组合可以用这张表快速对号入座ICANONECHOISIG典型行为111默认 shell行缓冲、回显、信号齐全001游戏输入按一个键动一步不回显Ctrl-C 仍有效010程序自定义回显Ctrl-C 作为普通按键处理000接近 cfmakeraw 的裸状态输入输出全手动动手验证三个位并不难stty 命令就能直接切stty -icanon # 关规范模式shell 编辑手感立刻变化 stty icanon # 恢复 stty -echo # 关回显打完命令屏幕不动 stty echo stty -isig # 关信号产生Ctrl-C 变成输入字符 stty isig在终端里逐条执行再配合上面那个读取程序比只看书印象深刻得多。提醒一句用自己的终端做实验每测完一个模式立刻恢复别把三个全关又没记住原值。真弄乱了命令行里敲stty sane能救回大半。注意stty 只影响当前 shell 所在的终端。程序里用 tcsetattr 修改后影响的是当前进程 fd 指向的终端。二者共享同一份底层配置改完互相看得见。3. 终端设备控制的最小实验关回显、读方向键、加超时3.1 实验一关掉 ECHO 做一个不回显的口令输入第一个实验不碰 ICANON只关 ECHO改动最小逻辑也最容易验证。完整代码#include stdio.h #include string.h #include termios.h #include unistd.h int main(void) { struct termios old, fresh; char buf[64]; int n; if (tcgetattr(STDIN_FILENO, old) ! 0) { perror(tcgetattr); return 1; } fresh old; fresh.c_lflag ~ECHO; if (tcsetattr(STDIN_FILENO, TCSANOW, fresh) ! 0) { perror(tcsetattr); return 1; } printf(password: ); fflush(stdout); n read(STDIN_FILENO, buf, sizeof(buf) - 1); if (n 0) { buf[n] \0; buf[strcspn(buf, \n)] \0; /* 去掉规范模式留下的换行 */ printf(\nyou entered: %s\n, buf); } tcsetattr(STDIN_FILENO, TCSANOW, old); /* 恢复回显 */ return 0; }关键动作依次是读取当前配置到 old拷贝一份到 fresh在 fresh 上清 ECHO 位写回生效读口令最后恢复 old。为什么用fresh old而不是在 old 上直接改因为 old 要留到退出时原样恢复这是终端实验最基本的纪律。注意这里没有关 ICANON所以用户必须按回车 read() 才返回buf 里会带着一个换行符strcspn 把它替换成 \0。还有两个细节容易被忽视。一是 printf 之后必须 fflush(stdout)否则 “password:” 还在缓冲层用户盯着黑屏以为程序死了。二是 read 的缓冲区长度用 sizeof(buf) - 1给字符串结尾留位置返回值 n 里包含换行处理顺序不能错。验证方法运行后输入几个字符屏幕上不应出现任何回显按回车后程序打印出原始内容。操作系统的实验报告里这道题的延伸通常是“为什么关掉 ECHO 后回车键还能结束输入”答案是 ICANON 没关行规程依然在做行缓冲。这个实验的另一个意义是 template所有需要静默输入的界面包括 sudo 的密码提示、各种交互式命令行向导本质都是这个逻辑。区别只是它们还会额外处理信号和超时。3.2 实验二用 cfmakeraw 进入原始模式直接读方向键方向键在终端里是一段转义序列按上键实际收到的是 ESC [ A 三个字节。规范模式下这三个字符合成一个“行”的一部分等回车才能交给程序要逐个字节拿到必须进非规范模式。最省事的方式是 cfmakeraw#include stdio.h #include termios.h #include unistd.h int main(void) { struct termios old, raw; char c; tcgetattr(STDIN_FILENO, old); raw old; cfmakeraw(raw); tcsetattr(STDIN_FILENO, TCSANOW, raw); printf(press arrow keys, q to quit\n); fflush(stdout); while (read(STDIN_FILENO, c, 1) 1) { if (c q) break; if (c \x1b) { char seq[2]; if (read(STDIN_FILENO, seq[0], 1) ! 1) continue; if (read(STDIN_FILENO, seq[1], 1) ! 1) continue; if (seq[0] [) { switch (seq[1]) { case A: printf(up\n); break; case B: printf(down\n); break; case C: printf(right\n); break; case D: printf(left\n); break; } fflush(stdout); } } } tcsetattr(STDIN_FILENO, TCSANOW, old); return 0; }cfmakeraw 做的事比单纯清 ICANON 多得多关掉输入映射ICRNL、IXON关掉输出后处理OPOST清掉 ECHO、ISIG、IEXTEN把字符宽度设为 8 位。一句话让终端退回“裸键盘加裸屏幕”中间不掺任何解释。代价是 Ctrl-C 失效了所以代码里用 q 作为退出键。如果编译时编译器警告 cfmakeraw 隐式声明在文件顶部加#define _DEFAULT_SOURCE再包含头文件即可。这是 glibc 对非 POSIX 扩展符号的常规保护。读取方向键时注意ESC 也是一个独立按键。用户按了单独的 ESC程序读到 \x1b 后会再等两个字节而这两个 read 是阻塞的。在专门跑循环的程序里问题不大但放进需要响应其他事件的程序就会出现“按一次 ESC 卡住 1 秒”的观感。这正是实验三要解决的。这个实验能让“行规程”从一个抽象名词变成可感知的行为你按下的每一个键都在被某个比你代码更底层的逻辑解释而 cfmakeraw 就是把这层解释彻底关掉的开关。3.3 实验三用 VMIN 与 VTIME 修掉方向键读取的卡死问题上一版的第二个 read 是阻塞的。如果用户只按 ESC 不按方向键或转义序列被拆散read 就一直挂着。解决思路不是放弃非规范模式而是给读取加超时。非规范模式下c_cc[VMIN] 和 c_cc[VTIME] 控制 read 的等待策略VMINVTIMEread 的行为00没有数据立即返回 0有数据立即返回00一直等直到读满 VMIN 个字节00最多等 VTIME 个 0.1 秒期间有数据就返回00等满 VMIN 个字节超出时间则返回已读到的字节把 VMIN 设为 0、VTIME 设为 10read 最长阻塞 1 秒。ESC 后续字节在超时内没来read 返回 0程序继续走主循环不再卡死#include stdio.h #include termios.h #include unistd.h int main(void) { struct termios old, raw; char c; tcgetattr(STDIN_FILENO, old); raw old; cfmakeraw(raw); raw.c_cc[VMIN] 0; raw.c_cc[VTIME] 10; tcsetattr(STDIN_FILENO, TCSANOW, raw); printf(arrow keys / q to quit, timeout 1s\n); fflush(stdout); while (1) { int n read(STDIN_FILENO, c, 1); if (n 0) { printf((timeout)\n); fflush(stdout); continue; } if (c q) break; if (c \x1b) { char seq[2]; if (read(STDIN_FILENO, seq[0], 1) ! 1) continue; if (read(STDIN_FILENO, seq[1], 1) ! 1) continue; if (seq[0] [) { switch (seq[1]) { case A: printf(up\n); break; case B: printf(down\n); break; case C: printf(right\n); break; case D: printf(left\n); break; } fflush(stdout); } } } tcsetattr(STDIN_FILENO, TCSANOW, old); return 0; }这里有一个语义必须记准VTIME 的单位是 0.1 秒VTIME10 等于 1 秒而第 5 章会用到的 poll() 超时单位是毫秒两个单位太容易搞混。另一个更深的语义VTIME 的倒计时从 read 被调用时开始不是从上一个字符到达时开始。所以方向键的 ESC 与后续字节间隔如果超过 1 秒seq[0] 或 seq[1] 的 read 返回 0程序直接 continue。表现就是方向键偶尔没反应但不卡死。在无线键盘、网络延迟大的场景超时可以调到 20 甚至 30对应 23 秒。坚持用 VMIN VTIME 还有一个原因它保持 read 的阻塞语义程序结构简单。想用非阻塞 I/O 也可以把终端 fd 设成 O_NONBLOCK但那会让 read 在缓冲为空时立即返回 EAGAIN循环逻辑要处理更多分支往往需要配 poll 才干净。实验做到这里你已经同时用到了 tcgetattr/tcsetattr、cfmakeraw、VMIN/VTIME 三组工具终端设备控制的最小闭环算是通了。接下来真正头疼的是程序退出后的现场恢复。4. 终端设备控制的避坑与排查五个常见翻车现场4.1 退出没恢复 termios终端进入粘滞状态现象程序正常退出后shell 突然变得很奇怪敲命令看不见字符按回车命令不执行有时连 Ctrl-C 都没反应。如果这是远程 ssh 窗口当前会话基本废掉只能重开。原因代码只做了 tcsetattr 修改退出前没恢复。更隐蔽的是程序被信号杀死你见程序卡住在另一个窗口kill -9 pidSIGKILL 无法捕获代码里无论有没有恢复逻辑都执行不到。解决第一层把恢复函数注册到退出必经路径第二层保留手工后悔药。static struct termios saved; void restore_termios(void) { tcsetattr(STDIN_FILENO, TCSANOW, saved); } int main(void) { tcgetattr(STDIN_FILENO, saved); atexit(restore_termios); /* ... 后面随意改终端 ... */ return 0; }atexit 覆盖正常 return 和 exit()但不覆盖 _exit() 和信号终止。对 SIGINT/SIGTERM 这类信号要显式注册 handler在恢复终端后调用 _exit()。注意信号处理函数里不能调 printf但 tcsetattr、write、_exit 都是异步信号安全函数。很多老代码在崩溃后终端“粘滞”就是因为只考虑了正常路径没考虑信号路径。4.2 ECHO 关了忘开屏幕从此“沉默”现象某个程序运行后终端里输入的字符全部不可见但命令确实在执行。退出程序后问题依旧用户以为自己键盘坏了。原因多数情况是 ECHO 位没恢复。另一种嵌套场景更刁钻程序 A 关掉 ECHO然后调用程序 BB 启动时保存的“旧配置”恰好是 A 改过的配置B 退出时把 A 的状态又恢复了一遍等于没恢复。真实工具链里很常见比如你的实验程序内部去调了 vim 或 less。解决先确认现象stty -a | grep echo看当前状态是-echo就执行stty echo立刻救回。程序层面的根治方案是所有对 termios 的修改收敛到一处退出恢复只用同一个 saved 副本不要每个函数各存各的。我看过一份实验代码三个函数分别 tcgetattr每个函数退出各做一套“恢复”状态越整越乱。终端状态是全局资源按全局变量管别按局部变量管。提示stty sane 是终端设备的“后悔药”能把大多数改乱的模式位恢复成默认值。它不保证还原到会话启动时的精确状态但至少能让你把命令敲下去。4.3 ISIG 被关Ctrl-C 失灵程序退不出去现象程序进入原始模式后按 Ctrl-C 没反应代码里又没有专门退出的键程序像一块死肉只能到另一个终端 kill。原因cfmakeraw 或手动清 ISIG 后行规程不再把 0x03 转成 SIGINTCtrl-C 作为普通字符进入缓冲区。如果代码没读它它就一直在那儿。解决设计原始模式交互程序时必须显式提供退出键。最小做法是 read 到 q 就 break更稳的是同时捕获 SIGTERM在 handler 里恢复终端再退出这样外部 kill、systemd 停服务也能清理现场。如果你想保留 Ctrl-C 的信号语义就别清 ISIG让信号机制正常工作如果程序有自己的按键解释逻辑把 Ctrl-C 当普通字符处理更统一。这个取舍没有标准答案但一定要事先想清楚。4.4 VMIN0 与 VTIME0忙循环吃满一个核现象程序在跑但又像没干活top 显示 CPU 占用接近 100%代码里明明有 read 却在空转。原因VMIN0 且 VTIME0 时read() 在终端没有数据也立即返回 0外层循环不处理返回值循环体就变成无限快速空转。这个组合的语义是“非阻塞轮询”不是“等待”。解决如果只是想要几十毫秒级别的等待把 VTIME 改成 5 或 10 即可如果程序结构需要同时监听多个 fd应该用 poll 而不是靠 read 反复探测struct pollfd pfd { .fd STDIN_FILENO, .events POLLIN, }; int pr poll(pfd, 1, 500); /* 500 毫秒 */ if (pr 0) { char c; read(STDIN_FILENO, c, 1); }poll 的第二个参数是 fd 数组长度第三个是超时毫秒数返回 0 表示超时正数表示有事件。用 poll 把“等待”和“读取”分开循环体就不会空转。如果你确实需要非阻塞读正确的姿势是把 fd 设成 O_NONBLOCK 并处理 EAGAIN而不是靠 00 组合撞大运。4.5 管道与重定向下 tcgetattr 报错程序以为自己不是终端现象程序编译通过想用脚本喂数据echo abc | ./demo一跑程序直接打印 tcgetattr: Inappropriate ioctl for device 退出。原因管道、普通文件、socket 都没有 termios 属性tcgetattr 对非终端 fd 返回 ENOTTY。错误信息很直白但没有经验的读者常把它当成权限或系统问题。解决操作终端属性前用 isatty 判断。#include unistd.h if (!isatty(STDIN_FILENO)) { fprintf(stderr, stdin is not a terminal\n); return 2; }判断之后分流需要终端交互的程序直接报错退出批处理程序可以绕开 termios按普通文件读。很多真实命令行工具的“交互模式”和“管道模式”就是这么分开的。顺带注意ssh 会话里 stdin 一般是 ptyisatty 返回真systemd 服务、cron 任务里 stdin 可能是 /dev/null也属于同一类坑。5. 终端设备控制的进阶用 SIGWINCH 与 ioctl 感知屏幕大小5.1 TIOCGWINSZ把终端窗口尺寸读出来很多程序需要按终端宽度排版进度条、表格、编辑器。传统做法是读环境变量 LINES/COLUMNS但环境变量只在 shell 启动时生效不会随窗口变化更新。正确的做法是 ioctl#include stdio.h #include sys/ioctl.h #include unistd.h int main(void) { struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, ws) 0) { printf(rows%d cols%d\n, ws.ws_row, ws.ws_col); } else { perror(TIOCGWINSZ); } return 0; }struct winsize 有四个字段ws_row 行数、ws_col 列数、ws_xpixel 与 ws_ypixel 像素尺寸。后两个字段很多终端驱动不填看作额外信息别依赖。这个 ioctl 的输出应该和tput lines、tput cols一致区别是 tput 是外部命令多一次 fork 和管道通信在循环里高频刷新排版时有可见开销。因为 ioctl 本身不需要终端进入任何特殊模式这个代码在普通 shell 里直接就能跑通是五个实验里“事故率”最低的一个。但它在整个实验里的位置很重要它把终端从“字符输入输出设备”扩展成了“可查询的显示设备”这是后续所有全屏程序的地基。5.2 SIGWINCH窗口一变就被通知窗口大小查询是瞬时的但每刷一帧都去 ioctl 有点浪费也不够优雅。终端窗口大小变化时内核会给前台进程组发 SIGWINCH。注册 handler 置标志位主循环看到标志位再重新查询#include signal.h #include stdio.h #include sys/ioctl.h #include unistd.h static volatile sig_atomic_t g_resized 0; static void on_winch(int sig) { (void)sig; g_resized 1; } int main(void) { struct sigaction sa { 0 }; sa.sa_handler on_winch; sigaction(SIGWINCH, sa, NULL); struct winsize ws; while (1) { if (g_resized) { g_resized 0; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, ws) 0) { printf(\rresized to %d x %d , ws.ws_row, ws.ws_col); fflush(stdout); } } usleep(100000); } return 0; }两个细节要记。一是用 sigaction 而不是 signal()signal() 在不同 Unix 上语义不一致有的版本处理完信号会自动重置 handler有的会中断系统调用行为飘忽sigaction 是 POSIX 推荐写法。二是标志位用 volatile sig_atomic_t这是 C 标准规定信号 handler 与主循环之间唯一安全的共享类型读写是原子的不会读到半新不旧的变量。运行这个程序时把终端窗口拉大缩小下一帧就会打印新尺寸。信号处理函数里只置标志位、不做输出这是信号处理的标准范式。为什么因为信号可能在任何一条指令之间打断主流程如果 handler 里调用不可重入函数轻则输出错乱重则死锁。终端设备控制实验做到这才算真正理解“信号不是用来干活的是用来提醒主循环干活”的。5.3 综合起来一个不依赖 ncurses 的秒级刷新小工具把前面所有内容整合成一个“时钟 窗口尺寸提示”的小工具非规范模式读按键、poll 做定时刷新、SIGWINCH 感知窗口变化、Ctrl-C 被打断时恢复现场。去掉业务逻辑后这就是 top/htop 一类命令的骨架#include poll.h #include signal.h #include stdio.h #include sys/ioctl.h #include termios.h #include time.h #include unistd.h static struct termios saved; static volatile sig_atomic_t g_resized 0; static void on_winch(int sig) { (void)sig; g_resized 1; } static void on_int(int sig) { (void)sig; tcsetattr(STDIN_FILENO, TCSANOW, saved); write(STDOUT_FILENO, \033[?25h, 6); _exit(130); } int main(void) { struct sigaction sa { 0 }; sa.sa_handler on_winch; sigaction(SIGWINCH, sa, NULL); sa.sa_handler on_int; sigaction(SIGINT, sa, NULL); tcgetattr(STDIN_FILENO, saved); struct termios raw saved; raw.c_lflag ~(ICANON | ECHO); raw.c_iflag ~(IXON | ICRNL); raw.c_cc[VMIN] 0; raw.c_cc[VTIME] 0; tcsetattr(STDIN_FILENO, TCSANOW, raw); printf(\033[?25l); /* 隐藏光标 */ fflush(stdout); struct pollfd pfd { .fd STDIN_FILENO, .events POLLIN }; while (1) { if (g_resized) { g_resized 0; struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, ws) 0) { printf(\033[2J\033[Hwindow %d x %d, ws.ws_row, ws.ws_col); fflush(stdout); } } int pr poll(pfd, 1, 1000); if (pr 0) { char c; if (read(STDIN_FILENO, c, 1) 1 c q) break; } else if (pr 0) { time_t now time(NULL); struct tm tmv; localtime_r(now, tmv); printf(\r%02d:%02d:%02d, tmv.tm_hour, tmv.tm_min, tmv.tm_sec); fflush(stdout); } } printf(\033[?25h); /* 恢复光标 */ tcsetattr(STDIN_FILENO, TCSANOW, saved); return 0; }代码里有几个有意为之的设计值得逐一说明。第一没有用 cfmakeraw而是手动清 ICANON、ECHO、IXON、ICRNL。原因在于 cfmakeraw 会连 ISIG 一起关Ctrl-C 会变成普通字符on_int 永远触发不了。手动模式下 ISIG 保持打开Ctrl-C 照常触发信号处理。写真实工具时经常遇到这个抉择原始程度不是越高越好够用就行。第二VMIN0 配合 VTIME0 让 read 不阻塞主循环真正的等待交给 poll超时 1000 毫秒正好每秒刷新一次时钟。不要试图用 VTIME10 做秒级节拍VTIME 只管 read 本身管不了循环里的其他工作单位还容易弄混。第三on_int 里 ESC[?25h 是 ANSI 转义序列把之前隐藏的光标恢复显示。如果退出时不恢复你的终端会一直处于无光标状态这是另一个隐蔽的“现场破坏”。编译运行gcc -o clock_tool clock_tool.c运行后第一行显示终端尺寸缩放窗口会更新下方每秒刷新时间按 q 正常退出按 Ctrl-C 也能退出且终端状态恢复。在常见 Linux 发行版的默认桌面终端里这套 ANSI 序列都兼容。能在自己的机器上把这段代码完整跑通终端设备控制从属性配置、非规范读取、信号处理到 ioctl 感知就都过了一遍。6. 终端设备控制实验的验证手艺用 strace 盯系统调用用 stty sane 当后悔药6.1 用 strace 验证 termios 调用有没有落在内核上实验做完代码行为也正常怎么证明你写的 termios 确实生效了而不是碰巧跑通把 strace 贴在可执行文件前面能看到进程发起的每一次系统调用gcc -o clock_tool clock_tool.c strace -e traceioctl,tcgetattr,tcsetattr,read -o trace.log ./clock_tool然后看 trace.log程序启动时应该有 tcgetattr随后是一次 tcsetattr窗口变化时会出现针对 TIOCGWINSZ 的 ioctl每次按键对应一次 read 系统调用如果一直没按键poll 会超时返回 0。strace 是把终端行为从“玄学”变成“可验证”的第一手段尤其适合排查偶发问题。比如程序报告 tcsetattr 失败但没打印原因用 strace 一看就能确认 errno 是 ENOTTY 还是 EINVAL。6.2 用 script 录制会话翻车也能回放第二个工具是 script它把整段终端会话录成文件包括控制字符之后可以用自带回放原样重放具体参数以你所用发行版的 man 手册为准。我自己的流程是跑任何会修改终端的程序之前先script -q session.log如果程序把终端搞乱退出后回看 session.log 能看到崩溃前后按过的每一个键。这比靠记忆复盘可靠得多。最后一条习惯比任何工具都管用凡是会改终端的程序启动第一行就 tcgetattr 保存全局旧值正常返回、SIGINT、SIGTERM 三条退出路径全部接恢复逻辑怕遗漏就把 stty sane 当最后的后悔药。我做终端方向这块踩过不少坑才养成这个习惯第一版实验代码也让自己的 shell 粘滞过好几回。终端配置属于全局可变状态不是函数局部变量不尊重它的程序轻则影响自己重则拖垮整个会话。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。