1. 程式人生 > >寫時拷貝技術:Copy-On-Write

寫時拷貝技術:Copy-On-Write

寫時拷貝技術
1、概念
Scott Meyers在《More Effective
C++》中舉了個例子,不知你是否還記得?在你還在上學的時候,你的父母要你不要看電視,而去複習功課,於是你把自己關在房間裡,做出一副正在複習功課的
樣子,其實你在幹著別的諸如給班上的某位女生寫情書之類的事,而一旦你的父母出來在你房間要檢查你是否在複習時,你才真正撿起課本看書。這就是“拖延戰
術”,直到你非要做的時候才去做。

當然,這種事情在現實生活中時往往會出事,但其在程式設計世界中搖身一變,就成為了最有用的技術,正如C++中的可以隨處宣告變數的特點一樣,Scott
Meyers推薦我們,在真正需要一個儲存空間時才去宣告變數(分配記憶體),這樣會得到程式在執行時最小的記憶體花銷。執行到那才會去做分配記憶體這種比較耗
時的工作,這會給我們的程式在執行時有比較好的效能。必竟,20%的程式運行了80%的時間。

當然,拖延戰術還並不只是這樣一種型別,這種技術被我們廣泛地應用著,特別是在作業系統當中,當一個程式執行結束時,作業系統並不會急著把其清除出記憶體,
原因是有可能程式還會馬上再執行一次(從磁碟把程式裝入到記憶體是個很慢的過程),而只有當記憶體不夠用了,才會把這些還駐留記憶體的程式清出。

寫時才拷貝(Copy-On-Write)技術,就是程式設計界“懶惰行為”——拖延戰術的產物。舉個例子,比如我們有個程式要寫檔案,不斷地根據網路傳來的
資料寫,如果每一次fwrite或是fprintf都要進行一個磁碟的I/O操作的話,都簡直就是效能上巨大的損失,因此通常的做法是,每次寫檔案操作都
寫在特定大小的一塊記憶體中(磁碟快取),只有當我們關閉檔案時,才寫到磁碟上(這就是為什麼如果檔案不關閉,所寫的東西會丟失的原因)。更有甚者是檔案關
閉時都不寫磁碟,而一直等到關機或是記憶體不夠時才寫磁碟,Unix就是這樣一個系統,如果非正常退出,那麼資料就會丟失,檔案就會損壞。

呵呵,為了效能我們需要冒這樣大的風險,還好我們的程式是不會忙得忘了還有一塊資料需要寫到磁碟上的,所以這種做法,還是很有必要的。

2、標準C++類std::string的Copy-On-Write
在我們經常使用的STL標準模板庫中的string類,也是一個具有寫時才拷貝技術的類。C++曾在效能問題上被廣泛地質疑和指責過,為了提高性
能,STL中的許多類都採用了Copy-On-Write技術。這種偷懶的行為的確使使用STL的程式有著比較高要效能。

這裡,我想從C++類或是設計模式的角度為各位揭開Copy-On-Write技術在string中實現的面紗,以供各位在用C++進行類庫設計時做一點
參考。
在講述這項技術之前,我想簡單地說明一下string類記憶體分配的概念。通過常,string類中必有一個私有成員,其是一個char*,使用者記錄從堆上
分配記憶體的地址,其在構造時分配記憶體,在析構時釋放記憶體。因為是從堆上分配記憶體,所以string類在維護這塊記憶體上是格外小心的,string類在返回
這塊記憶體地址時,只返回const char*,也就是隻讀的,如果你要寫,你只能通過string提供的方法進行資料的改寫。
 
2.1、 特性

由表及裡,由感性到理性,我們先來看一看string類的Copy-On-Write的表面特徵。讓我們寫下下面的一段程式:

#include <string>

#include
<stdio.h>
using namespace std;

int main()

{

   string str1 = "hello world";

   string str2 = str1;

   printf ("Sharing the memory:/n");

   printf ("/tstr1's address: %x/n", str1.c_str() );

   printf ("/tstr2's address: %x/n", str2.c_str() );

   str1[1]='q';

   str2[1]='w';

   printf ("After Copy-On-Write:/n");

   printf ("/tstr1's address: %x/n", str1.c_str() );

   printf ("/tstr2's address: %x/n", str2.c_str() );

   return 0;
}

這個程式的意圖就是讓第二個string通過第一個string構造,然後打印出其存放資料的記憶體地址,然後分別修改str1和str2的內容,再查一下
其存放記憶體的地址。程式的輸出是這樣的(我在VC6.0和g++ 2.95都得到了同樣的結果):

> g++ -o stringTest stringTest.cpp

> ./stringTest
Sharing the memory:

    str1's address: 343be9

    str2's address: 343be9

    After Copy-On-Write:

    str1's address: 3407a9

    str2's address: 343be9

從結果中我們可以看到,在開始的兩個語句後,str1和str2存放資料的地址是一樣的,而在修改內容後,str1的地址發生了變化,而str2的地址還
是原來的。從這個例子,我們可以看到string類的Copy-On-Write技術。

2.2、 深入

在深入這前,通過上述的演示,我們應該知道在string類中,要實現寫時才拷貝,需要解決兩個問題,一個是記憶體共享,一個是Copy-On-
Wirte,這兩個主題會讓我們產生許多疑問,還是讓我們帶著這樣幾個問題來學習吧:

 1、 Copy-On-Write的原理是什麼?

 2、 string類在什麼情況下才共享記憶體的?

 3、 string類在什麼情況下觸發寫時才拷貝(Copy-On-Write)?

 4、 Copy-On-Write時,發生了什麼?

 5、 Copy-On-Write的具體實現是怎麼樣的?


喔,你說只要看一看STL中stirng的原始碼你就可以找到答案了。當然,當然,我也是參考了string的父模板類basic_string的原始碼。但
是,如果你感到看STL的原始碼就好像看機器碼,並嚴重打擊你對C++自信心,乃至產生了自己是否懂C++的疑問,如果你有這樣的感覺,那麼還是繼續往下看
我的這篇文章吧。
OK,讓我們一個問題一個問題地探討吧,慢慢地所有的技術細節都會浮出水面的。

2.3、Copy-On-Write的原理是什麼?

有一定經驗的程式設計師一定知道,Copy-On-Write一定使用了“引用計數”,是的,必然有一個變數類似於RefCnt。當第一個類構造時,string的建構函式會根據傳入的引數從堆上分配記憶體,當有其它類需要這塊記憶體時,這個計數為自動累加,當有類析構時,這個計數會減一,直到最後一
個類析構時,此時的RefCnt為1或是0,此時,程式才會真正的Free這塊從堆上分配的記憶體。

是的,引用計數就是string類中寫時才拷貝的原理!

不過,問題又來了,這個RefCnt該存在在哪裡呢?如果存放在string類中,那麼每個string的例項都有各自的一套,根本不能共有一個
RefCnt,如果是宣告成全域性變數,或是靜態成員,那就是所有的string類共享一個了,這也不行,我們需要的是一個“民主和集中”的一個解決方法。
這是如何做到的呢?呵呵,人生就是一個糊塗後去探知,知道後和又糊塗的迴圈過程。別急別急,在後面我會給你一一道來的。

2.3.1、string類在什麼情況下才共享記憶體的?

這個問題的答案應該是明顯的,根據常理和邏輯,如果一個類要用另一個類的資料,那就可以共享被使用類的記憶體了。這是很合理的,如果你不用我的,那就不用共享,只有你使用我的,才發生共享。

使用別的類的資料時,無非有兩種情況,1)以別的類構造自己,2)以別的類賦值。第一種情況時會觸發拷貝建構函式,第二種情況會觸發賦值操作符。這兩種情況我們都可以在類中實現其對應的方法。對於第一種情況,只需要在string類的拷貝建構函式中做點處理,讓其引用計數累加;同樣,對於第二種情況,只需要過載string類的賦值操作符,同樣在其中加上一點處理。

嘮叨幾句:

1)構造和賦值的差別
對於前面那個例程中的這兩句:
string str1 = "hello world";
string str2 = str1;
不要以為有“=”就是賦值操作,其實,這兩條語句等價於:
string str1 ("hello world"); //呼叫的是建構函式

string str2 (str1); //呼叫的是拷貝建構函式

如果str2是下面的這樣情況:
1.string str2; //呼叫引數預設為空串的建構函式:string str2(“”);

2.str2 = str1; //呼叫str2的賦值操作:str2.operator=(str1);

2) 另一種情況
char tmp[]=”hello world”;
string str1 = tmp;
string str2 = tmp;

這種情況下會觸發記憶體的共享嗎?想當然的,應該要共享。可是根據我們前面所說的共享記憶體的情況,兩個string類的宣告和初始語句並不符合我前述的兩種
情況,所以其並不發生記憶體共享。而且,C++現有特性也無法讓我們做到對這種情況進行類的記憶體共享。

2.3.2、string類在什麼情況下觸發寫時才拷貝(Copy-On-Write)?

哦,什麼時候會發現寫時才拷貝?很顯然,當然是在共享同一塊記憶體的類發生內容改變時,才會發生Copy-On-Write。比如string類的
[]、=、+=、+、操作符賦值,還有一些string類中諸如insert、replace、append等成員函式,包括類的析構時。

修改資料才會觸發Copy-On-Write,不修改當然就不會改啦。這就是託延戰術的真諦,非到要做的時候才去做。

2.3.3、Copy-On-Write時,發生了什麼?

我們可能根據那個訪問計數來決定是否需要拷貝,參看下面的程式碼:

If ( RefCnt>0 )

{

   char* tmp = (char*) malloc(strlen(_Ptr)+1);

   strcpy(tmp, _Ptr);

   _Ptr = tmp;

}

上面的程式碼是一個假想的拷貝方法,如果有別的類在引用(檢查引用計數來獲知)這塊記憶體,那麼就需要把更改類進行“拷貝”這個動作。

我們可以把這個拷的執行封裝成一個函式,供那些改變內容的成員函式使用。


2.3.4、Copy-On-Write的具體實現是怎麼樣的?

最後的這個問題,我們主要解決的是那個“民主集中”的難題。請先看下面的程式碼:


string h1 = “hello”;

string h2= h1;

string h3;

h3 = h2;

string w1 = “world”;
string w2(“”);

w2=w1;

很明顯,我們要讓h1、h2、h3共享同一塊記憶體,讓w1、w2共享同一塊記憶體。因為,在h1、h2、h3中,我們要維護一個引用計數,在w1、w2中我
們又要維護一個引用計數。
如何使用一個巧妙的方法產生這兩個引用計數呢?我們想到了string類的記憶體是在堆上動態分配的,既然共享記憶體的各個類指向的是同一個記憶體區,我們為什麼不在這塊區上多分配一點空間來存放這個引用計數呢?這樣一來,所有共享一塊記憶體區的類都有同樣的一個引用計數,而這個變數的地址既然是在共享區上的,那麼所有共享這塊記憶體的類都可以訪問到,也就知道這塊記憶體的引用者有多少了。

請看下圖:

於是,有了這樣一個機制,每當我們為string分配記憶體時,我們總是要多分配一個空間用來存放這個引用計數的值,只要發生拷貝構造可是賦值時,這個記憶體
的值就會加一。而在內容修改時,string類為檢視這個引用計數是否為0,如果不為零,表示有人在共享這塊記憶體,那麼自己需要先做一份拷貝,然後把引用
計數減去一,再把資料拷貝過來。下面的幾個程式片段說明了這兩個動作:

//建構函式(分存記憶體)

string::string(const char* tmp)

{

   _Len = strlen(tmp);

   _Ptr = new char[_Len+1+1];

   strcpy( _Ptr, tmp );

   _Ptr[_Len+1]=0; // 設定引用計數
}

//拷貝構造(共享記憶體)

string::string(const string& str)

{

   if (*this != str)

        {  

            this->_Ptr = str.c_str(); //共享記憶體

            this->_Len = str.szie();

            this->_Ptr[_Len+1] ++; //引用計數加一

         }
}

//寫時才拷貝Copy-On-Write

char& string::operator[](unsigned int idx)

{

    if (idx > _Len || _Ptr == 0 )

         {

             static char nullchar = 0;

             return nullchar;

         }

    _Ptr[_Len+1]--; //引用計數減一

    char* tmp = new char[_Len+1+1];

    strncpy( tmp, _Ptr, _Len+1);

    _Ptr = tmp;

    _Ptr[_Len+1]=0; // 設定新的共享記憶體的引用計數

    return _Ptr[idx];

 }

//解構函式的一些處理


~string()
{

   _Ptr[_Len+1]--; //引用計數減一

  // 引用計數為0時,釋放記憶體

   if (_Ptr[_Len+1]==0)

       {

          delete[] _Ptr;

        }

 }

哈哈,整個技術細節完全浮出水面。

不過,這和STL中basic_string的實現細節還有一點點差別,在你開啟STL的原始碼時,你會發現其取引用計數是通過這樣的訪問:_Ptr[-1],標準庫中,把這個引用計數的記憶體分配在了前面(我給出來的程式碼是把引用計數分配以了後面,這很不好),分配在前的好處是當
string的長度擴充套件時,只需要在後面擴充套件其記憶體,而不需要移動引用計數的記憶體存放位置,這又節省了一點時間。

STL中的string的記憶體結構就像我前面畫的那個圖一樣,_Ptr指著是資料區,而RefCnt則在_Ptr-1 或是 _Ptr[-1]處。

2.4、臭蟲

  Bug

是誰說的“有太陽的地方就會有黑暗”?或許我們中的許多人都很迷信標準的東西,認為其是久經考驗,不可能出錯的。呵呵,千萬不要有這種迷信,因為任何設計再好,編碼再好的程式碼在某一特定的情況下都會有Bug,STL同樣如此,string類的這個共享記憶體/寫時才拷貝技術也不例外,而且這個Bug或許還會
讓你的整個程式crash掉!不信?!那麼讓我們來看一個測試案例
假設有一個動態連結庫(叫myNet.dll或myNet.so)中有這樣一個函式返回的是string類:

string GetIPAddress(string hostname)

{

   static string ip;

   ……

   ……

   return ip;

}

而你的主程式中動態地載入這個動態連結庫,並呼叫其中的這個函式:

main()
{

 //載入動態連結庫中的函式

   hDll = LoadLibraray(…..);

   pFun = GetModule(hDll, “GetIPAddress”);

//呼叫動態連結庫中的函式
   string ip = (*pFun)(“host1”);

   ……

   ……

 //釋放動態連結庫

   FreeLibrary(hDll);

   ……

   cout << ip << endl;

}

讓我們來看看這段程式碼,程式以動態方式載入動態連結庫中的函式,然後以函式指標的方式呼叫動態連結庫中的函式,並把返回值放在一個string類中,然後
釋放了這個動態連結庫。釋放後,輸入ip的內容。
根據函式的定義,我們知道函式是“值返回”的,所以,函式返回時,一定會呼叫拷貝建構函式,又根據string類的記憶體共享機制,在主程式中變數ip是和
函式內部的那個靜態string變數共享記憶體(這塊記憶體區是在動態連結庫的地址空間的)。而我們假設在整個主程式中都沒有對ip的值進行修改過。那麼在當
主程式釋放了動態連結庫後,那個共享的記憶體區也隨之釋放。所以,以後對ip的訪問,必然做造成記憶體地址訪問非法,造成程式crash。即使你在以後沒有使
用到ip這個變數,那麼在主程式退出時也會發生記憶體訪問異常,因為程式退出時,ip會析構,在析構時就會發生記憶體訪問異常。

記憶體訪問異常,意味著兩件事:1)無論你的程式再漂亮,都會因為這個錯誤變得暗淡無光,你的聲譽也會因為這個錯誤受到損失。2)未來的一段時間,你會被這
個系統級錯誤所煎熬(在C++世界中,找到並排除這種記憶體錯誤並不是一件容易的事情)。這是C/C++程式設計師永遠的心頭之痛,千里之堤,潰於蟻穴。而如果
你不清楚string類的這種特徵,在成千上萬行程式碼中找這樣一個記憶體異常,簡直就是一場噩夢。

備註:要改正上述的Bug,有很多種方法,這裡提供一種僅供參考:
string ip = (*pFun)(“host1”).cstr();

相關推薦

拷貝技術Copy-On-Write

寫時拷貝技術 1、概念 Scott Meyers在《More Effective C++》中舉了個例子,不知你是否還記得?在你還在上學的時候,你的父母要你不要看電視,而去複習功課,於是你把自己關在房間裡,做出一副正在複習功課的 樣子,其實你在幹著別的諸如給班上的某位女生寫情

拷貝COW(copy-on-write

display 語句 namespace div str pre style -a [0 寫時拷貝技術是通過"引用計數"實現的,在分配空間的時候多分配4個字節,用來記錄有多少個指針指向塊空間,當有新的指針指向這塊空間時,引用計數加一,當要釋放這塊空間時,引用計數減一

Linux拷貝技術(copy-on-write)

但是 現在 進程地址空間 優化 如何 進程創建 http exe fork COW技術初窺: 在Linux程序中,fork()會產生一個和父進程完全相同的子進程,但子進程在此後多會exec系統調用,出於效率考慮,linux中引入了“寫時復制“技術,也就是只有進程

Linux拷貝技術(copy-on-write)及fork、vfork流程介紹

COW技術初窺:       在Linux程式中,fork()會產生一個和父程序完全相同的子程序,但子程序在此後多會exec系統呼叫,出於效率考慮,linux中引入了“寫時複製“技術,也就是隻有程序空間的各段的內容要發生變化時,才會將父程序的內容複製一份給子程序。       那麼子程序的物理空間沒有程式碼

(轉)Linux拷貝技術(copy-on-write)

轉自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html COW技術初窺:       在Linux程式中,fork()會產生一個和父程序完全相同的子程序,但子程序在此後多會exec系統呼叫,出於效率考慮,linux中引入了“

拷貝技術(copy-on-write)

 傳統的fork()系統呼叫直接把所有的資源複製給新建立的程序。這種實現過於簡單並且效率低下,因為它拷貝的資料也許並不共享,更糟的情況是,如果新程序打算立即執行一個新的映像,那麼所有的拷貝都將前功盡棄。Linux的fork()使用寫時拷貝(copy-on-write)頁實現

Linux拷貝技術fork

源於網上資料 COW技術初窺:       在Linux程式中,fork()會產生一個和父程序完全相同的子程序,但子程序在此後多會exec系統呼叫,出於效率考慮,linux中引入了“寫時複製“技術,也就是隻有程序空間的各段的內容要發生變化時,才會將父程序的內容複製一份給子程序。       那麼子程序的物理

Java高併發程式設計Copy-On-Write容器

Copy-On-Write簡稱COW,是一種用於程式設計中的優化策略。其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,才會真正把內容Copy出去形成一個新的內容然後再改,這是一種延時懶惰策略。從JDK1.5開始Java併發包裡提供了兩個使用CopyOnWrite

linux拷貝技術

COW技術初窺:       在Linux程式中,fork()會產生一個和父程序完全相同的子程序,但子程序在此後多會exec系統呼叫,出於效率考慮,linux中引入了“寫時複製“技術,也就是隻有程序空間的各段的內容要發生變化時,才會將父程序的內容複製一份給子程序。       那麼子程序的物理空間沒有

Linux記憶體管理之程序建立的拷貝技術

Unix的程序建立很特別。許多其他的作業系統都提供了產生程序的機制,首先在新的地址空間建立程序,讀入可執行的檔案,最後開始執行。Unix採用了與眾不同的實現方式,它把上述步驟分解到兩個單獨的函式中去執行:fork()和exec()。(這裡的exec是指exec一族的函式,核

拷貝Copy-On-Write技術

1、概念 Scott Meyers在《More Effective C++》 中舉了個例子,不知你是否還記得?在你還在上學的時候,你的父母要你不要看電視,而去複習功課,於是你把自己關在房間裡,做出一副正在複習功課的樣子,其 實你在幹著別的諸如給班上的某位女生寫情書之類的事,而一旦你的父母出來在你房間要檢查你是

string類的簡單實現(拷貝Copy-on-write

前言:上一篇文章實現了string的深拷貝寫法;那我們能不能用淺拷貝寫string類呢?當然可以; 一、 (1) 當我們需要對拷貝之後的物件進行修改時,採用深拷貝的方式; 如果不需要修改,只是輸出字串的內容時,或者是當偶爾修改的的時候,我們再採用深拷貝的方

【C++】c++拷貝Copy On Write

Copy On Write Copy On Write(寫時複製)使用了“引用計數”(reference counting),會有一個變數用於儲存引用的數量。當第一個類構造時,string的建構函式會根據傳入的引數從堆上分配記憶體,當有其它類需要

拷貝(copy on write)

寫時拷貝和傳統深拷貝的區別:深拷貝是,每建立一個物件,則開闢一塊空間,不管讀寫 而寫時拷貝是用一塊空間count計數指向同一塊空間指標的數量。 如果只讀不寫,則只需要開闢一次空間。效率很高,記憶體佔用

拷貝Copy On Write)方案詳解

class String { public: String(char * str = "" ) //不能strlen(NULL) { _str = new char[strlen( str) + 5];

Copy-On-Write複製機制與Java中CopyOnWriteArrayList容器原始碼實現

Copy-on-Write機制簡稱COW,是一種併發設計策略。其基本思路是多執行緒同時共享同一個內容,當某個執行緒想要修改這個內容的時候,才會真正的把內容copy出去形成一個新的內容然後修改,其它的執行緒繼續讀舊的內容,直到修改完成。這是一種延時懶惰策略。 Copy-on-Write有

Java 中的複製 (Copy on Write, COW)

Background 寫時複製 (Copy on Write, COW) 有時也叫 "隱式共享", 顧名思義, 就是讓所有需要使用資源 R 的使用者共享資源 R 的同一個副本, 當其中的某一個使用者要對資源 R 進行修改操作時, 先複製 R 的一個副本 R' , 再進行修改操作; Problem 在 J

Copy-On-Write(寫入複製)技術

看google的hdfs論文時看到這個概念。 Copy-On-Write屬於邏輯快照的一種,還有一種物理快照,百度了一下 ,快照裡邊概念還挺多,主要是以前沒接觸過。以後再整理。 Copy-On-Write是寫入時才複製的意思,找到兩個例子 1.往磁碟寫資料,先寫到的是記憶體

【轉】Copy-On-Write技術 [ linux fork程序使用技術]

inux核心在使用fork建立程序時,基本上會使用Copy-On-Write(COW)技術。這裡解釋一下COW技術以及為什麼在fork中使用。 WIKI上對COW的解釋: Copy-on-write (sometimes referred to as "COW") is an optimization 

從win32中的複製(Copy on write )機制談起

我們知道,記憶體對映檔案的物理儲存器來自磁碟上已有的檔案,而不是來自也交換檔案。系統在載入exe和dll檔案的時候使用的是記憶體對映檔案來載入並執行exe和dll,這樣大大節省了頁交換檔案的空間以及應用程式的啟動時間。所以,實際上系統載入exe檔案的時候就是利用記憶體對映檔