apue:進程控制

    UNIX的進程控制,包括創建新進程、執行程序和進程終止;進程的各種ID—實際、有效和保存的用戶和組ID,以及它們如何受到進程控制原語的影響;解釋器文件和system函數。

進程標示

    每個進程都有一個非負整型的唯一進程ID。因爲進程ID標識符總是唯一的,常將其用做其他標識符的一部分以保證其唯一性。有某些專用的進程:進程ID0是調度進程,常常被稱爲交換進程(swapper)。該進程並不執行任何磁盤上的程序—它是內核的一部分,因此也被稱爲系統進程。進程ID1通常是init進程,在自舉過程結束時由內核調用。此進程負責在內核自舉後起動一個UNIX系統。init通常讀與系統有關的初始化文件(/etc/rc*文件),並將系統引導到一個狀態。init進程決不會終止。它是一個普通的用戶進程,但是它以超級用戶特權運行。在某些UNIX的虛存實現中,進程ID2是頁精靈進程(page daemon)。此進程負責支持虛存系統的請頁操作。與交換進程一樣,頁精靈進程也是內核進程。
    下列函數返回這些標識符。

#include <sys/types.h>
#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*/

fork函數

    一個現存進程調用fork函數是UNIX內核創建一個新進程的唯一方法

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
/*返回:子進程中爲0,父進程中爲子進程ID,出錯爲-1*/

    由fork創建的新進程被稱爲子進程(child process)。該函數被調用一次,但返回兩次。兩次返回的區別是子進程的返回值是0,而父進程的返回值則是新子進程的進程ID。子進程和父進程繼續執行fork之後的指令。子進程是父進程的複製品。
    一個進程打開了三個不同文件,它們是:標準輸入、標準輸出和標準出錯。在從fork返回時的情況如下圖所示:
進程共享文件示意
    在fork之後處理文件描述符有兩種常見的情況:
- 父進程等待子進程完成。在這種情況下,父進程無需對其描述符做任何處理。當子進程終止後,它曾進行過讀、寫操作的任一共享描述符的文件位移量已做了相應更新。
- 父、子進程各自執行不同的程序段。在這種情況下,在fork之後,父、子進程各自關閉它們不需使用的文件描述符,並且不干擾對方使用的文件描述符。這種方法是網絡服務進程中經常使用的。
除了打開文件之外,很多父進程的其他性質也由子進程繼承:
- 實際用戶ID、實際組ID、有效用戶ID、有效組ID。
- 添加組ID。
- 進程組ID。
- 對話期ID。
- 控制終端。
- 設置-用戶-ID標誌和設置-組-ID標誌。
- 當前工作目錄。
- 根目錄。
- 文件方式創建屏蔽字。
- 信號屏蔽和排列。
- 對任一打開文件描述符的在執行時關閉標誌。
- 環境。
- 連接的共享存儲段。
- 資源限制。
    父、子進程之間的區別是:
- fork的返回值。
- 進程ID。
- 不同的父進程ID。
- 子進程的tms_utime,tms_stime,tms_cutime以及tms_ustime設置爲0。
- 父進程設置的鎖,子進程不繼承。
- 子進程的未決告警被清除。
- 子進程的未決信號集設置爲空集。
    使fork失敗的兩個主要原因是:系統中已經有了太多的進程,或者該實際用戶ID的進程總數超過了系統限制。
    fork有兩種用法:
- 一個父進程希望複製自己,使父、子進程同時執行不同的代碼段。當這種請求到達時,父進程調用fork,使子進程處理此請求。父進程則繼續等待下一個服務請求。
- 一個進程要執行一個不同的程序。這對shell是常見的情況。在這種情況下,子進程在從fork返回後立即調用exec。

vfork函數

    vfork函數的調用序列和返回值與fork相同,但兩者的語義不同。vfork用於創建一個新進程,而該新進程的目的是exec一個新程序。vfork與fork一樣都創建一個子進程,但是它並不將父進程的地址空間完全複製到子進程中,因爲子進程會立即調用exec(或exit),於是也就不會存訪該地址空間。不過在子進程調用exec或exit之前,它在父進程的空間中運行。vfork和fork之間的另一個區別是:vfork保證子進程先運行,在它調用exec或exit之後父進程纔可能被調度運行。

wait和waitpid函數

    當一個進程正常或異常終止時,內核就向其父進程發送SIGCHLD信號。因爲子進程終止是個異步事件,所以這種信號也是內核向父進程發的異步通知。父進程可以忽略該信號,或者提供一個該信號發生時即被調用執行的函數。對於這種信號的系統默認動作是忽略它。
    調用wait或waitpid的進程可能會:
- 阻塞(如果其所有子進程都還在運行)。
- 待子進程的終止狀態立即返回(如果一個子進程已終止,正等待父進程存取其終止狀態)。出錯立即返回(如果它沒有任何子進程)。
- 如果進程由於接收到SIGCHLD信號而調用wait,則可期望wait會立即返回。但是如果在一個任一時刻調用wait,則進程可能會阻塞。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* statloc);
pid_t waitpid(pid_t pid, int* statloc, int options);
/*兩個函數返回:若成功則爲進程ID,若出錯則爲-1*/

    這兩個函數的區別是:
- 在一個子進程終止前,wait使其調用者阻塞,而waitpid有一選擇項,可使調用者不阻塞。
- waitpid並不等待第一個終止的子進程—它有若干個選擇項,可以控制它所等待的進程。
    如果一個子進程已經終止,是一個僵死進程,則wait立即返回並取得該子進程的狀態,否則wait使其調用者阻塞直到一個子進程終止。如調用者阻塞而且它有多個子進程,則在其一個子進程終止時,wait就立即返回。這兩個函數的參數statloc是一個整型指針。如果statloc不是一個空指針,則終止進程的終止狀態就存放在它所指向的單元內。如果不關心終止狀態,則可將該參數指定爲空指針。
    對於waitpid的pid參數的解釋與其值有關:
- pid==-1等待任一子進程。於是在這一功能方面waitpid與wait等效。
- pid>0等待其進程ID與pid相等的子進程。
- pid==0等待其組ID等於調用進程的組ID的任一子進程。
- pid<-1等待其組ID等於pid的絕對值的任一子進程。
    waitpid函數提供了wait函數沒有提供的三個功能:
- waitpid等待一個特定的進程(而wait則返回任一終止子進程的狀態)。
- waitpid提供了一個wait的非阻塞版本。有時希望取得一個子進程的狀態,但不想阻塞。
- waitpid支持作業控制。

wait3和wait4函數

    這兩個函數提供的功能比函數wait和waitpid所提供的分別要多一個,這與附加參數rusage有關。該參數要求內核返回由終止進程及其所有子進程使用的資源摘要。

#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*/

    資源信息包括用戶CPU時間總量、系統CPU時間總量、缺頁次數、接收到信號的次數等。這些資源信息只包括終止子進程,並不包括處於停止狀態的子進程。

競態條件

    當多個進程都企圖對共享數據進行某種處理,而最後的結果又取決於進程運行的順序時,認爲這發生了競態條件(race condition)。如果在fork之後的某種邏輯顯式或隱式地依賴於在fork之後是父進程先運行還是子進程先運行,那麼fork函數就會是競態條件活躍的孳生地。

exec函數

    有六種不同的exec函數可供使用,它們常常被統稱爲exec函數。這些exec函數都是UNIX進程控制原語。用fork可以創建新進程,用exec可以執行新的程序。exit函數和兩個wait函數處理終止和等待終止。這些是基本的進程控制原語。

#include <unistd.h>
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[]);
/*六個函數返回:若出錯則爲-1,若成功則不返回*/

    這些函數之間的第一個區別是前四個取路徑名作爲參數,後兩個則取文件名作爲參數。當指定filename作爲參數時:
- 如果filename中包含/,則就將其視爲路徑名。
- 否則就按PATH環境變量,在有關目錄中搜尋可執行文件。
    PATH變量包含了一張目錄表(稱爲路徑前綴),目錄之間用冒號(:)分隔。
    如果execlp和execvp中的任意一個使用路徑前綴中的一個找到了一個可執行文件,但是該文件不是由連接編輯程序產生的機器可執行代碼文件,則就認爲該文件是一個shell腳本,於是試着調用/bin/sh,並以該filename作爲shell的輸入。
    第二個區別與參數表的傳遞有關(l表示表(list),v表示矢量(vector))。函數execl、execlp和execle要求將新程序的每個命令行參數都說明爲一個單獨的參數。這種參數表以空指針結尾。對於另外三個函數(execv,execvp和execve),則應先構造一個指向各參數的指針數組,然後將該數組地址作爲這三個函數的參數。
    最後一個區別與向新程序傳遞環境表相關。以e結尾的兩個函數(execle和execve)可以傳遞一個指向環境字符串指針數組的指針。其他四個函數則使用調用進程中的environ變量爲新程序複製現存的環境。
    在執行exec後,進程ID沒有改變。除此之外,執行新程序的進程還保持了原進程的下列特徵:
- 進程ID和父進程ID。
- 實際用戶ID和實際組ID。
- 添加組ID。
- 進程組ID。
- 對話期ID。
- 控制終端。
- 鬧鐘尚餘留的時間。
- 當前工作目錄。
- 根目錄。
- 文件方式創建屏蔽字。
- 文件鎖。
- 進程信號屏蔽。
- 未決信號。
- 資源限制。
- tms_utime,tms_stime,tms_cutime以及tms_ustime值。
    在很多UNIX實現中,這六個函數中只有一個execve是內核的系統調用。另外五個只是庫函數,它們最終都要調用系統調用。這六個函數之間的關係如下圖:
exec函數關係示意

更改用戶ID和組ID

    可以用setuid函數設置實際用戶ID和有效用戶ID。與此類似,可以用setgid函數設置實際組ID和有效組ID。

#include <sys/types.h>
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
/*兩個函數返回:若成功則爲0,若出錯則爲-1*/

    有關改變用戶ID的規則:
- 若進程具有超級用戶特權,則setuid函數將實際用戶ID、有效用戶ID,以及保存的設置-用戶-ID設置爲uid。
- 若進程沒有超級用戶特權,但是uid等於實際用戶ID或保存的設置-用戶-ID,則setuid只將有效用戶ID設置爲uid。不改變實際用戶ID和保存的設置-用戶-ID。
- 如果上面兩個條件都不滿足,則errno設置爲EPERM,並返回出錯。
    關於內核所維護的三個用戶ID,還要注意下列幾點:
- 只有超級用戶進程可以更改實際用戶ID。通常,實際用戶ID是在用戶登錄時,由login程序設置的,而且決不會改變它。因爲login是一個超級用戶進程,當它調用setuid時,設置所有三個用戶ID。
- 僅當對程序文件設置了設置-用戶-ID位時,exec函數設置有效用戶ID。如果設置-用戶-ID位沒有設置,則exec函數不會改變有效用戶ID,而將其維持爲原先值。任何時候都可以調用setuid,將有效用戶ID設置爲實際用戶ID或保存的設置-用戶-ID。自然,不能將有效用戶ID設置爲任一隨機值。
- 保存的設置-用戶-ID是由exec從有效用戶ID複製的。在exec按文件用戶ID設置了有效用戶ID後,即進行這種複製,並將此副本保存起來。

setreuid和setregid函數

    setregid函數功能是交換實際用戶ID和有效用戶ID的值。

#include <sys/types.h>
#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
/*兩個函數返回:若成功則爲0,若出錯則爲-1*/

    一個非特權用戶總能交換實際用戶ID和有效用戶ID。這就允許一個設置-用戶-ID程序轉換成只具有用戶的普通許可權,以後又可再次轉換回設置-用戶-ID所得到的額外許可權。

seteuid和setegid函數

    兩個函數seteuid和setegid,它們只更改有效用戶ID和有效組ID。

#include <sys/types.h>
#include <unistd.h>
int seteuid(uid_t uid);
int setegid(gid_t gid);
/*兩個函數返回:若成功則爲0,若出錯則爲-1*/

    一個非特權用戶可將其有效用戶ID設置爲其實際用戶ID或其保存的設置-用戶-ID。對於一個特權用戶則可將有效用戶ID設置爲uid。

組ID

    添加組ID不受setgid函數的影響。設置不同用戶ID的函數如下:
設置ID函數關係示意

解釋器文件

    這種文件是文本文件,其起始行的形式是:

#!pathname[optional-argument]

    在驚歎號和pathname之間的空格是可任選的。
    pathname通常是個絕對路徑名,對它不進行什麼特殊的處理(不使用PATH進行路徑搜索)。對這種文件的識別是由內核作爲exec系統調用處理的一部分來完成的。內核使調用exec函數的進程實際執行的文件並不是該解釋器文件,而是在該解釋器文件的第一行中pathname所指定的文件。

system函數

    ANSIC定義了system函數,但是其操作對系統的依賴性很強。

#include <stdlib.h>
int system(const char* cmdstring);

    如果cmdstring是一個空指針,則僅當命令處理程序可用時,system返回非0值,這一特徵可以決定在一個給定的操作系統上是否支持system函數。在UNIX中,system總是可用的。因爲system在其實現中調用了fork、exec和waitpid,因此有三種返回值:
- 如果fork失敗或者waitpid返回除EINTR之外的出錯,則system返回-1,而且errno中設置了錯誤類型。
- 如果exec失敗(表示不能執行shell),則其返回值如同shell執行了exit(127)一樣。
- 否則所有三個函數(fork,exec和waitpid)都成功,並且system的返回值是shell的終止狀態,其格式已在waitpid中說明。

用戶標示

    任一進程都可以得到其實際和有效用戶ID及組ID。但是有時希望找到運行該程序的用戶的登錄名。如果一個用戶有多個登錄名,這些登錄名又對應着同一個用戶ID,用getlogin函數可以存取此登錄名。

#include <unistd.h>
char* getlogin(void);
/*返回:若成功則爲指向登錄名字符串的指針,若出錯則爲NULL*/

    如果調用此函數的進程沒有連接到用戶登錄時所用的終端,則本函數會失敗。通常稱這些進程爲精靈進程(daemon)。

進程時間

    任一進程都可調用times函數以獲得它自己及終止子進程的牆上時鐘時間、用戶CPU時間和系統CPU時間。

#include <sys/times.h>
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, teminated children*/
    clock_t tms_cstime; /*system CPU time, terminated children*/
};

    此結構沒有包含牆上時鐘時間。作爲代替,times函數返回牆上時鐘時間作爲函數值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章