COW奶牛!Copy On Write機制瞭解一下

Java3y發表於2018-10-31

前言

只有光頭才能變強

在讀《Redis設計與實現》關於雜湊表擴容的時候,發現這麼一段話:

執行BGSAVE命令或者BGREWRITEAOF命令的過程中,Redis需要建立當前伺服器程式的子程式,而大多數作業系統都採用寫時複製(copy-on-write)來優化子程式的使用效率,所以在子程式存在期間,伺服器會提高負載因子的閾值,從而避免在子程式存在期間進行雜湊表擴充套件操作,避免不必要的記憶體寫入操作,最大限度地節約記憶體。

觸及到知識的盲區了,於是就去搜了一下copy-on-write寫時複製這個技術究竟是怎麼樣的。發現涉及的東西蠻多的,也挺難讀懂的。於是就寫下這篇筆記來記錄一下我學習copy-on-write的過程。

本文力求簡單講清copy-on-write這個知識點,希望大家看完能有所收穫。

一、Linux下的copy-on-write

在說明Linux下的copy-on-write機制前,我們首先要知道兩個函式:fork()exec()。需要注意的是exec()並不是一個特定的函式, 它是一組函式的統稱, 它包括了execl()execlp()execv()execle()execve()execvp()

1.1簡單來用用fork

首先我們來看一下fork()函式是什麼鬼:

fork is an operation whereby a process creates a copy of itself.

fork是類Unix作業系統上建立程式的主要方法。fork用於建立子程式(等同於當前程式的副本)。

  • 新的程式要通過老的程式複製自身得到,這就是fork!

如果接觸過Linux,我們會知道Linux下init程式是所有程式的爹(相當於Java中的Object物件)

  • Linux的程式都通過init程式或init的子程式fork(vfork)出來的。

下面以例子說明一下fork吧:


#include <unistd.h>  
#include <stdio.h>  
 
int main ()   
{   
    pid_t fpid; //fpid表示fork函式返回的值  
    int count=0;
	
	// 呼叫fork,建立出子程式  
    fpid=fork();

	// 所以下面的程式碼有兩個程式執行!
    if (fpid < 0)   
        printf("建立程式失敗!/n");   
    else if (fpid == 0) {  
        printf("我是子程式,由父程式fork出來/n");   
        count++;  
    }  
    else {  
        printf("我是父程式/n");   
        count++;  
    }  
    printf("統計結果是: %d/n",count);  
    return 0;  
}  
複製程式碼

得到的結果輸出為:


我是子程式,由父程式fork出來

統計結果是: 1

我是父程式

統計結果是: 1

複製程式碼

解釋一下:

  • fork作為一個函式被呼叫。這個函式會有兩次返回,將子程式的PID返回給父程式,0返回給子程式。(如果小於0,則說明建立子程式失敗)。
  • 再次說明:當前程式呼叫fork(),會建立一個跟當前程式完全相同的子程式(除了pid),所以子程式同樣是會執行fork()之後的程式碼。

所以說:

  • 父程式在執行if程式碼塊的時候,fpid變數的值是子程式的pid
  • 子程式在執行if程式碼塊的時候,fpid變數的值是0

1.2再來看看exec()函式

從上面我們已經知道了fork會建立一個子程式。子程式的是父程式的副本

exec函式的作用就是:裝載一個新的程式(可執行映像)覆蓋當前程式記憶體空間中的映像,從而執行不同的任務

  • exec系列函式在執行時會直接替換掉當前程式的地址空間

我去畫張圖來理解一下:

exec函式的作用

參考資料:

1.3回頭來看Linux下的COW是怎麼一回事

fork()會產生一個和父程式完全相同的子程式(除了pid)

如果按傳統的做法,會直接將父程式的資料拷貝到子程式中,拷貝完之後,父程式和子程式之間的資料段和堆疊是相互獨立的

父程式的資料拷貝到子程式中

但是,以我們的使用經驗來說:往往子程式都會執行exec()來做自己想要實現的功能。

  • 所以,如果按照上面的做法的話,建立子程式時複製過去的資料是沒用的(因為子程式執行exec(),原有的資料會被清空)

既然很多時候複製給子程式的資料是無效的,於是就有了Copy On Write這項技術了,原理也很簡單:

  • fork建立出的子程式,與父程式共享記憶體空間。也就是說,如果子程式不對記憶體空間進行寫入操作的話,記憶體空間中的資料並不會複製給子程式,這樣建立子程式的速度就很快了!(不用複製,直接引用父程式的物理空間)。
  • 並且如果在fork函式返回之後,子程式第一時間exec一個新的可執行映像,那麼也不會浪費時間和記憶體空間了。

另外的表達方式:

在fork之後exec之前兩個程式用的是相同的物理空間(記憶體區),子程式的程式碼段、資料段、堆疊都是指向父程式的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個

當父子程式中有更改相應段的行為發生時,再為子程式相應的段分配物理空間

如果不是因為exec,核心會給子程式的資料段、堆疊段分配相應的物理空間(至此兩者有各自的程式空間,互不影響),而程式碼段繼續共享父程式的物理空間(兩者的程式碼完全相同)。

而如果是因為exec,由於兩者執行的程式碼不同,子程式的程式碼段也會分配單獨的物理空間。

Copy On Write技術實現原理:

fork()之後,kernel把父程式中所有的記憶體頁的許可權都設為read-only,然後子程式的地址空間指向父程式。當父子程式都只讀記憶體時,相安無事。當其中某個程式寫記憶體時,CPU硬體檢測到記憶體頁是read-only的,於是觸發頁異常中斷(page-fault),陷入kernel的一箇中斷例程。中斷例程中,kernel就會把觸發的異常的頁複製一份,於是父子程式各自持有獨立的一份。

Copy On Write技術好處是什麼?

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

Copy On Write技術缺點是什麼?

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

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

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

參考資料:

二、解釋一下Redis的COW

基於上面的基礎,我們應該已經瞭解COW這麼一項技術了。

下面我來說一下我對《Redis設計與實現》那段話的理解:

  • Redis在持久化時,如果是採用BGSAVE命令或者BGREWRITEAOF的方式,那Redis會fork出一個子程式來讀取資料,從而寫到磁碟中
  • 總體來看,Redis還是讀操作比較多。如果子程式存在期間,發生了大量的寫操作,那可能就會出現很多的分頁錯誤(頁異常中斷page-fault),這樣就得耗費不少效能在複製上。
  • 而在rehash階段上,寫操作是無法避免的。所以Redis在fork出子程式之後,將負載因子閾值提高,儘量減少寫操作,避免不必要的記憶體寫入操作,最大限度地節約記憶體。

參考資料:

三、檔案系統的COW

下面來看看檔案系統中的COW是啥意思:

Copy-on-write在對資料進行修改的時候,不會直接在原來的資料位置上進行操作,而是重新找個位置修改,這樣的好處是一旦系統突然斷電,重啟之後不需要做Fsck。好處就是能保證資料的完整性,掉電的話容易恢復

  • 比如說:要修改資料塊A的內容,先把A讀出來,寫到B塊裡面去。如果這時候斷電了,原來A的內容還在!

參考資料:

最後

最後我們再來看一下寫時複製的思想(摘錄自維基百科):

寫入時複製(英語:Copy-on-write,簡稱COW)是一種計算機程式設計領域的優化策略。其核心思想是,如果有多個呼叫者(callers)同時請求相同資源(如記憶體或磁碟上的資料儲存),他們會共同獲取相同的指標指向相同的資源,直到某個呼叫者試圖修改資源的內容時,系統才會真正複製一份專用副本(private copy)給該呼叫者,而其他呼叫者所見到的最初的資源仍然保持不變。這過程對其他的呼叫者都是透明的(transparently)。此作法主要的優點是如果呼叫者沒有修改該資源,就不會有副本(private copy)被建立,因此多個呼叫者只是讀取操作時可以共享同一份資源。

至少從本文我們可以總結出:

  • Linux通過Copy On Write技術極大地減少了Fork的開銷
  • 檔案系統通過Copy On Write技術一定程度上保證資料的完整性

其實在Java裡邊,也有Copy On Write技術。

Java中的COW

這部分留到下一篇來說,敬請期待~

如果大家有更好的理解方式或者文章有錯誤的地方還請大家不吝在評論區留言,大家互相學習交流~~~

參考資料:

一個堅持原創的Java技術公眾號:Java3y,歡迎大家關注

3y所有的原創文章:

相關文章