从源码角度剖析 golang 如何fork一个进程
从源码角度剖析 golang 如何fork一个进程
创建一个新进程分为两个步骤,一个是fork系统调用,一个是execve 系统调用,fork调用会复用父进程的堆栈,而execve直接覆盖当前进程的堆栈,并且将下一条执行指令指向新的可执行文件。
在分析源码之前,我们先来看看golang fork一个子进程该如何写。(严格的讲是先fork再execve创建一个子进程)
cmd := exec.Command("/bin/sh")
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
上述代码将fork一个子进程,然后子进程将会调用execve系统调用,使用新的可执行文件/bin/sh代替当前子进程的程序。并且当前的标准输入输出也传递给了子进程。
我们将着重看下golang是如何创建和将父进程的文件描述符传递给子进程的。
cmd.Run() 会调用到cmd.Start 方法,里面有一段逻辑和标准输入输出流的传递相关,我们来看看。
// /usr/local/go/src/os/exec/exec.go:625
func (c *Cmd) Start() error {
......
childFiles := make([]*os.File, 0, 3+len(c.ExtraFiles))
// 创建子进程的stdin 标准输入
stdin, err := c.childStdin()
if err != nil {
return err
}
childFiles = append(childFiles, stdin)
// 创建子进程的stdout 标准输出
stdout, err := c.childStdout()
if err != nil {
return err
}
childFiles = append(childFiles, stdout)
// 创建子进程的stderr 标准错误输出
stderr, err := c.childStderr(stdout)
if err != nil {
return err
}
// 此时childFiles 已经包含了上述3个标准输入输出流
childFiles = append(childFiles, stderr)
childFiles = append(childFiles, c.ExtraFiles...)
env, err := c.environ()
if err != nil {
return err
}
// os.StartProcess 将会启动一个子进程并从childFiles继承父进程的放入其中的文件描述符
c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{
Dir: c.Dir,
Files: childFiles,
Env: env,
Sys: c.SysProcAttr,
})
.....
}
如上所述,cmd.Start 会分别调用childStdin,childStdout,childStderr创建用于子进程的标准输入输出。来看看其中一个childStdin实现原理,其余childStdout,childStderr 实现原理也是和它类似的。
// /usr/local/go/src/os/exec/exec.go:489
func (c *Cmd) childStdin() (*os.File, error) {
.....
pr, pw, err := os.Pipe()
if err != nil {
return nil, err
}
c.childIOFiles = append(c.childIOFiles, pr)
c.parentIOPipes = append(c.parentIOPipes, pw)
// pw 写入的数据 来源于 c.Stdin 父进程会启动一个协程复制c.Stdin 到 pw
c.goroutine = append(c.goroutine, func() error {
_, err := io.Copy(pw, c.Stdin)
if skipStdinCopyError(err) {
err = nil
}
if err1 := pw.Close(); err == nil {
err = err1
}
return err
})
....
return pr, nil
}
childStdin 实际上是创建了一个管道,管道有返回值 pw,pr , 由pw写入的数据可以由pr进行读取,w 写入的数据 来源于 c.Stdin 父进程会启动一个协程复制c.Stdin 到 pw ,而c.Stdin 在我们最开的演示代码那里赋值为了标准输入。
cmd := exec.Command("/bin/sh")
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
而pr 则返回由父进程通过os.StartProcess的childFiles 传递给了子进程,并作为子进程的标准输入,当子进程启动后将会从pr中获取标准输入终端的数据。
看到这里,你应该能明白了,子进程是如何获取获取父进程的终端信息的了,通过建立了一个管道,然后将管道的一端传递给了子进程便能让父子进程进行通信了。
让我们再回到创建进程的主流程上,刚刚仅仅是分析出了,父进程将会为子进程创建它自己的标准输入输出流,虽然是通过管道包装的,但还没详细分析出os.StartProcess 方法究竟通过了哪些手段来让父进程的文件描述符传递给子进程。
注意下,golang中 fork 和execve 创建子进程 的过程 被封装成了一个统一的方法forkExec,它能够控制子进程,只继承特定的文件描述符,而对其他文件描述符则进行关闭。而内核fork系统调用则是会对父进程的所有文件描述符进行复制,那么golang又是如何做到只继承特定的文件描述符的呢?这个也是接下来分析的重点
接下来,让我们深入os.StartProcess 方法,看看golang是如何办到只继承父进程通过childFiles传递过来的文件描述符进行fork和execve调用的。
os.StartProcess 底层会调用到 forkAndExecInChild1 方法,由于代码比较长,我这里只列出了关键步骤,并对其进行了注释。
func forkAndExecInChild1(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr *ProcAttr, sys *SysProcAttr, pipe int) (r1 uintptr, err1 Errno, p [2]int, locked bool) {
...
// fork 调用前 会将attr.Files 里的数据复制到fd数组,我们传递给子进程的是childFiles,当代码执行到这里的时候,childFiles已经转化成了文件描述符存到attr.Files了。nextfd是为了后续再进行复制文件描述符时,不会对子进程要用到的文件描述符进行覆盖,会在接下来步骤1进行详细说明
nextfd = len(attr.Files)
for i, ufd := range attr.Files {
if nextfd < int(ufd) {
nextfd = int(ufd)
}
fd[i] = int(ufd)
}
nextfd++
.....
// 这里便进行了fork调用创建新进程了,不过可以看到这里用的是clone系统调用,其实它和fork类似,不过区别在于clone系统调用可以通过flags指定新进程 对于 父进程的哪些属性需要继承,哪些属性不需要继承,比如子进程需要新的网络命名空间,则需要指定flags为syscall.CLONE_NEWNS
r1, err1 = rawVforkSyscall(SYS_CLONE, flags, 0)
....
// 步骤1: 总之经过上面clone系统调用,已经产生了子进程了,下面两个步骤都是子进程才会进行的步骤,父进程在上述clone系统调用后,通过判断err1 != 0 || r1 != 0 便返回了。
// 这里将fd[i] < i 的文件描述符 通过dup 系统调用复制到了一个新的文件描述符,因为后续步骤2里我们需要将复制 fd[i] 到第i个文件描述符 ,如果fd[i] < i ,那么将会导致复制的fd[i] 是子进程已经产生复制行为的文件描述符,而不是父进程真正传递过来的文件描述符,所以要通过nextfd将这样的文件描述符复制到fd数组外,并且设置O_CLOEXEC,这样在后续的execve系统调用后,将会对它进行自动关闭。
for i = 0; i < len(fd); i++ {
if fd[i] >= 0 && fd[i] < i {
....
_, _, err1 = RawSyscall(SYS_DUP3, uintptr(fd[i]), uintptr(nextfd), O_CLOEXEC)
if err1 != 0 {
goto childerror
}
fd[i] = nextfd
nextfd++
}
}
....
// 步骤2 : 遍历fd 让 子进程fd[i] 个文件描述符复制给第i个文件描述符 ,注意这里就没有设置O_CLOEXEC了,因为这里的文件描述符我们希望execve后还存在
for i = 0; i < len(fd); i++ {
....
_, _, err1 = RawSyscall(SYS_DUP3, uintptr(fd[i]), uintptr(i), 0)
if err1 != 0 {
goto childerror
}
}
....
// 进行execve 系统调用
_, _, err1 = RawSyscall(SYS_EXECVE,
uintptr(unsafe.Pointer(argv0)),
uintptr(unsafe.Pointer(&argv[0])),
uintptr(unsafe.Pointer(&envv[0])))
}
可以看出,golang在execve前, 通过dup系统调用达到了继承父进程文件描述符的目的,最终达到的效果是继承attr.Files 参数里的文件描述符,期间由于dup的使用 产生的多余的文件描述符也标记为了O_CLOEXEC,在SYS_EXECVE 系统调用时,便会关闭掉。
但是仅仅看到这里,并不能说明golang会对attr.Files外的文件描述符也进行关闭,因为fork系统调用时,子进程会自动继承父进程的所有文件描述符,这些继承的文件描述符会在execve后自动关闭吗? 答案是默认是会的。
golang的 os.open 函数底层会调用下面的代码对文件进行打开操作,可以看到打开时固定设置了syscall.O_CLOEXEC flag,所以,子进程进行execve时变会自动对这些文件描述符进行关闭了。
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
setSticky := false
if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {
if _, err := Stat(name); IsNotExist(err) {
setSticky = true
}
}
var r int
for {
var e error
r, e = syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
if e == nil {
监听的socket文件也是默认开启了syscall.SOCK_NONBLOCK参数
// descriptor as nonblocking and close-on-exec.
func sysSocket(family, sotype, proto int) (int, error) {
s, err := socketFunc(family, sotype|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, proto)
if err != nil {
return -1, os.NewSyscallError("socket", err)
}
return s, nil
}
从源码角度剖析 golang 如何fork一个进程的更多相关文章
- ArrayList 从源码角度剖析底层原理
本篇文章已放到 Github github.com/sh-blog 仓库中,里面对我写的所有文章都做了分类,更加方便阅读.同时也会发布一些职位信息,持续更新中,欢迎 Star 对于 ArrayList ...
- 从JDK源码角度看Short
概况 Java的Short类主要的作用就是对基本类型short进行封装,提供了一些处理short类型的方法,比如short到String类型的转换方法或String类型到short类型的转换方法,当然 ...
- 从JDK源码角度看Byte
Java的Byte类主要的作用就是对基本类型byte进行封装,提供了一些处理byte类型的方法,比如byte到String类型的转换方法或String类型到byte类型的转换方法,当然也包含与其他类型 ...
- 从JDK源码角度看Object
Java的Object是所有其他类的父类,从继承的层次来看它就是最顶层根,所以它也是唯一一个没有父类的类.它包含了对象常用的一些方法,比如getClass.hashCode.equals.clone. ...
- 从JDK源码角度看Boolean
Java的Boolean类主要作用就是对基本类型boolean进行封装,提供了一些处理boolean类型的方法,比如String类型和boolean类型的转换. 主要实现源码如下: public fi ...
- 从源码角度深入理解Toast
Toast这个东西我们在开发中经常用到,使用也很简单,一行代码就能搞定: 1: Toast.makeText(", Toast.LENGTH_LONG).show(); 但是我们经常会遇到这 ...
- Android -- 带你从源码角度领悟Dagger2入门到放弃
1,以前的博客也写了两篇关于Dagger2,但是感觉自己使用的时候还是云里雾里的,更不谈各位来看博客的同学了,所以今天打算和大家再一次的入坑试试,最后一次了,保证最后一次了. 2,接入项目 在项目的G ...
- Android -- 带你从源码角度领悟Dagger2入门到放弃(二)
1,接着我们上一篇继续介绍,在上一篇我们介绍了简单的@Inject和@Component的结合使用,现在我们继续以老师和学生的例子,我们知道学生上课的时候都会有书籍来辅助听课,先来看看我们之前的Stu ...
- 从源码角度入手实现RecyclerView的Item点击事件
RecyclerView 作为 ListView 和 GridView 的替代产物,相信在Android界已广为流传. RecyclerView 本是不会有类似 ListView 的那种点击事件,但是 ...
- 从template到DOM(Vue.js源码角度看内部运行机制)
写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(https://github.com/answershuto/learnVue)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些 ...
随机推荐
- Kafka 集群调优
更多内容,前往 IT-BLOG 单个 kafka服务器足以满足本地开发或 POC要求,使用集群的最大好处是可以跨服务器进行负载均衡,再则就是可以使用复制功能来避免因单点故障造成的数据丢失.在维护 Ka ...
- Tomcat启动JSP项目,搞起来了
虽然有点复古,但是还是有很多小伙伴在使用的,小编来一篇保姆级教程 1.用idea打开jsp项目 2.添加tomcat配置 3.点击后会出现配置框,这里画框的地方都选上,版本选择1.8,其他的信息内容默 ...
- Sound Joy连接非华为手机热点
很简单,准备两个手机.常用手机和备用手机1.常用手机开启热点2.备用手机连接常用手机的热点,并且已经安装华为智慧生活app3.备用手机连接到常用手机热点后,再打开智慧生活app连接常用手机热点即可
- 【Visual Leak Detector】QT 中 VLD 输出解析(一)
说明 使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记. 目录 说明 1. 使用方式 2. 无内存泄漏时的输出报告 1. 使用方式 在 QT 中使用 VLD 的方法可以查看另外几篇博客: [Vi ...
- C#自定义事件(简单版本)
C#中的事件分为两种:一种是厂商微软在VS中已经内置,以供用户使用:另一种是有用户自己定义的事件: 先简单回顾下第一种: [场景1]一个Form上一个Textbox控件和Button控件,当用户按下B ...
- [IDE]IDEA build artifacts过程很慢的解决方案[转载]
解决方案 可能1 可能是缓存的文件太多了导致: File->Invalidate Caches /Restart,清理缓存, 并重启IDEA.重启之后,会重建索引, 此过程较慢, 但build的 ...
- linux shell 自动化部署 npm vue 项目
此 shell 是提供给前端登录服务器自动化部署 vue 项目的 用此命令,工具化部署项目,可以杜绝前端自己部署项目时,对服务器违规操作 如有其它问题,可在下方留言! #!/bin/sh # url: ...
- mysql 清空数据表id 重1开始 帝国cms清空数据表id 重1开始
alter table phome_ecms_news auto_increment=1; alter table phome_ecms_news_check auto_increment=1; al ...
- 每天掌握10道面试题,轻轻松松去面试(Yes, that's right, I'm kidding)!!!
一.4.12 1.说一说cookie sessionStorage localStorage 是什么,有什么区别? Cookie.sessionStorage 和 localStorage 都是在浏览 ...
- Go语言实现协程下载器
一般常用的下载方式是通过浏览器访问URL,然后基于HTTP进行下载.这种单线程下载方式通常比较慢,这里尝试使用Go语言实现一个多协程的下载器. 大致思路 按照传统的单线程的思路,实现下载要基于HTTP ...