8.1 引言算法
本章介紹UNIX的進程控制,包括建立新進程、執行程序和進程終止。還將說明進程屬性的各類ID-----實際、有效和保存的用戶和組ID,以及他們如何受到進程控制原語的影響。本章還包括瞭解釋器文件和system函數。本章最後講述大多數UNIX系統所提供的進程會計機制。這種機制使咱們可以從另外一個角度瞭解進程的控制功能。shell
8.2 進程標識符數組
每一個進程都有一個非負整型表示的唯一進程ID。由於進程標識符是唯一的,常將其用做其餘標識符的一部分以保證其唯一性。雖然是唯一的,可是進程ID能夠重用。(大多數UNIX系統實現延遲重用算法,使得賦予新建進程的ID不一樣於最近終止進程所使用的ID。這防止了將新進程誤認爲是使用同一ID的某個已終止的進程。編輯器
ID爲0一般是系統進程函數
ID爲1一般是init進程優化
除了進程ID,每一個進程還有其餘一些標識符。下列函數返回這些標識符ui
#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 getid(void) //返回值:調用進程的實際組id gid_t getegid(void) //返回值:調用進程的有效組id
這些函數都沒有出錯返回spa
8.3 fork函數操作系統
一個現有進程能夠調用fork函數建立一個新進程。命令行
#include<unistd.h> pid_t fork(void); //返回值:子進程返回0,父進程中返回子進程ID,出錯返回-1
將子進程ID返回給父進程的理由是:由於一個進程的子進程能夠有多個,而且沒有一個函數使一個進程能夠得到其全部子進程的進程ID
使子進程獲得返回值0的理由是:一個進程只會有一個父進程,因此子進程老是能夠調用getppid以得到其父進程的進程ID(進程ID0老是由內核交換進程使用,因此一個子進程的進程ID不多是0)
子進程是父進程的副本,但父、子進程並不共享這些存儲空間部分。父子進程共享正文段
因爲在fork以後常常跟隨者exec,因此如今的不少實現並不執行一個父進程數據段,棧和堆的徹底複製。做爲替代,使用了寫時複製技術。
實例:8_1 fork函數示例
1 #include"apue.h" 2 3 int glob=6; //external variable in initialized data 4 char buf[]="a write to stdout\n"; 5 6 int main() 7 { 8 int var; //automatic variable on the stack 9 pid_t pid; 10 var=88; 11 if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1) 12 err_sys("write error"); 13 printf("before fork\n");//we don't flush stdout 14 if((pid=fork())<0){ 15 err_sys("fork error"); 16 }else if(pid==0){ //child 17 glob++; 18 var++; 19 }else {sleep(2); 20 } 21 printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var); 22 exit(0); 23 } 24
通常來講,在fork以後是父進程仍是子進程先執行是不肯定的。這取決於內核的調度算法。8_1中是先讓父進程休眠2秒鐘,以使子進程先執行
當寫到標準輸出時,咱們將buf長度減去1做爲輸出字節數,這是爲了不將終止null字節寫出。strlen計算不包含終止null字節的字符串長度,而sizeof則計算包括終止null字節的緩衝區長度。二者之間的另外一個差異是,使用strlen需進行一次函數調用,而對於sizeof而言,由於緩衝區已用已知字符串進行了初始化,其長度是固定的,因此sizeof在編譯時計算緩衝區長度
在8_1中當將標準輸出重定向到一個文件時,卻獲得printf輸出行兩次。其緣由是,在fork以前調用了printf一次,但當調用fork時,卻獲得printf輸出行兩次。其緣由是,在fork以前調用了printf一次,但當調用fork時,該行數據仍在緩衝區中,而後在將父進程數據空間複製到子進程中時,該緩衝區也被複制到子進程中,因而那時父、子進程各自有了帶該行內容的標準I/O緩衝區。在exit以前的第二個printf將其數據添加到現有的緩衝區中。當每一個進程終止時,最終會沖洗其緩衝區的副本
父子進程的區別是:
-fork的返回值
-進程ID不一樣
-兩個進程具備不一樣的父進程ID:子進程的父進程ID是建立它的進程ID,而父進程ID則不變
-子進程的tms_utime,tms_stime,tme_cutime以及tme_ustime均被設置爲0
-父進程設置的文件鎖不會被子進程繼承
-子進程的未處理的鬧鐘被清除
-子進程的未處理信號集設置爲空集
使fork失敗的兩個主要緣由是:系統中已經有了太多的進程,或者實際用戶ID進程總數超過了系統限制
fork有下列兩種用法:
(1)一個進程但願複製本身,是父子進程同時執行不一樣代碼段
(2)一個進程要執行一個不一樣的程序。
8.4 vfork函數
vfork函數的調用序列和返回值與fork相同,但二者的語義不一樣。
vfork用於建立一個新進程,而該新進程的目的是exec一個新程序。vfork和fork同樣都建立一個子進程,可是它並不將父進程的地址空間徹底複製到子進程中,由於子進程會當即調用exec(或exit),因而也就不會存訪該地址空間。相反,在子進程調用exec或exit以前,它在父進程的空間中運行。這種優化工做方式在某些UNIX的頁式虛擬存儲器視線中提升了效率
vfork和fork之間的另外一個區別是:vfork保證子程序先運行,在它調用exec或exit之間後父進程纔可能被調度運行(若是在調用這兩個函數以前子程序依賴於父進程的進一步動做,則會致使死鎖)
實例:8_2 vfork函數實例
1 #include"apue.h" 2 int glob=6; 3 int main() 4 { 5 int var; 6 pid_t pid; 7 var=88; 8 9 printf("before vfork\n"); 10 if((pid=vfork())<0){ 11 err_sys("vfork error"); 12 }else if(pid==0){ 13 glob++; 14 var++; 15 _exit(0); 16 } 17 printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var); 18 exit(0); 19 }
vfork,它產生的子進程剛開始暫時與父進程共享地址空間(其實就是線程的概念了),由於這時候子進程在父進程的地址空間中運行,因此子進程不能進行寫操做,而且在兒子「霸佔」着老子的房子時候,要
委屈老子一下了,讓他在外面歇着(阻塞),一旦兒子執行了exec或者exit後,至關於兒子買了本身的房子了,這時候就至關於分家了。
8.5 exit函數
若是父進程在子進程以前終止,則對於父進程已經終止的全部進程,他們的父進程都改變爲init進程。咱們稱這些進程由init進程領養。其操做過程大體以下:在一個進程終止時,內核逐個檢查全部進程,以判斷它是不是正要終止進程的子程序,若是是,則將該進程的父進程ID更改成1(init進程ID),這種處理方法保證了每一個進程都有一個父進程。
另外一個咱們關心的狀況是若是子進程在父進程以前終止,那麼父進程又如何能在作相應檢查時獲得子程序的終止狀態呢?
內核爲每一個終止子進程保存了必定量的信息,因此當終止進程的父進程調用wait或waitpid,能夠獲得這些信息,這些信息至少包括進程ID,該進程的終止狀態,以及該進程使用的CPU時間總量。內核能夠釋放終止進程所使用的全部存儲區,關閉其全部打開文件。
8.6 wait和waitpid函數
#include<sys/wait.h> pid_t wait(int *statloc); pid_t waitpid(pid_t pid,int *statloc,int options); //兩個函數返回值:若成功則返回進程ID,0,若出錯則返回-1
這兩個函數區別以下:
-在一個子進程終止前,wait使其調用者阻塞,而waitpid有一個選項,可以使調用者不阻塞。
-waitpid並不等待在其調用以後的第一個終止子程序,它有若干個選項,能夠控制它所等待的進程
實例:8_3 打印exit狀態的說明
1 #include"apue.h" 2 #include<sys/wait.h> 3 void pr_exit(int status) 4 { 5 if(WIFEXITED(status)) 6 printf("normal termination,exit status= %d\n",WEXITSTATUS(status)); 7 else if(WIFSIGNALED(status)) 8 printf("abnormal termination,signal number= %d%s\n",WTERMSIG(status), 9 #ifdef WCOREDUMP 10 WCOREDUMP(status) ? "(core file generated)" : " "); 11 #else 12 ""); 13 #endif 14 else if(WIFSTOPPED(status)) 15 printf("child stopped,signal number= %d\n",WSTOPSIG(status)); 16 }
實例:8_4 演示不一樣的exit值
1 #include"apue.h" 2 #include<sys/wait.h> 3 void pr_exit(int ); 4 int main() 5 { 6 pid_t pid; 7 int status; 8 if((pid=fork())<0) 9 err_sys("fork error"); 10 else if(pid==0) 11 exit(7); 12 if(wait(&status)!=pid) 13 err_sys("wait error"); 14 pr_exit(status); 15 if((pid=fork())<0) 16 err_sys("fork error"); 17 else if(pid==0) 18 abort(); 19 if(wait(&status)!=pid) 20 err_sys("wait error"); 21 pr_exit(status); 22 if((pid=fork())<0) 23 err_sys("fork error"); 24 else if(pid==0) 25 // status/=0; 26 if(wait(&status)!=pid) 27 err_sys("wait error"); 28 pr_exit(status); 29 exit(0); 30 } 31 void pr_exit(int i) 32 { 33 printf("%d\n",i); 34 return; 35 }
waitpid函數提供了wait函數沒有提供的三個功能:
(1)waitpid可等待一個特定的進程,而wait則返回任一終止子進程的狀態。
(2)waitpid提供了一個wait的非阻塞版本。有時用戶但願取得一個子進程的狀態,但不想阻塞
(3)waitpid支持做業控制
8.7 waitid函數
#include<sys/wait.h> int waitid(idtype_t idtype,id_t id,siginfo_t *infop,int options); //返回值:若成功則返回0,若出錯則返回-1
與waitpid類似,waitid容許一個進程指定要等待的子進程。但它使用單獨的參數表示要等待的字進程的類型,而不是將此進程ID或進程組ID組合稱一個參數
8.8wait3 和wait4函數
#include<sys/types.h> #include<sys/wait.h> #include<sys/time.h> #include<sys/resource.h> pid_t wait3(int *statloc,int options,struct rusage *rusage); pid_t wait4(pid_t pid,int *statloc,int options,struct rusage *rusage); //返回值:若成功則返回進程ID,若出錯則返回-1
8.9 競爭條件
這部分操做系統原理已經講的很深了
程序清單 8_6 具備競爭條件的程序
1 #include"apue.h" 2 static void charatatime(char *); 3 4 int main() 5 { 6 pid_t pid; 7 if((pid=fork())<0){ 8 err_sys("fork error"); 9 }else if(pid==0){ 10 charatatime("output from child\n"); 11 }else { 12 charatatime("output from parent\n"); 13 } 14 exit(0); 15 } 16 static void charatatime(char *str) 17 { 18 char *ptr; 19 int c; 20 setbuf(stdout,NULL); 21 for(ptr=str;(c=*ptr++)!=0; ) 22 putc(c,stdout); 23 }
在程序中將標準輸出設置爲不帶緩衝的,因而每一個字符輸出都需調用一次write.本例的目的是使內核儘量在兩個進程之間進行屢次切換,以便演示競爭條件。
8.10 exec函數
調用exec函數時,該進程執行的程序徹底替換爲新程序,而新程序則從其main函數開始執行。由於調用exec並不建立新進程,因此先後的進程ID並未改變。exec只是用一個全新的程序替換了當前進程的正文,數據,堆和棧段
#include<unistd.h> int execl(const char *pathname,const char *arg(),.../*(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[]); //返回值:若出錯則返回-1,若成功則不返回值
這些函數之間的第一個區別是前4個去路徑名做爲參數,後兩個取文件名做爲參數。當指定filename做爲參數時:
-若是filename中包含/,則將其視爲路徑名
-不然就按PATH環境變量,在它所指定的各目錄中搜尋可執行文件。
若是execlp或execvp使用路徑前綴中的一個找到了一個可執行文件,可是該文件不是由鏈接編輯器產生的機器可執行文件,則認爲該文件是一個shell腳本,因而試着調用/bin/sh,並以該filename做爲shell的輸入
第二個區別與參數表的傳遞有關(1表示list,v表示適量vector),函數execl、execlp和execle要求將新進程的每一個命令行參數都說明爲一個單獨的參數。這種參數表以空指針結尾。對於另外三個函數(execv、execvp和execve),則應先構造一個指向各參數的指針數組,而後將該數組地址做爲這三個函數的參數
最後一個區別與向新進程傳遞環境表相關。以e結尾的兩個函數(execle和execve)能夠傳遞一個指向環境字符串指針數組的指針。其餘四個函數則使用調用進程中的environ變量爲新程序複製現有的環境。
注意:在執行exec先後實際用戶ID和實際組ID保持不變,而有效ID是否改變則取決於所執行程序文件的設置用戶ID位和設置組ID位是否設置。若是新程序的設置用戶ID位已設置,則有效用戶ID變成程序文件全部者的ID,不然有效用戶ID不變。對組ID的處理方式與此相同
實例:8_8 exec函數實例
1 #include"apue.h" 2 #include<sys/wait.h> 3 char *env_init[]={ "USER=unknow","PATH=/tmp",NULL}; 4 int main() 5 { 6 pid_t pid; 7 if((pid=fork())<0){ 8 err_sys("fork error"); 9 }else if(pid==0){//specify pathname,specify environment 10 if(execle("/home/sar/bin/echoall","echoall","myarg1","MY ARG2", 11 (char *)0,env_init)<0) 12 err_sys("execle error"); 13 } 14 if(waitpid(pid,NULL,0)<0) 15 err_sys("wait error"); 16 if((pid=fork())<0){ 17 err_sys("fork error"); 18 }else if(pid==0){//specify filename,inherit environment 19 if(execlp("echoall","echoall","only 1 arg",(char *)0)<0) 20 err_sys("execlp error"); 21 } 22 exit(0); 23 }
8.11 更改用戶ID和組ID
能夠用setuid函數設置實際用戶ID和有效用戶ID。setgid函數設置實際組ID和有效組ID
#include<unistd.h> int getuid(uid_t uid); int setgid(gid_t gid); //兩個函數返回值:若成功則返回0,若出錯則返回-1
規則:
(1):若進程具備超級用戶權限,則setuid函數將實際用戶ID、有效用戶ID、以及保存的設置用戶ID設置爲uid
(2):若進程沒有超級用戶特權,可是uid等於實際用戶ID或保存的設置用戶ID,則setuid只將有效用戶ID設置爲uid。不改變實際用戶ID和保存的設置用戶ID
(3):若是上面兩個條件都不知足,則將errno設置爲EPERM,並返回-1
1.setreuid和setregid函數
交換實際用戶ID和有效用戶ID的值
#include<unistd.h> int setreuid(uid_t ruid,uid_t euid); int setregid(gid_t rgid,gid_t egid); //兩個函數返回值:若成功則返回0,若出錯則返回-1
2.seteuid和setegid函數
只更改有效用戶ID
#include<unistd.h> int seteuid(uid_t uid); int setegid(gid_t gid); //返回值:T:0,F:-1
8.12 解釋器文件
解釋器文件是文本文件,其起始開頭形式是:
#! pathname [optional-argument] 例如:#!/bin/sh
內核使調用exec函數的進程實際執行的不是解釋器文件,而是該解釋器文件第一行中pathname所指定的文件,必定要將解釋器文件和解釋器區分開來
8.13 system函數
#include<stdlib.h> int system(const char *cmdstring);
若是cmdstring是一個空指針時,system返回非零值,這特徵能夠肯定在一個給定的操做系統上是否支持system函數
在UNIX中,system老是可用的
由於system在其實現中調用了fork、exec和waitpid,所以有三種返回值
(1)若是fork失敗或者waitpid返回除EINTR以外的出錯,則system返回-1,並且errno中設置了錯誤類型值
(2)若是exec失敗,則其返回值如同shell執行了exit(127)同樣
(3)不然全部三個函數都執行成功,而且system的返回值是shell的終止狀態,其格式已在waitpid說明。
使用system而不是直接使用fork和exec的優勢是:system進行了所需的各類出錯處理,以及各類信號處理
設置用戶ID或設置組ID程序決不該調用system函數,由於system中執行了fork和exec以後超級用戶權限仍會保持下來,若是一個進程正以特殊的權限運行,它又想生成另外一個進程執行另外一個程序,則它應當直接使用fork和exec,並且在fork以後,exec以前要改回到普通權限
8.14 進程會計
大多數UNIX系統提供了一個選項以進行進程會計處理。啓用該選項後,每當進程結束時內核就寫一個會計記錄。通常包括命令名,所使用的CPU時間總量,用戶ID和組ID,啓動時間等
超級用戶執行一個帶路徑名參數的accton命令啓動會計處理。會計記錄寫到指定的文件中(會計記錄結構定義在頭文件<sys/acct.h>中)
會計記錄所需的各類數據都由內核保存在進程表中,並在一個新進程被建立時置初值。每次進程終止時都會編寫一條會計記錄。這就意味着在會計文件中記錄的順序對應於終止的順序,而不是他們啓動的順序
會計記錄對應與進程而不是程序,在fork以後,內核爲子程序初始化一個目錄,而不是在一個新程序被執行時作這個工做。
8.15 用戶標識
系統一般記錄用戶登陸時所使用的名字,用getlogin函數能夠獲取此登錄名
#include<unistd.h> char *getlogin(void); //返回值:若成功則返回指向登錄名字符串的指針,若出錯則返回NULL
若是調用此函數的進程沒有鏈接到用戶登陸時所用的終端,則本函數會失敗
8.16 進程時間
任意進程均可調用times函數以得到它本身及已終止子程序的:牆上時鐘時間,用戶cpu時間,系統cpu時間
#include<sys/times.h> clock_t times(struct tms *buf); //返回值:若成功則返回流逝的牆上始終時間,若出錯則返回-1