之所以將Linux底層的寫時複製技術放在Redis篇幅下,是因為Redis進行RDB持久化時,BGSAVE
(後面稱之為"後臺儲存")會開闢一個子程式,將資料從記憶體寫進磁碟,這兒我產生了一個疑惑,就當這篇文章的引入場景:
如果我們記憶體中有4G資料,現在8:00執行後臺儲存,由於資料寫會磁碟需要時間,假設8:05資料才寫完畢,但是這中間的5分鐘,伺服器一直對外提供服務,如果很多資料在這期間遭受到了更改,那麼寫回磁碟的資料是8:00之前的資料還是儲存了8:00~8:05這段時間變化的資料呢?
如果儲存的是變化後的資料,那麼有一些問題需要繼續思考,資料寫回磁碟,勢必要經過buffer,那麼對於記憶體來說,完全寫完的時間是不太確定的,因為這中間資料一直在變化,沒法確定資料的邊界。
如果是儲存8:00那個時間片的資料快照,那也就是要將資料複製一份,避免伺服器提供服務時干擾到需要儲存的資料,這兒又有一個新問題,這麼整的話會不會記憶體溢位?畢竟記憶體是有限的,這樣簡單的複製就是double一下懷著這種疑問發現了Redis的bgsave命令底層其實是Linux的寫時複製技術
程式複製
在Linux程式中,fork()
會產生一個和父程式完全相同的子程式,但子程式在此後多會exec
系統呼叫,出於效率考慮,linux中引入了“寫時複製(Copy-On-Write)“技術,也就是隻有程式空間的各段的內容要發生變化時,才會將父程式的內容複製一份給子程式。關於程式空間淺析Linux程式空間佈局
那麼子程式的物理空間沒有程式碼,怎麼去取指令執行exec系統呼叫呢?
在fork
之後exec
之前兩個程式用的是相同的物理空間(記憶體區),子程式的程式碼段、資料段、堆疊都是指向父程式的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。
當父子程式中有更改相應段的行為發生時,再為子程式相應的段分配物理空間,如果不是因為exec
,核心會給子程式的資料段、堆疊段分配相應的物理空間(至此兩者有各自的程式空間,互不影響),而程式碼段繼續共享父程式的物理空間(兩者的程式碼完全相同)。而如果是因為exec
,由於兩者執行的程式碼不同,子程式的程式碼段也會分配單獨的物理空間。
還有個細節問題就是,fork
之後核心會通過將子程式放在佇列的前面,以讓子程式先執行,以免父程式執行導致寫時複製,而後子程式執行exec
系統呼叫,因無意義的複製而造成效率的下降。
程式空間結構
現在有一個父程式P1,這是一個主體,現在在其虛擬地址空間(有相應的資料結構表示)上有:正文段,資料段,堆,棧這四個部分(還有BSS、MMap等),相應的,核心要為這四個部分分配各自的物理塊。即:正文段塊,資料段塊,堆塊,棧塊。至於如何分配,這是核心去做的事,在此不詳述。
現在對比三種建立子程式的區別:
fork()
現在P1用fork()函式為程式建立一個子程式P2,核心操作:
複製P1的正文段,資料段,堆,棧這四個部分,注意是其內容相同。
為這四個部分分配物理塊,P2的:正文段->P1的正文段的物理塊,其實區別就是不為P2分配正文段塊,讓P2的正文段指向P1的正文段塊,資料段->P2自己的資料段塊(為其分配對應的塊),堆->P2自己的堆塊,棧->P2自己的棧塊。如下圖所示:同左到右大的方向箭頭表示複製內容。
P2:正文段===>PI的正文段的物理塊,其實就是不為P2分配正文段塊
P2的正文段===>P1的正文段塊
資料段===>P2自己的資料段塊(為其分配對應的塊)
堆===>P2自己的堆塊
棧===>P2自己的棧塊
如下圖所示:上面為父程式,下面為fork出來的子程式,可以看出只有子程式的正文段(Text Segment)是在實體記憶體重新分配的。
可以看見只有正文段實體記憶體會被重新分配。
寫時複製
寫時複製(Copy-On-Write),由前文可知,Linux複製子程式時,並不會為所有程式空間的塊都分配物理塊,寫時複製技術在Fork技術上有了進一步的優化,Text段也不重新分配實體記憶體,也就是剛分配時是下面這種形式:
寫時複製:核心只為新生成的子程式建立虛擬空間結構,它們來複制於父程式的虛擬究竟結構,但是不為這些段分配實體記憶體,任何段都不分配,它們共享父程式的物理空間,當父子程式中有更改相應段的行為發生時,再為子程式相應的段分配物理空間,例如途中的Stack塊,注意重新分配是以記憶體頁,也就是pagecache(4k)為基本單位的。
vfork()
這個做法更加火爆,核心連子程式的虛擬地址空間結構也不建立了,直接共享了父程式的虛擬空間,當然了,這種做法就順水推舟的共享了父程式的物理空間。
通過以上的分析,相信大家對程式有個深入的認識,它是怎麼一層層體現出自己來的,程式是一個主體,那麼它就有靈魂與身體,系統必須為實現它建立相應的實體, 靈魂實體與物理實體。這兩者在系統中都有相應的資料結構表示,物理實體更是體現了它的物理意義。
傳統的fork()系統呼叫直接把所有的資源複製給新建立的程式。這種實現過於簡單並且效率低下,因為它拷貝的資料也許並不共享,更糟的情況是,如果新程式打算立即執行一個新的映像,那麼所有的拷貝都將前功盡棄。Linux的fork()使用寫時拷貝(copy-on-write)頁實現。寫時拷貝是一種可以推遲甚至免除拷貝資料的技術。核心此時並不複製整個程式地址空間,而是讓父程式和子程式共享同一個拷貝。只有在需要寫入的時候,資料才會被複制,從而使各個程式擁有各自的拷貝。也就是說,資源的複製只有在需要寫入的時候才進行,在此之前,只是以只讀方式共享。這種技術使地址空間上的頁的拷貝被推遲到實際發生寫入的時候。在頁根本不會被寫入的情況下—舉例來說,fork()後立即呼叫exec()—它們就無需複製了。
fork()的實際開銷就是複製父程式的頁表以及給子程式建立惟一的程式描述符。在一般情況下,程式建立後都會馬上執行一個可執行的檔案,這種優化可以避免拷貝大量根本就不會被使用的資料(地址空間裡常常包含數十兆的資料)。由於Unix強調程式快速執行的能力,所以這個優化是很重要的。這裡補充一點:Linux COW與exec沒有必然聯絡