亚洲女人被黑人巨大进入-亚洲日本视频在线观看-亚洲AV秘片一区二区三3-亚洲欧美中文字幕乱码在线

<dfn id="uqq4w"><dl id="uqq4w"></dl></dfn>
  • <abbr id="uqq4w"></abbr>
    <center id="uqq4w"><cite id="uqq4w"></cite></center>
    ?
    徐州北大青鳥
    當(dāng)前位置: 主頁(yè) > 學(xué)在青鳥 > 編程技巧 >

    如何避免 Go 命令行執(zhí)行產(chǎn)生“孤兒”進(jìn)程?

    時(shí)間:2021-08-16 12:02來(lái)源:未知 作者:代碼如詩(shī) 點(diǎn)擊:
    在 Go 程序當(dāng)中,如果我們要執(zhí)行命令時(shí),通常會(huì)使用 exec.Command ,也比較好用,通常狀況下,可以達(dá)到我們的目的,如果我們邏輯當(dāng)中,需要終止這個(gè)進(jìn)程,則可以快速使用 cmd.Process
    在 Go 程序當(dāng)中,如果我們要執(zhí)行命令時(shí),通常會(huì)使用 exec.Command ,也比較好用,通常狀況下,可以達(dá)到我們的目的,如果我們邏輯當(dāng)中,需要終止這個(gè)進(jìn)程,則可以快速使用 cmd.Process.Kill() 方法來(lái)結(jié)束進(jìn)程。但當(dāng)我們要執(zhí)行的命令會(huì)啟動(dòng)其他子進(jìn)程來(lái)操作的時(shí)候,會(huì)發(fā)生什么情況?
     
    一  孤兒進(jìn)程的產(chǎn)生
     
    測(cè)試小程序:
     
    func kill(cmd *exec.Cmd) func() {
        return func() {
        if cmd != nil {
        cmd.Process.Kill()
        }
        }
    }
     
    func main() {
        cmd := exec.Command("/bin/bash", "-c", "watch top >top.log")
        time.AfterFunc(1*time.Second, kill(cmd))
        err := cmd.Run()
        fmt.Printf("pid=%d err=%s\n", cmd.Process.Pid, err)
    }
     
    執(zhí)行小程序:
     
    go run main.go
     
    pid=27326 err=signal: killed
     
    查看進(jìn)程信息:
     
    ps -j
     
    USER    PID  PPID  PGID   SESS JOBC STAT   TT       TIME COMMAND
    king  24324     1 24303      0    0 S    s012    0:00.01 watch top
     
    可以看到這個(gè) "watch top" 的 PPID 為 1,說(shuō)明這個(gè)進(jìn)程已經(jīng)變成了 “孤兒” 進(jìn)程。
     
    二  通過(guò)進(jìn)程組來(lái)解決掉所有子進(jìn)程
     
    在 linux 當(dāng)中,是有會(huì)話、進(jìn)程組和進(jìn)程組的概念,并且 Go 也是使用 linux 的 kill(2) 方法來(lái)發(fā)送信號(hào)的,那么是否可以通過(guò) kill 來(lái)將要結(jié)束進(jìn)程的子進(jìn)程都結(jié)束掉?
     
    linux 的 kill(2) 的定義如下:
     
    #include <signal.h>
     
    int kill(pid_t pid, int sig);
     
    如果 pid 為正數(shù)的時(shí)候,會(huì)給指定的 pid 發(fā)送 sig 信號(hào),如果 pid 為負(fù)數(shù)的時(shí)候,會(huì)給這個(gè)進(jìn)程組發(fā)送 sig 信號(hào),那么我們可以通過(guò)進(jìn)程組來(lái)將所有子進(jìn)程退出掉?改一下 Go 程序中 kill 方法:
     
    func kill(cmd *exec.Cmd) func() {
        return func() {
        if cmd != nil {
        // cmd.Process.Kill()
        syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
        }
        }
    }
     
    func main() {
        cmd := exec.Command("/bin/bash", "-c", "watch top >top.log")
        time.AfterFunc(1*time.Second, kill(cmd))
        err := cmd.Run()
        fmt.Printf("pid=%d err=%s\n", cmd.Process.Pid, err)
    }
     
    再次執(zhí)行:
     
    go run main.go
     
    會(huì)發(fā)現(xiàn)程序卡住了,我們來(lái)看一下當(dāng)前執(zhí)行的進(jìn)程:
     
    ps -j
     
    USER    PID  PPID  PGID   SESS JOBC STAT   TT       TIME COMMAND
    king 27655 91597 27655      0    1 S+   s012    0:01.10 go run main.go
    king 27672 27655 27655      0    1 S+   s012    0:00.03 ..../exe/main
    king 27673 27672 27655      0    1 S+   s012    0:00.00 /bin/bash -c watch top >top.log
    king 27674 27673 27655      0    1 S+   s012    0:00.01 watch top
     
    可以看到我們 go run 產(chǎn)生了一個(gè)子進(jìn)程 27672(command 那里是 go 執(zhí)行的臨時(shí)目錄,比較長(zhǎng),因此添加了省略號(hào)),27672 產(chǎn)生了 27673(watch top >top.log)進(jìn)程,27673 產(chǎn)生了 27674(watch top)進(jìn)程。那為什么沒(méi)有將這些子進(jìn)程都關(guān)閉掉呢?
     
    其實(shí)之類犯了一個(gè)低級(jí)錯(cuò)誤,從上圖中,我們可以看到他們的進(jìn)程組 ID 為 27655,但是我們傳遞的是 cmd 的 id 即 27673,這個(gè)并不是進(jìn)程組的 ID,因此程序并沒(méi)有 kill,導(dǎo)致 cmd.Run() 一直在執(zhí)行。
     
    在 Linux 中,進(jìn)程組中的第一個(gè)進(jìn)程,被稱為進(jìn)程組 Leader,同時(shí)這個(gè)進(jìn)程組的 ID 就是這個(gè)進(jìn)程的 ID,從這個(gè)進(jìn)程中創(chuàng)建的其他進(jìn)程,都會(huì)繼承這個(gè)進(jìn)程的進(jìn)程組和會(huì)話信息;從上面可以看出 go run main.go 程序 PID 和 PGID 同為 27655,那么這個(gè)進(jìn)程就是進(jìn)程組 Leader,我們不能 kill 這個(gè)進(jìn)程組,除非想“自殺”,哈哈哈。
     
    那么我們給要執(zhí)行的進(jìn)程,新建一個(gè)進(jìn)程組,在 Kill 不就可以了嘛。在 linux 當(dāng)中,通過(guò) setpgid 方法來(lái)設(shè)置進(jìn)程組 ID,定義如下:
     
    #include <unistd.h>
     
    int setpgid(pid_t pid, pid_t pgid);
     
    如果將 pid 和 pgid 同時(shí)設(shè)置成 0,也就是 setpgid(0,0),則會(huì)使用當(dāng)前進(jìn)程為進(jìn)程組 leader 并創(chuàng)建新的進(jìn)程組。
     
    那么在 Go 程序中,可以通過(guò) cmd.SysProcAttr 來(lái)設(shè)置創(chuàng)建新的進(jìn)程組,修改后的代碼如下:
     
    func kill(cmd *exec.Cmd) func() {
        return func() {
        if cmd != nil {
        // cmd.Process.Kill()
        syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
        }
        }
    }
     
    func main() {
        cmd := exec.Command("/bin/bash", "-c", "watch top >top.log")
      cmd.SysProcAttr = &syscall.SysProcAttr{
        Setpgid: true,
        }
        
        time.AfterFunc(1*time.Second, kill(cmd))
        err := cmd.Run()
        fmt.Printf("pid=%d err=%s\n", cmd.Process.Pid, err)
    }
     
    再次執(zhí)行:
     
    go run main.go
     
    pid=29397 err=signal: killed
     
    再次查看進(jìn)程:
     
     
    ps -j
     
    USER    PID  PPID  PGID   SESS JOBC STAT   TT       TIME COMMAND
     
     
    發(fā)現(xiàn) watch 的進(jìn)程都不存在了,那我們?cè)诳纯词欠襁€會(huì)有孤兒進(jìn)程:
     
    # 由于我測(cè)試的環(huán)境是mac,因此這個(gè)腳本只能在mac執(zhí)行
    ps -j | head -1;ps -j | awk '{if ($3 ==1 && $1 !="root"){print $0}}' | head
     
    USER    PID  PPID  PGID   SESS JOBC STAT   TT       TIME COMMAND
     
    已經(jīng)沒(méi)有孤兒進(jìn)程了,問(wèn)題至此已經(jīng)完全解決。
     
    三  子進(jìn)程監(jiān)聽父進(jìn)程是否退出(只能在 linux 下執(zhí)行)
     
    假設(shè)要調(diào)用的程序也是我們自己寫的其他應(yīng)用程序,那么可以使用 Linux 的 prctl 方法來(lái)處理, prctl 方法的定義如下:
     
    #include <sys/prctl.h>
     
    int prctl(int option, unsigned long arg2, unsigned long arg3,
              unsigned long arg4, unsigned long arg5);
     
    這個(gè)方法有一個(gè)重要的 option:PR_SET_PDEATHSIG,通過(guò)這個(gè)來(lái)接收父進(jìn)程的退出。
     
    讓我們來(lái)再次構(gòu)造一個(gè)有問(wèn)題的程序。
     
    有兩個(gè)文件,分別為 main.go 和 child.go 文件,main.go 會(huì)調(diào)用 child.go 文件。
     
    main.go 文件:
     
    package main
     
    import (
            "os/exec"
    )
     
    func main() {
            cmd := exec.Command("./child")
            cmd.Run()
    }
     
    child.go 文件:
     
    package main
     
    import (
        "fmt"
        "time"
    )
     
    func main() {
        for {
        time.Sleep(200 * time.Millisecond)
        fmt.Println(time.Now())
        }
    }
     
    在 Linux 環(huán)境中分別編譯這兩個(gè)文件:
     
    // 編譯 main.go 生成 main 二進(jìn)制文件
    go build -o main main.go
     
    // 編譯 child.go 生成 child 二進(jìn)制文件
    go build -o child child.go
     
    執(zhí)行 main 二進(jìn)制文件:
     
    ./main &
     
    查看他們的進(jìn)程:
     
    ps -ef
     
    UID        PID  PPID  C STIME TTY          TIME CMD
    root         1     0  0 06:05 pts/0    00:00:00 /bin/bash
    root     11514     1  0 12:12 pts/0    00:00:00 ./main
    root     11520 11514  0 12:12 pts/0    00:00:00 ./child
     
    可以看到 main 和 child 的進(jìn)程,child 是 main 的子進(jìn)程,我們將 main 進(jìn)程 kill 掉,在查看進(jìn)程狀態(tài):
     
    kill -9 11514
     
    ps -ef
     
    UID        PID  PPID  C STIME TTY          TIME CMD
    root         1     0  0 06:05 pts/0    00:00:00 /bin/bash
    root     11520     1  0 12:12 pts/0    00:00:00 ./child
     
    我們可以看到 child 的進(jìn)程,他的 PPID 已經(jīng)變成了 1,說(shuō)明這個(gè)進(jìn)程已經(jīng)變成了孤兒進(jìn)程。
     
    那接下來(lái)我們可以使用 PR_SET_PDEATHSIG 來(lái)保證父進(jìn)程退出,子進(jìn)程也退出,大致方式有兩種:使用 CGO 調(diào)用和使用 syscall.RawSyscall 來(lái)調(diào)用。
     
    1  使用 CGO
     
    將 child 修改成如下內(nèi)容:
     
    import (
        "fmt"
        "time"
    )
     
    // #include <stdio.h>
    // #include <stdlib.h>
    // #include <sys/prctl.h>
    // #include <signal.h>
    //
    // static void killTest() {
    //    prctl(PR_SET_PDEATHSIG,SIGKILL);
    // }
    import "C"
     
    func main() {
        C.killTest()
      
        for {
        time.Sleep(200 * time.Millisecond)
        fmt.Println(time.Now())
        }
    }
     
    程序中,使用 CGO,為了簡(jiǎn)單的展示,在 Go 文件中編寫了 C 的 killTest 方法,并調(diào)用了 prctl 方法,然后在 Go 程序中調(diào)用 killTest 方法,讓我們重新編譯執(zhí)行一下,再看看進(jìn)程:
     
    go build -o child child.go
    ./main & 
    ps -ef 
     
    UID        PID  PPID  C STIME TTY          TIME CMD
    root         1     0  0 06:05 pts/0    00:00:00 /bin/bash
    root     11663     1  0 12:28 pts/0    00:00:00 ./main
    root     11669 11663  0 12:28 pts/0    00:00:00 ./child
     
    再次 kill 掉 main,并查看進(jìn)程:
     
    kill -9 11663
    ps -ef
     
    UID        PID  PPID  C STIME TTY          TIME CMD
    root         1     0  0 06:05 pts/0    00:00:00 /bin/bash
     
    可以看到 child 的進(jìn)程也已經(jīng)退出了,說(shuō)明 CGO 調(diào)用的 prctl 生效了。
     
    2  syscall.RawSyscall 方法
     
    也可以采用 Go 中提供的 syscall.RawSyscall 方法來(lái)替代調(diào)用 CGO,在 Go 的文檔中,可以查看到 syscall 包中定義的常量(查看 linux,如果是本地 godoc,需要指定 GOOS=linux),可以看到我們要用的幾個(gè)常量以及他們對(duì)應(yīng)的數(shù)值:
     
    // 其他內(nèi)容省略掉了
    const(
        ....
        PR_SET_PDEATHSIG                 = 0x1
        ....
    )
     
    const(     
        .....
        SYS_PRCTL                  = 157
        .....
    )
     
    其中 PR_SET_PDEATHSIG 操作的值為 1,SYS_PRCTL 的值為 157,那么將 child.go 修改成如下內(nèi)容:
     
    package main
     
    import (
        "fmt"
        "os"
        "syscall"
        "time"
    )
     
    func main() {
        _, _, errno := syscall.RawSyscall(uintptr(syscall.SYS_PRCTL), uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGKILL), 0)
        if errno != 0 {
        os.Exit(int(errno))
        }
     
        for {
        time.Sleep(200 * time.Millisecond)
        fmt.Println(time.Now())
        }
    }
     
    再次編譯并執(zhí)行:
     
    go build -o child child.go
    ./main & 
    ps -ef
     
    UID        PID  PPID  C STIME TTY          TIME CMD
    root         1     0  0 06:05 pts/0    00:00:00 /bin/bash
    root     12208     1  0 12:46 pts/0    00:00:00 ./main
    root     12214 12208  0 12:46 pts/0    00:00:00 ./child
     
    將 main 進(jìn)程結(jié)束掉:
     
    kill -9 12208
    ps -ef
     
    UID        PID  PPID  C STIME TTY          TIME CMD
    root         1     0  0 06:05 pts/0    00:00:00 /bin/bash
     
    child 進(jìn)程已經(jīng)退出了,也達(dá)成了最終效果。
     
    四  總結(jié)
     
    當(dāng)我們使用 Go 程序執(zhí)行其他程序的時(shí)候,如果其他程序也開啟了其他進(jìn)程,那么在 kill 的時(shí)候可能會(huì)把這些進(jìn)程變成孤兒進(jìn)程,一直執(zhí)行并滯留在內(nèi)存中。當(dāng)然,如果我們程序非法退出,或者被 kill 調(diào)用,也會(huì)導(dǎo)致我們執(zhí)行的進(jìn)程變成孤兒進(jìn)程,那么為了解決這個(gè)問(wèn)題,從兩個(gè)思路來(lái)解決:
     
    給要執(zhí)行的程序創(chuàng)建新的進(jìn)程組,并調(diào)用 syscall.Kill,傳遞負(fù)值 pid 來(lái)關(guān)閉這個(gè)進(jìn)程組中所有的進(jìn)程(比較完美的解決方法)。
     
     
    如果要調(diào)用的程序也是我們自己編寫的,那么可以使用 PR_SET_PDEATHSIG 來(lái)感知父進(jìn)程退出,那么這種方式需要調(diào)用 Linxu 的 prctrl,可以使用 CGO 的方式,也可以使用 syscall.RawSyscall 的方式。
     
     
    但不管使用哪種方式,都只是提供了一種思路,在我們編寫服務(wù)端服務(wù)程序的時(shí)候,需要特殊關(guān)注,防止孤兒進(jìn)程消耗服務(wù)器資源。
    試聽課
    (責(zé)任編輯:代碼如詩(shī))
    ------分隔線----------------------------
    欄目列表
    推薦內(nèi)容