copy-on-write,即寫時複制技術,這是小編在學習 Redis 持久化時看到的一個概念,當然在這個概念很早就碰到過(Java 容器并發有這個概念),但是一直都沒有深入研究過,是以趁着這次機會對這個概念深究下。是以寫篇文章記錄下。
COW(copy-on-write 的簡稱),是一種計算機設計領域的優化政策,其核心思想是:如果有多個調用者(callers)同時要求相同資源(如記憶體或磁盤上的資料存儲),他們會共同擷取相同的指針指向相同的資源,直到某個調用者試圖修改資源的内容時,系統才會真正複制一份專用副本(private copy)給該調用者,而其他調用者所見到的最初的資源仍然保持不變。這過程對其他的調用者都是透明的(transparently)。此作法主要的優點是如果調用者沒有修改該資源,就不會有副本(private copy)被建立,是以多個調用者隻是讀取操作時可以共享同一份資源(摘自 維基百科)。
要了解 Linux 的 COW,必須要清楚兩個函數 <code>fork()</code>、<code>exec()</code>,其中 <code>exec()</code> 是一組函數的統稱,包括 <code>execl()</code>、<code>execlp()</code>、<code>execv()</code>、<code>execle()</code>、<code>execve()</code>、<code>execvp()</code>。
fork()
<code>fork()</code> 是什麼?它是 UNIX 作業系統中派生新程序的唯一方法,用于建立子程序,該子程序等同于其父程序的副本,他們具有相同的實體空間(記憶體區),子程序的代碼段、資料段、堆棧都是指向父程序的實體空間,注意是在執行 <code>exec()</code> 之前。
<code>fork()</code> 函數有一個特點就是,它是 調用一次,傳回兩次,調用是在父程序中調用建立子程序,傳回有兩個值,一個是傳回給父程序,傳回值為新子程序的程序 ID 号,一個傳回給子程序,傳回值為 0,是以我們基本上就可以根據傳回值判斷目前程序是子程序還是父程序。
因為任何子程序隻有一個父程序,我們可以通過調用 <code>getppid</code> 擷取父程序的程序 ID,而父程序可以擁有多個子程序,是以 <code>fork()</code> 之後傳回的就是子程序的程序 ID,這樣它才能識别它的子程序。
exec()
<code>fork()</code> 建立的子程序其實就是父程序的副本,如果僅僅隻是 fork 一個父程序副本其實沒有多大意義,我們肯定希望的子程序能夠幹一些活,一些與父程序不一樣的活,這個時候函數 <code>exec()</code> 就派上用場了。它的作用是 裝載一個新的程式,覆寫目前程序記憶體空間中的映像,進而執行不同的任務。
比如父程序要列印 hello world ,<code>fork</code> 出來的子程序将也是列印 hello world的。但是當子程序執行 <code>exec()</code> 後,就不一定是列印 hello world 了,有可能是執行 1 + 1 = 2。如下圖:

關于 <code>fork()</code> 與 <code>exec()</code> 的文章推薦如下:
程式員必備知識——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詳解:https://blog.csdn.net/sinat_35925219/article/details/52266261
linux系統程式設計之程序(五):exec系列函數(execl,execlp,execle,execv,execvp)使用:https://www.cnblogs.com/mickole/p/3187409.html
<code>fork</code> 會産生和父程序完全相同的子程序,如果采用傳統的做法,會直接将父程序的資料複制到子程序中去,子程序建立完成後,父程序和子程序之間的資料段和堆棧就完成獨立了,按照我們的慣例,子程序一般都會執行與父程序不一樣的功能,<code>exec()</code> 後會将原有的資料清空,這樣前面的複制過程就會變得無效了,這是一個非常浪費的過程,既然很多時間這種傳統的複制方式是無效的,于是就有了 copy-on-write 技術的,原理也是非常簡單的:
<code>fork</code> 的子程序與父程序共享記憶體空間,如果子程序不對記憶體空間進行修改的花,記憶體空間的資料并不會真實複制給子程序,這樣的結果會讓子程序建立的速度變得很快(不用複制,直接引用父程序的實體空間)。 <code>fork</code> 之後,子程序執行 <code>exec()</code> 也不會造成空間的浪費。
如下:
在網上看到還有個細節問題就是,fork之後核心會通過将子程序放在隊列的前面,以讓子程序先執行,以免父程序執行導緻寫時複制,而後子程序執行exec系統調用,因無意義的複制而造成效率的下降。
Copy On Write技術實作原理:
fork()之後,kernel把父程序中所有的記憶體頁的權限都設為read-only,然後子程序的位址空間指向父程序。當父子程序都隻讀記憶體時,相安無事。當其中某個程序寫記憶體時,CPU硬體檢測到記憶體頁是read-only的,于是觸發頁異常中斷(page-fault),陷入kernel的一個中斷例程。中斷例程中,kernel就會把觸發的異常的頁複制一份,于是父子程序各自持有獨立的一份。
我們知道 Redis 是單線程的,然後 Redis 的資料不可能一直存在記憶體中,肯定需要定時刷入硬碟中去的,這個過程則是 Redis 的持久化過程,那麼作為單線程的 Redis 是怎麼實作一邊響應用戶端指令一邊持久化的呢?答案就是依賴 COW,具體來說就是依賴系統的 <code>fork</code> 函數的 COW 實作的。
Redis 持久化有兩種:RDB 快照 和 AOF 日志。
RDB 快照表示的是某一時刻 Redis 記憶體中所有資料的寫照。在執行 RDB 持久化時,Redis 程序會 fork 一個子程序來執行持久化過程,該過程是阻塞的,當 fork 過程完成後父程序會繼續接收用戶端的指令。子程序與 Redis 程序共享記憶體中的資料,但是子程序并不會修改記憶體中的資料,而是不斷的周遊讀取寫入檔案中,但是 Redis 父程序則不一樣,它需要響應用戶端的指令對記憶體中資料不斷地修改,這個時候就會使用作業系統的 COW 機制來進行資料段頁面的分離,當 Redis 父程序對其中某一個頁面的資料進行修改時,則會将頁面的資料複制一份出來,然後對這個複制頁進行修改,這個時候子程序相應的資料頁并沒有發生改變,依然是 fork 那一瞬間的資料。
AOF 日志則是将每個收到的寫指令都寫入到日志檔案中來保證資料的不丢失。但是這樣會産生一個問題,就是随着時間的推移,日志檔案會越來越大,是以 Redis 提供了一個重寫過程(bgrewriteaof)來對日志檔案進行壓縮。該重寫過程也會調用 <code>fork()</code> 函數産生一個子程序來進行檔案壓縮。
關于 Redis 的持久化,請看這篇文章:【死磕 Redis】---- Redis 的持久化
熟悉 Java 并發的同學一定知道 Java 中也有兩個容器使用了 copy-on-write 機制,他們分别是 CopyOnWriteArrayList 和 CopyOnWriteArraySet,他在我們并發使用場景中用處還是挺多的。現在我們就 CopyOnWriteArrayList 來簡單分析下 Java 中的 copy-on-write。
CopyOnWriteArrayList 實作 List 接口,底層的實作是采用數組來實作的。内部持有一個私有數組 array 用于存放各個元素。
該數組不允許直接通路,隻允許 <code>getArray()</code> 和 <code>setArray()</code> 通路。
既然是 copy-on-write 機制,那麼對于讀肯定是直接通路該成員變量 array,如果是其他修改操作,則肯定是先複制一份新的數組出來,然後操作該新的數組,最後将指針指向新的數組即可,以 add 操作為例,如下:
添加的時候使用了鎖,如果不使用鎖的話,可能會出現多線程寫的時候出現多個副本。
讀操作如下:
讀操作沒有加鎖,則可能會出現髒資料。
是以 Java 中的 COW 容器的原理如下:
當我們在修改一個容器中的元素時,并不是直接操作該容器,而是将目前容器進行 copy,複制出一個新的容器,然後在再對該新容器進行操作,操作完成後,将原容器的引用指向新容易,讀操作直接讀取老容器即可。 它展現的也是一種懶惰原則,也有點兒讀寫分離的意思(讀和寫操作的是不用的容器) 這兩個容器适合讀多寫少的場景,畢竟每次寫的時候都要擷取鎖和對數組進行複制處理,性能是大問題。
關于 Java 的 COW 更多資料,請看這篇文章:聊聊并發-Java中的Copy-On-Write容器
COW奶牛!Copy On Write機制了解一下
PS:如果你覺得文章對你有所幫助,别忘了推薦或者分享,因為有你的支援,才是我續寫下篇的動力和源泉!
作者:chenssy。一個專注于【死磕 Java】系列創作的男人
出處:https://www.cnblogs.com/chenssy/p/15142814.html
作者個人網站:https://www.cmsblogs.com/。專注于 Java 優質系列文章分享,提供一站式 Java 學習資料
目前死磕系列包括:
1. 【死磕 Java 并發】:https://www.cmsblogs.com/category/1391296887813967872(已完成)
2.【死磕 Spring 之 IOC】:https://www.cmsblogs.com/category/1391374860344758272(已完成)
3.【死磕 Redis】:https://www.cmsblogs.com/category/1391389927996002304(已完成)
4.【死磕 Java 基礎】:https://www.cmsblogs.com/category/1411518540095295488
5.【死磕 NIO】:https://www.cmsblogs.com/article/1435620402348036096
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。