Linux頁出錯處理及需求加載,寫時複製源碼分析

寫時複製:

若干個進程都是讀一個頁面數據時,則共享這一個頁面,不需要複製這個頁面。(節約內存並加快進程創建速度),當某個進程想要對這個頁面進行寫操作時(修改數據),會影響共享這個頁面的其他進程,這時才把頁面進行復制(分別持有不同的頁面),這時進行寫操作不會影響到其他進程。
只有在寫的時候才複製。

需求加載

加載一個進程時,不會把這個進程的所有數據全部加載到內存,而是僅分配必要的內存,沒有給代碼段和數據段分配內存,那麼程序運行起來後,由於在對應的地址中沒有找到代碼或數據就會引發缺頁異常,這時操作系統會把相應的代碼或數據加載到內存,然後執行。
只有才需要的時候才加載。

上面兩個特性分別對應了兩個異常處理(寫時複製對應寫保護異常,需求加載對應缺頁異常),這兩個異常是Linux的兩種頁出錯情況。如下圖:
在這裏插入圖片描述

在看源碼之前先簡單描述一下,寫時複製及需求加載的處理過程:
寫時複製: 當使用fork函數創建一個子進程時,父子進程共享父進程的內存(不爲子進程複製一份父進程的內存),並且將父進程的內存區域訪問權限置爲只讀(父子進程都是隻讀),這樣只要父/子進程其中任何一個打算向這塊內存中寫數據時,就會引發寫保護異常(權限是隻讀),然後操作系統處理這個異常,把頁面複製一份給進程寫操作的進程,然後重新執行寫操作。
需求加載: 上邊介紹需求加載時已經說過了,不再費話。

源碼分析:

這裏只描述大體過程,不會對細節進行過多的描述,本文主要是分析頁出錯的處理,即寫保護異常及缺頁異常的處理過程,不對其他函數(如fork()等)進行分析,只描述出這些函數執行完的結果。

現在有進程A(持有一塊內存),執行fork函數後,出現子進程B,fork函數會把A持有的內存頁面置爲只讀(即A,B都只讀這個頁面),那麼不管誰朝這個頁面中寫數據都會引發異常(14號異常)。 同樣,缺頁也是引發14號異常。(寫保護和缺頁都屬於頁出錯)

14號異常對應的處理程序如下圖:
在這裏插入圖片描述
kernel/traps.c [代碼已刪減]

引發14號異常那麼就會執行page_fault這個處理函數;
在這裏插入圖片描述
mm/page.s [代碼已刪減]
當引起異常時(寫保護異常,缺頁異常都執行page_fault),在棧頂上會存在出錯碼(用於判斷是寫保護異常還是缺頁異常),把出錯的地址(虛擬地址)放到cr2寄存器中。
15行把出錯碼放到eax寄存器中,17行把出錯的虛擬地址放到edx中,然後壓棧,當作參數。
然後根據eax的值,也就是出錯碼來判斷這是寫保護異常還是缺頁異常,testl $1, %eax, 就是拿出錯碼的最後一行(頁存在位)和1比較,如果該位爲0,那麼就是缺頁了(不存在),執行do_no_page(缺頁處理函數),否則爲寫保護異常,執行do_wp_page(寫保護處理函數)。
加上18,19行的參數壓棧,那麼這兩個異常處理函數的形式應該是:

do_no_page(err_code, addr);
do_wp_page(err_code, addr);

在這裏插入圖片描述
下面先看實現寫時複製的do_wp_page函數,前邊已經描述過場景了,進程A(持有一塊內存),執行fork函數後,出現子進程B,fork函數會把A持有的內存頁面置爲只讀(即A,B都只讀這個頁面),那麼不管誰朝這個頁面中寫數據都會引發異常(14號異常)。
假設現在B進程寫數據時引發了寫保護異常。

進入do_wp_page處理:
如果不加說明,以下函數都在 mm/memory.c文件中,均已刪減。
在這裏插入圖片描述
可見,調用了up_wp_page, 本文不對細節進行過多的分析(比如參數中對地址的計算),計算後的結果是address(出錯的虛擬地址)對應的頁表的物理地址。
在這裏插入圖片描述
先申請了一塊內存(引發寫保護後要爲寫操作的進程申請一塊內存(準備複製),由於此時爲B進程,所以*table_entry = new | 7, 是修改了B進程了表項(每個進程有自己的頁表),或上7,也就是把倒數第1,2,3位全部置1, 頁表項結構如下:
在這裏插入圖片描述
倒數第1,2,3位分別爲U/S, R/W, P, 可見把R/W位置1,表示可寫(進程B對新申請的這塊內存可寫),然後copy_page就是在搬運內容(複製,此時才複製;在創建B進程的時候不復制,直到B進程進行寫操作時才複製),最後重新執行寫指令。
在這裏插入圖片描述
在寫操作之前,AB共享
在這裏插入圖片描述
當B的寫保護處理完成,內存情況如下:

在這裏插入圖片描述
注意此時A進程對其內存仍然爲只讀,如果A進程進行寫操作還會引發寫保護異常,那麼怎麼處理呢,下面看完整的up_wp_page()函數:
在這裏插入圖片描述
mem_map數組記錄了有幾個進程對相應內存的引用。比如,A,B共享一塊內存時,該內存在mem_map數組中的值爲2,表示有兩個引用,當B處理寫保護異常時,不會進入226-230行,引用不爲1, 爲2,當到234行時,引用減1, 因爲此時進程B申請了一塊新的內存,不在繼續引用與A共享的內存了,所以A的內存塊此時引用減少爲1.(mem_map相應位置值爲1)。
那麼A引發寫保護異常後不會像B一樣到231行申請內存然後複製,而是進入226-230,(因爲此時mem_map引用值爲1滿足條件),那麼就直接把頁表項倒數第二位置1(R/W=1,表示可寫),然後刷新,返回,繼續使用原內存,而不用申請內存,因爲此時只有一個進程A在引用這塊內存。
在這裏插入圖片描述
以上就是寫時複製的過程。

下面看需求加載
過程:訪問的頁面不存在(頁表項P位爲0),要去文件中把代碼或數據加載到內存。
和寫時複製都需要頁出錯異常一樣,都會進入page_fault那麼彙編函數,然後壓入參數,只不過調用的是do_no_page,處理函數,如下:
在這裏插入圖片描述
mm/memory.c [代碼已刪減]
首先申請一頁內存(一頁大小4KB),368行。
然後計算相應地址的代碼或數據在硬盤上存放的位置(在哪個數據塊中),
370-374,把代碼和數據所在的塊及後面三個數據塊(一共四個,此版本使用的文件系統一個數據塊爲1K,而申請的一頁爲4K,所以可以讀入4頁),
然後put_page把物理地址缺頁的虛擬地址address映射到物理地址page(get_free_page返回的是空閒頁的物理地址),本來虛擬地址address是沒有對應的物理地址(物理頁,所以才引起缺頁異常)的,現在把相應的代碼或數據加載到內存後,與虛擬地址建立映射,那麼就可以通過虛擬地址訪問到這個物理頁,就可以取出其中的代碼和數據,從而繼續執行了。只有在需要的時候(引起缺頁異常,確實需要執行這部分代碼的時候,不執行或不使用代碼/數據時,代碼/數據就老老實實躺在文件系統(硬盤)上)才加載。

如有錯誤和遺漏,還望指出。


參考:《Linux內核完全剖析》
源碼:Linux 0.11

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章