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

原文地址:https://www.cnblogs.com/lijianming180/p/12014127.html

时间: 2024-07-28 22:36:34

Golang os/exec 实现的相关文章

python os.exec*()家族函数的用法

execl(file, arg0,arg1,...) 用参数列表arg0, arg1 等等执行文件 execv(file, arglist) 除了使用参数向量列表,其他的和execl()相同 execle(file, arg0,arg1,... env) 和execl 相同,但提供了环境变量字典env execve(file,arglist, env) 除了带有参数向量列表,其他的和execle()相同 execlp(cmd, arg0,arg1,...) 于execl()相同,但是在用户的搜索

go语言中os/exec包的学习与使用

package main; import ( "os/exec" "fmt" "io/ioutil" "bytes" ) func main() { //在环境变量path中查找可执行二进制文件 //返回完整路径或者相对于当前目录的一个相对路径 file, _ := exec.LookPath("go"); fmt.Println(file); //返回一个cmd cmd := exec.Command(&

os/exec

package main import ( "bytes" "fmt" "io/ioutil" "os/exec" ) func main() { //搜索可执行的二进制文件路径 f, err := exec.LookPath("php") fmt.Println(f, err) argv := []string{"php"} c := exec.Command("which&

转---python os.exec*()家族函数的用法

execl(file, arg0,arg1,...) 用参数列表arg0, arg1 等等执行文件 execv(file, arglist) 除了使用参数向量列表,其他的和execl()相同 execle(file, arg0,arg1,... env) 和execl 相同,但提供了环境变量字典env execve(file,arglist, env) 除了带有参数向量列表,其他的和execle()相同 execlp(cmd, arg0,arg1,...) 于execl()相同,但是在用户的搜索

python之os.exec*族用法简结

os.exec*族主要用来代替当前进程,执行新的程序,不返回值.在UNIX上,新的执行程序加载到当前进程,与调用它的进程有相同的id. os.execl(path, arg0, arg1, ...) os.execle(path, arg0, arg1, ..., env) os.execlp(file, arg0, arg1, ...) os.execlpe(file, arg0, arg1, ..., env)o s.execv(path, args) os.execve(path, arg

Golang os/user 用户模块

os/user 用户模块 导入语法: import "os/user" 模块概述 os/user 模块的主要作用是通过用户名或者 id 从而获取其相关属性. User 结构体 type User struct { Uid string Gid string Username string Name string HomeDir string } User 代表一个用户账户. Uid :用户的 ID Gid :用户所属组的 ID,如果属于多个组,那么此 ID 为主组的 ID Userna

『Golang』—— 标准库之 os

Golang 的 os 库基本承袭 Unix 下 C 语言的用法 path 库: func Base(path string) string //取文件名,不含目录部分 func Dir(path string) string //取路径中的目录名部分,不含文件名 func Join(elem ...string) string //拼接字段,中间自动添加 '/' os 库: 1 ackage main 2 3 import "os" 4 import "os/exec&qu

golang+linux+pipline

golang里可以通过[os/exec]包调用linux里的命令,然而对于linux里的管道(“|”)却不能直接使用.所以写了一个管道的接口如下. func Pipline(cmds ...*exec.Cmd) ([]byte, []byte, error) { // At least one command if len(cmds) < 1 { return nil, nil, nil } var output bytes.Buffer var stderr bytes.Buffer var

golang使用graphviz

graphviz的介绍请参考: http://www.cnblogs.com/ghj1976/p/4539788.html  安装 graphviz 需要在 http://www.graphviz.org/Download_macos.php 下载对应操作系统的版本. 安装完成后,可以用 dot –V  命令查看安装的版本,确认安装 mac 下安装后,会有一个 Graphviz 的应用可以查看 *.gv 的文件, 如下图效果所示:   产生Graphviz 文件 用 Golang 产生Graph