進程控制的主要任務就是系統使用一些具備特定功能的程序端來建立、撤銷進程以及完成進程各狀態之間的轉換,從而達到多進程、高效率、併發的執行和協調,實現資源共享的目的。html
每一個進程都有惟一的、用非負整型表示的進程ID,這個ID就是進程標識符。起做用就如同身份證同樣,因其惟一性,系統能夠準確的定位到每個進程。進程標識符的類型是pid_t,本質是一個無符號整數。linux
雖然是惟一的,可是進程ID是可複用的,當一個進程終止後,其ID就稱爲複用的候選者,大多數UNIX/Linux系統實現了延時複用算法,使得賦予新建進程的ID不一樣於最近終止進程所使用的ID。這防止將新進程誤認爲是使用同一個ID的某個已終止的進程。算法
一個進程標識符對應惟一進程,多個進程標識符能夠對應同一個程序。所謂程序指的是可運行的二進制代碼的文件,把這種文件加載到內存中運行就獲得了一個進程。同一個程序文件加載屢次就會獲得不一樣的進程,所以進程標識符與進程之間是一一對應的,和程序是多對一的關係。shell
在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; }
運行結果如圖
進程是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()函數以後,子進程對父進程的影響是巨大的,其同步措施勢在必行。
子進程徹底複製了父進程地址空間的內容。但它並無複製代碼段,而是和父進程共用代碼端。這樣作是由於雖然因爲子進程可能執行不一樣的流程,會改變數據段,可是代碼是隻讀的,不存在被修改的問題,所以可共用。
從前面的示例中能夠看出子進程對於數據段和堆棧端變量的修該並不能影響到父進程的進程環境。父進程的資源大部分能被fork()所複製,只有一小部分資源不一樣於子進程。子進程繼承的資源狀況如表所示
如今的Linux內核實現fork()函數時每每實現了在建立子進程時並不當即複製父進程的數據段和堆棧段,而是當子進程修改這些數據內容時複製纔會發生,內核纔會給子進程分配進程空間,將父進程的內容複製過來,而後繼續後面的操做。這樣的實現更加合理,對於一些只是爲了複製自身完成一些工做的進程來講,這樣作的效率會更高。這也是現代操做系統中一個重要的概念——「寫時複製」的一個重要體現。
當一個進程正常或異常終止時,內核會向其父進程發送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就當即返回。由於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參數:
對於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);
使用函數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做爲參數時:
若是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沒改變。除此以外,執行新程序的進程還保持了原進程的下列特徵:
對打開文件的處理與每一個描述符的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); }
程序運行結果以下:
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); }
程序運行結果:
任一進程均可調用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函數返回的每秒時鐘滴答數)轉換成秒數。