一文搞懂Go?Exec?僵尸與孤兒進程
最近,使用 golang
去管理本地應用的生命周期,期間有幾個有趣的點,今天就一起看下。
場景一
我們來看看下面兩個腳本會產生什么問題:
創(chuàng)建兩個 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í)行腳本
輸出結果
$ ./start.sh
0
1
2
...
進程關系
查看進程信息
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
的 父進程(PPID)為start.sh
的進程id(PID)sub.sh
和start.sh
兩個進程的PGID
是同一個,( 屬一個進程組)。
刪除 start.sh
的進程
kill -9 31758 # 再查看進程組 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
進程不在了sub.sh
進程還在執(zhí)行sub.sh
進程的PID
變成了 1
問題1:
那sub.sh
這個進程現(xiàn)在屬于什么?
場景二
假設sub.sh
是實際的應用, start.sh
是應用的啟動腳本。
那么,golang
是如何管理他們的呢? 我們繼續(xù)看看下面 關于golang
的場景。
在上面兩個腳本的基礎上,我們用golang
的 os/exec
庫去調用 start.sh
腳本
package main import ( "context" "log" "os" "os/exec" "time" ) func main() { cmd := exec.CommandContext(context.Background(), "./start.sh") // 將 start.sh 和 sub.sh 移到當前目錄下 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
查看進程
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
三個進程為同一個進程組(同一個 PGID)
父子關系為: main.go
-> start.sh
-> sub.sh
刪除 start.sh
的進程
實際場景,有可能啟動程序掛了,導致我們無法監(jiān)聽到執(zhí)行程序的情況,刪除start.sh
進程,模擬下場景 :
kill -9 45462
再查看進程
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 退出也不知道了,那要如何處理?
問題分析
兩個場景中, 都有一個共同的點,就是
PPID
為1,這妥妥的成為沒人要的娃了——孤兒進程
場景二中,如果
cmd
的沒有進程沒有被回收,go
程序也無法管理,那么start.sh
就成為了占著茅坑不拉屎的子進程——僵尸進程
那究竟什么是孤兒進程
和 僵尸進程
?
孤兒進程
在類 UNIX
操作系統(tǒng)中,孤兒進程(Orphan Process)指:是在其父進程執(zhí)行完成或被終止后仍繼續(xù)運行的一類進程。
為避免孤兒進程退出時無法釋放所占用的資源而僵死,任何孤兒進程產生時都會立即為系統(tǒng)進程 init
或 systemd
自動接收為子進程,這一過程也被稱為收養(yǎng)
。在此需注意,雖然事實上該進程已有init
作為其父進程,但由于創(chuàng)建該進程的進程已不存在,所以仍應稱之為孤兒進程
。孤兒進程會浪費服務器的資源,甚至有耗盡資源的潛在危險。
解決&預防
終止機制:強制殺死孤兒進程(最常用的手段);
再生機制:服務器在指定時間內查找調用的客戶端,若找不到則直接殺死孤兒進程;
超時機制:給每個進程指定一個確定的運行時間,若超時仍未完成則強制終止之。若有需要,亦可讓進程在指定時間耗盡之前申請延時。
進程組:因為父進程終止或崩潰都會導致對應子進程成為孤兒進程,所以也無法預料一個子進程執(zhí)行期間是否會被“遺棄”。有鑒于此,多數(shù)類UNIX系統(tǒng)都引入了進程組以防止產生孤兒進程。
僵尸進程
在類 UNIX
操作系統(tǒng)中,僵尸進程(zombie process)指:完成執(zhí)行(通過exit系統(tǒng)調用,或運行時發(fā)生致命錯誤或收到終止信號所致),但在操作系統(tǒng)的進程表中仍然存在其進程控制塊,處于"終止狀態(tài)"的進程。
正常情況下,進程直接被其父進程 wait
并由系統(tǒng)回收。而僵尸進程與正常進程不同,kill
命令對僵尸進程無效,并且無法回收,從而導致資源泄漏。
解決&預防
收割僵尸進程的方法是通過 kill
命令手工向其父進程發(fā)送SIGCHLD信號。如果其父進程仍然拒絕收割僵尸進程,則終止父進程,使得 init
進程收養(yǎng)僵尸進程。init
進程周期執(zhí)行 wait
系統(tǒng)調用收割其收養(yǎng)的所有僵尸進程。
查看進程詳情
# 列出進程 ps -l
- USER:進程的所屬用戶
- PID:進程的進程ID號
- RSS:進程占用的固定的內存量 (Kbytes)
- S:查看進程狀態(tài)
- CMD:進程對應的實際程序
進程狀態(tài)(S)
- R:運行 Runnable (on run queue) 正在運行或在運行隊列中等待
- S:睡眠 Sleeping 休眠中,受阻,在等待某個條件的形成或接受到信號
- I:空閑 Idle
- Z:僵死 Zombie(a defunct process) 進程已終止,但進程描述符存在, 直到父進程調用wait4()系統(tǒng)調用后釋放
- D:不可中斷 Uninterruptible sleep (ususally IO) 收到信號不喚醒和不可運行, 進程必須等待直到有中斷發(fā)生
- T:終止 Terminate 進程收到SIGSTOP、SIGSTP、 SIGTIN、SIGTOU信號后停止運行運行
- P:等待交換頁
- W:無駐留頁 has no resident pages 沒有足夠的記憶體分頁可分配
- X:死掉的進程
Go解決方案
采用 殺掉進程組(kill process group,而不是只 kill 父進程,在 Linux 里面使用的是 kill -- -PID
) 與 進程wait方案,結果如下:
package main import ( "context" "log" "os" "os/exec" "syscall" "time" ) func main() { ctx := context.Background() cmd := exec.CommandContext(ctx, "./start.sh") // 設置進程組 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)聽進程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 } }
進行了 syscall.Wait4
對系統(tǒng)監(jiān)聽,正如"僵死 Zombie(a defunct process) 進程已終止,但進程描述符存在, 直到父進程調用wait4()系統(tǒng)調用后釋放",所說一致。
總結
嚴格地來說,僵尸進程并不是問題的根源,罪魁禍首是產生出大量僵尸進程的那個父進程。
因此,當我們尋求如何消滅系統(tǒng)中大量的僵尸進程時,更應該是在實際的開發(fā)過程中,思考如何避免僵尸進程的產生。
參考:
https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/syscall/syscall_linux.go;l=279
到此這篇關于一文搞懂Go Exec 僵尸與孤兒進程 的文章就介紹到這了,更多相關Go Exec 僵尸與孤兒進程內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
基于Go?goroutine實現(xiàn)一個簡單的聊天服務
對于聊天服務,想必大家都不會陌生,因為在我們的生活中經常會用到,本文我們用?Go?并發(fā)來實現(xiàn)一個聊天服務器,這個程序可以讓一些用戶通過服務器向其它所有用戶廣播文本消息,文中通過代碼示例介紹的非常詳細,需要的朋友可以參考下2023-06-06Go語言string,int,int64 ,float之間類型轉換方法
Go語言中int類型和string類型都是屬于基本數(shù)據(jù)類型,兩種類型的轉化都非常簡單。下面通過本文給大家分享Go語言string,int,int64 ,float之間類型轉換方法,感興趣的朋友一起看看吧2017-07-07