進程控制

進程標識

  每個進程都有一個非負整形表示的惟一進程ID。由於進程ID標識符老是惟一的,常將其用來做其餘標識符的一部分以保證其惟一性。例如,應用程序有時就把進程ID做爲名字的一部分來建立一個惟一的文件名。
  雖然是惟一的,可是進程ID是能夠複用的。當一個進程終止後,其進程ID就成爲複用的候選者。大多數UNIX系統實現延遲複用算法,使得賦予新建進程的ID不一樣於最近終止進程所使用的的ID。
系統中有一些專用進程。ID爲0的一般是調度進程,經常被稱爲交換進程(swapper)。該進程是內核的一部分,他並不執行任何磁盤上的程序,所以也被稱爲系統進程。進程ID 1一般是init進程,在自舉過程結束時由內核調用。該進程的程序文件在UNIX的早期版本中是/etc/init,在較新的版本中是/sbin/init。此進程負責在在自舉內核後啓動一個UNIX系統。init一般讀取與系統有關的初始化文件(/etc/rc*文件或者/etc/inittab文件,以及在/etc/init.d中的文件),並將系統引導到一個狀態(如多用戶)。init進程決不會終止。他是一個普通的用戶進程,可是以超級用戶特權運行。
  每一個UNIX系統實現都有他本身的一套提供操做系統服務的內核進程。例如,在某些UNIX的虛擬存儲實現中,進程ID 2是頁守護進程(page daemon),此進程負責支持虛擬存儲器系統的分頁操做。linux

#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

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

  由fork建立的新進程被稱爲子進程(child process)。fork函數被調用一次,但返回兩次。兩次返回的區別是子進程的返回值是0,而父進程的返回值是新建子進程的進程ID。
  子進程和父進程繼續執行fork調用以後IDE指令。子進程是父進程的副本。例如,子進程得到父進程數據空間、堆和棧的副本。注意,這是子進程所擁有的副本。父進程和子進程並不共享這些存儲空間部分。父進程和子進程共享正文段。 因爲在fork以後常常跟隨着exec,因此如今的不少實現並不執行一個父進程數據段、站和堆的徹底副本。做爲替代,使用了寫時複製(Copy-On_Write,COW)技術。這些區域由父進程和子進程共享,並且內核將他們的訪問權限改變爲只讀。若是父進程和子進程中任一個試圖修改這些區域,則內核只爲修改區域的那塊內存製做一個副本,一般是虛擬存儲系統中的一「頁」。算法

#include <stdio.h>
#include <unistd.h>

int global_var = 6;
char buf[] = "a write to stdout\n";

int main(void)
{
    int var;
    pid_t pid;

    var = 88;
    if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
    {
            printf("write error!\n");
    }
    printf("before fork\n");

    if ((pid = fork()) < 0)
    {
        printf("fork error!\n");
    } else if (pid == 0) {
        global_var++;
        var++;
        printf("ppid = %ld\n", (long)getppid());
        } else {
        sleep(2);
        }

    printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), global_var, var);
    return 0;
}

  能夠看到當咱們將輸出重定向到temp.out文件後多出個before fork的輸出。write函數是不帶緩存的。由於在fork以前調用write,因此其數據寫到標準輸出一次。可是標準IO是帶緩存的。若是標準輸出連到終端設備,則它是行緩存,不然它是全緩存。當以交互方式運行該程序時,只獲得printf輸出的行一次,其緣由是標準輸出緩存由新行符刷新。當咱們將printf("before fork \n");後的換行符去掉以後即printf("before fork");來驗證這一點,修改以後輸出結果是:shell

  能夠看到before fork打印了兩次,這說明由於咱們去掉了換行符因此標準輸出流的行緩存不會被flush。數組

  可是當將標註輸出從新定向到一個文件時,卻獲得printf輸出行兩次。其緣由是,將標準輸出從新定向到一個文件時標準輸出流就不是行緩存而是全緩存了,在fork以前調用了printf一次,但當調用fork時,該行數據仍在緩存中,而後在父進程數據空間複製到子進程的過程當中時,該緩存數據也被複制到了子進程中。因而那時父、子進程各自有了帶該行內容的緩存。在exit以前的第二個printf將其數據添加到現存的緩衝中。當每一個進程終止時,緩存中的內容將被寫到相應文件中。緩存

文件共享

  對於上面的程序須要注意:在重定向父進程的標準輸出時也重定向了子進程的標準輸出。fork的一個特性是全部由父進程打開的文件描述符都被複制到子進程中。父、子進程每一個相同的打開文件描述符共享一個文件表項。
  這種共享文件的方式使父子進程對同一文件使用了一個文件位移量。對於如下狀況:網絡

  一個進程fork了一個子進程,而後等待子進程終止。假定,做爲普通處理的一部分,父、子進程都向標準輸出執行寫操做。若是父進程使其標準輸出重定向(極可能是由shell實現的),那麼子進程寫到該標準輸出時,他將更新與父進程共享的該文件的位移量。在咱們所考慮的例子中,當父進程等待子進程時,子進程寫到標準輸出;而在子進程終止後,父進程也寫入到標準輸出上,而且知道其輸出會添加在子進程所寫數據以後。若是父、子進程不共享同一文件位移量,這種形式的交互就很難實現。app

  若是父、子進程寫到同一文件描述符文件,但又沒有任何形式的同步(例如使父進程等待子進程),那麼它們的輸出就會相互混合(假定所用的文件描述符是在fork以前打開的)。
  在fork以後處理文件描述符有兩種常見的狀況:ide

  1. 父進程等待子進程完成。這種狀況下,父進程無需對其描述符作任何處理。
  2. 父、子進程各自執行不一樣的程序段。在這種狀況下,在fork以後,父、子進程各自它們不需使用的文件描述符,而且不干擾對方使用的文件描述符。

    除了打開文件以外,不少父進程的其餘性質也會由子進程繼承:函數

  • 實際用戶ID、實際組ID、有效用戶ID、有效組ID。
  • 添加組ID。
  • 進程組ID。
  • 對話期ID。
  • 控制終端。
  • 設置-用戶-ID標誌和設置-組-ID標誌。
  • 當前工做目錄。
  • 根目錄。
  • 文件方式建立屏蔽字。(umask)
  • 信號屏蔽和排列。
  • 對任一打開文件描述符的在執行時關閉標誌。
  • 環境。
  • 鏈接的共享存儲段。

  父、子進程之間的區別是:測試

  1. fork的返回值。
  2. 進程ID
  3. 不一樣的父進程iD。
  4. 子進程的tms_utime,tms_stime,tms_cutime以及tms_ustime設置爲0。
  5. 父進程設置的鎖,子進程不繼承。
  6. 子進程的未決告警被清除。
  7. 子進程的未決信號集設置被清除。

使用fork失敗的緣由主要有兩個:

  1. 系統中已經有了太多的進程
  2. 該實際用戶ID的進程總數超過了系統限制。

fork有如下兩種用法:

  1. 一個父進程但願複製本身,使父進程和子進程同時執行不一樣的代碼段。這在網絡服務進程中是常見的–父進程等待客戶端服務請求。當這種請求到達時,父進程調用fork,使子進程處理此請求。父進程則繼續等待下一個服務請求。
  2. 一個進程要執行一個不一樣的程序。這對shell是常見的狀況。在這種狀況下,子進程從fork返回後當即調用exec。 

  有些操做系統將第二種用法的兩個操做組合成一個操做,稱爲spawn。UNIX系統將這兩個操做分開,由於在不少場合須要單獨使用fork,其後並不更隨exec。另外,將這兩個操做分開,使得子進程在fork和exec之間能夠更改本身的屬性,如I/O重定向,用戶ID、信號安排等。

vfork

  vfork函數的調用序列和返回值與fork相同,但二者的語義不一樣。
  vfork函數用於建立一個新進程,而該進程的目的是exec一個新程序。vfork與fork同樣都建立一個子進程,可是他並不將父進程的地址空間徹底複製到子進程中由於子進程會當即調用exec(或exit),因而也就不會引用該地址空間。不過在子進程調用exec或exit以前,他在父進程的空間中進行。這種優化工做方式在某些UNIX系統的實現中提升了效率,但若是子進程修改數據、進行函數調用、或者沒有調用exec或exit就返回均可能帶來未知的結果。
  vfork和fork之間的另外一個區別是:vfork保證子進程先運行,在它調用exec或exit以後父進程纔可能被調度運行(若是在調用exec/exit以前子進程依賴於父進程的進一步動做,則會致使死鎖)。當子進程調用這兩個函數中的任意一個時,父進程會恢復運行。

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int glob = 6;
int main(void)
{
    int var;
    pid_t pid;

    var = 88;
    printf("before vfork\n");

    if ((pid = vfork()) < 0) {
        fprintf(stderr, "vfork error\n");
    } else if (pid == 0) {
        glob++;
        var++;
        _exit(0);
    }

    printf("pid=%d,glob=%d,var=%d\n", getpid(), glob, var);

    return 0;
}

  調用了_exit而不是exit。_exit並不執行IO緩存的刷新操做。若是是調用exit而不是_exit,則該程序的輸出是:

  可見父進程的printf輸出消失了。其緣由:子進程調用了exit,它刷新開關閉了全部標準IO流,這包括標準輸出。雖然這是由子進程執行的,但倒是在父進程的地址空間中進行的,因此全部受到影響的標準IO FILE對象都是在父進程中。當父進程調用prinf時,標準輸出已經被關閉了,因而printf返回-1。

  可是,在本身的linux系統上實驗時,仍是有print輸出。

  之因此結果不一樣是由於在linux中子進程關閉的是本身的, 雖然他們共享標準輸入、標準輸出、標準出錯等 「打開的文件」, 子進程exit時,也不過是遞減一個引用計數,不可能關閉父進程的,因此父進程仍是有輸出的。

 wait和waitpid

  當一個進程正常或異常終止時會向父進程發送SIGCHLD信號。對於這種信號系統默認會忽略。調用wait/waidpid的進程可能會:

  1. 阻塞(若是其子進程都還在運行);
  2. 當即返回子進程的終止狀態(若是一個子進程已經終止正等待父進程存取其終止狀態);
  3. 出錯當即返回(若是它沒有任何子進程);(若是進程因爲收到SIGCHLD信號而調用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返回子進程ID,因此調用者知道是哪一個子進程終止了。
  參數statloc是一個整型指針。若是statloc不是一個空指針,則終止狀態就存放到它所指向的單元內。若是不關心終止狀態則將statloc設爲空指針。
  這兩個函數返回的整型狀態由實現定義。其中某些位表示退出狀態(正常退出),其餘位則指示信號編號(異常返回),有一位指示是否產生了一個core文件等等。POSIX.1規定終止狀態用定義在<sys/wait.h>中的各個宏來查看。有三個互斥的宏可用來取得進程終止的緣由,它們的名字都已WIF開始。基於這三個宏中哪個值是真,就可選用其餘宏(這三個宏以外的其餘宏)來取得終止狀態、信號編號等。

  wait是隻要有一個子進程終止就返回,waitpid能夠指定子進程等待。對於waitpid的pid參數:

  • pid == -1, 等待任一子進程。這時waitpid與wait等效。
  • pid > 0, 等待子進程ID爲pid。
  • pid == 0, 等待其組ID等於調用進程的組ID的任一子進程。
  • pid < -1 等待其組ID等於pid的絕對值的任一子進程。

  對於wait,其惟一的出錯是沒有子進程(函數調用被一個信號中斷,也可能返回另外一種出錯)。對於waitpid, 若是指定的進程或進程組不存在,或者調用進程沒有子進程都能出錯。options參數使咱們能進一步控制waitpid的操做。此參數或者是0,或者是下表中常數的逐位或運算。

  1. WCONTINUED:若實現支持做業控制,那麼由pid指定的任一子進程在中止後已經繼續,但其狀態還沒有報告,則返回其狀態(POSIX.1的XSI擴展)
  2. WNOHANG:沒有已終止的子進程時,則waitpid不阻塞。此時其返回值爲0
  3. WUNTRACED:若某實現支持做業控制,而由pid指定的任一子進程已處於中止狀態,而且其狀態自中止依賴還未報告過,則返回其狀態,WIFSTOPPED宏肯定返回值是否對應於一箇中止的子進程

waitid

#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
//返回值:成功返回0;出錯,返回-1
  • 與waitpid類似,waitid容許一個進程指定要等待的子進程,但它使用兩個單獨的參數表示要等待的子進程所屬的類型,而不是將此與進程ID或進程組ID組合成一個參數。
  • id參數的做用域idtype的值相關

idtype

P_PID 等待一特定進程,id包含要等待子進程的進程ID
P_PGID 等待一特定進程組的任一子進程;id包含要等待子進程的進程組ID
P_ALL 等待任一子進程;忽略id

options

  下面各標誌的按位或運算,指示調用者關注哪些狀態變化
  WCONTINUED、WEXITED、WSTOPPED這3個常量之一必須在options參數中指定

  1. WCONTINUED:等待一進程,它之前曾被中止,此後又已繼續,但其狀態還沒有報告
  2. WEXITED:等待已退出的進程
  3. WNOHANG:如無可用的子進程退出狀態,當即返回而非阻塞
  4. WNOWAIT:不破壞子進程退出狀態。該子進程退出狀態可由後續的wait、waitid、waitpid調用取得
  5. WSTOPPED:等待一進程,它已經中止,但其狀態還沒有報告

  Linux 3.2.0、Mac OS X 10.6.八、Solaris 10支持waitid。Mac OS X 10.6.8並無設置siginfo結構中的全部信息

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);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
 
//返回值:成功返回進程ID。出錯,返回-1

  大多數UNIX系統提供了另外兩個函數wait3和wait4。這兩個函數是從UNIX系統的BSD分支延襲下來的。它們提供的功能比POSIX.函數wait和waitpid、waitod所提供的分別要多一個,這與附加參數rusage有關。該參數要求內核返回由終止進程及其全部子進程使用的資源概況
  資源信息包括用戶CPU時間總量、系統CPU時間總量、缺頁次數、接收到信號的次數等

  有關細節請參閱getrusage(2)手冊頁。這些資源信息只包括終止子進程,並不包括處於中止狀態的子進程(這種資源信息與7.11節中所述的資源限制不一樣)

 競態調件

  當多個進程都企圖對某共享數據進行某種處理,而最後的結果又取決於進程運行的順序,則咱們認爲這發生了競態條件(race condition)。若是在fork以後的某種邏輯顯式或隱式地依賴於在fork以後是父進程先運行仍是子進程先運行,那麼fork函數就會是競態條件活躍的孽生地。
  若是一個進程但願等待一個子進程終止,則它必須調用wait函數。若是一個進程要等待其父進程終止,則可以使用下列形式的循環:

while(getppid() != 1)
    sleep(1);

  這種形式的循環(稱爲按期詢問(polling))的問題是它浪費了CPU時間,由於調用者每隔1秒都被喚醒,而後進行條件測試。
  爲了不競態條件和按期詢問,在多個進程之間須要有某種形式的信號機制。在UNIX中可使用信號機制,各類形式的進程間通訊(IPC)也可以使用。
  在父、子進程的關係中,經常有如下狀況:在fork以後,父、子進程都有一些事情要作。例如:父進程可能以子進程ID更新日誌文件中的一個記錄,而子進程則可能要爲父進程建立一個文件。在本例中,要求每一個進程在執行完它的一套初始化操做後要通知對方,而且在繼續運行以前,要等待另外一方完成其初始化操做。這種狀況能夠描述爲以下:

TELL_WAIT();

if ((pid = fork()) < 0) {
    err_sys("fork error");
} else if (pid == 0) {
    TELL_PARENT(getppid());
    WAIT_PARENT();
    exit(0);
}

TELL_CHILD(pid);
WAIT_CHILD();
exit(0);

exec

  當進程調用exec函數時,該進程徹底由新進程代換,而新程序則從其main函數開始執行。由於調用exec並不建立新進程,因此先後的進程ID不會改變(父子進程這種關係也不會變)。exec只是用另外一個程序替換了當前進程的正文、數據、堆和棧段。

#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環境變量。

  若是excelp和execvp中的任意一個使用路徑前綴中的一個找到了一個可執行文件,可是該文件不是機器可執行代碼文件,則就認爲該文件是一個shell腳本,因而試着調用/bin/sh,並以該filename做爲shell的輸入。
  第二個區別與參數表的傳遞有關(l 表示表(list),v 表示矢量(vector))。函數execl、execlp和execle要求將新程序的每一個命令行參數都說明爲一個單獨的參數。這種參數表以空指針結尾。另外三個函數execv,execvp,execve則應先構造一個指向個參數的指針數組,而後將該數組地址做爲這三個函數的參數。
  最後一個區別與向新程序傳遞環境表相關。以 e 結尾的兩個函數excele和exceve能夠傳遞一個指向環境字符串指針數組的指針。其餘四個函數則使用調用進程中的environ變量爲新程序複製現存的環境。
  六個函數之間的區別:

  每一個系統對參數表和環境表的總長度都有一個限制。當使用shell的文件名擴充功能產生一個文件名錶時,可能會收到此值的限制。例如,命令:

grep _POSIX_SOURCE /usr/include/*/*.h

  在某些系統上可能產生下列形式的shell錯誤:arg list too long

  執行exec後進程ID沒改變。除此以外,執行新程序的進程還保持了原進程的下列特徵:

  • 進程ID和父進程ID。
  • 實際用戶ID和實際組ID。
  • 添加組ID。
  • 進程組ID。
  • 對話期ID。
  • 控制終端。
  • 鬧鐘尚餘留的時間。
  • 當前工做目錄。
  • 根目錄。
  • 文件方式建立屏蔽字。
  • 文件鎖。
  • 進程信號屏蔽。
  • 未決信號。
  • 資源限制。
  • tms_utime,tms_stime,tms_cutime以及tms_ustime值。

  對打開文件的處理與每一個描述符的exec關閉標誌值有關。進程中每一個打開描述符都有一個exec關閉標誌。若此標誌設置,則在執行exec時關閉該文件描述符,不然該描述符仍打開。除非特意用fcntl設置了該標誌,不然系統的默認操做是在exec後仍保持這種描述符打開。
  POSIX.1明確要求在exec時關閉打開目錄流。這一般是由opendir函數實現的,它調用fcntl函數爲對應於打開目錄流的描述符設置exec關閉標誌。
  在exec先後實際用戶ID和實際組ID保持不變,而有效ID是否改變則取決於所執行程序的文件的設置-用戶-ID位和設置-組-ID位是否設置。若是新程序的設置-用戶-ID位已設置,則有效用戶ID變成程序文件的全部者的ID,不然有效用戶ID不變。對組ID的處理方式與此相同。

  在不少UNIX實現中,這六個函數只有一個execve是系統調用。另外5個是庫函數

更改用戶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的規則。

  • 若進程具備root特權,則setuid函數將實際用戶ID、有效用戶ID,以及保存的設置-用戶-ID設置爲uid。
  • 若進程沒有root權限,可是uid等於實際用戶ID或保存的設置-用戶-ID,則setuid只將有效用用戶ID設置爲uid。不改變實際用戶ID和保存的設置-用戶-ID。
  • 若是上面兩個條件都不知足,則errno設置爲EPERM,並返回出錯。

   在這裏假定_POSIX+_SAVED_IDS爲真。若是沒有提供這種功能,則上面所說的關於保存的設置-用戶-ID部分都無效。
  關於內核所維護的三個用戶ID,還要注意如下:

  • 只有root用戶能夠修改實際用戶ID。一般,實際用戶ID是在用戶登陸時,由login程序設置的,並且毫不會改變它。由於login進程是一個root進程,當它調用setuid時,設置全部三個用戶ID。
  • 僅當對程序文件設置了設置-用戶-ID位時,exec函數設置有效用戶ID。任什麼時候候均可以調用setuid,將有效用戶ID設置爲實際用戶ID或保存的設置-用戶-ID。天然,不能將有效用戶ID設置爲任一隨機值。
  • 保存的設置-用戶-ID是由exec從有效用戶ID複製的。在exec按文件用戶ID設置了有效用戶ID後,即進行這種複製,並將此副本保存起來。

  下表列出了改變這三個用戶ID的不一樣方法

 setreuid和setregid

  4.3+BSD支持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);

  其做用是一個非特權用戶總能交換實際用戶ID和有效用戶ID。這就容許一個設置-用戶-ID程序轉換成只具備用戶的普通權限,之後又可再次切換回設置-用戶-ID所獲得大的額外權限。

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,添加組ID不受setgid函數的影響。

解釋器文件

  解釋器文件就是linxu中的shell腳本。這種文件是文本文件,其起始行的形式是:

#! pathname [optional-argument]

  在感嘆號和pathname之間的空格是可任選的。最多見的是如下列行開始:

#! /bin/sh

  pathname一般是個絕對路徑名,對它不進行什麼特殊的處理(不適用PATH進行路徑搜索)。
  不少系統對解釋器文件第一行有長度限制(32個字符)。這包括#!、pathname、可選參數以及空格數。

system

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

  若是cmdstring是一個空指針,則僅當命令處理程序可用時,system返回非0,這一特徵能夠肯定在一個給定的操做系統上是否支持system函數。在UNIX中,system老是可用的。
  由於system函數在實現中調用了fork、exec和waitpid,所以有3種返回值。

  1. fork失敗或者waitpid返回除EINTR以外的出錯,則system返回-1,而且設置errno以指示錯誤類型。
  2. 若是exec失敗,則其返回值如同shell執行了exit。 (3)不然全部3個函數都成功,那麼system的返回值是shell的終止狀態,其格式在waitpid中說明。

一下是system的一種實現:

#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>

int system(const char *cmdstring)
{
    pid_t pid;
    int status;
    if (NULL == cmdstring)
    {
        return 1;
    }

    if ((pid = fork()) < 0)
    {
        status = -1;
    }
    else if (pid == 0) {
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        //execl("/home/dang/WorkSpace/test","xxxxxx","yyyyyy",(char*)0);
        _exit(127);
    } 
    else 
    { 
        while ((waitpid(pid, &status, 0)) < 0) 
        {
            if (errno != EINTR)
            {
                status = -1;
                break;
            }
        }
        printf("parent process\n");
    }

    return status;
}

int main(int argc, char **argv)
{
    if (system("date > file")<0)
    {
        puts("cmd is failed");
    }
    else
    {
        puts("cmd is success");
    }

    puts("main done");
    return 0;
}
View Code

  shell的-c選項告訴shell程序讀取下一個命令行參數(在這裏是cmdstring)做爲命令輸入。shell對以null字節終止的命令字符串進行語法分析,將他們分紅命令行參數。傳遞給shell的實際命令字符串能夠包括任一有效的shell命令。例如,能夠用<和>歲輸入和輸出重定向。
  首先在調用system函數時,若是出錯,則掉用exit函數退出,以下代碼測試爲:

#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(void)
{
    int status;
    if ((status = system("date")) < 0) 
    {
        printf("system error!\n");
    }

    exit(status);
    //pr_exit(status);

    if ((status = system("nosuchcommand")) < 0) 
    {
        printf("system error!\n");
    }

    exit(status);
    //pr_exit(status);

    if ((status = system("who;exit 44")) < 0) 
    {
        printf("system error!\n");
    }

    exit(status);
    //pr_exit(status);

    exit(0);
}
View Code

  在遇到第二個非法的system命令時,執行失敗,進程退出。 
  在調用system函數時,若是出錯,則調用pr_exit函數,打印出出錯緣由以及狀態,如下爲測試程序: 
pr_exit函數的實現爲:

#include <stdlib.h>
#include <stdio.h>

void pr_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", WTERMSIG(status),  
        #ifdef WCOREDUMP  
                WCOREDUMP(status) ? "(core file generated)" : "");  
        #else  
                "");  
        #endif  
        else if (WIFSTOPPED(status))  
                 printf("child stopped, signal number = %d\n", WSTOPSIG(status));  
} 
View Code

  將該函數編譯成共享庫形式,執行命令:

gcc -O -fpic -shared -o pr_exit.so pr_exit.c

  在主函數中能夠調用該函數,主函數爲:

#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(void)
{
    int status;
    if ((status = system("date")) < 0) 
    {
        printf("system error!\n");
    }

    pr_exit(status);

    if ((status = system("nosuchcommand")) < 0) 
    {
        printf("system error!\n");
    }

    pr_exit(status);

    if ((status = system("who;exit 44")) < 0) 
    {
        printf("system error!\n");
    }
    pr_exit(status);

    exit(0);
}

  編譯時加上該命令

gcc -o systemTest systemTest.c ./pr_exit.so 

  使用system而不直接使用fork和exec的優勢是:system函數進行了各類出錯處理以及各類信號處理

進程會計 

  大多數UNIX系統提供了一個選項以進行進程會計處理。啓用該選項後,每一個進程結束時內核就寫一個會計記錄。典型的會計記錄包含總量較小的二進制數據,通常包括命令名、所使用的CPU時間總量、用戶和組ID、啓動時間等。 

  會計記錄結構定義在頭文件

typedef u_short comp_t;
struct acct
{
    char ac_flag;
    char ac_stat;
    uid_t ac_uid;
    gid_t ac_gid;
    dev_t ac_tty;
    time_t ac_btime;
    comp_t ac_utime;
    comp_t ac_stime;
    comp_t ac_etime;
    comp_t ac_mem;
    comp_t ac_io;
    comp_t ac_rw;
    char ac_comm[8];
};

  會計記錄所需的各個數據(各CPU時間、傳輸的字節數等)都由內核保存早進程表中,並在一個新進程被建立時初始化。進程終止時寫一個會計記錄。這產生進程終止時寫一個會計記錄。這產生兩個後果。

  1. 咱們不能獲取永遠不終止的進程的會計記錄。像init這樣的進程在系統生命週期中一直在運行,並不產生會計記錄。這也一樣適合於內核守護進程,他們一般不會終止。
  2. 在會計文件中記錄的順序對應於進程終止的順序,而不是他們啓動的順序。爲了肯定啓動順序,須要讀所有會計文件,並按照日曆時間進行排序。 

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

用戶標識

  任一進程均可以獲得其實際用戶ID和有效用戶ID及組ID。可是,咱們有時候但願找到運行該程序用戶的登陸名。咱們能夠調用getpwuid。可是若是一個用戶有多個登陸名,這些登陸名又對應着同一個用戶ID,又將如何呢?能夠用getlogin函數能夠獲取登錄此登陸名。

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

  若是調用此函數的進程沒有鏈接到用戶登陸時所用的終端,則函數會失敗。一般稱這些進程爲守護進程(daemon)。 給出了登陸名,就可用getpwnam在口令文件中查找用戶的相應記錄,從而肯定其登陸shell等。

進程調度

  進程能夠經過調整nice值選擇以更低優先級運行。只有特權進程容許提升調度權限。
  nice值越小,優先級越高。NZERO是系統默認的nice值。
  進程能夠經過nice函數獲取或者更改她的nice值。使用這個函數,進程隻影響本身的nice值,不能影響任何其餘進程的nice值。

#include <unistd.h>
int nice(int incr);
返回值:若成功,返回信的nice值NZERO;若出錯,返回-1

  incr參數被增長到調用進程的nice值。若是incr太大,系統直接把他降到最大合法值,不給出提示。相似的,若是incr過小,系統也會無聲息的把他提升到最小合法值。因爲-1是合法的成功返回值,在調用nice函數以前須要清楚errno,在nice函數返回-1 時,須要檢查他的值。若是nice調用成功,而且返回值爲-1,那麼errno任然爲0.若是errno不爲0,說明nice調用失敗。
  getpriority函數能夠像nice函數那樣用於獲取進程的nice值,可是getpriority還能夠獲取一組相關進程的nice值。

#include <sys/resource.h>
int getpriority(int which ,id_t who);   
//返回值:若成功,返回-NZERO~NZERO之間的nice值,若出錯返回-1

  which參數能夠取下面三個值之一:PRIO_PROCESS表示進程,PRIO_PGRP表示進程組,PRIO_USER表示用戶ID。which參數控制who參數是如何解釋的,who參數選擇感興趣的一個或者多個進程。若是who參數爲0,表示調用進程、進程組或者用戶(取決於which參數的值)。當which設爲PRIO_USER並who爲0時,使用調用進程的實際用戶ID。若是which參數做用於多個進程,則返回全部進程中優先級最高的。
  setpriority函數能夠用於爲進程、進程組和屬於特定用戶ID的全部進程設置優先級

#include <sys/resource.h>
int setpriority(int which, id_t who, int value);   
//返回值:若成功,返回0;若出錯,返回-1

  參數which和who與getpriority相同。value增長到NZERO上,而後變爲新的nice值,如下的程序度量了調整nice值的效果。兩個進程並行運行,各自增長本身的計數器。

#include <errno.h>
#include <sys/time.h>
#include <sys/param.h>
#include <stdio.h>
#include <stdlib.h>

unsigned long long count;
struct timeval end;

void checktime(char *str)
{
    struct timeval tv;

    gettimeofday(&tv, NULL);
    if (tv.tv_sec >= end.tv_sec && tv.tv_usec >= end.tv_usec)
    {
        printf("%s count = %llu\n", str,count);
        exit(0);
    }
}

int main(int argc, char **argv)
{
    pid_t pid;
    char *s;
    int nzero, ret;
    int adj = 0;

    setbuf(stdout, NULL);
#if defined(NZERO)
    nzero = NZERO;
#elif defined(_SC_NZERO)
    nzero = sysconf(_SC_NZERO);
#else 
    nzero = 0;
//#error NZERO undefined    //編譯器緣由報錯
#endif
    printf("NZERO = %d\n",nzero);
    if (argc == 2)
        adj = strtol(argv[1], NULL, 10);
    gettimeofday(&end, NULL);
    end.tv_sec += 10;

    if ((pid = fork()) < 0) {
        printf("fork error!\n");
    }
    else if (pid == 0) {
        s = "child";
        printf("current nice value in child is %d,adjusting by %d\n", nice(0) + nzero, adj) ;
        errno = 0;
        if ((ret = nice(adj)) == -1 && errno != 0)
        {
            printf("child set schduling priority\n");
            printf("now child nice value in parent is %d\n", nice(0) + nzero);
        }
    }
    else {
        s = "parent";
        printf("current nice value in parent is %d\n",nice(0) + nzero);
    }

    for (; ;)
    {
        if (++count == 0)
        {
            printf("%s counter wrap", s);
        }
        checktime(s);
    }
}
View Code

進程時間

  任一進程均可以調用times函數獲取它本身以及終止子進程的牆上時鐘時間、用戶CPU時間和系統CPU時間。

#include <sys/times.h>
clock_t times(struct tms *buf);
//返回值:若成功,返回流逝的牆上時鐘時間;若出錯,返回-1

  此函數填寫由buf指向的tms結構,該結構定義以下:

struct tms
{   
    clock_t tms_utime;   
    clock_t tms_stime;
    clock_t tms_cutime;
    clock_t tms_cstime;
};

  此結構沒有包含牆上的時鐘時間。times函數返回牆上時鐘時間做爲其函數值。此值是相對於過去的某一時刻度量的,因此不能用其絕對值而必須使用其相對值。例如,調用times,保存其返回值。在之後的某個時間再次調用times,重新返回的值減去之前返回的值,此差值就是牆上時鐘時間。
  全部由此函數返回的clock_t值都用_SC_CLK_TCK(由sysconf函數返回的每秒時鐘滴答數)轉換成秒數。

  下面程序將每一個命令行參數做爲shell命令串執行,對每一個命令計時,並打印從tms結構取得的值。

#include <sys/times.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void  pr_times(clock_t, struct tms *, struct tms *);
static void  do_cmd(char *);
static void  pr_exit(int );

int
main(int argc, char *argv[])
{
    int     i;

    for (i = 1; i < argc; i++)
        do_cmd(argv[i]);    /* once for each command-line arg */
    exit(0);
}
static void
do_cmd(char *cmd)       /* execute and time the "cmd" */
{
    struct tms  tmsstart, tmsend;
    clock_t     start, end;
    int         status;

    fprintf(stderr, "\ncommand: %s\n", cmd);

    if ( (start = times(&tmsstart)) == -1)  /* starting values */
        printf("times error");

    if ( (status = system(cmd)) < 0)        /* execute command */
        printf("system() error");

    if ( (end = times(&tmsend)) == -1)      /* ending values */
        printf("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)    /* fetch clock ticks per second first time */
        if ( (clktck = sysconf(_SC_CLK_TCK)) < 0)
            printf("sysconf error");
    fprintf(stderr, "  real:  %7.2f\n", real / (double) clktck);
    fprintf(stderr, "  user:  %7.2f\n",
            (tmsend->tms_utime - tmsstart->tms_utime) / (double) clktck);
    fprintf(stderr, "  sys:   %7.2f\n",
            (tmsend->tms_stime - tmsstart->tms_stime) / (double) clktck);
    fprintf(stderr, "  child user:  %7.2f\n",
            (tmsend->tms_cutime - tmsstart->tms_cutime) / (double) clktck);
    fprintf(stderr, "  child sys:   %7.2f\n",
            (tmsend->tms_cstime - tmsstart->tms_cstime) / (double) clktck);
}

static void 
pr_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", WTERMSIG(status),  
        #ifdef WCOREDUMP  
                WCOREDUMP(status) ? "(core file generated)" : "");  
        #else  
                "");  
        #endif  
        else if (WIFSTOPPED(status))  
                 printf("child stopped, signal number = %d\n", WSTOPSIG(status));  
} 
View Code
相關文章
相關標籤/搜索