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.  } }


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM