UNIX進程控制

 1. 交換進程與init進程

  進程ID0是調度進程,常常被稱爲交換進程(swapper)。該進程並不執行任何磁盤上的程序。它是內核的一部分,因此也被稱爲系統進程。
  進程ID1通常是init進程,在自舉過程結束時由內核調用(swapper進程創建一個內核線程,然後exec來執行init)。該進程的程序文件/sbin/init。此進程負責在內核自舉後啓動一個UNIX系統。init通常讀與系統有關的初始化文件(/etc/rc*),並將系統引導到一個狀態(例如多用戶)。雖然init是一個普通的用戶進程(swapper進程是內核的系統進程而非用戶進程),但init進程決不會終止。但是它以超級用戶特權運行。
  在某些UNIX的虛存實現中,進程PID=2是頁精靈進程(page daemon)。此進程負責支持虛存系統的請頁操作。與交換進程一樣,頁精靈進程也是內核進程。
 
2. 下列函數返回相關標識符:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 返回:調用進程的進程I D
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
 
3. fork函數
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void); //返回:子進程中爲0,父進程中爲子進程PID,出錯爲-1
    由fork創建的新進程被稱爲子進程(child process)。該函數被調用一次,但返回兩次。兩次返回的區別是子進程的返回值是0,而父進程的返回值則是新子進程的進程PID。將子進程PID返回給父進程的理由是:因爲一個進程的子進程可以多於一個,所以沒有一個函數使一個進程可以獲得其所有子進程的進程ID。fork使子進程得到返回值0的理由是:一個進程只會有一個父進程,所以子進程總是可以調用getppid以獲得其父進程的進程PID(PID=0的進程總是由交換進程使用,所以一個子進程的進程PID不可能爲0)。
    子進程獲得父進程數據空間、堆和棧的複製品。注意,這是子進程所擁有的拷貝。父、子進程並不共享些存儲空間部分。如果正文段是隻讀的,則父、子進程共享正文段。現代很多實現並不做一個父進程數據段和堆的完全拷貝。因爲fork之後常常跟着exec,所以作爲代替使用了寫時複製(copy on write, COW)技術。這些區域由父、子進程共享,而內核將它們設爲只讀。如果有進程試圖修改這些區域,則內核爲有關部分,典型的是虛存系統中的“頁”,做一個拷貝。
    Linux 2.4.22以上的版本提供了一個新進程的系統調用clone,它是一個fork的泛型允許調用者控制哪些部分由父子進程共享。
    一般來說,fork之後父進程與子進程哪個先執行時不確定的,取決於內核使用的調度算法。如果要求父子進程之間的同步,則需要某種形式IPC機制.
    fork的一個特性是父進程的所有打開文件描述符都被複制到子進程中。但是父子進程每個相同的文件描述符共享一個文件表項(如圖所示)。這種機制使得父子進程使用同一個文件偏移量。例如假如子進程在fork之後先執行並寫入某個父進程打開的文件描述符中,那麼之後父進程寫入該描述符的數據就會在子進程寫入數據之後(在父進程wait等待子進程結束的情形)。如果父子進程並行執行,那麼應該在fork之後關閉它們不使用的描述符避免造成混亂(網絡服務進程中經常使用)。
 

    其他在fork之後繼承的內容:
• UID、GID、EUID、EGID。
• 添加組ID。
• 進程組ID。
• 對話期ID。
• 控制終端。
• SUID和SGID。
• 當前工作目錄。
• 根目錄。
• 文件方式創建屏蔽字。
• 信號屏蔽和排列。
• 對任一打開文件描述符的在執行時關閉close-on-exec標誌。這個標誌符的具體作用在於當開闢其他進程調用exec()族函數時,在調用exec函數之前爲exec族函數釋放對應的文件描述符。
• 環境。
• 連接的共享存儲段。
• 資源限制。
父、子進程之間的區別是:
• fork的返回值。
• 進程ID。
• 不同的父進程ID。
• 子進程的tmsutime,tmsstime , tmscutime以及tmsustime設置爲0。
• 父進程設置的鎖,子進程不繼承。
• 子進程的未決告警(alarm)被清除。信號的”未決“是一種狀態,指的是從信號的產生到信號被處理前的這一段時間。
• 子進程的未決信號集設置爲空集。
fork常見的兩種用法:
(1) 一個父進程希望複製自己,使父、子進程同時執行不同的代碼段。這在網絡服務進程中是常見的——父進程等待委託者的服務請求。當這種請求到達時,父進程調用fork,使子進程處理此請求。父進程則繼續等待下一個服務請求。
(2) 一個進程要執行一個不同的程序。這對s h e l l是常見的情況。在這種情況下,子進程在從fork返回後立即調用exec。
 
4.  vfork
    vfork函數的調用序列和返回值與fork相同,用於創建一個新進程,而該新進程的目的是exec一個新程序。它並不將父進程的地址空間完全複製到子進程中,因爲子進程會立即調用exec(或exit)。是也就不會存訪該地址空間。不過在子進程調用exec或exit之前,它在父進程的空間中運行。所以子程序在exec之間改變上面程序中的變量就會實際影響到父進程的變量。
    vfork和fork之間的另一個區別是: vfork保證子進程先運行,在它調用exec或exit之後父進程纔可能被調度運行。(如果在調用這兩個函數之前子進程依賴於父進程的進一步動作,則會導致死鎖。)
 
  1. #include "apue.h" 
  2.  
  3. int glob = 6;       /* external variable in initialized data */ 
  4.  
  5. int 
  6. main(void
  7.     int     var;        /* automatic variable on the stack */ 
  8.     pid_t   pid; 
  9.     var = 88; 
  10.  
  11.     printf("before vfork\n");   /* we don't flush stdio */ 
  12.  
  13.     if ((pid = vfork()) < 0) { 
  14.         err_sys("vfork error"); 
  15.     } else if (pid == 0) {      /* child */ 
  16.         glob++;                 /* modify parent's variables */ 
  17.         var++; 
  18.         _exit(0);        
  19.     }/* child terminates */ 
  20.  
  21.     /* 
  22.      * Parent continues here. 
  23.      */ 
  24.     printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var); 
  25.     exit(0); 
執行程序
$ a.out
before vfork
pid = 607, glob = 7, var = 89
由此可見子進程的執行改變了父進程空間裏的變量。如果將_exit改成exit,
$a.out
before vfork
從中可見,父進程printf的輸出消失了。其原因是子進程調用了exit,它刷新開關閉了所有標準I/O流,這包括標準輸出。雖然這是由子進程執行的,但卻是在父進程的地址空間中進行的,所以所有受到影響的標準I/O FILE對象都是在父進程中的。當父進程調用printf時,標準輸出已被關閉了,於是printf返回-1。
 
5. 進程終止
進程終止有三種正常終止法和兩種異常終止法:
(1). 正常終止:
(a). 在main函數內執行return語句。這等效於調用exit。
(b). 調用exit函數。此函數由ISO C定義,其操作包括調用各終止處理程序(終止處理程序在調用atexit函數時登錄),然後關閉所有標準I/O流等。因爲ISO C並不處理文件描述符、多進程(父、子進程)以及作業控制,所以這一定義對UNIX系統而言是不完整的。
(c). 調用_exit系統調用函數。此函數由exit調用,它處理UNIX特定的細節。並不flush標準I/O流。_exit系統調用由exit來調用。_Exit爲進程提供一種無需運行終止處理程序或信號處理程序而終止的方法。
(d). 進程的最後一個線程在其啓動例程中執行返回語句,但是線程的返回值不會用作進程的返回值。當最後一個線程從其啓動例程返回時,該進程以終止狀態0返回。
(e). 進程的最後一個線程調用pthread_exit。進程的終止狀態依然與線程的pthread_exit無關,進程終止狀態總是0。
(2) 異常終止:
(a) 調用abort。它產生SIGABRT信號,所以是下一種異常終止的一種特例。
(b) 當進程接收到某個信號時。如進程本身(例如調用abort函數)、其他進程和內核都能產生傳送到某一進程的信號。例如,進程越出其地址空間訪問存儲單元,或者除以0,內核就會爲該進程產生相應的信號。
    無論進程如何終止,最後都會執行內核中同一段代碼,這段代碼爲相應進程關閉打開的文件描述符,釋放它所使用的寄存器。並向它的父進程發送一個SIGCHLD,系統默認的處理動作是忽略該信號。
    注意,這裏使用了“退出狀態”(它是傳向exit或_exit的參數,或main的返回值。它們將作爲參數傳遞給三個終止函數exit, _exit, Exit)和“終止狀態”兩個術語,以表示有所區別。在最後調用_exit時內核將其退出狀態轉換成終止狀態。如果子進程正常終止,則父進程可以通過wait和waitpid獲得子進程的退出狀態。
    如果父進程在子進程之前終止,由init進程領養這些孤兒進程成爲它們新的父進程。具體過程是當一個進程終止時,內核會逐一檢查所有活動的進程,一判斷是否是正要終止進程的子進程。如果是,則將它們的ppid改爲1。這樣就確保每一個進程都有一個父進程。
    如果子進程在父進程之前終止,內核爲每個終止子進程保存了一定量的信息。(例如Linux的隊列中task_struct結構存在只是狀態爲Zombie)。父進程調用wait或waitpid就可以得到這些信息。包括PID,終止狀態,使用的CPU時間等等。然後內核會釋放終止進程的所有最後的空間。
    如果一個由init進程領養的子進程終止時,它不可能變爲zombie進程。因爲init設計在任何時候有一個子進程終止,它就會及時調用一個wait函數獲得其終止狀態,這樣就避免了大量zombie進程的產生。
    wait和waitpid函數:
#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就立即返回。因爲wait返回終止子進程的PID,所以它總能瞭解是哪一個子進程終止了。
    int *statloc如果不是一個空指針,則它返回終止進程的終止狀態。如果不關心終止狀態可以將其設爲空指針。關於終止狀態可以通過sys/wait.h下定義的一些宏來輔助判斷。
    waitpid的pid參數可以設置如下:-1等待任一子進程(同wait)。大於0表示等待與pid相等的子進程。0表示子進程組ID等於父進程組ID的任一子進程。小於-1表示子進程組ID等於pid絕對值的任一子進程。
    waitpid的options可以爲0或者下列或運算的結果,waitpid支持作業控制而wait不支持。
WNOHANG 若由pid指定的子進程並不立即可用,則waitpid不阻塞,此時其返回值爲0
WUNTRACED若該實現支持作業控制,則由pid指定的任一子進程狀態已暫停,且其狀態自暫停以來還未報告過,則返回其狀態。WIFSTOPPED宏確定返回值是否對應於一個暫停子進程。
WCONTINUED若實現作業控制,那麼由pid指定的任一子進程在暫停後已經積蓄,但其狀態未報告。則返回其狀態。
    另外的擴展:waitid(Linux不支持), wait3和wait4,更靈活。wait3和wait4還返回了所有終止進程與其子進程的作業彙總。
 
6. race condition

 

    多個進程都企圖對共享數據進行某種處理,而最後的結果又取決於進程運行的順序,這就發生了競爭條件。如果fork之後某種邏輯顯式或者隱式依賴於fork之後父進程和子進程的執行順序(這個順序不可預知,由內核調度決定),也是race condition經常發生的情形。
 
 
    如果一個進程希望等待一個子進程終止,則它必須調用wait函數。如果一個進程要等待其父進程終止,則可使用下列形式的循環:
 
 
while(getppid() !=1)
 
sleep(1);
    這種形式的循環(稱爲定期詢問(polling))的問題是它浪費了C P U時間,因爲調用者每隔1秒都被喚醒,然後進行條件測試。
    而通常的的實現是用信號和IPC機制。在子進程中實現TELL_PARENT(),WAIT_PARENT()函數/宏,父進程裏實現WAIT_CLILD和TELL_CHILD宏。
 
7. exec
    當進程調用一種exec函數時,該進程完全由新程序代換,而新程序則從其main函數開始執行。因爲調用exec並不創建新進程,所以前後的進程ID並未改變。
    exec, fork,wait和exit都是基本的進程控制原語,後面的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 [] );
/*六個函數返回:若出錯則爲-1,若成功則不返回*/
    後兩個如果包含/則被認爲是一個路徑名,否則則是文件名在PATH環境變量中查找相應的可執行文件。這個可執行文件也可以是一個解釋器文件(#!)。
    這幾個函數中l表示list,v表示向量。list的execl,execle,execlp表示參數的是可變參數列表形式,最後要跟一個空指針表示結束(char *)0
    以e結尾的兩個函數(execle和execve)可以傳遞一個指向環境字符串指針數組的指針。其他四個函數則使用調用進程中的environ變量爲新程序複製現存的環境。(如果系統支持putenv和setenv也可以在後面生成的子進程中修改,但不能影響父進程的環境)    
使用/顯示當前進程的所有環境變量:
for (char **ptr = environ; *ptr != 0; ptr++)
printf("%s\n", *ptr);
    在執行exec後,進程ID沒有改變。除此之外,執行新程序的進程還保持了原進程的下列特徵:
• 進程ID和父進程ID。
• 實際用戶ID和實際組ID。
• 添加組ID。
• 進程組ID。
• 對話期ID。
• 控制終端。
• 鬧鐘尚餘留的時間。
• 當前工作目錄。
• 根目錄。
• 文件方式創建屏蔽字。
• 文件鎖。
• 進程信號屏蔽。
• 未決信號。
• 資源限制。
• tms_utime, tms_stime, tms_cutime以及tmsustime值。
    對打開文件的處理與每個描述符的exec關閉標誌值(FDCLOEXEC)有關。若此標誌設置,則在執行exec時關閉該描述符,否則該描述符仍打開。除非特地用f c n t l設置了該標誌,否則系統的默認操作是在exec後仍保持這種描述符打開。
 
8. setuid(設置真實的UID)getuid,setreuid,seteuid,setfsuid
#include<unistd.h>
int setuid(uid_t uid)
//執行成功則返回0,失敗則返回-1,錯誤代碼存於errno。
    setuid()用來重新設置執行目前進程的UID。當有效的UID必須爲0(root)時。在Linux下,當root 使用setuid()來變換成參數中的UID時(EUID和UID同時變爲參數中的uid_t uid),也就是說,該進程往後將不再具有可setuid()的權利,如果只是向暫時拋棄root權限,稍後想重新取回權限,則必須使用seteuid()。如果是非root用戶使用setuid這個函數,它只能用來
    一般在編寫具setuid root的程序時,爲減少此類程序帶來的系統安全風險,在使用完root權限後建議馬上執行setuid(getuid());來拋棄root權限。此外,進程uid和euid不一致時Linux系統將不會產生core dump
 
9. system函數:執行一個字符串命令。
#include <stdlib.h>
int system(const char* cmdstring);
在實現中調用了fork,exec和waitpid。
 
10. process accounting
    當取了這種選擇項後,每當進程結束時內核就寫一個會計記錄。典型的會計記錄是3 2字節長的二進制數據,包括命令名、所使用的CPU時間總量、用戶ID和組ID、起動時間等。
    會計記錄所需的各個數據(各C P U時間、傳輸的字符數等)都由內核保存在進程表中,並在一個新進程被創建時置初值(例如fork之後在子進程中,內核爲子進程初始化一個記錄,而不是在新程序exec時。所以會計記錄對應的是進程而不是程序。)。進程終止時寫一個會計記錄。這就意味着在會計文件中記錄的順序對應於進程終止的順序,而不是它們起動的順序。爲了確定起動順序,需要讀全部會計文件,並按起動日曆時間進行排序。
#include <sys/type.h>
#include <sys/acct.h>
 
#define ACCFILE "/var/adm/pacct"
......
struct acct acdata;
......
if ( (fp = fopen(ACCTFILE, "r")) == NULL)
err_sys("can't open file", ACCTFILE);
 
while (fread(&acdata, sizeof(acdata), 1, fp) == 1) {
printf(......acdata.ac_comm...ac.etime)
.......
}
 
10. 獲得而用戶登陸名,而非用戶標誌。
#include <unistd.h>
char *getlogin(void);
得到了登錄名,就可用getpwnam在口令文件中查找相應記錄以確定其登錄shell等。
 
11. 進程時間
#include <sys/times.h>
clock_t times(struct tms *buf);
獲得一個起始時間和一個終止時間,然後經過static long clktck=0; clktck=sysconf(_SC_CLK_TCK); start_times->tms_utime/(double)clktck。
struct tms {
clock_t tms_utime; /* 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 */
}
 
 

 

Textbook:
APUE

 

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