从源码角度剖析 golang 如何fork一个进程

这篇具有很好参考价值的文章主要介绍了从源码角度剖析 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参数文章来源地址https://www.toymoban.com/news/detail-467319.html

// 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一个进程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Go For Web:Golang http 包详解(源码剖析)

    本文作为解决如何通过 Golang 来编写 Web 应用这个问题的前瞻,对 Golang 中的 Web 基础部分进行一个简单的介绍。目前 Go 拥有成熟的 Http 处理包,所以我们去编写一个做任何事情的动态 Web 程序应该是很轻松的,接下来我们就去学习了解一些关于 Web 的相关基础,了解一些概念,

    2023年04月14日
    浏览(34)
  • 《ARM Linux内核源码剖析》读书笔记——0号进程(init_task)的创建时机

    最近在读《ARM Linux内核源码剖析》,一直没有看到0号进程(init_task进程)在哪里创建的。直到看到下面这篇文章才发现书中漏掉了set_task_stack_end_magic(init_task)这行代码。 下面这篇文章提到:start_kernel()上来就会运行 set_task_stack_end_magic(init_task)创建初始进程。init_task是静态定义的

    2024年01月17日
    浏览(63)
  • 【Linux进程】查看进程&&fork创建进程

    目录 前言  1. 查看进程  2. 通过系统调用创建进程-fork初识 总结          你有没有想过在使用Linux操作系统时,后台运行的程序是如何管理的?在Linux中,进程是一个非常重要的概念。本文将介绍如何查看当前运行的进程,并且讨论如何使用fork创建新的进程。通过了解这些

    2024年01月22日
    浏览(52)
  • Golang的Fork/Join实现

    做过Java开发的同学肯定知道,JDK7加入的Fork/Join是一个非常优秀的设计,到了JDK8,又结合并行流中进行了优化和增强,是一个非常好的工具。 Fork/Join本质上是一种任务分解,即:将一个很大的任务分解成若干个小任务,然后再对小任务进一步分解,直到最小颗粒度,然后并发

    2024年02月08日
    浏览(35)
  • 【Linux初阶】fork进程创建 & 进程终止 & 进程等待

     🌟hello,各位读者大大们你们好呀🌟 🍭🍭系列专栏:【Linux初阶】 ✒️✒️本篇内容:fork进程创建,理解fork返回值和常规用法,进程终止(退出码、退出场景、退出方法、exit),进程等待(wait、waitpid),阻塞等待和非阻塞等待 🚢🚢作者简介:本科在读,计算机海洋

    2024年02月06日
    浏览(47)
  • 【Linux】进程查看|fork函数|进程状态

    🦄 个人主页—— 🎐 开着拖拉机回家_Linux,大数据运维-CSDN博客 🎐✨🍁 🪁🍁🪁🍁🪁🍁🪁🍁 🪁🍁🪁🍁🪁🍁🪁 🪁🍁🪁🍁🪁🍁🪁🍁🪁🍁🪁🍁 感谢点赞和关注 ,每天进步一点点!加油! 目录 一、基本概念 1.1 概念提出 1.2 特征 二、描述进程-PCB 2.1 什么是进程

    2024年02月04日
    浏览(42)
  • Linux中的进程、fork、进程状态、环境变量

            进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct 在Linux中描述进程的结构体叫做task_struct。task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包

    2024年02月10日
    浏览(42)
  • fork()函数创建子进程

    fork()用于创建子进程, 一次调用 会有 两个返回 (return),一次返回给父进程子进程的PID(Process ID),一次返回给子进程,其返回值为0. 返回值=0,子进程在运行 返回值0,父进程在运行 返回值0,fork()调用出错 进程获取 自己的PID:getpid() 进程获取 父进程PID:getppid() 由于一个进

    2023年04月08日
    浏览(38)
  • Fork() 函数:“父” 与 “子” 进程的交互(进程的创建)

    前面我们讲了C语言的基础知识,也了解了一些数据结构,并且讲了有关C++的一些知识,也学习了一些Linux的基本操作,也了解并学习了有关Linux开发工具vim 、gcc/g++ 使用、yum工具以及git 命令行提交代码也相信大家都掌握的不错,上一篇文章我们了解了关于进程的基本概念,今

    2024年02月08日
    浏览(34)
  • fork之后是子进程先执行还是父进程先执行

    CFS(完全公平调度器)是 Linux 内核2.6.23版本开始采用的进程调度器,它的基本原理是这样的:设定一个调度周期(sched_latency_ns),目标是让每个进程在这个周期内至少有机会运行一次,换一种说法就是每个进程等待CPU的时间最长不超过这个调度周期。然后根据进程的数量,大家平

    2024年02月06日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包