Linux內存管理學習筆記——內存尋址

最近開始想稍微深刻一點地學習Linux內核,主要參考內容是《深刻理解Linux內核》和《深刻理解Linux內核架構》以及源碼,經驗有限,只能分析出有限的內容,看完這遍之後再更深刻學習吧。node

1,內存地址linux

邏輯地址:包含在機器語言中用來指定一個操做數或一條指令的地址。c#

線性地址:一個32位無符號數,用於直接映射物理地址數組

物理地址:片上引腳尋址級別的地址緩存

2,邏輯地址->線性地址cookie

2.1 段選擇符與段寄存器數據結構

邏輯地址:段選擇符(16位)+段內偏移(32位)架構

 

index:在GDT或LDT中段描述符的位置app

TI:段描述符在GDT中(TI=0),段描述符在LDT中(TI=1)ide

RPL: 請求者特權級,當段選擇符裝入cs寄存器中,指示CPU的當前特權級

爲了方便的找到段選擇符,處理器提供六個段寄存器存放段選擇符。

cs ss ds es fs gs

其中     cs:代碼段寄存器,指向包含程序指令的段

            ss:棧段寄存器,指向包含當前程序棧的段

            ds:數據段寄存器,指向包含靜態數或者全局數據段

cs寄存器的RPL字段表示CPU的當前特權級(CPL),內核態0,用戶態3

 

2.2 段描述符 

每一個段由一個8字節的段描述符表示,它描述了段的特徵。

全局描述符表(GDT)和局部描述符表(LDT)存放段描述符。

一般只定義一個GDT,而每一個進程除了存放在GDT中的段以外,若是還須要建立附加的段,就能夠有本身的LDT。

GDT在主存中的位置和大小存放在gdtr控制寄存器中,當前正在被使用的LDT地址和大小放在ldtr寄存器中。、

爲了加快主存與CPU的數據交換,引入高速緩存,架在主存與CPU中間,每把一個段選擇符放進段選擇器,同時會把相應的段描述符放進這個寄存器,加快數據交換

 

 

Base:  段基地址

G :      粒度標誌:清0,段大小以字節爲單位,不然以4096字節爲單位。

Limit: 段長度

S:       系統標誌,0:系統段,存儲諸如LDT之類關鍵數據結構

DPL:  描述符特權級,用於存取這個段都要求的特權級。

2.3邏輯地址的轉換

 

 

3,Linux中的實現

3.1Linux中的分段

Linux以很是有限的方式使用分段。

四個主要的linux段的段描述符

 

 

相應的段選擇符由宏__USER_CS,__USER_DS,__KERNEL_CS,__KERNEL_DS分別定義。

爲了對內核代碼段尋址,只須要將__KERNEL_CS宏產生的值裝入cs寄存器中便可。

 

全部段的段基地址都爲0x00000000,地址空間都是從0~232-1。

可就是,Linux下邏輯地址與線性地址是一致的,邏輯地址的偏移量字段與相應線性地址的值老是一一對應的。

 

3.2Linux GDT

一個CPU對應一個GDT,全部的GDT都存放在cpu_gdt_table數組中。

 

每一個GDT包含18個段描述符和14個空的,未使用的或保留的項。

3個局部線程存儲段(TLS):線程私有數據

4個用戶態和內核態下的代碼段,數據段。

TSS段:任務狀態段,每一個CPU一個。全部的任務狀態段都存放在init_tss數組中。

             G標誌清0,Limit爲0xeb,也就是段長爲236bytes。DPL爲0,不容許用戶態訪問。

              進程切換,進程上下文切換時,這個段用於保存CPU寄存器的內容。

LDT段:通常指向包含缺省LDT表的段。(大多數用戶態下程序都不使用LDT,因此定義一個缺省的LDT供大多數進程共享)

             modify_ldt()系統調用容許進程建立本身的局部描述符表(例如Wine程序),此時LDT段相應的被修改。

double fault TSS:處理雙重錯誤異常的特殊的TSS段??

3個與高級電源管理有關的段

5個與支持PnP功能的BIOS服務有關的段。

 

4,線性地址->物理地址

 

 控制寄存器cr3中存放正在使用的頁目錄的物理地址。

頁目錄項和頁表項具備相同的結構

Present標誌:置1,所指頁或頁表在主存中;爲0,不在主存中。

                   當訪問一個地址時,頁目錄項或頁表項的Present標誌爲0。

               分頁單元將該線性地址存放在寄存器cr2中,產生14號異常:缺頁異常。

Field              :  20位,   頁目錄項中的Field指向包含一個頁表的頁框。

                                      頁表項中的Field指向包含一頁數據的頁框。

PCD/PWT標誌: 硬件高速緩存有關

Access標誌   :  每當分頁單元對相應頁框進行尋址時,設置這個標誌。

Dirty標誌      :  只應用於頁表項中,每當對一個頁框寫操做時就設置。

Read/Write  :   頁或頁表的存取權限

User/Supervisor:  訪問頁或頁表所需的特權級

Page Size  :  只應用於頁目錄項。設置爲1,則頁目錄項指向2M或4M的內存。(hugepage)

Global標誌: 只應用於頁表項,用於防止經常使用頁(全局頁)從TLB中刷新出去。(當cr4寄存器的PGE(頁全局啓用)標誌置位時,這個標誌纔有效)

 

 對於2M頁面,啓動時傳遞內核參數

hugepages=1024 

對於1GB的頁面:

default_hugepagesz=1G hugepagesz=1G hugepages=4

CPU所支持的hugepage大小能夠由cpu標誌得知

If PSE exists, 2M hugepages are supported; if pdpe1gb exists, 1G hugepages are supported.

 

For 2 MB pages, there is also the option of allocating hugepages after the system

has booted.

echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

 

mkdir /mnt/huge

mount -t hugetlbfs nodev /mnt/huge

 

只要是在 /mnt/huge/ 目錄下建立的文件,將其映射到內存中時都會使用 2MB 做爲分頁的基本單位。值得一提的是,hugetlbfs 中的文件是不支持讀 / 寫系統調用 ( 如read()write()等 ) 的,通常對它的訪問都是之內存映射的形式進行的。

 

在實際應用中,爲了使用大頁面,還須要將應用程序與庫libhugetlb連接在一塊兒。libhugetlb庫對malloc()/free()等經常使用的內存相關的庫函數進行了重載,以使得應用程序的數據能夠放置在採用大頁面的內存區域中,以提升內存性能。 

 

 

4.2 加快線性地址轉換

爲了縮小CPU和RAM之間的速度不匹配,引入了硬件高速緩存。基於局部性原理。

cache line :高速緩存與內存間一次傳輸數據的長度。

PCD 當訪問該頁框中數據時,高速緩存功能被啓用仍是禁用。

PWT:當數據被寫到頁框時,採用通寫策略仍是回寫策略。

Linux對全部頁框都啓用高速緩存,對寫操做都採用回寫策略。

 

TLB(transition lookaside buffers):線性地址第一次尋找時,CPU會在緩慢的RAM中查看頁表,而後把結果存儲到該CPU對應的TLB,但在以後再次引用這個線性地址時,會直接調用TLB中的內容,以加速尋址。多核系統無需爲每一個CPU同步TLB,由於不一樣CPU相同的線性地址可能指向不一樣的物理空間

 

5,Linux中的分頁

 

在32位系統中,upper dir是刪除了的,middle dir至關於硬件分頁中的頁目錄,table至關於頁表,global dir至關於PDPT。

事實上,分頁機制完成了如下兩個設計目標:

  1. 給不一樣的進程分配不一樣的物理空間,避免內存錯誤。
  2. 解耦了頁框與頁。前者是物理上實際的存儲空間,在RAM中,而頁只是一堆數據。分頁機制容許一個頁先存儲在一個頁框中,而後被取出,最後被放在另外一個頁框內。這是Linux中虛擬內存的實現基礎。

下面討論一下Linux中如何處理頁表的具體實現(代碼流程),每一個部分分析一兩個函數足以大概明白各類機制。

5.1:線性地址字段

PAGE_SHIFT:指定offset字段的位數

8 #define PAGE_SHIFT      13

PMD_SHIFT指定offset和table字段的總位數,分別在二級頁表和三級頁表定義了兩次,和PAGE_SHIFT的方式一致。

還有PUD_SHIFT,PGDIR_SHIFT,PTRS_PER_PTE,PTRS_PER_PMD等字段描述不一樣目錄項和表項數目。

5.2:頁表處理

只討論32位系統狀況,64位狀況經過pgprot_t保護

pte_t,pmd_t,pud_t,pgd_t分別描述頁表項,頁中間目錄項,頁上級目錄和頁全局目錄項的格式:

 94 #ifdef CONFIG_64BIT_PHYS_ADDR
 95   #ifdef CONFIG_CPU_MIPS32
 96     typedef struct { unsigned long pte_low, pte_high; } pte_t;
 97     #define pte_val(x)    ((x).pte_low | ((unsigned long long)(x).pte_high << 32))
 98     #define __pte(x)      ({ pte_t __pte = {(x), ((unsigned long long)(x)) >> 32}; __pte; })
 99   #else
100      typedef struct { unsigned long long pte; } pte_t;
101      #define pte_val(x) ((x).pte)
102      #define __pte(x)   ((pte_t) { (x) } )
103   #endif
104 #else
105 typedef struct { unsigned long pte; } pte_t;
106 #define pte_val(x)      ((x).pte)
107 #define __pte(x)        ((pte_t) { (x) } )
108 #endif
109 typedef struct page *pgtable_t;

能夠看到,若是不指定64位硬件,就默認按照32位硬件系統處理,即便定義爲64位物理地址,若是CPU的MIPS的32位,也會另行定義。

內核也提供了宏能夠讀取/設置頁標誌,以pte_user()和pte_wrprotect()爲例:

237 static inline int pte_user(pte_t pte)   { return pte_val(pte) & __PAGE_PROT_USER; }

內聯函數的方法定義

120 static inline pte_t pte_wrprotect(pte_t pte)
121 {
122         pte_val(pte) &= ~(_PAGE_WRITE | _PAGE_SILENT_WRITE);
123         return pte;
124 }

其中_PAGE_SILENT_WRITE是爲了看是32位系統仍是64位系統。

5.3:頁表操做

該部分不少內容與虛擬地址相關,暫不分析。總體來講,就是得到各個頁表不一樣表項的地址,這裏只大概看一下其中一個函數,pgd_alloc(mm)

Pgd_alloc(mm)分配一個新的頁全局目錄,若是PAE被激活,還會分配三個對應的頁中間目錄。這個函數調用機制比較複雜,在此處不加細節說明的說一下流程和設計的數據結構。在以後的內存管理中會詳細分析一次頁表的全部重要數據結構和函數,先理順總體思路。

調用開始於

15 static inline pgd_t *pgd_alloc (struct mm_struct *mm)
 16 {
 17         return (pgd_t *)get_zeroed_page(GFP_KERNEL);
 18 }

傳入的mm在80x86體系中被忽略。而後經過get_zeroed_page()函數調用__get_free_pages()函數,這個是比較複雜的函數,返回一個32位地址,如前文所言,這個是不能用於表示高地址的頁表的。

2750 unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
2751 {
2752         struct page *page;
2753 
2754         /*
2755          * __get_free_pages() returns a 32-bit address, which cannot represent
2756          * a highmem page
2757          */
2758         VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);
2759 
2760         page = alloc_pages(gfp_mask, order);
2761         if (!page)
2762                 return 0;
2763         return (unsigned long) page_address(page);
2764 }

而後調用alloc_pages()函數實際調用到以下過程

2111 struct page *alloc_pages_current(gfp_t gfp, unsigned order)
2112 {
2113         struct mempolicy *pol = get_task_policy(current);
2114         struct page *page;
2115         unsigned int cpuset_mems_cookie;
2116 
2117         if (!pol || in_interrupt() || (gfp & __GFP_THISNODE))
2118                 pol = &default_policy;
2119 
2120 retry_cpuset:
2121         cpuset_mems_cookie = get_mems_allowed();
2122 
2123         /*
2124          * No reference counting needed for current->mempolicy
2125          * nor system default_policy
2126          */
2127         if (pol->mode == MPOL_INTERLEAVE)
2128                 page = alloc_page_interleave(gfp, order, interleave_nodes(pol));
2129         else
2130                 page = __alloc_pages_nodemask(gfp, order,
2131                                 policy_zonelist(gfp, pol, numa_node_id()),
2132                                 policy_nodemask(gfp, pol));
2133 
2134         if (unlikely(!put_mems_allowed(cpuset_mems_cookie) && !page))
2135                 goto retry_cpuset;
2136 
2137         return page;
2138 }

這個函數裏首先是get_task_policy()分配內存節點,若是沒有特殊的policy分配,會使用default的policy,而後分配內存,

96 static inline unsigned int get_mems_allowed(void)
 97 {
 98         return read_seqcount_begin(&current->mems_allowed_seq);
 99 }

而後根據參數判斷是以interleaved方式仍是alloc_pages_nodemask()的方式分配頁,這裏也是頁分配的核心。

而後把成功分配的頁返回便可,這就是流程。一開始,都是先分配一個zero_page

 

進程頁表:

每個進程都有它本身的頁全局目錄和頁表集,當發生進程切換時,cr3的內容被保存在前一個執行進程的task_struct中,(task_struct->mm->pgd)

將下一個進程的pgd地址裝入cr3寄存器。

內核提供了豐富的API能夠查看頁表的狀態和值

進程的線性地址空間分爲兩部分

0-3G    用戶態與內核態均可尋址

3G-4G 只有內核態才能尋址

進程的頁全局目錄的第一部分表項映射的線性地址小於3G

剩餘的表項對於全部進程都是相同的,等於內核頁表中的相應表項。

 

內核頁表:

內核維持着一組本身使用的頁表,駐留在主內核頁全局目錄中。在系統初始化時創建。

內核頁表的初始化創建分爲兩個階段:

第一階段下,CPU處於實模式,分頁功能還沒有開啓,內核建立一個有限的空間,把內核的代碼段,數據段,初始頁表和用於存放動態數據結構放進RAM,第二階段,經過臨時頁全局目錄的初始化和臨時頁表的初始化獲得臨時內核頁表,其存放在swapper_pg_dir中。第一階段,假設內核使用的段以及相應的數據結構能容納與RAM的前8MB裏,則必須講用戶模式的0x00000000到0x007fffff和內核的0xc0000000到0xc07fffff映射到RAM的前8MB的物理地址,即0x00000000到0x007fffff,而後把0xc0000000開始的線性地址轉化爲從0開始的物理地址,完成最終內核頁表。

這就是大體流程,具體代碼實現有待考究。

6,處理硬件高速緩存和TLB

硬件高速緩存的同步,由處理器自動完成。

TLB的同步,由內核完成,由於線性地址到物理地址的映射是否有效,由內核決定。

 

在一個處理器上運行的函數發送處理器間中斷(????),給其餘CPU,強制它們執行適當的函數刷新TLB。

 

 

 

進程切換通常都會引發TLB表項刷新,除了如下狀況:

兩個使用相同頁表集的普通進程之間執行進程切換(線程,mm_struct)

普通進程與內核線程間執行進程切換(內核線程直接使用前一個進程的mm_struct)

 

爲了不多處理器系統上無用的TLB刷新,內核使用一種叫作lazy TLB模式的技術。

相關文章
相關標籤/搜索