久久久网中文字幕_精品国产电影自在免费观看_中文字幕电影亚洲精品_亚洲色精品Aⅴ一区区三区

?
徐州北大青鳥
當前位置: 主頁 > 學在青鳥 > 編程技巧 >

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

時間:2021-08-16 12:02來源:未知 作者:代碼如詩 點擊:
在 Go 程序當中,如果我們要執(zhí)行命令時,通常會使用 exec.Command ,也比較好用,通常狀況下,可以達到我們的目的,如果我們邏輯當中,需要終止這個進程,則可以快速使用 cmd.Process
在 Go 程序當中,如果我們要執(zhí)行命令時,通常會使用 exec.Command ,也比較好用,通常狀況下,可以達到我們的目的,如果我們邏輯當中,需要終止這個進程,則可以快速使用 cmd.Process.Kill() 方法來結束進程。但當我們要執(zhí)行的命令會啟動其他子進程來操作的時候,會發(fā)生什么情況?
 
一  孤兒進程的產生
 
測試小程序:
 
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
 
查看進程信息:
 
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
 
可以看到這個 "watch top" 的 PPID 為 1,說明這個進程已經變成了 “孤兒” 進程。
 
二  通過進程組來解決掉所有子進程
 
在 linux 當中,是有會話、進程組和進程組的概念,并且 Go 也是使用 linux 的 kill(2) 方法來發(fā)送信號的,那么是否可以通過 kill 來將要結束進程的子進程都結束掉?
 
linux 的 kill(2) 的定義如下:
 
#include <signal.h>
 
int kill(pid_t pid, int sig);
 
如果 pid 為正數(shù)的時候,會給指定的 pid 發(fā)送 sig 信號,如果 pid 為負數(shù)的時候,會給這個進程組發(fā)送 sig 信號,那么我們可以通過進程組來將所有子進程退出掉?改一下 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
 
會發(fā)現(xiàn)程序卡住了,我們來看一下當前執(zhí)行的進程:
 
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 產生了一個子進程 27672(command 那里是 go 執(zhí)行的臨時目錄,比較長,因此添加了省略號),27672 產生了 27673(watch top >top.log)進程,27673 產生了 27674(watch top)進程。那為什么沒有將這些子進程都關閉掉呢?
 
其實之類犯了一個低級錯誤,從上圖中,我們可以看到他們的進程組 ID 為 27655,但是我們傳遞的是 cmd 的 id 即 27673,這個并不是進程組的 ID,因此程序并沒有 kill,導致 cmd.Run() 一直在執(zhí)行。
 
在 Linux 中,進程組中的第一個進程,被稱為進程組 Leader,同時這個進程組的 ID 就是這個進程的 ID,從這個進程中創(chuàng)建的其他進程,都會繼承這個進程的進程組和會話信息;從上面可以看出 go run main.go 程序 PID 和 PGID 同為 27655,那么這個進程就是進程組 Leader,我們不能 kill 這個進程組,除非想“自殺”,哈哈哈。
 
那么我們給要執(zhí)行的進程,新建一個進程組,在 Kill 不就可以了嘛。在 linux 當中,通過 setpgid 方法來設置進程組 ID,定義如下:
 
#include <unistd.h>
 
int setpgid(pid_t pid, pid_t pgid);
 
如果將 pid 和 pgid 同時設置成 0,也就是 setpgid(0,0),則會使用當前進程為進程組 leader 并創(chuàng)建新的進程組。
 
那么在 Go 程序中,可以通過 cmd.SysProcAttr 來設置創(chuàng)建新的進程組,修改后的代碼如下:
 
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
 
再次查看進程:
 
 
ps -j
 
USER    PID  PPID  PGID   SESS JOBC STAT   TT       TIME COMMAND
 
 
發(fā)現(xiàn) watch 的進程都不存在了,那我們在看看是否還會有孤兒進程:
 
# 由于我測試的環(huán)境是mac,因此這個腳本只能在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
 
已經沒有孤兒進程了,問題至此已經完全解決。
 
三  子進程監(jiān)聽父進程是否退出(只能在 linux 下執(zhí)行)
 
假設要調用的程序也是我們自己寫的其他應用程序,那么可以使用 Linux 的 prctl 方法來處理, prctl 方法的定義如下:
 
#include <sys/prctl.h>
 
int prctl(int option, unsigned long arg2, unsigned long arg3,
          unsigned long arg4, unsigned long arg5);
 
這個方法有一個重要的 option:PR_SET_PDEATHSIG,通過這個來接收父進程的退出。
 
讓我們來再次構造一個有問題的程序。
 
有兩個文件,分別為 main.go 和 child.go 文件,main.go 會調用 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)境中分別編譯這兩個文件:
 
// 編譯 main.go 生成 main 二進制文件
go build -o main main.go
 
// 編譯 child.go 生成 child 二進制文件
go build -o child child.go
 
執(zhí)行 main 二進制文件:
 
./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     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 的進程,child 是 main 的子進程,我們將 main 進程 kill 掉,在查看進程狀態(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 的進程,他的 PPID 已經變成了 1,說明這個進程已經變成了孤兒進程。
 
那接下來我們可以使用 PR_SET_PDEATHSIG 來保證父進程退出,子進程也退出,大致方式有兩種:使用 CGO 調用和使用 syscall.RawSyscall 來調用。
 
1  使用 CGO
 
將 child 修改成如下內容:
 
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,為了簡單的展示,在 Go 文件中編寫了 C 的 killTest 方法,并調用了 prctl 方法,然后在 Go 程序中調用 killTest 方法,讓我們重新編譯執(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     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,并查看進程:
 
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 的進程也已經退出了,說明 CGO 調用的 prctl 生效了。
 
2  syscall.RawSyscall 方法
 
也可以采用 Go 中提供的 syscall.RawSyscall 方法來替代調用 CGO,在 Go 的文檔中,可以查看到 syscall 包中定義的常量(查看 linux,如果是本地 godoc,需要指定 GOOS=linux),可以看到我們要用的幾個常量以及他們對應的數(shù)值:
 
// 其他內容省略掉了
const(
    ....
    PR_SET_PDEATHSIG                 = 0x1
    ....
)
 
const(     
    .....
    SYS_PRCTL                  = 157
    .....
)
 
其中 PR_SET_PDEATHSIG 操作的值為 1,SYS_PRCTL 的值為 157,那么將 child.go 修改成如下內容:
 
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 進程結束掉:
 
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 進程已經退出了,也達成了最終效果。
 
四  總結
 
當我們使用 Go 程序執(zhí)行其他程序的時候,如果其他程序也開啟了其他進程,那么在 kill 的時候可能會把這些進程變成孤兒進程,一直執(zhí)行并滯留在內存中。當然,如果我們程序非法退出,或者被 kill 調用,也會導致我們執(zhí)行的進程變成孤兒進程,那么為了解決這個問題,從兩個思路來解決:
 
給要執(zhí)行的程序創(chuàng)建新的進程組,并調用 syscall.Kill,傳遞負值 pid 來關閉這個進程組中所有的進程(比較完美的解決方法)。
 
 
如果要調用的程序也是我們自己編寫的,那么可以使用 PR_SET_PDEATHSIG 來感知父進程退出,那么這種方式需要調用 Linxu 的 prctrl,可以使用 CGO 的方式,也可以使用 syscall.RawSyscall 的方式。
 
 
但不管使用哪種方式,都只是提供了一種思路,在我們編寫服務端服務程序的時候,需要特殊關注,防止孤兒進程消耗服務器資源。
試聽課
(責任編輯:代碼如詩)
------分隔線----------------------------
欄目列表
推薦內容