1. 程式人生 > >Redis持久化之父子程序與寫時複製

Redis持久化之父子程序與寫時複製

之所以將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程序空間佈局](https://www.cnblogs.com/Courage129/p/14231781.html) 那麼子程序的物理空間沒有程式碼,怎麼去取指令執行exec系統呼叫呢? 在`fork`之後`exec`之前兩個程序用的是相同的物理空間(記憶體區),子程序的程式碼段、資料段、堆疊都是指向父程序的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。 當父子程序中有更改相應段的行為發生時,再為子程序相應的段分配物理空間,如果不是因為`exec`,核心會給子程序的資料段、堆疊段分配相應的物理空間(至此兩者有各自的程序空間,互不影響),而程式碼段繼續共享父程序的物理空間(兩者的程式碼完全相同)。而如果是因為`exec`,由於兩者執行的程式碼不同,子程序的程式碼段也會分配單獨的物理空間。 ![](https://img2020.cnblogs.com/blog/2002319/202101/2002319-20210129091422232-729327440.png) 還有個細節問題就是,`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)是在實體記憶體重新分配的。 ![](https://img2020.cnblogs.com/blog/2002319/202101/2002319-20210129091432705-875422829.png) 可以看見只有正文段實體記憶體會被重新分配。 ## 寫時複製 寫時複製(Copy-On-Write),由前文可知,Linux複製子程序時,並不會為所有程式空間的塊都分配物理塊,寫時複製技術在Fork技術上有了進一步的優化,Text段也不重新分配實體記憶體,也就是剛分配時是下面這種形式: ![](https://img2020.cnblogs.com/blog/2002319/202101/2002319-20210129091442008-681224132.png) 寫時複製:核心只為新生成的子程序建立虛擬空間結構,它們來複制於父程序的虛擬究竟結構,但是不為這些段分配實體記憶體,任何段都不分配,它們共享父程序的物理空間,當父子程序中有更改相應段的行為發生時,再為子程序相應的段分配物理空間,例如途中的Stack塊,注意重新分配是以記憶體頁,也就是pagecache(4k)為基本單位的。 ## vfork() 這個做法更加火爆,核心連子程序的虛擬地址空間結構也不建立了,**直接共享了父程序的虛擬空間**,當然了,這種做法就順水推舟的共享了父程序的物理空間。 通過以上的分析,相信大家對程序有個深入的認識,它是怎麼一層層體現出自己來的,程序是一個主體,那麼它就有靈魂與身體,系統必須為實現它建立相應的實體, 靈魂實體與物理實體。這兩者在系統中都有相應的資料結構表示,物理實體更是體現了它的物理意義。 傳統的fork()系統呼叫直接把所有的資源複製給新建立的程序。這種實現過於簡單並且效率低下,因為它拷貝的資料也許並不共享,更糟的情況是,如果新程序打算立即執行一個新的映像,那麼所有的拷貝都將前功盡棄。**Linux的fork()使用寫時拷貝(copy-on-write)頁實現**。寫時拷貝是一種可以推遲甚至免除拷貝資料的技術。核心此時並不複製整個程序地址空間,而是讓父程序和子程序共享同一個拷貝。只有在需要寫入的時候,資料才會被複制,從而使各個程序擁有各自的拷貝。也就是說,資源的複製只有在需要寫入的時候才進行,在此之前,只是以只讀方式共享。這種技術使地址空間上的頁的拷貝被推遲到實際發生寫入的時候。在頁根本不會被寫入的情況下—舉例來說,fork()後立即呼叫exec()—它們就無需複製了。 fork()的實際開銷就是複製父程序的頁表以及給子程序建立惟一的程序描述符。在一般情況下,程序建立後都會馬上執行一個可執行的檔案,這種優化可以避免拷貝大量根本就不會被使用的資料(地址空間裡常常包含數十兆的資料)。由於Unix強調程序快速執行的能力,所以這個優化是很重要的。這裡補充一點:**Linux COW與exec沒有必然