一、syscall.Exec()

函数原型
func Exec(argv0 string, argv []string, envv []string) (err error)

execve(2)

此方法会将在当前进程空间里,用新的程序覆盖掉当前程序,并执行新的程序,它们依然在同一个进程里,只是进程的内容发生了变化。

main11.go

package main

import (
	"fmt"
	"syscall"
)

func main() {
	// 打印当前进程号
	fmt.Println(syscall.Getpid())
}

go build main11.go 得到 main11 可执行文件

main10.go

package main

import (
	"fmt"
	"os"
	"os/exec"
	"syscall"
)

func main() {
	fmt.Println("11111")
	fmt.Println(syscall.Getpid())

	f3()

	// 后面的依然属于当前进程的内容,不会被执行到
	fmt.Println("22222")
}

func f3() {
	// LookPath获得绝对地址;参数可以是绝对路径或相对路径。
	// binary 应该是一个可执行文件,对于全局的命令,也需要找到其具体的命令文件的位置。
	binary, lookErr := exec.LookPath("./main11")
	if lookErr != nil {
		panic(lookErr)
	}

	// 不需要参数
	args := []string{""}

	// 使用当前进程的环境变量
	env := os.Environ()

	// 执行,并进入新的程序
	execErr := syscall.Exec(binary, args, env)
	if execErr != nil {
		panic(execErr)
	}

	// 后面的依然属于当前进程的内容,不会被执行到
}

go run main10.go

输出
11111
4758
4758

可见它们是在同一个进程中执行的。

二、os.StartProcess()

函数原型
func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error)

函数说明
StartProcess starts a new process with the program, arguments and attributes specified by name, argv and attr. The argv slice will become os.Args in the new process, so it normally starts with the program name.

If the calling goroutine has locked the operating system thread with runtime.LockOSThread and modified any inheritable OS-level thread state (for example, Linux or Plan 9 name spaces), the new process will inherit the caller’s thread state.

StartProcess is a low-level interface. The os/exec package provides higher-level interfaces.

If there is an error, it will be of type *PathError.

fork & exec守护进程
func (*Process) Killfunc (*Process) Releasefunc (*Process) Signalfunc (*Process) Wait回收子进程,防止出现僵尸进程

main11.go

package main

import (
	"fmt"
	"os"
	"strconv"
)

func main() {
	f, err := os.OpenFile("note.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, os.ModePerm)
	if err != nil {
		panic(err)
	}

	f.WriteString("parent: " + strconv.Itoa(os.Getppid()))
	f.WriteString("\n")
	f.WriteString("son: " + strconv.Itoa(os.Getpid()))
	fmt.Println()

	f.Close()
}

go build main11.go 得到 main11 可执行文件。

main10.go

package main

import (
	"fmt"
	"os"
	"os/exec"
	"syscall"
	"time"
)

func main() {
	fmt.Println("11111")
	fmt.Println(syscall.Getpid())

	f4()

	fmt.Println("22222")
	time.Sleep(time.Second * 10)
}

func f4() {
	binary, lookErr := exec.LookPath("./main11")
	if lookErr != nil {
		panic(lookErr)
	}
	args := []string{""}
	pro, err := os.StartProcess(binary, args, &os.ProcAttr{})
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("son ", pro.Pid)
}

打印信息
11111
5989
son 5994
22222

日志信息
parent: 5989
son: 5994

Wait
// Wait waits for the Process to exit, and then returns a
// ProcessState describing its status and an error, if any.
// Wait releases any resources associated with the Process.
// On most operating systems, the Process must be a child
// of the current process or an error will be returned.
func (p *Process) Wait() (*ProcessState, error) {
	return p.wait()
}

在各个编程语言中,这都是一样的,当前程序调起的进程视为子进程,如果父进程先于子进程结束,那么子进程成为孤儿进程,被初始化进程接管(pid=1),如果子进程先于父进程结束,而父进程并没有处理子进程结束的信号,那么子进程将死不瞑目,成为僵尸进程,无法被kill掉,除非父进程结束了(僵尸进程会随之消失);所以在某些场景下,是需要回收子进程的,也就是调用 Wait 方法。

它的作用是:如果子进程结束,那么子进程会有退出码 exit code,Stderr,Srdout 等信息,操作系统就会给父进程发送 SIGCHLD信号,父进程需要阻塞等待,并处理这个信号;

三、os/exec 包

此包是对 StartProcess 的封装,并提供更高级的功能,推荐使用此方法。
并且可以通过 cmd.Process 对象控制子进程。

依然使用上面的 main11 可执行程序。

main10.go

package main

import (
	"fmt"
	"os/exec"
	"syscall"
	"time"
)

func main() {
	fmt.Println("11111")
	fmt.Println(syscall.Getpid())

	f2()

	fmt.Println("22222")
	time.Sleep(time.Second * 10)
}
func f2() {
	cmd := exec.Command("./main11")
	err := cmd.Start()
	if err != nil {
		panic(err)
	}

	fmt.Println("son ", cmd.Process.Pid)
}

os/exec 文档参考 https://golang.google.cn/pkg/os/exec/

四、go程序执行shell命令

[program arg1 arg2 ...]
ps -ef | grep xxxls -l >> note.log
xxxxx/bin/bash -c xxxxx/bin/bash-c
func main(){
	c := "ls -l >> note.log"
	cmd := exec.Command("/bin/bash", "-c", c)
	err := cmd.Run()
	if err != nil {
		fmt.Println(err)
	}
}
ll ls -l

因此执行外部命令可以

cmd := exec.Command("/app/read5/read5", "serve", "-d")
cmd := exec.Command("/bin/bash", "-c", "/app/read5/read5 serve -d")
cmd := exec.Command("/bin/bash", "-c", "cd /app/read5 && ./read5 serve -d")
Stdout, Stderrio.Writerbytes.Buffercmd.Runcmd.Output
cs := []string{"-c" ,`ps -ef | grep "show serve" | grep -v "grep" | grep -v "expect"| wc -l`}
cmd := exec.Command("/bin/bash", cs...)
ret, err := cmd.Output()
if err != nil {
	fmt.Println("err: ", err)
} else {
	fmt.Println(string(ret))
}

五、关于进程参数 args

ls -l >> note.log
func f2() {
	c := "-c ls -l >> note.log"
	cmd := exec.Command("sh", strings.Split(c, " ")...)
	
	fmt.Println(cmd.String()) // /usr/bin/sh -c ls -l >> note.log
	
	err := cmd.Run()
	if err != nil {
		fmt.Println(err)
	}
}

func f1() {
	c := "ls -l >> note.log"
	cmd := exec.Command("/bin/bash", "-c", c)
	
	fmt.Println(cmd.String()) // /usr/bin/sh -c ls -l >> note.log
	
	err := cmd.Run()
	if err != nil {
		fmt.Println(err)
	}
}
/usr/bin/sh -c ls -l >> note.logf1 成功, f2 失败/bin/bash 只有一个参数 -c/bin/bash -c ls

所以args的写法还是要规范,比如将 f2 缓存 f3。

func f3() {
	c := []string{"-c", "ls -l >> note.log"}
	cmd := exec.Command("/bin/bash", c...)
	fmt.Println(cmd.String()) // /usr/bin/sh -c ls -l >> note.log
	err := cmd.Run()
	if err != nil {
		fmt.Println(err)
	}
}

六、cmd 对象注释

type Cmd struct {  
    Path         string   // 运行命令的路径,绝对路径或者相对路径  
    Args         []string  // 命令参数  
    Env          []string   // 进程环境,如果环境为空,则使用当前进程的环境  
    Dir          string   // 指定command的工作目录,如果dir为空,则comman在调用进程所在当前目录中运行  
    Stdin        io.Reader // 标准输入,如果stdin是nil的话,进程从null device中读取(os.DevNull),stdin也可以时一个
    						// 文件,否则的话则在运行过程中再开一个goroutine去/读取标准输入  
    Stdout       io.Writer  // 标准输出  
    Stderr       io.Writer // 错误输出,如果这两个(Stdout和Stderr)为空的话,则command运行时将响应的文件描述符连接到
    						// os.DevNull  
    ExtraFiles   []*os.File // 打开的文件描述符切片,可为进程添加fd,比如 socket 
    SysProcAttr  *syscall.SysProcAttr // 系统的进程属性
    Process      *os.Process    // Process是底层进程,只启动一次,就是 os.StartProcess 返回的进程对象
    ProcessState *os.ProcessState  // ProcessState包含一个退出进程的信息,当进程调用Wait或者Run时便会产生该信息.  
}  

Output() 和 CombinedOutput() 不能够同时使用,因为command的标准输出只能有一个,同时使用的话便会定义了两个,便会报错。

func (c *Cmd) Run() error

开始指定命令并且等待他执行结束,如果命令能够成功执行完毕,则返回nil,否则的话边会产生错误。

func (c *Cmd) Start() error 
需要手动调用Wait方法WaitStart

一个command只能使用Start()或者Run()中的一个启动命令,不能两个同时使用。

func (c *Cmd) StderrPipe() (io.ReadCloser, error)  

StderrPipe返回一个pipe,这个管道连接到command的标准错误,当command命令退出时,Wait将关闭这些pipe。

func (c *Cmd) StdinPipe() (io.WriteCloser, error) 

StdinPipe返回一个连接到command标准输入的管道pipe。

func (c *Cmd) StdoutPipe() (io.ReadCloser, error)     

StdoutPipe返回一个连接到command标准输出的管道pipe。

func (c *Cmd) Wait() error 

Wait等待command退出,他必须和Start一起使用,如果命令能够顺利执行完并顺利退出则返回nil,否则的话便会返回error,其中Wait会是放掉所有与cmd命令相关的资源。

守护进程

由以上可知,golang程序开启守护进程就很容易了。

var daemonFlag = "-d"
args := os.Args
daemon := false
for k, v := range args {
	if v == daemonFlag {
		daemon = true
		args[k] = ""
	}
}

if daemon {
	util.Daemonize(args...)
} else {
	// 运行你的程序
	......
}

func Daemonize(args ...string) {
	var arg []string
	if len(args) > 1 {
		arg = args[1:]
	}
	cmd := exec.Command(args[0], arg...)
	cmd.Env = os.Environ()
	cmd.Start()
}
-d./voteapi serve -d

父进程先于子进程退出,不需要使用Wait。

补充说明

os/execfork/execO_CLOEXEC
os.OpenFilesyscall.O_CLOEXEC
func openFile(name string, flag int, perm FileMode) (file *File, err error) {
	r, e := syscall.Open(fixLongPath(name), flag|syscall.O_CLOEXEC, syscallMode(perm))
	if e != nil {
		return nil, e
	}
	return newFile(r, name, "file"), nil
}
/dev/nullcmd.ExtraFiles
var fn = "testproc_f1.txt"
var isChild bool
var openfile *os.File

func main() {
	isChild = os.Getenv("IS_CHILD") != ""

	if !isChild {
		openfile, _ = os.OpenFile(fn, os.O_RDWR|os.O_APPEND, os.ModePerm)
		defer openfile.Close()
		openfile.WriteString("father")
	} else {

	}

	go handleSignals()

	for {
		time.Sleep(1 * time.Second)
	}
}

func fork() (err error) {
	env := append(
		os.Environ(),
		"IS_CHILD=1",
	)

	path := os.Args[0]
	var args []string
	if len(os.Args) > 1 {
		args = os.Args[1:]
	}

	cmd := exec.Command(path, args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	// cmd.ExtraFiles = []*os.File{openfile}
	cmd.Env = env

	err = cmd.Start()
	if err != nil {
		log.Fatalf("Restart: Failed to launch, error: %v", err)
	}

	return
}

func handleSignals() {
	var sig os.Signal
	sigChan := make(chan os.Signal, 1024)

	signal.Notify(
		sigChan,
		syscall.SIGHUP,
		syscall.SIGTERM,
	)

	pid := syscall.Getpid()
	for {
		sig = <-sigChan
		switch sig {
		case syscall.SIGHUP:
			log.Println(pid, "Received SIGHUP. forking.")
			err := fork()
			if err != nil {
				log.Println("Fork err:", err)
			}
		case syscall.SIGTERM:
			log.Println(pid, "Received SIGTERM.")
		default:
			log.Printf("Received %v: nothing i care about...\n", sig)
		}
	}
}
ps -ef |grep testproc
root      1248  1090  0 09:42 pts/0    00:00:00 ./testproc

ls -l /proc/1248/fd
总用量 0
lrwx------ 1 root root 64 11月 16 09:42 0 -> /dev/pts/0
lrwx------ 1 root root 64 11月 16 09:42 1 -> /dev/pts/0
lrwx------ 1 root root 64 11月 16 09:42 2 -> /dev/pts/0
lrwx------ 1 root root 64 11月 16 09:42 3 -> /data/www/golang/project/hashcompare/testproc_f1.txt
lrwx------ 1 root root 64 11月 16 09:42 4 -> anon_inode:[eventpoll]
lr-x------ 1 root root 64 11月 16 09:42 5 -> pipe:[936229]
l-wx------ 1 root root 64 11月 16 09:42 6 -> pipe:[936229]

kill -SIGHUP 1248

ps -ef |grep testproc
root      1248  1090  0 09:42 pts/0    00:00:00 ./testproc
root      1258  1248  0 09:44 pts/0    00:00:00 ./testproc

ls -l /proc/1258/fd
total 0
lr-x------ 1 root root 64 11月 16 09:44 0 -> /dev/null
lrwx------ 1 root root 64 11月 16 09:44 1 -> /dev/pts/0
lrwx------ 1 root root 64 11月 16 09:44 2 -> /dev/pts/0
lrwx------ 1 root root 64 11月 16 09:44 3 -> anon_inode:[eventpoll]
lr-x------ 1 root root 64 11月 16 09:44 4 -> pipe:[936313]
l-wx------ 1 root root 64 11月 16 09:44 5 -> pipe:[936313]

可以看到 0 和 1,2是不同的,并且没有包含 testproc_f1.txt 文件。

其中 eventpoll 为信号监听用的。

cmd.ExtraFiles = []*os.File{openfile}
ps -ef |grep testproc
root      1267  1090  0 09:46 pts/0    00:00:00 ./testproc
root      1275  1267  0 09:47 pts/0    00:00:00 ./testproc

ls -l /proc/1275/fd
total 0
lr-x------ 1 root root 64 11月 16 09:47 0 -> /dev/null
lrwx------ 1 root root 64 11月 16 09:47 1 -> /dev/pts/0
lrwx------ 1 root root 64 11月 16 09:47 2 -> /dev/pts/0
lrwx------ 1 root root 64 11月 16 09:47 3 -> /data/www/golang/project/hashcompare/testproc_f1.txt
lrwx------ 1 root root 64 11月 16 09:47 4 -> anon_inode:[eventpoll]
lr-x------ 1 root root 64 11月 16 09:47 5 -> pipe:[936430]
l-wx------ 1 root root 64 11月 16 09:47 6 -> pipe:[936430]
cmd.ExtraFiles
func main() {
	isChild = os.Getenv("IS_CHILD") != ""

	if !isChild {
		openfile, _ = os.OpenFile(fn, os.O_RDWR|os.O_APPEND, os.ModePerm)
		defer openfile.Close()
		openfile.WriteString("father")
	} else {
		f := os.NewFile(uintptr(3), "testproc_f1.txt")
		defer f.Close()
		f.WriteString("son")
	}

	go handleSignals()

	for {
		time.Sleep(1 * time.Second)
	}
}

有时候,一个进程打开的 fd 很多,比如有多个对外建立的socket连接,此时通过 lsof 命令看以看到更详细的信息

# ls -l /proc/1299/fd
总用量 0
lrwx------ 1 root root 64 11月 16 09:53 0 -> /dev/pts/0
lrwx------ 1 root root 64 11月 16 09:53 1 -> /dev/pts/0
lrwx------ 1 root root 64 11月 16 09:53 2 -> /dev/pts/0
lrwx------ 1 root root 64 11月 16 09:53 3 -> /data/www/golang/project/hashcompare/testproc_f1.txt
lrwx------ 1 root root 64 11月 16 09:53 4 -> anon_inode:[eventpoll]
lr-x------ 1 root root 64 11月 16 09:53 5 -> pipe:[936660]
l-wx------ 1 root root 64 11月 16 09:53 6 -> pipe:[936660]
lrwx------ 1 root root 64 11月 16 09:53 7 -> socket:[936664]

# lsof -p 1299
COMMAND   PID USER   FD      TYPE DEVICE SIZE/OFF     NODE NAME
testproc 1299 root  cwd       DIR   0,58     4096      415 /data/www/golang/project/hashcompare
testproc 1299 root  rtd       DIR  253,0     4096      128 /
testproc 1299 root  txt       REG   0,58  6223068      433 /data/www/golang/project/hashcompare/testproc
testproc 1299 root  mem       REG  253,0  2156240 33632456 /usr/lib64/libc-2.17.so
testproc 1299 root  mem       REG  253,0   142144 34128089 /usr/lib64/libpthread-2.17.so
testproc 1299 root  mem       REG  253,0   163312 34127918 /usr/lib64/ld-2.17.so
testproc 1299 root    0u      CHR  136,0      0t0        3 /dev/pts/0
testproc 1299 root    1u      CHR  136,0      0t0        3 /dev/pts/0
testproc 1299 root    2u      CHR  136,0      0t0        3 /dev/pts/0
testproc 1299 root    3u      REG   0,58      166      435 /data/www/golang/project/hashcompare/testproc_f1.txt
testproc 1299 root    4u  a_inode    0,9        0     4554 [eventpoll]
testproc 1299 root    5r     FIFO    0,8      0t0   936660 pipe
testproc 1299 root    6w     FIFO    0,8      0t0   936660 pipe
testproc 1299 root    7u     IPv6 936664      0t0      TCP *:6060 (LISTEN)