Shell 启动程序流程 —— fork + exec 模型
概要:以执行 ls 命令为例,讲解 shell 启动程序的整体流程:fork 创建子进程、子进程通过 exec 替换为 ls 程序,以及对应的系统调用细节。
父进程 (shell)
└── fork()
└── 子进程
└── exec(ls)
一、整体流程是什么
当在终端输入:
ls
真正发生的事情其实是:
- Shell 进程创建一个子进程
- 子进程把自己替换成 ls 程序
- ls 程序开始运行
对应的系统调用:
fork() -> 创建子进程
exec() -> 加载新程序
二、第一步:fork() 创建子进程
shell 会调用:
pid = fork();
fork() 的作用:
复制当前进程,生成一个新的进程
结果是:
| 进程 | 返回值 |
|---|---|
| 父进程 | 子进程PID |
| 子进程 | 0 |
因此代码一般这样写:
pid = fork();
if (pid == 0) {
// 子进程
} else {
// 父进程
}
执行后:
shell
|
fork
/ \
/ \
父进程 子进程
注意:
子进程会 继承父进程的几乎所有东西
- 地址空间
- 打开的文件
- 环境变量
- 工作目录
但 PID不同
三、第二步:子进程执行 exec()
子进程接下来会调用:
exec("ls", argv);
exec() 的作用:
把当前进程的程序替换成另一个程序
关键点:
exec 不会创建新进程 它只是 替换当前进程的代码和内存
所以执行后:
子进程(原本是shell副本)
|
exec
|
v
ls
执行完成后:
- 代码段 → 变成
ls - 数据段 → 变成
ls - 堆栈 → 变成
ls
但是:
| 不会改变 |
|---|
| PID |
| 父进程 |
| 打开的文件 |
四、完整执行时序
完整过程:
用户输入: ls
1 Shell 进程运行
shell (PID 1000)
2 fork 创建子进程
shell (PID 1000)
|
fork
|
+---- shell子进程 (PID 1001)
3 子进程 exec
shell (PID 1000)
ls (PID 1001)
也就是说:
shell
|
+-- ls
五、为什么要 fork + exec?
为什么不直接 exec?
因为:
exec 会把当前进程替换掉
如果 shell 自己 exec:
shell -> exec(ls)
那 shell 就没了!
所以必须:
shell
|
fork
|
子进程 exec(ls)
这样:
shell 还在
ls 在子进程运行
留下评论