Bash中子进程解惑
Tags: bash, strace, process
Published: 2024-05-29 07:14:03

## 问题
Bash使用或者读各种文档时候，提到很多情况使用子进程执行，另外有的文档说执行命令时候，直接新建子进程，然后在子进程中执行，父进程等待知道子进程执行结束，对此有些疑惑，本文使用`strace`查看子进程和信号情况。本次使用Bash版本为`5.1.x`
```bash
bash --version
# GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
# ...
```


## 背景知识
1. 新建进程和执行过程。Linux进程执行先使用`fork`库函数(glibc)调用系统调用`clone`新建子进程，然后在子进程中使用exec各种库函数(glibc)(比如`execl`等)调用`execve`系统调用执行命令。上面提到的两个系统调用`clone`和`execve`可以使用`strace`查看。
2. Bash有多种情况会使用子进程执行，比如`$(command)`, `|`, 外部命令等
3. [strace](https://man7.org/linux/man-pages/man1/strace.1.html)工具用于trace系统调用和信号，官方的man文档可以很快的掌握。本次主要使用如下命令查看：
```bash
# 2763是Bash进程pid
# `-f` 跟踪子进程
# `-v` 查看详细参数
# 如果想过滤掉signal，比如SIGCHLD，使用`-e signal=none`
# 如果想过滤掉exit和attach消息，使用`-qq`或者自定义`--quiet=exit`
sudo strace -f -e execve -p 2763
```

## 实操
### 使用`strace`查看子进程和信号情况
打开两个terminal，一个用于执行Bash命令，一个执行`strace`查看结果。
1. 获取前者的进程id
```bash
echo $$ # 获取Bash pid，比如2763
```
2. 在另一个terminal中输入上面所述的`strace`命令，使用获取的pid替换
3. 在Bash终端中输入`echo hello`, 会发现strace终端没有输出
4. 在Bash终端中输入`echo hello | xargs printf "%s\n"`, 后者会有类似输出
```
strace: Process 2763 attached
strace: Process 3016 attached
strace: Process 3017 attached
[pid  3016] +++ exited with 0 +++
[pid  3017] execve("/usr/bin/xargs", ["xargs", "printf", "%s\\n"], 0x5575977a96d0 /* 55 vars */) = 0
strace: Process 3018 attached
[pid  3018] execve("/home/shouhua/.local/bin/printf", ["printf", "%s\\n", "hello"], 0x7fffdf063448 /* 55 vars */) = -1 ENOENT (No such file or directory)
[pid  3018] execve("/usr/local/sbin/printf", ["printf", "%s\\n", "hello"], 0x7fffdf063448 /* 55 vars */) = -1 ENOENT (No such file or directory)
[pid  3018] execve("/usr/local/bin/printf", ["printf", "%s\\n", "hello"], 0x7fffdf063448 /* 55 vars */) = -1 ENOENT (No such file or directory)
[pid  3018] execve("/usr/sbin/printf", ["printf", "%s\\n", "hello"], 0x7fffdf063448 /* 55 vars */) = -1 ENOENT (No such file or directory)
[pid  3018] execve("/usr/bin/printf", ["printf", "%s\\n", "hello"], 0x7fffdf063448 /* 55 vars */) = 0
[pid  3018] +++ exited with 0 +++
[pid  3017] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3018, si_uid=1000, si_status=0, si_utime=0, si_stime=1} ---
[pid  3017] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3016, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
```
上述输出很清晰涉及到三个进程，`pipe`，`xargs`和`printf`，每个进程退出时发送`SIGCHLD`信号

### 使用`strace`查看执行细节
上述输出日志中可以看到各种执行时参数，这对于调式某些shell expansion情况特别有用，比如参数是否expansion等。比如`ls *.txt`和`ls "*.txt"`。
前者的输出，可以看到`*.txt`的使用pattern match进行了filename expansion
```
strace: Process 2763 attached
strace: Process 3055 attached
[pid  3055] execve("/usr/bin/ls", ["ls", "--color=auto", "plain.txt", "request.txt", "test.txt"], 0x5575977a96d0 /* 55 vars */) = 0
[pid  3055] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3055, si_uid=1000, si_status=0, si_utime=0, si_stime=1} ---
```
后者输出，对比之下，发现double quote下的没有进行filename expansion，并且Bash报错，`'*.txt': No such file or directory`
```
strace: Process 2763 attached
strace: Process 3071 attached
[pid  3071] execve("/usr/bin/ls", ["ls", "--color=auto", "*.txt"], 0x5575977a96d0 /* 55 vars */) = 0
[pid  3071] +++ exited with 2 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3071, si_uid=1000, si_status=2, si_utime=0, si_stime=0} ---
```
还可以通过给`strace`添加`-v`查看子进程继承的环境变量。当然上述也可以通过命令`set -x`输出调试结果。

### `bash -c`执行情况
默认情况下会新建进程执行里面的命令，然后里面的命令如果是builtin命令在本进程执行，如果是外部命令则在子进程中执行，但是最后一个命令会在本进程执行。
```
strace: Process 2763 attached
strace: Process 3088 attached
[pid  3088] execve("/usr/bin/bash", ["bash", "-c", "echo hello; sleep .5; ls; hostna"...], 0x5575977a96d0 /* 55 vars */) = 0
strace: Process 3089 attached
[pid  3089] execve("/usr/bin/sleep", ["sleep", ".5"], 0x564ff0d5df60 /* 55 vars */) = 0
[pid  3089] +++ exited with 0 +++
[pid  3088] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3089, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
strace: Process 3090 attached
[pid  3090] execve("/usr/bin/ls", ["ls"], 0x564ff0d5df60 /* 55 vars */) = 0
[pid  3090] +++ exited with 0 +++
[pid  3088] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3090, si_uid=1000, si_status=0, si_utime=0, si_stime=1} ---
[pid  3088] execve("/usr/bin/hostname", ["hostname"], 0x564ff0d61c00 /* 55 vars */) = 0
[pid  3088] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3088, si_uid=1000, si_status=0, si_utime=0, si_stime=2} ---

```
### Bash不同版本细节差异
在`bash -c`中执行的日志中发现，最后hostname命令没有新开子进程执行，而是直接在父进程`3088`中执行，但是在Bash另外一个版本`4.4.x`中是在子进程中执行的，可以猜测，新版本的Bash在`bash -c`执行命令时，如果只有一个命令或者命令组中最后一个命令的情况会在当前进程中执行，不新建子进程。
```bash
bash --version
# GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)
# ...
```
以下为输出日志，注意`hostname`那行使用进程`52098`执行的，此时父进程为`51801`
```
strace: Process 51801 attached
strace: Process 52095 attached
[pid 52095] execve("/bin/bash", ["bash", "-c", "sleep .5; ls; hostname"], 0x5654ba8e1490 /* 31 vars */) = 0
strace: Process 52096 attached
[pid 52096] execve("/bin/sleep", ["sleep", ".5"], 0x55910809ad80 /* 31 vars */) = 0
[pid 52096] +++ exited with 0 +++
[pid 52095] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=52096, si_uid=3001, si_status=0, si_utime=0, si_stime=0} ---
strace: Process 52097 attached
[pid 52097] execve("/bin/ls", ["ls"], 0x55910809ad80 /* 31 vars */) = 0
[pid 52097] +++ exited with 0 +++
[pid 52095] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=52097, si_uid=3001, si_status=0, si_utime=0, si_stime=0} ---
strace: Process 52098 attached
[pid 52098] execve("/bin/hostname", ["hostname"], 0x55910809ad80 /* 31 vars */) = 0
[pid 52098] +++ exited with 0 +++
[pid 52095] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=52098, si_uid=3001, si_status=0, si_utime=0, si_stime=0} ---
[pid 52095] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=52095, si_uid=3001, si_status=0, si_utime=0, si_stime=0} ---
```

### 调试`trap`命令
```bash
trap "echo child exit" SIGCHLD
```