天天看點

程序控制之 fork、wait、exec族函數 及程序資源控制 三,wait 和 waitpid 函數四,exec家族五,在linux中,fork一個子程序,怎麼樣控制它的運作時間以及占用記憶體

一,fork

//頭檔案

#include <unistd.h>

//函數定義

pid_t fork( void );

傳回值:子程序中傳回0,父程序中傳回子程序ID,出錯傳回-1函數說明:一個現有程序可以調用fork函數建立一個新程序。由fork建立的新程序被稱為子程序(child process)。fork函數被調用一次但傳回兩次。兩次傳回的唯一差別是子程序中傳回0值而父程序中傳回子程序ID。子程序是父程序的副本,它将獲得父程序資料空間、堆、棧等資源的副本。

注意,在fork()的調用處,整個父程序空間會原模原樣地複制到子程序中,包括指令,變量值,程式調用棧,環境變量,緩沖區,等等。 子程序持有的是上述存儲空間的“副本”,這意味着父子程序間不共享這些存儲空間,它們之間共享的存儲空間隻有代碼段。

(這裡有一個關于fork比較有趣的面試題,  一個fork的面試題 )

 ,首先來看例1

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

void main()
{
    int i;
    printf("hello, %d\n",getpid());
    i=2;
    fork();
    printf("var %d in %d\n", i, getpid());
}
           

這是在我的機器上一次執行的結果:

hello, 2808

var 2 in 2808

var 2 in 2809

為什麼會有兩次輸出var 2 一行呢?看似不可思議吧…要解釋原因,就牽涉到了我們要讨論的fork,它到底做了什麼?

fork英文是叉的意思.在這裡的意思是程序從這裡開始分叉,分成了兩個程序,一個是父程序,一個子程序.子程序拷貝

了父程序的絕大部分.棧,緩沖區等等.系統為子程序建立一個新的程序表項,其中程序id與父程序是不相同的,這也就

是說父子程序是兩個獨立的程序,雖然父子程序共享代碼空間.但是在牽涉到寫資料時子程序有自己的資料空間,這

是因為copy on write機制,在有資料修改時,系統會為子程序申請新的頁面.再來複習下程序的有關知識.系統通過進

程控制塊PCB來管理程序.程序的執行,可以看作是在它的上下文中執行.一個程序的上下文(context)由三部分組成:

使用者級上下文,寄存器上下文和系統級上下文.使用者級上下文中有正文,資料,使用者棧和共享存儲區;寄存器上下文中有

個非常重要的程式計數器(傳說中的)PC,還有棧指針和通用寄存器等;系統級上下文分靜态和動态,PCB中程序表項,

U區,還有本程序的表項,頁表,系統區表項等都屬于靜态部分,而核心棧等則屬于動态部分.

回到fork上來.fork在核心中對應的是do_fork函數.詳見:核心 do_fork

函數源代碼淺析(http://bbs.chinaunix.net/thread-2011594-1-1.html). 上面已經提到,fork後,子程序拷貝了父程序

的程序表項,還有棧,緩沖區,U區等等.當然在這之前會去檢查系統有沒有可用的資源,取一個空閑的程序表項和唯

一的PID号等工作.(後面的例子會展現子程序到底拷貝了父程序的哪些東西.)需要指出的是,這裡所說的拷貝,并不

是說子程序再申請頁面,将父程序中的全部拷貝過來.而是,他們共享一個空間,子程序隻是作一層映射而已,這個時

候程序頁面标記為隻讀.在有資料修改時,才會申請新的頁面,拷貝過來,并标記為可寫.fork執行後,對父程序和子進

程不同的地方還有,對父程序傳回子程序的pid号,對子程序傳回的是0.大緻的算法描述為:

    if (目前正在執行的是父程序)

{

      将子程序的狀态設定為”就緒狀态”;

      return (子程序的pid号); }

else

{

     初始化U區等工作;

     return 0;

}

現在來看例1,是不是已經清晰了很多? 在執行了fork之後,父子程序分别都執行了下一步printf語句.由于fork拷貝走

了pc,是以在子程序中不會再從main入口重新執行,而是執行fork後的下一條指令.而i是儲存在程序棧空間中的,是以

子程序中也存在. 有了前面的基礎,再看下面一個例2:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void main()
{
    int i=0;
    pid_t fork_result;
    printf("pid:%d->main begin()\n",getpid());
    fork_result=fork();
    if(fork_result<0)
    {   
        printf("fork fail\n");
        exit(1);
    }   
    for(i=0; i<3; i++)
    {   
        if(fork_result==0)
            printf("in ID %d child process: %d\n", getpid(), i);
        else
            printf("in ID %d parent process: %d\n", getpid(), i);
    }   
}
           

這次輸出可以更明确的顯示出子程序到底拷貝了些什麼.我機器上的兩次執行結果:

pid:3881->main begin()

in ID 3881 parent process: 0

in ID 3881 parent process: 1

in ID 3881 parent process: 2

in ID 3882 child process: 0

in ID 3882 child process: 1

in ID 3882 child process: 2

pid:3881->main begin()

in ID 3882 child process: 0

in ID 3882 child process: 1

in ID 3882 child process: 2

in ID 3881 parent process: 0

in ID 3881 parent process: 1

in ID 3881 parent process: 2

同時也可以說明,父子程序到底哪個先執行,是跟cpu排程有關系的.如果想固定順序,那麼就要用wait或vfork函數.

繼續看例3:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
void main()
{
        printf("hello world %d",getpid());
        //fflush(0);
        fork();
}
           

執行上面的程式,可以發現輸出了兩遍hello world, hello world 3929hello world 3929.而且兩次的pid号都是一樣

的.這是為什麼呢? 這其實是因為printf的行緩沖的問題,printf語句執行後,系統将字元串放在了緩沖區内,并沒有輸

出到stdout.不明白的話看下面的例子:

#include <stdio.h>
int main(){
    printf("hello world");
    while(1);
    return 0;
}
           

執行上面的程式你會發現,程式陷入死循環,并沒有輸出”hello world”.這就是因為把”hello world”放入了緩沖區.我們平常加’\n’的話,

就會重新整理緩沖區,那樣就會直接輸出到stdout了.因為子程序将這些緩沖也拷貝走了,是以子程序也列印了一遍.父程序直到最後才輸

出.他們的輸出是一樣的,輸出的pid是一緻的,因為子程序拷貝走的是printf語句執行後的結果.如果利用setbuf設定下,或者在printf語

句後調用fflush(0);強制重新整理緩沖區,就不會有這個問題了.這個例子從側面顯示出子程序也拷貝了父程序的緩沖區.關于fork的應用還

很多很多,在實際項目中需要了再去深入研究.關于fork和exec的差別,exec是将本程序的映像給替換掉了,跟fork差别還是很大的,其

實fork建立子程序後,大部分情況下,子程序會調用exec去執行不同的程式的. 

二、vfork()

pid_t vfork( void );

vfork與fork主要有三點差別:

.fork():子程序拷貝父程序的資料段,堆棧段;vfork():子程序與父程序共享資料段.fork()父子程序的執行次序不确定。

vfork 保證子程序先運作,在調用 exec 或 exit 之前與父程序資料是共享的(即子程序在調用exec或exit 之前。它在父

程序的空間中運作),在它調用 exec或 exit 之後父程序才可能被排程運作。vfork()保證子程序先運作,在它調用 exec

或 exit 之後父程序才可能被排程運作.如果在調用這兩個函數之前子程序依賴于父程序

的進一步動作,則會導緻死鎖。

#include <unistd.h>
#include <stdio.h>
int main(void)
{
pid_t pid;
int count=0;
pid=vfork();
count++;
printf("count= %d\n",count);
return 0;
}
           

執行結果:

./test

count= 1

count= 1

Segmentation fault (core dumped)

分析:

通過将fork()換成vfork(),由于vfork()是共享資料段,為什麼結果不是2呢,答案是:

**vfork保證子程序先運作,在它調用 exec 或 exit 之後父程序才可能被排程運作.如果在調用這兩個函數之前子程序依賴于父程序的進

一步動作,則會導緻死鎖.

3)做最後的修改,在子程序執行時,調用_exit(),程式如下:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main(void)
{
pid_t pid;
int count=0;

pid=vfork();

if(pid==0)
{
   count++;
_exit(0);
}
else
{
count++;
}
printf("count= %d\n",count);

return 0;
}
           

執行結果:

./test

count= 2

分析:如果子程序中如果沒有調用_exit(0),則父程序不可能被執行,在子程序調用exec(),exit()之後父程序才可能被調用.

是以加上_exit(0),使子程序退出,父程序執行.

這樣 else 後的語句就會被父程序執行,又因在子程序調用 exec 或 exit 之前與父程序資料是共享的,

是以子程序退出後把父程序的資料段 count 改成1了,子程序退出後,父程序又執行,最終就将count 變成了 2.

簡要的概括的說是:

1)fork()系統調用是建立一個新程序的首選方式,fork的傳回值要麼是0,要麼是非0,父程序與子程序的根本差別在于fork函數的傳回值.

2)vfork()系統調用除了能保證使用者空間記憶體不會被複制之外,它與fork幾乎是完全相同的.vfork存在的問題是它要求子程序立即調用exec,

而不用修改任何記憶體,這在真正實作的時候要困難的多,尤其是考慮到exec調用有可能失敗.

3)vfork()的出現是為了解決當初fork()浪費使用者空間記憶體的問題,因為在fork()後,很有可能去執行exec(),vfork()的思想就是取消這種複制.

4)現在的所有unix變量都使用一種寫拷貝的技術(copy on write),它使得一個普通的fork調用非常類似于vfork.是以vfork變得沒有必要.

 三,wait 和 waitpid 函數

  首先說明子程序與父程序先後終止産生的問題:

 1,如果父程序在子程序終止之前終止,對于父程序已經終止的所有子程序,他們的父程序都改為 init程序(pid為1) 我們稱這些子程序

      由init程序領養。

2,如果子程序在父程序之前終止,核心為每個終止的子程序儲存了一定量的資訊,是以當終止程序的父程序 調用wait 和 waitpid 時,

      可以得到這些資訊(包含了程序ID、該程序的終止狀态、以及該程序使用的CPU時間總量等)。

       對于一個已經終止、但是父程序尚未對其進行善後處理(擷取終止子程序的有關資訊并釋放它占用的資源) 的程序被稱為僵屍程序

    (APUE P182中fork兩次避免僵死程序,比較有意思)。

我們一般用wait & waitpid  獲得子程序終止狀态。 

wait的函數原型是:  

#include<sys/types.h>

#include <sys/wait.h>

pid_t wait(int *status)  

參數status用來儲存被收集程序退出時的一些狀态,它是一個指向int類型的指針。程序一旦調用了wait,就立即阻塞自己,由wait自動分

析是否目前程序的某個子程序已經退出,如果讓它找到了這樣一個已經變成僵屍的子程序, wait就會收集這個子程序的資訊,并把它徹底

銷毀後傳回;如果沒有找到這樣一個子程序,wait就會一直阻塞在這裡,直到有一個出現為止。

pid_t waitpid(pid_t pid,int *status,int options)

waitpid多出了兩個可由使用者控制的參數pid和options.,下面介紹這兩個參數:

從參數的名字pid和類型pid_t中就可以看出,這裡需要的是一個程序ID。但當pid取不同的值時,在這裡有不同的意義。   

 pid>0時,隻等待程序ID等于pid的子程序,不管其它已經有多少子程序運作結束退出了,隻要指定的子程序還沒有結束,waitpid就會一

直等下去。    

 pid= -1時,等待任何一個子程序退出,沒有任何限制,此時waitpid和wait的作用一模一樣。     

pid=0時,等待同一個程序組中的任何子程序,如果子程序已經加入了别的程序組,waitpid不會對它做任何理睬。    

 pid<-1時,等待一個指定程序組中的任何子程序,這個程序組的ID等于pid的絕對值。

options   options提供了一些額外的選項來控制waitpid,

目前在Linux中隻支援WNOHANG和WUNTRACED兩個選項, option 可以為 0 或可以用"|"運算符把它們連接配接起來使用。

 WNOHANG 若pid指定的子程序沒有結束,則waitpid()函數傳回0,不予以等待。若結束,則傳回該子程序的ID。

 WUNTRACED 若子程序進入暫停狀态,則馬上傳回,但子程序的結束狀态不予以理會。WIFSTOPPED(status)宏确定傳回值是否對應與一個

  暫停子程序。

例如:在幾乎同一時刻,有N個client 的 FIN發向伺服器,同樣的,N個SIGCHLD信号到達伺服器,然而,UNIX的信号往往是不會排隊的,顯然這樣一來,

信号處理函數将隻會執行一次,殘留剩餘N-1個子程序作為僵屍程序駐留在核心空間。此時,正确的解決辦法是利用waitpid(-1, &stat, WNOHANG)

防止留下僵屍程序。其中的pid為-1表明等待任一個終止的子程序,而WNOHANG選擇項通知核心在沒有已終止程序項時不要阻塞。

wait&waitpid 差別 :

waitpid提供了wait函數不能實作的3個功能: 

 1,waitpid等待特定的子程序, 而wait則傳回任一終止狀态的子程序; 

 2,waitpid提供了一個wait的非阻塞版本(waitpid的 WNOHANG選項); 

 3, waitpid支援作業控制(以WUNTRACED選項). 用于檢查wait和waitpid兩個函數傳回終止狀态的宏: 這兩個函數傳回的子程序狀态都儲存在statloc指針中, 

用以下3個宏可以檢查該狀态: 

WIFEXITED(status): 若為正常終止, 則為真. 此時可執行 WEXITSTATUS(status): 取子程序傳送給exit或_exit參數的低8位. 

WIFSIGNALED(status): 若為異常終止, 則為真.此時可執行 WTERMSIG(status): 取使子程序終止的信号編号.

WIFSTOPPED(status): 若為目前暫停子程序, 則為真. 此時可執行 WSTOPSIG(status): 取使子程序暫停的信号編号.

四,exec家族

一個fork的面試題

1.exec家族一共有六個函數,分别是:

(1)int execl(const char *path, const char *arg, ......);

(2)int execle(const char *path, const char *arg, ...... , char * const envp[]);

(3)int execv(const char *path, char *const argv[]);

(4)int execve(const char *path, char *const argv[], char *const envp[]);

(5)int execvp(const char *file, char * const argv[]);

(6)int execlp(const char *file, const char *arg, ......);

其中隻有execve是真正意義上的系統調用,其它都是在此基礎上經過包裝的庫函數。

    exec函數族的作用是根據指定的檔案名找到可執行檔案,并用它來取代調用程序的内容,換句話說,就是在調用程序内部執行

一個可執行檔案。這裡的可執行檔案既可以是二進制檔案,也可以是任何Linux下可執行的腳本檔案。與一般情況不同,exec函數

族的函數執行成功後不會傳回,因為調用程序的實體,包括代碼段,資料段和堆棧等都已經被新的内容取代,隻留下程序ID等一

些表面上的資訊仍保持原樣,頗有些神似"三十六計"中的"金蟬脫殼"。看上去還是舊的軀殼,卻已經注入了新的靈魂。隻有調用失

敗了,它們才會傳回一個-1,從原程式的調用點接着往下執行。

2.它們之間的差別:

第一個差別是:

前四個取路徑名做為參數,後兩個取檔案名做為參數,如果檔案名中不包含“/”則從PATH環境變量中搜尋可執行檔案,

如果找到了一個可執行檔案,但是該檔案不是連接配接編輯程式産生的可執行代碼檔案,則當做shell腳本處理。

第二個差別:

前兩個和最後一個函數中都包括“ l ”這個字母,而另三個都包括“ v”, " l "代表 list即表,而" v "代表 vector即矢量,

也是是前三個函數的參數都是以list的形式給出的,但最後要加一個空指針,如果用常數0來表示空指針,則必須将

它強行轉換成字元指針,否則有可能出錯。,而後三個都是以矢量的形式給出,即數組。

最後一個差別:

與向新程式傳遞環境變量有關,如第二個和第四個以e結尾的函數,可以向函數傳遞一個指向環境字元串指針數組的

指針。即自個定義各個環境變量,而其它四個則使用程序中的環境變量。

 3.執行個體講解:

(1)在平時的程式設計中,如果用到了exec函數族,一定記得要加錯誤判斷語句。先判斷execl的傳回值,如果出錯,可以用perror( )函數

列印出錯誤資訊。

如:if (execl(“path”,”..””(char *)0) < 0)

    {

       perror(“execl error!”);

   }

如果調用出錯,可輸出:execl error!: 錯誤原因  這樣可友善查找出錯原因

(2)注意下面書寫格式:

先定義一個指針數組:char *argv[]={“ls”,”-l”,(char *)0}

用execv調用ls:    execv(“/bin/ls”,argv)

 如果用execvp

execvp(“ls”,argv)      //直接寫ls就可以了

注意:

execl調用 shell時,要在 shell腳本中指明使用的 shell版本: #! /bin/bash。在指令行下執行 shell腳本,系統為它自動打開一個 shell,在程式中

沒有shell,在調用shell腳本時,會出錯,是以要在shell腳本中先打開shell。

int execl(const char *path, const char *arg, ...);
execl()用來執行參數path字元串所代表的檔案路徑, 接下來的參數代表執行該檔案時傳遞的argv[0],argv[1].....是後一個參數必須用空指針NULL作結束。      

五,在linux中,fork一個子程序,怎麼樣控制它的運作時間以及占用記憶體

 首先,我們fork出一個子程序後,父程序與子程序并行執行,我們可以用wait系列函數對子程序進行等待,并用結構體去記錄子程序各類資源的使用狀況。其中我們用到了struct rusage中的ru_utime和ru_stime,前者是使用的使用者時間,後者是系統時間,不過後面那個似乎沒怎麼用。當然,在運作過程中,如果子程序程式執行時有錯誤産生,我們可以使用WIFEXITED(stutas) 這個宏來判斷父程序等待後子程序是否為正常結束。

這麼一個程式架構。

    pid = vfork();//用vfork可以保證子程序比父程序先運作

    if(pid==0)

    {

        //這裡進行程序限制和程序輸入輸出重定向。

        //exec族函數

        exit(1);

    }

    wait(&status);//父程序等待子程序運作

    if(!WIFEXITED(status))

    {

       //處理程式非正常結束狀态

    }

status:指向子程序的傳回狀态,可通過以下宏進行檢索

WIFEXITED(status) //傳回真如果子程序正常終止,例如:通過調用exit(),_exit(),或者從main()的return語句傳回。

WEXITSTATUS(status) //傳回子程序的退出狀态。這應來自子程序調用exit()或_exit()時指定的參數,或者來自main内部return語句參數的最低位元組。隻有WIFEXITED傳回真時,才應該使用。

WIFSIGNALED(status) //傳回真如果子程序由信号所終止

WTERMSIG(status) //傳回導緻子程序終止的信号數量。隻有WIFSIGNALED傳回真時,才應該使用。

WCOREDUMP(status) //傳回真如果子程序導緻核心轉存。隻有WIFSIGNALED傳回真時,才應該使用。并非所有平台都支援這個宏,使用時應放在#ifdef WCOREDUMP ... #endif内部。

WIFSTOPPED(status) //傳回真如果信号導緻子程序停止執行。

WSTOPSIG(status) //傳回導緻子程序停止執行的信号數量。隻有WIFSTOPPED傳回真時,才應該使用。

WIFCONTINUED(status) //傳回真如果信号導緻子程序繼續執行。

非 正常結束是什麼呢???~~我們可以自己定義,比如tle,mle,runtime error,都會在子程序中發出相應的信号。如果接受到這些信号,則作出相應的動作。系統資源方面的限制可以通過getrlimit和setrlimit 來讀取和設定。這兩個函數都利用一個通用結構rlimit來描述資源限制。該結構定義在頭檔案sys/resource.h中,有兩個成員rlim_t rlim_cur,rlim_t rlim_max,前者是軟限制,後者是硬限制,我們可以通過設定軟限制的值來控制子程序的流程。

有許多系統資源可以進行限制,它們由rlimit函數中的resource參數指定,并在頭檔案sys/resource.h中定義

resource參數

說    明

RLIMIT_CORE

核心轉儲(core dump)檔案的大小限制(以位元組為機關)

RLIMIT_CPU

CPU時間限制(以秒為機關)

RLIMIT_DATA

資料段限制(以位元組為機關)

RLIMIT_FSIZE

檔案大小限制(以位元組為機關)

RLIMIT_NOFILE

可以打開的檔案數限制

RLIMIT_STACK

棧大小限制(以位元組為機關)

RLIMIT_AS

位址空間(棧和資料)限制(以位元組為機關)

例如:

        getrlimit(RLIMIT_CPU, &limit);

        limit.rlim_cur = 1;

        setrlimit(RLIMIT_CPU, &limit);

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <signal.h>
#include <sys/wait.h>

int main()
{
   int status;
   struct rlimit memlim,timlim;
    getrlimit(RLIMIT_CPU,&timlim);
    getrlimit(RLIMIT_AS ,&memlim);
    memlim.rlim_cur=3*1024*1024; // MB
    timlim.rlim_cur=1;// s
 
    int pid = vfork();//用vfork可以保證子程序比父程序先運作
    if(pid==0)
    {
        setrlimit( RLIMIT_CPU,&timlim);
        setrlimit( RLIMIT_AS,&memlim);
        int *p = (int *)calloc(8*1024*1024, sizeof(int));
        if(-1==execl("/home/daniel/my_linux/apue/my_programming/my",\
						"my", (char *)0))
        {
            perror( "execl error\n ");
            exit(1);
        }
        exit(0);
    }
  wait(&status);
  printf("%d\n",status);
  if(!WIFEXITED(status))
  {
    int sig=0;
       //處理程式非正常結束狀态
    if(WIFSIGNALED(status))
      sig=WTERMSIG(status);
    else
     return 1;
   // printf("%d\n",sig);
    if(sig == SIGXCPU)//24
        {
                printf("tle\n");
            }
            if(sig == SIGXFSZ)
        {
                printf("ole\n");
            }
            if(sig == SIGSEGV)
       {
                printf("re\n");
            }
            if(sig==SIGKILL) // 6
       {
                printf("mle\n");         
            }
           //if (WCOREDUMP(status))
        //printf("mle\n");

    }
  return 0;

}
           

其中my為:

#include <stdio.h>

int main()
{
	char *p = (char *)calloc(12*1024*1024, sizeof(char));
	while(1);
	return 0;
}
           

以上代碼可以捕獲到子程序逾時(152, tle), 但是記憶體超限制後不能檢測出問題,不知為什麼?

setrlimit限制記憶體,沒有起作用,為等等進一步實驗~~

繼續閱讀