APUE學習筆記:第八章 進程控制

8.1 引言算法

本章介紹UNIX的進程控制,包括建立新進程、執行程序和進程終止。還將說明進程屬性的各類ID-----實際、有效和保存的用戶和組ID,以及他們如何受到進程控制原語的影響。本章還包括瞭解釋器文件和system函數。本章最後講述大多數UNIX系統所提供的進程會計機制。這種機制使咱們可以從另外一個角度瞭解進程的控制功能。shell

 

8.2 進程標識符數組

每一個進程都有一個非負整型表示的唯一進程ID。由於進程標識符是唯一的,常將其用做其餘標識符的一部分以保證其唯一性。雖然是唯一的,可是進程ID能夠重用。(大多數UNIX系統實現延遲重用算法,使得賦予新建進程的ID不一樣於最近終止進程所使用的ID。這防止了將新進程誤認爲是使用同一ID的某個已終止的進程。編輯器

ID爲0一般是系統進程函數

ID爲1一般是init進程優化

除了進程ID,每一個進程還有其餘一些標識符。下列函數返回這些標識符ui

#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 getid(void)
        //返回值:調用進程的實際組id
gid_t getegid(void)
        //返回值:調用進程的有效組id

這些函數都沒有出錯返回spa

 

8.3 fork函數操作系統

一個現有進程能夠調用fork函數建立一個新進程。命令行

#include<unistd.h>

pid_t fork(void);
            //返回值:子進程返回0,父進程中返回子進程ID,出錯返回-1

將子進程ID返回給父進程的理由是:由於一個進程的子進程能夠有多個,而且沒有一個函數使一個進程能夠得到其全部子進程的進程ID

使子進程獲得返回值0的理由是:一個進程只會有一個父進程,因此子進程老是能夠調用getppid以得到其父進程的進程ID(進程ID0老是由內核交換進程使用,因此一個子進程的進程ID不多是0)

 

子進程是父進程的副本,但父、子進程並不共享這些存儲空間部分。父子進程共享正文段

因爲在fork以後常常跟隨者exec,因此如今的不少實現並不執行一個父進程數據段,棧和堆的徹底複製。做爲替代,使用了寫時複製技術。

實例:8_1 fork函數示例

 1 #include"apue.h"
 2 
 3 int glob=6; //external variable in initialized data
 4 char buf[]="a write to stdout\n";
 5 
 6 int main()
 7 {
 8     int var; //automatic variable on the stack
 9     pid_t pid;
10     var=88;
11     if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1)
12     err_sys("write error");
13     printf("before fork\n");//we don't flush stdout
14     if((pid=fork())<0){
15     err_sys("fork error");
16     }else if(pid==0){    //child
17     glob++;
18     var++;
19     }else {sleep(2);
20 }
21     printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var);
22     exit(0);
23 }
24     

通常來講,在fork以後是父進程仍是子進程先執行是不肯定的。這取決於內核的調度算法。8_1中是先讓父進程休眠2秒鐘,以使子進程先執行

當寫到標準輸出時,咱們將buf長度減去1做爲輸出字節數,這是爲了不將終止null字節寫出。strlen計算不包含終止null字節的字符串長度,而sizeof則計算包括終止null字節的緩衝區長度。二者之間的另外一個差異是,使用strlen需進行一次函數調用,而對於sizeof而言,由於緩衝區已用已知字符串進行了初始化,其長度是固定的,因此sizeof在編譯時計算緩衝區長度

在8_1中當將標準輸出重定向到一個文件時,卻獲得printf輸出行兩次。其緣由是,在fork以前調用了printf一次,但當調用fork時,卻獲得printf輸出行兩次。其緣由是,在fork以前調用了printf一次,但當調用fork時,該行數據仍在緩衝區中,而後在將父進程數據空間複製到子進程中時,該緩衝區也被複制到子進程中,因而那時父、子進程各自有了帶該行內容的標準I/O緩衝區。在exit以前的第二個printf將其數據添加到現有的緩衝區中。當每一個進程終止時,最終會沖洗其緩衝區的副本

 

父子進程的區別是:

-fork的返回值

-進程ID不一樣

-兩個進程具備不一樣的父進程ID:子進程的父進程ID是建立它的進程ID,而父進程ID則不變

-子進程的tms_utime,tms_stime,tme_cutime以及tme_ustime均被設置爲0

-父進程設置的文件鎖不會被子進程繼承

-子進程的未處理的鬧鐘被清除

-子進程的未處理信號集設置爲空集

 

使fork失敗的兩個主要緣由是:系統中已經有了太多的進程,或者實際用戶ID進程總數超過了系統限制

 

fork有下列兩種用法:

(1)一個進程但願複製本身,是父子進程同時執行不一樣代碼段

(2)一個進程要執行一個不一樣的程序。

 

8.4 vfork函數

vfork函數的調用序列和返回值與fork相同,但二者的語義不一樣。

vfork用於建立一個新進程,而該新進程的目的是exec一個新程序。vfork和fork同樣都建立一個子進程,可是它並不將父進程的地址空間徹底複製到子進程中,由於子進程會當即調用exec(或exit),因而也就不會存訪該地址空間。相反,在子進程調用exec或exit以前,它在父進程的空間中運行。這種優化工做方式在某些UNIX的頁式虛擬存儲器視線中提升了效率

vfork和fork之間的另外一個區別是:vfork保證子程序先運行,在它調用exec或exit之間後父進程纔可能被調度運行(若是在調用這兩個函數以前子程序依賴於父進程的進一步動做,則會致使死鎖)

實例:8_2 vfork函數實例

 1 #include"apue.h"
 2 int glob=6;
 3 int main()
 4 {
 5     int var;
 6     pid_t pid;
 7     var=88;
 8     
 9     printf("before vfork\n");
10     if((pid=vfork())<0){
11     err_sys("vfork error");
12     }else if(pid==0){
13     glob++;
14     var++;
15     _exit(0);
16     }
17     printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var);
18     exit(0);
19 }

 

vfork,它產生的子進程剛開始暫時與父進程共享地址空間(其實就是線程的概念了),由於這時候子進程在父進程的地址空間中運行,因此子進程不能進行寫操做,而且在兒子「霸佔」着老子的房子時候,要
委屈老子一下了,讓他在外面歇着(阻塞),一旦兒子執行了exec或者exit後,至關於兒子買了本身的房子了,這時候就至關於分家了。

8.5 exit函數

若是父進程在子進程以前終止,則對於父進程已經終止的全部進程,他們的父進程都改變爲init進程。咱們稱這些進程由init進程領養。其操做過程大體以下:在一個進程終止時,內核逐個檢查全部進程,以判斷它是不是正要終止進程的子程序,若是是,則將該進程的父進程ID更改成1(init進程ID),這種處理方法保證了每一個進程都有一個父進程。

另外一個咱們關心的狀況是若是子進程在父進程以前終止,那麼父進程又如何能在作相應檢查時獲得子程序的終止狀態呢?

內核爲每一個終止子進程保存了必定量的信息,因此當終止進程的父進程調用wait或waitpid,能夠獲得這些信息,這些信息至少包括進程ID,該進程的終止狀態,以及該進程使用的CPU時間總量。內核能夠釋放終止進程所使用的全部存儲區,關閉其全部打開文件。

 

8.6 wait和waitpid函數

#include<sys/wait.h>
 
pid_t wait(int *statloc);

pid_t waitpid(pid_t pid,int *statloc,int options);

            //兩個函數返回值:若成功則返回進程ID,0,若出錯則返回-1

這兩個函數區別以下:

-在一個子進程終止前,wait使其調用者阻塞,而waitpid有一個選項,可以使調用者不阻塞。

-waitpid並不等待在其調用以後的第一個終止子程序,它有若干個選項,能夠控制它所等待的進程

實例:8_3 打印exit狀態的說明

 1 #include"apue.h"
 2 #include<sys/wait.h>
 3 void pr_exit(int status)
 4 {
 5     if(WIFEXITED(status))
 6     printf("normal termination,exit status= %d\n",WEXITSTATUS(status));
 7     else if(WIFSIGNALED(status))
 8     printf("abnormal termination,signal number= %d%s\n",WTERMSIG(status),
 9 #ifdef WCOREDUMP 
10     WCOREDUMP(status) ? "(core file generated)" : " ");
11 #else
12     "");
13 #endif 
14     else if(WIFSTOPPED(status))
15     printf("child stopped,signal number= %d\n",WSTOPSIG(status));
16 }

實例:8_4 演示不一樣的exit值

 1 #include"apue.h"
 2 #include<sys/wait.h>
 3 void pr_exit(int );
 4 int main()
 5 {
 6     pid_t pid;
 7     int status;
 8     if((pid=fork())<0)
 9     err_sys("fork error");
10     else if(pid==0)
11     exit(7);
12     if(wait(&status)!=pid)
13     err_sys("wait error");
14     pr_exit(status);
15     if((pid=fork())<0)
16     err_sys("fork error");
17     else if(pid==0)
18     abort();
19     if(wait(&status)!=pid)
20     err_sys("wait error");
21     pr_exit(status);
22     if((pid=fork())<0)
23     err_sys("fork error");
24     else if(pid==0)
25 //    status/=0;
26     if(wait(&status)!=pid)
27     err_sys("wait error");
28     pr_exit(status);
29     exit(0);
30 }
31 void pr_exit(int  i)
32 {
33  printf("%d\n",i);
34 return;
35 }

waitpid函數提供了wait函數沒有提供的三個功能:

(1)waitpid可等待一個特定的進程,而wait則返回任一終止子進程的狀態。

(2)waitpid提供了一個wait的非阻塞版本。有時用戶但願取得一個子進程的狀態,但不想阻塞

(3)waitpid支持做業控制

 

8.7 waitid函數

#include<sys/wait.h>

int waitid(idtype_t idtype,id_t id,siginfo_t *infop,int options);

        //返回值:若成功則返回0,若出錯則返回-1

與waitpid類似,waitid容許一個進程指定要等待的子進程。但它使用單獨的參數表示要等待的字進程的類型,而不是將此進程ID或進程組ID組合稱一個參數

 

8.8wait3 和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

 

8.9 競爭條件

這部分操做系統原理已經講的很深了

 

程序清單 8_6 具備競爭條件的程序

 1 #include"apue.h"
 2 static void charatatime(char *);
 3 
 4 int main()
 5 {
 6     pid_t pid;
 7     if((pid=fork())<0){
 8     err_sys("fork error");
 9     }else if(pid==0){
10     charatatime("output from child\n");
11     }else {
12     charatatime("output from parent\n");
13     }
14     exit(0);
15 }
16 static void charatatime(char *str)
17 {
18     char *ptr;
19     int c;
20     setbuf(stdout,NULL);
21     for(ptr=str;(c=*ptr++)!=0; )
22     putc(c,stdout);
23 }

在程序中將標準輸出設置爲不帶緩衝的,因而每一個字符輸出都需調用一次write.本例的目的是使內核儘量在兩個進程之間進行屢次切換,以便演示競爭條件。

 

8.10 exec函數

調用exec函數時,該進程執行的程序徹底替換爲新程序,而新程序則從其main函數開始執行。由於調用exec並不建立新進程,因此先後的進程ID並未改變。exec只是用一個全新的程序替換了當前進程的正文,數據,堆和棧段

#include<unistd.h>
int execl(const char *pathname,const char *arg(),.../*(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,若成功則不返回值

這些函數之間的第一個區別是前4個去路徑名做爲參數,後兩個取文件名做爲參數。當指定filename做爲參數時:

-若是filename中包含/,則將其視爲路徑名

-不然就按PATH環境變量,在它所指定的各目錄中搜尋可執行文件。

若是execlp或execvp使用路徑前綴中的一個找到了一個可執行文件,可是該文件不是由鏈接編輯器產生的機器可執行文件,則認爲該文件是一個shell腳本,因而試着調用/bin/sh,並以該filename做爲shell的輸入

第二個區別與參數表的傳遞有關(1表示list,v表示適量vector),函數execl、execlp和execle要求將新進程的每一個命令行參數都說明爲一個單獨的參數。這種參數表以空指針結尾。對於另外三個函數(execv、execvp和execve),則應先構造一個指向各參數的指針數組,而後將該數組地址做爲這三個函數的參數

最後一個區別與向新進程傳遞環境表相關。以e結尾的兩個函數(execle和execve)能夠傳遞一個指向環境字符串指針數組的指針。其餘四個函數則使用調用進程中的environ變量爲新程序複製現有的環境。

注意:在執行exec先後實際用戶ID和實際組ID保持不變,而有效ID是否改變則取決於所執行程序文件的設置用戶ID位和設置組ID位是否設置。若是新程序的設置用戶ID位已設置,則有效用戶ID變成程序文件全部者的ID,不然有效用戶ID不變。對組ID的處理方式與此相同

實例:8_8 exec函數實例

 1 #include"apue.h"
 2 #include<sys/wait.h>
 3 char *env_init[]={ "USER=unknow","PATH=/tmp",NULL};
 4 int main()
 5 {
 6     pid_t pid;
 7     if((pid=fork())<0){
 8     err_sys("fork error");
 9     }else if(pid==0){//specify pathname,specify environment
10     if(execle("/home/sar/bin/echoall","echoall","myarg1","MY ARG2",
11         (char *)0,env_init)<0)
12     err_sys("execle error");
13     }
14     if(waitpid(pid,NULL,0)<0)
15     err_sys("wait error");
16     if((pid=fork())<0){
17     err_sys("fork error");
18     }else if(pid==0){//specify filename,inherit environment
19     if(execlp("echoall","echoall","only 1 arg",(char *)0)<0)
20     err_sys("execlp error");
21     }
22     exit(0);
23 }

 

8.11 更改用戶ID和組ID

能夠用setuid函數設置實際用戶ID和有效用戶ID。setgid函數設置實際組ID和有效組ID

#include<unistd.h>

int getuid(uid_t uid);

int setgid(gid_t gid);

            //兩個函數返回值:若成功則返回0,若出錯則返回-1

規則:

(1):若進程具備超級用戶權限,則setuid函數將實際用戶ID、有效用戶ID、以及保存的設置用戶ID設置爲uid

(2):若進程沒有超級用戶特權,可是uid等於實際用戶ID或保存的設置用戶ID,則setuid只將有效用戶ID設置爲uid。不改變實際用戶ID和保存的設置用戶ID

(3):若是上面兩個條件都不知足,則將errno設置爲EPERM,並返回-1

 

1.setreuid和setregid函數

交換實際用戶ID和有效用戶ID的值

#include<unistd.h>

int setreuid(uid_t ruid,uid_t euid);

int setregid(gid_t rgid,gid_t egid);

            //兩個函數返回值:若成功則返回0,若出錯則返回-1

2.seteuid和setegid函數

只更改有效用戶ID

#include<unistd.h>

int seteuid(uid_t uid);

int setegid(gid_t gid);

        //返回值:T:0,F:-1

 

8.12 解釋器文件

解釋器文件是文本文件,其起始開頭形式是:

#! pathname [optional-argument] 例如:#!/bin/sh

內核使調用exec函數的進程實際執行的不是解釋器文件,而是該解釋器文件第一行中pathname所指定的文件,必定要將解釋器文件和解釋器區分開來

8.13 system函數

#include<stdlib.h>

int system(const char *cmdstring);

若是cmdstring是一個空指針時,system返回非零值,這特徵能夠肯定在一個給定的操做系統上是否支持system函數

在UNIX中,system老是可用的

由於system在其實現中調用了fork、exec和waitpid,所以有三種返回值

(1)若是fork失敗或者waitpid返回除EINTR以外的出錯,則system返回-1,並且errno中設置了錯誤類型值

(2)若是exec失敗,則其返回值如同shell執行了exit(127)同樣

(3)不然全部三個函數都執行成功,而且system的返回值是shell的終止狀態,其格式已在waitpid說明。

 

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

設置用戶ID或設置組ID程序決不該調用system函數,由於system中執行了fork和exec以後超級用戶權限仍會保持下來,若是一個進程正以特殊的權限運行,它又想生成另外一個進程執行另外一個程序,則它應當直接使用fork和exec,並且在fork以後,exec以前要改回到普通權限

 

8.14 進程會計

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

超級用戶執行一個帶路徑名參數的accton命令啓動會計處理。會計記錄寫到指定的文件中(會計記錄結構定義在頭文件<sys/acct.h>中)

會計記錄所需的各類數據都由內核保存在進程表中,並在一個新進程被建立時置初值。每次進程終止時都會編寫一條會計記錄。這就意味着在會計文件中記錄的順序對應於終止的順序,而不是他們啓動的順序

會計記錄對應與進程而不是程序,在fork以後,內核爲子程序初始化一個目錄,而不是在一個新程序被執行時作這個工做。

 

8.15 用戶標識

系統一般記錄用戶登陸時所使用的名字,用getlogin函數能夠獲取此登錄名

#include<unistd.h>

char *getlogin(void);

        //返回值:若成功則返回指向登錄名字符串的指針,若出錯則返回NULL

若是調用此函數的進程沒有鏈接到用戶登陸時所用的終端,則本函數會失敗

 

8.16 進程時間

任意進程均可調用times函數以得到它本身及已終止子程序的:牆上時鐘時間,用戶cpu時間,系統cpu時間

#include<sys/times.h>

clock_t times(struct tms *buf);

  //返回值:若成功則返回流逝的牆上始終時間,若出錯則返回-1
相關文章
相關標籤/搜索