在程序的執(zhí)行過程中,因?yàn)橛龅侥撤N障礙而使 CPU 無法最終訪問到相應(yīng)的物理內(nèi)存單元,即無法完成從虛擬地址到物理地址映射的時(shí)候,CPU 會(huì)產(chǎn)生一次缺頁異常,從而進(jìn)行相應(yīng)的缺頁異常處理?;?CPU 的這一特性,Linux 采用了請(qǐng)求調(diào)頁(Demand Paging)和寫時(shí)復(fù)制(Copy On Write)的技術(shù)。 1. 請(qǐng)求調(diào)頁是一種動(dòng)態(tài)內(nèi)存分配技術(shù),它把頁框的分配推遲到不能再推遲為止。這種技術(shù)的動(dòng)機(jī)是:進(jìn)程開始運(yùn)行的時(shí)候并不訪問地址空間中的全部?jī)?nèi)容。事實(shí)上,有 一部分地址也許永遠(yuǎn)也不會(huì)被進(jìn)程所使用。程序的局部性原理也保證了在程序執(zhí)行的每個(gè)階段,真正使用的進(jìn)程頁只有一小部分,對(duì)于臨時(shí)用不到的頁,其所在的頁 框可以由其它進(jìn)程使用。因此,請(qǐng)求分頁技術(shù)增加了系統(tǒng)中的空閑頁框的平均數(shù),使內(nèi)存得到了很好的利用。從另外一個(gè)角度來看,在不改變內(nèi)存大小的情況下,請(qǐng) 求分頁能夠提高系統(tǒng)的吞吐量。當(dāng)進(jìn)程要訪問的頁不在內(nèi)存中的時(shí)候,就通過缺頁異常處理將所需頁調(diào)入內(nèi)存中。 2. 寫時(shí)復(fù)制主要應(yīng)用于系統(tǒng)調(diào)用fork,父子進(jìn)程以只讀方式共享頁框,當(dāng)其中之一要修改頁框時(shí),內(nèi)核才通過缺頁異常處理程序分配一個(gè)新的頁框,并將頁框標(biāo)記 為可寫。這種處理方式能夠較大的提高系統(tǒng)的性能,這和Linux創(chuàng)建進(jìn)程的操作過程有一定的關(guān)系。在一般情況下,子進(jìn)程被創(chuàng)建以后會(huì)馬上通過系統(tǒng)調(diào)用 execve將一個(gè)可執(zhí)行程序的映象裝載進(jìn)內(nèi)存中,此時(shí)會(huì)重新分配子進(jìn)程的頁框。那么,如果fork的時(shí)候就對(duì)頁框進(jìn)行復(fù)制的話,顯然是很不合適的。 在上述的兩種情況下出現(xiàn)缺頁異常,進(jìn)程運(yùn)行于用戶態(tài),異常處理程序可以讓進(jìn)程從出現(xiàn)異常的指令處恢復(fù)執(zhí)行,使用戶感覺不到異常的發(fā)生。當(dāng)然,也 會(huì)有異常無法正?;謴?fù)的情況,這時(shí),異常處理程序會(huì)進(jìn)行一些善后的工作,并結(jié)束該進(jìn)程。也就是說,運(yùn)行在用戶態(tài)的進(jìn)程如果出現(xiàn)缺頁異常,不會(huì)對(duì)操作系統(tǒng)核 心的穩(wěn)定性造成影響。那么對(duì)于運(yùn)行在核心態(tài)的進(jìn)程如果發(fā)生了無法正?;謴?fù)的缺頁異常,應(yīng)該如何處理呢?是否會(huì)導(dǎo)致系統(tǒng)的崩潰呢?是否能夠解決好內(nèi)核態(tài)缺頁 異常對(duì)于操作系統(tǒng)核心的穩(wěn)定性來說會(huì)產(chǎn)生很大的影響,如果一個(gè)誤操作就會(huì)造成系統(tǒng)的Oops,這對(duì)于用戶來說顯然是不能容忍的。本文正是針對(duì)這個(gè)問題,介 紹了一種Linux內(nèi)核中所采取的解決方法。 在讀者繼續(xù)往下閱讀之前,有一點(diǎn)需要先說明一下,本文示例中所選的代碼取自于Linux-2.4.0,編譯環(huán)境是gcc-2.96,objdump的版本是2.11.93.0.2,具體的版本信息可以通過以下的命令進(jìn)行查詢: $ gcc -v GCC的擴(kuò)展功能 由于本文中會(huì)用到GCC的擴(kuò)展功能,即匯編器as中提供的.section偽操作,在文章開始之前我再作一個(gè)簡(jiǎn)要的介紹。此偽操作對(duì)于不同的可 執(zhí)行文件格式有不同的解釋,我也不一一列舉,僅對(duì)我們所感興趣的Linux中常用的ELF格式的用法加以描述,其指令格式如下: .section NAME[, "FLAGS"] 在Linux內(nèi)核中,通過使用.section的偽操作,可以把隨后的代碼匯編到一個(gè)由NAME指定的段中。而FLAGS字段則說明了該段的屬性,它可以用下面介紹的單個(gè)字符來表示,也可以是多個(gè)字符的組合。 'a' 可重定位的段;'w' 可寫段; 'x' 可執(zhí)行段; 'W' 可合并的段; 's' 共享段。 舉個(gè)例子來說明,讀者在后面會(huì)看到的:.section .fixup, "ax"。這樣的一條指令定義了一個(gè)名為.fixup的段,隨后的指令會(huì)被加入到這個(gè)段中,該段的屬性是可重定位并可執(zhí)行。 內(nèi)核缺頁異常處理 在老版本的Linux中,這個(gè)工作是通過函數(shù)verify_area來完成的: extern inline int verify_area(int type, const void * addr, unsigned long size) 該函數(shù)驗(yàn)證了是否可以以type中說明的訪問類型(read or write)訪問從地址addr開始、大小為size的一塊虛擬存儲(chǔ)區(qū)域。為了做到這一點(diǎn),verify_read首先需要找到包含地址addr的虛擬存 儲(chǔ)區(qū)域(vma)。一般的情況下(正確運(yùn)行的程序)這個(gè)測(cè)試都會(huì)成功返回,在少數(shù)情況下才會(huì)出現(xiàn)失敗的情況。也就是說,大部分的情況下內(nèi)核在一些無用的驗(yàn) 證操作上花費(fèi)了不算短的時(shí)間,這從操作系統(tǒng)運(yùn)行效率的角度來說是不可接受的。 為了解決這個(gè)問題,現(xiàn)在的Linux設(shè)計(jì)中將驗(yàn)證的工作交給虛存中的硬件設(shè)備來完成。當(dāng)系統(tǒng)啟動(dòng)分頁機(jī)制以后,如果一條指令的虛擬地址所對(duì)應(yīng)的 頁框(page frame)不在內(nèi)存中或者訪問的類型有錯(cuò)誤,就會(huì)發(fā)生缺頁異常。處理器把引起缺頁異常的虛擬地址裝到寄存器CR2中,并提供一個(gè)出錯(cuò)碼,指示引起缺頁異 常的存儲(chǔ)器訪問的類型,隨后調(diào)用Linux的缺頁異常處理函數(shù)進(jìn)行處理。 Linux中進(jìn)行缺頁異常處理的函數(shù)如下: asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)/* Are we prepared to handle this kernel fault? */ if ((fixup = search_exception_table(regs->eip)) != 0) { regs->eip = fixup; return; } ……………………… } 首先讓我們來看看傳給這個(gè)函數(shù)調(diào)用的兩個(gè)參數(shù):它們都是通過entry.S在堆棧中建立的(arch/i386/kernel /entry.S),參數(shù)regs指向保存在堆棧中的寄存器,error_code中存放著異常的出錯(cuò)碼,具體的堆棧布局參見圖一(堆棧的生成過程請(qǐng)參考 《Linux內(nèi)核源代碼情景分析》一書) 該函數(shù)首先從CPU的控制寄存器CR2中獲取出現(xiàn)缺頁異常的虛擬地址。由于缺頁異常處理程序需要處理的缺頁異常類型很多,分支也很復(fù)雜?;诒疚牡闹髦?,我們只關(guān)心以下的幾種內(nèi)核缺頁異常處理的情況: 1." 程序要訪問的內(nèi)核地址空間的內(nèi)容不在內(nèi)存中,先跳轉(zhuǎn)到標(biāo)號(hào)vmalloc_fault,如果當(dāng)前訪問的內(nèi)容所對(duì)應(yīng)的頁目錄項(xiàng)不在內(nèi)存中,再跳轉(zhuǎn)到標(biāo)號(hào)no_context; 2. 缺頁異常發(fā)生在中斷或者內(nèi)核線程中,跳轉(zhuǎn)到標(biāo)號(hào)no_context; 3. 程序在核心態(tài)運(yùn)行時(shí)訪問用戶空間的數(shù)據(jù),被訪問的數(shù)據(jù)不在內(nèi)存中 a) 出現(xiàn)異常的虛擬地址在進(jìn)程的某個(gè)vma中,但是系統(tǒng)內(nèi)存無法分配空閑頁框(page frame),則先跳轉(zhuǎn)到標(biāo)號(hào)out_of_memory,再跳轉(zhuǎn)到標(biāo)號(hào)no_context; b) 出現(xiàn)異常的虛擬地址不屬于進(jìn)程任一個(gè)vma,而且不屬于堆棧擴(kuò)展的范疇,則先跳轉(zhuǎn)到標(biāo)號(hào)bad_area,最終也是到達(dá)標(biāo)號(hào)no_context。 從上面的這幾種情況來看,我們關(guān)注的焦點(diǎn)最后集中到標(biāo)號(hào)no_context處,即對(duì)函數(shù)search_exception_table的調(diào) 用。這個(gè)函數(shù)的作用就是通過發(fā)生缺頁異常的指令(regs->eip)在異常表(exception table)中尋找下一條可以繼續(xù)運(yùn)行的指令(fixup)。這里提到的異常表包含一些地址對(duì),地址對(duì)中的前一個(gè)地址表示出現(xiàn)異常的指令的地址,后一個(gè)表 示當(dāng)前一個(gè)指令出現(xiàn)錯(cuò)誤時(shí),程序可以繼續(xù)得以執(zhí)行的修復(fù)地址。 如果這個(gè)查找操作成功的話,缺頁異常處理程序?qū)⒍褩V械姆祷氐刂罚╮egs->eip)修改成修復(fù)地址并返回,隨后,發(fā)生異常的進(jìn)程將按照fixup中安排好的指令繼續(xù)執(zhí)行下去。當(dāng)然,如果無法找到與之匹配的修復(fù)地址,系統(tǒng)只有打印出出錯(cuò)信息并停止運(yùn)作。 那么,這個(gè)所謂的修復(fù)地址又是如何生成的呢?是系統(tǒng)自動(dòng)生成的嗎?答案當(dāng)然是否定的,這些修復(fù)指令都是編程人員通過as提供的擴(kuò)展功能寫進(jìn)內(nèi)核源碼中的。下面我們就來分析一下其實(shí)現(xiàn)機(jī)制。 |
|