linux源碼解讀(八):內存管理——分頁和分段


  1、計算的內存和磁盤都是用來存儲數據的,作用上沒有本質區別,但是這兩種存儲介質的特性卻差異巨大:

  •   內存需要上電才能存儲數據,一旦掉電數據就沒了,磁盤卻不需要用電也能保存數據
  •        內存的速度很快,大約100ns就能讀寫數據,而磁盤是毫秒級別的,理論速度差了幾萬倍;

       所以計算機運行時為了追求速度,會盡量把數據放內存,那么問題來了:內存因為價格原因,空間比磁盤小很多,怎么高效地管理和利用內存的存儲空間了?

  2、(1)舉個農業的例子:

  •   上千畝的農田,農場主肯定會根據時令季節、農產種類和價格、農田本身的肥沃度等因素把農田分成很多塊,不同的農田種植不同的農產品,內存管理和種田的策略沒有本質區別:需要把內存化整為零,不同的部分存儲不同的數據,比如內核的代碼段、數據段、堆棧段等,這就誕生了GDT表;同理用戶的應用程序也有代碼段、數據段、堆棧段,所以也誕生了LDT;這兩種表需要使用到ds、cs、ss等段寄存器,這就是內存分段的原因;
    •   這里也引申出了另一個問題:為啥要把內核和用戶程序分開了?原因也很簡單:如果在一起,用戶程序能隨意讀寫內核代碼指令或數據,豈不是很危險? 分開后,用戶程序執行時如果要調用操作系統的某些接口(比如print、open、read、write、send等),需要通過中斷(int)或系統調用(syscall/sysenter)的方式提權進入內核才行,通過此方式保證用戶程序不能隨意更改內核代碼或數據
    •         這里有引申出另一個問題: cpu是典型的指哪打哪,只要eip指向哪,就從哪取數據當成指令來執行,那么cpu是怎么判斷當前是內核態還是用戶態的了?通過段寄存器的算選擇子:一共有2位,可以表示4種不同的狀態,一般只用了00和11這兩種;00表示0,就是內核態,俗稱0環;11表示3,就是用戶程序,俗稱3環;程序在運行時,如果eip跨越了段寄存器基址+limit的限制(LDT或GDT有標識的),cpu硬件會先檢查段選擇子的權限,看看是不是從3環到0環;如果是,就必須通過上述的中斷或系統調用方式,否則直接在硬件層面出異常報錯!當然,從3環到3環也是要報錯的(還是通過LDT檢查是不是已經超出了本應用程序的cs、ds、ss的范圍),這就從硬件層面的機制上保證了A應用程序不會被B應用程序讀寫而導致數據泄露或程序被破壞(進程切換靠tss,也是cpu從硬件層面保證的機制)!
    •        還有最后一個問題:為啥通過軟中斷或系統調用從3環進0環就是安全(當然這是相對的安全)的,而3環直接訪問0環的代碼或數據就是危險的了? 中斷或系統調用,3環只需要提供調用號就行了,不需要知道具體實現的代碼是怎么寫的,也完全看不見;只要操作系統內核保證系統調用的實現過程是安全的就ok了!軟中斷或系統調用:本質上就是提供了3環低權限到0環高權限的提權通道,讓代碼進入0環執行;但執行的代碼又是操作系統實現定義好的,用戶的應用程序是沒法隨意更改的,由此又保障了內核的代碼和數據安全,一個字:絕
  •         上述利用ds、cs、ss等寄存器把內存划分成一段一段地形式,不同的段分別給內核、應用程序使用;代碼一旦要跨段運行,需要檢查段的權限來確認是否能夠跨段,這是早期操作系統和cpu硬件隔離程序的做法,現在的64位操作系統已經不是這么做的了!大家可以用windbg看看64位的windows系統,可以發現ds、cs、ss居然都是從0開始的,limit也都是0xffffffffffffff,用行話說要“平坦化”了,這種情況怎么隔離內核和應用程序了?-----分頁
    •   每個進程都有自己的CR3值,通過CR3頁表映射,把邏輯地址轉成物理地址;不同的進程有不同的CR3和不同的頁表映射,即使是相同的邏輯地址,也會映射到不同的物理地址,由此讓不同的進程擁有不用的物理地址,和上述通過ds、cs、ss方式對內存分段的思路是一樣的,只不過換了一種方式實現
    •       分頁的另一個好處:大塊的物理內存經過長時間的分配和回收,可能已經支離破碎;經過虛擬內存映射后,把零散的物理內存映射成大整塊的虛擬內存;
    •       分頁的漏洞: 人為更改頁表,把邏輯地址映射的物理地址改成其他的物理地址,導致應用程序讀取的數據或代碼是錯誤的,達到無痕hook的目的!windows下可以用來過PG保護的;
    •       內核代碼不分頁,永占物理內存,願意也很簡單:內核代碼是核心,需要一直運行,當然不能被換出內存了,舉個例子:發生缺頁異常了,需要把頁面的數據從磁盤重新加載到內存,缺頁異常的handler也在內核,如果連這部分代碼都分頁,這個頁面被放入磁盤,豈不成了死循環?所以只有應用程序的內存需要分頁管理!

  (2)概念澄清:

    邏輯地址:程序員寫代碼看到和使用的地址,是操作系統分配給每個進程的獨立地址空間,也就是CR3轉換前的地址空間

    線性地址:等於段基址+邏輯地址;windows 64位的操作系統段平坦化以后,線性地址事實上等於邏輯地址了;

              物理地址:cpu物理總線表示的地址

   (3) 頁的屬性也有很多位來表示,學名叫頁表描述,如下:每位的作用在參考鏈接1有,這里不再追溯!

          

     這里重點說一下第5位access位:該頁是否能被訪問;如果不能,其他應用程序訪問時會報錯,這可以用於動態反調試 和反hook;這里有必要解釋一下為啥低12bit可以用來表示各種屬性,而不是用來表示地址了?

  由於cpu把物理內存按照4KB划分成1頁,所以頁目錄和頁表也是存儲在物理頁的;頁目錄和頁表也是地址,每個item都是4byte,所以每個物理頁能容納1024個item;換句話說:頁目錄每個item指向一個裝了1024個頁表的物理頁,由於物理頁是4KB對齊的,導致頁目錄表的每個item的低12bit都是0;頁目錄同理:每個頁目錄的item都指向物理頁的開始位置,導致頁目錄的每個item的第12bit都是0;所以第12bit實際上不可以不用來表示地址的,剛好就用來表示各種頁屬性了!整個示意如下:

  • 這里容易混淆的就是頁目錄地址和頁表項,因為存儲這兩類信息的內存本身是地址,恰好頁目錄和頁表項的含義也是地址,兩個地址在編碼時非常容易混淆!明顯的區別就是:頁目錄和頁表項本身的地址是4字節遞增的,但存儲的內容是頁首地址,以0x1000的顆粒度對齊的
  • 實際存儲時,頁目錄項和頁表項每個item的低12bit大概率會被用於指明各種屬性,不太可能直接存0;實際計算下一級地址時需要清零
  • 后面的代碼會大量涉及到*((unsigned long *) ((address>>20) &0xffc))))和((address>>10) & 0xffc),分別是頁目錄表中頁表項的值,和頁表項的偏移,兩者相加就是頁表項的線性地址;

         

        3、內存管理

  大家還記得文件的inode和數據塊是怎么管理的么?當時用了bitmap來標記的;邏輯塊的最小使用大小是4KB。如果這4KB被分配使用,那么bitmap對應的bit設置為1;0.11版本的linux采用了相同的管理策略,仍然采用bitmap的方式標記內存頁。如果某一頁已經分配使用,那么bitmap對應的bit設置為1,否則設置為0;這里實際采用了名為mem_map的char數組標記3840個頁面(15MB內存)被引用的次數

/* these are not to be changed without changing head.s etc */
// linux0.11內核默認支持的最大內存容量是16MB,可以修改這些定義適合更多的內存。
// 內存低端(1MB)
#define LOW_MEM 0x100000
// 分頁內存15 MB,主內存區最多15M.
#define PAGING_MEMORY (15*1024*1024)
// 分頁后的物理內存頁面數(3840)
#define PAGING_PAGES (PAGING_MEMORY>>12)
// 指定地址映射為頁號
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
// 頁面被占用標志.
#define USED 100

// 物理內存映射字節圖(1字節代表1頁內存)。每個頁面對應的字節用於標志頁面當前引
// 用(占用)次數。它最大可以映射15MB的內存空間。在初始化函數mem_init()中,對於
// 不能用做主內存頁面的位置均都預先被設置成USED(100).
static unsigned char mem_map [ PAGING_PAGES ] = {0,};

   查找空閑或使用的物理頁面是,需要挨個遍歷mem_map數組,linux 0.11是這樣做的;同時也遍歷pa_dir數組,看看哪些頁目錄和頁表項已經被使用了

//// 計算內存空閑頁面數並顯示
// [?? 內核中沒有其他地方調用該函數,Linus調試過程中用的]
void calc_mem(void)
{
    int i,j,k,free=0;
    long * pg_tbl;

    // 掃描內存頁面映射數組mem_map[],獲取空閑頁面數並顯示。然后掃描所有的頁目
    // 錄項(除0,1項),如果頁目錄項有效,則統計對應頁表中有效頁面數,並顯示。頁
    // 目錄項0-3被內核使用,因此應該從第5個目錄項(i=4)開始掃描。
    for(i=0 ; i<PAGING_PAGES ; i++)
        if (!mem_map[i]) free++;
    printk("%d pages free (of %d)\n\r",free,PAGING_PAGES);
    for(i=2 ; i<1024 ; i++) {               // 初始值應該等於4;有1024個頁目錄
        if (1&pg_dir[i]) {
            pg_tbl=(long *) (0xfffff000 & pg_dir[i]);/*得到頁表項;每個頁目錄有1024個頁表項;每個頁表項對應1024個物理頁*/
            for(j=k=0 ; j<1024 ; j++)
                if (pg_tbl[j]&1)
                    k++;
            printk("Pg-dir[%d] uses %d pages\n",i,k);
        }
    }
}

  當然,在正式使用前,必須先初始化的,方法如下:

// 物理內存管理初始化
// 該函數對1MB以上的內存區域以頁面為單位進行管理前的初始化設置工作。一個頁面長度
// 為4KB bytes.該函數把1MB以上所有物理內存划分成一個個頁面,並使用一個頁面映射字節
// 數組mem_map[]來管理所有這些頁面。對於具有16MB內存容量的機器,該數組共有3840
// 項((16MB-1MB)/4KB),即可管理3840個物理頁面。每當一個物理內存頁面被占用時就把
// mem_map[]中對應的字節值增1;若釋放一個物理頁面,就把對應字節值減1。若字節值為0,
// 則表示對應頁面空閑;若字節值大於或等於1,則表示對應頁面被占用或被不同程序共享占用。
// 在該版本的Linux內核中,最多能管理16MB的物理內存,大於16MB的內存將棄之不用。
// 對於具有16MB內存的PC機系統,在沒有設置虛擬盤RAMDISK的情況下start_mem通常是4MB,
// end_mem是16MB。因此此時主內存區范圍是4MB-16MB,共有3072個物理頁面可供分配。而
// 范圍0-1MB內存空間用於內核系統(其實內核只使用0-640Kb,剩下的部分被部分高速緩沖和
// 設備內存占用)。
// 參數start_mem是可用做頁面分配的主內存區起始地址(已去除RANDISK所占內存空間)。
// end_mem是實際物理內存最大地址。而地址范圍start_mem到end_mem是主內存區。
void mem_init(long start_mem, long end_mem)
{
    int i;

    // 首先將1MB到16MB范圍內所有內存頁面對應的內存映射字節數組項置為已占用狀態,
    // 即各項字節全部設置成USED(100)。PAGING_PAGES被定義為(PAGING_MEMORY>>12),
    // 即1MB以上所有物理內存分頁后的內存頁面數(15MB/4KB = 3840).
    HIGH_MEMORY = end_mem;                  // 設置內存最高端(16MB)
    for (i=0 ; i<PAGING_PAGES ; i++)
        mem_map[i] = USED;
    // 然后計算主內存區起始內存start_mem處頁面對應內存映射字節數組中項號i和主內存區頁面數。
    // 此時mem_map[]數組的第i項正對應主內存區中第1個頁面。最后將主內存區中頁面對應的數組項
    // 清零(表示空閑)。對於具有16MB物理內存的系統,mem_map[]中對應4MB-16MB主內存區的項被清零。
    i = MAP_NR(start_mem);      // 主內存區(也就是用戶程序內存)其實位置處頁面號
    end_mem -= start_mem;
    end_mem >>= 12;             // 主內存區中的總頁面數
    while (end_mem-->0)
        mem_map[i++]=0;         // 主內存區頁面對應字節值清零
}

        根據mem_init推斷早期linux的內存使用分布如下:

    

  主內存(也就是用戶應用程序)內存初始化完成后就可以按照頁的顆粒度申請使用了;怎么找到空閑的物理頁面了?當然是從mem_map數組開始找了,代碼如下:

//// 在主內存區中取空閑屋里頁面。如果已經沒有可用物理內存頁面,則返回0.
// 輸入:%1(ax=0) - 0; %2(LOW_MEM)內存字節位圖管理的其實位置;%3(cx=PAGING_PAGES);
// %4(edi=mem_map+PAGING_PAGES-1).
// 輸出:返回%0(ax=物理內存頁面起始地址)。
// 上面%4寄存器實際指向mem_map[]內存字節位圖的最后一個字節。本函數從位圖末端開
// 始向前掃描所有頁面標志(頁面總數PAGING_PAGE),若有頁面空閑(內存位圖字節為
// 0)則返回頁面地址。注意!本函數只是指出在主內存區的一頁空閑物理內存頁面,但
// 並沒有映射到某個進程的地址空間中去。后面的put_page()函數即用於把指定頁面映射
// 到某個進程地址空間中。當然對於內核使用本函數並不需要再使用put_page()進行映射,
// 因為內核代碼和數據空間(16MB)已經對等地映射到物理地址空間。
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");

__asm__("std ; repne ; scasb\n\t"   // 置方向位,al(0)與對應每個頁面的(di)內容比較
    "jne 1f\n\t"                    // 如果沒有等於0的字節,則跳轉結束(返回0).
    "movb $1,1(%%edi)\n\t"          // 1 => [1+edi],將對應頁面內存映像bit位置1.
    "sall $12,%%ecx\n\t"            // 頁面數*4k = 相對頁面其實地址
    "addl %2,%%ecx\n\t"             // 再加上低端內存地址,得頁面實際物理起始地址
    "movl %%ecx,%%edx\n\t"          // 將頁面實際其實地址->edx寄存器。
    "movl $1024,%%ecx\n\t"          // 寄存器ecx置計數值1024
    "leal 4092(%%edx),%%edi\n\t"    // 將4092+edx的位置->dei(該頁面的末端地址)
    "rep ; stosl\n\t"               // 將edi所指內存清零(反方向,即將該頁面清零)
    "movl %%edx,%%eax\n"            // 將頁面起始地址->eax(返回值)
    "1:"
    :"=a" (__res)
    :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
    "D" (mem_map+PAGING_PAGES-1)
    );
return __res;           // 返回空閑物理頁面地址(若無空閑頁面則返回0).
}

  用完之后必須要及時釋放,不要“占着茅坑不拉屎”,釋放的原理也很簡單,把mem_map對應的內容清零就行了,如下:這里不得不吐槽一下,還是下面這種C代碼看着簡單;上面那種匯編語法看的人腦殼疼┭┮﹏┭┮

//// 釋放物理地址addr開始的1頁面內存。
// 物理地址1MB以下的內容空間用於內核程序和緩沖,不作為分配頁面的內存空間。因此
// 參數addr需要大於1MB.
void free_page(unsigned long addr)
{
    // 首先判斷參數給定的物理地址addr的合理性。如果物理地址addr小於內存低端(1MB)
    // 則表示在內核程序或高速緩沖中,對此不予處理。如果物理地址addr>=系統所含物
    // 理內存最高端,則顯示出錯信息並且內核停止工作。
    if (addr < LOW_MEM) return;
    if (addr >= HIGH_MEMORY)
        panic("trying to free nonexistent page");
    // 如果對參數addr驗證通過,那么就根據這個物理地址換算出從內存低端開始記起的
    // 內存頁面號。頁面號 = (addr - LOW_MEM)/4096.可見頁面號從0號開始記起。此時
    // addr中存放着頁面號。如果該頁面號對應的頁面映射字節不等於0,則減1返回。此
    // 時該映射字節值應該為0,表示頁面已釋放。如果對應頁面字節原本就是0,表示該
    // 物理頁面本來就是空閑的,說明內核代碼出問題。於是顯示出錯信息並停機。
    addr -= LOW_MEM;
    addr >>= 12;
    if (mem_map[addr]--) return;
    mem_map[addr]=0;
    panic("trying to free free page");
}

  當進程退出do_exit時,不僅要free_page,進程自己的頁表、頁目錄項也要free,操作的代碼如下: 找到頁目錄的基址,然后挨個遍歷;遍歷時找到頁表項,進而找到使用的物理內存,挨個釋放! 

/*
 * This function frees a continuos block of page tables, as needed
 * by 'exit()'. As does copy_page_tables(), this handles only 4Mb blocks.
 */
//// 根據指定的線性地址和限長(頁表個數),釋放對應內存頁表指定的內存塊並置表項為
// 空閑。頁目錄位於物理地址0開始處,共1024項,每項4字節,共占4K字節。每個目錄項
// 指定一個頁表。內核頁表從物理地址0x1000處開始(緊接着目錄空間),共4個頁表。每
// 個頁表有1024項,每項4字節。因此占4K(1頁)內存。各進程(除了在內核代碼中的進
// 程0和1)的頁表所占據的頁面在進程被創建時由內核為其主內存區申請得到。每個頁表
// 項對應1耶物理內存,因此一個頁表最多可映射4MB的物理內存。
// 參數:from - 起始線性基地址;size - 釋放的字節長度。
int free_page_tables(unsigned long from,unsigned long size)
{
    unsigned long *pg_table;
    unsigned long * dir, nr;

    // 首先檢測參數from給出的線性基地址是否在4MB的邊界處。因為該函數只能處理這
    // 種情況。若from=0,則出錯。說明視圖釋放內核和緩沖所占空間。
    if (from & 0x3fffff)/*4MB取整;4MB也是一個目錄項(第二級)對應的地址空間*/
        panic("free_page_tables called with wrong alignment");
    if (!from)
        panic("Trying to free up swapper memory space");
    // 然后計算參數size給出的長度所占的頁目錄項數(4MB的進位整數倍),也即所占
    // 頁表數。因為1個頁表可管理4MB物理內存,所以這里用右移22位的方式把需要復制
    // 的內存長度值除以4MB.其中加上0x3fffff(即4MB-1)用於得到進位整數倍結果,即
    // 除操作若有余數則進1。例如,如果原size=4.01Mb,那么可得到結果sieze=2。接
    // 着結算給出的線性基地址對應的其實目錄項。對應的目錄項號=from>>22.因為每
    // 項占4字節,並且由於頁目錄表從物理地址0開始存放,因此實際目錄項指針=目錄
    // 項號<<2,也即(from>>20)。& 0xffc確保目錄項指針范圍有效,即用於屏蔽目錄項
    // 指針最后2位。因為只移動了20位,因此最后2位是頁表項索引的內容,應屏蔽掉。
    size = (size + 0x3fffff) >> 22;
    dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
    // 此時size是釋放的頁表個數,即頁目錄項數,而dir是起始目錄項指針。現在開始
    // 循環操作頁目錄項,依次釋放每個頁表中的頁表項。如果當前目錄項無效(P位=0)
    // 表示該目錄項沒有使用(對應的頁表不存在),則繼續處理下一個目錄項。否則從目
    // 錄項總取出頁表地址pg_table,並對該頁表中的1024個表項進行處理。釋放有效頁
    // 表項(P位=1)對應的物理內存頁表。然后該頁表項清零,並繼續處理下一頁表項。
    // 當一個頁表所有表項都處理完畢就釋放該頁表自身占據的內存頁面,並繼續處理下
    // 一頁目錄項。最后刷新也頁變換高速緩沖,並返回0.
    for ( ; size-->0 ; dir++) {
        if (!(1 & *dir))
            continue;
        pg_table = (unsigned long *) (0xfffff000 & *dir);  // 取頁表地址
        for (nr=0 ; nr<1024 ; nr++) {
            if (1 & *pg_table)                          // 若該項有效,則釋放對應頁。 
                free_page(0xfffff000 & *pg_table);
            *pg_table = 0;                              // 該頁表項內容清零。
            pg_table++;                                 // 指向頁表中下一項。
        }
        free_page(0xfffff000 & *dir);                   // 釋放該頁表所占內存頁面。
        *dir = 0;                                       // 對應頁表的目錄項清零
    }
    invalidate();                                       // 刷新頁變換高速緩沖。
    return 0;
}

  和free_page_table對應的是copy_page_table,這個函數是在copy_mem里面被調用的,而copy_mem是在fork里面被調用的,說明父進程生成子進程時拷貝的內存並不是真正的物理地址,而是先拷貝了頁目錄項和頁表項,物理內存暫時共用,等到缺頁時才找空閑的物理頁分配

  整個函數的實現連linus都說在內存管理中是最復雜的之一!

/*
 *  Well, here is one of the most complicated functions in mm. It
 * copies a range of linerar addresses by copying only the pages.
 * Let's hope this is bug-free, 'cause this one I don't want to debug :-)
 *
 * Note! We don't copy just any chunks of memory - addresses have to
 * be divisible by 4Mb (one page-directory entry), as this makes the
 * function easier. It's used only by fork anyway.
 *
 * NOTE 2!! When from==0 we are copying kernel space for the first
 * fork(). Then we DONT want to copy a full page-directory entry, as
 * that would lead to some serious memory waste - we just copy the
 * first 160 pages - 640kB. Even that is more than we need, but it
 * doesn't take any more memory - we don't copy-on-write in the low
 * 1 Mb-range, so the pages can be shared with the kernel. Thus the
 * special case for nr=xxxx.
 */
//// 復制頁目錄表項和頁表項:暫時不拷貝具體的頁,提高進程fork的效率
// 復制指定線性地址和長度內存對應的頁目錄項和頁表項,從而被復制的頁目錄和頁表對
// 應的原物理內存頁面區被兩套頁表映射而共享使用。復制時,需申請新頁面來存放新頁
// 表,原物理內存區將被共享。此后兩個進程(父進程和其子進程)將共享內存區,直到
// 有一個進程執行寫操作時,內核才會為寫操作進程分配新的內存頁(寫時復制機制)。
// 參數from、to是線性地址,size是需要復制(共享)的內存長度,單位是byte.
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
    unsigned long * from_page_table;
    unsigned long * to_page_table;
    unsigned long this_page;
    unsigned long * from_dir, * to_dir;
    unsigned long nr;

    // 首先檢測參數給出的原地址from和目的地址to的有效性。原地址和目的地址都需要
    // 在4Mb內存邊界地址上。否則出錯死機。作這樣的要求是因為一個頁表的1024項可
    // 管理4Mb內存。源地址from和目的地址to只有滿足這個要求才能保證從一個頁表的
    // 第一項開始復制頁表項,並且新頁表的最初所有項都是有效的。然后取得源地址和
    // 目的地址的其實目錄項指針(from_dir 和 to_dir).再根據參數給出的長度size計
    // 算要復制的內存塊占用的頁表數(即目錄項數)。
    if ((from&0x3fffff) || (to&0x3fffff))
        panic("copy_page_tables called with wrong alignment");
    from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
    to_dir = (unsigned long *) ((to>>20) & 0xffc);
    size = ((unsigned) (size+0x3fffff)) >> 22;
    // 在得到了源起始目錄項指針from_dir和目的起始目錄項指針to_dir以及需要復制的
    // 頁表個數size后,下面開始對每個頁目錄項依次申請1頁內存來保存對應的頁表,並
    // 且開始頁表項復制操作。如果目的目錄指定的頁表已經存在(P=1),則出錯死機。
    // 如果源目錄項無效,即指定的頁表不存在(P=1),則繼續循環處理下一個頁目錄項。
    for( ; size-->0 ; from_dir++,to_dir++) {
        if (1 & *to_dir)
            panic("copy_page_tables: already exist");
        if (!(1 & *from_dir))
            continue;
        // 在驗證了當前源目錄項和目的項正常之后,我們取源目錄項中頁表地址
        // from_page_table。為了保存目的目錄項對應的頁表,需要在住內存區中申請1
        // 頁空閑內存頁。如果取空閑頁面函數get_free_page()返回0,則說明沒有申請
        // 到空閑內存頁面,可能是內存不夠。於是返回-1值退出。
        from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
        if (!(to_page_table = (unsigned long *) get_free_page()))
            return -1;    /* Out of memory, see freeing */
        // 否則我們設置目的目錄項信息,把最后3位置位,即當前目錄的目錄項 | 7,
        // 表示對應頁表映射的內存頁面是用戶級的,並且可讀寫、存在(Usr,R/W,Present).
        // (如果U/S位是0,則R/W就沒有作用。如果U/S位是1,而R/W是0,那么運行在用
        // 戶層的代碼就只能讀頁面。如果U/S和R/W都置位,則就有讀寫的權限)。然后
        // 針對當前處理的頁目錄項對應的頁表,設置需要復制的頁面項數。如果是在內
        // 核空間,則僅需復制頭160頁對應的頁表項(nr=160),對應於開始640KB物理內存
        // 否則需要復制一個頁表中的所有1024個頁表項(nr=1024),可映射4MB物理內存。
        *to_dir = ((unsigned long) to_page_table) | 7;
        nr = (from==0)?0xA0:1024;
        // 此時對於當前頁表,開始循環復制指定的nr個內存頁面表項。先取出源頁表的
        // 內容,如果當前源頁表沒有使用,則不用復制該表項,繼續處理下一項。否則
        // 復位表項中R/W標志(位1置0),即讓頁表對應的內存頁面只讀。然后將頁表項復制
        // 到目錄頁表中。
        for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
            this_page = *from_page_table;
            if (!(1 & this_page))
                continue;
            this_page &= ~2;
            *to_page_table = this_page;
            // 如果該頁表所指物理頁面的地址在1MB以上,則需要設置內存頁面映射數
            // 組mem_map[],於是計算頁面號,並以它為索引在頁面映射數組相應項中
            // 增加引用次數。而對於位於1MB以下的頁面,說明是內核頁面,因此不需
            // 要對mem_map[]進行設置。因為mem_map[]僅用於管理主內存區中的頁面使
            // 用情況。因此對於內核移動到任務0中並且調用fork()創建任務1時(用於
            // 運行init()),由於此時復制的頁面還仍然都在內核代碼區域,因此以下
            // 判斷中的語句不會執行,任務0的頁面仍然可以隨時讀寫。只有當調用fork()
            // 的父進程代碼處於主內存區(頁面位置大於1MB)時才會執行。這種情況需要
            // 在進程調用execve(),並裝載執行了新程序代碼時才會出現。
            // *from_page_table = this_page; 這句是令源頁表項所指內存頁也為只讀。
            // 因為現在開始有兩個進程公用內存區了。若其中1個進程需要進行寫操作,
            // 則可以通過頁異常寫保護處理為執行寫操作的進程匹配1頁新空閑頁面,也
            // 即進行寫時復制(copy on write)操作。
            if (this_page > LOW_MEM) {
                *from_page_table = this_page;
                this_page -= LOW_MEM;
                this_page >>= 12;
                mem_map[this_page]++;
            }
        }
    }
    invalidate();
    return 0;
}

  經過上面的線性地址虛擬化操作,給人感覺就是個活生生的“盜夢空間”:線性地址本身是人為虛擬構造出來的,硬件層面讀寫數據還要先轉成物理內存,所以線性地址並不是真正的地址(這不廢話么),32bit可以根據業務需求賦予各種不同的含義,只要在頁目錄表和頁表項的對應關系映射好就行了!這里的映射具體怎么操作了?也很簡單:就是給數組元素賦值,或者是給指針內容賦值;linux 0.11版本把頁表項和物理地址映射的方法如下:

  最核心的“映射”代碼其實很簡單:page_table[(address>>12) & 0x3ff] = page | 7;

/*
 * This function puts a page in memory at the wanted address.
 * It returns the physical address of the page gotten, 0 if
 * out of memory (either when trying to access page-table or
 * page.)
 */
//// 把一物理內存頁面映射到線性地址空間指定處。
// 或者說是把線性地址空間中指定地址address出的頁面映射到主內存區頁面page上。主
// 要工作是在相關頁面目錄項和頁表項中設置指定頁面的信息。若成功則返回物理頁面地
// 址。在處理缺頁異常的C函數do_no_page()中會調用此函數。對於缺頁引起的異常,由於
// 任何缺頁緣故而對頁表作修改時,並不需要刷新CPU的頁變換緩沖(或稱Translation Lookaside
// Buffer - TLB),即使頁表中標志P被從0修改成1.因為無效葉項不會被緩沖,因此當修改
// 了一個無效的頁表項時不需要刷新。在次就表現為不用調用Invalidate()函數。
// 參數page是分配的主內存區中某一頁面(頁幀,頁框)的指針;address是線性地址。
unsigned long put_page(unsigned long page,unsigned long address)
{
    unsigned long tmp, *page_table;

/* NOTE !!! This uses the fact that _pg_dir=0 */

    // 首先判斷參數給定物理內存頁面page的有效性。如果該頁面位置低於LOW_MEM(1MB)
    // 或超出系統實際含有內存高端HIGH_MEMORY,則發出警告。LOW_MEM是主內存區可能
    // 有的最小起始位置。當系統物理內存小於或等於6MB時,主內存區起始於LOW_MEM處。
    // 再查看一下該page頁面是否已經申請的頁面,即判斷其在內存頁面映射字節圖mem_map[]
    // 中相應字節是否已經置位。若沒有則需發出警告。
    if (page < LOW_MEM || page >= HIGH_MEMORY)
        printk("Trying to put page %p at %p\n",page,address);
    if (mem_map[(page-LOW_MEM)>>12] != 1)
        printk("mem_map disagrees with %p at %p\n",page,address);
    // 然后根據參數指定的線性地址address計算其在也目錄表中對應的目錄項指針,並
    // 從中取得二級頁表地址。如果該目錄項有效(P=1),即指定的頁表在內存中,則從中
    // 取得指定頁表地址放到page_table 變量中。否則就申請一空閑頁面給頁表使用,並
    // 在對應目錄項中置相應標志(7 - User、U/S、R/W).然后將該頁表地址放到page_table
    // 變量中。
    page_table = (unsigned long *) ((address>>20) & 0xffc);
    if ((*page_table)&1)
        page_table = (unsigned long *) (0xfffff000 & *page_table);
    else {
        if (!(tmp=get_free_page()))
            return 0;
        *page_table = tmp|7;
        page_table = (unsigned long *) tmp;
    }
    // 最后在找到的頁表page_table中設置相關頁表內容,即把物理頁面page的地址填入
    // 表項同時置位3個標志(U/S、W/R、P)。該頁表項在頁表中索引值等於線性地址位21
    // -- 位12組成的10bit的值。每個頁表共可有1024項(0 -- 0x3ff)。
    page_table[(address>>12) & 0x3ff] = page | 7;
/* no need for invalidate */
    return page;
}

  4、(1)為了節約物理內存,不同進程可能會共享同樣的物理頁面,舉個例子:同時打開兩個notepad,操作系統會同時生成兩個進程,但由於運行的是同樣的程序,最起碼代碼段是可以共享的,所以這兩個進程的代碼段是可以設置成一樣的!linux設置共享物理頁的方式如下:

  •   因為共享的肯定是物理頁面,所以要先根據線性地址算出物理地址
  •        修改目標進程的頁表項,讓某個頁表項保存物理頁起始地址(這種物理頁的掛載思路在windows下叫shadow walker,可以用來過PG保護的);
/*
 * try_to_share() checks the page at address "address" in the task "p",
 * to see if it exists, and if it is clean. If so, share it with the current
 * task.
 *
 * NOTE! This assumes we have checked that p != current, and that they
 * share the same executable.
 */
//// 嘗試對當前進程指定地址處的頁面進行共享處理。
// 當前進程與進程p是同一執行代碼,也可以認為當前進程是由p進程執行fork操作產生的
// 進程,因此它們的代碼內容一樣。如果未對數據段內容做過修改那么數據段內容也應一
// 樣。參數address是進程中的邏輯地址,即是當前進程欲與p進程共享頁面的邏輯頁面地
// 址。進程P是將被共享頁面的進程。如果P進程address出的頁面存在並且沒有被修改過的
// 話,就讓當前進程與p進程共享之。同時還需要驗證指定地址處是否已經申請了頁面,若
// 是則出錯,死機。返回:1 - 頁面共享處理成功;0 - 失敗。
static int try_to_share(unsigned long address, struct task_struct * p)
{
    unsigned long from;
    unsigned long to;
    unsigned long from_page;
    unsigned long to_page;
    unsigned long phys_addr;

    // 首先分別求的指定進程p中和當前進程中邏輯地址address對應的頁目錄項。為了計
    // 算方便先求出指定邏輯地址address出的'邏輯'頁目錄項號,即以進程空間(0 - 64 MB)
    // 算出的頁目錄項號。該'邏輯'頁目錄項號加上進程p在CPU 4G線性空間中的實際頁目
    // 錄項from_page。而'邏輯'頁目錄項號加上當前進程CPU 4G線性空間中起始地址對應
    // 的頁目錄項,即可最后得到當前進程中地址address處頁面所對應的4G線性空間中的
    // 實際頁目錄項to_page。
    from_page = to_page = ((address>>20) & 0xffc);
    from_page += ((p->start_code>>20) & 0xffc);
    to_page += ((current->start_code>>20) & 0xffc);
    // 在得到p進程和當前進程address對應的目錄項后,下面分別對進程p和當前進程進行
    // 處理。下面首先對p進程的表項進行操作。目標是取得p進程中address對應的物理內
    // 存頁面地址,並且該物理頁面存在,而且干凈(沒有被修改過)。
    // 方法是先取目錄項內容。如果該目錄項無效(P=0),表示目錄項對應的二級頁表不存
    // 在,於是返回。否則取該目錄項對應頁表地址from,從而計算出邏輯地址address
    // 對應的頁表項指針,並取出該頁表項內容臨時保存在phys_addr中。
/* is there a page-directory at from? */
    from = *(unsigned long *) from_page;
    if (!(from & 1))
        return 0;
    from &= 0xfffff000;
    from_page = from + ((address>>10) & 0xffc);
    phys_addr = *(unsigned long *) from_page;//終於找到物理地址了
/* is the page clean and present? */
    // 接着看看頁表項映射的物理頁面是否存在並且干凈。0x41對應頁表項中的D(dirty)
    // 和P(present)標志。如果頁面不干凈或無效則返回。然后我們從該表項中取出物
    // 理頁面地址再保存在phys_addr中。最后我們再檢查一下這個物理頁面地址的有效性,
    // 即它不應該超過機器最大物理地址值,也不應該小於內存低端(1 MB).
    if ((phys_addr & 0x41) != 0x01)
        return 0;
    phys_addr &= 0xfffff000;
    if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
        return 0;
    // 下面首先對當前進程的表項進行操作。目標是取得當前進程中address對應的頁表
    // 項地址,並且該頁表項還沒有映射物理頁面,即其P=0。
    // 首先取當前進程頁目錄項內容->to.如果該目錄項無效(P=0),即目錄項對應的二級
    // 頁表不存在,則申請一空閑頁面來存放頁表,並更新目錄項to_page內容,讓其指向
    // 內存頁面。
    to = *(unsigned long *) to_page;
    if (!(to & 1)) {
        if ((to = get_free_page()))
            *(unsigned long *) to_page = to | 7;
        else
            oom();
    }
    // 否則取目錄項中的頁表地址->to,加上頁表項索引值<<2,即頁表項在表中偏移地址,
    // 得到頁表地址->to_page.針對頁表項,如果我們此時我們檢查出其對應的物理頁面
    // 已經存在,即頁表的存在位P=1,則說明原本我們想共享進程p中對應的物理頁面,
    // 但現在我們自己已經占有了(映射有)物理頁面。於是說明內核出錯,死機。
    to &= 0xfffff000;
    to_page = to + ((address>>10) & 0xffc);
    if (1 & *(unsigned long *) to_page)
        panic("try_to_share: to_page already exists");
    // 在找到了進程p中邏輯地址address處對應的干凈且存在的物理頁面,而且也確定了
    // 當前進程中邏輯地址address所對應的耳機頁表項地址之后,我們現在對他們進行
    // 共享處理。方法很簡單,就是首先對p進程的頁表項進行修改,設置其寫保護(R/W=0,
    // 只讀)標志,然后讓當前進程復制p進程的這個頁表項。此時當前進程邏輯地址address
    // 處頁面即被映射到p進程邏輯地址address處頁面映射的物理頁面上。
/* share them: write-protect */
    *(unsigned long *) from_page &= ~2;
    *(unsigned long *) to_page = *(unsigned long *) from_page;
    // 隨后刷新頁變換高速緩沖。計算所操作屋里頁面的頁面號,並將對應頁面映射字節數
    // 組項中的引用遞增1。最后返回1,表示共享處理成功。
    invalidate();
    phys_addr -= LOW_MEM;
    phys_addr >>= 12;
    mem_map[phys_addr]++;
    return 1;
}

  (2)當發生缺頁時,首先看看有沒有運行同樣文件的進程;如果有,先共享一下改進程的物理頁面,達到節約內存的目的:注意這里面有個count字段,可以用來檢測app多開的!由此也引申出了另一個沙箱的概念:虛擬出另一塊內存,但是count就是1,避開檢測

//// 共享頁面處理。
// 在發生缺頁異常時,首先看看能否與運行同一個執行文件的其他進程進行頁面共享處理
// 該函數首先判斷系統是否有另一個進程也在運行當前進程一樣的執行文件。若有,則在
// 系統當前所有任務中尋找這樣的任務。若找到了這樣的任務就嘗試與其共享指定地址處
// 的頁面。若系統中沒有其他任務正在運行與當前進程相同的執行文件,那么共享頁面操
// 作的前提條件不存在,因此函數立刻退出。判斷系統中是否有另一個進程也在執行同一
// 個執行文件的方法是零用進程任務數據結構中的executable字段。該字段指向進程正在
// 執行程序在內存中的i節點。根據該i節點的引用次數i_count我們可以進行這種判斷。若
// executable->i_count值大於1,則表明系統中可能有兩個進程運行同一個執行文件,於
// 是可以再對task struct數組中所有任務比較是否有相同的executable字段來最后確定多個
// 進程運行着相同執行文件的情況。
// 參數address是進程中的邏輯地址,即是當前進程欲與p進程共享頁面的邏輯頁面地址。
// 返回:1 - 共享操作成功,0 - 失敗。
static int share_page(unsigned long address)
{
    struct task_struct ** p;

    // 首先檢查一下當前進程的executable字段是否指向某執行文件的i節點,以判斷本
    // 進程是否有對應的執行文件。如果沒有,則返回0.如果executable的確指向某個i
    // 節點,則檢查該i節點引用計數值。如果當前進程運行的執行文件的內存i節點引用
    // 計數等於1(executable->i_count=1),表示當前系統中只有1個進程(即當前進程)在
    // 運行該執行文件。因此無共享可言,直接退出函數。
    if (!current->executable)
        return 0;
    if (current->executable->i_count < 2)
        return 0;
    // 否則搜索任務數組中所有任務。尋找與當前進程可共享頁面的進程,即運行相同的
    // 執行文件的另一個進程,並嘗試對指定地址的頁面進行共享。如果找到某個進程p,
    // 其executable字段值與當前進程的相同,則調用try_to_share()嘗試頁面共享。若
    // 共享操作成功,則函數返回1。否則返回0,表示共享頁面操作失敗.
    for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
        if (!*p)
            continue;
        if (current == *p)
            continue;
        // 如果executable不等,表示運行的不是與當前進程相同的執行文件,因此也繼續
        // 尋找。
        if ((*p)->executable != current->executable)
            continue;
        if (try_to_share(address,*p))
            return 1;
    }
    return 0;
}

  (3)發生缺頁后,linux搜先檢查能不能和其他進程共享物理頁;如果還是不行,那么再重新分配物理頁,從磁盤把對應的可執行文件讀到物理頁(注意:這里是把可執行文件的數據拷貝到內存,和windows下有個隱藏的pagefile.sys文件不一樣,后者存放的是臨時換出來的內存物理頁),最后把物理頁掛載到進程的頁表項,整個過程就是do_no_page:

//// 執行缺頁處理
// 是訪問不存在頁面處理函數。頁異常中斷處理過程中調用的函數。在page.s程序中被調
// 用。函數參數error_code和address是進程在訪問頁面時由CPU因缺頁產生異常而自動生
// 成。該函數首先嘗試與已加載的相同文件進行頁面共享,或者只是由於進程動態申請內
// 存頁面而只需映射一頁物理內存即可。若共享操作不成功,那么只能從相應文件中讀入
// 所缺的數據頁面到指定線性地址處。
void do_no_page(unsigned long error_code,unsigned long address)
{
    int nr[4];
    unsigned long tmp;
    unsigned long page;
    int block,i;

    // 首先取線性空間中指定地址address處頁面地址。從而可算出指定線性地址在進程
    // 空間相對於進程基地址的偏移長度值tmp,即對應的邏輯地址。
    address &= 0xfffff000;
    tmp = address - current->start_code;
    // 若當進程的executable節點指針空,或者指定地址超出(代碼+數據)長度,則申請
    // 一頁物理內存,並映射到指定的線性地址處。executable是進程正在運行的執行文
    // 件的i節點結構。由於任務0和任務1的代碼在內核中,因此任務0,任務1以及任務1
    // 派生的沒有調用過execute()的所有任務的executable都為0.若該值為0,或者參數
    // 指定的線性地址超出代碼加數據長度,則表明進程在申請新的內存頁面存放堆或棧
    // 中數據。因此直接調用取空閑頁面函數get_empty_page()為進程申請一頁物理內存
    // 並映射到指定線性地址處。進程任務結構字段start_code是線性地址空間中進程代
    // 碼段地址,字段end_data是代碼加數據長度。對於Linux0.11內核,它的代碼段和
    // 數據段其實基址相同。
    if (!current->executable || tmp >= current->end_data) {
        get_empty_page(address);
        return;
    }
    if (share_page(tmp))
        return;
    if (!(page = get_free_page()))
        oom();
/* remember that 1 block is used for header */
    // 因為塊設備上存放的執行文件映象第1塊數據是程序頭結構,因此在讀取該文件時
    // 需要跳過第1塊數據。所以需要首先計算缺頁所在數據塊號。因為每塊數據長度為
    // BLOCK_SIZE=1KB,因此一頁內存課存放4個數據塊。進程邏輯地址tmp除以數據塊大
    // 小再加上1即可得出缺少的頁面在執行映象文件中的起始塊號block。根據這個塊號
    // 和執行文件的i節點,我們就可以從映射位圖中找到對應塊設備中對應的設備邏輯塊
    // 號(保存在nr[]數組中)。利用bread_page()即可把這4個邏輯塊讀入到物理頁面page中。
    block = 1 + tmp/BLOCK_SIZE;
    for (i=0 ; i<4 ; block++,i++)
        nr[i] = bmap(current->executable,block);
    bread_page(page,current->executable->i_dev,nr);
    // 在讀設備邏輯塊操作時,可能會出現這樣一種情況,即在執行文件中的讀取頁面位
    // 置可能離文件尾不到1個頁面的長度。因此就可能讀入一些無用的信息,下面的操作
    // 就是把這部分超出執行文件end_data以后的部分清零處理。
    i = tmp + 4096 - current->end_data;
    tmp = page + 4096;
    while (i-- > 0) {
        tmp--;
        *(char *)tmp = 0;
    }
    // 最后把引起缺頁異常的一頁物理頁面映射到指定線性地址address處。若操作成功
    // 就返回。否則就釋放內存頁,顯示內存不夠。
    if (put_page(page,address))
        return;
    free_page(page);
    oom();
}

   (4)當兩個進程共享代碼、甚至數據段的物理內存時,如果一個進程改寫了物理內存,那么另一個進程是不是也要受影響了?比如同時打開兩個notepad,其中一個編輯,另一個不變,是不是另一個也能實時跟新編輯內容了?顯然不行(如何行那還的了?)!  其中一個編輯,會更改數據段的內容,此時如果想要另一個進程免受影響,會啟動“copy on write”機制,給其中一個進程單獨分配物理頁,並重新掛載到頁目錄項,實現代碼如下:

/*
 * This routine handles present pages, when users try to write
 * to a shared page. It is done by copying the page to a new address
 * and decrementing the shared-page counter for the old page.
 *
 * If it's in code space we exit with a segment error.
 */
//// 執行寫保護頁處理。
// 是寫共享頁面處理函數。是頁異常中斷處理過程中調用的C函數。在page.s程序中被調用。
// 參數error_code是進程在寫寫保護頁面時由CPU自動產生,address是頁面線性地址。
// 寫共享頁面時,需復制頁面(寫時復制).
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* we cannot do this yet: the estdio library writes to code space */
/* stupid, stupid. I really want the libc.a from GNU */
    if (CODE_SPACE(address))
        do_exit(SIGSEGV);
#endif
    // 調用上面函數un_wp_page()來處理取消頁面保護。但首先需要為其准備好參數。參
    // 數是線性地址address指定頁面在頁表中的頁表項指針,其計算方法是:
    // 1.((address>>10) & 0xffc): 計算指定線性地址中頁表項在頁表中的偏移地址;因
    // 為根據線性地址結構,(address>>12)就是頁表項中的索引,但每項占4個字節,因
    // 此乘4后:(address>>12)<<2=(address>>10)&0xffc就可得到頁表項在表中的偏移
    // 地址。與操作&0xffc用於限制地址范圍在一個頁面內。又因為只移動了10位,因此
    // 最后2位是線性地址低12位中的最高2位,也應屏蔽掉。因此求線性地址中頁表項在
    // 頁表中偏移地址直觀一些的表示方法是(((address>>12)&ox3ff)<<2).
    // 2.(0xfffff000 & *((address>>20) &0xffc)):用於取目錄項中頁表的地址值;其中,
    // ((address>>20) &0xffc)用於取線性地址中的目錄索引項在目錄表中的偏移地址。
    // 因為address>>22是目錄項索引值,但每項4個字節,因此乘以4后:(address>>22)<<2
    // = (address>>20)就是指定在目錄表中的偏移地址。&0xffc用於屏蔽目錄項索引值中
    // 最后2位。因為只移動了20位,因此最后2位是頁表索引的內容,應該屏蔽掉。而
    // *((address>>20) &0xffc)則是取指定目錄表項內容中對應頁表的物理地址。最后與
    // 上0xfffff000用於屏蔽掉頁目錄項內容中的一些標志位(目錄項低12位)。直觀表示為
    // (0xfffff000 & *(unsigned log *) (((address>>22) & 0x3ff)<<2)).
    // 3.由1中頁表項中偏移地址加上2中目錄表項內容中對應頁表的物理地址即可得到頁
    // 表項的指針(物理地址)。這里對共享的頁面進行復制。
    un_wp_page((unsigned long *)
        (((address>>10) & 0xffc) + (0xfffff000 &
        *((unsigned long *) ((address>>20) &0xffc)))));

}

   上面只調用了一個函數,核心功能就是:(1)去掉頁面寫保護   (2)從新找個未使用的物理頁掛載到頁表項(第二級) (3)物理頁數據復制

//// 取消寫保護頁面函數。用於頁異常中斷過程中寫保護異常的處理(寫時復制)。
// 在內核創建進程時,新進程與父進程被設置成共享代碼和數據內存頁面,並且所有這些
// 頁面均被設置成只讀頁面。而當新進程或原進程需要向內存頁面寫數據時,CPU就會檢測
// 到這個情況並產生頁面寫保護異常。於是在這個函數中內核就會首先判斷要寫的頁面是
// 否被共享。若沒有則把頁面設置成可寫然后退出。若頁面是出於共享狀態,則需要重新
// 申請一新頁面並復制被寫頁面內容,以供寫進程單獨使用。共享被取消。本函數供下面
// do_wp_page()調用。
// 輸入參數為頁表項指針,是物理地址。[up_wp_page -- Un-Write Protect Page]
void un_wp_page(unsigned long * table_entry)
{
    unsigned long old_page,new_page;

    // 首先取參數指定的頁表項中物理頁面位置(地址)並判斷該頁面是否是共享頁面。如
    // 果原頁面地址大於內存低端LOW_MEM(表示在主內存區中),並且其在頁面映射字節
    // 圖數組中值為1(表示頁面僅被引用1次,頁面沒有被共享),則在該頁面的頁表項
    // 中置R/W標志(可寫),並刷新頁變換高速緩沖,然后返回。即如果該內存頁面此時只
    // 被一個進程使用,並且不是內核中的進程,就直接把屬性改為可寫即可,不用再重
    // 新申請一個新頁面。
    old_page = 0xfffff000 & *table_entry;
    if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
        *table_entry |= 2;
        invalidate();
        return;
    }
    // 否則就需要在主內存區申請一頁空閑頁面給執行寫操作的進程單獨使用,取消頁面
    // 共享。如果原頁面大於內存低端(則意味着mem_map[]>1,頁面是共享的),則將原頁
    // 面的頁面映射字節數組遞減1。然后將指定頁表項內容更新為新頁面地址,並置可讀
    // 寫等標志(U/S、R/W、P)。在刷新頁變換高速緩沖之后,最后將原頁面內容復制
    // 到新頁面上。
    if (!(new_page=get_free_page()))
        oom();
    if (old_page >= LOW_MEM)
        mem_map[MAP_NR(old_page)]--;
    *table_entry = new_page | 7;
    invalidate();
    copy_page(old_page,new_page);
}    

  (5)page_fault的編號是0x14,linux的handler是這樣的。里面有兩個最重要的函數調用:缺頁時調用do_no_page;寫入只讀頁時調用do_wp_page

/*
 * page.s contains the low-level page-exception code.
 * the real work is done in mm.c
 */

.globl page_fault       # 聲明為全局變量。將在traps.c中用於設置頁異常描述符。

page_fault:
    xchgl %eax,(%esp)       # 取出錯碼到eax;發生異常后,cpu會把error_code自動入棧,不需要軟件設置干預
    pushl %ecx
    pushl %edx
    push %ds
    push %es
    push %fs
    movl $0x10,%edx         # 置內核數據段選擇符
    mov %dx,%ds
    mov %dx,%es
    mov %dx,%fs
    movl %cr2,%edx          # 取引起頁面異常的線性地址;發生異常后,cpu會把異常的地址放入CR2寄存器,不需要軟件設置干預
    pushl %edx              # 將該線性地址和出錯碼壓入棧中,作為將調用函數的參數
    pushl %eax
    testl $1,%eax           # 測試頁存在標志P(為0),如果不是缺頁引起的異常則跳轉
    jne 1f
    call do_no_page         # 調用缺頁處理函數
    jmp 2f
1:    call do_wp_page         # 調用寫保護處理函數
2:    addl $8,%esp            # 丟棄壓入棧的兩個參數,彈出棧中寄存器並退出中斷。
    pop %fs
    pop %es
    pop %ds
    popl %edx
    popl %ecx
    popl %eax
    iret

 

其他:

  (1)判斷給定線性地址是否位於當前進程的代碼段中,這個思路可用於檢測自己的so是否被第三方調用了

// CODE_SPACE(addr)((((addr)+0xfff)&~0xfff)<current->start_code+current->end_code).
// 該宏用於判斷給定線性地址是否位於當前進程的代碼段中,"(((addr)+4095)&~4095)"
// 用於取得線性地址addr所在內存頁面的末端地址。
/*
    ~4095=0xF000,作用是把低12bit清零,只保留高4bit;
    (addr)+4095:相當於頁的進位相加,比如addr=2048,那么結果等於6143=0x17ff
    (((addr)+4095)&~4095) = 0x17ff&0xF000=0x1000,也就是說虛擬地址2048所在頁的末端地址是0x1000=4096
*/
#define CODE_SPACE(addr) ((((addr)+4095)&~4095) < \
current->start_code + current->end_code)

  (2)史上最快內存數據復制函數:直接用movsl批量復制

// 從from處復制1頁內存到to處(4K字節)。
#define copy_page(from,to) \
__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024))

   (3)如果運行的用戶程序實在太多,物理內存確實不夠用了,需要物理內存的應用程序會被中止,並報OOM的錯誤(java的碼農同學是不是很熟悉了?)

//// 顯示內存已用完出錯信息,並退出。
static inline volatile void oom(void)
{
    printk("out of memory\n\r");
    // do_exit應該使用退出代碼,這里用了信號值SIGSEGV(11)相同值的出錯碼含義是
    // “資源暫時不可用”,正好同義。
    do_exit(SIGSEGV);
}

  (4)源碼中有大量的這種代碼,這是用來干啥的了?

*((unsigned long *) ((address>>20) &0xffc))

  我們挨個分解:

  •  address>>22是目錄項索引值(地址移位后只剩10bit了,也就是第一級的頁目錄表)
  • 由於是索引值,需要乘以4得到地址,所以(address>>22)<<2 = address>>20了
  • 由於只移動了20位,還有2位要清零,所以&FFC
  • 最后 *((address>>20) &0xffc) 則是取指定目錄表項(第一級)內容中對應頁表的物理地址(取出來的物理地址0x1000對齊)

  

參考:

1、https://zhuanlan.zhihu.com/p/67053210  頁表描述符


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM