一、 進程創建: Unix 下的進程創建很特別,與許多其他操作系統不同,它分兩步操作來創建和執行進程: fork() 和 exec() 。首先,fork() 通過拷貝當前進程創建一個子進程;然後,exec() 函數負責讀取可執行文件並將其載入地址空間開始運行。 1、fork() :kernel/fo ...
一、 進程創建:
Unix 下的進程創建很特別,與許多其他操作系統不同,它分兩步操作來創建和執行進程: fork() 和 exec() 。首先,fork() 通過拷貝當前進程創建一個子進程;然後,exec() 函數負責讀取可執行文件並將其載入地址空間開始運行。
1、fork() :kernel/fork.c
在Linux系統中,通過調用fork()來創建一個進程。調用 fork() 的進程稱為父進程,新產生的進程稱為子進程。在該調用結束時,在返回點這個相同的位子上,父進程恢復執行,子進程開始執行。fork()系統調用從內核返回兩次:一次返回到父進程,另一次返回到新產生的子進程。使用fork()創建新進程的流程如下:
1)fork() 調用clone;
2)clone() 調用 do_fork();
3)do_fork() 調用 copy_process() 函數,copy_process() 函數將完成第 4-11 步;
4)調用 dup_task_struct() 為新進程創建一個內核棧、thread_info結構和task_struct,這些值與當前進程的值相同;
5)檢查並確保新創建這個子進程後,當前用戶所擁有的進程數目沒有超出給它分配的資源的限制;
6)清理子進程進程描述符中的一些成員(清零或初始化,如PID),以使得子進程與父進程區別開來;
7)將子進程的狀態設置為 TASK_UNINTERRUPTIBLE,保證它不會投入運行;
8)調用 copy_flags() 以更新 task_struct 的 flags 成員;
9)調用 alloc_pid() 為新進程分配一個有效的 PID;
10)根據傳遞給clone() 的參數標誌,copy_process() 拷貝或共用打開的文件、文件系統信息、信號處理函數、進程地址空間和命名空間等;
11)做一些掃尾工作並返回一個指向子進程的指針。
12)回到 do_fork() 函數,如果 copy_process() 函數成功返回,新創建的子進程將被喚醒並讓其投入運行。
下麵用一段簡單的代碼演示一下 fork() 函數:
1 #include <unistd.h> 2 #include <stdio.h> 3 4 int main(){ 5 pid_t fpid; 6 int count= 0; 7 fpid = fork(); // fpid 為fork()的返回值 8 if(fpid < 0){ // 當fork()的返回值為負值時,表明調用 fork() 出錯 9 printf("error in fork!"); 10 } 11 else if(fpid == 0){ // fork() 返回值為0,表明該進程是子進程 12 printf("this is a child process, the process id is %d\n",getpid()); 13 count++; 14 } 15 else{ // fork() 返回值大於0,表明該進程是父進程,這時返回值其實是子進程的PID 16 printf("this is a father process, the process id is %d\n",getpid()); 17 count++; 18 } 19 printf("計數 %d 次\n",count); 20 return 0; 21 }
輸出結果:
可以看到,調用 fork() 函數後,原本只有一個進程,變成了兩個進程。這兩個進程除了 fpid 的值不同外幾乎完全相同,它們都繼續執行接下來的程式。由於 fpid 的值不同,因此會進入不同的判斷語句,這也是為什麼兩個結果有不同之處的原因。另外,可以看到,父進程的 PID 剛好比子進程的 PID 小1。 fork() 的返回值有以下三種:
a)在父進程中,fork() 返回新創建子進程的 PID;
b)在子進程中,fork() 返回0;
c)如果 fork() 調用出錯,則返回負值
2、exec() :fs/exec.c (源程式 exec.c 實現對二進位可執行文件和 shell 腳本文件的載入與執行)
通常,創建新的進程都是為了立即執行新的、不同的程式,而接著調用 exec() 這組函數就可以創建新的地址空間,並把新的程式載入其中。
exec() 並不是一個函數,而是一個函數簇,一共包含六個函數,分別為: execl、execlp、execle、execv、execvp、execve,定義如下:
#include <unistd.h> int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);
這六個函數的功能其實差不多,只是接受的參數不同。exec() 函數的參數主要有3個部分:執行文件部分、命令參數部分和環境變數部分:
1)執行文件部分:也就是函數中的 path 部分,該部分指出了可執行文件的查找方式。其中 execl、execle、execv、execve的查找方式都是使用的絕對路徑,而 execlp和execvp則可以只給出文件名進行查找,系統會從環境變數 "$PATH"中查找相應的路徑;
2)命令參數部分:也就是函數中的 file 部分,該部分指出了參數的傳遞方式以及要傳遞哪些參數。這裡,"l"結尾的函數表示使用逐個列舉的方式傳遞參數;"v"結尾的表示將所有參數整體構造成一個指針數組進行傳遞,然後將該數組的首地址當做參數傳遞給它,數組中的最後一個指針要求為 NULL;
3)環境變數部分:exec() 函數簇使用了系統預設的環境變數,也可以傳入指定的環境變數。其中 execle 和execve 這兩個函數就可以在 envp[] 中指定當前進程所使用的環境變數。
· 當 exec() 執行成功時,exec() 函數會取代執行它的進程,此時,exec() 函數沒有返回值,進程結束。當 exec() 函數執行失敗時,將返回失敗信息(返回 -1),進程繼續執行後面的代碼。
通常,exec() 會放在 fork() 函數的子進程部分,來替代子進程繼續執行,exec() 執行成功後子進程就會消失,但是執行失敗的話,就必須要使用 exit() 函數來讓子進程退出。下麵用一段簡單的代碼來演示一下 exec() 函數簇中的一個函數的用法,其餘的參考:https://www.cnblogs.com/dongguolei/p/8098181.html
1 #include <unistd.h> 2 #include <stdio.h> 3 #include <errno.h> 4 #include <string.h> 5 6 int main(){ 7 int childpid; 8 pid_t fpid; 9 fpid = fork(); 10 if(fpid == 0){ // 子進程 11 char *execv_str[] = {"ps","aux",NULL}; // 指令:ps aux 查看系統中所有進程 12 if( execv("/usr/bin/ps",execv_str) < 0 ){ 13 perror("error on exec\n"); 14 exit(0); 15 } 16 } 17 else{ 18 wait(&childpid); 19 printf("execv done\n"); 20 } 21 }
在這個程式中,使用 fork() 創建了一個子進程,隨後立即調用 exec() 函數簇中的 execv() 函數,execv() 函數執行了一條指令,顯示當前系統中所有的進程,結果如下(進程有很多,這裡只截了一部分):
註意看最後兩個進程,分別是父進程和調用 fork() 後創建的子進程。
二、進程終結
進程被創建後,最終要終結。當一個進程終結時,內核必須釋放它所占有的資源,並把這一消息告訴其父進程。系統通過 exit() 系統調用來處理終止和退出進程的相關工作,而大部分工作則由 do_exit() 來完成 (kernel/exit.c):
1)將task_struct 中的標誌成員設置為 PF_EXITING;
2)調用 del_timer_sync() 刪除任一內核定時器,以確保沒有定時器在排隊,也沒有定時器處理程式在運行;
3)調用 exit_mm() 函數釋放進程占用的 mm_struct,如果沒有別的進程使用它們(地址空間被共用),就徹底釋放它們;
4)調用 sem__exit() 函數,如果進程排隊等候 IPC 信號,它則離開隊列;
5)調用 exit_files() 和 exit_fs(),以分別遞減文件描述符、文件系統數據的引用計數,若其中某個引用計數的值降至零,則表示沒有進程使用相應的資源,可以釋放掉進程占用的文件描述符、文件系統資源;
6)把 task_struct 的 exit_code 成員設置為進程的返回值;
7)調用 exit_notify() 向父進程發送信號,並把進程狀態設置為 EXIT_ZOMBIE;
8)調用 schedule() 切換到新的進程,繼續執行。由於 EXIT_ZOMBIE 狀態的進程不會被再調度,所以這是進程所執行的最後一段代碼, do_exit() 沒有返回值。
至此,與進程相關聯的所有資源都被釋放掉了,進程不可運行並處於 EXIT_ZOMBIE 退出狀態。此時,進程本身所占用的記憶體還沒有釋放,如內核棧、thread_info 結構和 task_struct 結構等,它存在的意義是向父進程提供信息,當父進程收到信息後,或者通知內核那是無關的信息後,進程所持有的剩餘的記憶體將被釋放。父進程可以通過 wait4() 系統調用查詢子進程是否終結,這其實使得進程擁有了等待特定進程執行完畢的能力。
系統通過調用 release_task() 來釋放進程描述符。