从源码角度剖析 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的原理及集群部署详解
kafka原理详解 消息队列概述 消息队列分类 点对点 组成:消息队列(Queue).发送者(Sender).接收者(Receiver) 特点:一个生产者生产的消息只能被一个接受者接收,消息一旦被消费 ...
- BGF bivariate generating function 双变量生成函数
目录 定义 BGF bivariate generating function horizonal GF 和 vertical GF 例子 组合数 horizonal GF vertical GF ( ...
- mybatis-spring注解MapperScan的原理
很多开发者应该都知道,我们只使用@MapperScan这个注解就可以把我们写的Mybatis的Mapper接口加载到Spring的容器中,不需要对每个Mapper接口加@Mapper这个注解了,加快了 ...
- java中foreach循环用法详解
前言 在前面的文章中,千锋壹哥给大家讲解了for.while.do-while三种循环结构,并讲解了如何跳出循环的几种方式,比如break.continue.return等.但是截止到目前,与循环相关 ...
- ChannelInboundHandlerAdapter 与 SimpleChannelInboundHandler 功能详解
SimpleChannelInboundHandler [类的关系]:如下就是两个类的声明,SimpleChannelInboundHandler是继承 ChannelInboundHandlerAd ...
- PTA题目总结
(1)前言:第一次题目集主要考察JAVA的一些语法知识,比如,控制台的输入,输出时保留两位小数,数组的使用,第十题有点难度,当时没写出来,现在想想 也还好,就是读懂题目有点费劲,第一次题目的题量比较大 ...
- PHP微信三方平台-微信支付(扫码支付)
1.官方文档地址: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1 2.逻辑分析: 生成支付二维码->用户扫码支付-& ...
- pandas之分类操作
通常情况下,数据集中会存在许多同一类别的信息,比如相同国家.相同行政编码.相同性别等,当这些相同类别的数据多次出现时,就会给数据处理增添许多麻烦,导致数据集变得臃肿,不能直观.清晰地展示数据. 针对上 ...
- 补五月三号java基础知识
1.泛型技术可以通过一种类型或方法操纵各种不同类型的对象,同时又提供了编译时的类型安全保证.2.容器(即集合)是以类库形式 提供的多种数据结构,用户在编程时可直接使用3.泛型其实质就是将数据的类型参数 ...
- day107:MoFang:Python操作MongoDB数据库:PyMongo
目录 PyMongo 1.PyMongo安装 2.数据库连接 3.数据库管理 4.集合管理 5.文档管理 PyMongo 1.PyMongo安装 pip install pymongo 2.数据库连接 ...