1、fork
現在P1用fork()函數爲進程創建一個子進程P2,
a. fork
內核:
-
複製P1的正文段,數據段,堆,棧這四個部分,注意是其內容相同。
-
爲這四個部分分配物理塊,P2的:正文段->PI的正文段的物理塊,其實就是不爲P2分配正文段塊,讓P2的正文段指向P1的正文段塊,數據段->P2自己的數據段塊(爲其分配對應的塊),堆->P2自己的堆塊,棧->P2自己的棧塊。如下圖所示:同左到右大的方向箭頭表示複製內容。
b. COW
寫時複製技術:內核只爲新生成的子進程創建虛擬空間結構,它們來複制於父進程的虛擬究竟結構,但是不爲這些段分配物理內存,它們共享父進程的物理空間,當父子進程中有更改相應段的行爲發生時,再爲子進程相應的段分配物理空間。
c.vfork
vfork():這個做法更加火爆,內核連子進程的虛擬地址空間結構也不創建了,直接共享了父進程的虛擬空間,當然了,這種做法就順水推舟的共享了父進程的物理空間
2.fork()函數
#include<unistd.h>
#include<sys/types.h>
pid_t fork( void);
(pid_t 是一個宏定義,其實質是int 被定義在#include<sys/types.h>中)
返回值: 若成功調用一次則返回兩個值,子進程返回0,父進程返回子進程ID;否則,出錯返回-1
#include<sys/types.h> //對於此程序而言此頭文件用不到
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc, charchar ** argv ){
//由於會返回兩次,下面的代碼會被執行兩遍
//如果成功創建子進程:
//1. 父進程返回子進程ID,因此(父進程)會走一遍“分支3”
//2. 子進程返回0,因此(子進程)會走一遍“分支2”
pid_t pid = fork();
if (pid < 0){ //分支1
fprintf(stderr, "error!");
}else if( 0 == pid ){//分支2
printf("This is the child process!");
_exit(0);
}else{//分支3
printf("This is the parent process! child process id = %d", pid);
}
//可能需要時候wait或waitpid函數等待子進程的結束並獲取結束狀態
exit(0);
}
由於文件的操作是通過文件描述符表、文件表、v-node表三個聯繫起來控制的,其中文件表、v-node表是所有的進程共享,而每個進程都存在一個獨立的文件描述符表。
fork的另一個特性是所有由父進程打開的描述符都被複制到子進程中。父、子進程中相同編號的文件描述符在內核中指向同一個file結構體,也就是說,file結構體的引用計數要增加。
3. COW()函數
fork函數用於創建子進程,典型的調用一次,返回兩次的函數,其中返回子進程的PID和0,其中調用進程返回了子進程的PID,而子進程則返回了0,這是一個比較有意思的函數,但是兩個進程的執行順序是不定的。fork()函數調用完成以後父進程的虛擬存儲空間被拷貝給了子進程的虛擬存儲空間,因此也就實現了共享文件等操作。但是虛擬的存儲空間映射到物理存儲空間的過程中採用了寫時拷貝技術(具體的操作大小是按着頁控制的),該技術主要是將多進程中同樣的對象(數據)在物理存儲其中只有一個物理存儲空間,而當其中的某一個進程試圖對該區域進行寫操作時,內核就會在物理存儲器中開闢一個新的物理頁面,將需要寫的區域內容複製到新的物理頁面中,然後對新的物理頁面進行寫操作。這時就是實現了對不同進程的操作而不會產生影響其他的進程,同時也節省了很多的物理存儲器。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
int main(){
char p = 'p';
int number = 11;
if(fork()==0) /*子進程*/
{
p = 'c'; /*子進程對數據的修改*/
printf("p = %c , number = %d \n ",p,number);
exit(0);
}
/*父進程*/
number = 14; /*父進程對數據修改*/
printf("p = %c , number = %d \n ",p,number);
exit(0);
}
$ gcc -g TestWriteCopyTech.c -o TestWriteCopyTech
$ ./TestWriteCopyTech
p = p , number = 14 -----父進程打印內容
$ p = c , number = 11 -----子進程打印內容
原因分析:
由於存在企圖進行寫操作的部分,因此會發生寫時拷貝過程,子進程中對數據的修改,內核就會創建一個新的物理內存空間。然後再次將數據寫入到新的物理內存空間中。可知,對新的區域的修改不會改變原有的區域,這樣不同的空間就區分開來。但是沒有修改的區域仍然是多個進程之間共享。
fork()函數的代碼段基本是隻讀類型的,而且在運行階段也只是複製,並不會對內容進行修改,因此父子進程是共享代碼段,而數據段、Bss段、堆棧段等會在運行的過程中發生寫過程,這樣就導致了不同的段發生相應的寫時拷貝過程,實現了不同進程的獨立空間。
4、具體使用場景
1 fork使用場景
2. vfork分析
5、 fork代碼分析
#include "apue.h"
int glob = 6;
int main(void)
{
int var;
pid_t pid;
var = 88;
int status;
printf("bdfore vfork\n");
if((pid = fork())<0)
{
err_sys("vfork error");
}else if(pid == 0)
{
glob++;
var++;
printf("son vfork\n");
printf("pid =%d,=%d\n",getpid(),pid);
if((pid=fork())<0)
{err_sys("fork_error");}
else if(pid>0){
//printf("second parent pid =%d,=%d\n",getpid(),pid);
exit(0);}
sleep(2);
printf("pid =%d,=%d\n",getpid(),pid);
printf("Second child, parent pid = %d\n", getppid());
exit(0);
}
else{
if(wait(&status)==pid)
{
printf("wait1");
}
// if(waitpid(pid,NULL,0)!=pid)//成功返回pid
// {
// err_sys("waitpid error");
// }
printf("--- fork,%d\n",pid);
}
printf("pid =%d,=%d glob = %d, var = %d\n",getpid(),pid,glob,var);
exit(0);
}
打印結果如下
bdfore vfork
son vfork
pid =25215,=0
wait1--- fork,25215
pid =25214,=25215 glob = 6, var = 88
pid =25216,=0
Second child, parent pid = 1
上述代碼中,第二個children由於父進程exit了,所以其父進程爲init,pid爲1
運行順序爲,第一個子進程,第一個父進程,第二個子進程!
5、Linux進程的五個段
下面我們來簡單歸納一下進程對應的內存空間中所包含的5種不同的數據區都是幹什麼的。
BSS段:BSS段(bss segment)通常是指用來存放程序中未初始化的全局變量的一塊內存區域。BSS是英文Block Started by Symbol的簡稱。BSS段屬於靜態內存分配。
數據段:數據段(data segment)通常是指用來存放程序中已初始化的全局變量的一塊內存區域。數據段屬於靜態內存分配。
代碼段:代碼段(code segment/text segment)通常是指用來存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經確定,並且內存區域通常屬於只讀, 某些架構也允許代碼段爲可寫,即允許修改程序。
堆(heap):堆是用於存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)若程序員不釋放,則會有內存泄漏,系統會不穩定,Windows系統在該進程退出時由OS釋放,Linux則只在整個系統關閉時OS纔去釋放(參考Linux內存管理)。
棧(stack):棧又稱堆棧, 是用戶存放程序臨時創建的局部變量,也就是說我們函數括弧“{}”中定義的變量(但不包括static聲明的變量,static意味着在數據段中存放變 量)。除此以外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,並且待到調用結束後,函數的返回值也會被存放回棧中。由於棧的先進後出特點,所以 棧特別方便用來保存/恢復調用現場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數據的內存區。