如何在 Go 中優雅關閉子進程

有時我們會遇到這樣的需求,在一個主進程中啓動另外一個進程,而在 Go 中可以使用 exec 包的 Cmd 來輕鬆實現這類需求,例如代碼:

package main

import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "os/signal"
)

func main() {
    cmd := exec.Cmd{
        Path: "nc",
        Args: []string{"-u", "-l", "8888"},
        Dir:  "/usr/bin",
    }

    if err := cmd.Start(); err != nil {
        log.Panic(err)
    }

    fmt.Println("Start child process with pid", cmd.Process.Pid)

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    s := <-c
    fmt.Println("Got signal:", s)
}

這段代碼的含義是: 使用 nc -u -l 8888 來模擬一個常駐進程,然後通過 Go 的 exec.Cmd 來運行它,並且 Go 代碼不退出,運行代碼:


$ go run main.go

Start child process with pid 35904

輸出結果表明我們已經通過 Go 成功調用外部命令,起了一個子進程,其進程號爲 35904,我們還可以通過命令 ps -ef 35904 來確認:

 UID   PID  PPID   C STIME   TTY           TIME CMD
2062309935 35904 35903   0  3:36PM ttys008    0:00.00 -u -l 8888

如何結束子進程
首先想到的就是 kill 命令,嘗試使用 kill 35904

$ kill 35904
$ ps -ef 35904

UID   PID  PPID   C STIME   TTY           TIME CMD
2062309935 35904 35903   0  3:36PM ttys008    0:00.00 (nc)

發現 kill 命令並不好用,進程還在,然後換成 kill -9 也同樣不起作用。不過該進程已經停止運行了,可以看到監聽由 0:00.00 -u -l 8888 變成了 0:00.00 (nc), 不再監聽 8888 端口,只是進程資源還沒釋放而已。

  • 使用 Go 代碼結束該進程
    因爲 Go 的 Cmd 內置了 Process.Kill() 函數,我們可以嘗試使用它來關閉子進程,修改代碼,添加如下內容:
// After five second, kill cmd's process
time.Sleep(5 * time.Second)
cmd.Process.Kill()

重新運行代碼,發現 5 秒過後,該子進程還在。其實調用 cmd.Process.Kill() 和外部使用 kill 命令是一樣的,父進程還沒有釋放資源,所以子進程不能清理完成。

  • 使用 cmd.Wait() 完成資源清理,修改後的完整代碼如下:
package main

import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "os/signal"
    "time"
)

func main() {
    cmd := exec.Cmd{
        Path: "nc",
        Args: []string{"-u", "-l", "8888"},
        Dir:  "/usr/bin",
    }

    if err := cmd.Start(); err != nil {
        log.Panic(err)
    }

    fmt.Println("Start child process with pid", cmd.Process.Pid)

    // Wait releases any resources associated with the Cmd
    go func() {
        if err := cmd.Wait(); err != nil {
            fmt.Printf("Child process %d exit with err: %v\n", cmd.Process.Pid, err)
        }
    }()

    // After five second, kill cmd's process
    time.Sleep(5 * time.Second)
    cmd.Process.Kill()

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    s := <-c
    fmt.Println("Got signal:", s)
}

運行代碼,可以得到如下結果:

$ go run main.go

Start child process with pid 41666
Child process 41666 exit with err: signal: killed

再通過 ps -el 41666 命令確認子進程 41666 已不存在。

結語

Go 中 exec.Cmd 封裝的很好,對於外部命令調用非常方便,但是使用它的時候,需要注意對子進程的資源進行釋放,其關鍵函數就是 cmd.Wait(), 所以用到 cmd 的地方,一定添加 cmd.Wait() 的邏輯。

參考鏈接:

https://golang.org/pkg/os/exec/#Cmd.Wait

http://www.songjiayang.com/posts/go-zhong-you-ya-guan-bi-zi-jin-cheng

golang 微信交流羣請+微信17812796384

51reboot 的 Golang 課程6月15月開課

51reboot 的 運維前端課程正在火熱招生中

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章