Linux應用——程序基礎

jll133688發表於2024-05-24

誰來呼叫 main 函式

在執行 main 函式之前,會有一段引導程式碼,最終由這段程式碼呼叫 main 函式,這段引導程式碼不需要自己編寫,而是在編譯、連結中由連結器將這段程式連結到應用程式中,構成最終的可執行檔案,載入器會將可執行檔案載入到記憶體中

程序的終止

正常終止

  1. 在 main 函式中透過 return 返回,終止程序
  2. 呼叫庫函式 exit 終止程序
  3. 呼叫系統呼叫_exit/_Exit

異常終止

  1. 呼叫 abort 函式終止程序
  2. 被訊號終止

終止程序:exit()和_exit()

exit()和_exit()用法

  • void _exit(int status):終止程序的執行,引數 status 表示程序終止時的狀態,通常 0 表示正常終止,非零值表示發證了錯誤,如 open 開啟檔案失敗(不是 abort 所表示的異常)
  • void exit(int status):引數 status 含義同上

exit()和_exit()區別

  1. exit() 是庫函式,_exit() 是一個系統呼叫,他們所需要包含的標頭檔案不同
  2. 這兩個函式的終止目的相同,都是終止程序,但在終止過程前需要做的處理不一樣

exit()在終止程序的時候會呼叫終止處理函式:int atexit(void (*function)(void));,可以呼叫多個 atexit,呼叫順序和註冊順序相反

標準輸出預設是行快取,檢測到"\n"後才會把該行輸出,_exit()不會重新整理 IO 快取,因此沒有"\n"的情況時該行不會輸出

  • 不會重新整理 stdio 緩衝的情況
    • _exit()/_Exit()
    • 被訊號終止

exit()和 return 的區別

  1. exit()為庫函式,return 為 C 語言的語句
  2. exit()函式最終會進入到核心,把控制權交給核心,最終由核心去終止程序;return 並不會進入核心,只是一個函式的返回,返回到它的上層呼叫,最終由上層呼叫終止程序
  • return 和 exit 同樣會呼叫終止處理函式、重新整理 IO 快取

exit()和 abort 區別

  1. exit 函式用於正常終止程序(執行一些清理工作),abort 用於異常終止程序(不會執行清理工作,會直接終止程序),abort 本質上是直接執行 SIGABRT 訊號的系統預設處理操作

程序的環境變數

環境變數的概念

  • 環境變數是指在程序執行環境中定義一些變數,類似於程序的全域性變數,可以在程式的任何地方獲取,只需宣告即可。但與全域性變數不同的是,這些環境變數可以被其他子程序所繼承,也就是具有繼承性
  • 環境變數的本質還是變數,不過這些變數沒有型別,都是以字串的形式儲存在一個字串陣列當中,稱為環境表(以 NULL 結尾),陣列中的每個環境變數都是以 name = value 這種形式定義的,name 表示變數名稱,value 表示變數值

環境變數相關命令

  1. env:使用命令檢視環境變數
  2. echo $name:檢視環境變數
  3. export name=value:自定義/修改環境變數(注意等號前後不要有空格)
  4. unset name:刪除環境變數

常見的環境變數

  1. PATH:用於指定可執行程式的搜尋路徑
  2. HOME:當前使用者的家目錄
  3. LOGNAME:指定當前登入的使用者
  4. HOSTNAME:指定主機名
  5. SHELL:指定當前 shell 解析器
  6. PWD:指定程序的當前工作目錄

環境變數的組織形式

在應用程式中獲取環境變數

在每個應用程式中都有一組環境變數,是在程序建立中從父程序中繼承過來的

  1. environ 變數獲取:全域性變數,可以直接在程式中使用,只需要申明就好,environ 實際上是一個指標,指向環境表
extern char **environ;  // 申明一下,即可使用environ[i]
  1. 透過 main 函式獲取(儘量不要使用這種方式,有的系統可能不支援)
int main(int argc, char *argv[], char *env[]);   // 第三個引數為程序的環境表
  1. 透過 getenv 獲取指定的環境變數(庫函式)
#include <stdlib.h>
char *getenv(const char *name);  // 如果存放該環境變數,則返回該環境變數的值對應字串的指標;如果不存在該環境變數,則返回NULL
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
// 測試
int main(int argc, char *argv[]){
    char *get_str;
    get_str = getenv(argv[1]);
    if(get_str == NULL){
        printf("error!\n");
        exit(1);
    }
    printf("%s\n", get_str);
    exit(0);
}

使用 getenv()需要注意,不應該去修改其返回的字串,修改該字串意味著修改了環境變數對應的值,Linux 提供了相應的修改函式,如果需要修改環境變數的值應該使用這些函式,不應直接改動該字串。

新增/修改/刪除環境變數

  1. putenv:新增/修改環境變數(有對應的 name 則修改,沒有則新增)
#include <stdlib.h>
int putenv(char *string);  // string是一個字串指標,指向name=value形式的字串;成功返回 0,失敗將返回非0值,並設定 errno

該函式呼叫成功之後,引數 string 所指向的字串就成為了程序環境變數的一部分了,換言之,putenv()函式將設定 environ 變數中的某個元素指向該 string 字串,而不是指向它的複製副本,這裡需要注意!因此,不能隨意修改引數 string 所指向的內容,這將影響程序的環境變數,引數 string 不應為自動變數(即在棧中分配的字元陣列),因為自動變數的生命週期是函式內部,出了函式之後就不存在了(可以使用 malloc 分配堆記憶體,或者直接使用全域性變數)

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
// 測試
int main(int argc, char *argv[]){
    char *get_str;
    if(putenv(argv[1])){
        printf("error!\n");
        exit(-1);
    }
    for(int i = 0; environ[i] != NULL; i++){
        printf("%s\n", environ[i]);
    }
    return 0;
}

上述程式碼 putenv 在本程序範圍內修改了環境變數,程序結束後,原來的環境變數不變

  1. setenv:新增/修改環境變數(推薦使用這個)(可替代 putenv 函式)
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);  // name為環境變數的名稱,value為環境變數的值
// overwrite:若name標識的環境變數已經存在,在引數overwrite為0的情況下,setenv()函式將不改變現有環境變數的值,也就是說本次呼叫沒有產生任何影響;如果引數overwrite的值為非0,若引數name標識的環境變數已經存在,則覆蓋,不存在則表示新增新的環境變數。
  • setenv 和 putenv 的區別:
    • setenv 會將使用者傳入的 name=value 字串複製到自己的緩衝區中,而 putenv 不會
    • setenv()可透過引數 overwrite 控制是否需要修改現有變數的值而僅以新增變數為目的,顯然 putenv()並不能進行控制
  1. name=value ./test:在程序執行時新增環境變數(可同時新增多個環境變數,用空格隔開)
  2. unsetenv:從環境表中移除引數 name 標識的環境變數
#include <stdlib.h>
int unsetenv(const char *name);

清空環境變數

  1. 將 environ 設定為 NULL
  2. 透過 clearenv 來清空環境變數
#include <stdlib.h>
int clearenv(void);

建立子程序

所有的子程序都是由父程序建立出來的

  • 比如在終端執行./test,這個程序是由 shell 程序(bash、sh 等 shell 解析器)建立出來的
  • 最原始的程序為 init 程序,它的 PID 為 1,由它建立出其他程序

getpid()獲取當前程序的 PID,getppid()獲取當前程序父程序的 PID,命令列中透過 ps-aux/pstree -T 命令檢視 PID

父子程序間的檔案共享

  • 檔案共享:多個程序、多個執行緒對同一檔案進行讀寫操作
  • 子程序會複製父程序開啟的所有檔案描述符(fd)

  • 驗證父子程序間的檔案共享是按照接續寫(使用這個)還是分別寫:

接續寫:兩個檔案描述符指向同一個檔案表,使用同一個讀寫指標
分別寫:可能會出現資料覆蓋的情況

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// 測試
int main(void){
    int fd;
    int pid;
    fd = open("./test.txt", O_WRONLY | O_TRUNC);
    if(fd == -1){
        printf("error");
        return 1;
    }
    pid = fork();
    if(pid > 0){
        printf("parent %d %d\n", pid, getppid());
        write(fd, "123456", 6);
        close(fd);
    }else if(pid == 0){
        printf("child %d %d\n", getpid(), getppid());  
        write(fd, "Hello World", 11);
        close(fd);
    }else{
        printf("build error\n");
        exit(-1);
    }
    exit(0);
}

父子程序間的競爭關係

fork 之後不知道是父程序先返回還是子程序先返回,由測試結果來看,絕大部分情況下是父程序先返回

父程序監視子程序

  • 父程序需要知道子程序的狀態改變:

    • 子程序終止
    • 子程序因為收到停止訊號而停止執行(SIGSTOP、SIGTSTP)
    • 子程序在停止狀態下因為收到恢復訊號而恢復執行(SIGCONT)
  • 以上也是 SIGCHLD 訊號的三種觸發情況,當子程序發生狀態改變時,核心會向父程序傳送這個 SIGCHLD 訊號

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
int  waitid(idtype_t  idtype, id_t id, siginfo_t *infop, int options);

wait 函式

  • wait 函式為系統呼叫,可以等待程序的任一子程序終止,同時獲取子程序的終止資訊(監視子程序第一種狀態的改變),作用:
    1. 監視子程序什麼時候被終止,以及獲取子程序終止時的狀態資訊
    2. 回收子程序的一些資源(俗稱“收屍”)
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);  // wstatus用於存放子程序終止時的狀態資訊,可以設定為NULL,表示不接收子程序終止時的狀態資訊
// 返回值:若成功返回終止的子程序對應的程序號,失敗則返回-1
  • 程序呼叫 wait()函式的情況:
    1. 如果該程序沒有子程序(即沒有需要等待的子程序),那麼 wait()將返回-1,並且將 errno 設定為 ECHILD
    2. 如果該程序所有子程序都還在執行,則 wait()會一直阻塞等待,直到某個子程序終止
    3. 如果呼叫 wait()之前該程序已經有一個或多個子程序終止了,那麼呼叫 wait()不會阻塞,會回收子程序的一些資源,注意一次 wait 呼叫只能為一個已經終止的子程序“收屍”
      1. status 為 NULL 或者 (int *)0 時,返回該退出的子程序的 PID 號
      2. 如果父程序關注子程序的退出時狀態,可以使用如下方式,status 將儲存子程序結束時的狀態資訊(子程序退出時 exit 裡的引數會被儲存到 status 中)

int status;
wait(&status);

  • 可以透過以下宏來檢查 status 引數:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
int main(void){
    int pid = fork();
    int ret;
    int status;
    if(pid > 0){
        printf("parent %d %d\n", pid, getppid());
        ret = wait(&status);
        printf("wait return %d %d\n", ret, WEXITSTATUS(status));
        exit(0);
    }else if(pid == 0){
        printf("child %d %d\n", getpid(), getppid());
        exit(3);  
    }else{
        printf("build error\n");
        exit(-1);
    }
    exit(0);
}
  • 使用 wait()的限制:
    • 如果父程序建立了多個子程序,使用 wait()將無法等待某個特定的子程序的完成,只能按照順序等待下一個子程序的終止,一個一個來、誰先終止就先處理誰;
    • 如果子程序沒有終止,正在執行,那麼 wait()總是保持阻塞,有時我們希望執行非阻塞等待,是否有子程序終止,透過判斷即可得知;
    • 使用 wait()只能發現那些被終止的子程序,對於子程序因某個訊號(譬如 SIGSTOP 訊號)而停止(注意這裡停止指的暫停執行),或是已停止的子程序收到 SIGCONT 訊號後恢復執行的情況就無能為力了(沒法監視後兩種狀態改變

waitpid 函式

waitpid 函式沒有 wait 函式存在的限制

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *wstatus, int options);  // wstatus的含義同wait裡的
// 返回值:與wait基本相同,但引數options包含了WNOHANG標誌時,返回值可能會出現0
  • 引數 pid 表示需要等待的某個具體子程序,取值如下:

    • pid > 0:等待程序號為 pid 的子程序
    • pid = 0:等待該父程序的所有子程序
    • pid < -1:等待程序組識別符號與 pid 絕對值相等的所有子程序(特殊情況可能為負數)
    • pid = -1:等待任意子程序
  • 引數 options 是一個位掩碼,設定為 0 時功能和 wait 相同(pid 為-1 時):

    • WNOHANG:如果子程序沒有發生狀態改變(終止、暫停),則立即返回,也就是執行非阻塞等待,透過返回值可以判斷是否有子程序發生狀態改變,若返回值等於 0 表示沒有發生改變(可以實現輪詢 poll 來監視子程序的狀態)
    • WUNTRACED:除了返回終止的子程序的狀態資訊外,還返回因訊號停止(暫停執行)的子程序狀態資訊
    • WCONTINUED:返回那些因收到 SIGCONT 訊號而恢復執行的子程序的狀態資訊

非同步方式監視子程序

  • 可以為 SIGCHLD 訊號(子程序退出時發給父程序的訊號)繫結一個訊號處理函式(為父程序繫結),然後在訊號處理函式中呼叫 wait/waitpid 函式回收子程序(針對第一種狀態,其他兩種狀態可以進行相應處理)

  • 這樣可以使得父程序做自己的事情(非同步),不用阻塞或者輪詢等待子程序結束(也可以透過多執行緒來實現)

  • 使用這一方法的注意事項:

    • 當呼叫訊號處理函式時,會暫時將引發呼叫的訊號新增到程序的訊號掩碼中(除非 sigaction()指定了 SA_NODEFER 標誌),這樣一來,當 SIGCHLD 訊號處理函式正在為一個終止的子程序“收屍”時,如果相繼有兩個子程序終止,即使產生了兩次 SIGCHLD 訊號,父程序也只能捕獲到一次 SIGCHLD 訊號,結果是,父程序的 SIGCHLD 訊號處理函式每次只呼叫一次 wait(),那麼就會導致有些殭屍程序成為“漏網之魚”
    • 解決方案就是:在 SIGCHLD 訊號處理函式中迴圈以非阻塞方式來呼叫 waitpid(),直至再無其它終止的子程序需要處理為止,所以,通常 SIGCHLD 訊號處理函式內部程式碼為:

while (waitpid(-1, NULL, WNOHANG) > 0)
continue;

上述程式碼一直迴圈下去,直至 waitpid()返回 0,表明再無殭屍程序存在;或者返回-1,表明有錯誤發生。

殭屍程序和孤兒程序

  • 孤兒程序:父程序先於子程序結束,在 Linux 系統當中,所有的孤兒程序都自動成為 init 程序(程序號為 1)的子程序,換言之,某一子程序的父程序結束後,init 程序變成了孤兒程序的“養父”;這是判定某一子程序的“生父”是否還“在世”的方法之一

  • 殭屍程序:子程序先於父程序結束,此時父程序還未來得及給子程序“收屍”,那麼此時子程序就變成了一個殭屍程序。

    • 當父程序呼叫 wait()(或waitpid、waitid等)為子程序“收屍”後,殭屍程序就會被核心徹底刪除
    • 如果父程序並沒有呼叫 wait()函式然後就退出了,那麼此時 init 程序將會接管它的子程序並自動呼叫 wait(),故而從系統中移除殭屍程序
    • 如果父程序建立了某一子程序,子程序已經結束,而父程序還在正常執行,但父程序並未呼叫 wait()回收子程序,此時子程序變成一個殭屍程序。首先來說,這樣的程式設計是有問題的,如果系統中存在大量的殭屍程序,它們勢必會填滿核心程序表,從而阻礙新程序的建立。需要注意的是,殭屍程序是無法透過訊號將其殺死的,即使是SIGKILL訊號也不行,那麼這種情況下,只能殺死殭屍程序的父程序或者等待其父程序終止,init 程序將會接管這些殭屍程序,從而將它們從系統中清理掉)
    • 所以,在我們的一個程式設計中,一定要監視子程序的狀態變化,如果子程序終止了,要呼叫 wait()將其回收,避免殭屍程序

執行新程式

  • 子程序和父程序執行的不是同一個程式,比如test程序透過fork函式建立子程序後,這個子程序也執行test這個程式,當這個子程序啟動後,透過呼叫庫函式或者系統呼叫用一個新的程式去替換test程式,然後從main函式開始執行這個新程式

execve函式

  • execve為系統呼叫,可以將新程式載入到某一程序的記憶體空間,透過呼叫 execve()函式將一個外部的可執行檔案載入到程序的記憶體空間執行,使用新的程式替換舊的程式,而程序的棧、資料、以及堆資料會被新程式的相應部件所替換,然後從新程式的main()函式開始執行
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
  • 引數及返回值含義:
    • pathname:指向新程式的路徑名(絕對路徑/相對路徑),對應 main(int argc, char *argv[])的 argv[0]
    • argv[]:傳遞給新程式的命令列引數(字串陣列,以 NULL 結束),對應 main 函式的 argv 引數
    • envp:指定了新程式的環境變數列表,對應新程式的 environ 陣列,以 NULL 結束
    • 返回值:呼叫成功不會返回(執行新程式去了),失敗返回-1,並設定 errno

注意 pathname 可以是路徑:./test,也可以是可執行檔名稱:test(一切接檔案)(在同一個目錄下)

exec 族庫函式

  • exec 族庫函式基於 execve 系統呼叫來實現
#include <unistd.h>
extern char **environ;
// execl("/bin/ls", "ls", "-a", "-l", NULL);
int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);  
//  execlp("ls", "ls", "-a", "-l", NULL);
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
// execle("/bin/ls", "ls", "-a", "-l", NULL, environ);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);

// execv("/bin/ls", argv[1]);
int execv(const char *pathname, char *const argv[]);
// execvp("ls", argv[1]);
int execvp(const char *file, char *const argv[]);
// execvp("ls", argv[1], environ);
int execvpe(const char *file, char *const argv[], char *const envp[]);
  • 引數含義
    • pathname 同 execve,指向新程式的路徑名,file 引數指向新程式檔名,它會去程序的 PATH 環境變數所定義的路徑尋找這個新程式(相容絕對路徑和相對路徑)
    • arg 引數將傳遞給新程式的引數列表依次排列,透過多個字串來傳遞,以 NULL 結尾
    • 預設情況下,新程式保留原來程式的環境表

system 函式

  • system 為庫函式,可以很方便地在程式中執行任意 shell 命令

  • system 內部實現原理:system()函式其內部的是透過呼叫 fork()、execl()以及 waitpid()這三個函式來實現它的功能。首先 system()會呼叫 fork()建立一個子程序,然後子程序會呼叫 execl()載入一個shell 直譯器程式(通常是/bin/sh程式),這是子程序就是一個 shell 程序了,這個 shell 子程序解析 command 引數對應的命令,並建立一個或多個子程序執行命令(命令時可執行程式,每執行一個命令,shell 子程序就需要建立一個程序然後載入命令/可執行程式),當命令執行完後 shell 子程序會終止執行,system()中的父程序會呼叫 waitpid()等待回收shell 子程序,直到 shell 子程序退出,父程序回收子程序後,system 函式返回。

  • system 每執行一個 shell 命令,system 函式至少要建立兩個子程序:

    1. system 函式建立 shell 子程序
    2. shell 子程序根據命令建立它的子程序(一個或多個,根據命令而定)
#include <stdlib.h> 
int system(const char *command);   // system("ls -al")
  • 引數及返回值含義:
    • command:指向需要執行的 shell 命令,如"ls -al"
    • 返回值:
      • 當引數 command 為 NULL,如果 shell 可用則返回一個非 0 值,若不可用則返回 0;針對一些非 UNIX 系統,該系統上可能是沒有 shell 的(bash/sh/csh),這樣就會導致 shell 不可用
      • 如果 command 不為 NULL,則:
        • 如果 system 無法建立子程序(fork 失敗)或無法獲取子程序的終止狀態(waitpid 返回-1),那麼 system()返回-1
        • 如果子程序不能執行 shell(execl 執行不成功),則 system()的返回值就是子程序透過呼叫_exit(127)終止了
        • 如果所有的系統呼叫都成功,system()函式會返回執行 command 的 shell 程序的終止狀態(執行最後一個命令的終止狀態)
// 根據system函式的功能以及該函式在不同情況下的返回值所實現的一個簡易的system函式
int system(const char *command){
    if(command == NULL){   // 返回值1
        if("當前系統中存在可用的shell解析器程式 bash/sh/csh")
            return "非零值";
        else
            return 0;
    }
    pid_t pid = fork();  // 建立子程序,該子程序會變為shell子程序
    switch(pid){
        case -1:        // 建立子程序失敗,返回值2
            return -1;
        // 子程序
        case 0:
            excel("/bin/sh", "sh", "-c", command, NULL);  // 載入shell解析器,如果成功不會返回
            _exit(127);  // 載入shell解析器失敗,呼叫_exit(127),返回值3
        // 父程序
        default:
            int status;
            int ret;

            ret = waitpid(pid, &status, NULL); // 等待回收子程序
            if(ret == -1)
                return -1;   // 無法獲取子程序的狀態資訊,返回值1
            return status;   // 返回子程序的狀態資訊
    }   // 如果所有系統呼叫都成功,那麼system返回shell子程序的終止狀態資訊
        // 即返回執行最後一個命令的終止資訊,返回值4
}
  • system()在使用上簡單,但是是以犧牲效率為代價的

vfork 函式

fork 系統呼叫使用場景

  • 父程序希望子程序複製自己,父子程序執行相同的程式,各自在自己的程序空間中執行
  • 子程序執行一個新的程式,從該程式的 main 函式開始執行,呼叫 exec 函式

fork 函式的缺點

fork+exec 配合使用時,效率比較低

vfork 函式

vfork 為系統呼叫,也是用來建立一個程序,返回值也是一樣的

fork 與 vfork 不同點

  1. 對於 fork 函式,fork 會為子程序建立一個新的地址空間(也就是程序空間),子程序幾乎完全複製了父程序,包括資料段、程式碼段、堆、棧等;而對於 vfork 函式,子程序在終止或者成功呼叫 exec 函式之前,子程序與父程序共享地址空間,共享所有記憶體,包括資料段、堆疊等,所以在子程序在終止或成功呼叫 exec 函式前,不要去修改除 vfork 的返回值的 pid_t 型別的變數之外的任何變數(父程序的變數)也不要呼叫任何其它函式(除 exit 和 exec 函式之外的任何其它函式),否則將會影響到父程序(vfork 函式的正確使用方法就是建立子程序後立馬呼叫 exec 載入新程式,所以沒有意義去呼叫其他函式或者修改變數)

注意:vfork 建立的子程序如果要終止應呼叫 exit,而不能呼叫 exit 或 return 返回,因為如果子程序呼叫 exit 或 return 終止,則會呼叫父程序繫結的終止處理函式以及重新整理父程序的 stdio 緩衝,影響到父程序

  1. 對於 fork 函式,fork 呼叫之後,父、子程序的執行次序不確定;而對於 vfork 函式,vfork 函式會保證子程序先執行,父程序此時處於阻塞、掛起狀態,在子程序終止或成功呼叫 exec 函式之後,父程序才會被排程執行

注意:如果子程序在終止或成功呼叫 exec 函式之前,依賴於父程序的進一步動作,將會導致死鎖!

  1. vfork 函式在建立子程序時,不用複製父程序的資料段、程式碼段、堆疊等,所以 vfork 函式的效率要高於 fork 函式

目前的 fork 函式使用了寫時複製技術,效率還算可以,所以儘量不要用 vfork,以免產生一些難以排查的問題

程序狀態和程序間的關係

程序狀態

  • 程序狀態有六種:

    • R(TASK_RUNNING):執行狀態或可執行狀態(就緒態):正在執行的程序或者在程序佇列中等待執行的程序都處於該狀態,所以該狀態實際上包含了執行態和就緒態這兩個基本狀態
    • S(TASK_INTERRUPTIBLE):可中斷睡眠狀態(淺度睡眠):可中斷睡眠狀態也被稱為淺度睡眠狀態,處於這個狀態的程序由於在等待某個事件(等待資源有效)而被系統掛起,譬如等待 IO 事件、主動呼叫 sleep 函式等。一旦資源有效時就會進入到就緒態,當然該狀態下的程序也可被訊號或中斷喚醒(所以可中斷的意思就是,即使未等到資源有效,也可被訊號中斷喚醒,譬如 sleep(5)休眠 5 秒鐘,通常情況下 5 秒未到它會一直睡眠、阻塞,但在這種情況下,收到訊號就會讓它結束休眠、被喚 )
    • D(TASK_UNINTERRUPTIBLE):不可中斷睡眠狀態(深度睡眠):不可中斷睡眠狀態也被稱為深度睡眠狀態,該狀態下的程序也是在等待某個事件、等待資源有效,一旦資源有效就會進入到就緒態;與淺度睡眠狀態的區別在於,深度睡眠狀態的程序不能被訊號或中斷喚醒,只有當它所等待的資源有效時才會被喚醒(一般該狀態下的程序正在跟硬體互動、互動過程不允許被其它程序中斷)
    • T(TASK_STOPPED):停止狀態(暫停狀態):當程序收到停止訊號時(譬如 SIGSTOP、SIGTSTP 等停止訊號),就會由執行狀態進入到停止狀態。當處於停止狀態下,收到恢復訊號(譬如 SIGCONT 訊號)時就會進入到就緒態
    • Z(TASK_ZOMBIE):**殭屍狀態 **:表示程序已經終止了,但是並未被其父程序所回收,也就是程序已經終止,但並未徹底消亡。需要其父程序回收它的一些資源,歸還系統,然後該程序才會從系統中徹底刪除
    • X(TASK_DEAD): 死亡狀態:此狀態非常短暫、ps 命令捕捉不到。處於此狀態的程序即將被徹底銷燬,可以認為就是殭屍程序被回收之後的一種狀態
  • ps 命令檢視到的程序狀態資訊中,除了第一個大寫字母用於表示程序狀態外,還有其他一些字元:

    • s:表示當前程序是一個會話的首領程序
    • l:表示當前程序包含了多個執行緒
    • N:表示低優先順序
    • <:表示高優先順序
    • +:表示當前程序處於前臺程序組中

程序間的關係

兩個程序之間的關係主要包括:父子關係程序組會話

程序組

  • 需要注意以下問題:

    • 每個程序必定屬於某一個程序組,並且只能在一個程序組中
    • 每一個程序組都有一個組長程序(建立程序組的程序),組長程序的程序 ID (PID)就等於該程序組的程序組 ID(PGID)。
    • 只要程序組中還存在至少一個程序,那麼該程序組就存在,這與其組長程序是否終止無關(組長程序終止並不一定導致程序組終止)
    • 一個程序組可以包含一個或多個程序,程序組的生命週期從建立開始,直到組內所有的程序終止或離開該程序組
    • 預設情況下,新建立的程序會繼承父程序的程序組 ID(PGID),子程序與父程序在同一個程序組中
  • 獲取/建立程序組

#include <sys/types.h>
#include <unistd.h>

pid_t getpgid(pid_t pid);   // 引數pid為指定要獲取哪個程序的程序組ID,如果引數為0表示呼叫者程序組的ID;如果呼叫成功,返回程序組ID,失敗返回-1,並設定errno
int setpgid(pid_t pid, pid_t pgid); // 用法1:將引數pid指定的程序的程序組ID設定為引數pgid(要保證這兩個程序組在同一個會話中);用法2:如果gpid所指定的程序組不存在,那麼會建立一個新的程序組,由引數pid指定的程序作為這個程序組的組長(這種情況下要保證pid = pgid);特殊取值:如果引數pid等於0,則表示使用呼叫者的程序ID;如果引數gpid等於0,則表示第二個引數設定為等於第一個引數pid

pid_t getpgrp(void); // 返回值為呼叫者程序對應的程序組ID,等價於getpid(0)  /* POSIX.1 version */
pid_t getpgrp(pid_t pid);             /* BSD version *///(函式過載)

int setpgrp(void); //等價於setpgid(0, 0)=setpgid(getpgrp(), getpgrp()) /* System V version */
int setpgrp(pid_t pid, pid_t pgid);         /* BSD version */

一個程序只能修改它自己或它的子程序所屬的程序組,並且子程序在呼叫 exec 之後就不能再修改子程序所屬的程序組了

會話

  • 需要注意的問題:

    • 每個程序組必定屬於某個會話,並且只能在一個會話中
    • 一個會話包含一個或多個程序組,最多隻能有一個前臺程序組(前臺作業)(可以沒有),其它的都是後臺程序組(後臺作業)
    • 每個會話都有一個會話首領(首領程序),即建立會話的程序
    • 同樣每個會話也有 ID 標識,稱為會話 ID(簡稱:SID),每個會話的 SID 就是會話首領程序的程序 ID(PID)。所以如果兩個程序的 SID 相同,表示它們倆在同一個會話中。在應用程式中呼叫 getsid 函式獲取程序的 SID
    • 會話的生命週期從會話建立開始,直到會話中所有程序組生命週期結束,與會話首領程序是否終止無關
    • 一個會話可以有控制終端、也可沒有控制終端,每個會話最多只能連線一個控制終端。控制終端與會話中的所有程序相關聯、繫結,控制、影響著會話中所有程序的一些行為特性,譬如控制終端產生的訊號,將會傳送給該會話中的程序(譬如 CTRL+C、CTRL+Z、CTRL+\ 產生的中斷訊號、停止訊號、退出訊號,將傳送給前臺程序組);譬如前臺程序可以透過終端與使用者進行互動、從終端讀取使用者輸入的資料,程序產生的列印資訊會透過終端顯示出來;譬如當控制終端關閉的時候,會話中的所有程序也被終止
    • 當我們在 Ubuntu 系統中開啟一個終端,那麼就建立了一個新的會話(shell 程序就是這個會話的首領程序,也就意味著該會話的 SID 等於 shell 程序的 PID),開啟了多少個終端,其實就是建立了多少個會話
    • 預設情況下, 新建立的程序會繼承父程序的會話 ID,子程序與父程序在同一個會話中 (也可以說子程序繼承了父程序的控制終端)
  • 關於前臺與後臺的一些操作:

    • 執行程式時,後面新增 & 使其在後臺執行
    • fg 命令可以將後臺程序調至前臺繼續執行
    • Ctrl+Z 可以將前臺程序調至後臺,並處於停止狀態(暫停狀態)

注意前臺程序組中的所有程序都是前臺程序,所以終端產生的訊號( CTRL+C、CTRL+Z、CTRL+\ )它們都會接收到

  • 獲取/建立會話
#include <sys/types.h>
#include <unistd.h>

pid_t getsid(pid_t pid);  // 如果引數pid為0,則返回撥用者程序的會話ID;如果引數pid不為0,則返回引數pid指定的程序對應的會話ID;如果失敗的話返回-1,並設定errno
pid_t setsid(void);  // 如果呼叫者程序不是程序組的組長程序(如果是組長則不能使用setsid),則建立一個**新會話**,呼叫者程序是新會話的首領程序,也會建立一個**新的程序組**(因為一個會話至少要存在一個程序組),呼叫者程序也是新程序組的組長程序,但是該會話**沒有控制終端、脫離控制終端** (ps 命令可以檢視程序的控制終端TTY)
// setsid的返回值:如果成功,則返回新會話的SID,如果失敗返回-1,並設定errno

守護程序

什麼是守護程序

守護程序(Daemon)也稱為精靈程序,是執行在後臺的一種特殊程序,它獨立於控制終端並且週期性地執行某種任務或等待處理某些事情的發生,主要表現為以下兩個特點:

Linux 系統中有很多系統服務,大多數服務都是透過守護程序來實現的,譬如系統日誌服務程序 syslogd、web 伺服器 httpd、郵件伺服器 sendmail 和資料庫伺服器 mysqld 等。守護程序(Daemon)的名字通常以字母 d 結尾

編寫守護程序

守護程序的重點在於脫離控制終端,但是除了這個關鍵點之外,還需要注意其它的一些問題,編寫守護程序一般包括如下幾個步驟:

父程序訊號處理機制對子程序的影響

父程序繫結的訊號處理函式對子程序的影響

fork 後子程序會繼承父程序繫結的訊號處理函式,如果呼叫 exec 載入新程式後,就不會再繼承這個訊號處理函式了

父程序的訊號掩碼對子程序的影響

fork 後子程序會繼承父程序的訊號掩碼,執行 exec 後仍會繼承這個訊號掩碼

相關文章