1. 程式人生 > >linux終端關閉時為什麼會導致在其上啟動的程序退出?

linux終端關閉時為什麼會導致在其上啟動的程序退出?

現象

經常在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)。

這裡寫圖片描述

圖片:

  1. hw程序的ppid=5933,說明父程序為第一個bash,這個bash的父程序為gnome-ternimal程序,gnome-ternimal是centos視覺化介面的終端管理程序,每開啟一個終端,它都會啟動一個bash程序,而使用者的命令也是直接由bash程序執行的。
  2. hw程式和第一個bash同屬於一個session(sid=5933),這個sid等於bash的pid,所以第一個bash是這個session的leader。
  3. 圖片中還顯示了bash和hw程序擁有共同的終端裝置pts/2,它是一種字元裝置,不同於上面提到的gnome-ternimal程序。
  4. 當控制終端(對應gnome-ternimal)檢測到終端裝置斷(對應pts/2)開連線時,會通知裝置的控制程序,即傳送SIGHUP訊號給session leader(對應bash程序)。
  5. bash程序在收到SIGHUP後,將訊號發給session下的所有程序,導致使用者啟動的程序退出。

下面通過strace命令來驗證以上結論:

  1. 跟蹤hw程序(命令意為跟蹤pid為6367的程序上與signal有關的系統呼叫):

    strace -e trace=signal -p 6367

  2. 跟蹤bash程序(命令意為跟蹤pid為5933的程序上與signal有關的系統呼叫):

    strace -e trace=signal -p 5933

  3. 關閉啟動hw程式的終端,觀察strace輸出.

hwd的strace如下,si_pid=5933說明是5933這個程序發了SIGHUP給它,也就是bash程序:

這裡寫圖片描述

bash的strace略微複雜:

這裡寫圖片描述

  1. kill(4294960929, SIGHUP)

    kill第一個引數是32位有符號整數,轉換成int就是-6367,當引數為負時表示傳送給這個數絕對值的程序組,即pgrp=6367的所有程序,在上面的圖片中可以看到hw程序正好屬於該程序組。

  2. kill(5933, SIGHUP)

    5933是自己的pid,bash在第一次收到SIGHUP時先把訊號發給session內其他程序,然後再次傳送SIGHUP命令給自己,將自己殺死,後面的si_pid=5933也證實了這一點。

如何讓終端關閉時程序不退出

根據上面的結論,要使終端關閉時程序不退出,有以下幾種情況:

  1. 使用者程序攔截SIGHUP訊號。
  2. 使用者程序和bash程序不在一個session。

下面依次驗證這兩種情況

攔截SIGHUP

修改hw程式,忽略SIGHUP訊號:

signal(SIGHUP, SIG_IGN);

執行hw程式,並檢視程序,可以看到hw程序和父程序bash:

這裡寫圖片描述

關閉終端,在另一個終端檢視程序:

這裡寫圖片描述

bash程序已經退出,但hw程序還在,符合預期!!而且hw程序的ppid變成了1,說明hw在父程序bash退出後變成孤兒程序被init程序收養。

新建session&setsid

為了使使用者程序和bash不在同一個session,需要呼叫setsid方法,該方法的作用是新建一個新的session,並使自己成為leader。

// 先fork
int pid = fork();
if(pid > 0){
    // 父程序, 直接退出
    return 1;
}else if(pid == 0){
    // 子程序
    // 建立新的session
    setsid();
    //
    printf("Hello World!\n");
    printf("sleeping...\n");
    while(1){
        sleep(1);
    }
}

呼叫setsid前先fork,因為若不fork,hw作為程序組的leader,是不允許重建session的,原因留給讀者自己思考。

編譯並執行hw,檢視程序:

這裡寫圖片描述

可以看到,相比之前,有幾個不同的地方:

  1. 程式啟動完,返回終端,hw切換到後臺執行。
  2. hw程序的父程序不再是bash,而是init程序。
  3. hw沒有關聯的終端裝置(pts/2)。

關閉終端,看到bash已經消失,但對hw程序沒有任何影響:

這裡寫圖片描述

更簡單的方法

  1. setsid命令,用setsid來啟動程式,這樣就不用修改任何程式碼也可以做到使啟動的程序在新的session中,並且終端關閉時,程序不退出。

    setsid ./hw

  2. nohup命令,被nohup啟動的程式會忽略SIGHUP訊號。

    nohup ./hw

其他

命令列中&的作用:

./hw &

&的作用是使程式在後臺執行,輸入fg命令又可以使程式切換到前臺。雖然在後臺執行,但並不能保證程序在終端關閉時不退出。

總結

簡而言之,終端在關閉時會發送SIGHUP給對應的bash程序,bash程序收到這個訊號後首先將它發給session下面的程序,如果你的程式沒有對SIGHUP訊號做特殊處理,那麼程序就會隨著終端關閉而退出。