session 是什么?
我們常見的 Linux session 一般是指 shell session。Shell session 是終端中當前的狀態,在終端中只能有一個 session。當我們打開一個新的終端時,總會創建一個新的 shell session。
就進程間的關系來說,session 由一個或多個進程組組成。一般情況下,來自單個登錄的所有進程都屬於同一個 session。我們可以通過下圖來理解進程、進程組和 session 之間的關系:
會話是由會話中的第一個進程創建的,一般情況下是打開終端時創建的 shell 進程。該進程也叫 session 的領頭進程。Session 中領頭進程的 PID 也就是 session 的 SID。我們可以通過下面的命令查看 SID:
[root@localhost ~]# ps -o pid,ppid,pgid,sid,tty,comm PID PPID PGID SID TT COMMAND 46882 46878 46882 46882 pts/0 bash 46899 46882 46899 46882 pts/0 ps
Session 中的每個進程組被稱為一個 job,有一個 job 會成為 session 的前台 job(foreground),其它的 job 則是后台 job(background)。每個 session 連接一個控制終端(control terminal),控制終端中的輸入被發送給前台 job,從前台 job 產生的輸出也被發送到控制終端上。同時由控制終端產生的信號,比如 ctrl + z 等都會傳遞給前台 job。
一般情況下 session 和終端是一對一的關系,當我們打開多個終端窗口時,實際上就創建了多個 session。
Session 的意義在於多個工作(job)在一個終端中運行,其中的一個為前台 job,它直接接收該終端的輸入並把結果輸出到該終端。其它的 job 則在后台運行。
session 的誕生與消亡
通常,新的 session 由系統登錄程序創建,session 中的領頭進程是運行用戶登錄 shell 的進程。新創建的每個進程都會屬於一個進程組,當創建一個進程時,它和父進程在同一個進程組、session 中。
將進程放入不同 session 的惟一方法是使用 setsid 函數使其成為新 session 的領頭進程。這還會將 session 領頭進程放入一個新的進程組中。
當 session 中的所有進程都結束時 session 也就消亡了。實際使用中比如網絡斷開了,session 肯定是要消亡的。另外就是正常的消亡,比如讓 session 的領頭進程退出。一般情況下 session 的領頭進程是 shell 進程,如果它處於前台,我們可以使用 exit 命令或者是 ctrl + d 讓它退出。或者我們可以直接通過 kill 命令殺死 session 的領頭進程。這里面的原理是:當系統檢測到掛斷(hangup)條件時,內核中的驅動會將 SIGHUP 信號發送到整個 session。通常情況下,這會殺死 session 中的所有進程。
session 與終端的關系
如果 session 關聯的是偽終端,這個偽終端本身就是隨着 session 的建立而創建的,session 結束,那么這個偽終端也會被銷毀。
如果 session 關聯的是 tty1-6,tty 則不會被銷毀。因為該終端設備是在系統初始化的時候創建的,並不是依賴該會話建立的,所以當 session 退出,tty 仍然存在。只是 init 系統在 session 結束后,會重啟 getty 來監聽這個 tty。
nohup
如果我們在 session 中執行了 nohup 等類似的命令,當 session 消亡時,相關的進程並不會隨着 session 結束,原因是這些進程不再受 SIGHUP 信號的影響。比如我們執行下面的命令:
$ nohup sleep 1000 >/dev/null 2>&1 &
此時 sleep 進程的 sid 和其它進程是相同的,還可以通過 pstree 命令看到進程間的父子關系:
如果我們退出當前 session 的領頭進程(bash),sleep 進程並不會退出,這樣我們就可以放心的等待該進程運行結果了。
nohup 並不改變進程的 sid,同時也說明在這種情況中,雖然 session 的領頭進程退出了,但是 session 依然沒有被銷毀(至少 sid 還在被引用)。重新建立連接,通過下面的命令查看 sleep 進程的信息,發現進程的 sid 依然是 7837:
但是此時的 sleep 已經被系統的 1 號進程 systemd 收養了:
setsid
setsid 會創建一個新的 session,它的目的是讓進程在后台執行命令,實現方式就是讓命令進程運行在一個新的與終端脫離的 session 中。看下面的示例:
$ setsid sleep 1000
查找之下居然沒有發現 sleep 進程的蹤跡:
通過 grep 查詢 sleep 進程的 PID:
去查看 sleep 進程所在的 sid,發現是一個新的 session ID,並且沒有關聯終端:
當一個進程通過調用 setsid 成為一個新的 session 領頭進程時,它會與控制終端斷開連接。
此時通過 pstree 查看進程間的關系,發現 sleep 進程直接被系統的 1 號進程 systemd 收養了:
控制終端(controlling terminal)
控制終端是進程的一個屬性。通過 fork 系統調用創建的子進程會從父進程那里繼承控制終端。這樣,session 中的所有進程都從 session 領頭進程那里繼承控制終端。Session 的領頭進程稱為終端的控制進程(controlling process)。簡單點說就是:一個 session 只能與一個終端關聯,這個終端被稱為 session 的控制終端(controlling terminal)。同時只能由 session 的領頭進程來建立或者改變終端與 session 的聯系。我們可以通過 ps 命令查看進程的控制終端:
支持 job control 的 shell 必須能夠控制在某一時刻由哪個 job 使用終端。否則,可能會有多個 job 試圖同時從終端讀取數據,這會導致進程在接收用戶輸入時的混亂。為了防止這種情況發生,shell 必須按照預定的協議與終端驅動程序協作。
shell 一次只允許一個 job(進程組)訪問控制終端。來自控制終端的某些輸入會導致信號被發送到與控制終端關聯的 job(進程組)中的所有進程。該 job 被稱為控制終端上的前台 job。由 shell 管理的其他 job 在不訪問終端的情況下,被稱為后台 job。
Shell 的職責是通知 job 何時停止何時啟動,還要把 job 的信息通知給用戶,並提供機制允許用戶繼續暫停的 job、在前台和后台之間切換 job。比如前台 job 可以無限制的自由使用控制終端,而后台 job 則不可以。當后台 job 中的進程試圖從其控制終端讀取數據時,通常會向進程組發送 SIGTTIN 信號。這通常會導致該組中的所有進程停止(變成 stopped 狀態)。類似地,當后台 job 中的進程試圖寫入其控制終端時,默認行為是向進程組發送 SIGTTOU 信號,但是否允許寫入的控制會更加的復雜。
參考:
What is the definition of a “session” in linux?
Linux session和進程組概述
Job Control
Linux TTY/PTS概述
setsid source code