写时复制就这么几行代码,还是不会?
?
作者 | 閃客
來(lái)源 | 低并發(fā)編程
這里講的是 Linux 內(nèi)核里的寫(xiě)時(shí)復(fù)制原理。
寫(xiě)時(shí)復(fù)制的原理網(wǎng)上講述的文章很多,今天來(lái)一篇很直接的文章,通過(guò)看看 Linux 0.11 這個(gè)最簡(jiǎn)單的操作系統(tǒng),從源碼層面把寫(xiě)時(shí)復(fù)制的原理搞清楚。
很簡(jiǎn)單哦,你可別中途就放棄了。
直接干!
哦不行,干之前先來(lái)點(diǎn)儲(chǔ)備知識(shí),如果你已經(jīng)有了這一 pa 可以略過(guò),不過(guò)我估計(jì)你沒(méi)有...
儲(chǔ)備知識(shí)
堅(jiān)持看完這部分,寫(xiě)時(shí)復(fù)制用到的這里的知識(shí)點(diǎn)只有其中一個(gè)位的值而已,但我把周邊也給你講講。
32 位模式下,Intel 設(shè)計(jì)了頁(yè)目錄表和頁(yè)表兩種結(jié)構(gòu),用來(lái)給程序員們提供分頁(yè)機(jī)制。
在 Intel Volume-3 Chapter 4.3 Figure 4-4 中給出了頁(yè)表和頁(yè)目錄表的數(shù)據(jù)結(jié)構(gòu),PDE 就是頁(yè)目錄表,PTE 就是頁(yè)表。
大部分的操作系統(tǒng)使用的都是 4KB 的頁(yè)框大小,Linux 0.11 也是,所以我們只看 4KB 頁(yè)大小時(shí)的情況即可。
一個(gè)由程序員給出的邏輯地址,要先經(jīng)過(guò)分段機(jī)制的轉(zhuǎn)化變成線(xiàn)性地址,再經(jīng)過(guò)分頁(yè)機(jī)制的轉(zhuǎn)化變成物理地址。
Figure 4-2 給出了線(xiàn)性地址到物理地址,也就是分頁(yè)機(jī)制的轉(zhuǎn)化過(guò)程。
這里的 PDE 就是頁(yè)目錄表,PTE 就是頁(yè)表,剛剛說(shuō)過(guò)了。
在手冊(cè)接下來(lái)的 Table 4-5 和 Table 4-6 中,詳細(xì)解釋了頁(yè)目錄表和頁(yè)表數(shù)據(jù)結(jié)構(gòu)各字段的含義。
Table 4-5 是頁(yè)目錄表。
Table 4-6 是頁(yè)表。
他們幾乎都是一樣的含義,我們就只看頁(yè)表就好了,看一些比較重要的位。
31:12 表示頁(yè)的起始物理地址,加上線(xiàn)性地址的后 12 位偏移地址,就構(gòu)成了最終要訪(fǎng)問(wèn)的內(nèi)存的物理地址,這個(gè)就不說(shuō)了。
第 0 位是 P,表示 Present,存在位。
第 1 位是 RW,表示讀寫(xiě)權(quán)限,0 表示只讀,那么此時(shí)往這個(gè)頁(yè)表示的內(nèi)存范圍內(nèi)寫(xiě)數(shù)據(jù),則不允許。
第 2 位是 US,表示用戶(hù)態(tài)還是內(nèi)核態(tài),0 表示內(nèi)核態(tài),那么此時(shí)用戶(hù)態(tài)的程序往這個(gè)內(nèi)存范圍內(nèi)寫(xiě)數(shù)據(jù),則不允許。
在 Linux 0.11 的 head.s 里,初次為頁(yè)表設(shè)置的值如下。
setup_paging:...movl?$pg0+7,_pg_dir?????/*?set?present?bit/user?r/w?*/movl?$pg1+7,_pg_dir+4???????/*??---------?"?"?---------?*/movl?$pg2+7,_pg_dir+8???????/*??---------?"?"?---------?*/movl?$pg3+7,_pg_dir+12??????/*??---------?"?"?---------?*/movl?$pg3+4092,%edimovl?$0xfff007,%eax?????/*??16Mb?-?4096?+?7?(r/w?user,p)?*/std 1:??stosl...后三位是 7,用二進(jìn)制表示就是 111,即初始設(shè)置的 4 個(gè)頁(yè)目錄表和 1024 個(gè)頁(yè)表,都是:
存在(1),可讀寫(xiě)(1),用戶(hù)態(tài)(1)
好了,儲(chǔ)備知識(shí)就到這里。
如果你前面沒(méi)讀懂,你只需要知道,頁(yè)表當(dāng)中有一位是表示讀\寫(xiě)的,而 Linux 0.11 初始化時(shí),把它設(shè)置為了 1,表示可讀寫(xiě)。
寫(xiě)時(shí)復(fù)制的本質(zhì)
在調(diào)用 fork() 生成新進(jìn)程時(shí),新進(jìn)程與原進(jìn)程會(huì)共享同一內(nèi)存區(qū)。只有當(dāng)其中一個(gè)進(jìn)程進(jìn)行寫(xiě)操作時(shí),系統(tǒng)才會(huì)為其另外分配內(nèi)存頁(yè)面。
不過(guò)我們考慮寫(xiě)時(shí)復(fù)制并不用這么復(fù)雜,去掉些細(xì)節(jié)就是。
原來(lái)的進(jìn)程通過(guò)自己的頁(yè)表占用了一定范圍的物理內(nèi)存空間。
調(diào)用 fork 創(chuàng)建新進(jìn)程時(shí),原本頁(yè)表和物理地址空間里的內(nèi)容,都要進(jìn)行復(fù)制,因?yàn)檫M(jìn)程的內(nèi)存空間是要隔離的嘛。
但 fork 函數(shù)認(rèn)為,復(fù)制物理地址空間里的內(nèi)容,比較費(fèi)時(shí),所以姑且先只復(fù)制頁(yè)表,物理地址空間的內(nèi)容先不復(fù)制。
如果只有讀操作,那就完全沒(méi)有影響,復(fù)不復(fù)制物理地址空間里的內(nèi)容就無(wú)所謂了,這就很賺。但如果有寫(xiě)操作,那就不得不把物理地址空間里的值復(fù)制一份,保證進(jìn)程間的內(nèi)存隔離。
有寫(xiě)操作時(shí),再?gòu)?fù)制物理內(nèi)存,就叫寫(xiě)時(shí)復(fù)制。
看看代碼咋寫(xiě)的
有上述的現(xiàn)象,必然是在 fork 時(shí),對(duì)頁(yè)表做了手腳,這回知道為啥儲(chǔ)備知識(shí)里講頁(yè)表結(jié)構(gòu)了吧??
同時(shí),只要有寫(xiě)操作,就會(huì)觸發(fā)寫(xiě)時(shí)復(fù)制這個(gè)邏輯,這是咋做到的呢?答案是通過(guò)中斷,具體是缺頁(yè)中斷。
好的,首先來(lái)看 fork。
這里只看其中關(guān)鍵的復(fù)制頁(yè)表的代碼。
int?copy_page_tables(...)?{...//?源頁(yè)表和新頁(yè)表一樣this_page?=?*from_page_table;...//?源頁(yè)表和新頁(yè)表均置為只讀this_page?&=?~2;*from_page_table?=?this_page;... }還記得知識(shí)儲(chǔ)備當(dāng)中的頁(yè)表結(jié)構(gòu)吧,就是把 R/W 位置 0 了。
用剛剛的 fork 圖表示就是。
那么此時(shí),再次對(duì)這塊物理地址空間進(jìn)行寫(xiě)操作時(shí),就不允許了。
但不允許并不是真的不允許,Intel 會(huì)觸發(fā)一個(gè)缺頁(yè)中斷,具體是 0x14 號(hào)中斷,中斷處理程序里邊怎么處理,那就由 Linux 源碼自由發(fā)揮了。
Linux 0.11 的缺頁(yè)中斷處理函數(shù)的開(kāi)頭是用匯編寫(xiě)的,看著太鬧心了,這里我選 Linux 1.0 的代碼給大家看,邏輯是一樣的。
void?do_page_fault(...,?unsigned?long?error_code)?{...???if?(error_code?&?1)do_wp_page(error_code,?address,?current,?user_esp);elsedo_no_page(error_code,?address,?current,?user_esp);... }可以看出,根據(jù)中斷異常碼 error_code 的不同,有不同的邏輯。
那觸發(fā)缺頁(yè)中斷的異常碼都有哪些呢?
在 Intel Volume-3 Chapter 4.7 Figure 4-12 中給出。
可以看出,當(dāng) error_code 的第 0 位,也就是存在位為 0 時(shí),會(huì)走 do_no_page 邏輯,其余情況,均走 do_wp_page 邏輯。
我們 fork 的時(shí)候只是將讀寫(xiě)位變成了只讀,存在位仍然是 1 沒(méi)有動(dòng),所以會(huì)走 do_wp_page 邏輯。
void?do_wp_page(unsigned?long?error_code,unsigned?long?address)?{//?后面這一大坨計(jì)算了?address?在頁(yè)表項(xiàng)的指針un_wp_page((unsigned?long?*)(((address>>10)?&?0xffc)?+?(0xfffff000?&*((unsigned?long?*)?((address>>20)?&0xffc))))); }void?un_wp_page(unsigned?long?*?table_entry)?{unsigned?long?old_page,new_page;old_page?=?0xfffff000?&?*table_entry;//?只被引用一次,說(shuō)明沒(méi)有被共享,那只改下讀寫(xiě)屬性就行了if?(mem_map[MAP_NR(old_page)]==1)?{*table_entry?|=?2;invalidate();return;}//?被引用多次,就需要復(fù)制頁(yè)表了new_page=get_free_page();mem_map[MAP_NR(old_page)]--;*table_entry?=?new_page?|?7;invalidate();copy_page(old_page,new_page); }//?刷新頁(yè)變換高速緩沖宏函數(shù) #define?invalidate()?\ __asm__("movl?%%eax,%%cr3"::"a"?(0))我用圖直接說(shuō)明這段代碼的細(xì)節(jié)。
剛剛 fork 完一個(gè)進(jìn)程,是這個(gè)樣子的對(duì)吧?
這是我們對(duì)著這個(gè)物理空間范圍,寫(xiě)一個(gè)值,就會(huì)觸發(fā)上述函數(shù)。
假如是進(jìn)程 2 寫(xiě)的。
顯然此時(shí)這個(gè)物理空間被引用了大于 1 次,所以要復(fù)制頁(yè)面。
new_page=get_free_page();并且更改頁(yè)面只讀屬性為可讀寫(xiě)。
*table_entry?=?new_page?|?7;圖示就是這樣。
是不是很簡(jiǎn)單。
那此時(shí)如果進(jìn)程 1 再寫(xiě)呢?那么引用次數(shù)就等于 1 了,只需要更改下頁(yè)屬性即可,不用進(jìn)行頁(yè)面復(fù)制操作。
if?(mem_map[MAP_NR(old_page)]==1)?...圖示就是這樣。
就這么簡(jiǎn)單。
是不是從細(xì)節(jié)上看,和你原來(lái)理解的寫(xiě)時(shí)復(fù)制,還有點(diǎn)不同。
缺頁(yè)中斷的處理過(guò)程中,除了寫(xiě)時(shí)復(fù)制原理的 do_wp_page,還有個(gè) do_no_page,是在頁(yè)表項(xiàng)的存在位 P 為 0 時(shí)觸發(fā)的。
這個(gè)和進(jìn)程按需加載內(nèi)存有關(guān),如果還沒(méi)加載到內(nèi)存,會(huì)通過(guò)這個(gè)函數(shù)將磁盤(pán)中的數(shù)據(jù)復(fù)制到內(nèi)存來(lái)~
往期推薦
如果讓你來(lái)設(shè)計(jì)網(wǎng)絡(luò)
這種本機(jī)網(wǎng)絡(luò) IO 方法,性能可以翻倍!
留不住客戶(hù)?該從你的系統(tǒng)上找找原因了!
明明還有大量?jī)?nèi)存,為啥報(bào)錯(cuò)“無(wú)法分配內(nèi)存”?
點(diǎn)分享
點(diǎn)收藏
點(diǎn)點(diǎn)贊
點(diǎn)在看
?
?
總結(jié)
以上是生活随笔為你收集整理的写时复制就这么几行代码,还是不会?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 争分夺秒:阿里实时大数据技术全力助战双1
- 下一篇: 用了 HTTPS,没想到还是被监控了!