8.1 引言算法
本章介紹UNIX的進程控制,包括建立新進程、執行程序和進程終止。還將說明進程屬性的各類ID——實際、有效和保護的用戶和組ID,以及它們如何受到進程控制原語的影響。本章還包括瞭解釋器文件和system函數。本章最後講述大多數UNIX系統所提供的進程會計機制。這種機制使咱們能從另外一個角度瞭解進程的控制功能。shell
8.2 進程標誌符數組
每一個進程都有要給非負整數表示惟一進程ID,雖然進程ID是惟一的,可是能夠重用。當一個進程終止後,其進程ID就能夠再次使用了。大多數UNIX系統實現延遲重用算法,使得賦予新建進程的ID不一樣於最近終止進程所使用的ID。這防止了將新進程誤認爲是使用同一ID的某個已終止的先前進程。安全
系統中有一些專用進程,但具體細節因實現而異。ID爲0的進程一般是調度進程,經常被稱爲交換進程。該進程是內核的一部分,它並不執行任何磁盤上的程序,所以也被稱爲系統進程。進程ID 1 一般是init 進程,在自舉過程結束時由內核調用。該進程的程序文件在UNIX的早期版本中是/etc/init,在較新版本中是 /sbin/init。此進程負責在自舉內核後啓動一個UNIX系統。init 一般讀與系統有關的初始化文件,並將系統引導到一個狀態(例如多用戶)。init進程決不會終止。它是一個普通的用戶進程(與交換進程不一樣,它不是內核中的系統進程),可是它以超級用戶特權運行。本章稍後部分會說明 init 如何稱爲全部孤兒進程的父進程。網絡
每一個UNIX 系統實現都有它本身的一套提供操做系統服務的內核進程,例如,在某些UNIX的虛擬存儲器實現中,進程ID 2 是頁守護進程。此進程負責支持虛擬存儲系統的分頁操做。編輯器
除了進程 ID,每一個進程還有一些其餘的標識符。下列函數返回這些標識符。函數
pid_t getpid(void); pid_t getppid(void);
uid_t getuid(void); // 返回進程的實際用戶ID uid_t geteuid(void); // 返回進程的有效用戶ID gid_t getgid(void); gid_t getegid(void);
注意,這些函數都沒有出錯返回,在下一節中討論 fork 函數時,將進一步討論父進程 ID。測試
8.3 fork函數優化
一個現有進程能夠調用 fork 函數建立一個新進程ui
pid_t fork(void);
由 fork 建立的新進程被稱爲子進程。fork函數被調用一次,但返回兩次。子進程的返回值是0,父進程的返回值是新子進程的進程 ID。
子進程和父進程繼續執行 fork 調用以後的指令,子進程是父進程的副本。例如,子進程得到父進程的數據空間、堆和棧的副本。注意,這是子進程所擁有的副本。父、子進程並不共享這些存儲空間部分。父子進程共享正文段(見7.6節)
因爲在 fork 以後常常跟隨着 exec,因此如今的不少實現並不執行一個父進程數據段、棧和堆徹底複製。做爲替代,使用寫時複製技術。這些區域由父、子進程共享,並且內核將它們的訪問權限改變爲只讀的。若是父、子進程中的任一個試圖修改這些區域,則內核只爲修改區域的那塊內存製做一個副本,一般是虛擬存儲器系統中的一「頁」。
通常來講,在 fork 以後父子進程執行的現後順序是不肯定的。若是要求父子進程之間相互同步,則要求某種形式的進程間通訊。
文件共享
fork的一個特性是父進程的全部打開文件描述符都被複制到子進程中。父、子進程的每一個相同的打開描述符共享一個文件表項(如圖3-3)。
考慮下述狀況,一個進程具備三個不一樣的打開文件,他們是標準輸入、標準輸出和標準出錯。在從fork返回時,咱們有了下圖。
這種共享方式使父、子進程對同一個文件使用了一個文件偏移量。這樣就能夠實現父子進程交互寫同一個文件。
若是父、子進程寫到同一描述符文件,但又沒有任何形式的同步,那麼它們的輸出就會相互 混合(假定所用的描述符是在fork以前開打的)。雖然這種狀況是可能發生的,但這並非經常使用的操做模式。
在fork以後處理文件描述符有兩種常見的狀況:
(1)父進程等待子進程完成。在這種狀況下,父進程無需對其描述符作任何處理。當子進程終止後,它曾進行讀、寫操做的任一共享描述符的文件偏移量已執行了相應更新。
(2)父、子進程各自執行不一樣的程序段。在這種狀況下,在fork以後,父、子進程各自關閉它們不須要使用的文件描述符,這樣就不會干擾對方使用的文件描述符。這種是網絡服務進程中常用的。
除了打開文件以外,父進程的不少其餘屬性也由子進程繼承,包括
(1)實際用戶ID、實際組ID、有效用戶ID、有效組ID
(2)附加組ID
(3)進程組ID
(4)會話ID
(5)控制終端
(6)設置用戶ID標誌和設置組ID標誌
(7)當前工做目錄
(8)根目錄
(9)文件模式建立屏蔽字
(10)信號屏蔽和安排
(11)針對任一打開文件描述符的在執行時關閉標誌。
(12)環境
(13)鏈接的共享存儲段
(14)存儲映射
(15)資源限制
父子進程之間的區別是:
(1)fork的返回值
(2)進程ID不一樣
(3)兩個進程具備不一樣的父進程ID
(4)進程的 tms_utime、tms_stime、tms_cutime以及tms_ustime均被設置爲0。
(5)父進程設置的文件鎖不會被子進程繼承。
(6)子進程的未處理鬧鐘(alarm)被清除
(7)子進程的未處理信號集設置爲空集。
使fork失敗的主要緣由是進程太多了。
fork有兩種用法:
(1)一個父進程但願複製本身,使父、子進程同時執行不一樣的代碼段。這在網絡服務進程中是常見的——父進程等待客戶端的服務請求。當這種狀況到達時,父進程調用 fork,使子進程處理此請求。父進程則繼續等待下一個服務請求到達。
(2)一個進程要執行一個不一樣的程序。這對shell是常見的狀況。在這種狀況下,子進程從fork返回後當即調用 exec。
某些操做系統將(2)中的兩個操做(fork、exec)組合成一個,並稱其爲spawn。UNIX將這兩個操做分開,使得子進程在 fork 和 exec 之間能夠更改本身的屬性。例如 I/O 重定向、用戶ID、信號安排等。在15章中有不少這方面的例子。
8.4 vfork函數
vfork函數的調用序列和返回值與fork相同,但二者的語義不一樣。
vfork被認爲有瑕疵,應被棄用。
vfork用於建立一個新進程,而該新進程的目的是exec一個新程序。因爲vfork不會將父進程的地址空間徹底複製到子進程中,由於子進程會當即調用exec或exit,因而就不會訪問該地址空間。相反,在子進程調用 exec 或 exit 以前,他在父進程的空間中運行。這種優化工做方式在某些UNIX的頁式虛擬存儲器實現中提升了效率。(這與上一節中說起的在fork以後跟隨exec,並採用在寫時複製技術類似,並且不復制比部分複製要更快一些。)
vfork和 fork之間的另外一個區別是:vfork保證子進程先運行,在它調用exec或exit以後父進程纔可能被調度運行。(若是在調用這兩個函數以前子進程依賴於父進程的進一步動做,則會致使死鎖)。
int glob = 6; int main(void) { int var; pid_t pid; var = 88; printf("before vfork\n"); if ((pid = vfork()) < 0) { err_sys("vfork error"); } else if (pid == 0) { glob++; var++; _exit(0); } printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var); exit(0); }
子進程對變量 glob 和 var 作增1操做,結果改變了父進程中的變量值。由於子進程在父進程的地址空間中運行,因此這並不使人驚訝。可是其做用的確與 fork 不一樣。
注意,在程序清單中,調用了 _exit 而不是 exit。正如7.3節所述, _exit並不執行標準 I/O 緩衝的沖洗操做。若是調用的是exit而不是 _exit,則程序的輸出是不肯定的。它依賴標準 I/O 庫的實現。
若是該實現也關閉標準 I/O 流,那麼表示那麼標準輸出 FILE 對象的相關存儲區將被清 0 。注意,父進程的 STDOUT_FILENO 仍舊有效,子進程獲得的是父進程的文件描述符數組的副本。但因爲沒有緩衝區,因此父進程調用 printf 時不會產生任何輸出。
8.5 exit函數
如7.3節所述,進程有下面5中正常終止方式:
(1)main函數內return,等效於調用 exit。
(2)調用 exit 函數。其操做包括調用各終止處理程序(終止處理程序在調用 atexit 函數時登記),而後關閉全部標準 I/O 流等。由於 ISO C 並不處理文件描述符、多進程(父、子進程)以及做業控制,因此這必定義對UNIX系統而言是不完整的。
(3)調用 _exit 或 _Exit 函數。其存在的目的是爲進程提供一種無需運行終止處理程序或信號處理程序而終止的方法。在 UNIX 中, _Exit 和 _exit 是同義的,並不清洗標準 I/O 流。_exit函數有 exit 調用。
(4)進程最後一個線程在啓動例程中執行返回語句。可是,該線程的返回值不會用做進程的返回值。當最後一個線程從其啓動例程返回時,該進程以終止狀態0返回。
(5)進程的最後一個線程調用 pthread_exit 函數。如同前面同樣,在這種狀況下,進程終止狀態老是0,這與傳送給 pthread_exit 的參數無關。
三種異常終止方式以下
(1)調用 abort。它產生 SIGABORT 信號,這是下一種異常終止的一種特例。
(2)當進程接受到某些信號時。信號可由進程自身(例如調用abort函數)、其餘進程或內核產生。例如,進程越出其餘地址空間訪問存儲單元或者除以0,內核就會爲該進程產生相應的信號。
(3)最後一個線程對「取消」請求作出響應。按系統默認,「取消」以延遲方式發生:一個線程要求取消另外一個線程,一段時間後,目標線程終止。
無論進程如何終止,最後都會執行內核中的同一段代碼。這段代碼爲相應進程關閉全部打開描述符,釋放它所使用的存儲器等。
對於上述任意一種情形,咱們都但願終止進程可以通知其父進程它是如何終止的。對於三個終止函數(exit、_exit和_Exit),實現這一點的方法是,將其退出狀態做爲參數傳給函數。在異常終止狀況下,內核(不是進程自己)產生一個指示其異常終止緣由的終止狀態。在任意一種狀況下,該終止進程的父進程都能用wait或waitpid函數取得其終止狀態。
注意,這裏使用「退出狀態」(它是傳向exit或 _exit 的參數,或 main 的返回值)和「終止狀態」兩個術語,以表示有所區別。在最後調用_exit時,內核將退出狀態換成終止狀態(回憶圖7-1)。下一節中的表8-1說明父進程檢查子進程終止狀態的不一樣方法。若是子進程正常終止,則父進程能夠得到 子進程的退出狀態。
當子進程退出後,將其終止狀態返回給父進程,可是若是父進程提早終止,那麼init成爲父進程。其操做過程大體以下:在一個進程終止時,內核逐個檢查全部活動進程,以判斷它是否正要終止進程的子進程,若是是,則將該進程的父進程ID更改成1(init進程的ID)。這種處理方法保證了每一個進程都有一個父進程。
另外一個咱們關係的狀況是若是子進程在父進程以前終止,那麼父進程又如何能在作相應及檢查時獲得子進程的終止狀態呢?對此問題的回答是:內核爲每一個終止子進程保持了必定量的信息,因此當終止進程的父進程調用 wait 或 waitpid 時,能夠獲得這些信息。這些信息至少包括進程 ID、該進程的終止狀態、以及該進程使用 CPU 時間總量。內核能夠釋放終止進程所使用的全部存儲區,關閉其全部打開文件。在UNIX術語中,一個已經終止,可是其父進程還沒有對其進行善後處理(獲取終止子進程的有關信息,釋放它仍佔用的資源)的進程被稱爲僵死進程(zombie)。ps(1)命令將僵死進程的狀態打印爲 Z。若是編寫一個長期運行的程序,它調用 fork 產生了不少子進程,那麼除非父進程等待取得子進程的終止狀態,不然這些子進程終止後就會變成僵死進程。
最後一個要考慮的問題是:一個由Init進程領養的進程終止時會發生什麼?它會不會變成一個僵死進程?答案是「否」,由於Init被編寫成不管什麼時候只要有一個子進程終止,init就會調用一個wait函數取得其終止狀態。
8.6 wait和waitpid函數
當一個進程正常或異常終止時,內核就向其父進程發送 SIGCHILD 信號。父進程能夠選擇忽略,或者捕捉,對於這種信號的系統默認動做是忽略它。
如今須要知道的是調用 wait 或 waitpid的進程可能發生什麼狀況:
(1)若是其全部子進程都還在運行,則阻塞。
(2)若是一個子進程已終止,正等待父進程獲取其終止狀態,則取得該子進程的終止狀態當即返回。
(3)若是他沒有任何子進程,則當即出錯返回
pid_t wait(int *statloc); pid_t waitpid(pid_t pid, int *statloc, int options); 兩個函數的返回值:若成功則返回進程ID,0(見後面說明),若出錯則返回-1
兩個函數的區別以下:
(1)waitpid能夠設置不阻塞。
(2)waitpid並不等的在其調用以後的第一個終止子進程,它有若干個選項,能夠控制它所等待的進程。
對於 statloc 用於得到返回的狀態,其中某些爲表示退出狀態(正常返回),其餘位則指示信號編號(異常返回),有一個位指示是否產生一個core文件等。使用下列的宏來查看
void pre_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%s\n", WTERMGIS(status)), #ifdef WCOREDUMP WCOREDUMP(status) ? " (core file generated)" : ""); #else ""); #endif else if (WIFSTOPPED(status)) printf("child stopped, signal number = %d\n", WSTOPSIG(status)); }
對於 waitpid 的函數中 pid 參數的做用解釋以下:
pid == -1 等待任一子進程。與 wait 等效。
pid > 0 等待其進程ID == pid 的子進程
pid == 0 等待其組ID等於調用進程組ID的任一子進程
pid < -1 等待其組ID等於pid絕對值的任一子進程。
對於 wait ,其惟一出錯是調用進程沒有子進程(函數調用被一個信號中斷時,也可能返回另外一種出錯。第10章討論)。可是對於 waitpid,若是指定的進程或進程組不存在,或者參數pid指定的進程不是調用進程的子進程則都將出錯。
options參數使咱們能進一步控制waitpid的操做。此參數能夠是0。或者按照下表常量位或運算的結果。
waitpid函數提供了wait函數沒有提供的三個功能:
(1)waitpid可等待一個特定的進程,而wait則返回任一終止子進程的狀態。
(2)waitpid提供了一個wait的非阻塞版本。
(3)waitpid支持做用控制(利用 WUNTRACED 和 WCONTINUED 選項)。
若是一個進程fork一個子進程,但不要它等待子進程終止,也不但願子進程處於僵死狀態直到父進程終止,實現這一要求的技巧是調用 fork 兩次。(本質上就是讓 init 進程管理孫子進程)
// 調用 fork 兩次以免僵死進程 int main(void) { pid_t pid; if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { if ((pid = fork()) < 0) err_sys("fork error"); else if (pid > 0) exit(0); sleep(2); printf("second child, parent pid = %d\n", getppid()); exit(0); } if (waitpid(pid, NULL, 0) != pid) err_sys("waitpid error"); exit(0); }
8.9 競態條件
當多個進程都企圖對共享數據進行某種處理,而最後的結果又取決於進程運行的順序,則咱們認爲這發生了競態條件。
在父、子進程的關係中,經常出現下述狀況,在調用 fork 以後,父、子進程都有一些事情要作。例如,父進程可能要用子進程ID更新日誌文件中的一個記錄,而子進程則可能要爲父進程建立一個文件,在本例中,要求每一個進程在執行完它的一套初始化操做後要通知對方,而且在繼續運行以前,要等待另外一方完成其初始化操做。這種方案能夠用代碼描述以下:
TELL_WAIT(); // set thing up for TELL_XXX & WAIT_XXX if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { // child // child does whatever is necessary .. TELL_PARENT(getppid()); //tell parent we're done WAIT_PARENT(); // and wait for parent // and the child continues on its way ... exit(0); } // parent does whatever is necessary ... TELL_CHILD(pid); // tell child we're done WAIT_CHILD(); // and wait for child // and the parent continues on its way... exit(0);
TELL_WAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT、WAIT_CHILD能夠是宏,也能夠是函數。
在後面幾章會說明實現這些TELL和WAIT例程的不一樣方法:10.16節中說明使用信號的一種實現,程序清單15-3說明使用管道的一種實現。下面先看一個使用這5各例程的實例。
程序清單8-6輸出兩個字符串:一個由子進程輸出,另外一個由父進程輸出。由於輸出依賴內核使用這兩個進程運行的順序及每一個進程運行的時間程度,因此該程序包含了一個競爭條件。
static void charatatime(char *); int main(void) { pid_t pid; if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { charatatime("output from child\n"); } else { charatatime("output from parent\n"); } exit(0); } static void charatatime(char *str) { char *ptr; int c; setbuf(stdout, NULL); // set unbuffered for (ptr = str; (c = *ptr++) != 0;) putc(c, stdout); }
在程序中將標準輸出設置爲不帶緩衝的,因而每一個字符輸出都須要調用一次write。本例的目的是使內核能儘量地在兩個進程間屢次切換,以便演示競態條件。結果以下 :
修改程序清單上面程序,使用 TELL 和WAIT 函數,因而有以下:
運行此程序則可以獲得預期的輸出。
8.10 exec函數
當進程調用 exec函數時,該進程的程序徹底替換爲新程序,而新程序則從其main函數開始執行。由於exec並不建立新進程,因此先後的進程ID並無改變。exec只是用一個全新的程序替換了當前進程的正文、數據、堆和棧段。
有6種不一樣的exec函數可以使用。
這些函數之間的第一個區別是前4個取路徑名做爲參數,後兩個則取文件名做爲參數。而當指定filename做爲參數時:
(1)若是filename中包含/,則將其視爲路徑名。
(2)不然就按PATH環境變量,在它所指定的各目錄中搜索可執行文件。
PATH變量包含了一張目錄表(稱爲路徑前綴),目錄之間用冒號(:)分隔。例如,name=value環境字符串
PATH=/bin:/usr/bin:/usr/local/bin:.
指定在4各目錄中進行搜索。最後的路徑前綴表示當前目錄。(零長前綴也表示當前目錄。在value的開始處可用 : 表示,在行中間則要用 :: 表示,在行尾則以 : 表示。)
若是execlp或execvp使用路徑前綴找到了一個可執行文件,可是該文件不是由鏈接編輯器產生的機器可執行文件,則認爲該文件是一個shell腳本,因而試着調用/bin/sh,並以該filename做爲shell的輸入。
第二個區別與參數表的傳遞有關(l表示list,v表示矢量vector)。函數execl、execlp和execle要求將新程序的每一個命令行參數都說明爲一個單獨的參數。這種參數表以空指針結尾。對於另外三個函數(execv、execvp和execve),則應先構造一個指向各參數的指針數組,而後將該數組地址做爲這三個函數的參數。
execl、execle和execlp三個函數表示命令行參數的通常方法是:
char *arg0, char *arg1,..., char*argn, (char *)0
應當特別指出的是:在最後一個命令行參數以後跟了一個空指針,若是用常數0來表示一個空指針,則必須將他強制轉換爲一個字符指針,不然將他解釋爲整形參數。若是一個整形數的長度與char *的長度不一樣,那麼exec函數的實際參數就將出錯。
最後一個區別與新程序傳遞環境表相關。以e結尾的兩個函數(execle和execve)能夠傳遞一個指向環境字符串指針數組的指針。其餘四個函數則使用調用進程中的environ變量爲新程序複製現有環境(回憶7.9節,其中曾說起若是系統支持setenv和putenv這樣的函數,則可更改當前環境和後面生成的子進程的環境,但不能影響父進程的環境)。一般,一個進程容許將其環境傳播給其子進程,但有時也有 這種狀況,即進程想要爲子進程指定某一個肯定的環境。例如,在初始化一個新登錄的shell時,login程序一般建立一個只定義少數幾個變量的特殊環境,而在咱們登錄時,能夠經過shell啓動文件,將其餘變量加到環境中。
execle的參數是:
char *pathname, char *arg0, ..., char *argn, (char *)0, char *envp[]
從中可見,最後一個參數是指向環境字符串的各字符指針構成的數組的地址。而函數原型中,全部命令行參數、空指針和envp指針都用省略號(...)表示。
這6個exec函數的參數很難記憶。函數名中的字符會給咱們一些幫助。字母p表示該函數取filename做爲參數,而且用PATH環境變量尋找可執行文件。字母 l 表示該函數取一個參數表,它與字母 v 互斥。 v 表示該函數取一個 argv[] 矢量。最後,字母 e 表示該函數取 envp[] 數組,而不使用當前環境。下表顯示了這 6 個函數之間的區別。
前面曾說起在執行 exec 後,進程 ID 沒有改變。除此以外,執行新程序的進程還保持了原進程的如下特徵:
(1)進程ID和父進程ID
(2)實際用戶ID和實際組ID
(3)附加組ID
(4)進程組ID
(5)會話ID
(6)控制終端
(7)鬧鐘尚餘留的時間
(8)當前工做目錄
(9)根目錄
(10)文件模式建立屏蔽字
(11)文件鎖
(12)進程信號屏蔽
(13)未處理信號
(14)資源限制
(15)tms_utime、tms_stime、tms_cutime以及tms_cstime值。
對打開文件的處理與每一個描述符的執行時關閉(close-on-exec)標誌有關。見圖3-1以及3.14節中對 FD_CLOEXEC 的說明,進程中每一個打開描述符都有一個執行時關閉標誌。若此標誌設置,則在執行exec時關閉該描述符,不然該描述符仍然打開。除非特意用 fcntl 設置了該標誌,不然系統的默認操做是在執行exec後仍保持這種描述符打開。
POSIX.1明確要求在執行exec時關閉打開的目錄流(見4.21節中所述的opendir函數),這一般是由opendir函數實現的,它調用fcntl函數爲對應於打開目錄流的描述符設置執行時關閉標誌。
注意,在執行exec先後實際用戶ID和實際 組ID保持不變,而有效ID是否改變則取決於所執行程序文件的設置用戶ID位和設置組ID位是否設置。若是新程序的設置用戶ID位已設置,則有效用戶ID變成程序文件全部者的ID,不然有效用戶ID不變,對組ID的處理方式與此相同。
6個函數之間的關係以下圖:
在這種安排中,庫函數execlp和execvp使用PATH環境變量,查找第一個包含名爲filename的可執行文件的路徑名前綴。
char *env_init[] = {"USR=unknow", "PATH=/tmp", NULL}; int main(void) { pid_t pid; if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { /* specify pathname, specify environment */ if (execle("/home/sar/bin/echoall", "myarg1", "MY ARG2", (char *)0, env_init) < 0) err_sys("execle error"); } if (waitpid(pid, NULL, 0) < 0) err_sys("wait error"); if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { /* specify filename, inherit environment */ if (execlp("echoall", "echoall", "only 1 arg", (char *)0) < 0) err_sys("execlp error"); } exit(0); }
// echoall int main(int argc, char *argv[]) { int i; char **ptr; extern char **environ; for (i = 0; i < argc; i++) printf("argv[%d]: %s\n", i, argv[i]); for (ptr = environ; *ptr != 0; ptr++) printf("%s\n", *ptr); exit(0); }
注意,shell提示符出如今第二個exec打印argv[0]以前。這是由於父進程並不等待該子進程結束。
8.11 更改用戶ID和組ID
進程的用戶ID和組ID絕對了其特權的大小。
通常而言,在設計應用程序時,咱們老是試圖使用最小特權模型。依照此模型,咱們的程序應當只具備爲完成給定任務所需的最小特權。這減小了安全性受到損害可能性,這種安全性損害是因爲惡意用戶試圖哄騙咱們程序以未預料的方式使用特權所形成的。
可使用 setuid 函數設置實際用戶ID和有效用戶ID。與此相似,能夠用setgid函數設置實際組ID和有效組ID
int setuid(uid_t uid); int setgid(gid_t gid);
8.12 解釋器文件
現在UNIX系統都支持解釋器文件,這種文件時文本文件,其起始行形式是:
#! pathname [optional-argument]
感嘆號和pathname之間的空格是可選的。最多見的解釋器文件如下列行開始:
#!/bin/sh
pathname一般是絕對路徑,對它不進行什麼特殊處理(即不使用PATH進行 路徑搜索)。內核調用exec函數的進程實際執行的並非該解釋器文件,而是該解釋器文件第一行中 pathname 所指定的文件。必定要將解釋器文件(文本文件,它以 #! 開頭)和解釋器(由該解釋器文件第一行中的pathname指定)區分開來。
讓咱們觀察一個實例,從中可瞭解當被執行文件是解釋器文件時,內核如何處理exec函數的參數及解釋器文件第一行的可選參數。
int main(void) { pid_t pid; if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { if (execl("/home/sar/bin/testinterp", "testinterp", "myarg1", "MY ARG2", (char *)0) < 0) err_sys("execl error"); } if (waitpid(pid, NULL, 0) < 0) err_sys("waitpid error"); exit(0);
程序echoarg(解釋器)回送每個命令行參數(他就是程序清單7-3).注意,這裏 argv[0] 是該解釋器的pathname,argv[1]是解釋器文件中的可選參數,其他參數是pathname(/home/sar/bin/testinterp),以及程序清單8-10中調用 execl 的第二個和第三個參數(myarg1和 MY ARG2)。調用execl時的 argv[1] 和 argv[2] 已右移兩個位置。注意,內核取 execl 調用中的 pathname 而非第一個參數 (testinterp),由於通常而言, pathname 包含了比第一個參數更多的信息。
8.13 system函數
在程序中執行一個命令字符串很方便。例如,假定要將時間和日期放到某個文件中,則可以使用6.10節中說明的函數實現這一點。調用 time 獲得當前日曆時間,接着調用 localtime 將日曆時間轉換爲年、月、日、時、分、秒、週日形式,而後調用 strftime 對上面的結果進行格式化處理,最後將結果寫到文件中。可是用下面的 system 函數則更容易作到這一點。
int system(const char *cmdstring);
system("data > file");
若是 cmdstring 是一個空指針,則僅當命令處理程序可用時,system 返回值非 0 值,這一特徵能夠肯定在一個給定操做系統上是否支持 system 函數。
由於 system 在其實現中調用了 fork、exec和 waitpid,所以有三種返回值:
(1)若是 fork 失敗或者 waitpid 返回除 EINTR 以外的出錯,則 system 返回 -1,並且 errno 中設置了錯誤類型值。
(2)若是 exec 失敗(表示不能執行 shell),則其返回值如同 shell 執行了 exit(127)同樣。
(3)不然全部三個函數(fork、exec和waitpid)都執行成功,而且 system 的返回值是 shell 的終止狀態,其格式已在 waitpid 中說明。
若是 waitpid 由一個捕捉到的信號中斷,則某些早期的 system 實現都返回錯誤類型值 EINTR,可是,由於沒有可用的清理策略能讓應用程序從這種錯誤類型中恢復,因此 POSIX 後來增長了下列要求:在這種狀況下 system 不返回一個錯誤(10.5節將討論被中斷的系統調用)。
下面是 system 函數的一種實現。它沒有對信號進行處理。10.18節中將修改此函數使其進行信號處理。
int system(const char *cmdstring) /* version without signal handling */ { pid_t pid; int status; if (cmdstring == NULL) return 1; /* always a command processor with UNIX */ if ((pid = fork()) < 0) { status = -1 /* probably out of processes */ } else if (pid == 0) { /* child */ execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); _exit(127); /* execl error */ } else { /* parent */ while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) { status = -1; /* error other than EINTR from waitpid() */ break; } } } return status; }
shell 的 -c 選項告訴shell 程序取下一個命令參數(在這裏是 cmdstring)做爲命令輸入(而不是從標準輸入或從一個給定的文件中讀命令)。shell對以 null 字符終止的命令字符串進行語法分析,將它們分紅命令行參數。傳遞給 shell 的實際命令字符串能夠包含任一有效 shell 命令。例如,能夠用<和>重定向輸入和輸出。
若是不使用 shell 執行此命令,而是試圖由咱們本身去執行它,那麼將至關困難。首先,咱們必須用 execlp 而不是 execl,像 shell 那樣使用 PATH 變量。咱們必須將 null 結尾的命令字符串分紅各個命令參數,以便調用 execlp。最後,咱們也不能使用任何一個 shell 元字符。
注意,咱們調用 _exit 而不是 exit。這是爲了防止任一標準 I/O 緩衝區(這些緩衝區會在 fork 中由父進程複製到子進程)在子進程中被沖洗。
用程序清單8-13對system的這種版本進行了測試(pr_exit函數定義在程序清單8-3中)
int main(void) { int status; if ((status = system("date")) < 0) err_sys("system() error"); pr_exit(status); if ((status = system("nosuchcommand")) < 0) err_sys("system() error"); pr_exit(status); if ((status = system("who; exit 44")) < 0) err_sys("system() error"); pr_exit(status); exit(0); }
使用 system 而不是直接使用 fork 和 exec 的優勢是:system 進行了所需的各類出錯處理,以及各類信號(在10.18節中的 system 函數的下一個版本中)。
在UNIX 早期版本中,都沒有 waitpid 函數,因而父進程用下列形式的語句等待子進程:
while ((lastpid = wait(&status) != pid && lastpid != -1)) ;
若是調用 system 的進程在調用它以前已經生成它本身的子進程,那麼將引發問題。由於上面的 while 語句一直循環執行,直到由 system 產生的子進程終止才中止,若是不是 pid 標識的任一子進程在 pid 子進程以前終止,則它們的進程 ID 和 終止狀態都會被 while 語句丟棄。實際上,因爲 wait不能等待一個指定的進程以及其餘一些緣由,POSIX 才定義了 waitpid 函數。若是不提供 waitpid 函數,popen 和 pclose 函數也會發生一樣的問題。
設置用戶 ID 程序
若是在一個設置用戶 ID 程序中調用 system,那麼發生什麼呢?這是一個安全性方面的漏洞,毫不應當這樣作。下面程序對其命令行參數調用 system 函數。
int main(int argc, char *argv[]) { int status; if (argc < 2) err_quit("command-line argument required"); if ((status = system(argv[1])) < 0) err_sys("system() error"); pr_exit(status); exit(0); }
將此程序編譯稱可執行文件 tsys。
程序清單8-15是另外一個簡單程序,它打印其實際和有效用戶ID
int main(void) { printf("read uid = %d, effective uid = %d\n", getuid(), geteuid()); exit(0); }
將此程序編譯成可執行文件 printuids。運行這兩個程序,獲得下列結果:
咱們給予 tsys 程序的超級用戶權限在 system 中執行了 fork 和 exec 以後仍會保持下來。
若是一個進程正以特殊權限(設置用戶ID或設置組ID)運行,它又想生成另外一個進程執行另外一個程序,則它應當直接使用 fork 和 exec,並且在 fork 以後、exec 以前要改回到普通權限。設置用戶ID或設置組ID程序決不該調用 system 函數。
8.14進程會計
8.15用戶標識
8.16 進程時間
在 1.10 節中說明了咱們能夠測量的三種時間:牆上時鐘時間、用戶cpu時間和系統cpu時間。任一進程均可調用 times 函數以得到它本身及終止進程的上述值。
clock_t times(struct tms *buf); 返回值:若成功返回流逝的牆上時鐘時間(單位:時鐘滴答數),若出錯則返回-1
此函數添寫由 buf 指向的 tms 結構,該結構定義以下:
struct tms { clock_t tms_utime; /* user CPU time */ clock_t tms_stime; /* system CPU time */ clock_t tms_cutime; /* user CPU time, terminated children */ clock_t tms_cstime; /* system CPU time, terminated children */ };
注意,此結構沒有包含牆上時鐘時間的任何測量值。做爲替代,times函數返回牆上時鐘時間做爲其函數值。此值是相對於過去的某一時刻測量的,因此不能用其絕對值,而必須使用其相對值。例如,調用 times,保存其返回值。在之後某個時間再次調用 times,重新的返回值中減去之前的返回值,此差值就是牆上時鐘時間(一個長期運行的進程可能會使牆上時鐘時間溢出,固然這種可能性極小)
該結構中兩個針對子進程的字段包含了此進程用 wait、waitid或waitpid已等待到的各個子進程的值。
全部由此函數返回的 clock_t 值都用 _SC_CLK_TCK 變換成秒數。
// 時間以及執行全部命令行參數 static void pr_times(clock_t, struct tms *, struct tms *); static void do_cmd(char *); int main(int argc, char *argv[]) { int i; setbuf(stdout, NULL); for (i = 1; i < argc; i++) do_cmd(argv[i]); /* once for each command-line arg */ exit(0); } static void do_cmd(char *cmd) { struct tms tmsstart, tmsend; clock_t start, end; int status; printf("\ncommand: %s\n", cmd); if ((start = times(&tmsstart)) == -1) /* starting values */ err_sys("time error"); if ((status = system(cmd)) < 0) /* execute command */ err_sys("system() error"); if ((end = times(&tmsend)) == -1) err_sys("times error"); pr_times(end_start, &tmsstart, &tmsend); pr_exit(status); } static void pr_times(clock_t real, struct tms *tmsstart, struct tms *tmsend) { static long clktck = 0; if (clktck == 0) if ((clktck = sysconf(_SC_CLK_TCK)) < 0) err_sys("sysconf error"); printf(" real: %7.2f\n", real/(double)clktck); printf(" user: %7.2f\n", (tmsend->tms_utime - tmsstart->tms_utime)/(double)clktck); printf(" sys: %7.2f\n", (tmsend->tms_cutime - tmsstart->tms_sutime) / (double)clktck); printf(" child user: %7.2f\n", (tmsend->tms_cutime - tmsstart->tms_cutime)/ (double) clktck); printf(" child sys: %7.2f\n", (tmsend->tms_cstime - tmsstart->tms_cstime) / (double)clktck); }
運行程序,獲得:
在這兩個實例中,子進程中顯示的全部CPU時間都是執行shell和命令的子進程所使用的CPU時間。