NJUOS-13-系统调用和Shell
本文最后更新于:2 年前
这一节会比较有趣昂!Unix Shell和这些东西联系起来啦!!!
Shell
这就是 Shell (内核 Kernel 提供系统调用;Shell 提供用户接口)
- “与人类直接交互的第一个程序”
- 帮助人类创建/管理进程 (应用程序)、数据文件……
The UNIX Shell
“终端” 时代的伟大设计
- “Command-line interface” (CLI) 的巅峰
Shell 是一门 “把用户指令翻译成系统调用” 的编程语言
- man sh (推荐阅读!), bash, …
- 原来我们一直在编程
- 直到有了 Graphical Shell (GUI)
- Windows, Gnome, Symbian, Android
脾气有点小古怪的 UNIX 世界
“Unix is user-friendly; it’s just choosy about who its friends are.”
- 但如果把 shell 理解成编程语言,“不好用” 好像也没什么毛病了
你见过哪个编程语言 “好用” 的?
(UNIX 世界有很多历史遗留约定)
A Zero-dependency UNIX Shell (from xv6)
1 |
|
- 零库函数依赖 (-ffreestanding 编译、ld 链接)
- 可以作为最小 Linux 的 init 程序
- 用到文件描述符:一个打开文件的 “指针”
上面这个编译好了之后,是直接可以跑的昂!!!
上面这些代码写的十分精巧,下去要读一读昂!!!
支持的功能:
- 命令执行
ls
- 重定向
ls > a.txt
- 管道
ls | wc -l
- 后台
ls &
- 命令组合
(echo a ; echo b) | wc -l
A Zero-dependency UNIX Shell (from xv6)
我们应该如何阅读 sh-xv6.c 的代码? -> 这里以fork()为例子
- strace + gdb!
- set follow-fork-mode, set follow-exec-mode
Pipe的实现理解
- 表达式求值:
- shell解析:
这里的echo可能涉及系统调用啊,list;,这里的;又涉及到执行顺序。语法树中,先执行左边的语法树,再执行右边的语法树。可能还会涉及到编译原理相关的一些内容。
Pipe的形式语义 & 实现
1是管道的读口,0是管道的写口。
执行fork(),会完整把进程状态拷贝一份(包括这里的文件描述符,意味着,父子进程其实共享了一份管道。管道不会被复制一份哈!!!因为fork()完全拷贝的是进程的状态和享有的资源(例如printf缓冲区),而pipe是操作系统的资源,因此复制的时候,只能复制进程中的对应的管道创建的文件描述符昂!!!)
下面有点代码的解析,首先这里有几个概念:0 -> stdin,1 -> stdout, p[0] -> 管道输入,p[1] -> 管道输出。
1 |
|
- 子进程执行管道左边的指令,并且把输入塞进管道。
- 子进程执行管道右边的指令,取出管道中的内容,并计算得出结果。
- 触发管道操作本身的父进程,wait上面两个子进程的结束,这就是管道!!!优雅干净!!!
- 本质还是上面形式语义的那一棵树树!!!|变成了管道,左右两边的命令变成了子进程执行而已昂,通过创建的管道进行了进程间的同步!!!
关键点
- 命令的执行、重定向、管道和对应的系统调用
- 这里用到 minimal.S 会简化输出
1 |
|
- 还可以用管道过滤不想要的系统调用
如何理解底层源码调用
- 我们应该如何阅读 sh-xv6.c 的代码? -> 这里以fork()为例子
- strace + gdb!
- set follow-fork-mode, set follow-exec-mode
- strace + gdb!
一步一步跟着走
- 系统调用追踪
Strace -f -o /tmp/strace.log ./sh
Tail -f /tmp/strace.log
- 仔细看看执行命令的时候,系统调用时如何运行的,也会对于我们理解指令底层的执行,有更好的帮助昂!!!
- 小技巧:如何想要系统调用的序列干净一点,可以把所有的进程绑定到同一个CPU上(Shell),干净!!!
The Shell Programming Language
基于文本替换的快速工作流搭建
- 重定向:
cmd > file < file 2> /dev/null
- 顺序结构:
cmd1; cmd2
,cmd1 && cmd2
,cmd1 || cmd2
- 管道:
cmd1 | cmd2
- 预处理:
$()
,<()
- 变量/环境变量、控制流……
Job control
- 类比窗口管理器里的 “叉”、“最小化”
- jobs, fg, bg, wait
- (今天的 GUI 并没有比 CLI 多做太多事)
UNIX Shell: Traps and Pitfalls
在 “自然语言”、“机器语言” 和 “1970s 的算力” 之间达到优雅的平衡
- 平衡意味着并不总是完美
- 操作的 “优先级”?
ls > a.txt | cat
(bash/zsh)
- 文本数据 “责任自负”
- 有空格?后果自负!(PowerShell: 我有 object stream pipe 啊喂) -> touch “a b.txt” -> vim a b.txt -> 寄了,同时操作两个文件,OS操作的时候,当成两个文件orz。Unix的设计里面,空格会具有二义性,既可能是命令的分割,也可能是文件的分割,这是“自然语言的缺陷!!!”,本身我们也会引发歧义昂,不是操作系统的问题昂!!!
- 行为并不总是 intuitive
1 |
|
Unix Shell在做啥?
命令 -> 一颗树(Semantics) -> 根据这棵树,执行系统调用!!!
展望未来
Open question: 我们能否从根本上改变命令行的交互模式?
Shell 连接了用户和操作系统
- 是 “自然语言”、“机器语言” 之间的边缘地带!
- 非常适合 BERT 这样的语言模型
已经看到的一些方向
- fish, zsh, …
- Stackoverflow, tldr, thef**k (自动修复)
- Command palette of vscode (Ctrl-Shift-P)
- Executable formal semantics for the POSIX shell (POPL’20)
终端和 Job Control
Shell 还有一些未解之谜
为什么 Ctrl-C 可以退出程序?
为什么有些程序又不能退出?
- 没有人 read 这个按键,为什么进程能退出?
- Ctrl-C 到底是杀掉一个,还是杀掉全部?
- 如果我 fork 了一份计算任务呢?
- 如果我 fork-execve 了一个 shell 呢?
- Hmmm……
为什么 fork-printf.c 会在管道时有不同表现?
- libc 到底是根据什么调整了缓冲区的行为?
为什么 Tmux 可以管理多个窗口?
- tmux是怎么实现的呢?
答案:终端
终端是 UNIX 操作系统中一类非常特别的设备!
- RTFM: tty, stty, …
tmux开的每一个新的端口,都是一个终端!!!
虽然三个窗口在一个终端上,但是其实有三个状态!!!
有意思的操作!!!!
- 在tty1 -> echo hello > /dev/pts/2。在tty1中将hello打印到第二个终端上。
- 甚至可以vim /dev/pts/2,编辑终端!!!,:%!ls,然后保存到vim中。实质上保存过程中,vim就会将数据写入我们的设备中。
观察 Tmux 的实现
首先,我们可以 “使用” tmux
- 在多个窗口中执行 tty,会看到它们是不同的终端设备!
然后,我们也可以把 tmux “打开”
- strace (
-o
) 可以看到一些关键的系统调用 (以及 man 7 pty)
tmux把你按键的信息捕捉下来,并且转发给对应的terminal,就可以实现了昂!!!
终端相关的 API
为什么 fork-printf
能识别 tty 和管道?
- 当然是观察 strace 了!
- 找到是哪个系统调用 “识别” 出了终端?
1 |
|
通过fstat系统调用,查看是不是终端!!!
Session, Process Group 和信号
1 |
|
ctrl + c,本质上我们终端会给我们的前台进程发送一个信号,进程收到这个信号后,可以有相应的信号的handler。(ctrl + c -> SIGINT, ctrl + / -> SIGQUIT)。
如果进程没有自定义handler的话,SIGINT默认对应的就是退出!
上面的程序会从标准输入里面读入数据,并且打印出来。ctrl + c没有用!!!
终端给进程发信号!!!
- 如果fork拷贝了一份呢??? -> 对应同一个外设tty,对于多个进程,怎么去发送SIGINT等信号呢(是不是应该把所有的fork(),都发送一次呢???)
- 任何时候,只能有一个前台的进程组(Terminal里面),因此ctrl + c杀不掉后台的进程。
- 如果我们启动一个shell,会启动一个session
- 一个session里面,有很多“进程组”的概念。进程组可以放前台,可以放后台。无论fork多少个进程,所有的进程都是属于一个进程组(Process Group ID是一样的)!
- ctrl + c或者ctrl + /等terminal信号,会发送给前台进程组里所有的进程。(这样就可以一起退出)
- 有些进程可以拒绝退出,转为后台执行(收到SIGINT之后,自己Handle就可以了嘛,本质上就是:
SIGSEGV 和 SIGFPE
大家熟悉的 Segmentation Fault/Floating point exception (core dumped)
- #GP, #PF 或 #DIV
- UNIX 系统会给进程发送一个信号
- 此时可以生成一个 “core” 文件 (ELF 格式),能用 gdb 调试
UNIX (System V) 信号其实是有一些 dark corners 的
- 如果SIGSEGV里再次SIGSEGV?
- POSIX.1 solved the portability mess by specifying sigaction(2), which provides explicit control of the semantics when a signal handler is invoked; use that interface instead of signal()
- 支持多线程 (早期的 UNIX 还没有多线程)、信号屏蔽、……
- POSIX.1 solved the portability mess by specifying sigaction(2), which provides explicit control of the semantics when a signal handler is invoked; use that interface instead of signal()
Job Control 背后的机制
RTFM: setpgid/getpgid(2),它解释了 process group, session, controlling terminal 之间的关系
$ man setpgid
——你神奇地发现,读手册不再是障碍了!
- The PGID (process-group ID) is preserved across an execve(2) and inherited in fork(2)…
- Each process group is a member of a session
Job Control: RTFM (cont’d)
- A session can have a controlling terminal
- At any time, one (and only one) of the process groups in the session can be the foreground process group for the terminal; the remaining process groups are in the background.
./a.out &
创建新的进程组 (使用 setpgid)
- If a signal is generated from the terminal (e.g., typing the interrupt key to generate SIGINT), that signal is sent to the foreground process group.
- Ctrl-C 是终端 (设备) 发的信号,发给 foreground 进程组
- 所有 fork 出的进程 (默认同一个 PGID) 都会收到信号
- 可以修改 signal-handler.c 观察到这个行为
- At any time, one (and only one) of the process groups in the session can be the foreground process group for the terminal; the remaining process groups are in the background.
Job Control: RTFM (cont’d)
- Only the foreground process group may read(2) from the terminal; if a background process group tries to read(2) from the terminal, then the group is sent a SIGTTIN signal, which suspends it.
- 这解释了
cat &
时你看到的 “suspended (tty input)” - 同一个进程组的进程 read tty 会竞争
- signal-handler.c 同样可以观察到这个行为
- 这解释了
- The setpgid() and getpgrp() calls are used by programs such as bash(1) to create process groups in order to implement shell job control.
- 如果希望从进程组里 detach, 使用 setpgid
ps -eo pid,pgid,cmd
可以查看进程的 pgid
shell在执行的时候,可以通过fg命令,把controlling terminal交给某个前台进程,由进程执行,这个时候,shell程序就不能接受来自用户的输入了,转为这个前台进程使用controlling terminal。
References
- 不怕读手册:man sh
- shell可以直接把指令丢到后台运行,jobs可以看到其中的内容昂!!!
1970年代的Shell就很酷啦。
- tmux也可以完成类似的功能昂!!!把任务放在后台执行昂!!!
- 重定向和管道啊之类,多种命令的组合的优先级,在不同的shell之间,可能是不一样的昂!!!(bash和zsh就不一样昂!!!)
- tty可以查看当前终端
- man setpgid , get-bid