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。