1. 程式人生 > >深入理解Linux核心day01--記憶體定址

深入理解Linux核心day01--記憶體定址

記憶體定址


記憶體地址:

    邏輯地址: 段+偏移量 組成
    線性地址: 可用來表達4GB的地址 (也稱虛擬地址)
    實體地址: 用於記憶體晶片級記憶體單元定址。他們與微處理器地址引腳傳送到記憶體總線上的電訊號相對應
    記憶體控制單元(MMU) 通過一種稱為分段單元的硬體店裡把一個邏輯地址轉換為線性地址,接著通過分頁單元的硬體電路把線性地址轉換成一個實體地址。
              分段單元              分頁單元
    邏輯地址----------》線性地址---------------》實體地址
    

硬體中的分段:

    從80286模式開始,Intel微處理器以兩種不同的方式執行地址轉換,這兩種方式分別為 真實模式 和 保護模式。

段選擇符和段暫存器:

    一個邏輯地址由兩個部分組成:一個段識別符號和一個指定段內相對地址的偏移符。
    段識別符號是一個16位長的欄位,稱為段選擇符
    段偏移量是一個32位常的欄位。
    為了快速方便地找到段選擇符,處理器提供了段暫存器,段暫存器的唯一目的就是存放段選擇符。這些暫存器稱為CS,SS,DS,ES,FS,GS;
    3個有專門的用途:
        CS 程式碼段暫存器,指向包含程式指令的段。
        SS 棧段暫存器,指向包含當前程式棧的段。
        DS 資料段暫存器,指向包含靜態資料或者全域性資料段。
    CS暫存器還有一個很重要的功能:它含有一個兩位的欄位,用以指明CPU的當前特權級。值為0代表最高優先順序,而值為3表示最低優先順序。Linux只用0級和3級,分別稱為核心態和使用者態。

段描述符:

    每個段都有一個8位元組的段描述符表示,它描述段的特徵。段描述符放在全域性描述符表(GDT)或區域性描述符表中(LDT)。
    GDT在主存中的地址和大小存放在gdtr控制暫存器中,當前正被使用的LDT地址和大小放在ldtr控制暫存器中。
    

快速訪問段描述符:

    邏輯地址由16位段選擇符和32位偏移量組成,段暫存器僅僅存放段選擇符。
    每當一個段選擇符被裝入段暫存器時,相應的段描述符就由記憶體裝入到對應的非程式設計CPU暫存器。
    由於一個段描述符是8位元組長,因此它在GDT或者LDT內的相對地址是由段選擇符的最高13位的值乘以8得到的。
    

分段單元:

    一個邏輯地址如何轉換成相應的線性地址?
        1、線檢查段選擇符的TI欄位,以決定段描述符儲存在哪一個描述符表中。TI欄位指明描述符是在GDT中(在這種情況下,分段單元從gdtr暫存器中得到GDT的線性基地址)還是在啟用的LDT中(在這種情況下,分段單元從ldtr暫存器中得到GDT的線性基地址)。
        2、從段選擇符的index欄位計算段描述符的地址,index欄位的值乘以8(一個段描述符的大小),這個結果再與gdtr或ldtr暫存器中的內容相加。
        3、把邏輯地址的偏移量和段描述符base欄位的值相加就得到了線性地址。
        注意:有了與段暫存器相關的不可程式設計暫存器,只有當段暫存器的內容被改變時才需要執行前兩個操作。

Linux中的分段:

    分段可以給每一個程序分配不同的線性地址空間,而分頁可以把同一線性地址空間空間對映到不同的物理空間。
    與分段相比Linux更喜歡分頁方式,因為:
        1、當所有記憶體使用相同的段暫存器值時,記憶體管理變得更簡單,也就是說他們能共享同樣的一組線性地址。
        2、Linux設計目標之一是可以把他一直到絕大多數留下的處理器平臺上,然而,risc體系結構對分段的支援很有限。
    執行在使用者態的所有Linux程序都是用一對相同的段來對指令和資料定址。這兩個段就是所謂的使用者程式碼段和使用者資料段。類似的,執行在核心態的所有Linux程序都是用一堆相同的段對指令和資料定址;他們分別叫做核心程式碼段和核心資料段。
    相應的段選擇符由巨集__USER_CS、__USER_DS、__KERNEL_CS 、__KERNEL_DS分別定義。例如對核心程式碼段定址,核心只需要把__KERNEL_CS巨集產生的值裝進CS段暫存器中即可。
    注意,與段相關的線性地址從0開始,達到2的32此方-1的定址限長。這就意味著在使用者態或核心態下的所有程序都可以使用相同的邏輯地址。
    所有段都從0x00000000開始,這可以得到另一個重要結論,那就是在Linux下邏輯地址與線性地址是一致的,即邏輯地址的偏移量欄位的值與相應的線性地址的值總是一致的。
    

Linux GDT

    在單處理器系統中只有一個GDT,而在多處理器系統中每個CPU對應一個GDT。所有的GDT都存放在cpu_gdt_table陣列中,而所有GDT的地址和它們的大小(當初始化gdtr暫存器時使用)被存放在cpu_gdt_descr陣列中。
    每個GDT包含18個段描述符和14個空的,未使用的,或保留的項。插入為使用項的目的是為了使經常一起訪問的描述符能夠處理同一個32位元組的硬體快取記憶體行中。
    每一個GDT中包含的18個段描述符指向下列的段:
        使用者態和核心態下的程式碼段和資料段共4個。
        任務狀態段(TSS),每個處理器有一個。
        一個包括預設區域性描述表的段。
        三個區域性執行緒儲存(TLS)段:這個機制允許多執行緒應用程式使用最多3個區域性與執行緒的資料段。系統使用set_thread_area()和get_thread_area()分別為正在執行的程序建立和撤銷一個TLS段
        與高階電源管理(AMP)相關的3個段。
        與支援即插即用(PnP)功能的BIOS服務程式相關的5個段.
        被核心用來處理“雙重錯誤”異常的特殊TSS段。
        

Linux LDT

    大多數使用者態下Linux程式不使用區域性描述符表,這樣核心就定義了一個預設的LDT工大多數程序共享。預設的區域性描述表存放在default_ldt陣列中。它包含5個項,但是核心僅僅有效的使用了兩個項.
    某些情況下,程序仍然需要建立自己的區域性描述符。這對有些應用程式有用像wine那樣的程式,他們面向段的微軟Windows應用程式。modify_ldt()系統呼叫允許程序建立自己的區域性描述符表。
    

硬體中的分頁

    分頁單元把線性地址轉換為實體地址。
    其中的一個關鍵任務就是把所請求的訪問型別與線性地址的訪問許可權相比較,如果這次記憶體訪問是無效的,就產生一個缺頁異常。
    為了效率起見,線性地址被分成以固定長度的組,稱為頁(page)。頁內部連續的線性地址被對映到連續的實體地址中。這樣,核心可以指定一個頁的實體地址和其存取許可權。而不用指定頁所包含的全部線性地址的存取許可權。
    分頁單元把所有的RAM分成固定長度的頁框(page frame)(有時叫做物理頁)。每一個頁框包含一個頁(page),也就是說一個頁框的長度就是一個頁的長度。
    把線性地址對映到實體地址的資料結構稱為頁表(page table)。頁表存放在主存中,並在啟用分頁單元之前必須由核心對頁表進行適當的初始化。
    

常規分頁:

    從80386起,Intel處理器的分頁單元處理4KB的頁。
    32位的線性地址被分為3個域:
        Directory(目錄) 最高10位
        Table(頁表) 中間10位
        Offset(偏移量) 最低10位
    線性地址的轉換分為兩步完成,每一步都是基於一種轉換表,第一種轉換表稱為頁目錄表(page directory),第二種轉換表稱為頁表(page table)
    使用這種二級模式的目的在於減少每一個程序頁表所需RAM的數量。如果使用簡單的一級頁表,那將需要高達2的20次方個頁表來表示每個程序的頁表,即使一個程序並不使用哪個範圍內的所有地址。
    二級模式通過只為程序實際使用的那些虛擬記憶體區請求頁表來減少記憶體容量。
    每個活動程序必須有一個分配給它的頁目錄。不過,沒必要馬上為程序的所有頁表都分配RAM。只有程序實際需要一個頁表是才給該頁表分配RAM會更為有效率。
    線性地址內的Directory欄位決定頁目錄中的目錄項,而目錄項指向適當的頁表。地址的Table欄位依次有決定頁表中的表象,而頁項含有頁所有頁框的實體地址。Offset欄位決定頁框內的相對位置。由於它是12位長,故每一頁含有4096位元組的資料。
    

擴充套件分頁:

    擴充套件分頁允許頁框大小為4MB而不是4KB。擴充套件粉筆用於把大段連續的線性地址轉換成相應的實體地址,在這些情況下,核心可以不用中間頁表進行地址轉換,從而節省記憶體並保留TLB項。
    正如前面所描述的,通過設定頁目錄的Page Size標誌啟動擴充套件分頁功能。在這種情況下,分頁單元把32位的線性地址分成兩個欄位:
        directory 最高10位
        Offset 其餘22位
    擴充套件分頁和正常分頁的頁目錄項基本相同,除了:
        Page Size標誌必須被設定。
        20位實體地址欄位只有最高10位是有意義的。這是用為每一個實體地址都是在以4MB為邊界的地方開始,故這個地址的最低22位為0.
        

硬體保護方案:

    分頁單元和分段單元的保護方案不同。
    

64位作業系統的分頁

    所有64位處理器的硬體分頁系統都使用了額外的分頁級別。使用的級別數量取決於處理器的型別。
    

硬體快取記憶體:

    如今的微處理器時鐘頻率接近幾GHz,而動態RAM(DRAM)晶片的存取時間是時鐘週期的數百倍。這意味著,當從RAM中去運算元項RAM中存放結果這樣的指令執行時,CPU可能等待很長時間。
    為了縮小CPU和RAM之間的速度不匹配,引入了硬體快取記憶體記憶體。硬體快取記憶體基於著名的區域性性原理,該原理即使用程式結構和也適用於資料結構。為此,80x86體系結構中引入了一個叫行(line)的新單位。行由幾十個連續的位元組組成,它們以脈衝突發模式在慢速DRAM和快速的用來實現快取記憶體的片上靜態RAM(SRAM)之間傳遞,用來實現快取記憶體。
    快取記憶體單元插在分頁單元和主記憶體之間。它包含一個硬體快取記憶體記憶體(hardware cache memory)和一個快取記憶體控制器(cache controller)。
    快取記憶體記憶體存放記憶體中真正的行。快取記憶體控制器存放一個表項陣列,每個表項對應快取記憶體記憶體中的一個行。每個表項有一個標籤(tag)和描述快取記憶體行狀態的幾個標誌(flag)。
    

Linux中的分頁:

    Linux採用了一種同時適用於32位和64位系統的普通分頁模型。
    直到2.6.10版本,Linux採用三級分頁模型。從2.6.11版本開始,採用四級分頁模型。
    其中四級分頁模型的4中頁表分別被稱為:
        頁全域性目錄(Page Global Directory)
        頁上級目錄(Page Upper Directory)
        頁中間目錄(Page Middle Directory)
        頁表(Page Table)
        

線性地址欄位:

    下列巨集簡化了頁表處理:
        PAGE_SHIFT:指定Offset欄位的位數;
        PAGE_SIZE:用於返回頁的大小。
        PAGE_MASK:產生的值為0xfffff000,用以遮蔽Offset欄位的所有位。
        PMD_SHIFT:指定線性地址的Offset欄位和Table欄位的總位數;換句話說模式頁中間目錄項可以對映的區域大小的對數。
        PMD_SIZE:用於計算由頁中間目錄的一個單獨表項所對映的區域大小,也就是一個頁表的大小。
        PMD_MASK:用於遮蔽Offset欄位與Table欄位的所有位。
        PUD_SHIFT:確定頁上級目錄項能對映的區域大小的對數。
        PUD_SIZE:用於計算頁全域性目錄中的一個單獨表項所能對映的區域大小。
        PUD_MASK:用於遮蔽offset欄位、Table欄位、Middle Air欄位和Upper Air欄位的所有位。
        PGDIR_SHIFT:確定頁全域性目錄項能對映的區域大小的對數。
        PGDIR_SIZE:計算頁全域性目錄中一個單獨表項所能對映的區域大小。
        PGDIR_MASK:遮蔽offset、Table、Middle Air、Upper Air欄位的所有位。
        PTRS_PER_PTE,PTRS_PER_PMD,PTRS_PER_PUD,PTRS_PER_PGD:
            用於計算頁表、頁中間目錄、頁上級目錄和全域性目錄表中表項的個數。
            當PAE被禁止時,他們產生的值分別為1024,1,1,0.當PAE被啟用是,產生的值分別為512,512,1,4
        

頁表處理:

    pte_t,pmd_t,pud_t,pgd_t分別表示描述頁表項、頁中間目錄項、頁上級目錄和頁全域性目錄項的格式。當PAE被啟用時,他們都是64位的資料型別,否則都是32位資料型別。
    pgprot_t是另一個64位(PAE啟用時)或32位(PAE禁止時)的資料型別,它表示與一個單獨表項相關的保護標誌。
    五個型別轉換巨集(__pte,__pmd,__pud__pgd,__pgprot)把一個無符號整數轉換成所需的型別。
    另外五個型別轉換巨集(pte_val,pmd_val,pud_val,pgd_val,pgprot_val)執行相反的轉換,即把上面提到的四種特殊的型別轉換成一個無符號的整數。
    核心還提供了許多巨集和函式用於讀或修改頁表選項:
        1)如果相應表項的值為0,那麼紅pte_none,pmd_none,pud_none,pgd_none產生的值為1,否則產生的值為0;
        2)巨集pte_clear,pmd_clear,pud_clear,pgd_clear清除相應頁表的一個表項,由此禁止程序使用該頁表項對映的線性地址。
            ptep_get_and_clear()函式清除一個頁表項並返返回前一個值。
        3)set_ptr,set_pmd,set_pud,set_pgd向一個頁表項中寫入指定的值。
            set_pte_atomic與set_pte的作用是相同的,但是當PAE被啟用的時候同樣能保證64位的值被原子的寫入。
        4)如果a、b兩個頁表項指向同一頁並且指定相同的訪問優先順序,那麼pte_same(a,b)返回1,否則返回0。
        5)如果頁中間目錄項e指向一個大型頁(2MB或4MB)那麼pmd_large(e)返回1,否則返回0.
        
實體記憶體佈局:
    在初始化階段,核心必須建立一個實體地址對映來指定哪些實體地址範圍對核心可用而哪些不可用(或者因為它們對映硬體裝置I/O的共享記憶體、或者因為相應的頁框含有BIOS資料)。
    核心將下列頁框記為保留:
        在不可用的實體地址範圍內的頁框。
        含有核心程式碼和初始化的資料結構的頁框。
    

程序頁表

    程序的線性地址空間分為兩部分:
        從0x00000000到0xbfffffff的線性地址,無論程序允許在使用者態還是核心態都可以定址。
        從0xc0000000到0xffffffff的線性地址,只有核心態的程序才能定址。
    當程序執行在使用者態時,它產生的線性地址小於0xc0000000;當程序執行在核心態時,它執行核心程式碼,所產生的地址大於等於0xc0000000。但是,在默寫情況下,核心為了檢索或存放資料必須訪問使用者態線性地址空間。
    

核心頁表:

    核心維持著一組自己使用的頁表,駐留在所謂的主核心頁全域性目錄中。系統初始化後,這組頁表還從未被任何程序或者任何核心執行緒直接使用;更確切地說,主核心頁全域性目錄的最高目錄項部分作為參考模型,位系統中每個普通經常對應的頁全域性目錄項提供參考模型。
    核心如何初始化自己的頁表?這個過程分為兩個階段。事實上,核心映像剛剛被裝入記憶體後,CPU仍然執行在真實模式,所以分頁功能沒有被啟動。
        第一階段,核心建立一個有限的地址空間,包括核心的程式碼段和資料段、初始化頁表和用於存放動態資料結構的共128KB大小的空間。這是最小限度的地址空間僅夠將核心裝入RAM和對其初始化核心資料結構。
        第二階段,核心充分利用剩餘的RAM並適當地建立分頁表。下面解釋這個方案是如何實施的。

臨時核心頁表:

    臨時頁全域性目錄是在核心編譯過程中靜態初始化的,而臨時頁表是由startup_32()組合語言函式初始化的。在這個階段PAE支援並未啟用。
    臨時全域性目錄放在swapper_pg_dir變數中。臨時頁表在pg0變數處開始存放,緊接著核心未初始化的資料段後面。為了簡單起見,我們假設核心使用的段、臨時頁表和128KB的記憶體範圍能容納於RAM前8MB空間裡,為了對映RAM前8MB的空間,需要用到兩個頁表。
    分頁第一階段的目標是允許在真實模式下和保護模式下都容易地對著8MB定址。因此,核心必須建立一個對映,把0x00000000到0x007fffff的線性地址和0x00800000到0xc07fffff的線性地址對映到從0x00000000到0x007fffff的實體地址。
        換句話說買就是核心在初始化的第一階段,可以通過與實體地址相同的線性地址或者通過從0xc0000000開始的8MB線性地址對RAM的錢8MB進行定址。
    

當RAM小於896MB時的最終核心頁表

    由核心頁表所提供的最終對映必須從0xc0000000開始的線性地址轉換為從0開始的實體地址。
    巨集__pa用於把從PAGE_OFFSET開始的線性地址轉換為相應的實體地址,而巨集__va做相反的轉換。
    主核心頁全域性目錄仍然儲存在swapper_pg_dir變數中。它由paging_init()函式初始化。該函式進行如下操作:
        1、呼叫pagetable_init()適當地建立頁表項
        2、把swapper_pg_dir的實體地址寫入cr3控制暫存器中。
        3、如果CPU支援PAE並且如果核心編譯支援PAE,則將cr4控制暫存器的PAE標誌置位
        4、呼叫__flush_tlb_all()使TLB的所有項無效。

當RAM大小在896MB和4096MB之間時的最終核心頁表

    在這種情況下,並不把RAM全部對映到核心地址空間。Linux在初始化階段可以做的最好的事是把一個具有896MB的RAM視窗對映到核心線性地址空間。如果需要對現有RAM其餘部分定址,那麼就必須把某些其他的線性地址間隔對映到所需的RAM。這意味著修改某些頁表項的值。
    核心使用與前一種相同的程式碼來初始化頁全域性目錄。
    

當RAM大於4096MB時的最終核心頁表

    現在讓我們考慮RAM大於4GB計算機的核心頁表初始化;更確切地說,我們處理一下發生的情況:
        CPU模型支援實體地址擴充套件(PAE)
        RAM容量大於4GB
        核心以PAE支援來編譯
    儘管PAE支援36位實體地址,但是線性地址依然是32位地址。如前所述,Linux對映一個896MB的RAM視窗到核心線性地址空間;剩餘的RAM留著不對映,並用動態重對映來處理。
    以前一種情況的主要差異是使用三級分頁模型。

固定對映的線性地址

    我們看到核心線性地址第四個GB的初始部分對映系統的實體記憶體。但是至少128MB的線性地址總是留作他用,因此核心使用這些線性地址實現非連續記憶體分配和固定對映的線性地址。
    固定對映線性地址基本上是一種類似於0xffffc000這樣的常量線性地址,起對應的實體地址不比等於線性地址減去0xc0000000,而是可以以任意方式建立。
    我們將在後面的章節看到,核心使用固定對映的線性地址來代替指標變數,應為這些指標變數的值從不改變。
    每個固定對映的線性地址都存放線上性地址第四個GB的末端。
    為了把一個實體地址與固定對映的線性地址關聯起來,核心使用set_fixmap(idx,phys)和set_fixmap_nocache(idx,phys)巨集。
    這兩個函式都把fix_to_virt(idx)線性地址對應一個頁表項初始化為實體地址phys;
    反過來,clear_fixmap(idx)用來撤銷固定對映線性地址idx和實體地址之間的連線。

處理硬體快取記憶體和TLB

    硬體快取記憶體和轉換後院快取器(TLB)在提高現代計算機體系結構的效能上扮演著非常重要的角色。
    

處理硬體快取記憶體

    為了使快取記憶體的命中率達到最優化,核心在下列決策中考慮體系結構:
        一個數據結構中最常使用的欄位放在該資料結構記憶體低的低偏移部分,以便他們能夠處於快取記憶體的同一行中
        當為一個大組資料結構分配空間時,核心試圖把他們都存放在記憶體中,以便所有快取記憶體行按同一方式使用。
        

處理TLB

    處理器不能自動同步他們自己的TLB快取記憶體,因為決定線性地址和實體地址之間對映何時不再有效的是核心,而不是硬體。
    一般來說,任何程序切換都會暗示著更換活動頁表級。相對於過期的頁表,本地TLB表項必須重新整理;這個過程在核心把新的頁全域性目錄的地址寫入cr3控制器時會自動完成。
    不過核心在下列情況下將避免TLB被重新整理:
        1、當兩個使用相同頁表集的不同程序之間執行程序切換時
        2、當一個普通程序和一個核心執行緒間執行程序切換時。
    事實上,每個核心執行緒並不擁有自己的頁表集;更確切地說,它使用一個普通程序的頁表集。