一,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限制記憶體,沒有起作用,為等等進一步實驗~~