一文搞懂Go?Exec?僵尸與孤兒進(jìn)程
最近,使用 golang 去管理本地應(yīng)用的生命周期,期間有幾個(gè)有趣的點(diǎn),今天就一起看下。
場景一
我們來看看下面兩個(gè)腳本會(huì)產(chǎn)生什么問題:
創(chuàng)建兩個(gè) shell 腳本
- start.sh
#!/bin/sh sh sub.sh
- sub.sh
#!/bin/sh n=0 while [ $n -le 100 ] do echo $n let n++ sleep 1 done
執(zhí)行腳本
輸出結(jié)果
$ ./start.sh
0
1
2
...
進(jìn)程關(guān)系
查看進(jìn)程信息
ps -j USER PID PPID PGID SESS JOBC STAT TT TIME COMMAND root 31758 31346 31758 0 1 S+ s000 0:00.00 /bin/sh ./start.sh root 31759 31758 31758 0 1 S+ s000 0:00.01 sh sub.sh
sub.sh的 父進(jìn)程(PPID)為start.sh的進(jìn)程id(PID)sub.sh和start.sh兩個(gè)進(jìn)程的PGID是同一個(gè),( 屬一個(gè)進(jìn)程組)。
刪除 start.sh 的進(jìn)程
kill -9 31758 # 再查看進(jìn)程組 ps -j ## 返回 USER PID PPID PGID SESS JOBC STAT TT TIME COMMAND root 31759 1 31758 0 0 S s000 0:00.03 sh sub.sh
start.sh進(jìn)程不在了sub.sh進(jìn)程還在執(zhí)行sub.sh進(jìn)程的PID變成了 1
問題1:
那sub.sh 這個(gè)進(jìn)程現(xiàn)在屬于什么?
場景二
假設(shè)sub.sh 是實(shí)際的應(yīng)用, start.sh 是應(yīng)用的啟動(dòng)腳本。
那么,golang 是如何管理他們的呢? 我們繼續(xù)看看下面 關(guān)于golang的場景。
在上面兩個(gè)腳本的基礎(chǔ)上,我們用golang 的 os/exec庫去調(diào)用 start.sh腳本
package main
import (
"context"
"log"
"os"
"os/exec"
"time"
)
func main() {
cmd := exec.CommandContext(context.Background(), "./start.sh")
// 將 start.sh 和 sub.sh 移到當(dāng)前目錄下
cmd.Dir = "/Go/src/go-code/cmd/"
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Printf("cmd.Start error %+v \n", err)
}
for {
select {
default:
log.Println(cmd.Process.Pid)
time.Sleep(2 * time.Second)
}
}
}
執(zhí)行程序
go run ./main.go
查看進(jìn)程
ps -j USER PID PPID PGID SESS JOBC STAT TT TIME COMMAND root 45458 45457 45457 0 0 Ss+ s004 0:00.03 ...___1go_build_go_code_cmd root 45462 45458 45457 0 0 S+ s004 0:00.01 /bin/sh ./start.sh root 45463 45462 45457 0 0 S+ s004 0:00.03 sh sub.sh
發(fā)現(xiàn) go 、 start.sh 、sub.sh 三個(gè)進(jìn)程為同一個(gè)進(jìn)程組(同一個(gè) PGID)
父子關(guān)系為: main.go -> start.sh -> sub.sh
刪除 start.sh 的進(jìn)程
實(shí)際場景,有可能啟動(dòng)程序掛了,導(dǎo)致我們無法監(jiān)聽到執(zhí)行程序的情況,刪除start.sh進(jìn)程,模擬下場景 :
kill -9 45462
再查看進(jìn)程
ps -j USER PID PPID PGID SESS JOBC STAT TT TIME COMMAND root 45458 45457 45457 0 0 Ss+ s004 0:00.03 ...___1go_build_go_code_cmd root 45462 1 45457 0 0 S+ s004 0:00.01 (bash) root 45463 45462 45457 0 0 S+ s004 0:00.03 sh sub.sh
- 發(fā)現(xiàn)沒,
start.sh的PPID為1 - 即使
start.sh的PPID變成了1 ,log.Println(cmd.Process.Pid)還持續(xù)的輸出 .
問題2:
那如果 PPID為1 ,golang程序不就無法管理了嗎? 即使 sub.sh 退出也不知道了,那要如何處理?
問題分析
兩個(gè)場景中, 都有一個(gè)共同的點(diǎn),就是
PPID為1,這妥妥的成為沒人要的娃了——孤兒進(jìn)程場景二中,如果
cmd的沒有進(jìn)程沒有被回收,go程序也無法管理,那么start.sh就成為了占著茅坑不拉屎的子進(jìn)程——僵尸進(jìn)程
那究竟什么是孤兒進(jìn)程 和 僵尸進(jìn)程?
孤兒進(jìn)程
在類 UNIX 操作系統(tǒng)中,孤兒進(jìn)程(Orphan Process)指:是在其父進(jìn)程執(zhí)行完成或被終止后仍繼續(xù)運(yùn)行的一類進(jìn)程。
為避免孤兒進(jìn)程退出時(shí)無法釋放所占用的資源而僵死,任何孤兒進(jìn)程產(chǎn)生時(shí)都會(huì)立即為系統(tǒng)進(jìn)程 init 或 systemd 自動(dòng)接收為子進(jìn)程,這一過程也被稱為收養(yǎng)。在此需注意,雖然事實(shí)上該進(jìn)程已有init作為其父進(jìn)程,但由于創(chuàng)建該進(jìn)程的進(jìn)程已不存在,所以仍應(yīng)稱之為孤兒進(jìn)程。孤兒進(jìn)程會(huì)浪費(fèi)服務(wù)器的資源,甚至有耗盡資源的潛在危險(xiǎn)。
解決&預(yù)防
終止機(jī)制:強(qiáng)制殺死孤兒進(jìn)程(最常用的手段);
再生機(jī)制:服務(wù)器在指定時(shí)間內(nèi)查找調(diào)用的客戶端,若找不到則直接殺死孤兒進(jìn)程;
超時(shí)機(jī)制:給每個(gè)進(jìn)程指定一個(gè)確定的運(yùn)行時(shí)間,若超時(shí)仍未完成則強(qiáng)制終止之。若有需要,亦可讓進(jìn)程在指定時(shí)間耗盡之前申請延時(shí)。
進(jìn)程組:因?yàn)楦高M(jìn)程終止或崩潰都會(huì)導(dǎo)致對應(yīng)子進(jìn)程成為孤兒進(jìn)程,所以也無法預(yù)料一個(gè)子進(jìn)程執(zhí)行期間是否會(huì)被“遺棄”。有鑒于此,多數(shù)類UNIX系統(tǒng)都引入了進(jìn)程組以防止產(chǎn)生孤兒進(jìn)程。
僵尸進(jìn)程
在類 UNIX 操作系統(tǒng)中,僵尸進(jìn)程(zombie process)指:完成執(zhí)行(通過exit系統(tǒng)調(diào)用,或運(yùn)行時(shí)發(fā)生致命錯(cuò)誤或收到終止信號所致),但在操作系統(tǒng)的進(jìn)程表中仍然存在其進(jìn)程控制塊,處于"終止?fàn)顟B(tài)"的進(jìn)程。
正常情況下,進(jìn)程直接被其父進(jìn)程 wait 并由系統(tǒng)回收。而僵尸進(jìn)程與正常進(jìn)程不同,kill 命令對僵尸進(jìn)程無效,并且無法回收,從而導(dǎo)致資源泄漏。
解決&預(yù)防
收割僵尸進(jìn)程的方法是通過 kill 命令手工向其父進(jìn)程發(fā)送SIGCHLD信號。如果其父進(jìn)程仍然拒絕收割僵尸進(jìn)程,則終止父進(jìn)程,使得 init 進(jìn)程收養(yǎng)僵尸進(jìn)程。init 進(jìn)程周期執(zhí)行 wait 系統(tǒng)調(diào)用收割其收養(yǎng)的所有僵尸進(jìn)程。
查看進(jìn)程詳情
# 列出進(jìn)程 ps -l
- USER:進(jìn)程的所屬用戶
- PID:進(jìn)程的進(jìn)程ID號
- RSS:進(jìn)程占用的固定的內(nèi)存量 (Kbytes)
- S:查看進(jìn)程狀態(tài)
- CMD:進(jìn)程對應(yīng)的實(shí)際程序
進(jìn)程狀態(tài)(S)
- R:運(yùn)行 Runnable (on run queue) 正在運(yùn)行或在運(yùn)行隊(duì)列中等待
- S:睡眠 Sleeping 休眠中,受阻,在等待某個(gè)條件的形成或接受到信號
- I:空閑 Idle
- Z:僵死 Zombie(a defunct process) 進(jìn)程已終止,但進(jìn)程描述符存在, 直到父進(jìn)程調(diào)用wait4()系統(tǒng)調(diào)用后釋放
- D:不可中斷 Uninterruptible sleep (ususally IO) 收到信號不喚醒和不可運(yùn)行, 進(jìn)程必須等待直到有中斷發(fā)生
- T:終止 Terminate 進(jìn)程收到SIGSTOP、SIGSTP、 SIGTIN、SIGTOU信號后停止運(yùn)行運(yùn)行
- P:等待交換頁
- W:無駐留頁 has no resident pages 沒有足夠的記憶體分頁可分配
- X:死掉的進(jìn)程
Go解決方案
采用 殺掉進(jìn)程組(kill process group,而不是只 kill 父進(jìn)程,在 Linux 里面使用的是 kill -- -PID) 與 進(jìn)程wait方案,結(jié)果如下:
package main
import (
"context"
"log"
"os"
"os/exec"
"syscall"
"time"
)
func main() {
ctx := context.Background()
cmd := exec.CommandContext(ctx, "./start.sh")
// 設(shè)置進(jìn)程組
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
cmd.Dir = "/Users/Wilbur/Project/Go/src/go-code/cmd/"
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Printf("cmd.Start error %+v \n", err)
}
// 監(jiān)聽進(jìn)程wait
errCmdCh := make(chan error, 1)
go func() {
errCmdCh <- cmd.Wait()
}()
for {
select {
case <-ctx.Done():
log.Println("ctx.done")
pid := cmd.Process.Pid
if err := syscall.Kill(-1*pid, syscall.SIGKILL); err != nil {
return
}
case err := <-errCmdCh:
log.Printf("errCmdCh error %+v \n", err)
return
default:
log.Println(cmd.Process.Pid)
time.Sleep(2 * time.Second)
}
}
}
剖析 cmd.Wait() 源碼
在 os/exec_unix下:
var (
status syscall.WaitStatus
rusage syscall.Rusage
pid1 int
e error
)
for {
pid1, e = syscall.Wait4(p.Pid, &status, 0, &rusage)
if e != syscall.EINTR {
break
}
}
進(jìn)行了 syscall.Wait4對系統(tǒng)監(jiān)聽,正如"僵死 Zombie(a defunct process) 進(jìn)程已終止,但進(jìn)程描述符存在, 直到父進(jìn)程調(diào)用wait4()系統(tǒng)調(diào)用后釋放",所說一致。
總結(jié)
嚴(yán)格地來說,僵尸進(jìn)程并不是問題的根源,罪魁禍?zhǔn)资钱a(chǎn)生出大量僵尸進(jìn)程的那個(gè)父進(jìn)程。
因此,當(dāng)我們尋求如何消滅系統(tǒng)中大量的僵尸進(jìn)程時(shí),更應(yīng)該是在實(shí)際的開發(fā)過程中,思考如何避免僵尸進(jìn)程的產(chǎn)生。
參考:
https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/syscall/syscall_linux.go;l=279
到此這篇關(guān)于一文搞懂Go Exec 僵尸與孤兒進(jìn)程 的文章就介紹到這了,更多相關(guān)Go Exec 僵尸與孤兒進(jìn)程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang之?dāng)?shù)據(jù)校驗(yàn)的實(shí)現(xiàn)代碼示例
這篇文章主要介紹了golang之?dāng)?shù)據(jù)校檢的實(shí)現(xiàn)代碼示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10
go?defer?return?panic?執(zhí)行順序示例詳解
這篇文章主要介紹了go?defer?return?panic?執(zhí)行順序,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-01-01
Go設(shè)計(jì)模式原型模式考查點(diǎn)及使用詳解
這篇文章主要為大家介紹了Go設(shè)計(jì)模式原型模式考查點(diǎn)及使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

