【Linux】fork()函數總結,以及fork()引出的一些知識點

一、 進程相關概念html

  當一個進程的時間片用完後,系統把相關的寄存器的值保存到該進程表相應的表項裏。同時把代替該進程即將執行的進程的上下文從進程表中讀出,並更新相應的寄存器值,這個過程稱爲上下文交換。上下文交換其實就是讀出新的進程的PC(程序計數器),指示當前進程的下一條將要執行的指令。一個進程主要包含如下三個元素:(1)一個正在執行的程序;(2)與該進程相關聯的所有數據(變量,內存,緩衝區)(3)程序上下文(程序計數器pc)linux

二、頭文件:shell

  #include <unistd.h>函數

  #include <types.h>測試

三、函數原型:spa

  pid_t fork(void);操作系統

  pid_t是一個宏,其實質是int類型,被定義在#include <sys/types.h>中;.net

  返回值:1)成功:調用一次則返回兩個值:子進程返回0,父進程返回子進程的PID線程

         2)失敗:返回-1指針

四、獲取進程id

  getpid()getppid()

五、Fork過程

  當前進程(調用者,父進程)調用fork()建立一個新的進程(子進程)。子進程是父進程的副本,它將得到父進程數據空間、堆、棧(上下文)等資源的副本(注意:(1)子進程copy父進程的變量,內存與緩衝區,即整個的數據空間的內容,但數據空間是獨立的,即父子進程並不共享這些存儲空間。(2)父子進程對打開文件的共享: fork以後,子進程會繼承父進程所打開的文件表,即父子進程共享文件表,該文件表是由內核維護的,兩個進程共享文件狀態,偏移量等。這一點很重要,當在父進程中關閉文件時,子進程的文件描述符仍然有用,相應的文件表也不會被釋放。(3爲了提升效率,fork後並不當即複製父進程空間,採用了COWCopy-On-Write);當父子進程任意之一,要修改數據段、堆、棧時,進行復制操做,但僅複製修改區域;)。父子進程間共享的存儲空間只有代碼段(只讀的,且僅共享fork()後面的代碼段)。子進程和父進程繼續執行fork調用以後的指令。(4)fork以後,這兩個進程執行沒有固定的前後順序,哪一個進程先執行要看系統的進程調度策略。

6看一個程序:

#include<unistd.h>
#include<stdio.h>
int glob = 6;
char buf[] = "a write to stdout\n";
int  main(void)
{
        int var = 88;
        pid_t   pid;
        if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
        {        
                printf("write error」);
        		return 0;
   		 }
        printf("before fork\n"); //注意這一行
        if ( (pid = fork()) < 0)
  	    {
                printf("fork error");
  		     	return 0;
   		 }
        else if (pid == 0) 
   		 {
                glob++;
                var++;
        } 
  		 else
   		 {
                sleep(2);
   		 }
        printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);  
  		return 0;
}//fork_test.c

選擇輸出的方式不一樣,會有不一樣的輸出結果。

直接輸出到控制檯:
a write to stdout
before fork
pid = 1867, glob = 7, var = 89
pid = 1866, glob = 6, var = 88
使用重定向:./fork_test >fork_test.txt,fork_test.txt內容以下:

a write to stdout
before fork
pid = 1939, glob = 7, var = 89
before fork
pid = 1938, glob = 6, var = 88

  爲何會有這麼大的不一樣?咱們先來看看stdoutSTDOUT_FILENO的區別(OS和C++的區別)。

  stdin類型爲 FILE*;STDIN_FILENO類型爲 int。使用stdin的函數主要有:fread、fwrite、fclose等,基本上都以f開頭;使用STDIN_FILENO的函數有:read、write、close等。操做系統一級提供的文件API都是以文件描述符來表示文件,STDIN_FILENO就是標準輸入設備(通常是鍵盤)的文件描述符。標準C++一級提供的文件操做函數庫都是用FILE*來表示文件,stdin就是指向標準輸入設備文件的FILE*。

  經過man stdin能夠查看到:stdin, stdout, stderr - standard I/O streams(具體自行man)。

  stdin / stdout / stderr 分別是指向stream的FILE型的指針變量。當程序啓動時,與其結合的整型文件描述符(fd)分別是0,1,2。STDIN_FILENO / STDOUT_FILENO / STDERR_FILENO 是在<unistd.h>文件中定義的預編譯macro。其值是0,1,2。(經過freopen(3),能夠改變與這3個文件描述符(fd)結合的stream值)。也就是說,程序啓動時:FILE * stdin / stdout / stderr 對應的 文件描述符(fd)分別是 STDIN_FILENO(0) / STDOUT_FILENO(1) / STDERR_FILENO(2) 。可是,能夠經過FILE *freopen(const char *path, const char *mode, FILE *stream); 來改變,使文件描述符(fd)對應到其餘的stream上。(結合到哪一個文件stream上,那個文件stream就是變成 標準輸入輸出)。(標準輸入stdin/標準輸出stdout/error輸出stderr分別改變。)

總結一句話:STDOUT_FILENOOS提供的,定義在頭文件<unistd.h>,沒有buffer(直接的系統調用);FILE *stdout是標準C++提供的,定義在頭文件<stdio.h>,有buffer

使用stdin / stdout / stderr的函數主要有:freadfwritefclose等,基本上都以f開頭。

使用STDIN_FILENO / STDOUT_FILENO / STDERR_FILENO的函數有:readwriteclose等。

上程序:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#define CHANGE_BY_FREOPEN /* 須要註釋掉,進行切換 */
int main(int argc,char**argv)
{
         char buf[]="hello,world\n";
	#ifdef CHANGE_BY_FREOPEN
	 freopen("stdout_text.txt","w",stdout);
  	//freopen("stderr_text.txt","w",stderr);/* stderr */
	#endif
  	printf("%s",buf);
 	fwrite(buf,strlen(buf), 1,stdout);
 	write(STDOUT_FILENO,&buf,strlen(buf));
  	perror("error out");/* stderr */
  	return 0;
}/* stdouttest.c */    

 

編譯命令: gcc -o stdouttest stdouttest.c
①當 #define CHANGE_BY_FREOPEN 被註釋掉的時候:終端輸出的結果是:

hello,world

hello,world

hello,world

error out: Sucess

②當 #define CHANGE_BY_FREOPEN 有效的時候:終端輸出的結果是:

error out: Sucess

同時,會建立一個名爲stdout_text.txt的文件,該文件中的內容是:

hello,world

hello,world

hello,world
//freopen("stderr_text.txt","w",stderr);/* stderr */若是有效的話():①shell中,什麼也不輸出。②stdout_text.txt 中,輸出3個(hello,world + 換行符)。③stderr_text.txt 中,輸出1個(error out: Success)。 

 

接着stdoutSTDOUT_FILENO的區別後,下面看下關於"printf"/"write"和緩衝的說明:

printf是在stdio.h中聲明的函數,而標準IO都是帶緩衝的,因此printf是帶緩衝的。而write則是不帶緩衝的。

標準IO在輸入或輸出到終端設備時,它們是行緩衝的,不然(文件)它們是全緩衝的。而標準錯誤流stderr是不使用緩衝的。更爲準確的描述是:當且僅當標準輸入和標準輸出並不涉及交互式設備使,他們纔是全緩衝的。標準出錯流不使用緩衝。下列狀況會引起緩衝區的刷新(清空緩衝區)

1、緩衝區滿時;
2、執行flush語句;
3、執行endl語句(printf"\n")
4關閉文件。

綜上所述:write的內容在父進程直接輸出到了設備,「before fork」在主線程輸出到終端後由於換行符而清空了緩衝區,因此也只輸出了一次。而重定向到"a.txt"時,printf使用的是全緩衝,因此「before fork」並未輸出到設備,而是隨着fork()而被複制了一份到子進程的空間中,因此輸出了兩次。

注意:在重定向父進程輸出時,子進程也被重定向了。

另外:爲何父進程要等待2秒?在fork時,父進程全部打開的文件描述符都被複制一份到子進程中,然而他們共享文件描述符所指向的文件對象(FILE結構:描述文件讀寫指針,偏移量,標誌等)。若是不等待,文件偏移量被交替修改,極可能產生混亂的輸出。(實際上,這麼小的程序,進程執行是很快的,通過實際測試,這部分能夠不用,不會產生混亂輸出。)

再看一個例子:

#include <stdio.h> 
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
	pid_t childpid;
	char buf[100]={0};
	int status;//供wait使用的參數
	int f;//定義文件標識符
	f=open("text",O_CREAT|O_RDWR,0664);
	if(f==-1)//文件標識符
{
		perror("create file failed!");
		return 1;
	}
	strcpy(buf,"father process’s data\n");
	printf("%d\n",15);
	//此時建立子進程
	childpid=fork();
	printf("childpid=%d",childpid);
	if(childpid==0)
{	strcpy(buf,"child process’s data\n");
		printf("%s",buf);
		puts("child process is working");
		printf("child process’s PID is: %d\n",getpid());
		printf("father process’s PID is: %d\n",getppid());
		int n1= write(f,buf,strlen(buf));//把buf的數據輸出重定向到文件中去
		close(f);
		exit(0);//子進程調用exit函數進入僵死狀態,參數0表示正常退出
	}
	else {wait(&status);//wait函數是一個等待子進程退出的函數,其參數是一個int類型的指針,保存子進程退出的一些信息
		printf("%s/",buf);
puts("father process is working");
		printf("father process’s PID is: %d\n",getpid());
		printf("child process’s PID is: %d\n",childpid);
		int n=write(f,buf,strlen(buf));
		close(f);
	}
	return 0;
}

程序運行結果以下:

15//父進程打印出來的

Child process’s data

Child process is working

Child process’s PID is:4850

Father process’s PID is: 4849

Father process’s data

Father process is working

Father process’s PID is:4849

Child process’s PID is:4850
若是把 printf("%d/n",15); 改成printf("%d",15); ,那麼運行的結果爲:

15Child process’s data//15是子進程打印出來的

Child process is working

Child process’s PID is:4850

Father process’s PID is: 4849

15Father process’s data//15是父進程打印出來的

Father process is working

Father process’s PID is:4849

Child process’s PID is:4850

分析以下:

1.wait()函數阻塞父進程,直到子進程返回。所以,子進程先執行,直到退出爲止。(固然,這裏也能夠用sleep(10)來完成一樣的功能)

函數說明:wait()會暫時中止目前進程的執行直到有信號來到或子進程結束若是在調用wait()時子進程已經結束wait()會當即返回子進程結束狀態值子進程的結束狀態值會由參數status 返回而子進程的進程識別碼也會一快返回若是不在乎結束狀態值則參數 status 能夠設成NULL. 子進程的結束狀態值請參考waitpid().

2.爲何printf("%d/n",15); 與 printf("%d",15);打印的結果不相同?

printf("%d/n",15); 與 printf("%d",15); 區別:前者將數據已經輸出到終端上了,後者的數據還在緩衝區內。當建立子進程時,子進程要copy父進程的數據,包括copy緩衝區,因此,第一個程序只打印出一個15,而第二個程序打印出兩個15.還要注意一點,第一個結果的15是由父進程打印出來的,而第二個結果因爲子進程先執行,複製緩衝區,因此子進程先打印出15,然後父進程纔打印出15.

3. close(f),當子進程已經關閉了文件,父進程怎麼還能將數據寫入?

在前面的分析中得知,父子進程共享同一個文件表,共享文件表的狀態,偏移位置等信息。因此在子進程關閉文件描述符後,在父進程中仍然是有效的,而父進程寫數據也從文件的當前位置開始寫。

linux系統文件流,緩衝及文件描述符與進程之間的關係,可參考http://topic.csdn.net/u/20090309/18/3aba9e11-c8a8-492b-9fe7-29043974a102.html

 

再來幾個fork的小題目:

 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main()
{
    pid_t   pid1;
    pid_t   pid2;
    pid1 = fork();
    pid2 = fork();
    printf("pid1=%d,pid2=%d\n",pid1,pid2);
    exit(0);
}

要求以下:已知從這個程序執行到這個程序的全部進程結束這個時間段內,沒有其它進程執行。
 1、請說出執行這個程序後,一共運行了幾個進程。
 2、若是其中一個進程的輸出結果是「pid1=1001, pid2=1002」,寫出其餘進程的輸出結果(不考慮進程執行順序)。

答案:

1、一共執行了四個進程。(P0, P1, P2, P3

2、另外幾個進程的輸出分別爲:

 pid1:1001, pid2:0

 pid1:0, pid2:1003

 pid1:0, pid2:0

#include <unistd.h>
#include <stdio.h> 
int main () 
{ 
	pid_t fpid; //fpid表示fork函數返回的值
	int count=0;
	fpid=fork(); 
	if (fpid < 0) 
		printf("error in fork!"); 
	else if (fpid == 0) {
		printf("i am the child process, my process id is %d\n",getpid()); 
		printf("我是爹的兒子\n");//對某些人來講中文看着更直白。
		count++;
	}
	else {
		printf("i am the parent process, my process id is %d\n",getpid()); 
		printf("我是孩子他爹\n");
		count++;
	}
	printf("統計結果是: %d\n",count);
	return 0;
}

運行結果是:
    i am the child process, my process id is 5574
    我是爹的兒子
    統計結果是: 1
    i am the parent process, my process id is 5573
    我是孩子他爹
    統計結果是: 1

#include <unistd.h>
#include <stdio.h>
int main(void)
{
   int i=0;
   for(i=0;i<2;i++){
       pid_t fpid=fork();
       if(fpid==0)
    	   printf("%d child  %4d %4d %4d/n",i,getppid(),getpid(),fpid);
       else
    	   printf("%d father%4d %4d %4d/n",i,getppid(),getpid(),fpid);
   }
   return 0;
}

程序運行結果:

0 father 1958 4982 4983 第一行 (P0i=0時執行fork()後進入else產生的)

1 father 1958 4982 4984 第二行 (P0i=1時執行fork()後進入else產生的)

0 child 4982 4983 0 第三行 (P0i=0時執行fork()後進入if產生的)

1 child 4982 4984 0 第四行 (P0i=1時執行fork()後進入if產生的)

1 father 4982 4983 4985 第五行 (P1i=1時執行fork()後進入else產生的)

1 child 1 4985 0 第六行 (P1i=1時執行fork()後進入if產生的)

分析以下:

假設主程序原來的進程爲P0

i=0時,程序進入循環,P0調用fork(),隨後產生出P1,在P0fork()返回的fpid是子進程的PID,因此fpid不爲0,此時判斷語句進入else,因此會有第一行;在P1fork()返回的fpid0,此時判斷語句進入if,因此會有第三行。

i=1時,(如下P0P1沒有前後順序,主要看OS的策略)P0再次調用fork(),隨後產生出P2,在P0fork()返回fpid!=0,進入else,因此會有第二行;在P2fork()返回fpid=0,進入if,因此會有第四行。另外,i=0時產生的P1也會調用fork(),隨後產生出P3,在P1fork()返回fpid!=0,進入else,因此會有第五行;在P2fork()返回fpid=0,進入if,因此會有第六行。

上面值得注意的是第六行中的getppid()值爲1。按照上面的分析,按說4985的父進程應該4983嗎?怎麼會是1呢?這裏得講到進程的建立和死亡的過程,進程4983執行完第二個循環後,main函數就該退出了,也即進程該死亡了,由於它已經作完全部事情了。4983死亡後,4985就沒有父進程了,這在操做系統是不被容許的,因此4985的父進程就被置爲init了,init是永遠不會死亡的。看下面的linux父子進程終止的前後順序不一樣產生不一樣的結果:

1)父進程先於子進程終止:

此種狀況就是孤兒進程。當父進程先退出時,系統會讓init進程接管子進程 。
2)子進程先於父進程終止,而父進程又沒有調用waitwaitpid函數

此種狀況子進程進入僵死狀態,且會一直保持下去直到系統重啓。子進程處於僵死狀態時,內核只保存進程的一些必要信息以備父進程所需。此時子進程始終佔有着資源,同時也減小了系統能夠建立的最大進程數。

僵死狀態:一個已經終止、可是其父進程還沒有對其進行善後處理(獲取終止子進程的有關信息,釋放它仍佔有的資源)的進程被稱爲僵死進程(zombie)ps命令將僵死進程的狀態打印爲
3)子進程先於父進程終止,而父進程調用了waitwaitpid函數 
此時父進程會等待子進程結束。

那麼有人可能會問:4983執行完後結束了,從而致使4985沒有了父進程;一樣,4982在第二次循環執行完後也結束了,不是也應該致使4984沒有了父進程了嗎?爲何第四行中的getppid()不是1?這其實在上面提到過,就是由於操做系統調度的問題。理論上來講,第四行和第六行的getppid()「都有可能爲1」。

#include <unistd.h>
#include <stdio.h>
int main(void)
{
   int i=0;
   for(i=0;i<N;i++){
       pid_t fpid=fork();
       if(fpid==0)
    	   printf("son/n");
       else
    	   printf("father/n");
   }
   return 0;
}

上面程序的循環中有個N,總結一下規律:對於這種N次循環的狀況,執行printf函數的次數爲2*(2^N-1)次,建立的子進程數爲2^N-1個。

 

最後再上一份代碼:

#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
   fork();
   fork() && fork() || fork();
   fork();
   return 0;
   //printf(「+ \n」);
}

問題:不算main這個進程自身,程序到底建立了多少個進程?

答案:答案是總共20個進程,除去main進程,還有19個進程。

 

參考文章:

http://hi.baidu.com/passerryan/item/bbe792245816c61209750821

http://www.cnblogs.com/lq0729/archive/2011/10/24/2222536.html

http://blog.csdn.net/xiaoxi2xin/archive/2010/04/24/5524769.aspx

http://blog.csdn.net/chenjin_zhong/article/details/6099228

http://blog.csdn.net/jason314/article/details/5640969

相關文章
相關標籤/搜索