zoukankan      html  css  js  c++  java
  • Golang os/exec 实现

    os/exec 实现了golang调用shell或者其他OS中已存在的命令的方法. 本文主要是阅读内部实现后的一些总结.

    如果要运行ls -rlt,代码如下:

    package main
    
    import (
    	"fmt"
    	"log"
    	"os/exec"
    )
    
    func main() {
    
    	cmd := exec.Command("ls", "-rlt")
    	stdoutStderr, err := cmd.CombinedOutput()
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Printf("%sn", stdoutStderr)
    }

    如果要运行ls -rlt /root/*.go, 使用cmd := exec.Command("ls", "-rlt", "/root/*.go")是错误的.
    因为底层是直接使用系统调用execve的.它并不会向Shell那样解析通配符. 变通方案为golang执行bash命令, 如:

    package main
    
    import (
    	"fmt"
    	"log"
    	"os/exec"
    )
    
    func main() {
    
    	cmd := exec.Command("bash", "-c","ls -rlt /root/*.go")
    	stdoutStderr, err := cmd.CombinedOutput()
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Printf("%sn", stdoutStderr)
    }

    源码分析

    一. os/exec是高阶库,大概的调用关系如下:

                                                                     
                             +----------------+                      
                             | (*Cmd).Start() |                      
                             +----------------+                      
                                     |                               
                                     v                               
      +-------------------------------------------------------------+
      | os.StartProcess(name string, argv []string, attr *ProcAttr) |
      +-------------------------------------------------------------+
                                     |                               
                                     v                               
              +-------------------------------------------+          
              | syscall.StartProcess(name, argv, sysattr) |          
              +-------------------------------------------+          
    

    二. (*Cmd).Start()主要处理如何与创建后的通信. 比如如何将一个文档内容作为子进程的标准输入, 如何获取子进程的标准输出.
    这里主要是通过pipe实现, 如下是处理子进程标准输入的具体代码注释.

    // 该函数返回子进程标准输入对应的文档信息. 在fork/exec后子进程里面将其对应的文档描述符设置为0
    func (c *Cmd) stdin() (f *os.File, err error) {
        // 如果没有定义的标准输入来源, 则默认是/dev/null
    	if c.Stdin == nil {
    		f, err = os.Open(os.DevNull)
    		if err != nil {
    			return
    		}
    		c.closeAfterStart = append(c.closeAfterStart, f)
    		return
    	}
    
        // 如果定义子进程的标准输入为父进程已打开的文档, 则直接返回
    	if f, ok := c.Stdin.(*os.File); ok {
    		return f, nil
    	}
    
        // 如果是其他的,比如实现了io.Reader的一段字符串, 则通过pipe从父进程传入子进程
        // 创建pipe, 成功execve后,在父进程里关闭读. 从父进程写, 从子进程读.
        // 一旦父进程获取子进程的结果, 即子进程运行结束, 在父进程里关闭写.
    	pr, pw, err := os.Pipe()
    	if err != nil {
    		return
    	}
    
    	c.closeAfterStart = append(c.closeAfterStart, pr)
        c.closeAfterWait = append(c.closeAfterWait, pw)
        
        // 通过goroutine将c.Stdin的数据写入到pipe的写端
    	c.goroutine = append(c.goroutine, func() error {
    		_, err := io.Copy(pw, c.Stdin)
    		if skip := skipStdinCopyError; skip != nil && skip(err) {
    			err = nil
    		}
    		if err1 := pw.Close(); err == nil {
    			err = err1
    		}
    		return err
    	})
    	return pr, nil
    }

    三. golang里使用os.OpenFile打开的文档默认是`close-on-exec”
    除非它被指定为子进程的标准输入,标准输出或者标准错误输出, 否则在子进程里会被close掉.

    file_unix.go里是打开文档的逻辑:

    // openFileNolog is the Unix implementation of OpenFile.
    // Changes here should be reflected in openFdAt, if relevant.
    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 {
    			break
    		}

    如果要让子进程继承指定的文档, 需要使用大专栏  Golang os/exec 实现de>ExtraFiles字段

    func main() {
    	a, _ := os.Create("abc")
    	cmd := exec.Command("ls", "-rlt")
    	cmd.ExtraFiles = append(cmd.ExtraFiles, a)
    	stdoutStderr, err := cmd.CombinedOutput()
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Printf("%sn", stdoutStderr)
    }

    四. 当父进程内存特别大的时候, fork/exec的性能非常差, golang使用clone系统调优并大幅优化性能. 代码如下:

    	locked = true
    	switch {
    	case runtime.GOARCH == "amd64" && sys.Cloneflags&CLONE_NEWUSER == 0:
    		r1, err1 = rawVforkSyscall(SYS_CLONE, uintptr(SIGCHLD|CLONE_VFORK|CLONE_VM)|sys.Cloneflags)
    	case runtime.GOARCH == "s390x":
    		r1, _, err1 = RawSyscall6(SYS_CLONE, 0, uintptr(SIGCHLD)|sys.Cloneflags, 0, 0, 0, 0)
    	default:
    		r1, _, err1 = RawSyscall6(SYS_CLONE, uintptr(SIGCHLD)|sys.Cloneflags, 0, 0, 0, 0, 0)
    	}

    网上有很多关于讨论该性能的文章:
    https://zhuanlan.zhihu.com/p/47940999
    https://about.gitlab.com/2018/01/23/how-a-fix-in-go-19-sped-up-our-gitaly-service-by-30x/
    https://github.com/golang/go/issues/5838

    五. 父进程使用pipe来探测在创建子进程execve时是否有异常.
    syscall/exec_unix.go中. 如果execve成功,则该pipe因close-on-exec在子进程里自动关闭.

    	// Acquire the fork lock so that no other threads
    	// create new fds that are not yet close-on-exec
    	// before we fork.
    	ForkLock.Lock()
    
    	// Allocate child status pipe close on exec.
    	if err = forkExecPipe(p[:]); err != nil {
    		goto error
    	}
    
    	// Kick off child.
    	pid, err1 = forkAndExecInChild(argv0p, argvp, envvp, chroot, dir, attr, sys, p[1])
    	if err1 != 0 {
    		err = Errno(err1)
    		goto error
    	}
    	ForkLock.Unlock()
    
    	// Read child error status from pipe.
    	Close(p[1])
    	n, err = readlen(p[0], (*byte)(unsafe.Pointer(&err1)), int(unsafe.Sizeof(err1)))
    	Close(p[0])

    六. 当子进程运行完后, 使用系统调用wait4回收资源, 可获取exit code,信号rusage使用量等信息.
    七. 有超时机制, 如下例子是子进程在5分钟没有运行时也返回. 不会长时间阻塞进程.

    package main
    
    import (
    	"context"
    	"os/exec"
    	"time"
    )
    
    func main() {
    	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
    	defer cancel()
    
    	if err := exec.CommandContext(ctx, "sleep", "5").Run(); err != nil {
    		// This will fail after 100 milliseconds. The 5 second sleep
    		// will be interrupted.
    	}
    }

    具体是使用context库实现超时机制. 一旦时间达到,就给子进程发送kill信号,强制中止它.

    	if c.ctx != nil {
    		c.waitDone = make(chan struct{})
    		go func() {
    			select {
    			case <-c.ctx.Done():
    				c.Process.Kill()
    			case <-c.waitDone:
    			}
    		}()
    	}

    八. 假设调用一个脚本A, A有会调用B. 如果此时golang进程超时kill掉A, 那么B就变为pid为1的进程的子进程.
    有时这并不是我们所希望的.因为真正导致长时间没返回结果的可能是B进程.所有更希望将A和B同时杀掉.
    在传统的C代码里,我们通常fork进程后运行setsid来解决. 对应golang的代码为:

    func main() {
    	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
    	defer cancel()
    	cmd := exec.CommandContext(ctx, "sleep", "5")
    	cmd.SysProcAttr.Setsid = true
    
    	if err := cmd.Run(); err != nil {
    		// This will fail after 100 milliseconds. The 5 second sleep
    		// will be interrupted.
    	}
    }
  • 相关阅读:
    python之类的详解
    flask中cookie和session介绍
    Flask数据库的基本操作
    CSRF原理
    Ajax.2
    浅谈Ajax
    Django中的缓存机制
    Django简介
    HTTP协议
    web应用
  • 原文地址:https://www.cnblogs.com/lijianming180/p/12014127.html
Copyright © 2011-2022 走看看