DEV Community

Michael
Michael

Posted on • Edited on

Bash中子进程解惑

问题

Bash使用或者读各种文档时候,提到很多情况使用子进程执行,另外有的文档说执行命令时候,直接新建子进程,然后在子进程中执行,父进程等待知道子进程执行结束,对此有些疑惑,本文使用strace查看子进程和信号情况。本次使用Bash版本为5.1.x

bash --version
# GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
# ...
Enter fullscreen mode Exit fullscreen mode

背景知识

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

实操

使用strace查看子进程和信号情况

打开两个terminal,一个用于执行Bash命令,一个执行strace查看结果。

  1. 获取前者的进程id
echo $$ # 获取Bash pid,比如2763
Enter fullscreen mode Exit fullscreen mode
  1. 在另一个terminal中输入上面所述的strace命令,使用获取的pid替换
  2. 在Bash终端中输入echo hello, 会发现strace终端没有输出
  3. 在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} ---
Enter fullscreen mode Exit fullscreen mode

上述输出很清晰涉及到三个进程,pipexargsprintf,每个进程退出时发送SIGCHLD信号

使用strace查看执行细节

上述输出日志中可以看到各种执行时参数,这对于调式某些shell expansion情况特别有用,比如参数是否expansion等。比如ls *.txtls "*.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} ---
Enter fullscreen mode Exit fullscreen mode

后者输出,对比之下,发现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} ---
Enter fullscreen mode Exit fullscreen mode

还可以通过给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} ---

Enter fullscreen mode Exit fullscreen mode

Bash不同版本细节差异

bash -c中执行的日志中发现,最后hostname命令没有新开子进程执行,而是直接在父进程3088中执行,但是在Bash另外一个版本4.4.x中是在子进程中执行的,可以猜测,新版本的Bash在bash -c执行命令时,如果只有一个命令或者命令组中最后一个命令的情况会在当前进程中执行,不新建子进程。

bash --version
# GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)
# ...
Enter fullscreen mode Exit fullscreen mode

以下为输出日志,注意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} ---
Enter fullscreen mode Exit fullscreen mode

调试trap命令

trap "echo child exit" SIGCHLD
Enter fullscreen mode Exit fullscreen mode

Top comments (0)