进程是如何创建的?

这次简单总结探究一下 Go 对进程相关操作的封装。

Go 中通过名称或者路径可以很简单的启动一个进程(命令):

ret, _ := exec.Command("bash", "-c", "ls -a -l -h").Output()
1

先来看看 Go 创建运行一个进程大致流程:

  1. 将命令、参数,以及环境变量、要运行的工作目录等数据,传递给 os.StartProcess 函数
  2. os.StartProcess 进行最后的准备工作,例如子进程环境变量未设置则设置为当前进程的环境变量
  3. 接下来会调用 syscall.StartProcess 启动进程,不同操作系统在这里实现方式不一样
  4. 最后等待子进程执行完成,收集子进程信息,这个过程也是因系统而异,例如 Linux 系统是调用的 wait4

如果你熟悉 C 语言,那么创建子进程需要用到的系统调用也一定很熟悉,在 Linux 系统下,整个过程中主要涉及到 dup2forkexec 等。

接下来主要看一下创建子进程的核心流程 syscall.StartProcess 中如何组合使用这些系统调用的:

  1. 使用 fork (opens new window) 系统调用,复制父进程来创建子进程
    • Linux 系统中代码为:rawVforkSyscall(SYS_CLONE, uintptr(SIGCHLD|CLONE_VFORK|CLONE_VM)|sys.Cloneflags),clone 函数更基础,可以设置标志位来实现不同的调用,这段代码相当于 vfork
    • macOS 系统中代码为:rawSyscall(abi.FuncPCABI0(libc_fork_trampoline), 0, 0, 0),为 fork 系统调用
    • vfork 和 fork 主要区别为:
      • fork 父子进程内存隔离,具有单独的地址空间,vfork 反之,子进程修改了内存,父进程会看到(fork 不会完全 copy 一份内存给子进程,而是使用 COW (opens new window) 机制)
      • fork 父子进程可同时执行,vfork 只能等待子进程结束或者执行完成了 exec 调用
  2. 通过 fork 的返回值判断当前在父进程中还是在子进程中,为 0 则为在子进程中
  3. 在子进程逻辑中,如果对子进程有某些设置要求,则进行设置调用,例如 chrootusergroupschdir
  4. 在子进程逻辑中,执行 execve (opens new window) 系统调用,替换当前正在运行的子进程(因为是从父进程复制来的,和父进程代码段、堆栈段等一致,需要替换成要执行的程序,并做初始化)

以上是一个标准的创建子进程的流程。

# Go runtime 特殊逻辑

在创建进程的时候,有几个特殊的 runtime 函数:

func runtime_BeforeFork()
func runtime_AfterFork()
func runtime_AfterForkInChild()
1
2
3

# runtime_BeforeFork

解决 #18600 (opens new window)

  1. M 的 locks++,locks 大于 0 的时候,runtime 一些操作会被禁止,例如禁止抢占;其他 +1 的情况例如,当前 M 进入系统调用,则会被 +1
    关于 locks 的作用,可以参考 go 系统调用相关函数的注释:
  2. 使用 sigprocmask (opens new window) 保存当前线程的 signal mask 到 GMP 的 M sigmask 字段中(保存当前注册监听的信号)
  3. 使用 sigprocmask 阻塞所有信号

内存问题:

  1. 设置一个标志位,目的是不允许 runtime 在 fork 和 exec 调用之间,执行栈增长或者申请内存(根本原因不清楚,猜测可能是 fork 相关内存的机制有关把。。)

# runtime_AfterFork

TODO

# runtime_AfterForkInChild

TODO

# clone 函数

TODO:docker 相关的使用、flag 参数

# 创建进程为什么要大费周章?

你可能会有疑问,为什么创建子进程没有一个简单的系统调用,而是需要很多步骤,其实也有单独的函数用于创建进程,例如 posix_spawn (opens new window)system (opens new window)。但是本质上,这些函数也是对 forkexec 调用的封装。并不是用来取代标准的流程的。

说句题外话,Rust (opens new window) 中是使用 posix_spawn 和标准方式结合的方式创建进程的,而 Go(1.17)中只使用常规的方式。

# Windows 系统特殊的方式

Windows 下自然是很特殊的,没有那么多流程,只需要调用 CreateProcessA (opens new window) 函数即可,当然与之相关的还有一些列的函数,具体参考 Windows API (opens new window)。毕竟 Windows 并不符合 POSIX 标准。体现了操作系统 API 之间的差异。

# 参考文献