進程控制的主要任務就是系統使用一些具有特定功能的程序端來創建、撤銷進程以及完成進程各狀態之間的轉換,從而達到多進程、高效率、並發的執行和協調,實現資源共享的目的。
1、進程標識
每個進程都有唯一的、用非負整型表示的進程ID,這個ID就是進程標識符。起作用就如同身份證一樣,因其唯一性,系統可以准確的定位到每一個進程。進程標識符的類型是pid_t,本質是一個無符號整數。
雖然是唯一的,但是進程ID是可復用的,當一個進程終止后,其ID就稱為復用的候選者,大多數UNIX/Linux系統實現了延時復用算法,使得賦予新建進程的ID不同於最近終止進程所使用的ID。這防止將新進程誤認為是使用同一個ID的某個已終止的進程。
一個進程標識符對應唯一進程,多個進程標識符可以對應同一個程序。所謂程序指的是可運行的二進制代碼的文件,把這種文件加載到內存中運行就得到了一個進程。同一個程序文件加載多次就會得到不同的進程,因此進程標識符與進程之間是一一對應的,和程序是多對一的關系。
在Linux shell中,可以使用ps命令查看當前用戶所使用的進程。

第一列內容是進程標識符(PID),這個標識符是唯一的;最后一列內容是進程的程序文件名。我們可以從中間找到有多個進程對應同一個程序文件名的情況,這是因為有一些常用的程序被多次運行了,比如shell和vi編輯器等。
每個進程都有6個重要的ID值,分別是:進程ID、父進程ID、有效用戶ID、有效組ID、實際用戶ID和實際組ID。這6個ID保存在內核中的數據結構中,有些時候用戶程序需要得到這些ID。
例如,在/proc文件系統中,每一個進程都擁有一個子目錄,里面存有進程的信息。當使用進程讀取這些文件時,應該先得到當前進程的ID才能確定進入哪一個進程的相關子目錄。由於這些ID存儲在內核之中,因此,Linux提供一組專門的接口函數來訪問這些ID值。
Linux環境下分別使用getpid()和getppid()函數來得到進程ID和父進程ID,分別使用getuid()和geteuid()函數來得到進程的用戶ID和有效用戶ID,分別使用getgid()和getegid()來獲得進程的組ID和有效組ID,其函數原型如下:
#include <unistd.h> pid_t getpid(void); //獲取進程ID pid_t getppid(void); //獲取父進程ID uid_t getuid(void); //獲取用戶ID uid_t geteuid(void); //獲取有效用戶ID gid_t getgid(void); //獲取組ID gid_t getegid(void); //獲取有效組ID
函數執行成功,返回當前進程的相關ID,執行失敗,則返回-1。
示例:
獲取當前進程的ID信息:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> int main(int argc,char *argv[]) { pid_t pid=0,ppid=0; uid_t uid=0,euid=0; gid_t gid=0,egid=0; pid=getgid(); ppid=getppid(); uid=getuid(); euid=geteuid(); gid=getgid(); egid=getegid(); printf("當前進程ID:%u\n",pid); printf("父進程ID:%u\n",ppid); printf("用戶ID:%u\n",uid); printf("有效用戶ID:%u\n",euid); printf("組ID:%u\n",gid); printf("有效組ID:%u\n",egid); return 0; }
運行結果如圖

2、進程創建
進程是Linux系統中最基本的執行單位。Linux系統允許任何一個用戶創建一個子進程。創建之后,子進程存在於系統之中,並且獨立於父進程。該子進程可以接受系統調度,可以分配到系統資源。系統能檢測到它的存在,並且會賦予它與父進程同樣的權利。
Linux系統中,使用函數fork()可以創建一個子進程,其函數原型如下:
#include <stdio.h> pid_t fork(void);
除了0號進程以外,任何一個進程都是由其他進程創建的。創建新進程的進程,即調用函數fork()的進程就是父進程。
函數fork()不需要參數,返回值是一個進程的ID。返回值情況有以下三種:
(1)對於父進程,函數fork()返回新創建的子進程的ID。
(2)對於子進程,函數fork()返回0.由於系統的0號進程是內核進程,所以子進程的進程號不可能是0,由此可以區分父進程和子進程。
(3)如果出錯,返回-1。
fork的一個特性是父進程的所有打開文件描述符都被復制到子進程中去。在fork之后處理的文件描述符有兩種常見的情況:
1. 父進程等待子進程完成。在這種情況下,父進程無需對其描述符做任何處理。當子進程終止后,子進程對文件偏移量的修改和已執行的更新。
2. 父子進程各自執行不同的程序段。這種情況下,在fork之后,父子進程各自關閉他們不需要使用的文件描述符,這樣就不會干擾對方使用文件描述符。這種方法在網絡服務進程中經常使用。
下面通過一個示例對此函數進行了解
#include <stdio.h>
#include <stdlib.h> #include <unistd.h> int global; int main(int argc,char *argv[]) { pid_t pid; int stack=1; int *heap=NULL; heap=(int*)malloc(sizeof(int)); *heap=2; pid=fork(); if(pid<0) { perror("fork()"); exit(1); } else if(pid==0)//0是第一個父進程 { global++; stack++; (*heap)++; printf("the child,data:%d,stack:%d,heap:%d\n",global,stack,*heap); exit(0); }else { sleep(2); printf("the child,data:%d,stack:%d,heap:%d\n",global,stack,*heap); exit(0); } return 0; }
程序運行結果如下:

函數fork()會創建一個新的進程,並從內核中為此進程得到一個新的可用的進程ID,之后為這個新進程分配進程空間,並將父進程的進程空間中的內容復制到子進程的進程空間中,包括父進程的數據段和堆棧段,並且和父進程共享代碼段。這時候,系統中又多出一個進程,這個進程和父進程一樣,兩個進程都要接受系統的調用。
下列兩種情況可能會導致fork()的出錯:
(1)系統中已經存在了太多的進程。
(2)調用函數fork()的用戶進程太多。
一般系統中對每個用戶所創建的進程數是有限的,如果數量不加限制,那么用戶可以利用這一缺陷惡意攻擊系統。
創建共享空間的子進程
進程在創建一個新的子進程之后,子進程的地址空間完全和父進程分開。父子進程是兩個獨立的進程,接受系統調度和分配系統資源的機會均等,因此父進程和子進程更像是一對兄弟。如果父子進程共用父進程的地址空間,則子進程就不是獨立於父進程的。
Linux環境下提供了一個與fork()函數類似的函數,也可以用來創建一個子進程,只不過新進程與父進程共用父進程的地址空間,其函數原型如下:
#include <unistd.h> pid_t vfork(void);
現在通過一個示例對vfork()函數進行理解
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int globvar = 6; int main(void) { int var; pid_t pid; var = 88; printf("before vfork\n"); if((pid = vfork()) < 0 ) { perror("vfork()"); } else if(pid == 0) { globvar ++; var ++; _exit(0); } printf("pid = %ld, glob = %d, var = %d\n",(long)getpid(), globvar, var); exit(0); }
程序運行結果:

(1) vfork()函數產生的子進程和父進程完全共享地址空間,包括代碼段、數據段和堆棧段,子進程對這些共享資源所做的修改,可以影響到父進程。由此可知,vfork()函數與其說是產生了一個進程,還不如說是產生了一個線程。
(2) vfork()函數產生的子進程一定比父進程先運行,也就是說父進程調用了vfork()函數后會等待子進程運行后再運行。
下面的示例程序用來驗證以上兩點。在子進程中,我們先讓其休眠2秒以釋放CPU控制權,在前面的fork()示例代碼中我們已經知道這樣會導致其他線程先運行,也就是說如果休眠后父進程先運行的話,則第(2)點則為假;否則為真。第(2)點為真,則會先執行子進程,那么全局變量便會被修改,如果第(1)點為真,那么后執行的父進程也會輸出與子進程相同的內容。代碼如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int global = 1; int main(void) { pid_t pid; int stack = 1; int *heap; heap = (int *)malloc(sizeof(int)); *heap = 1; pid = vfork(); if (pid < 0) { perror("fail to vfork"); exit(-1); } else if (pid == 0) { //sub-process, change values sleep(2);//release cpu controlling global = 999; stack = 888; *heap = 777; //print all values printf("In sub-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap); exit(0); } else { //parent-process printf("In parent-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap); } return 0; }
程序運行結果:

在使用vfork()函數時應該注意不要在任何函數中調用vfork()函數。下面的示例是在一個非main函數中調用了vfork()函數。該程序定義了一個函數f1(),該函數內部調用了vfork()函數。之后,又定義了一個函數f2(),這個函數沒有實際的意義,只是用來覆蓋函數f1()調用時的棧幀。main函數中先調用f1()函數,接着調用f2()函數。
//@file vfork.c //@brief vfork() usage #include <stdio.h> #include <stdlib.h> #include <unistd.h> int f1(void) { vfork(); return 0; } int f2(int a, int b) { return a+b; } int main(void) { int c; f1(); c = f2(1,2); printf("%d\n",c); return 0; }
程序運行結果:

通過上面的程序運行結果可以看出,一個進程運行正常,打印出了預期結果,而另一個進程似乎出了問題,發生了段錯誤。出現這種情況的原因可以用下圖來分析一下:
左邊這張圖說明調用vfork()之后產生了一個子進程,並且和父進程共享堆棧段,兩個進程都要從f1()函數返回。由於子進程先於父進程運行,所以子進程先從f1()函數中返回,並且調用f2()函數,其棧幀覆蓋了原來f1()函數的棧幀。當子進程運行結束,父進程開始運行時,就出現了右圖的情景,父進程需要從f1()函數返回,但是f1()函數的棧幀已經被f2()函數的所替代,因此就會出現父進程返回出錯,發生段錯誤的情況。
由此可知,使用vfork()函數之后,子進程對父進程的影響是巨大的,其同步措施勢在必行。
3、父子進程
子進程完全復制了父進程地址空間的內容。但它並沒有復制代碼段,而是和父進程共用代碼端。這樣做是因為雖然由於子進程可能執行不同的流程,會改變數據段,但是代碼是只讀的,不存在被修改的問題,因此可共用。
從前面的示例中可以看出子進程對於數據段和堆棧端變量的修該並不能影響到父進程的進程環境。父進程的資源大部分能被fork()所復制,只有一小部分資源不同於子進程。子進程繼承的資源情況如表所示

現在的Linux內核實現fork()函數時往往實現了在創建子進程時並不立即復制父進程的數據段和堆棧段,而是當子進程修改這些數據內容時復制才會發生,內核才會給子進程分配進程空間,將父進程的內容復制過來,然后繼續后面的操作。這樣的實現更加合理,對於一些只是為了復制自身完成一些工作的進程來說,這樣做的效率會更高。這也是現代操作系統中一個重要的概念——“寫時復制”的一個重要體現。
4、進程資源回收
當一個進程正常或異常終止時,內核會向其父進程發送SIGCHLD信號。因為子進程終止是個異步事件(這可以在父進程運行的任意時刻發生),所以這種信號也是內核向父進程發送的異步通知。父進程可以選擇-忽略該信號,或者提供一個該信號發生時被調用執行的信號處理函數,對於這種信號,系統默認的是忽略它。
linux系統提供了函數wait()和waitpid()來回收子進程資源,其函數原型如下:
#include <sys/wait.h> #include <sys/types.h> pid_t wait(int *statloc); pid_t waitpidd(pid_t pid, int *statloc, int options);
這兩個函數區別:
- wait如果在子進程終止前調用則會阻塞,而waitpid有一選項可以使調用者不阻塞。
- waitpid並不等待第一個終止的子進程--它有多個選項,可以控制它所等待的進程。
如果調用者阻塞而且它有多個子進程,則在其一個子進程終止時,wait就立即返回。因為wait返回子進程ID,所以調用者知道是哪個子進程終止了。
參數statloc是一個整型指針。如果statloc不是一個空指針,則終止狀態就存放到它所指向的單元內。如果不關心終止狀態則將statloc設為空指針。
這兩個函數返回的整型狀態由實現定義。其中某些位表示退出狀態(正常退出),其他位則指示信號編號(異常返回),有一位指示是否產生了一個core文件等等。POSIX.1規定終止狀態用定義在<sys/wait.h>中的各個宏來查看。有三個互斥的宏可用來取得進程終止的原因,它們的名字都已WIF開始。基於這三個宏中哪一個值是真,就可選用其他宏(這三個宏之外的其他宏)來取得終止狀態、信號編號等。

下面的程序中pr_exit函數使用上表中的宏以打印進程的終止狀態。
#include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> void pr_exit(int status) { if (WIFEXITED(status)) { printf("normal termination, exit status=%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("abnormal termination, signal number = %d\n", WTERMSIG(status), #ifdef WCOREDUMP WCOREDUMP(status) ? "(core file generated)" : ""); #else ""); #endif } else if (WIFSTOPPED(status)) { printf("child stopped, signal number = %d\n", WSTOPSIG(status)); } } int main(void) { pid_t pid; int status; if ((pid = fork()) < 0) { fprintf(stderr, "fork error"); } else if (pid == 0) { exit(7); } if (wait(&status) != pid) { fprintf(stderr, "wait error"); } pr_exit(status); if ((pid = fork()) < 0) { fprintf(stderr, "fork error"); } else if (pid == 0) { abort(); } if (wait(&status) != pid) { fprintf(stderr, "wait error"); } pr_exit(status); if ((pid = fork()) < 0) { fprintf(stderr, "fork error"); } else if (pid == 0) { status = 8; } if (wait(&status) != pid) { fprintf(stderr, "wait error"); } pr_exit(status); return 0; }
編譯運行結果:

wait是只要有一個子進程終止就返回,waitpid可以指定子進程等待。對於waitpid的pid參數:
- pid == -1, 等待任一子進程。這時waitpid與wait等效。
- pid > 0, 等待子進程ID為pid。
- pid == 0, 等待其組ID等於調用進程的組ID的任一子進程。
- pid < -1 等待其組ID等於pid的絕對值的任一子進程。
對於wait,其唯一的出錯是沒有子進程(函數調用被一個信號中斷,也可能返回另一種出錯)。對於waitpid, 如果指定的進程或進程組不存在,或者調用進程沒有子進程都能出錯。 options參數使我們能進一步控制waitpid的操作。此參數或者是0,或者是下表中常數的逐位或運算。

競態條件
當多個進程都企圖對某共享數據進行某種處理,而最后的結果又取決於進程運行的順序,則我們認為這發生了競態條件(race condition)。如果在fork之后的某種邏輯顯式或隱式地依賴於在fork之后是父進程先運行還是子進程先運行,那么fork函數就會是競態條件活躍的孽生地。
如果一個進程希望等待一個子進程終止,則它必須調用wait函數。如果一個進程要等待其父進程終止,則可使用下列形式的循環:
while(getppid() != 1) sleep(1);
這種形式的循環(稱為定期詢問(polling))的問題是它浪費了CPU時間,因為調用者每隔1秒都被喚醒,然后進行條件測試。
為了避免競態條件和定期詢問,在多個進程之間需要有某種形式的信號機制。在UNIX中可以使用信號機制,各種形式的進程間通信(IPC)也可使用。
在父、子進程的關系中,常常有以下情況:在fork之后,父、子進程都有一些事情要做。例如:父進程可能以子進程ID更新日志文件中的一個記錄,而子進程則可能要為父進程創建一個文件。在本例中,要求每個進程在執行完它的一套初始化操作后要通知對方,並且在繼續運行之前,要等待另一方完成其初始化操作。這種情況可以描述為如下:
TELL_WAIT(); if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { TELL_PARENT(getppid()); WAIT_PARENT(); exit(0); } TELL_CHILD(pid); WAIT_CHILD(); exit(0);
5、進程體替換
使用函數fork()創建新的進程后,子進程往往需要調用函數exec以執行另一個程序。當進程調用函數exec()時,該進程執行的程序完全替換為新程序,而新程序則從其函數main()開始執行。因為調用exec並不能創建新進程,所以前后的進程ID並未改變,函數exec指示用磁盤上的一個程序替換了當前進程的正文段、數據段、堆段和棧段。
通常有6種exec()函數可供使用,它們統稱為exec()函數族,我們可以使用其中任意一個。exec()函數族使Linux系統對進程的控制更加完善。使用fork()創建新進程,使用函數exec()執行新程序,使用函數exit()和wait()終止進程和等待進程終止。exec()函數原型如下:
#include <unistd.h> extern char **environ; int execl(const char *pathname, const char *arg0, ... /* (char *) 0 */); int execv(const char *pathname, char *const argv[]); int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */); int execve(const char *pathname, char *const argv[], char *const envp[]); int execlp(const char *filename, const char *arg0, ... /* (char *) 0 */); int execvp(const char *filename, char *const argv[]);
這些函數之間的第一個區別是前四個取路徑名作為參數,后兩個取文件名作為參數。當制定filename作為參數時:
- 如果filename中包含/,則就將其視為路徑名。
- 否則按PATH環境變量。
如果excelp和execvp中的任意一個使用路徑前綴中的一個找到了一個可執行文件,但是該文件不是機器可執行代碼文件,則就認為該文件是一個shell腳本,於是試着調用/bin/sh,並以該filename作為shell的輸入。
第二個區別與參數表的傳遞有關(l 表示表(list),v 表示矢量(vector))。函數execl、execlp和execle要求將新程序的每個命令行參數都說明為一個單獨的參數。這種參數表以空指針結尾。另外三個函數execv,execvp,execve則應先構造一個指向個參數的指針數組,然后將該數組地址作為這三個函數的參數。
最后一個區別與向新程序傳遞環境表相關。以 e 結尾的兩個函數excele和exceve可以傳遞一個指向環境字符串指針數組的指針。其他四個函數則使用調用進程中的environ變量為新程序復制現存的環境。
六個函數之間的區別:

每個系統對參數表和環境表的總長度都有一個限制。當使用shell的文件名擴充功能產生一個文件名表時,可能會收到此值的限制。
歸結起來,6個exec()函數之間的關系如下:

執行exec后進程ID沒改變。除此之外,執行新程序的進程還保持了原進程的下列特征:
- 進程ID和父進程ID。
- 實際用戶ID和實際組ID。
- 添加組ID。
- 進程組ID。
- 對話期ID。
- 控制終端。
- 鬧鍾尚余留的時間。
- 當前工作目錄。
- 根目錄。
- 文件方式創建屏蔽字。
- 文件鎖。
- 進程信號屏蔽。
- 未決信號。
- 資源限制。
- tms_utime,tms_stime,tms_cutime以及tms_ustime值。
對打開文件的處理與每個描述符的exec關閉標志值有關。進程中每個打開描述符都有一個exec關閉標志。若此標志設置,則在執行exec時關閉該文件描述符,否則該描述符仍打開。除非特地用fcntl設置了該標志,否則系統的默認操作是在exec后仍保持這種描述符打開。
POSIX.1明確要求在exec時關閉打開目錄流。這通常是由opendir函數實現的,它調用fcntl函數為對應於打開目錄流的描述符設置exec關閉標志。
在exec前后實際用戶ID和實際組ID保持不變,而有效ID是否改變則取決於所執行程序的文件的設置-用戶-ID位和設置-組-ID位是否設置。如果新程序的設置-用戶-ID位已設置,則有效用戶ID變成程序文件的所有者的ID,否則有效用戶ID不變。對組ID的處理方式與此相同。
示例:使用execl()進行進程體替換
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc,char *argv[]) { int count =0; pid_t pd =0; if(argc<2) { printf("Usage Error!\n"); exit(1); } for(count=1;count<argc;count++)//指令輸入多少個文件,創建多少個進程 { pd=fork(); if(pd<0) { perror("fork()"); exit(1); }else if(pd==0) { printf("Child Start PID=%d\t****\n",getpid());//創建進程成功輸出當前進程PID execl("/bin/ls","ls",argv[count],NULL); //調用execl函數切換新進程,第一參數path字符指針所指向要執行的文件路徑, 接下來的參數代表執行該文件時傳遞的參數列表:argv[0],argv[1]... 最后一個參數須用空指針NULL作結束。 perror("execl"); exit(1); } else { wait();//等待當前進程終止 printf("Child End PID=%d\t****\n\n",getpid()); } } exit(0); }
程序運行結果如下:

6、調用命令行
C程序調用shell腳本共同擁有三種法子 :system()、popen()、exec系列數call_exec1.c 。其中system() 不用你自己去產生進程。它已經封裝了,直接增加自己的命令,使用起來最為方便,這里重點講解Linux下使用函數system()調用Shell命令,其函數原型如下:
#include <stdlib.h> int system(const char *command);
參數command是需要執行的Shell命令。函數system的返回值比較復炸,其為一個庫函數,封裝了fork()、exec()、和waitpid(),其函數原型如下:
int system(const char * cmdstring) { pid_t pid; int status; if(cmdstring == NULL){ return (1); } if((pid = fork())<0){ status = -1; } else if(pid == 0){ execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); -exit(127); //子進程正常執行則不會執行此語句 } else{ while(waitpid(pid, &status, 0) < 0){ if(errno != EINTER){ status = -1; break; } } } return status; }
其返回值需要根據着三個函數加以區分:
如果fork()或waitpid()執行失敗,函數system()返回-1.
如果函數exec()執行失敗,函數system的返回值於shell調用的exit的返回值一樣,表示指定文件不可執行。
如果三個文件都執行成功,函數system()返回執行程序的終止狀態,其值和命令“echo $”的值是一樣的。
如果參數command所指向的字符串為NULL,函數system返回1,這可以用來測試當前系統是否支持函數system。對於Linux來說,其全部支持函數system。
函數system()的執行效率比較低:在函數system中要兩次調用函數fork()和exec(),第一次加載Shell程序,第二次加載需要執行的程序(這個程序由Shell負責加載)。但是對比直接使用fork()+exec()的方法,函數system()雖然效率較低,卻有以下優點:
(1)添加了出錯處理函數
(2)添加了信號處理函數
(3)調用了wait()函數,保證不會出現僵屍進程。
示例:
使用system函數調用系統命令行
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main(int argc,char **argv[]) { char *command=NULL; int flag=0; command=(char*)malloc(1024*sizeof(char)); memset(command,0,1024*sizeof(char)); while(1) { printf("my-cmd@"); if(fgets(command,100,stdin)!=NULL) { if(strcmp(command,"exit\n")==0) { puts("quit successful"); break; } flag=system(command); if(flag==-1) { perror("fork()"); exit(1); } memset(command,0,100); } } free(command); command=NULL; exit(0); }
程序運行結果:

7、進程時間
任一進程都可調用times函數以獲得它自己及終止子進程的時鍾時間、用戶CPU時間和系統CPU時間。
#include <sys/times.h> clock_t times(struct tms *buf);
返回: 若成功則為經過的時鍾時間,若出錯則為-1
此函數填寫由buf指向的tms結構,該結構定義如下:
struct tms { clock_t tms_utime; /* 用戶CPU時間 */ clock_t tms_stime; /* 系統CPU時間 */ clock_t tms_cutime; /* 終止子進程用戶CPU時間 */ clock_t tms_cstime; /* 終止子進程系統CPU時間 */ }
此結構沒有時鍾時間。作為代替,times函數返回時鍾時間作為函數值。此至是相對於過去的某一時刻度量的,所以不能用其絕對值而應該使用其相對值。例: 調用times,保存其返回值,在以后的某個時間再次調用times,從新返回的值中減去以前返回的值,此差值就是時鍾時間。
所有由次函數返回的clock_t值都用_SC_CLK_TCK(由sysconf函數返回的每秒時鍾滴答數)轉換成秒數。


