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
命令过滤出感兴趣的系统调用。
- 使用
ps -e -o ppid,pid,comm | grep -w 'ps'
命令查看当前shell的进程id,也就是ps的ppid - 使用
strace -f -p 133 -o trace.txt &
在后台追踪 - 运行程序
./dummy_arm64
- 使用
fg %1
命令将strace
进程放到前台,然后使用Ctrl + C
结束strace
,停止追踪 - 使用
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
放到后台进行追踪。
- 使用
trace-cmd record -P 133 ... &
命令在后台追踪,完整命令见代码块,注意等待trace-cmd
输出信息再执行下一步 - 运行程序
./dummy_arm64
- 使用
fg %1
命令将trace-cmd
放到前台,然后使用Ctrl + C
结束trace-cmd
,停止追踪,注意只能按一次Ctrl + C
,按完Ctrl + C
后可以按Enter
- 使用
trace-cmd report > dummy.txt
命令将输出内容保存到文件 - 使用
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
系统调用,在内核态,clone
和fork
最终都会调用到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
。