父子進程共享資源的關系
fork()
用來創建進程fork(void)
在linux中所有進程都是由init進程直接或間接創建
成功:在父進程中將返回子進程的PID;子進程返回0,以區別父進程
失敗:父進程中返回-1
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5 int main(int argc,char *argv[])
6 {
7 pid_t pid;
8 if((pid=fork())==-1)
9 printf("fork error");
10 printf("bye\n");
11 printf("當前進程的進程號pid:%d\n當前進程的父進程號ppid:%d\n",getpid(),getppid());
12 return 0;
13 }
結果:
[root@sun PCB]# ps aux
root 3905 0.0 0.1 108468 1904 pts/0 S Dec17 0:00 bash
[root@sun PCB]# ./fork
bye
bye
當前進程的進程號pid:4570
當前進程的父進程號ppid:3905
pid=fork()中pid的值:4571 //在父進程中將返回子進程的PID
當前進程的進程號pid:4571
當前進程的父進程號ppid:4570
pid=fork()中pid的值:0 //子進程返回0,以區別父進程
子進程中的代碼在fork返回位置執行;子進程創建成功之後,和父進程同時執行,競爭系統資源,誰先執行由調度算法決定。
父子進程
子進程會復制父進程的幾乎所有信息:子進程復制父進程用戶空間所有數據;
子進程復制父進程內核空間PCB中絕大多數數據;
一、文件流緩沖區的資源位於用戶空間,所以全部復制。即如果流緩沖區中有臨時信息,都會復制到子進程的用戶空間流緩沖區中。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int main(int argc,char *argv[])
6 {
7 pid_t pid;
8 printf("在fork之前,有回車\n");
9 printf("在fork之前,沒有回車,getpid()——pid=%d\t",getpid());
10 pid=fork();
11 if(pid==0)
12 printf("\nfork後創建的子進程getpid()——pid=%d\n",getpid());
13 else
14 printf("\nfork後創建的父進程getpid()——pid=%d\n",getpid());
15 }
[root@sun PCB]# ./streamfork
在fork之前,有回車
在fork之前,沒有回車,getpid()——pid=5536
fork後創建的父進程getpid()——pid=5536
在fork之前,沒有回車,getpid()——pid=5536
fork後創建的子進程getpid()——pid=5537
按照上面所說,子進程要在fork方法執行並返回某值後才會復制代碼到子進程,子進程從返回值位置向後執行,不會執行之前的代碼,但這段代碼卻輸出了之前的代碼,這就是復制了緩沖區的緣故。
之所以出現兩次輸出,有兩方面原因,首先是跟printf的緩沖機制有關,我們在前面說過printf("%d",i)=fprintf(stdout,"%d",i),就是說printf函數輸出某些內容時,操作系統僅僅是把該內容放到了stdout的緩沖隊列裏了,並沒有立刻寫到屏幕上。但是,只要看到有/n 則會立即刷新stdout,才能夠馬上打印了。
其次就是因為復制了緩沖區。由於父進程在fork前輸出的第二個printf函數時沒有回車,而輸出流是帶緩沖的,從而該信息緩存到用戶空間,在fork創建子進程後,系統為子進程復制父進程數據空間以及標準輸出緩沖區,子進程刷新了輸出緩沖區,將數據輸出。
二、子進程復制父進程的數據段,BSS段,代碼段,堆空間,棧空間,文件描述符,但是對於文件描述符關聯的內核文件表項(即struct file結構體)則是采用共享的方式
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <sys/types.h>
7
8 int main(int argc,char *argv[])
9 {
10 pid_t pid;
11 int fd;
12 int i=1;
13 int status;
14 char *ch1="hello";
15 char *ch2="world";
16 char *ch3="IN";
17 if((fd=open("test.txt",O_RDWR|O_CREAT,0644))==-1)
18 {
19 perror("parent open");
20 exit(EXIT_FAILURE);
21 }
22 if(write(fd,ch1,strlen(ch1))==-1)
23 {
24 perror("parent write");
25 exit(EXIT_FAILURE);
26 }
27 if((pid=fork())==-1)
28 {
29 perror("fork");
30 exit(EXIT_FAILURE);
31 }
32 else if(pid==0)
33 {
34 i=2;
35 printf("in chile\n");
36 printf("i=%d\n",i);
37 if(write(fd,ch2,strlen(ch2)));
38 perror("chile write");
39 return 0;
40 }
41 else
42 {
43 sleep(1);//等待子進程先執行
44 printf("in parent\n");
45 printf("i=%d\n",i);
46 if(write(fd,ch3,strlen(ch3)));
47 perror("parent write");
48 wait(&status);//等待子進程結束
49 return 0;
50 }
51 }
[root@sun PCB]# ./forkfilrstruct
in chile
i=2
chile write: Success
//在這裏明顯等待1s才出現in parent,即sleep()讓父進程等待1s好讓子進程完成寫ch2的操作,1s後再寫ch3
in parent
i=1
parent write: Success
[root@sun PCB]# cat test.txt
helloworldIN
從test.txt的內容可以看出,父子進程對同一個文件操作,寫入數據也不覆蓋,即說明父子進程共享文件偏移,因此共享文件表項
而從變量i可以看出子進程賦值後父進程的i值不變,說明父子進程各自擁有這一變量的副本,互相不影響。
這裏對wait函數稍加介紹:
wait(等待子進程中斷或結束)
wait()會暫時停止進程的執行,直到有信號來到或子進程結束。如果在調用wait()時子進程已經結束,則wait()會立即返回子進程結束狀態值,由參數status 返回,而子進程的進程識別碼也會一起返回。如果不在意結束狀態值,則參數status 可以設成NULL。
vfork()
vfolk()創建新進程時不復制父進程的地址空間,而是在必要的時候才申請新的存儲空間,共享父進程的代碼以及數據段等
1 #include<unistd.h>
2 #include<error.h>
3 #include<sys/types.h>
4 #include<stdio.h>
5 #include<stdlib.h>
6 int glob=6; //已初始化全局變量,存放在數據段
7 int main()
8 {
9 int var;
10 pid_t pid;
11 var=88; //局部變量,存放在棧
12 printf("in beginning:\tglob=%d\tvar=%d\n",glob,var);
13 if((pid=vfork())<0)
14 {
15 perror("vfork");
16 exit(EXIT_FAILURE);
17 }
18 else if(pid==0)
19 {
20 printf("in child,modify the var:glob++,var++\n");
21 glob++;
22 var++;
23 printf("in child:\tglob=%d\tvar=%d\n",glob,var);
24 _exit(0);
25 }
26 else
27 {
28 printf("in parent:\tglob=%d\tvar=%d\n",glob,var);
29 return 0;
30 }
31 }
輸出的glob,var的值相同,均是自加之後的結果,說明子進程修改後父進程跟著改變,即兩者共享。
若vfork改成fork,則子進程是自加後的結果,父進程不變,說明子進程是父進程的一份復制。
但是由於父子空間共享內存空間,使得由子函數調用vfork創建的子進程(架設子進程為先執行函數的進程)調用其它函數或運行其他程序後會,父進程會出現段錯誤,如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 void test()
5 {
6 pid_t pid;
7 pid=vfork();//創建子進程
8 if(pid==-1)
9 {
10 perror("vfork");
11 exit(EXIT_FAILURE);
12 }
13 else if(pid==0) //子進程先運行
14 {
15 printf("1:child pid=%d,ppid=%d\n",getpid(),getppid());
16 return;
17 }
18 else
19 printf("2:parent pid=%d,ppid=%d\n",getpid(),getppid());
20 }
21 void fun()
22 {
23 int i;
24 int buf[100];
25 for(i=0;i<100;i++)
26 buf[i]=0;
27 printf("3:child pid=%d,ppid=%d\n",getpid(),getppid());
28 }
29 int main()
30 {
31 pid_t pid;
32 test();
33 fun();
34 }
1.創建主函數,申請棧空間(局部變量、返回值、參數等)
2.調用test函數,申請test函數的棧空間
3.test函數創建子進程,子進程先運行,在test中輸出pid和ppid,清理棧空間
4.子進程調用fun函數,覆蓋原來test函數的棧空間,執行完畢後退出
5.父進程從返回處開始執行,可是棧已經不存在了
所以如果希望在創建的子進程中運行新的程序,則用fork()函數創建子進程,再用exec系列函數替代子進程用戶空間的資源(代碼、堆、棧等),內核信息基本不修改。
execl系列
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5 int main(int argc,char* argv[])
6 {
7 pid_t pid;
8 if((pid=fork())<0)
9 {
10 printf("error");
11 }
12 else if(pid==0)
13 {
14 execl("/bin/ls","ls","-l","/home",(char *)0);
15 }
16 else
17 printf("father ok!\n");
18
19 }
[root@sun task]# ./execl
father ok!
[root@sun task]# 總用量 16
drwxr-xr-x. 4 root root 4096 11月 14 23:10 df
drwx------. 29 hadoop hadoop 4096 9月 4 23:15 hadoop
drwx------. 44 sun sun 4096 12月 30 04:45 sun
drwxr-xr-x. 12 root root 4096 12月 30 05:01 test
在執行execl系列函數時,默認情況下,新代碼可以使用原來代碼中打開的文件描述符,即執行execl時,並不關閉進程原來打開的文件
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 #include <fcntl.h>
5 #include <string.h>
6 #include <stdlib.h>
7
8 int main(int argc,char* argv[])
9 {
10 int fd,status;
11 pid_t pid;
12 fd=open("test.txt",O_RDWR|O_APPEND|O_CREAT,0644);//打開文件,產生一個文件描述符fd,從文件尾開始追加
13 if(fd==-1)
14 {
15 perror("open");
16 exit(EXIT_FAILURE);
17 }
18 printf("before child process write\n");
19 system("cat test.txt");//創建新進程,在新進程中運行命令,直到新進程運行結束在運行父進程
20 if((pid=fork())==-1)
21 {
22 perror("fork");
23 exit(EXIT_FAILURE);
24 }
25 if(pid==0)
26 {
27 char buf[128];
28 sprintf(buf,"%d",fd);//將文件描述符寫入緩沖區
29 execl("./newcode","newcode",buf,(char *)0);//執行newcode,把文件描述符以參數的形式傳遞給代碼newcode,在newcode中執行對文件的追加寫入工作
30 }
31 else
32 {
33 wait(&status);
34 printf("after child_process write\n");
35 system("cat test.txt");
36 }
37 }
1 #include <stdio.h>
2 #include <string.h>
3 #include <unistd.h>
4
5 int main(int argc,char* argv[])
6 {
7 int i;
8 int fd;
9 char *ptr="helloworld\n";
10 fd=atoi(argv[1]);//argv[1]中的值是寫入buf的fd=open("test.txt",O_RDWR|O_APPEND|O_CREAT,0644)
11 i=write(fd,ptr,strlen(ptr));//寫入fd關聯的test.txt中,執行成功,說明原來的文件描述符可以使用
12 if(i<=0)
13 perror("write");
14 close(fd);
15 }
[root@sun task]# ./system_execl
before child process write
我是測試文件
after child_process write
我是測試文件
helloworld
父子進程共享文件描述符:此說法,其實是父子進程共享 文件表項(父進程和子進程共享同一個file table entry)
由於子進程是父進程的拷貝,子進程會拷貝父進程的進程描述符中的文件描述符表,可以說繼承父進程的文件描述字(files_struct中的struct file *fd_array[NR_OPEN_DEFAULT]的拷貝)
如果我們沒有調用exec函數,則我們父子進程的代碼段,堆棧,數據段都完全相同(因為是拷貝),所以此時我們的子進程可以使用fork()之前的fd值,雖然此時fd是屬於子進程的數據段(他是之前fd的拷貝)
一個進程一旦調用exec類函數,它本身就"死亡"了,系統把代碼段替換成新的程序的代碼,廢棄原有的數據段和堆棧段,並為新程序分配新的數據段與堆棧段,唯一留下的,就是進程號,也就是說,對系統而言,還是同一個進程,不過已經是另一個程序了。(不過exec類函數中有的還允許繼承環境變量之類的信息。)
所以進程的存儲映像被新程序代替(也就是說,含有對方地址的套接字地址結構也丟失了),但是由於即便exec之後,打開的文件描述符依然存在,所以我們可以通過調用getpeername(fd.....)獲得對端的ip和端口號
一般來說:調用exec之前在進程中打開的描述字在exec之後還是保持打開狀態的,我們可以通過fcntl函數設置FD_CLOEXEC描述字標誌來關閉。此又稱為文件描述字標簽,默認情況是清除的
由於exec之後,原來connfd描述字肯定沒有了,所以我們必須還原這個connfd
1、把connfd當成一個字符串,作為exec的命令行參數給新程序
2、在調用exec之前,把某個描述字設置成connfd,(通過close(fd),然後調用dup(connfd),則根據規則,使用最小未被使用的fd,及是fd,這樣fd就和connfd一樣指向同一個文件表項),通常我們用0,1,2設置成connfd。
在C程序中,文件由文件指針或者文件描述符表示。ISO C的標準I/0庫函數(fopen, fclose, fread, fwrite, fscanf, fprintf等)使用文件指針,UNIX的I/O函數(open, close, read, write, ioctl)使用文件描述符。下面重點來說下,文件描述符是如何工作的。
文件描述符相當於一個邏輯句柄,而open,close等函數則是將文件或者物理設備與句柄相關聯。句柄是一個整數,可以理解為進程特定的文件描述符表的索引。先介紹下面三個概念,後面講下open、close等操作以後,文件和文件描述符產生什麽關系,以及fork後文件描述符的繼承等問題。
文件描述符表:用戶區的一部分,除非通過使用文件描述符的函數,否則程序無法對其進行訪問。對進程中每個打開的文件,文件描述符表都包含一個條目。
系統文件表:為系統中所有的進程共享。對每個活動的open, 它都包含一個條目。每個系統文件表的條目都包含文件偏移量、訪問模式(讀、寫、or 讀-寫)以及指向它的文件描述符表的條目計數。
內存索引節點表: 對系統中的每個活動的文件(被某個進程打開了),內存中索引節點表都包含一個條目。幾個系統文件表條目可能對應於同一個內存索引節點表(不同進程打開同一個文件)。
1、舉例: 執行myfd = open( "/home/lucy/my.dat", O_RDONLY); 以後,上述3個表的關系原理圖如下:
如果上面進程在open以後又執行了close()函數,操作系統會刪除文件描述符表的第四個條目和系統文件表的對應條目(若指向它的描述符表唯一),並對內存索引節點表條目中的計數減1,如果自減以後變為0,說明沒有其他進程鏈接此文件,將索引節點表條目也刪除,而這裏進程B也在open這個文件,所以索引節點表條目保留。
2、文件描述符的繼承 通過fork()創建子進程時,子進程繼承父進程環境和上下文的大部分內容的拷貝,其中就包括文件描述符表。 (1)對於父進程在fork()之前打開的文件來說,子進程都會繼承,與父進程共享相同的文件偏移量。如下圖所示(0-1-2 表示 標準輸入-輸出-錯誤):
系統文件表位於系統空間中,不會被fork()復制,但是系統文件表中的條目會保存指向它的文件描述符表的計數,fork()時需要對這個計數進行維護,以體現子進程對應的新的文件描述符表也指向它。程序關閉文件時,也是將系統文件表條目內部的計數減一,當計數值減為0時,才將其刪除。
(2)相反,如果父進程先進程fork,再打開my.dat,這時父子進程關於my.dat 的文件描述符表指向不同的系統文件表條目,也不再共享文件偏移量(fork以後2個進程分別open,在系統文件表中創建2個條目);但是關於標準輸入,標準輸出,標準錯誤,父子進程還是共享的。
父子進程共享資源的關系