1. 程式人生 > >UNIX環境高階程式設計(3) 第八章

UNIX環境高階程式設計(3) 第八章

8 程序控制

8.1 引言

8.2 程序標識

#include <unistd.h>
    pid_t getpid(void);
        return: 呼叫程序的程序ID

    pid_t getppid(void);
        return: 父程序ID

    uid_t getuid(void);
        return: 實際使用者ID

    uid_t geteuid(void);
        return: 有效使用者ID

    gid_t getgid(void);
        return: 實際組ID

    gid_t getegid(void
); return: 有效組ID 上函式都沒有出錯返回。 pid_t fork(void); return: 子程序返回0,父程序返回子程序ID(>0),error: -1 建立一個新程序(子程序)

8.3 函式fork

#include <unistd.h>
pid_t fork(void);
返回值:子程序返回0,父程序返回子程序ID,若出錯,返回-1.

生成當前程序的一個相同副本,該副本稱之為”子程序”。原程序的所有資源都以適當的方式複製到子程序,因此執行了該系統呼叫之後,原來的程序就有了2個獨立的例項,包括

  1) 同一組開啟檔案
  2) 同樣的工作目錄
  3) 記憶體中同樣的資料(2個程序各有一個副本)

如果父程序和子程序寫同一描述符指向的檔案,但有沒有任何形式的同步,那麼它們的輸出就會出現混亂。雖然這種情況可能會發生,但這並是常用的操作模式。在fork之後處理檔案描述符有以下兩種常見的情形:

a,父程序等待子程序完成。在這情況下,父程序無需對其描述符做任何處理。當子程序終止後,他曾進行過讀、寫操作的任一共享描述符的檔案偏移量做了相應更新。
b,父程序和子程序各自執行不同的程式段。這種情況下,在fork之後,父程序和子程序各自關閉它們不需要使用的檔案描述符,這樣就不會干擾對方使用的檔案描述符。是網路服務程序常用的方法。

除了開啟檔案之外,父程序的很多其他屬性也由子程序繼承,包括:

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

父程序和子程序之間的區別在於:

  • fork的返回值不同。
  • 程序ID不同
  • 這兩個程序的父程序ID不同:子程序的父程序ID是建立它的程序ID,而父程序的ID則不變。
  • 子程序的tms_utime、tms_stime、tms_cutime和tms_ustime的值設定為0.
  • 子程序不繼承父程序設定的檔案鎖。
  • 子程序的未處理鬧鐘被清除。
  • 子程序的未處理訊號集設定為空集。

fork函式被呼叫一次,返回兩次。子程序中返回值是0,父程序中返回值是子程序的pid

子程序是父程序的副本,子程序獲得父程序的資料空間、堆和棧的副本。注意,在是子程序擁有的副本。父子程序並不共享這些儲存空間部分。父子程序共享正文段。

由於在fork之後經常跟隨著exec,所以現在的很多實現並不執行一個父程序資料段、堆和棧的完全副本。作為替代,使用了寫時複製技術。

4種平臺都支援的變體:vfork;Linux的變體:clone系統呼叫,允許呼叫者控制哪些部分由父子程序共享。

fork之後是父程序先執行還是子程序先執行是不確定的

父程序中的所有開啟檔案描述符都被複制到子程序中,父子程序為每個相同的開啟描述符共享一個檔案表項,故共享同一檔案偏移量。如果父子程序寫同一描述符執行的檔案,又沒有任何形式的同步,那麼它們的輸出就會混合。 在fork之後處理檔案描述符有以下兩種常見的情況:

  • 父程序等待子程序完成。這種情況下,父程序無需對其描述符做任何處理。
  • 父程序和子程序各自執行不同的程式段。這種情況下,fork之後,父子程序各自它們不需要使用的檔案描述符。

strlen和sizeof的區別:前者不包括null位元組,一次函式呼叫;後者包括null位元組,編譯時計算

除了檔案描述符之外,父程序的很多其他屬性也由子程序繼承,包括: 實際使用者ID、實際組ID、有效使用者ID、有效組ID

附屬組ID
程序組ID
會話ID
控制終端
SUID和SGID標誌(stat結構的st_mode成員)
當前工作目錄
根目錄
檔案模式建立遮蔽字umask
訊號遮蔽和處理
對任一開啟檔案描述符的執行時關閉(close-on-exec)標誌
環境
連線的共享儲存段
儲存映像
資源限制
是否繼承nice值由具體實現自行決定

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

1, fork的返回值不同
2, pid不同
3, 這兩個程序的父程序不同
4, 子程序的tms_utime、tms_stime、tms_cutime和tms_ustime的值設定為0
5, 子程序不繼承父程序設定的檔案鎖
6, 子程序的未處理鬧鐘被清除
7, 子程序的未處理訊號集設定為空集

fork失敗的兩個主要原因:

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

fork有以下兩種用法:

1) 一個父程序希望複製自己,使父程序和子程序同時執行不同的程式碼段。這在網路伺服器中是常見的。
2) 一個程序要執行一個不同的程式。這對shell是常見的情況。某些系統將fork+exec組合成一個操作spawn

vfork和fork函式的區別: * vfork函式用於建立一個新程序,而該新程序的目的是exec一個新程式,故不將父程序的地址空間完全複製到子程序中,因為子程序會立即呼叫exec(或exit),於是也就不會引用該地址空間。不管在子程序呼叫exec或exit之前,它在父程序的空間中執行。 * 另一個區別是vfork保證子程序先執行,在它呼叫exec或exit之後父程序才可能被排程執行。故如果在呼叫這兩個函式之前子程序依賴於父程序的進一步動作,則會導致死鎖。

8.5 函式exit

5種正常終止方式:

1,從main中執行return,等效於呼叫exit
2,呼叫exit函式,呼叫各終止處理程式,關閉標準I/O流,最後呼叫_exit函式
3,呼叫_exit或_Exit
4,程序的最後一個執行緒在其啟動例程執行return語句,該程序以終止狀態0返回
5,程序的最後一個執行緒呼叫pthread_exit,程序終止狀態總是0

3種異常終止方式:

1,呼叫abort,它產生SIGABRT訊號
2,當程序接收到某些訊號時,訊號可由程序自身(如呼叫abort函式)、其他程序或核心產生
3,最後一個執行緒對“取消”請求做出響應

不管程序如何終止,最後都會執行核心中的同一段程式碼。這段程式碼為相應的程序關閉所有開啟描述符,釋放它所使用的儲存器等。 注意:“退出狀態”(3個exit函式的引數或main的返回值)區別於“終止狀態”。在最後呼叫_exit時,核心將退出狀態轉換為終止狀態。

  • 如果父程序在子程序之前終止,則稱子程序為孤兒程序。子程序 ppid變為1,稱這些程序由init程序收養。一個init程序收養的程序終止時,init會呼叫一個wait函式取得其終止狀態,防止它成為殭屍程序。
  • 如果子程序在父程序之前終止,核心為每個終止子程序儲存了一定量的資訊,至少包括pid、該程序的終止狀態以及該程序使用的CPU時間總量。核心可以釋放終止程序所使用的所有儲存區,關閉其所有開啟檔案。在Unix術語中,一個已經終止、但其父程序尚未對其進行善後處理(獲取終止子程序的有關資訊、釋放它仍佔用的資源)的程序被稱為殭屍程序zombie/defunct。

8.6 函式wait和waitpid

#include <sys/wait.h>
    pid_t wait(int *statloc);
    pit_t waitpid(pid_t pid, int statloc, int options);
        return: 程序ID,error: 0 or -1
  • 呼叫wait或waitpid的程序可能:
    如果其所有子程序都還在執行,則阻塞 如果一個子程序終止,正等待其父程序獲取其終止狀態,則取得該子程序的終止狀態立即返回 如果它沒有任何子程序,則立即出錯返回
  • 如果程序由於收到SIGCHLD訊號而呼叫wait,我們期望wait會立即返回。
  • wait與waitpid的區別 waitpid有一選項,可使呼叫者不阻塞 waitpid可以控制它所等待的程序
  • 若statloc不是NULL,則終止程序的終止狀態就存放在它所指向的單元內。該整型狀態字由實現定義,其中某些位表示退出狀態(正常返回),其他位則指示訊號編號(異常返回),有一位指示是否產生了core檔案。

  • waitpid函式中的pid引數的解釋: pid == -1,等待任一子程序,等價於wait函式 pid > 0,等待pid等於該值的子程序 pid == 0,等待組ID等於呼叫程序組ID的任一子程序 pid < 0,等待組ID等於pid絕對值的任一子程序

  • waitpid函式中的options引數:WNOHANG(不阻塞)、WCONTINUED、WUNTRACED
  • 如果一個程序fork一個子程序,但不要它等待子程序終止,也不希望子程序處於殭屍狀態直到父程序終止,實現這一要求的訣竅是呼叫fork兩次。
#include <sys/wait.h>
    int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
        return: 0, error: -1
* idtype引數:P_PID、P_PGID、P_ALL
* options引數:WEXITED、WNOHANG...
* infop引數是指向siginfo結構的指標

8.8 函式wait3、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);
    pit_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
        Both return: 程序ID, error: -1
  • 允許核心返回由終止程序及其所有子程序使用的資源概況,包括使用者CPU時間總量、系統CPU時間總量、缺頁次數、接收到訊號的次數等。 man 2 getrusage

8.9 競爭條件

當多個程序都企圖對共享資料進行某種處理,而最後的結果又取決於程序執行的順序時,則發生了競爭條件。fork函式是競爭條件活躍的滋生地。

8.10 函式exec

一個可執行的二進位制檔案來載入另一個應用程式,來”代替”當前執行的程序,即載入了一個新的程序。因為exec並不建立新程序,所以必須首先使用fork複製一箇舊的程式,然後呼叫exec在系統上建立另一個應用程式

總體來說:fork負責產生空間、exec負責載入實際的需要執行的程式函式exec

當程序呼叫exec函式時,該程序執行的程式完全替換為新程式,而新程式則從其main函式開始執行。

因為呼叫exec並不建立新程序,所以前後的程序ID並未改變。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[]);
        All seven return: −1 on error, no return on success
  • l表示列表list,新程式的每個命令列引數都是一個單獨的引數,空指標結尾
  • v表示向量vector,指標陣列
  • e代表傳遞一個指向環境字串指標陣列的指標
  • p代表使用呼叫程序中的environ變數為新程式複製現有的環境
  • 在執行exec後,pid沒有改變。但新程式從呼叫程序繼承了下列屬性:

    • pid和ppid
    • 實際使用者ID和實際組ID
    • 附屬組ID
    • 程序組ID
    • 會話ID
    • 控制終端
    • 鬧鐘尚餘留的時間
    • 當前工作目錄
    • 根目錄
    • 檔案模式建立遮蔽字umask
    • 檔案鎖
    • 程序訊號遮蔽
    • 未處理訊號
    • 資源限制
    • nice值
    • tms_utime、tms_stime、tms_cutime、tms_cstim

    對開啟檔案的處理:若檔案描述符的執行時關閉(close-on-exec,預設通過fcntl設定)標誌被設定(預設沒有設定),則在執行exec時關閉該描述符;否則仍保持開啟。POSIX.1明確要求在exec時關閉開啟目錄流。

8.11 更改使用者ID和更改組ID

#include <unistd.h>
    int setuid(uid_t uid);
    int setgid(gid_t gid);
        return: 0; error:-1
  • 關於誰能更改ID的若干規則(這裡討論更改使用者ID的規則,同樣適用於組ID)
  • 實際使用者ID ruid、有效使用者ID euid、儲存的設定使用者ID sSUID。假定_POSIX_SAVED_IDS為真 若程序具有root特權,則setuid函式將ruid、euid和sSUID設定為引數uid的值 若程序沒有root特權,但uid等於ruid或sSUID,則setuid函式只將euid設定為uid 如果上面兩個條件都不滿足,則errno設定為EPERM,並返回-1
  • 關於核心所維護的3個使用者ID,還有注意以下幾點:
    • 只有root程序可以更改ruid。通常,ruid是在使用者登入時,由login程式設定的,而且決不會改變它。
    • 僅當對程式檔案設定了SUID位,exec函式才設定euid。沒有設定SUID位,則euid = ruid。
    • sSUID是由exec複製euid而得到的。
#include <unistd.h>
    int setreuid(uid_t ruid, uid_t euid);
    int setregid(gid_t rgid, gid_t egid);
        return: 0, error: -1
  • 交換實際使用者ID和有效使用者ID的值,如若其中任一引數的值為-1,則表示相應的ID應當保持不變。
  • 規則:一個非root使用者總能交換ruid和euid,這就允許一個設定了SUID的程式交換成普通使用者許可權後,可以再次交換會SUID許可權
#include <unistd.h>
    int seteuid(uid_t uid);
    int setegid(gid_t gid);
        Both return: 0 if OK, −1 on error
  • 對於非root使用者,可將euid設定為其ruid或sSUID;這與setuid函式一樣
  • 對於root使用者,可將其euid設定為uid,而ruid、sSUID保持不變
  • 組ID:上面所說的一切都以類似方式適用於各個組ID。附屬組ID不受setgid、setregid、setegid函式的影響。

8.12 直譯器檔案

現今UNIX系統都支援直譯器檔案(interpreter file),它是一種文字檔案,起始行形式是: #! pathname[optinal-argument] shell指令碼中常見的行開始:#! /bin/sh

上面pathname是絕對路徑名,對它不進行特殊處理,對此檔案的識別是由核心作為exec系統呼叫處理的一部分完成的。所以真正執行檔案的是exec函式程序,而不是直譯器檔案。

直譯器是pathname指定的。

8.13 函式system

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

如果cmdstring是一個空指標,則僅當命令處理程式可用時,system返回非0值,這一特徵可以確定給定的作業系統上是否支援system函式。在UNIX中,system總是可用的。

因為system在其實現中呼叫了fork、exec和waitpid,所以有3種返回值:

  1. fork失敗或者waitpid返回除EINTR之外的出錯,則system返回-1,並且設定errno以指示錯誤型別。
  2. 如果exec失敗(表示不能執行shell),則其返回值如同shell執行了exit(127)一樣。
  3. 否則所有3個函式(fork、exec和waitpid)都成功,那麼system的返回值是shell的終止狀態,其格式已在waitpid中說明。

8.14 程序會計

  • accton命令啟用會計處理;在Linux中,該檔案是/var/account/pacct
#include <sys/acct.h>
typedef u_short comp_t; /* 3-bit base 8 exponent; 13-bit fraction */
struct acct
{
 char ac_flag; /* flag (see Figure 8.26) */
 char ac_stat; /* termination status (signal & core flag only) */
 /* (Solaris only) */
 uid_t ac_uid; /* real user ID */
 gid_t ac_gid; /* real group ID */
 dev_t ac_tty; /* controlling terminal */
 time_t ac_btime; /* starting calendar time */
 comp_t ac_utime; /* user CPU time */
 comp_t ac_stime; /* system CPU time */
 comp_t ac_etime; /* elapsed time */
 comp_t ac_mem; /* average memory usage */
 comp_t ac_io; /* bytes transferred (by read and write) */
 /* "blocks" on BSD systems */
 comp_t ac_rw; /* blocks read or written */
 /* (not present on BSD systems) */
 char ac_comm[8]; /* command name: [8] for Solaris, */
 /* [10] for Mac OS X, [16] for FreeBSD, and */
 /* [17] for Linux */
};

大多數UNIX系統提供了一個選項一進行程序會計(process accounting)處理。啟用該選項後,沒當程序結束時核心就寫一個會計記錄。典型的會計記錄包含總量較小的二進位制資料,一般包括命令名、所使用的的CPU時間總量、使用者ID和組ID、啟動時間等。

會計記錄所需的各個資料(各CPU時間、傳輸的字元數等)都有核心保安在程序表中,並在一個新程序被建立是初始化(如fork之後再子程序中)。程序終止時寫一個會計記錄。這產生兩個後果:

1, 我們不能獲取永遠不終止的程序的會計記錄。像init這樣的核心守護程序。
2, 在會計檔案中記錄的順序對應於程序終止的順序,而不是他們啟動的順序。

會計記錄對應於程序而不是程式。在fork之後,核心位子程序初始化一個記錄,而不是在一個新程序被執行時初始化。雖然exec並不建立一個新的會計記錄,但相應記錄中的命令名改變了,AFORK標誌則被清除。這意味著,如果一個程序順序執行了3個程式(A exec B、B exec C,最後是C exit),只會寫一個會計記錄。在該記錄中命令名對應於程式C,但CPU時間是程式A、B和C之和。

8.15 使用者標識

  • 獲取登陸名
#include <unistd.h>
    char *getlogin(void);
    Returns: pointer to string giving login name if OK, NULL on error
  • 如果呼叫此函式的程序沒有連線到使用者登陸時所用的終端,則函式會失敗。通常稱這些程序為守護程序。

8.16 程序排程

  • 程序通過調整nice值選擇以更低優先順序執行。只有特權程序允許提高排程許可權。
  • nice值的範圍在0~(2*NZERO)-1之間,NZERO為系統預設的nice值。nice值越小,優先順序越高
#include <unistd.h>
    int nice(int incr);
        Returns: new nice value − NZERO if OK, −1 on error

#include <sys/resource.h>
    int getpriority(int which, id_t who);
        Returns: nice value between −NZERO and NZERO−1 if OK, −1 on error

#include <sys/resource.h>
    int setpriority(int which, id_t who, int value);
        Returns: 0 if OK, −1 on error

8.17 程序時間

我們可以度量的3個時間:

1,牆上時鐘時間;
2,使用者CPU時間;
3,系統CPU時間。
#include <sys/times.h>
    clock_t times(struct tms *buf );
        Returns: elapsed wall clock time in clock ticks if OK, −1 on error
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函式返回牆上時鐘時間作為其函式值。此值相對於過去的某一時刻度量的。前後使用相減就是時間差。