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