天天看點

Linux核心筆記004 - 從記憶體管理開始,認識Linux核心

本文轉自網絡文章,内容均為非盈利,版權歸原作者所有。

轉載此文章僅為個人收藏,分享知識,如有侵權,馬上删除。

原文作者:jmpcall

專欄位址:https://zhuanlan.kanxue.com/user-815036.htm

1. 系統初始化

    Linux核心筆記001、Linux核心筆記002、Linux核心筆記003,對應的是《Linux核心源代碼情景分析》第一章内容,在進入第二章學習之前,本篇筆記先跳躍到第10.1節——系統初始化(第一階段):

  • 實模式

        記憶體是揮發性的存儲媒體,斷電後,資料就沒有了,相應的,開機時,是通過"燒"在不揮發存儲媒體上的初始引導程式(比如BIOS程式),将核心程式從磁盤引導區讀入記憶體的。在核心被加載到記憶體後,指令就會從BIOS跳轉到核心代碼開始執行,這時CPU還是實模式狀态,核心代碼做好一些基礎準備後,再通過設定相關寄存器,将CPU切換到保護模式狀态。

        這個過程後續會詳細學習,本篇筆記暫不記錄,目前隻需要知道一個gdt_table變量:

ENTRY(gdt_table)
    .quad 0x0000000000000000   /* NULL descriptor */
    .quad 0x0000000000000000   /* not used */
    .quad 0x00cf9a000000ffff   /* 0x10 kernel 4GB code at 0x00000000 */
    .quad 0x00cf92000000ffff   /* 0x18 kernel 4GB data at 0x00000000 */
    .quad 0x00cffa000000ffff   /* 0x23 user   4GB code at 0x00000000 */
    .quad 0x00cff2000000ffff   /* 0x2b user   4GB data at 0x00000000 */
    .quad 0x0000000000000000   /* not used */
    .quad 0x0000000000000000   /* not used */
    /*
     * The APM segments have byte granularity and their bases
     * and limits are set at run time.
     */
    .quad 0x0040920000000000   /* 0x40 APM set up for bad BIOS's */
    .quad 0x00409a0000000000   /* 0x48 APM CS    code */
    .quad 0x00009a0000000000   /* 0x50 APM CS 16 code (16 bit) */
    .quad 0x0040920000000000   /* 0x58 APM DS    data */
    .fill NR_CPUS*4,8,0        /* space for TSS's and LDT's */
           

        以上代碼,就是核心對這個變量的初始化内容,編譯後,這個内容會存入核心鏡像檔案的資料段,跟鏡像檔案的指令段内容一樣,也會在開機時,被加載到記憶體的指定位置。書中1.2節,介紹過GDTR/LDTR寄存器,通過gdt_table變量名,應該已經可以猜出,它就是GDTR指向的全局描述符表,用于CPU切換到保護模式時進行段式尋址。

        另外,在實模式階段,核心會将CS寄存器設定為__KERNEL_CS,即0x10(二進制:10|0|00),按照保護模式下段寄存器的含義,即:index=2、TI=0、RPL=0。

        "TI=0"表示使用全局描述符表,即gdt_table,則"index=2"索引到的描述符為0x00cf9a000000ffff,轉換為二進制:

0000 0000 1100 1111 1001 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
           

        再結合段描述符的結構定義,得到:

        ① B0-B15、B16-B31都為0,即段基址為0

        ② L0-L15、L16-L19都為1,G位為1,即段長度為4G

        也就是說,CS寄存器"指向"的段,是從0位址開始,4G長度的整個記憶體。其實,"3、4、5"索引處的描述符,同樣是這種情況:"3"與"2"的差別,僅在于type字段,分别表示代碼段、資料段,用于指派給CS和DS、ES、SS;"4、5"與"2、3"相比,又僅僅是RPL字段有差別,表示權限級别分别為0、3。

        這是因為,Linux核心緊接着就會跳轉到startup_32代碼處,開啟CPU的頁式管理功能,它根本沒打算使用段式映射的方式,進行記憶體管理,隻是由于80386尋址邏輯,總是會先經過段式映射過程,Linux核心這樣設定,一方面保證不會遇到CPU内部的檢查錯誤(越權、越界),另一方面,映射後邏輯位址也能保持不變,在軟體層,對于後續的頁式映射過程,讓段式映射過程變得"透明"了;

  • 保護模式

        跳轉到startup_32時,CPU已經切換到保護模式狀态了,同進入保護模式前,要設定好段寄存器、段描述符表的道理一樣,在開啟頁式映射功能前,也要準備好一定量的目錄表、頁面表内容。

        我當初就有過一個疑問:開啟頁式管理時通路記憶體,要事先由配置設定函數建立了映射關系才行,那麼剛開啟頁式管理時,配置設定函數本身需要通路的記憶體,又是什麼時候建立映射關系的呢?

        其實,這裡就是源頭。可以了解成,頁式管理開啟前,指令中包含什麼位址,實際通路到的也是這個位址,沒有"配置設定"的概念,頁式管理開啟後,指令中直接包含的位址,都要利用"配置設定"操作事先建立的頁式映射關系,才能得到實體位址。這裡就相當于,在為開啟頁式映射後緊接着的一些操作"配置設定"記憶體,跟應用程式開發中,執行一個算法前,先調用malloc()配置設定一塊記憶體,是一樣的道理。

/*
 * swapper_pg_dir is the main page directory, address 0x00101000
 *
 * On entry, %esi points to the real-mode code as a 32-bit pointer.
 */
/*
 * 引導過程更之前的階段,會将startup_32代碼片段,複制到0x100000實體位址處,并且跳轉語句為"ljmp 0x100000",而不是"ljmp startup_32":
 * 如果在程式裡,将跳轉語句寫成"ljmp startup_32",編譯後會生成形似"ff 2d XX XX XX CX"的二進制指令,"XXXXXXX"部分,表示startup_32指令塊在二進制檔案中的偏移,它受整個程式中定義變量的多少,以及其它函數的情況影響,增删一個函數,或者在某個函數增删一行代碼,都有可能會影響startup_32的位置,另外,為了保證核心基本邏輯,通路的都是核心空間,核心程式中所有符号的位址,都會加上連結腳本中指定的偏移0xC0000000,進而最終生成到跳轉指令中的位址為0xCXXXXXXX
 * 而此時已經是保護模式,再加上Linux核心的設計,使得段式映射前後的位址保持不變,是以"ljmp startup_32"指令就會跳轉到真實記憶體的0xCXXXXXXX處,而不是startup_32真正所在的0x100000處
*/
ENTRY(stext)
ENTRY(_stext)
startup_32:
/*
 * Set segments to known values
 */
/*
 * DS、ES、FS、GS都設定為__KERNEL_DS
 * Linux核心真正希望使用的隻有頁式管理,但由于80386硬體設計的原因,進入頁式映射前,要保證段式映射也能順利完成
*/
    cld    ; DF标志位清0
    movl $(__KERNEL_DS),%eax
    movl %eax,%ds
    movl %eax,%es
    movl %eax,%fs
    movl %eax,%gs
#ifdef CONFIG_SMP
/*
 * BX在本段代碼中,表示"是否為次cpu"
 * bx與自己相或,結果作為跳轉條件,如果bx為0,即主cpu執行到這,會跳轉到緊接着的第一個"1"标号處
*/
    orw %bx,%bx
    jz 1f
 
/*
 * New page tables may be in 4Mbyte page mode and may
 * be using the global pages. 
 *
 * NOTE! If we are on a 486 we may have no cr4 at all!
 * So we do not try to touch it unless we really have
 * some bits in it to set.  This won't work if the BSP
 * implements cr4 but this AP does not -- very unlikely
 * but be warned!  The same applies to the pse feature
 * if not equally supported. --macro
 *
 * NOTE! We have to correct for the fact that we're
 * not yet offset PAGE_OFFSET..
 */
/*
 * 次cpu進入startup_32前,bx被設定為1,執行這段代碼,最關鍵的邏輯是,直接跳轉到"3"标号處,使用主cpu設定好的頁表,自己不再設定
*/
#define cr4_bits mmu_cr4_features-__PAGE_OFFSET
    cmpl $0,cr4_bits
    je 3f
        /* 如果支援PSE/PAE,設定CR4寄存器 */
    movl %cr4,%eax     # Turn on paging options (PSE,PAE,..)
    orl cr4_bits,%eax
    movl %eax,%cr4
    jmp 3f
1:
#endif
/*
 * Initialize page tables
 */
/*
 * 彙編代碼中,可以通過.org讓編譯器将變量安排在指定偏移處,比如pg0、empty_zero_page,分别被安排在二進制檔案中的0x2000、0x4000處,加上連結腳本指定的偏移,編譯位址分别為0xC0002000、0xC0004000
 * "Linux核心筆記002"已經說明過,0-3G範圍的虛拟位址,在不同程序中,會映射到不同的實體位址,而3G-4G的虛拟位址為核心空間,映射關系不能因程序不同而不同,否則就跟使用者空間一樣屬于各個程序的私有空間了,進而,對于3G-3G+896MB範圍的所有虛拟位址,都是按照"減0xC0000000偏移"的規則,建立一個固定的映射關系(剩餘128MB核心空間屬于高端記憶體,後期學習)
 * 那麼,對于虛拟位址0xC0002000、0xC0004000,經過映射後,對應的實體位址就應該分别為0x2000、0x4000,然而代碼執行到此處,還沒開啟頁式管理,也還沒有建立這樣的映射,是以通過指令本身減掉了0xC0000000(__PAGE_OFFSET)偏移,它跟開啟頁式管理後,cpu通過映射找到的實體位址是一樣的
 * "1"、"2"标号處代碼結合一起,正是用于建立0xC0000000-0xC0800000這部分核心空間(開頭8MB)的映射,從0x2000開始,依次寫入頁表項0x007、0x1007、0x2007..,直到0x4000處結束,進而建立了2個PT(頁表),後續再指定好目錄表,并設定好目錄項,開啟項式管理後,就可以正常通路0xC0000000-0xC0800000範圍内的虛拟位址了
*/
    movl $pg0-__PAGE_OFFSET,%edi /* initialize page tables */
    movl $007,%eax     /* "007" doesn't mean with right to kill, but
                   PRESENT+RW+USER */
2:  stosl    ; 将EAX值,複制到ES:DI,此時為保護模式狀态,擴充段為從0開始的整個4G空間,是以ES:DI從0x2000開始
    add $0x1000,%eax    ; 0x007、0x1007、0x2007 ..
    cmp $empty_zero_page-__PAGE_OFFSET,%edi
    jne 2b
 
/*
 * Enable paging
 */
/*
 * swapper_pg_dir由.org指定偏移為0x1000,程式将它的位址設定到CR3寄存器中,即用它指向的一頁内容,作為目錄表
 * "80386硬體API"會把CR3"參數"的值,直接當作實體位址,由于此時還沒有開啟頁式管理,仍然需要指令本身從編譯生成的虛拟位址中,減掉__PAGE_OFFSET偏移
 * 然後,将CR0最高位(PG标志位)設定為1,開啟頁式管理
*/
3:
    movl $swapper_pg_dir-__PAGE_OFFSET,%eax
    movl %eax,%cr3     /* set the page table pointer.. */
    movl %cr0,%eax
    orl $0x80000000,%eax
    movl %eax,%cr0     /* ..and set paging (PG) bit */
/*
 * 以下這條跳轉指令,用于丢棄已經在"cpu的取指令流水線"中的内容(Intel在i386技術資料中的建議)
 * 另外,每執行一條指令,IP寄存器的值,就會加上這條指令的長度,指向下一條指令,到目前為止,IP寄存器都是在0x100000的基礎上加,接下來這條指令的位址大概為0x100XXX,由于上一條指令已經開啟頁式映射,是以這個位址也需要有映射關系,cpu才能得到它的實體位址,其實,稍後就可以看到,目錄表最開始2項,也有初始值,也指向上面建立的2個頁表,進而使得0-8MB虛拟位址也有映射關系,并且可以保持映射前後的值不變,目的就是用于這種過渡期
*/
    jmp 1f         /* flush the prefetch-queue */
1:
/*
 *  編譯後,"1f"标号的位址為0xCXXXXXXX,按照如下指令跳轉一下,IP寄存器的值就是核心空間的位址了,就不用依賴目錄表中最開始的2個目錄項了,另外,引用程式中的變量,也不用依賴指令本身減掉__PAGE_OFFSET,進而完全過渡到頁式管理
*/
    movl $1f,%eax
    jmp *%eax      /* make sure eip is relocated */
1:
    /* Set up the stack pointer */
    lss stack_start,%esp
           

        pg0、pg1、empty_zero_page位置安排:

/*
 * The page tables are initialized to only 8MB here - the final page
 * tables are set up later depending on memory size.
 */
.org 0x2000
ENTRY(pg0)
 
.org 0x3000
ENTRY(pg1)
 
/*
 * empty_zero_page must immediately follow the page tables ! (The
 * initialization loop counts until empty_zero_page)
 */
 
.org 0x4000
ENTRY(empty_zero_page)
           

        目錄表初始化内容:

/*
 * This is initialized to create an identity-mapping at 0-8M (for bootup
 * purposes) and another mapping of the 0-8M area at virtual address
 * PAGE_OFFSET.
 */
.org 0x1000
ENTRY(swapper_pg_dir)
        /* 用于0-8MB虛拟位址映射(屬于使用者空間,與核心空間開頭8MB映射到相同的實體位址,臨時用于過渡,最終會被撤消) */
    .long 0x00102007
    .long 0x00103007
        /* 接下來766個目錄項初始化為0 */
    .fill BOOT_USER_PGD_PTRS-2,4,0
    /* default: 766 entries */
        /* 用于 0xC0000000 - 0xC0000000+8MB 核心位址映射 */
    .long 0x00102007
    .long 0x00103007
        /* 接下來254個目錄項初始化為0 */
    /* default: 254 entries */
    .fill BOOT_KERNEL_PGD_PTRS-2,4,0
           

2. Linux記憶體管理的基本架構

    通過Linux核心對待80386段式管理特性的方式,很容易可以了解,軟體可以根據自己的需要,選擇性的使用硬體特性,比如剪刀一般是用于剪東西,但有些人也會用它松螺絲。換句話說,隻要最終能将"硬體API"的"參數"設定正确,保證硬體内部不出錯,将"參數"設定成什麼,以及如何"設定",都由軟體自己決定。

    對于頁式管理特性的利用,Linux核心的做法如下:

Linux核心筆記004 - 從記憶體管理開始,認識Linux核心
  • 雖然看起來有些"特别"

        線性位址到實體位址的映射,80386的硬體邏輯是:線性位址前10位作為索引,到目錄表中找到目錄項,進而找到一張頁表;再根據接着的10位,到頁表中找到而表項,進而找到目标頁;根據最後12位的偏移值,最終在目标頁中映射到一個實體位址。

        那麼,Linux核心在建立映射關系時,也應該将線性位址看成3部分,隻不過硬體是使用,軟體是設定,比如線性位址"0x80000000",如果核心按8位、8位、4位、12位劃分,建立映射時,它設定的就是0x80下标的目錄項,而硬體執行映射時,找到的是0x200下标的目錄項,顯然就會是空的或者其它虛拟位址的目錄項。

0x 1000000000 0000000000  000000000000  // 10,10,12
0x 10000000 00000000 0000 000000000000  // 8,8,4,12
           
  • 但也很容易了解

        首先,4層是邏輯劃分,如果劃分成10,0,10,12,那麼實際劃分仍然是3層,這是可行性;另外,Linux軟體不光隻運作在80386上,還要支援其它型号的cpu,包括Intel的其它系列,以及其它廠商的cpu,不光要考慮當下,還要考慮将來,因為cpu都已經從8位發展到32位了,将來勢必會發展到更多位數,邏輯上支援4層,對于不同的cpu,簡單指定下位數的配置設定就能适應了,這是軟體設計的必要性。

3. 位址映射的全過程

    前面已經學習了10.1節,那麼這部分内容其實就非常好了解了,書裡面拿了一個使用者空間的位址舉例,目前為止,初學者可能還是不能完全體會使用者态和核心态的本質,為了保持節奏,可以先不用擔心這一點,學習完第三章——中斷、異常和系統調用,以及第四章——程序與程序排程,自然就能明白了。

    目前為止,隻是學習了映射過程,關于記憶體管理的内容還很多:

  • 每個程序有4G虛拟空間,其中0-3G由各個程序獨自使用,一方面數量有上限,也屬于資源,另一方面映射關系還沒有撤消,就不能拿來映射到另外一個實體位址,是以核心要對每個程序的虛拟空間進行管理,另外,實體位址更加需要管理;
  • 虛拟空間又分為棧和堆,局部變量使用的是棧記憶體,malloc()配置設定的是堆記憶體,核心對它們的管理,也有差別;
  • 換入換出技術,支援将記憶體的資料,臨時存入交換分區,需要時再從磁盤讀回記憶體,也涉及到複雜的管理邏輯。

    是以不要松懈,繼續加油!

繼續閱讀