Linux下fork與寫時拷貝技術(COW)詳解

1. 寫時拷貝的概念

Linux在使用fork()函數進程創建時,傳統fork()的做法是系統把所有的資源複製給新創建的進程,這種方式不僅單一,而且效率低下。因爲所拷貝的數據或別的資源可能是可以共享的。現在Linux的fork()使用寫時拷貝頁來實現新進程的創建,它是一種可推遲甚至避免數據拷貝的技術,剛開始時內核並不會複製整個地址空間,而是讓父子進程共享地址空間,只有在寫時才複製地址空間,使得父子進程都擁有獨立的地址空間,即資源的複製是在只有需要寫入時纔會發生,因此而稱之爲Copy on Write(COW)。在此之前都是以讀的方式去和父進程共享資源,這樣,在頁根本不會被寫入的場景下,fork()立即執行exec(),無需對地址空間進行復制,fork()的實際開銷就是複製父進程的一個頁表和爲子進程創建一個進程描述符,也就是說只有當進程空間中各段的內存內容發生變化時,父進程纔將其內容複製一份傳給子進程,大大提高了效率。

那麼子進程的物理空間沒有代碼,怎麼去取指令執行exec系統調用呢?

其實,在fork()之後,exec()之前,子進程和父進程是共享物理空間(內存區)的,子進程的代碼段,數據段和堆棧都指向父進程物理空間,即兩者的虛擬空間不同,但物理空間其實是同一個,當父進程或者子進程有需要修改段的行爲時,再爲子進程分配相應段的物理空間,若不是exec則內核會給子進程的數據段,堆棧段分配相應的物理空間,至此二者各自有各自的物理空間,互不影響。而代碼段則繼續共享父進程的物理空間,因爲兩者的代碼完全相同,但如果是因爲exec,,由於二者的執行的代碼不同,則也需爲子進程分配代碼段的物理空間。

2. 詳細

現在有一個父進程P1,這是一個主體,那麼它是有靈魂也就身體的。現在在其虛擬地址空間(有相應的數據結構表示)上有:正文段,數據段,堆,棧這四個部分,相應的,內核要爲這四個部分分配各自的物理塊。即:正文段塊,數據段塊,堆塊,棧塊。至於如何分配,這是內核去做的事,在此不詳述。

3. 關於fork函數

#include<unistd.h>
pid_t fork(void);
//返回:在子進程中返回0,在父進程中返回子進程的id,出錯返回-1.

fork在子進程中返回0而不是父進程的ID的原因在於:任何子進程只有一個父進程,而且子進程總是可以通過調用getppid取得父進程的ID。相反,父進程可以有許多子進程,而且無法獲得各個子進程的進程ID。如果父進程想要跟蹤所有子進程的ID,那麼它必須記錄每次調用fork的返回值,所以父進程返回的是子進程的進程ID
fork有兩個典型的用法:
1.一個進程創建一個自身的拷貝,這樣每個拷貝都可以在另一個拷貝執行其他任務的同時處理各自的某個操作。這是網絡服務器的典型用法。
2.一個進程想要執行另一個程序。既然創建新進程的唯一方法爲調用fork,該進程於是首先調用fork創建一個自身的拷貝,然後其中一個拷貝(通常爲子進程)調用exec把自身替換成新的程序。這是諸如shell之類程序的典型用法。

4. 關於exec函數

#include<unistd.h>
int execl(const char *pathname, const char *arg0,.../* (char *)0 */);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0,.../* (char *)0,char *const envp[] */);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0,.../* (char *)0 */);
int execvp(const char *filename, char *const argv[]);
//所有六個函數返回:-1——失敗,無返回——成功

從上面我們已經知道了fork會創建一個子進程。子進程的是父進程的副本。
exec函數的作用就是:裝載一個新的程序(可執行映像)覆蓋當前進程內存空間中的映像,從而執行不同的任務。

exec系列函數在執行時會直接替換掉當前進程的地址空間。
我去畫張圖來理解一下:
在這裏插入圖片描述

現在P1用fork()函數爲進程創建一個子進程P2
內核:

  • 複製P1的正文段,數據段,堆,棧這四個部分,注意是其內容相同。
  • 爲這四個部分分配物理塊,P2的:正文段->PI的正文段的物理塊,其實就是不爲P2分配正文段塊,讓P2的正文段指向P1的正文段塊,數據段->P2自己的數據段塊(爲其分配對應的塊),堆->P2自己的堆塊,棧->P2自己的棧塊。

如下圖所示:同左到右大的方向箭頭表示複製內容。
在這裏插入圖片描述

5.Copy On Write技術原理

內核只爲新生成的子進程創建虛擬空間結構,它們來複制於父進程的虛擬究竟結構,但是不爲這些段分配物理內存,它們共享父進程的物理空間,當父子進程中有更改相應段的行爲發生時,再爲子進程相應的段分配物理空間。

在這裏插入圖片描述
Copy On Write技術好處是什麼?

  • COW技術可減少分配和複製大量資源時帶來的瞬間延時。
  • COW技術可減少不必要的資源分配。比如fork進程時,並不是所有的頁面都需要複製,父進程的代碼段和只讀數據段都不被允許修改,所以無需複製。

Copy On Write技術缺點是什麼?

  • 如果在fork()之後,父子進程都還需要繼續進行寫操作,那麼會產生大量的分頁錯誤(頁異常中斷page-fault),這樣就得不償失。

幾句話總結Linux的Copy On Write技術:

  • fork出的子進程共享父進程的物理空間,當父子進程有內存寫入操作時,read-only內存頁發生中斷,將觸發的異常的內存頁複製一份(其餘的頁還是共享父進程的)。
  • fork出的子進程功能實現和父進程是一樣的。如果有需要,我們會用exec()把當前進程映像替換成新的進程文件,完成自己想要實現的功能。

6.關於vfork函數

vfork():這個做法更加火爆,內核連子進程的虛擬地址空間結構也不創建了,直接共享了父進程的虛擬空間,當然了,這種做法就順水推舟的共享了父進程的物理空間。
在這裏插入圖片描述
PS:實際上COW技術不僅僅在Linux進程上有應用,其他例如C++的String在有的IDE環境下也支持COW技術,即例如:

string str1 = "hello world";
string str2 = str1;

之後執行代碼:

str1[1]='q';
str2[1]='w';

執行修改後,此時str1的地址會發生變化,而str2的地址還是原來的。即在複製對象時,並不真正爲新對象開闢內存空間,而是在新對象的內存映射表中設立一個指針,指向源對象,這樣在進行讀操作時因爲並不修改對象,並不會給源對象帶來影響,當某一時刻要對某一對象進行修改時,即寫操作時,再將對象複製到新的內存空間中去,在這上面執行修改,以避免相互之間的影響。這樣做的一個好處也是儘可能提高效率。
這就是C++中的COW技術的應用,不過VS2005似乎已經不支持COW。

參考鏈接:

https://blog.csdn.net/bad_good_man/article/details/49364947

https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html

https://blog.csdn.net/weixin_33701617/article/details/88716535?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章