终端
内容回顾
我们已经知道如何用 “文件描述符” 相关的系统调用访问操作系统中的对象:open, read, write, lseek, close
。操作系统也提供了 mount, pipe, mkfifo
这些系统调用能 “创建” 操作系统中的对象。当然,我们也知道操作系统中的对象远不止于此,还有很多有趣的对象我们还没有深入了解过——终端就让人细思恐极。
终端
终端的前身可以追溯到打字机 Typewriter
,打字机时代遗留了一些遗产,例如:
Shift
: 使字锤或字模向上移动一段距离,切换字符集CR/LF
:CR
指\r
,将打印头移回行首;LF
指\n
,将纸张向上移动一行。正常来说一次换行需要\r\n
,UNIX 中的\n
同时包含CR/LF
Tab/Backspace
: 向前移动/向后移动
终端的演进历程: Typewriter -> Teletypewriter -> Video Teletypewriter -> Pseudo Terminal
终端作为输出设备,接受 UART 信号并显示;作为输入设备,把按键的 ASCII 码输出到 UART
目前大多使用伪终端,伪终端使用一对管道提供双向通信通道,由主设备 PTY Master
和从设备 PTY Slave
组成:
(如 VS Code Terminal)
(如 bash/zsh)
简单来说,流程如下:
- 用户输入:
- 用户在 终端模拟器 中键入命令 (如
ls
)。 - 终端模拟器将这些按键数据写入 PTY Master 端。
- 内核中的 PTY 驱动程序将数据转发到配对的 PTY Slave 端。
- Shell 进程的标准输入连接到 PTY Slave,因此它会读取到用户输入的数据并执行命令。
- 用户在 终端模拟器 中键入命令 (如
- 程序输出:
- Shell (或其子进程
ls
) 执行后,将结果 (文件列表) 写入其标准输出或标准错误。 - 由于标准输出/错误连接到了 PTY Slave,数据被 PTY 驱动程序捕获并转发到 PTY Master 端。
- 终端模拟器 从 PTY Master 端读取到输出数据,并将其渲染显示在窗口中。
- Shell (或其子进程
通过这种方式,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
读取用户名后,execve
成 login
程序,验证用户密码。
当在终端上输入 Ctrl+C
时,终端如何知道应该停止哪些进程呢?
每个进程都属于一个进程组 (Process Group),进程组有一个进程组ID (PGID)。多个进程组可以组成一个会话 (Session)。
- 会话领导者: 创建会话的进程
- 进程组领导者: 进程组中第一个进程,通常其 PID 等于 PGID
- 前台进程组: 当前可以接收终端信号的进程组
当终端驱动程序检测到 Ctrl+C
(ASCII 码 0x03) 时,终端驱动生成 SIGINT
信号,将其发送给当前前台进程组的所有进程。
UNIX Shell
UNIX Shell 是基于文本替换的极简编程语言,有一些独特的语言机制:
1# 命令替换 - 将命令输出作为参数2echo "文件数量: $(ls | wc -l)"34# 进程替换 - 将命令输出作为临时文件5diff <(ls dir1) <(ls dir2)67# 错误重定向 - 丢弃错误输出8cmd 2> /dev/null910# 顺序执行 - 无条件执行11cmd1; cmd21213# 条件执行 - cmd1 成功才执行 cmd214cmd1 && cmd21516# 管道链 - 多级数据流处理17ps aux | grep firefox | wc -l
这些命令会被翻译成系统调用序列,Shell 是 Kernel 之外的壳。