現象
經常在linux下開發的人應該都有這樣的經驗,就是在終端上啟動的程序,在關閉終端時,這個程序的進程也被一起關閉了。看下面這個程序,為了使進程永遠運行,在輸出helloworld后,循環調用sleep:
直接關閉這個終端,在另一個終端上查找該進程,已經找不到了:
這個行為看起來似乎是理所當然的,也符合人的第一感覺:”在終端上啟動的程序是屬于終端的,所以當關閉終端時,這個終端里的一包裹進程都一起被解決掉了”。但這種說法是不能使一個會思考且充滿好奇心的人信服的。
下面我們就從linux進程管理的細節來剖析其根本原因。
終端進程
linux系統是基于進程的,幾乎每個命令都可以在相應的目錄下找到它們的程序,執行一個命令相當于啟動一個或多個程序,終端也不例外,在我centos下面終端對應一個bash程序(不同操作系統終端的bash程序可能不一樣),它位于/usr/bin/下面:
每當打開一個終端都會啟動一個bash進程,我這里啟動了兩個終端,可以看到有兩個bash進程:
終端進程與啟動進程的關系
linux系統里面所有的進程的關系可以看做一個樹形結構,系統持續運行,進程的不斷啟動就是不斷fork的過程(fork是linux系統api,作用是復制自己來生成子進程),從系統啟動、初始化、登錄終端、到執行命令都是生成子進程的過程:
init進程是所有進程的祖先,它的pid(進程id)為1,ppid(父進程id)也為1,因為它沒有父進程,系統內的其他進程都是由它或者它的子進程fork而來。
我們在linux上作業的終端對應了一個bash進程,在其上運行的命令和程序都是bash的子進程,或由bash的子進程衍生。
用hw程序驗證一下,可以看到hw進程的父進程正好是bash進程:
但這并不能解釋為什么終端關閉了在上面運行的程序也跟著退出,因為在linux下,進程之間的關系并不像線程那樣,當主線程退出時,子線程一起被強制退出。進程之間沒有主次的區別,但有父子關系,而父子進程的運行是相對獨立的,一方的退出不會導致另一方退出。
進程session-揭開真相
在linux下,一個session是由一組進程組構成的,每個進程組又由多個進程構成。
在一個bash上運行的程序都歸屬于一個session(除非特別處理),而這個bash就是這個session的leader。每個session又可以關聯一個控制終端(Controlling Terminal)。
圖片:
- hw進程的ppid=5933,說明父進程為第一個bash,這個bash的父進程為gnome-ternimal進程,gnome-ternimal是centos可視化界面的終端管理進程,每打開一個終端,它都會啟動一個bash進程,而用戶的命令也是直接由bash進程執行的。
- hw程序和第一個bash同屬于一個session(sid=5933),這個sid等于bash的pid,所以第一個bash是這個session的leader。
- 圖片中還顯示了bash和hw進程擁有共同的終端設備pts/2,它是一種字符設備,不同于上面提到的gnome-ternimal進程。
- 當控制終端(對應gnome-ternimal)檢測到終端設備斷(對應pts/2)開連接時,會通知設備的控制進程,即發送SIGHUP信號給session leader(對應bash進程)。
- bash進程在收到SIGHUP后,將信號發給session下的所有進程,導致用戶啟動的進程退出。
下面通過strace命令來驗證以上結論:
-
跟蹤hw進程(命令意為跟蹤pid為6367的進程上與signal有關的系統調用):
strace -e trace=signal -p 6367
-
跟蹤bash進程(命令意為跟蹤pid為5933的進程上與signal有關的系統調用):
strace -e trace=signal -p 5933
-
關閉啟動hw程序的終端,觀察strace輸出.
hwd的strace如下,si_pid=5933說明是5933這個進程發了SIGHUP給它,也就是bash進程:
bash的strace略微復雜:
-
kill(4294960929, SIGHUP)
kill第一個參數是32位有符號整數,轉換成int就是-6367,當參數為負時表示發送給這個數絕對值的進程組,即pgrp=6367的所有進程,在上面的圖片中可以看到hw進程正好屬于該進程組。
-
kill(5933, SIGHUP)
5933是自己的pid,bash在第一次收到SIGHUP時先把信號發給session內其他進程,然后再次發送SIGHUP命令給自己,將自己殺死,后面的si_pid=5933也證實了這一點。
如何讓終端關閉時進程不退出
根據上面的結論,要使終端關閉時進程不退出,有以下幾種情況:
- 用戶進程攔截SIGHUP信號。
- 用戶進程和bash進程不在一個session。
下面依次驗證這兩種情況
攔截SIGHUP
修改hw程序,忽略SIGHUP信號:
- 1
- 2
執行hw程序,并查看進程,可以看到hw進程和父進程bash:
關閉終端,在另一個終端查看進程:
bash進程已經退出,但hw進程還在,符合預期!!而且hw進程的ppid變成了1,說明hw在父進程bash退出后變成孤兒進程被init進程收養。
新建session&setsid
為了使用戶進程和bash不在同一個session,需要調用setsid方法,該方法的作用是新建一個新的session,并使自己成為leader。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
調用setsid前先fork,因為若不fork,hw作為進程組的leader,是不允許重建session的,原因留給讀者自己思考。
編譯并執行hw,查看進程:
可以看到,相比之前,有幾個不同的地方:
- 程序啟動完,返回終端,hw切換到后臺運行。
- hw進程的父進程不再是bash,而是init進程。
- hw沒有關聯的終端設備(pts/2)。
關閉終端,看到bash已經消失,但對hw進程沒有任何影響:
更簡單的方法
-
setsid命令,用setsid來啟動程序,這樣就不用修改任何代碼也可以做到使啟動的進程在新的session中,并且終端關閉時,進程不退出。
setsid ./hw
-
nohup命令,被nohup啟動的程序會忽略SIGHUP信號。
nohup ./hw
其他
命令行中&的作用:
- 1
- 2
&的作用是使程序在后臺運行,輸入fg命令又可以使程序切換到前臺。雖然在后臺運行,但并不能保證進程在終端關閉時不退出。
總結
簡而言之,終端在關閉時會發送SIGHUP給對應的bash進程,bash進程收到這個信號后首先將它發給session下面的進程,如果你的程序沒有對SIGHUP信號做特殊處理,那么進程就會隨著終端關閉而退出。