誰來調用 main 函數 在運行 main 函數之前,會有一段引導代碼,最終由這段代碼調用 main 函數,這段引導代碼不需要自己編寫,而是在編譯、鏈接中由鏈接器將這段程式鏈接到應用程式中,構成最終的可執行文件,載入器會將可執行文件載入到記憶體中 進程的終止 正常終止 在 main 函數中通過 ret ...
誰來調用 main 函數
在運行 main 函數之前,會有一段引導代碼,最終由這段代碼調用 main 函數,這段引導代碼不需要自己編寫,而是在編譯、鏈接中由鏈接器將這段程式鏈接到應用程式中,構成最終的可執行文件,載入器會將可執行文件載入到記憶體中
進程的終止
正常終止
- 在 main 函數中通過 return 返回,終止進程
- 調用庫函數 exit 終止進程
- 調用系統調用_exit/_Exit
異常終止
- 調用 abort 函數終止進程
- 被信號終止
終止進程:exit()和_exit()
exit()和_exit()用法
void _exit(int status)
:終止進程的運行,參數 status 表示進程終止時的狀態,通常 0 表示正常終止,非零值表示發證了錯誤,如 open 打開文件失敗(不是 abort 所表示的異常)void exit(int status)
:參數 status 含義同上
exit()和_exit()區別
exit()
是庫函數,_exit()
是一個系統調用,他們所需要包含的頭文件不同- 這兩個函數的終止目的相同,都是終止進程,但在終止過程前需要做的處理不一樣
exit()在終止進程的時候會調用終止處理函數:int atexit(void (*function)(void));,可以調用多個 atexit,調用順序和註冊順序相反
標準輸出預設是行緩存,檢測到"\n"後才會把該行輸出,_exit()不會刷新 IO 緩存,因此沒有"\n"的情況時該行不會輸出
- 不會刷新 stdio 緩衝的情況
- _exit()/_Exit()
- 被信號終止
exit()和 return 的區別
- exit()為庫函數,return 為 C 語言的語句
- exit()函數最終會進入到內核,把控制權交給內核,最終由內核去終止進程;return 並不會進入內核,只是一個函數的返回,返回到它的上層調用,最終由上層調用終止進程
- return 和 exit 同樣會調用終止處理函數、刷新 IO 緩存
exit()和 abort 區別
- exit 函數用於正常終止進程(執行一些清理工作),abort 用於異常終止進程(不會執行清理工作,會直接終止進程),abort 本質上是直接執行 SIGABRT 信號的系統預設處理操作
進程的環境變數
環境變數的概念
- 環境變數是指在進程運行環境中定義一些變數,類似於進程的全局變數,可以在程式的任何地方獲取,只需聲明即可。但與全局變數不同的是,這些環境變數可以被其他子進程所繼承,也就是具有繼承性
- 環境變數的本質還是變數,不過這些變數沒有類型,都是以字元串的形式存儲在一個字元串數組當中,稱為環境表(以 NULL 結尾),數組中的每個環境變數都是以
name = value
這種形式定義的,name 表示變數名稱,value 表示變數值
環境變數相關命令
env
:使用命令查看環境變數echo $name
:查看環境變數export name=value
:自定義/修改環境變數(註意等號前後不要有空格)unset name
:刪除環境變數
常見的環境變數
PATH
:用於指定可執行程式的搜索路徑HOME
:當前用戶的家目錄LOGNAME
:指定當前登錄的用戶HOSTNAME
:指定主機名SHELL
:指定當前 shell 解析器PWD
:指定進程的當前工作目錄
環境變數的組織形式
在應用程式中獲取環境變數
在每個應用程式中都有一組環境變數,是在進程創建中從父進程中繼承過來的
- environ 變數獲取:全局變數,可以直接在程式中使用,只需要申明就好,environ 實際上是一個指針,指向環境表
extern char **environ; // 申明一下,即可使用environ[i]
- 通過 main 函數獲取(儘量不要使用這種方式,有的系統可能不支持)
int main(int argc, char *argv[], char *env[]); // 第三個參數為進程的環境表
- 通過 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 提供了相應的修改函數,如果需要修改環境變數的值應該使用這些函數,不應直接改動該字元串。
添加/修改/刪除環境變數
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 在本進程範圍內修改了環境變數,進程結束後,原來的環境變數不變
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()並不能進行控制
name=value ./test
:在進程執行時添加環境變數(可同時添加多個環境變數,用空格隔開)unsetenv
:從環境表中移除參數 name 標識的環境變數
#include <stdlib.h>
int unsetenv(const char *name);
清空環境變數
- 將 environ 設置為 NULL
- 通過 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 函數為系統調用,可以等待進程的任一子進程終止,同時獲取子進程的終止信息(監視子進程第一種狀態的改變),作用:
- 監視子進程什麼時候被終止,以及獲取子進程終止時的狀態信息
- 回收子進程的一些資源(俗稱“收屍”)
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus); // wstatus用於存放子進程終止時的狀態信息,可以設置為NULL,表示不接收子進程終止時的狀態信息
// 返回值:若成功返回終止的子進程對應的進程號,失敗則返回-1
- 進程調用 wait()函數的情況:
- 如果該進程沒有子進程(即沒有需要等待的子進程),那麼 wait()將返回-1,並且將 errno 設置為 ECHILD
- 如果該進程所有子進程都還在運行,則 wait()會一直阻塞等待,直到某個子進程終止
- 如果調用 wait()之前該進程已經有一個或多個子進程終止了,那麼調用 wait()不會阻塞,會回收子進程的一些資源,註意一次 wait 調用只能為一個已經終止的子進程“收屍”
- status 為
NULL
或者(int *)0
時,返回該退出的子進程的 PID 號 - 如果父進程關註子進程的退出時狀態,可以使用如下方式,
status
將保存子進程結束時的狀態信息(子進程退出時exit
里的參數會被保存到status
中)
- 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 函數至少要創建兩個子進程:
- system 函數創建 shell 子進程
- 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 不同點
- 對於 fork 函數,fork 會為子進程創建一個新的地址空間(也就是進程空間),子進程幾乎完全拷貝了父進程,包括數據段、代碼段、堆、棧等;而對於 vfork 函數,子進程在終止或者成功調用 exec 函數之前,子進程與父進程共用地址空間,共用所有記憶體,包括數據段、堆棧等,所以在子進程在終止或成功調用 exec 函數前,不要去修改除 vfork 的返回值的 pid_t 類型的變數之外的任何變數(父進程的變數)、也不要調用任何其它函數(除 exit 和 exec 函數之外的任何其它函數),否則將會影響到父進程(vfork 函數的正確使用方法就是創建子進程後立馬調用 exec 載入新程式,所以沒有意義去調用其他函數或者修改變數)
註意:vfork 創建的子進程如果要終止應調用 exit,而不能調用 exit 或 return 返回,因為如果子進程調用 exit 或 return 終止,則會調用父進程綁定的終止處理函數以及刷新父進程的 stdio 緩衝,影響到父進程
- 對於 fork 函數,fork 調用之後,父、子進程的執行次序不確定;而對於 vfork 函數,vfork 函數會保證子進程先運行,父進程此時處於阻塞、掛起狀態,在子進程終止或成功調用 exec 函數之後,父進程才會被調度運行
註意:如果子進程在終止或成功調用 exec 函數之前,依賴於父進程的進一步動作,將會導致死鎖!
- 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 後仍會繼承這個信號掩碼