少于 1 分钟阅读 次阅读

概要:以执行 ls 命令为例,讲解 shell 启动程序的整体流程:fork 创建子进程、子进程通过 exec 替换为 ls 程序,以及对应的系统调用细节。

父进程 (shell)
    └── fork()
            └── 子进程
                    └── exec(ls)

一、整体流程是什么

当在终端输入:

ls

真正发生的事情其实是:

  1. Shell 进程创建一个子进程
  2. 子进程把自己替换成 ls 程序
  3. 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 在子进程运行

留下评论