1. 程式人生 > >UNIX環境高階程式設計學習筆記(九)程序控制

UNIX環境高階程式設計學習筆記(九)程序控制

1.程序標識

每個程序都有一個非負整型表示的唯一程序ID。因為程序ID識別符號總是唯一的,常將其用作其他識別符號的一部分以保證其唯一性。程序ID是可複用的,當一個程序終止後,其程序ID就成為複用的候選者。

ID為0的程序通常是排程程序,常常被稱為交換程序(swapper)。該程序是核心的一部分,它不執行任何磁碟上的程式,因此也被稱為系統程序。程序ID 1 通常是init程序,在自舉過程結束時由核心呼叫。該程序的程式檔案在UNIX早期版本中是/etc/init,在較新版本中是/sbin/init。此程序負責在自舉核心後啟動一個UNIX系統。init 通常讀取與系統有關的出事後文件(/etc/rc*檔案或/etc/inittab檔案,以及在/etc/init.d中的檔案),並將系統引導到一個狀態(如多使用者)。init程序決不會終止。它是一個以超級使用者特權執行的普通使用者程序(不同於交換程序,它不是核心的系統程序)。init 會成為所有孤兒程序的父程序。

每個UNIX系統實現都有它自己的一套作業系統服務的核心程序,如在某些UNIX的虛擬儲存器實現中,程序ID 2是頁守護程序,此程序負責支援虛擬儲存器系統的分頁操作。

下面這些函式返回程序的相關標識:

#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

2.函式fork和vfork

一個現有的程序可以呼叫fork函式建立一個新程序。

#include <unistd.h>
pid_t fork(void);
pid_t vfork(void);

由fork建立的新程序被稱為子程序,fork被呼叫一次,但返回兩次。兩次返回的區別是子程序的返回值是0,而父程序的返回值則是新建子程序的程序ID。

將子程序ID返回給父程序的理由是:因為一個程序的子程序可以有多個,並且沒有一個函式使一個程序可以獲得其所有子程序的程序ID。fork使子程序得到返回值0的理由是:一個程序只會有一個父程序,所以子程序總是可以呼叫getppid獲得其父程序的程序ID(程序ID 0 為核心交換程序使用,所有一個子程序的程序ID不可能為0)。

由於在fork之後經常跟隨著exec,所以現在的很多實現並不執行一個父程序的資料段、棧和堆的完全副本。作為替代,使用了寫時複製(Copy-On-Write, COW)技術。這些區域由父程序和子程序共享,而且核心將它們的訪問許可權修改為只讀。如父程序或子程序中的任一個試圖修改這些區域,則核心只為修改區域的那塊記憶體製作一個副本,通常是虛擬儲存系統中的一“頁”。

fork之後是由父程序先執行還是子程序先執行是不確定的,這取決於核心所使用的排程演算法。
fork的一個特性是父程序的所有開啟檔案描述符都被複制到子程序中。且共享同一檔案偏移量。

fork之後處理檔案描述符有以下兩種常見情況:

  • 父程序等待子程序完成。在這種情況下,父程序無需對其描述符做任何處理,子程序終止後,它曾經進行讀寫操作的任一共享描述符的檔案偏移量做了相應更新。
  • 父程序和子程序各自執行不同的程式段。在這種情況下,父子程序都關閉它們不需使用的檔案描述符,這樣就不會干擾對方使用的檔案描述符。

子程序會繼承很多父程序的屬性,包括:檔案描述符、實際使用者ID、實際組ID、有效使用者ID、有效組ID、附屬組ID、程序組ID、會話ID、控制終端、設定使用者ID標誌和設定組ID標誌、當前工作目錄、根目錄、檔案模式建立遮蔽字、訊號遮蔽和安排、對任一開啟檔案描述符的執行時關閉(close-on-exec)標誌、環境、連線的共享儲存段、儲存映像、資源限制

父子程序之間的區別具體如下:

  • fork的返回值不同
  • 程序ID不同
  • 程序的父程序ID不同
  • 子程序的tms_utime、tms_stime、tms_cutime和tms_ustime被設定為0
  • 子程序不繼承父程序設定的檔案鎖
  • 子程序未處理的鬧鐘被清除 子程序的未處理訊號集被設定為空集

fork失敗的主要原因:

  • 系統中已經有了太多的程序
  • 該實際使用者ID的程序總數超過了系統限制

fork的兩種用法:

  • 一個父程序希望複製自己,使父程序和子程序同時執行不同的程式碼段。
  • 一個程序要執行一個不同的程式。

vfork和fork一樣都建立一個子程序,但它並不完全複製父程序的地址空間,因為子程序會立刻呼叫exec(或exit),於是也就不會引用該地址空間。不過在子程序呼叫exec和exit之前,它在父程序的空間中執行。
vfork與fork的另一個區別是:vfork保證子程序先執行,在它呼叫exec或exit之後父程序才可能被排程執行,當子程序呼叫者兩個函式其中任意一個時,父程序會恢復執行。

3.函式exit

程序的5種正常終止及3種異常終止方式:

  1. 在main函式內執行
  2. return語句
  3. 呼叫exit函式
  4. 呼叫_exit和_Exit函式
  5. 程序的最後一個執行緒在啟動例程中執行return語句 程序的最後一個執行緒呼叫pthread_exit函式
  6. 呼叫abort
  7. 當程序接收到某些訊號時
  8. 最後一個執行緒對“取消”請求作出響應

對於上述任意一種終止情形,我們都希望終止程序能夠通知其父程序它是如何終止的。對於3個終止函式(exit、_exit、_Exit),實現這一點的方法是,將其退出狀態作為引數傳送給函式。在異常終止情況,核心(不是程序本身)產生一個指示其異常終止原因的終止狀態。在任意一種情況下,該終止程序的父程序都能用wait、waitpid函式取得其終止狀態。

退出狀態與終止狀態有所區別。在最後呼叫_exit時,核心將退出狀態轉換成終止狀態。

對於父程序已經終止的所有程序,它們的父程序都改變為init程序。我們稱這些程序由init程序收養。其操作過程大致是:一個程序終止時,核心逐個檢查所有活動程序,以判斷它是否是正要終止程序的子程序,如果是,則該程序的父程序ID就更改為1(init程序的ID)。

如果子程序在父程序之前終止,父程序如何在做相應檢查時得到子程序的終止狀態呢?如果子程序完全消失了,父程序無法獲取其終止狀態。核心為每個終止子程序儲存了一定量的資訊,所以當終止程序的父程序呼叫wait或者waitpid時,可以得到這些資訊。這些資訊至少包括程序ID、該程序的終止狀態以及該程序使用CPU時間總量。核心可以釋放終止程序所使用的所有儲存區,關閉其所有開啟檔案。

一個已經終止、但是其父程序尚未對其進行善後處理(獲取終止子程序的有關資訊、釋放它仍佔有的資源)的程序被稱為僵死程序。 ps命令將僵死程序的狀態列印為 Z 。如果編寫一個長期執行的程式,它fork了很多子程序,那麼除非父程序等待取得子程序的終止狀態,這些子程序終止後就會變成僵死程序。

init程序收養的程序終止時不會變成僵死程序,因為init被編寫成無論何時只要有一個子程序終止,init就會呼叫一個wait函式取得其終止狀態。

4.函式wait和waitpid
當一個程序正常或者異常終止時,核心就向其父程序傳送SIGCHLD訊號。因為子程序終止是非同步事件,所以這種訊號也是核心向父程序發的非同步通知。父程序可以選擇忽略該訊號,或者提供一個該訊號發生時即被呼叫執行的函式(訊號處理程式)。對於這種訊號,系統的預設動作是忽略它。

呼叫wait和waitpid的程序可能發生:

  1. 如果其所有子程序都還在執行,則阻塞
  2. 如果一個子程序已終止,正等待父程序獲取其終止狀態,則取得該子程序的終止狀態立即返回。
  3. 如果它沒有任何子程序,則立即出錯返回。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

這兩個函式的區別如下:

  • 在一個子程序終止之前,wait使其呼叫者阻塞,而waitpid有一選項,可使呼叫者不阻塞。
  • waitpid並不等待在其呼叫之後的第一個終止的子程序,它有若干選項,可以控制它所等待的程序。

兩個函式的引數statloc是一個整型指標。如果statloc不是一個空指標,則終止程序的終止狀態就存放在它所指向的單元內。如果不關心終止狀態,可將該引數指定為空指標。
waitpid根據引數pid等待一個特定的程序。options使得我們能進一步控制waitpid的操作。

POSIX.1規定,終止狀態用定義在sys/wait.h中的各個巨集來檢視,有4個互斥的巨集可用來取得程序終止的原因,它們的名字都以WIF開始。

巨集 說明
WIFEXITED 返回真時表示子程序正常終止
WIFSIGNALED 返回真時表示子程序收到訊號而導致異常終止
WIFSTOPPED 返回真時表示子程序處於停止狀態
WIFCONTINUED 返回真時表示子程序進入暫停後繼續的狀態

fork兩次可以避免僵死程序。

第一個子程序fork後在第二個子程序之前終止,則第二個子程序會由init程序收養,然後用wait函式獲取第一個子程序的終止狀態防止其成為僵死程序。

5.函式waitid和wait3、wait4

Single UNIX Specification 包括了另一個獲取程序終止狀態的函式,即waitid,此函式類似於waitpid,但提供了更多的靈活性。

#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

與waitpid相似,waitid允許一個程序指定要等待在子程序。但它使用兩個獨立的引數表示要等待的子程序所屬的型別,而不是將此程序與程序ID或程序組ID組合成一個引數,id引數的作用與idtype的值相關。

常量 說明
P_PID 等待一特定程序:id包含要等待子程序的程序ID
P_PGID 等待一特定程序組中任一子程序:id包含要等待子程序的程序組ID
P_ALL 等待任一子程序:忽略id

options引數是下列各標誌的按位或運算。這些標誌指示呼叫者關注哪些狀態變化。

常量 說明
WCONTINUED 等待一子程序,它以前曾被停止,此後又已繼續,但其狀態尚未報告
WEXITED 等待已退出的程序
WNOHANG 如無可用的子程序退出狀態,立即返回而非阻塞
WNOWAIT 不破壞子程序退出狀態。該子程序的退出狀態可由後續的wait函式獲取
WSTOPPED 等待一子程序,它已經停止,但其狀態尚未報告

WCONTINUED、WEXITED或WSTOPPED這3個常量之一必須在options引數中指定。

wait3和wait4函式比起wait、waitpid和waitid提供的功能多一個。這與附加引數有關。該引數允許核心返回由終止程序及其所有子程序使用的資源概況。資源統計資訊包括使用者CPU時間總量、系統CPU時間總量、缺頁次數、接受到訊號的次數等。

#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);

6.競爭條件
當多個程序都企圖對共享資料進行某種處理,而最後的結果又取決於程序執行的順序時,則認為發生了競爭條件。

7.函式exec

fork函式建立新的子程序後,子程序往往要呼叫一種exec函式以執行另一個程式。當程序呼叫一種exec函式時,該程序執行的程式完全替換為新程式,而新程式則從其main函式開始執行。因為呼叫exec並不建立新程序,所以前後的程序並未改變。exec只是用磁碟上的一個新程式替換了當前程序的正文段、資料段、堆段和棧段。

有7種不同的exec函式可供使用,它們常常被統稱為exec函式,可以使用這7個函式中的任一個。這些exec函式使得UNIX系統程序控制原語更加完善。用fork可以建立新程序,用exec可以初始執行新的程式。exit函式和wait函式處理終止和等待終止。這些是我們需要的基本的程序控制原語。使用這些原語可構造另外一些如popen和system之類的函式。

#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[]);
int fexecve(int fd, char *const argv[], char *const envp[]);

8.更改使用者id和更改組id

#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
int seteuid(uid_t uid);
int setegid(gid_t gid);

設定不同使用者ID的各函式如下圖所示:

這裡寫圖片描述

9.直譯器檔案
所有現今的UNIX系統都支援直譯器檔案(interpreter file )。這種檔案是文字檔案,其起始行的形式是:

#! pathname [optional-argument]

pathname通常是絕對路徑名,對它不進行什麼特殊的處理(不使用PATH進行路徑搜尋)。對這種檔案的識別是由核心作為exec系統呼叫處理的一部分來完成的。核心使呼叫exec函式的程序實際執行的並不是該直譯器檔案,而是在該直譯器檔案第一行中pathname所指定的檔案。一定要將直譯器檔案(文字檔案,它以#!開頭)和直譯器(由該直譯器檔案第一行中的pathname指定)區分開來。

很多系統對直譯器檔案第一行有長度限制。這包括#!、pathname、可選引數、終止換行符以及空格數。

10.函式system

這個函式使用/bin/sh執行指定的命令串執行標準的shell命令。形如:

$?/bin/sh?-c?cmdstring
#include <stdlib.h>
int system(const char *cmdstring);

應注意的是,設定了SetUID 或 SetGID 的程式不應使用 system 函式。另外,作為伺服器程式時,也不應使用system 處理客戶程式提供的字串引數,以避免惡意使用者利用 shell 中的特殊操作符進行越權操作。

使用system而不是直接使用fork和exec的優點是:system進行了所需的各種出錯處理以及各種訊號處理。

11.程序排程
UNIX系統歷史上對程序提供的只是基於排程優先順序的粗粒度的控制。排程策略和排程優先順序是由核心確定的。程序可以通過調整nice值選擇以更低優先順序執行(通過調整nice值降低它對CPU的佔有,因此該程序是“友好的”)。只有特權程序允許提高排程許可權。

#include <unistd.h>
int nice(int incr);

incr引數被增加到呼叫程序的nice值上。如果incr太大,系統直接把它降到最大合法值,不給出提示。類似地,如果incr太小,系統也會無聲息地把它提高到最小合法值。由於-1是合法的成功返回值,在呼叫nice函式之前需要清楚errno,在nice函式返回一1時,需要檢查它的值。如果nice呼叫成功,並且返回值為-1,那麼errno仍然為0。如果errno不為0,說明nice呼叫失敗。

getpriority函式可以像nice函式那樣用於獲取程序的nice值,但是getpriority還可以獲取一組相關程序的nice值。setpriority函式可用於為程序、程序組和屬於特定使用者ID的所有程序設定優先順序。

#include <sys/resource.h>
int getpriority(int which, id_t who);
int setpriority(int which, id_t who, int value);

12.程序時間
我們可以度量的3個時間:牆上時鐘時間、使用者CPU時間和系統CPU時間。任一程序都可呼叫times函式獲得它自己以及己終止子程序的上述值。

#include <sys/times.h>
clock_t times(struct tms *buf);

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 */
};