这次简单总结探究一下 Go 对进程相关操作的封装。
Go 中通过名称或者路径可以很简单的启动一个进程(命令):
ret, _ := exec.Command("bash", "-c", "ls -a -l -h").Output()
1
先来看看 Go 创建运行一个进程大致流程:
- 将命令、参数,以及环境变量、要运行的工作目录等数据,传递给
os.StartProcess
函数 os.StartProcess
进行最后的准备工作,例如子进程环境变量未设置则设置为当前进程的环境变量- 接下来会调用
syscall.StartProcess
启动进程,不同操作系统在这里实现方式不一样 - 最后等待子进程执行完成,收集子进程信息,这个过程也是因系统而异,例如 Linux 系统是调用的
wait4
如果你熟悉 C 语言,那么创建子进程需要用到的系统调用也一定很熟悉,在 Linux 系统下,整个过程中主要涉及到 dup2
、fork
、exec
等。
接下来主要看一下创建子进程的核心流程 syscall.StartProcess
中如何组合使用这些系统调用的:
- 使用 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 调用
- Linux 系统中代码为:
- 通过
fork
的返回值判断当前在父进程中还是在子进程中,为 0 则为在子进程中 - 在子进程逻辑中,如果对子进程有某些设置要求,则进行设置调用,例如
chroot
、user
、groups
、chdir
等 - 在子进程逻辑中,执行 execve (opens new window) 系统调用,替换当前正在运行的子进程(因为是从父进程复制来的,和父进程代码段、堆栈段等一致,需要替换成要执行的程序,并做初始化)
以上是一个标准的创建子进程的流程。
# Go runtime 特殊逻辑
在创建进程的时候,有几个特殊的 runtime 函数:
func runtime_BeforeFork()
func runtime_AfterFork()
func runtime_AfterForkInChild()
1
2
3
2
3
# runtime_BeforeFork
解决 #18600 (opens new window) :
- M 的 locks++,locks 大于 0 的时候,runtime 一些操作会被禁止,例如禁止抢占;其他 +1 的情况例如,当前 M 进入系统调用,则会被 +1
关于 locks 的作用,可以参考 go 系统调用相关函数的注释:- proc.go#L3726 (opens new window)
reentersyscall
函数的注释 - 相关
Syscall
实现文件结构参考:README.md (opens new window)
- proc.go#L3726 (opens new window)
- 使用 sigprocmask (opens new window) 保存当前线程的 signal mask 到 GMP 的 M
sigmask
字段中(保存当前注册监听的信号) - 使用
sigprocmask
阻塞所有信号
内存问题:
- 设置一个标志位,目的是不允许 runtime 在 fork 和 exec 调用之间,执行栈增长或者申请内存(根本原因不清楚,猜测可能是 fork 相关内存的机制有关把。。)
# runtime_AfterFork
TODO
# runtime_AfterForkInChild
TODO
# clone 函数
TODO:docker 相关的使用、flag 参数
# 创建进程为什么要大费周章?
你可能会有疑问,为什么创建子进程没有一个简单的系统调用,而是需要很多步骤,其实也有单独的函数用于创建进程,例如 posix_spawn (opens new window)、system (opens new window)。但是本质上,这些函数也是对 fork
、exec
调用的封装。并不是用来取代标准的流程的。
说句题外话,Rust (opens new window) 中是使用 posix_spawn
和标准方式结合的方式创建进程的,而 Go(1.17)中只使用常规的方式。
# Windows 系统特殊的方式
Windows 下自然是很特殊的,没有那么多流程,只需要调用 CreateProcessA (opens new window) 函数即可,当然与之相关的还有一些列的函数,具体参考 Windows API (opens new window)。毕竟 Windows 并不符合 POSIX 标准。体现了操作系统 API 之间的差异。
# 参考文献
- Go by Example: Spawning Processes (opens new window)
- 用 seccomp 去限制 Go 语言中的 ForkExec 所产生的子进程 (opens new window)
- Why do we need to fork to create new processes? (opens new window)
- The difference between fork(), vfork(), exec() and clone() (opens new window)
- Difference Between fork() and vfork() (opens new window)