copy-on-write,即寫時複制技術,這是小編在學習 Redis 持久化時看到的一個概念,當然在這個概念很早就碰到過(Java 容器並發有這個概念),但是一直都沒有深入研究過,所以趁著這次機會對這個概念深究下。所以寫篇文章記錄下。

COW(copy-on-write 的簡稱),是一種計算機設計領域的優化策略,其核心思想是:如果有多個調用者(callers)同時要求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統才會真正複制一份專用副本(private copy)給該調用者,而其他調用者所見到的最初的資源仍然保持不變。這過程對其他的調用者都是透明的(transparently)。此作法主要的優點是如果調用者沒有修改該資源,就不會有副本(private copy)被創建,因此多個調用者只是讀取操作時可以共享同一份資源(摘自 維基百科)。

Linux 中的 copy-on-write

要理解 Linux 的 COW,必須要清楚兩個函數 fork()exec(),其中 exec() 是一組函數的統稱,包括 execl()execlp()execv()execle()execve()execvp()

fork()

fork() 是什麼?它是 UNIX 操作系統中派生新進程的唯一方法,用於創建子進程,該子進程等同於其父進程的副本,他們具有相同的物理空間(內存區),子進程的代碼段、數據段、堆棧都是指向父進程的物理空間,注意是在執行 exec() 之前。

fork() 函數有一個特點就是,它是 調用一次,返回兩次,調用是在父進程中調用創建子進程,返回有兩個值,一個是返回給父進程,返回值為新子進程的進程 ID 號,一個返回給子進程,返回值為 0,所以我們基本上就可以根據返回值判斷當前進程是子進程還是父進程。

因為任何子進程只有一個父進程,我們可以通過調用 getppid 獲取父進程的進程 ID,而父進程可以擁有多個子進程,所以 fork() 之後返回的就是子進程的進程 ID,這樣它才能識別它的子進程。

exec()

fork() 創建的子進程其實就是父進程的副本,如果僅僅只是 fork 一個父進程副本其實沒有多大意義,我們肯定希望的子進程能够幹一些活,一些與父進程不一樣的活,這個時候函數 exec() 就派上用場了。它的作用是 裝載一個新的程序,覆蓋當前進程內存空間中的映像,從而執行不同的任務

比如父進程要打印 hello world ,fork 出來的子進程將也是打印 hello world的。但是當子進程執行 exec() 後,就不一定是打印 hello world 了,有可能是執行 1 + 1 = 2。如下圖:

關於 fork()exec() 的文章推薦如下:

fork 會產生和父進程完全相同的子進程,如果采用傳統的做法,會直接將父進程的數據複制到子進程中去,子進程創建完成後,父進程和子進程之間的數據段和堆棧就完成獨立了,按照我們的慣例,子進程一般都會執行與父進程不一樣的功能,exec() 後會將原有的數據清空,這樣前面的複制過程就會變得無效了,這是一個非常浪費的過程,既然很多時間這種傳統的複制方式是無效的,於是就有了 copy-on-write 技術的,原理也是非常簡單的:

fork 的子進程與父進程共享內存空間,如果子進程不對內存空間進行修改的花,內存空間的數據並不會真實複制給子進程,這樣的結果會讓子進程創建的速度變得很快(不用複制,直接引用父進程的物理空間)。

fork 之後,子進程執行 exec() 也不會造成空間的浪費。

如下:

在網上看到還有個細節問題就是,fork之後內核會通過將子進程放在隊列的前面,以讓子進程先執行,以免父進程執行導致寫時複制,而後子進程執行exec系統調用,因無意義的複制而造成效率的下降。

Copy On Write技術實現原理:

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

Redis 中的 copy-on-write

我們知道 Redis 是單線程的,然後 Redis 的數據不可能一直存在內存中,肯定需要定時刷入硬盤中去的,這個過程則是 Redis 的持久化過程,那麼作為單線程的 Redis 是怎麼實現一邊響應客戶端命令一邊持久化的呢?答案就是依賴 COW,具體來說就是依賴系統的 fork 函數的 COW 實現的。

Redis 持久化有兩種:RDB 快照 和 AOF 日志。

RDB 快照錶示的是某一時刻 Redis 內存中所有數據的寫照。在執行 RDB 持久化時,Redis 進程會 fork 一個子進程來執行持久化過程,該過程是阻塞的,當 fork 過程完成後父進程會繼續接收客戶端的命令。子進程與 Redis 進程共享內存中的數據,但是子進程並不會修改內存中的數據,而是不斷的遍曆讀取寫入文件中,但是 Redis 父進程則不一樣,它需要響應客戶端的命令對內存中數據不斷地修改,這個時候就會使用操作系統的 COW 機制來進行數據段頁面的分離,當 Redis 父進程對其中某一個頁面的數據進行修改時,則會將頁面的數據複制一份出來,然後對這個複制頁進行修改,這個時候子進程相應的數據頁並沒有發生改變,依然是 fork 那一瞬間的數據。

AOF 日志則是將每個收到的寫命令都寫入到日志文件中來保證數據的不丟失。但是這樣會產生一個問題,就是隨著時間的推移,日志文件會越來越大,所以 Redis 提供了一個重寫過程(bgrewriteaof)來對日志文件進行壓縮。該重寫過程也會調用 fork() 函數產生一個子進程來進行文件壓縮。

關於 Redis 的持久化,請看這篇文章:【死磕 Redis】---- Redis 的持久化

Java 中的 copy-on-write

熟悉 Java 並發的同學一定知道 Java 中也有兩個容器使用了 copy-on-write 機制,他們分別是 CopyOnWriteArrayList 和 CopyOnWriteArraySet,他在我們並發使用場景中用處還是挺多的。現在我們就 CopyOnWriteArrayList 來簡單分析下 Java 中的 copy-on-write。

CopyOnWriteArrayList 實現 List 接口,底層的實現是采用數組來實現的。內部持有一個私有數組 array 用於存放各個元素。

private transient volatile Object[] array;

該數組不允許直接訪問,只允許 getArray()setArray() 訪問。

 final Object[] getArray() {
return array;
} final void setArray(Object[] a) {
array = a;
}

既然是 copy-on-write 機制,那麼對於讀肯定是直接訪問該成員變量 array,如果是其他修改操作,則肯定是先複制一份新的數組出來,然後操作該新的數組,最後將指針指向新的數組即可,以 add 操作為例,如下:

 public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 獲取老數組
Object[] elements = getArray();
int len = elements.length; // 複制出新數組
Object[] newElements = Arrays.copyOf(elements, len + 1); // 添加元素到新數組中
newElements[len] = e; //把原數組引用指向新數組
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

添加的時候使用了鎖,如果不使用鎖的話,可能會出現多線程寫的時候出現多個副本。

讀操作如下:

 public E get(int index) {
return get(getArray(), index);
} private E get(Object[] a, int index) {
return (E) a[index];
}

讀操作沒有加鎖,則可能會出現髒數據。

所以 Java 中的 COW 容器的原理如下:

當我們在修改一個容器中的元素時,並不是直接操作該容器,而是將當前容器進行 copy,複制出一個新的容器,然後在再對該新容器進行操作,操作完成後,將原容器的引用指向新容易,讀操作直接讀取老容器即可。

它體現的也是一種懶惰原則,也有點兒讀寫分離的意思(讀和寫操作的是不用的容器)

這兩個容器適合讀多寫少的場景,畢竟每次寫的時候都要獲取鎖和對數組進行複制處理,性能是大問題。

關於 Java 的 COW 更多資料,請看這篇文章:聊聊並發-Java中的Copy-On-Write容器

參考資料

【死磕 Java 基礎】 — 談談那個寫時拷貝技術(copy-on-write)的更多相關文章

  1. [轉] Linux寫時拷貝技術(copy-on-write)

    PS:http://blog.csdn.net/zxh821112/article/details/8969541 進程間是相互獨立的,其實完全可以看成A.B兩個進程各自有一份單獨的liba.so和l ...

  2. Linux寫時拷貝技術(copy-on-write)

    COW技術初窺: 在Linux程序中,fork()會產生一個和父進程完全相同的子進程,但子進程在此後多會exec系統調用,出於效率考慮,linux中引入了“寫時複制“技術,也就是只有進程空間的各段的內 ...

  3. 【轉】Linux寫時拷貝技術(copy-on-write)

    http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html 源於網上資料 COW技術初窺: 在Linux程序中,fork()會 ...

  4. copy-on-write(寫時拷貝技術)

    今天看<Unix環境高級編程>的fork函數與vfork函數時,看見一個copy-on-write的名詞,貌似以前也經常聽見別人說過這個,但也一直不明白這究竟是什麼東西.所以就好好在網上了 ...

  5. Linux寫時拷貝技術【轉】

    本文轉載自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html COW技術初窺: 在Linux程序中,fork()會產 ...

  6. Linux寫時拷貝技術(copy-on-write)

    1.傳統的fork()函數創建一個子進程,子進程和父進程共享正文段,複制數據段,堆,棧到子進程示意圖如下: 2.Linux的fork()函數-寫時複制(copy-on-write)創建一個子進程,內核 ...

  7. 寫時拷貝(Copy On Write)方案詳解

    本文旨在通過對 寫時拷貝 的四個方案(Copy On Write)分析,讓大家明白寫時拷貝的實現及原理. 關於淺拷貝與深拷貝,我在之前的博客中已經闡述過了  淺拷貝容易出現指針懸掛的問題,深拷貝效率低 ...

  8. 寫時拷貝COW(copy-on-write)

        寫時拷貝技術是通過"引用計數"實現的,在分配空間的時候多分配4個字節,用來記錄有多少個指針指向塊空間,當有新的指針指向這塊空間時,引用計數加一,當要釋放這塊空間時,引用計數 ...

  9. 死磕 java同步系列之自己動手寫一個鎖Lock

    問題 (1)自己動手寫一個鎖需要哪些知識? (2)自己動手寫一個鎖到底有多簡單? (3)自己能不能寫出來一個完美的鎖? 簡介 本篇文章的目標一是自己動手寫一個鎖,這個鎖的功能很簡單,能進行正常的加鎖. ...

  10. 【死磕Java並發】----- 死磕 Java 並發精品合集

    [死磕 Java 並發]系列是 LZ 在 2017 年寫的第一個死磕系列,一直沒有做一個合集,這篇博客則是將整個系列做一個概覽. 先來一個總覽圖: [高清圖,請關注"Java技術驛站&quo ...

隨機推薦

  1. MyEclipse 10 之下Web Service 的創建和實現

    (一)Web service服務端開發 1. 新建一個Web service project, 菜單New -> Web Service Project, 2. 新建一個 Java Bean, ...

  2. Xcode 之自己編譯靜態庫

    今天介紹下,如何利用Xcode,新建一個靜態庫,以及如何編譯成i386.armv7.armv7s 等平臺架構. 開發環境:MAC OS X 10.9.4 + Xcode 5.0.2 背景知識:庫分兩種 ...

  3. getResources提取資源文件

    String pxsize = context.getResources().getString(R.string.hello); 資源文件格式: <?xml version="1.0 ...

  4. Spring MVC執行流程

    SpringMVC是隸屬於Spring Web中的一部分, 屬於錶現層的框架. 其使用了MVC架構模式的思想, 將Web層進行職責解耦, 使用請求-響應模型簡化Web開發 SpringMVC通過中央調 ...

  5. .net ElasticSearch-Sql 擴展類【原創】

    官方提供的是java sdk,並支持jdbc方式的查詢結果輸出;但是卻沒有.net sdk的支持. 開發 ElasticSearch-Sql 第三方開源項目的.net sdk,未來集成入bsf框架.( ...

  6. 066 基於checkpoint的HA機制實現

    1.說明 針對需要恢複的應用場景,提供了HA的的機制 內部實現原理:基於checkpoint的 當程序被kill的時候,下次恢複的時候,會從checkpoint對用的文件中進行數據的恢複 2.HA原理 ...

  7. php laravel 多條件篩選

    效果如圖,點擊的條件出現在已選擇的地方,點擊已選擇的條件可以删除當前點擊的條件 語言是php 框架是laravel. 一.html <div class="doctor-conditi ...

  8. headfirst python 01~02

    列錶 列錶就像是數組 在python 創建一個列錶時, 解釋器會在內存中創建一個類似數組的數據結構來存儲數據, 數據項自下而上(形成一個堆棧), 類似於其他編程語言中的數組. 列錶中常用方法: cas ...

  9. Java如何獲取本地計算機的IP地址和主機名?

    在Java編程中,如何獲取本地計算機的IP地址和主機名? 以下示例顯示如何使用InetAddress類的getLocalAddress()方法獲取系統的本地IP地址和主機名. package com. ...

  10. 二、利用EnterpriseFrameWork快速開發Web系統(B/S)

    EnterpriseFrameWork框架實例源代碼下載: 實例下載 本章通過一個開發實例來講解Web系統的開發經過,以及會碰到的一些問題.實例功能就是業務系統中最常見的增.删.改.查來演示,用一個界 ...