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!
如果接觸過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變數
1.2再來看看exec()函式
從上面我們已經知道了fork會建立一個子程序。子程序的是父程序的副本。
exec函式的作用就是:裝載一個新的程式(可執行映像)覆蓋當前程序記憶體空間中的映像,從而執行不同的任務。
-
exec系列函式在執行時會直接替換掉當前程序的地址空間。
我去畫張圖來理解一下:
exec函式的作用
參考資料:
-
程式設計師必備知識——fork和exec函式詳解https://blog.csdn.net/bad_good_man/article/details/49364947
-
linux中fork()函式詳解(原創!!例項講解):https://blog.csdn.net/jason314/article/details/5640969
-
linux c語言 fork() 和 exec 函式的簡介和用法:https://blog.csdn.net/nvd11/article/details/8856278
-
Linux下Fork與Exec使用:https://www.cnblogs.com/hicjiajia/archive/2011/01/20/1940154.html
-
Linux 系統呼叫 —— fork()核心原始碼剖析:https://blog.csdn.net/chen892704067/article/details/76596225
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()
把當前程序映像替換成新的程序檔案,完成自己想要實現的功能。
參考資料:
-
Linux程序基礎:http://www.cnblogs.com/vamei/archive/2012/09/20/2694466.html
-
Linux寫時拷貝技術(copy-on-write)http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html
-
當你在 Linux 上啟動一個程序時會發生什麼?https://zhuanlan.zhihu.com/p/33159508
-
Linux fork()所謂的寫時複製(COW)到最後還是要先複製再寫嗎?https://www.zhihu.com/question/265400460
-
寫時拷貝(copy-on-write) COW技術https://blog.csdn.net/u012333003/article/details/25117457
-
Copy-On-Write 寫時複製原理https://blog.csdn.net/ppppppppp2009/article/details/22750939
二、解釋一下Redis的COW
基於上面的基礎,我們應該已經瞭解COW這麼一項技術了。
下面我來說一下我對《Redis設計與實現》那段話的理解:
-
Redis在持久化時,如果是採用BGSAVE命令或者BGREWRITEAOF的方式,那Redis會fork出一個子程序來讀取資料,從而寫到磁碟中。
-
總體來看,Redis還是讀操作比較多。如果子程序存在期間,發生了大量的寫操作,那可能就會出現很多的分頁錯誤(頁異常中斷page-fault),這樣就得耗費不少效能在複製上。
-
而在rehash階段上,寫操作是無法避免的。所以Redis在fork出子程序之後,將負載因子閾值提高,儘量減少寫操作,避免不必要的記憶體寫入操作,最大限度地節約記憶體。
參考資料:
-
fork()後copy on write的一些特性:https://zhoujianshi.github.io/articles/2017/fork()%E5%90%8Ecopy%20on%20write%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/index.html
-
寫時複製:https://miao1007.github.io/gitbook/java/juc/cow/
三、檔案系統的COW
下面來看看檔案系統中的COW是啥意思:
Copy-on-write在對資料進行修改的時候,不會直接在原來的資料位置上進行操作,而是重新找個位置修改,這樣的好處是一旦系統突然斷電,重啟之後不需要做Fsck。好處就是能保證資料的完整性,掉電的話容易恢復。
-
比如說:要修改資料塊A的內容,先把A讀出來,寫到B塊裡面去。如果這時候斷電了,原來A的內容還在!
參考資料:
-
檔案系統中的 copy-on-write 模式有什麼具體的好處?https://www.zhihu.com/question/19782224/answers/created
-
新一代 Linux 檔案系統 btrfs 簡介:https://www.ibm.com/developerworks/cn/linux/l-cn-btrfs/
最後
最後我們再來看一下寫時複製的思想(摘錄自維基百科):
寫入時複製(英語:Copy-on-write,簡稱COW)是一種計算機程式設計領域的優化策略。其核心思想是,如果有多個呼叫者(callers)同時請求相同資源(如記憶體或磁碟上的資料儲存),他們會共同獲取相同的指標指向相同的資源,直到某個呼叫者試圖修改資源的內容時,系統才會真正複製一份專用副本(private copy)給該呼叫者,而其他呼叫者所見到的最初的資源仍然保持不變。這過程對其他的呼叫者都是透明的(transparently)。此作法主要的優點是如果呼叫者沒有修改該資源,就不會有副本(private copy)被建立,因此多個呼叫者只是讀取操作時可以共享同一份資源。
至少從本文我們可以總結出:
-
Linux通過Copy On Write技術極大地減少了Fork的開銷。
-
檔案系統通過Copy On Write技術一定程度上保證資料的完整性。
其實在Java裡邊,也有Copy On Write技術。
Java中的COW