南京大学 操作系统 (JYY) 学习笔记:终端、进程组与 UNIX Shell 的前世今生

发布时间:2026/9/25 5:43:18

南京大学 操作系统 (JYY) 学习笔记:终端、进程组与 UNIX Shell 的前世今生
写在前面这是本系列的第八篇。前面的课程中我们已经知道如何用“文件描述符”相关的系统调用访问操作系统中的对象open,read,write,lseek,close也知道了如何用pipe创建管道对象。但操作系统中的对象远不止于此。今天我们要深入一个每天都在用但细思恐极的对象——终端 (Terminal)。从机械打字机开始一探究竟Ctrl-C到底做了什么并在此基础上剖析 UNIX Shell 的底层原理。终端的历史从机械打字机到显示器我们今天在键盘上敲击的每一个按键在终端里看到的每一次换行都刻满了 100 多年前机械时代的烙印。打字机时代遗产 (Typewriter, 1860s)QWERTY 键盘:这其实是为了降低打字速度而设计的防卡纸方案。因为早期的纯机械结构按键太快会导致字锤卡在一起。Shift 键:按住它机械结构会使字锤或字模向上移动一段距离从而切换字符集打出大写字母或符号。毕竟大小写各占一个物理按键太浪费空间了。回车与换行 (CR LF):\rCR (Carriage Return - 回车):将物理打印头移回行首。\nLF (Line Feed - 换行):滚筒转动将纸张向上移动一行。在 UNIX 中\n在逻辑上同时包含了 CR 和 LF 的动作而在 Windows 中历史遗留问题导致换行依然严格保留着\r\n两个字符。Tab Backspace:它们在早期本质上是“位置移动”指令。在纸上打错字是去不掉的所以早期的 Backspace 配合减号使用退格然后打个横线表示“错了划掉”。封神之路VT100 终端 (1978)为了发电报人们发明了电传打字机 (Teletypewriter, 即 TTY)。后来加入了屏幕进化成了 Video Teletypewriter。DEC 公司在 1978 年推出的VT100终端成为了事实上的行业标准。它是首个完整实现ANSI Escape Sequence (转义序列)的终端。80 列 × 24 行的字符显示成为了至今深深影响终端窗口的标准布局。今天我们用的终端伪终端 (Pseudo Terminal)你在系统里比如 VSCode、Tmux、SSH可以随意创建无数个黑框框它们其实都是伪终端 (PTY)。PTY一对双向通信的“管道”伪终端在底层是成对出现的主设备 (PTY Master):连接到你看到的那个“终端模拟器”黑框框 GUI 界面。从设备 (PTY Slave):连接到 Shell 或其他后台程序例如/dev/pts/0。终端模拟器是怎么实现的现在利用学过的系统调用你也可以自己写一个终端模拟器了调用openpty()函数通过/dev/ptmx向操作系统申请一个新终端。系统会返回两个文件描述符Master 和 Slave。调用fork()创建子进程。子进程把stdin/stdout/stderr全部重定向指向 Slave然后execve执行bash。父进程从 Master 读取子进程的输出渲染画到屏幕上把你敲击的键盘按键写入 Master 传给子进程。极客扩展既然是通过转义序列 (Escape Sequence) 渲染字符那能不能渲染图片现代终端如 Kitty扩展了转义序列允许控制大小、位置甚至动画例如\033[60C_开头\033\结尾传递图像数据。未来终端内嵌 WebView 也是大势所趋。终端的模式与属性终端的两种模式Canonical Mode (规范模式 / 按行处理):你敲击键盘时程序读不到数据终端自己提供行编辑功能比如退格修改。直到你按下回车整行数据才会被发送给程序。Non-canonical Mode (非规范模式 / 按字符处理):你按下的每一个字符都会立即发送给程序。用于实现交互式程序比如 Vim、SSH。终端属性控制 (tcgetattr / tcsetattr)这些 API 可以控制终端的各种行为回显、信号处理、特殊字符拦截等。比如当你在终端输入密码时看不见字符底层就是调用了这个 API 临时关闭了“终端回显”功能。进程管理与 Job Control (作业控制)当系统里有一大棵复杂的进程树有的在前台有的在后台这时你按下Ctrl-C到底该终止哪个进程答案终端才不管呢终端非常蠢它只管传输字符。Ctrl-C本质上只是发送了一个 End of Text (ETX) 控制字符\x03。Ctrl-D只是发送了 End of Transmission (EOT) 字符\x04。(你可以用stty -a命令看到当前终端的所有按键绑定奇怪的知识又增加了)操作系统如何处理 Ctrl-C当操作系统收到\x03这个字符时它需要对“当前进程”采取行动。但fork()产生了那么庞大的树状结构谁才是“当前进程”为了解决这个问题UNIX 引入了两个令人头疼的编号Session ID (会话大分组):子进程会继承父进程的 Session ID。一个 Session 关联一个控制终端 (controlling terminal)。当 Leader比如你的 SSH 登录窗口退出时全体进程都会收到 Hang Up (SIGHUP) 信号集体自杀。Process Group ID (进程组小分组):一个会话里可以有多个进程组但只能有一个前台进程组。当操作系统收到Ctrl-C带来的\x03字符时它会向前台进程组里的所有进程发送SIGINT(中断信号)不能误伤后台进程。无奈的妥协千疮百孔的 API为了管理这些组诞生了setsid,getpgid,tcsetpgrp等极不优雅的 API。再加上各种uid,effective uid,saved uid的权限管理……几乎任何人、任何软件都无法预知未来的走向历史的糟粕就这样成了 POSIX 标准的一部分。学会去未来回头看现在如果抛开历史包袱人机交互的方式根本不应该是这样的。现代 GUI 管理器Window Manager只需要“进程组”就够了。关掉窗口强行杀掉该组所有进程一个不留。Android 系统里干脆把每个 App 当作不同的 User 处理。Snap 甚至用AppArmorseccompnamespaces把程序关在绝对隔离的沙箱里运行。信号与 Ctrl-C 的底层代码演示当操作系统决定向前台进程发送信号时底层发生了什么注册处理程序 (signal / sigaction):告诉操作系统如果收到某个信号请去执行函数f。发送信号 (kill):强行在程序的正常执行流中插入一个向f的跳转。这就是为什么信号能打断死循环。代码演示#includestdio.h#includestdlib.h#includesignal.h#includeunistd.hvoidhandler(intsignum){switch(signum){caseSIGINT:printf(Received SIGINT!\n);break;caseSIGQUIT:printf(Received SIGQUIT!\n);exit(0);break;}}voidcleanup(){printf(atexit() cleanup\n);}intmain(){// 拦截 Ctrl-C (SIGINT) 和 Ctrl-\ (SIGQUIT)signal(SIGINT,handler);signal(SIGQUIT,handler);atexit(cleanup);while(1){charbuf[4096];intnreadread(STDIN_FILENO,buf,sizeof(buf));buf[nread-1]\0;printf([%d] Got: %s\n,getpid(),buf);if(nread0){perror(read);exit(1);}sleep(1);}}运行结果你会发现按下Ctrl-C时程序不会退出了而是打印出我们自定义的语句。只有按下Ctrl-\才会触发SIGQUIT退出。rootLAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec8/signal# ./signal^CReceived SIGINT![1927]Got: s[1927]Got: s ^\Received SIGQUIT!atexit()cleanupUNIX Shell 编程语言UNIX 的用户可都是 Hackers所以 UNIX Shell 被设计成了一种基于文本替换的极简编程语言。只有一种类型字符串。语言机制极其简洁预处理$(), 重定向, 顺序结构||, 管道|。底层核心揭秘重定向 () 是怎么实现的利用子进程继承文件描述符的特性配合dup2移花接木// 1. 父进程先以只读/只写的方式打开好输入/输出文件intfd_inopen(...,O_RDONLY|O_CLOEXEC);intfd_outopen(...,O_WRONLY|O_CLOEXEC);intpidfork();if(pid0){// 2. 子进程中利用 dup2 将标准输入(0) 和标准输出(1) 重定向到文件 fd 上dup2(fd_in,0);dup2(fd_out,1);// 3. 执行目标程序目标程序根本不知道自己被重定向了它只管傻傻地往 0 读往 1 写。execve(...);}else{// 4. 父进程关闭自己用不到的 fd等待子进程执行完毕close(fd_in);close(fd_out);waitpid(pid,status,0);}这几十行代码就是你敲下ls out.txt背后操作系统发生的所有秘密。Shell 的反思优点与缺点优点高效、简洁、精确。它是一种“自然编程语言”一行命令协同调度多个程序。最适合 quick dirty 的 hackers。AI 时代遇到不懂的命令直接man tcsetpgrp | ag -q 帮我生成新手友好的教程或者出了打错字的命令直接用开源神器thefuck自动纠正。缺点文本数据的“责任自负”。Shell 的设计被“1970s 的算力、算法和工程能力”严重束缚。空格 灾难文件名里带个空格Shell 脚本随时崩溃。优先级混乱ls a.txt | cat到底先执行重定向还是先执行管道bash和zsh的行为甚至都不一样致敬经典课程最后提到了xv6-riscv的 shell 源码。一个真正能用的微型操作系统 Shell没有 libc 的加持底层完全是依靠刚才讲的这些系统调用手搓出来的。这很牛逼但也告诉我们底层的系统编程没有魔法只有一层一层严谨的系统调用抽象。

相关新闻

Android逆向工程实战:从APK拆包到动态脱壳的完整指南

Android逆向工程实战:从APK拆包到动态脱壳的完整指南

2026/8/23 1:23:11

1. 项目概述:为什么我们需要一个“拆包器”?在Android应用开发与安全研究的圈子里,逆向工程是一个绕不开的话题。无论是安全研究员分析恶意软件的行为,还是开发者学习优秀应用的架构设计,亦或是测试人员验证应用的安全…

TI TPS25910负载开关EVM评估:浪涌抑制与电源时序控制实战

TI TPS25910负载开关EVM评估:浪涌抑制与电源时序控制实战

2026/8/23 1:23:11

1. 项目概述与核心价值在服务器、存储阵列或者电信设备这类对电源可靠性要求极高的系统中,你肯定遇到过这样的场景:一块新的硬盘或板卡在带电状态下插入背板,瞬间的电流冲击可能导致系统电压跌落,甚至触发保护、导致重启。又或者&…

高性能嵌入式处理器电源设计:基于TI PMIC的DRA7xxP/TDA2Pxx电源管理方案解析

高性能嵌入式处理器电源设计:基于TI PMIC的DRA7xxP/TDA2Pxx电源管理方案解析

2026/9/8 0:01:38

1. 项目概述:为高性能处理器构建稳健的电源骨架在嵌入式系统,尤其是汽车电子和工业控制这类对可靠性要求极高的领域,电源设计从来都不是简单的“供电”问题。它更像是在为整个系统的“心脏”搭建一套精密、可靠且智能的“血液循环系统”。处理…

CANN/GE ACL数据集缓冲区添加函数

CANN/GE ACL数据集缓冲区添加函数

2026/9/23 22:20:06

aclmdlAddDatasetBuffer 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Te…

用ffmpeg高效批量调整图片尺寸的实战指南

用ffmpeg高效批量调整图片尺寸的实战指南

2026/9/23 14:32:22

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

Transformers 音频特征提取工具库 audio_utils 全解析:从 Mel 刻度换算到对数 Mel 频谱

Transformers 音频特征提取工具库 audio_utils 全解析:从 Mel 刻度换算到对数 Mel 频谱

2026/9/24 3:39:17

Transformers 音频特征提取工具库 audio_utils 全解析:从 Mel 刻度换算到对数 Mel 频谱 【免费下载链接】transformers 🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and mu…

RustFS 多节点集群重启与滚动升级实战:Readiness、Quorum 与 Degraded 模式完全指南

RustFS 多节点集群重启与滚动升级实战:Readiness、Quorum 与 Degraded 模式完全指南

2026/9/23 14:31:31

RustFS 多节点集群重启与滚动升级实战:Readiness、Quorum 与 Degraded 模式完全指南 【免费下载链接】rustfs 🚀2.3x faster than MinIO for 4KB object payloads. RustFS is an open-source, S3-compatible high-performance object storage system sup…

Java Integer缓存揭秘:128陷阱原理、避坑与面试全解

Java Integer缓存揭秘:128陷阱原理、避坑与面试全解

2026/9/24 7:10:52

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

RustFS Scanner 数据用量发布权威性决策:配额准入如何获得可用的权威依据

RustFS Scanner 数据用量发布权威性决策:配额准入如何获得可用的权威依据

2026/9/23 14:34:03

RustFS Scanner 数据用量发布权威性决策:配额准入如何获得可用的权威依据 【免费下载链接】rustfs 🚀2.3x faster than MinIO for 4KB object payloads. RustFS is an open-source, S3-compatible high-performance object storage system supporting mi…

远程协作的工作台整理

远程协作的工作台整理

2026/9/24 16:02:49

远程协作的工作台整理远程协作的核心不是再加一个工具,而是让交接信息足够完整。异步任务要写明目标、输入位置、完成标准和需要决策的人。 工作台的最小配置 将日程、待办、代码和沟通入口收拢到少数固定位置;通知按紧急程度分层。工作台不需要模仿办公…

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

2026/9/21 23:38:13

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

2026/9/25 4:22:14

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…