操作系统-第八讲-终端

终端

jyy 老师的讲义


内容回顾

我们已经知道如何用 “文件描述符” 相关的系统调用访问操作系统中的对象:open, read, write, lseek, close。操作系统也提供了 mount, pipe, mkfifo 这些系统调用能 “创建” 操作系统中的对象。当然,我们也知道操作系统中的对象远不止于此,还有很多有趣的对象我们还没有深入了解过——终端就让人细思恐极。

终端

终端的前身可以追溯到打字机 Typewriter,打字机时代遗留了一些遗产,例如:

  1. Shift: 使字锤或字模向上移动一段距离,切换字符集
  2. CR/LF: CR\r,将打印头移回行首;LF\n,将纸张向上移动一行。正常来说一次换行需要 \r\n,UNIX 中的 \n 同时包含 CR/LF
  3. Tab/Backspace: 向前移动/向后移动

终端的演进历程: Typewriter -> Teletypewriter -> Video Teletypewriter -> Pseudo Terminal

终端作为输出设备,接受 UART 信号并显示;作为输入设备,把按键的 ASCII 码输出到 UART

目前大多使用伪终端,伪终端使用一对管道提供双向通信通道,由主设备 PTY Master 和从设备 PTY Slave 组成:

终端模拟器
(如 VS Code Terminal)
用户输入命令
PTY Master
接收输入
内核 PTY 驱动
转发数据
PTY Slave
Shell 标准输入
Shell 进程
(如 bash/zsh)
读取并执行
Shell 进程
输出结果
PTY Slave
标准输出/错误
内核 PTY 驱动
转发数据
PTY Master
输出到终端
终端模拟器
渲染显示

简单来说,流程如下:

  1. 用户输入:
    • 用户在 终端模拟器 中键入命令 (如 ls)。
    • 终端模拟器将这些按键数据写入 PTY Master 端。
    • 内核中的 PTY 驱动程序将数据转发到配对的 PTY Slave 端。
    • Shell 进程的标准输入连接到 PTY Slave,因此它会读取到用户输入的数据并执行命令。
  2. 程序输出:
    • Shell (或其子进程 ls) 执行后,将结果 (文件列表) 写入其标准输出或标准错误。
    • 由于标准输出/错误连接到了 PTY Slave,数据被 PTY 驱动程序捕获并转发到 PTY Master 端。
    • 终端模拟器 从 PTY Master 端读取到输出数据,并将其渲染显示在窗口中。

通过这种方式,Shell 进程会认为自己正在与一个真实的物理终端交互,而终端模拟器则通过 PTY Master/Slave 对来管理和显示这个交互过程。伪终端经常被创建,可以通过系统调用 openpty() 通过 /dev/ptmx 申请一个新终端,返回两个文件描述符 (master/slave)。

终端分为两种模式:按行处理或按字符处理 (Canonical Mode/Non-canonical Mode)。也可以控制终端的各种行为,例如回显、信号处理、特殊字符等。

终端和操作系统

当系统启动时,内核创建第一个用户空间进程 init (PID 1),init 进程会读取配置文件,为每个终端 (tty1, tty2, ...) 启动 getty 进程。getty 进程会打开物理终端设备,将自己的 stdin/stdout/stderr 都指向该终端,显示登录提示符并等待用户输入用户名。getty 读取用户名后,execvelogin 程序,验证用户密码。

当在终端上输入 Ctrl+C 时,终端如何知道应该停止哪些进程呢?

每个进程都属于一个进程组 (Process Group),进程组有一个进程组ID (PGID)。多个进程组可以组成一个会话 (Session)。

  • 会话领导者: 创建会话的进程
  • 进程组领导者: 进程组中第一个进程,通常其 PID 等于 PGID
  • 前台进程组: 当前可以接收终端信号的进程组

当终端驱动程序检测到 Ctrl+C (ASCII 码 0x03) 时,终端驱动生成 SIGINT 信号,将其发送给当前前台进程组的所有进程。

信号处理:我们可以通过 signal (sigaction) 注册信号处理程序——这也解释了为什么有些程序不能用 Ctrl-C 退出。即便是终止信号,我们也可以执行清理代码退出。

UNIX Shell

UNIX Shell 是基于文本替换的极简编程语言,有一些独特的语言机制:

bash shell_mechanisms.sh
1# 命令替换 - 将命令输出作为参数
2echo "文件数量: $(ls | wc -l)"
3
4# 进程替换 - 将命令输出作为临时文件
5diff <(ls dir1) <(ls dir2)
6
7# 错误重定向 - 丢弃错误输出
8cmd 2> /dev/null
9
10# 顺序执行 - 无条件执行
11cmd1; cmd2
12
13# 条件执行 - cmd1 成功才执行 cmd2
14cmd1 && cmd2
15
16# 管道链 - 多级数据流处理
17ps aux | grep firefox | wc -l

这些命令会被翻译成系统调用序列,Shell 是 Kernel 之外的壳。

Freestanding Shell 这个 Shell 没有引用任何库文件——它只通过系统调用访问操作系统中的对象。为了便于调试,我们编写了 Python 脚本,打印出当前所有被调试进程打开的文件,包括读写权限和文件。没错——进程的任何瞬间都能表示成状态机的状态,包括指向操作系统对象的指针 (文件描述符)。

总结

Take-away Messages: 通过 freestandingshell,我们阐释了 “可以在系统调用上创建整个操作系统应用世界” 的真正含义:操作系统的 API 和应用程序是互相成就、螺旋生长的:有了新的应用需求,就有了新的操作系统功能。而 UNIX 为我们提供了一个非常精简、稳定的接口 (fork, execve, exit, pipe ,...),纵然有沉重的历史负担,它在今天依然工作得很好。