COW奶牛!Copy On Write机制了解一下
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
前言
只有光頭才能變強(qiáng)
在讀《Redis設(shè)計與實現(xiàn)》關(guān)于哈希表擴(kuò)容的時候,發(fā)現(xiàn)這么一段話:
執(zhí)行BGSAVE命令或者BGREWRITEAOF命令的過程中,Redis需要創(chuàng)建當(dāng)前服務(wù)器進(jìn)程的子進(jìn)程,而大多數(shù)操作系統(tǒng)都采用寫時復(fù)制(copy-on-write)來優(yōu)化子進(jìn)程的使用效率,所以在子進(jìn)程存在期間,服務(wù)器會提高負(fù)載因子的閾值,從而避免在子進(jìn)程存在期間進(jìn)行哈希表擴(kuò)展操作,避免不必要的內(nèi)存寫入操作,最大限度地節(jié)約內(nèi)存。
觸及到知識的盲區(qū)了,于是就去搜了一下copy-on-write寫時復(fù)制這個技術(shù)究竟是怎么樣的。發(fā)現(xiàn)涉及的東西蠻多的,也挺難讀懂的。于是就寫下這篇筆記來記錄一下我學(xué)習(xí)copy-on-write的過程。
本文力求簡單講清copy-on-write這個知識點(diǎn),希望大家看完能有所收獲。
一、Linux下的copy-on-write
在說明Linux下的copy-on-write機(jī)制前,我們首先要知道兩個函數(shù):fork()和exec()。需要注意的是exec()并不是一個特定的函數(shù), 它是一組函數(shù)的統(tǒng)稱, 它包括了execl()、execlp()、execv()、execle()、execve()、execvp()。
1.1簡單來用用fork
首先我們來看一下fork()函數(shù)是什么鬼:
fork is an operation whereby a process creates a copy of itself.
fork是類Unix操作系統(tǒng)上創(chuàng)建進(jìn)程的主要方法。fork用于創(chuàng)建子進(jìn)程(等同于當(dāng)前進(jìn)程的副本)。
- 新的進(jìn)程要通過老的進(jìn)程復(fù)制自身得到,這就是fork!
如果接觸過Linux,我們會知道Linux下init進(jìn)程是所有進(jìn)程的爹(相當(dāng)于Java中的Object對象)
- Linux的進(jìn)程都通過init進(jìn)程或init的子進(jìn)程fork(vfork)出來的。
下面以例子說明一下fork吧:
#include?<unistd.h>?? #include?<stdio.h>??int?main?()??? {???pid_t?fpid;?//fpid表示fork函數(shù)返回的值??int?count=0;// 調(diào)用fork,創(chuàng)建出子進(jìn)程??fpid=fork();// 所以下面的代碼有兩個進(jìn)程執(zhí)行!if?(fpid?<?0)???printf("創(chuàng)建進(jìn)程失敗!/n");???else?if?(fpid?==?0)?{??printf("我是子進(jìn)程,由父進(jìn)程fork出來/n");???count++;??}??else?{??printf("我是父進(jìn)程/n");???count++;??}??printf("統(tǒng)計結(jié)果是:?%d/n",count);??return?0;?? }??得到的結(jié)果輸出為:
我是子進(jìn)程,由父進(jìn)程fork出來統(tǒng)計結(jié)果是: 1我是父進(jìn)程統(tǒng)計結(jié)果是: 1解釋一下:
- fork作為一個函數(shù)被調(diào)用。這個函數(shù)會有兩次返回,將子進(jìn)程的PID返回給父進(jìn)程,0返回給子進(jìn)程。(如果小于0,則說明創(chuàng)建子進(jìn)程失敗)。
- 再次說明:當(dāng)前進(jìn)程調(diào)用fork(),會創(chuàng)建一個跟當(dāng)前進(jìn)程完全相同的子進(jìn)程(除了pid),所以子進(jìn)程同樣是會執(zhí)行fork()之后的代碼。
所以說:
- 父進(jìn)程在執(zhí)行if代碼塊的時候,fpid變量的值是子進(jìn)程的pid
- 子進(jìn)程在執(zhí)行if代碼塊的時候,fpid變量的值是0
1.2再來看看exec()函數(shù)
從上面我們已經(jīng)知道了fork會創(chuàng)建一個子進(jìn)程。子進(jìn)程的是父進(jìn)程的副本。
exec函數(shù)的作用就是:裝載一個新的程序(可執(zhí)行映像)覆蓋當(dāng)前進(jìn)程內(nèi)存空間中的映像,從而執(zhí)行不同的任務(wù)。
- exec系列函數(shù)在執(zhí)行時會直接替換掉當(dāng)前進(jìn)程的地址空間。
我去畫張圖來理解一下:
參考資料:
- 程序員必備知識——fork和exec函數(shù)詳解https://blog.csdn.net/bad_good_man/article/details/49364947
- linux中fork()函數(shù)詳解(原創(chuàng)!!實例講解):https://blog.csdn.net/jason314/article/details/5640969
- linux c語言 fork() 和 exec 函數(shù)的簡介和用法:https://blog.csdn.net/nvd11/article/details/8856278
- Linux下Fork與Exec使用:https://www.cnblogs.com/hicjiajia/archive/2011/01/20/1940154.html
- Linux 系統(tǒng)調(diào)用 —— fork()內(nèi)核源碼剖析:https://blog.csdn.net/chen892704067/article/details/76596225
1.3回頭來看Linux下的COW是怎么一回事
fork()會產(chǎn)生一個和父進(jìn)程完全相同的子進(jìn)程(除了pid)
如果按傳統(tǒng)的做法,會直接將父進(jìn)程的數(shù)據(jù)拷貝到子進(jìn)程中,拷貝完之后,父進(jìn)程和子進(jìn)程之間的數(shù)據(jù)段和堆棧是相互獨(dú)立的。
但是,以我們的使用經(jīng)驗來說:往往子進(jìn)程都會執(zhí)行exec()來做自己想要實現(xiàn)的功能。
- 所以,如果按照上面的做法的話,創(chuàng)建子進(jìn)程時復(fù)制過去的數(shù)據(jù)是沒用的(因為子進(jìn)程執(zhí)行exec(),原有的數(shù)據(jù)會被清空)
既然很多時候復(fù)制給子進(jìn)程的數(shù)據(jù)是無效的,于是就有了Copy On Write這項技術(shù)了,原理也很簡單:
- fork創(chuàng)建出的子進(jìn)程,與父進(jìn)程共享內(nèi)存空間。也就是說,如果子進(jìn)程不對內(nèi)存空間進(jìn)行寫入操作的話,內(nèi)存空間中的數(shù)據(jù)并不會復(fù)制給子進(jìn)程,這樣創(chuàng)建子進(jìn)程的速度就很快了!(不用復(fù)制,直接引用父進(jìn)程的物理空間)。
- 并且如果在fork函數(shù)返回之后,子進(jìn)程第一時間exec一個新的可執(zhí)行映像,那么也不會浪費(fèi)時間和內(nèi)存空間了。
另外的表達(dá)方式:
在fork之后exec之前兩個進(jìn)程用的是相同的物理空間(內(nèi)存區(qū)),子進(jìn)程的代碼段、數(shù)據(jù)段、堆棧都是指向父進(jìn)程的物理空間,也就是說,兩者的虛擬空間不同,但其對應(yīng)的物理空間是同一個。
當(dāng)父子進(jìn)程中有更改相應(yīng)段的行為發(fā)生時,再為子進(jìn)程相應(yīng)的段分配物理空間。
如果不是因為exec,內(nèi)核會給子進(jìn)程的數(shù)據(jù)段、堆棧段分配相應(yīng)的物理空間(至此兩者有各自的進(jìn)程空間,互不影響),而代碼段繼續(xù)共享父進(jìn)程的物理空間(兩者的代碼完全相同)。
而如果是因為exec,由于兩者執(zhí)行的代碼不同,子進(jìn)程的代碼段也會分配單獨(dú)的物理空間。
Copy On Write技術(shù)實現(xiàn)原理:
fork()之后,kernel把父進(jìn)程中所有的內(nèi)存頁的權(quán)限都設(shè)為read-only,然后子進(jìn)程的地址空間指向父進(jìn)程。當(dāng)父子進(jìn)程都只讀內(nèi)存時,相安無事。當(dāng)其中某個進(jìn)程寫內(nèi)存時,CPU硬件檢測到內(nèi)存頁是read-only的,于是觸發(fā)頁異常中斷(page-fault),陷入kernel的一個中斷例程。中斷例程中,kernel就會把觸發(fā)的異常的頁復(fù)制一份,于是父子進(jìn)程各自持有獨(dú)立的一份。
Copy On Write技術(shù)好處是什么?
- COW技術(shù)可減少分配和復(fù)制大量資源時帶來的瞬間延時。
- COW技術(shù)可減少不必要的資源分配。比如fork進(jìn)程時,并不是所有的頁面都需要復(fù)制,父進(jìn)程的代碼段和只讀數(shù)據(jù)段都不被允許修改,所以無需復(fù)制。
Copy On Write技術(shù)缺點(diǎn)是什么?
- 如果在fork()之后,父子進(jìn)程都還需要繼續(xù)進(jìn)行寫操作,那么會產(chǎn)生大量的分頁錯誤(頁異常中斷page-fault),這樣就得不償失。
幾句話總結(jié)Linux的Copy On Write技術(shù):
- fork出的子進(jìn)程共享父進(jìn)程的物理空間,當(dāng)父子進(jìn)程有內(nèi)存寫入操作時,read-only內(nèi)存頁發(fā)生中斷,將觸發(fā)的異常的內(nèi)存頁復(fù)制一份(其余的頁還是共享父進(jìn)程的)。
- fork出的子進(jìn)程功能實現(xiàn)和父進(jìn)程是一樣的。如果有需要,我們會用exec()把當(dāng)前進(jìn)程映像替換成新的進(jìn)程文件,完成自己想要實現(xiàn)的功能。
參考資料:
- Linux進(jìn)程基礎(chǔ):http://www.cnblogs.com/vamei/archive/2012/09/20/2694466.html
- Linux寫時拷貝技術(shù)(copy-on-write)http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html
- 當(dāng)你在 Linux 上啟動一個進(jìn)程時會發(fā)生什么?https://zhuanlan.zhihu.com/p/33159508
- Linux fork()所謂的寫時復(fù)制(COW)到最后還是要先復(fù)制再寫嗎?https://www.zhihu.com/question/265400460
- 寫時拷貝(copy-on-write) COW技術(shù)https://blog.csdn.net/u012333003/article/details/25117457
- Copy-On-Write 寫時復(fù)制原理https://blog.csdn.net/ppppppppp2009/article/details/22750939
二、解釋一下Redis的COW
基于上面的基礎(chǔ),我們應(yīng)該已經(jīng)了解COW這么一項技術(shù)了。
下面我來說一下我對《Redis設(shè)計與實現(xiàn)》那段話的理解:
- Redis在持久化時,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那Redis會fork出一個子進(jìn)程來讀取數(shù)據(jù),從而寫到磁盤中。
- 總體來看,Redis還是讀操作比較多。如果子進(jìn)程存在期間,發(fā)生了大量的寫操作,那可能就會出現(xiàn)很多的分頁錯誤(頁異常中斷page-fault),這樣就得耗費(fèi)不少性能在復(fù)制上。
- 而在rehash階段上,寫操作是無法避免的。所以Redis在fork出子進(jìn)程之后,將負(fù)載因子閾值提高,盡量減少寫操作,避免不必要的內(nèi)存寫入操作,最大限度地節(jié)約內(nèi)存。
參考資料:
- fork()后copy on write的一些特性:https://zhoujianshi.github.io/articles/2017/fork()%E5%90%8Ecopy%20on%20write%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/index.html
- 寫時復(fù)制:https://miao1007.github.io/gitbook/java/juc/cow/
三、文件系統(tǒng)的COW
下面來看看文件系統(tǒng)中的COW是啥意思:
Copy-on-write在對數(shù)據(jù)進(jìn)行修改的時候,不會直接在原來的數(shù)據(jù)位置上進(jìn)行操作,而是重新找個位置修改,這樣的好處是一旦系統(tǒng)突然斷電,重啟之后不需要做Fsck。好處就是能保證數(shù)據(jù)的完整性,掉電的話容易恢復(fù)。
- 比如說:要修改數(shù)據(jù)塊A的內(nèi)容,先把A讀出來,寫到B塊里面去。如果這時候斷電了,原來A的內(nèi)容還在!
參考資料:
- 文件系統(tǒng)中的 copy-on-write 模式有什么具體的好處?https://www.zhihu.com/question/19782224/answers/created
- 新一代 Linux 文件系統(tǒng) btrfs 簡介:https://www.ibm.com/developerworks/cn/linux/l-cn-btrfs/
最后
最后我們再來看一下寫時復(fù)制的思想(摘錄自維基百科):
寫入時復(fù)制(英語:Copy-on-write,簡稱COW)是一種計算機(jī)程序設(shè)計領(lǐng)域的優(yōu)化策略。其核心思想是,如果有多個調(diào)用者(callers)同時請求相同資源(如內(nèi)存或磁盤上的數(shù)據(jù)存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調(diào)用者試圖修改資源的內(nèi)容時,系統(tǒng)才會真正復(fù)制一份專用副本(private copy)給該調(diào)用者,而其他調(diào)用者所見到的最初的資源仍然保持不變。這過程對其他的調(diào)用者都是透明的(transparently)。此作法主要的優(yōu)點(diǎn)是如果調(diào)用者沒有修改該資源,就不會有副本(private copy)被建立,因此多個調(diào)用者只是讀取操作時可以共享同一份資源。
至少從本文我們可以總結(jié)出:
- Linux通過Copy On Write技術(shù)極大地減少了Fork的開銷。
- 文件系統(tǒng)通過Copy On Write技術(shù)一定程度上保證數(shù)據(jù)的完整性。
其實在Java里邊,也有Copy On Write技術(shù)。
這部分留到下一篇來說,敬請期待~
如果大家有更好的理解方式或者文章有錯誤的地方還請大家不吝在評論區(qū)留言,大家互相學(xué)習(xí)交流~~~
參考資料:
- 寫時復(fù)制,寫時拷貝,寫時分裂,Copy on write:https://my.oschina.net/dubenju/blog/815836
- 不會產(chǎn)奶的COW(Copy-On-Write)https://www.jianshu.com/p/b2fb2ee5e3a0
一個堅持原創(chuàng)的Java技術(shù)公眾號:Java3y,歡迎大家關(guān)注
3y所有的原創(chuàng)文章:
- 文章的目錄導(dǎo)航(腦圖+海量視頻資源):https://github.com/ZhongFuCheng3y/3y
轉(zhuǎn)載于:https://my.oschina.net/u/3777556/blog/2354406
總結(jié)
以上是生活随笔為你收集整理的COW奶牛!Copy On Write机制了解一下的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 蚂蚁金服 Service Mesh 实践
- 下一篇: 给程序员的十点启示