Golang os/exec 實現

os/exec 實現了golang調用shell或者其餘OS中已存在的命令的方法. 本文主要是閱讀內部實現後的一些總結.git

若是要運行ls -rlt,代碼以下:github

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命令, 如:golang

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是高階庫,大概的調用關係以下:shell

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

二. (*Cmd).Start()主要處理如何與建立後的通訊. 好比如何將一個文檔內容做爲子進程的標準輸入, 如何獲取子進程的標準輸出.
這裏主要是經過pipe實現, 以下是處理子進程標準輸入的具體代碼註釋.bash

// 該函數返回子進程標準輸入對應的文檔信息. 在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掉.app

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系統調優並大幅優化性能. 代碼以下:gitlab

 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.  } }
相關文章
相關標籤/搜索