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

Golang os/exec 实现的更多相关文章

  1. golang os/exec 执行外部命令

    exec包执行外部命令,它将os.StartProcess进行包装使得它更容易映射到stdin和stdout,并且利用pipe连接i/o. func LookPath(file string) (st ...

  2. [golang][译]使用os/exec执行命令

    [golang][译]使用os/exec执行命令 https://colobu.com/2017/06/19/advanced-command-execution-in-Go-with-os-exec ...

  3. golang中os/exec包用法

    exec包执行外部命令,它将os.StartProcess进行包装使得它更容易映射到stdin和stdout,并且利用pipe连接i/o. 1.func LookPath(file string) ( ...

  4. golang语言中os/exec包的学习与使用

    package main; import ( "os/exec" "fmt" "io/ioutil" "bytes" ) ...

  5. golang 通过exec Command启动的进程如何关闭的解决办法 以及隐藏黑色窗口

    golang 通过exec Command启动的进程如何关闭的解决办法 在用exec包调用的其他进程后如何关闭结束,可以使用context包的机制进行管理,context包的使用详见:https:// ...

  6. go os/exec执行外部程序

    Go提供的os/exec包可以执行外部程序,比如调用系统命令等. 最简单的代码,调用pwd命令显示程序当前所在目录: package main import ( "fmt" &qu ...

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

    execl(file, arg0,arg1,...) 用参数列表arg0, arg1 等等执行文件 execv(file, arglist) 除了使用参数向量列表,其他的和execl()相同 exec ...

  8. [go]os/exec执行shell命令

    // exec基础使用 import ( "os/exec" ) cmd = exec.Command("C:\\cygwin64\\bin\\bash.exe" ...

  9. python之os.exec*族用法简结

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

随机推荐

  1. 2020牛客寒假算法基础集训营3 B 牛牛的DRB迷宫II

    题目描述 牛牛有一个n*m的迷宫,对于迷宫中的每个格子都为'R','D','B'三种类型之一,'R'表示处于当前的格子时只能往右边走'D'表示处于当前的格子时只能往下边走,而'B'表示向右向下均可以走 ...

  2. 干货 | 用Serverless快速在APP中构建调研问卷

    Serverless 计算将会成为云时代默认的计算范式,并取代 Serverful (传统云)计算模式,因此也就意味着服务器 -- 客户端模式的终结. ------<简化云端编程:伯克利视角下的 ...

  3. 简单模拟B1001

    #include<iostream> using namespace std; int main() { int n; ; cin >> n; ) { == ) { n = ( ...

  4. POJ 1789:Truck History

    Truck History Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 21376   Accepted: 8311 De ...

  5. python 爬虫 多线程 多进程

    一.程序.进程和线程的理解  程序:就相当于一个应用(app),例如电脑上打开的一个程序. 进程:程序运行资源(内存资源)分配的最小单位,一个程序可以有多个进程. 线程:cpu最小的调度单位,必须依赖 ...

  6. AttributeError: module 'selenium.webdriver.common.service' has no attribute 'Service'

    今天爬虫时需要使用到selenium, 使用pip install selenium进行安装. 可是一开始写程序就遇到了AttributeError: module 'selenium.webdriv ...

  7. nginx 报错Malformed HTTP request line, git 报错fatal: git-write-tree: error building trees

    nginx 报错由于url里有空格,包括url本身或者参数有空格 git 报错是因为解决冲突的时候没有add,即没有merge

  8. ZJNU 1244/1245 - 森哥数——高级

    打表找规律吧…… 一定要记得每一步都得开long long 然后可以发现所有的森哥数每一位只可能是0,1,2,3 就可以想到最高O(3^9)的算法 枚举1e9之内的所有满足条件的数判断 枚举9位数,最 ...

  9. Linux进程的诞生和消亡

    1.进程的诞生 (1).进程0和进程1 (内核里边的固有的) (2).fork函数和vfork函数用于新进程的产生 2.进程的消亡 (1).正常终止和异常终止 (2).进程在运行时需要消耗系统资源(内 ...

  10. Linux进程的引入

    1.什么是进程? (1).进程是一个动态过程而不是静态实物 (2).进程就是程序的一次运行过程,一个静态的可执行程序a.out的一次运行过程(./a.out从运行到结束)就是一个进程. (3).进程控 ...