1. 简介

通常来说,shell要启动一个新的程序,会先fork一个新进程,然后执行exec系统调用将替换新进程的执行程序。posix规定了posix_spawn函数,封装了fork+exec的过程,提供了更加高效的接口。

2. 追踪进程

2.1. 一个比hello world还简单的程序

C语言编程通常会以printf("hello world\n");作为第一个程序。这里写一个简单的程序来演示进程的创建、执行和退出。

main函数的返回值会被作为进程的退出码传递给exit系统调用,如果使用void main(void),进程的退出值可能是不确定的,这里为了方便理解,使main函数的返回值为0。

1int main(void)
2{
3    return 0;
4}

2.2. strace追踪系统调用

这里使用qemu模拟ARM64环境,可以参考https://gitee.com/kingdix10/eel

由于buildroot构建的根文件系统默认只有一个shell会话,这里使用前后台程序的方式来实现追踪。 如果可以启动多个终端,可以在另一终端运行strace

strace可以使用-e指定要追踪的系统调用,这里为了记录所有系统调用进行分析,在追踪完成后使用grep命令过滤出感兴趣的系统调用。

  1. 使用ps -e -o ppid,pid,comm | grep -w 'ps'命令查看当前shell的进程id,也就是ps的ppid
  2. 使用strace -f -p 133 -o trace.txt &在后台追踪
  3. 运行程序./dummy_arm64
  4. 使用fg %1命令将strace进程放到前台,然后使用Ctrl + C结束strace,停止追踪
  5. 使用grep -E 'fork|clone|exec|wait|exit' trace.txt查看strace的输出
1# grep -E 'fork|clone|exec|wait|exit' trace.txt
2133   clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xffff8d0310e0) = 158
3133   wait4(-1,  <unfinished ...>
4158   execve("./dummy_arm64", ["./dummy_arm64"], 0xaaab1c5be680 /* 11 vars */) = 0
5158   exit_group(0)                     = ?
6158   +++ exited with 0 +++
7133   <... wait4 resumed>[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WSTOPPED, NULL) = 158
8133   wait4(-1, 0xffffc00c7b9c, WNOHANG|WSTOPPED, NULL) = 0
9133   wait4(-1,  <detached ...>

2.3. ftrace追踪内核流程

strace类似,这里也将trace-cmd放到后台进行追踪。

  1. 使用trace-cmd record -P 133 ... &命令在后台追踪,完整命令见代码块,注意等待trace-cmd输出信息再执行下一步
  2. 运行程序./dummy_arm64
  3. 使用fg %1命令将trace-cmd放到前台,然后使用Ctrl + C结束trace-cmd,停止追踪,注意只能按一次Ctrl + C,按完Ctrl + C后可以按Enter
  4. 使用trace-cmd report > dummy.txt命令将输出内容保存到文件
  5. 使用grep __arm64_sys_ dummy.txt命令查看感兴趣的系统调用
1trace-cmd record -P 133 -p function_graph -g __arm64_sys_fork -g __arm64_sys_clone -g __arm64_sys_clone3 -g __arm64_sys_execve* -g __arm64_sys_exit_group -g __arm64_sys_waitid -g __arm64_sys_wait4 -n gic_handle_irq -n down_read -n down_write -n up_read -n up_write -n *spin_*lock* --max-graph-depth 5  -c &
2
3grep __arm64_sys_ dummy.txt
4              sh-133   [000]   772.904058: funcgraph_entry:                   |  __arm64_sys_clone() {
5              sh-133   [000]   772.908318: funcgraph_entry:                   |  __arm64_sys_wait4() {
6     dummy_arm64-164   [001]   772.910101: funcgraph_entry:                   |  __arm64_sys_execve() {
7     dummy_arm64-164   [001]   772.939417: funcgraph_entry:                   |  __arm64_sys_exit_group() {
8              sh-133   [000]   772.946847: funcgraph_entry:                   |  __arm64_sys_wait4() {
9              sh-133   [000]   777.114793: funcgraph_entry:                   |  __arm64_sys_wait4() {

3. 流程说明

3.1. 父进程clone

shell作为父进程,执行clone系统调用,在内核态,clonefork最终都会调用到kernel_fork,再调到copy_process

copy_process中复制父进程的资源和环境到子进程。

3.2. 子进程execve

新进程创建后,子进程调用execve系统调用,向内核传递可执行文件路径。内核读取并解析elf文件,替换子进程的代码段、数据段等。

3.3. 子进程exit

按照进程的默认行为,新进程执行完毕后,会调用exit系统调用,最终会调用到do_exit函数,释放进程的文件、内存等资源,但是会保留子进程的struct task_struct

默认子进程退出时还会向父进程发送SIGCHLD信号。

3.4. 父进程wait

clone后,父进程会调用wait4系统调用,进入阻塞状态,之后被子进程发送的信号唤醒。之后内核会释放子进程的struct task_struct