随着內建技術越來越精細,記憶體存儲量從位元組,千位元組,兆位元組,GB,…容量越來越大。如何有效地管理記憶體是一門藝術。在80X86體系中通過分段部件和分頁部件提供記憶體管理的支援,由此從換分出實位址模式,保護模式。實位址模式下一般是裸機程式,Linux啟動起始核心代碼載入記憶體運作并沒有記憶體管理機制,是以核心是以裸機程式運作,接着通過建立自舉配置設定器建立一個零時的記憶體配置設定器來支援系統進一步建構記憶體管理子系統,待記憶體管理子系統建構完成以後便抛棄這個自舉配置設定器,同時核心由實位址模式進入到保護模式。保護模式下的記憶體又分為分段管理,分頁管理。分頁管理是分段管理的進化版。在硬體上對分段管理有段寄存器來支援,分頁管理有MMU支援。
一.分段管理
1.16位CPU
Intel 8086是16位CPU,它隻有16位寄存器、16位資料總線和20位位址總線,它隻能運作在實模式,16位的CPU如何通路20位的位址範圍(2的20次方=1 048 576=1M)的記憶體空間。為了能夠通路到整個位址空間,在CPU裡添加了4個段寄存器,寄存器位數為16位,分别為CS(代碼段寄存器)DS(資料段寄存器) SS(堆棧段寄存器)ES(擴充段寄存器)。是以段寄存器就是為了解決CPU位數和位址總線不同的問題而誕生的。在實模式下通過 實體位址=段值*16+偏移,乘以16(2的4次方)意味着左移四位,加上段寄存器本身的16位,湊成20位,段值和偏移都是16位的 具有1MB(2^16 * 2^4 + offset)的尋址能力。
2.32位的CPU
Intel的CPU發展到80386時,CPU變成了32位,位址總線變成32位,尋址空間達到了2的32次方=4GB。然而寄存器大小為了相容之前體系下的版本,寄存器依舊是16比特位寬,這下尋址能力不足了。于是新增了GDTR(全局的段的描述附表寄存器),LDTR(局部的描述附表寄存器)兩個32位的寄存器
當x86 CPU 工作在保護模式時,可以使用全部32根位址線通路4GB的記憶體,因為80386的所有通用寄存器都是32位的,是以用任何一個通用寄存器來間接尋址,不用分段就可以通路4G空間中任意的記憶體位址。但是一個位址空間是否可以被寫入,可以被多少優先級的代碼寫入,是不是允許執行等等涉及保護的問題就出來了。要解決這些問題,必須對一個位址空間定義一些安全上的屬性。段寄存器這時就派上了用場。但是設計屬性和保護模式下段的參數,要表示的資訊太多了,要用64位長的資料才能表示。
我們把着64位的屬性資料叫做段描述符,它包含3個變量:段實體基位址、段界限、段屬性 80386的段寄存器是16位的,無法放下保護模式下64位的段描述符。把所有段的段描述符順序存放在記憶體中的指定位置,組成一個段描述符表(Descriptor Table);而段寄存器中的16位用來做索引資訊,這時,段寄存器中的資訊不再是段位址了,而是段選擇子(Selector)。可以通過它在段描述符表中“選擇”一個項得到段的全部資訊。
段寄存器存放着段選擇子,段選擇子是段的一個16位辨別符。它并不直接指向段,而是指向段選擇符表中定義段的段描述符。它有三個字段内容:請求特權級RPL(Request Privilege Level)、表訓示标志TI(Table Index)、索引值(Index),描述符索引數量為2的13次方=8192,12個表項被系統預留了。使用者隻能用8080個表項,TI代表着從GDTR還是LDTR索引,RPL代表核心态。
段描述符是GDT和LDT表中的一個資料結構項,用來向處理器提供一個有關段的位置和大小資訊以及通路控制的狀态資訊。包含三個主要字段:段基位址、段限長、和段屬性。段描述符通常由編譯器、連接配接器、加載器或者作業系統來建立
段限長為16+4=20位,段限長由G位控制,G=0代表機關1B,段長1M,G=1代表機關4KB,段長4KB*1M(2的20次方)=4G,剛好一個實體記憶體大小。段基位址長16+8+8=32位,與位址總線長度等。
保護模式下分段映射:GDT [DS>>3].BaseAddr + IP(邏輯位址32位) = 線性位址,GDT[DS >>3]這個是段選擇符高13位(15-3)儲存着是以向左移動3個位對齊。
段描述符表: 是段描述符的一個數組。
LDT可以看做是GDT中的一個段,不過這個段裡的内容是一張段描述符表,通過TI來辨別到哪張表選擇描述符。于是尋址過程變成了:
1、段寄存器中存放段選擇子Selector
2、GDTR/LDTR中存放着GDT/LDT段描述符表的基位址
3、由段寄存器中的段選擇符的TI位決定到GDT/LDT表尋找描述符
4、通過選擇子根據GDT/LDT中的基位址,就能找到對應的段描述符 具體來說是:選擇子*8 (段描述符64位=8位元組)
5、段描述符中有段的實體基位址,就得到段在記憶體中的基位址
6、加上偏移量(IP),就找到在這個段中存放的資料的線性位址(隻有分段機制情況下,就是真實實體位址)。
在系統運作多個任務的時候,多個任務共享的段存放由GDT表中的段描述符辨別,不共享的段由LDT表中的段描述符辨別。在切換任務時隻需重新裝在LDTR。
3.GDT初始化
linux-5.4.80/arch/x86/boot/header.S
start_of_setup:
......
# Jump to C code (should not return)
calll main
linux-5.4.80/arch/x86/boot/main.c
void main(void)
{
......
//進入保護模式
go_to_protected_mode();
}
linux-5.4.80/arch/x86/boot/pm.c
void go_to_protected_mode(void)
{
//離開實位址模式,禁用中斷
realmode_switch_hook();
//使能A20
if (enable_a20()) {
puts("A20 gate not responding, unable to boot...\n");
die();
}
//重置協處理器
reset_coprocessor();
/* Mask all interrupts in the PIC */
mask_all_interrupts();
//設定IDT,GDT
setup_idt();
setup_gdt();
//跳轉到code32_start指定的入口即startup_32或startup_64
protected_mode_jump(boot_params.hdr.code32_start,
(u32)&boot_params + (ds() << 4));
}
linux-5.4.80/arch/x86/boot/pm.c
設定GDT
static void setup_gdt(void)
{
//初始化GDT,設定CS,DS,TSS描述符表項
static const u64 boot_gdt[] __attribute__((aligned(16))) = {
/* CS: code, read/execute, 4 GB, base 0 */
[GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),
/* DS: data, read/write, 4 GB, base 0 */
[GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),
/* TSS: 32-bit tss, 104 bytes, base 4096 */
/* We only have a TSS here to keep Intel VT happy;
we don't actually use it for anything. */
[GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),
};
static struct gdt_ptr gdt;
gdt.len = sizeof(boot_gdt)-1;
gdt.ptr = (u32)&boot_gdt + (ds() << 4);
asm volatile("lgdtl %0" : : "m" (gdt));
}
//gdt指針指向gdt表
struct gdt_ptr {
u16 len;
u32 ptr;
} __attribute__((packed));
linux-5.4.80/arch/x86/include/asm/segment.h
構造GDT/LDT段描述符表項
#define GDT_ENTRY(flags, base, limit) \
((((base) & _AC(0xff000000,ULL)) << (56-24)) | \
(((flags) & _AC(0x0000f0ff,ULL)) << 40) | \
(((limit) & _AC(0x000f0000,ULL)) << (48-16)) | \
(((base) & _AC(0x00ffffff,ULL)) << 16) | \
(((limit) & _AC(0x0000ffff,ULL))))
linux-5.4.80/arch/x86/kernel/head_32.S
protected_mode_jump跳轉到此處
__HEAD
ENTRY(startup_32)
movl pa(initial_stack),%ecx
/* test KEEP_SEGMENTS flag to see if the bootloader is asking
us to not reload segments */
testb $KEEP_SEGMENTS, BP_loadflags(%esi)
jnz 2f
//讀取GDTR寄存器
lgdt pa(boot_gdt_descr)
movl $(__BOOT_DS),%eax
movl %eax,%ds
movl %eax,%es
movl %eax,%fs
movl %eax,%gs
movl %eax,%ss
......
//調用i386_start_kernel
call *(initial_code)
......
ENTRY(initial_code)
.long i386_start_kernel
.data
.globl boot_gdt_descr
ALIGN
# early boot GDT descriptor (must use 1:1 address mapping)
.word 0 # 32 bit align gdt_desc.address
boot_gdt_descr:
.word __BOOT_DS+7
.long boot_gdt - __PAGE_OFFSET
# boot GDT descriptor (later on used by CPU#0):
.word 0 # 32 bit align gdt_desc.address
ENTRY(early_gdt_descr)
.word GDT_ENTRIES*8-1
.long gdt_page /* Overwritten for secondary CPUs */
.align L1_CACHE_BYTES
//段限長4GB
ENTRY(boot_gdt)
.fill GDT_ENTRY_BOOT_CS,8,0
.quad 0x00cf9a000000ffff /* kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* kernel 4GB data at 0x00000000 */
linux-5.4.80/arch/x86/kernel/head32.c
asmlinkage __visible void __init i386_start_kernel(void)
{
/* Make sure IDT is set up before any exception happens */
idt_setup_early_handler();
......
start_kernel(); //開始核心其他項的啟動
}
linux-5.4.80/arch/x86/include/asm/desc_defs.h
Linux通過desc_struct來表示GDT段描述符表項
struct desc_struct {
u16 limit0;
u16 base0;
u16 base1: 8, type: 4, s: 1, dpl: 2, p: 1;
u16 limit1: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
} __attribute__((packed));
linux-5.4.80/arch/x86/include/asm/desc.h
每個處理器都對應一個gdt
struct gdt_page {
struct desc_struct gdt[GDT_ENTRIES];
} __attribute__((aligned(PAGE_SIZE)));
DECLARE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page);
linux-5.4.80/arch/x86/kernel/cpu/common.c
DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = {
#ifdef CONFIG_X86_64
[GDT_ENTRY_KERNEL32_CS] = GDT_ENTRY_INIT(0xc09b, 0, 0xfffff),
[GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xa09b, 0, 0xfffff),
[GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc093, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER32_CS] = GDT_ENTRY_INIT(0xc0fb, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff),
#else
......
[GDT_ENTRY_ESPFIX_SS] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff),
[GDT_ENTRY_PERCPU] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff),
GDT_STACK_CANARY_INIT
#endif
} };
EXPORT_PER_CPU_SYMBOL_GPL(gdt_page);
linux-5.4.80/arch/x86/include/asm/desc_defs.h
使用GDT_ENTRY_INIT宏來填充表項
#define GDT_ENTRY_INIT(flags, base, limit) \
{ \
.limit0 = (u16) (limit), \
.limit1 = ((limit) >> 16) & 0x0F, \
.base0 = (u16) (base), \
.base1 = ((base) >> 16) & 0xFF, \
.base2 = ((base) >> 24) & 0xFF, \
.type = (flags & 0x0f), \
.s = (flags >> 4) & 0x01, \
.dpl = (flags >> 5) & 0x03, \
.p = (flags >> 7) & 0x01, \
.avl = (flags >> 12) & 0x01, \
.l = (flags >> 13) & 0x01, \
.d = (flags >> 14) & 0x01, \
.g = (flags >> 15) & 0x01, \
}
4.任務切換
linux-5.4.80/include/linux/mm_types.h
通過mm_context_t來關聯任務
struct mm_struct {
......
mm_context_t context;
......
}
linux-5.4.80/arch/x86/include/asm/mmu.h
typedef struct {
......
struct ldt_struct *ldt;
......
} mm_context_t;
linux-5.4.80/arch/x86/include/asm/mmu_context.h
struct ldt_struct {
struct desc_struct *entries; //段表項結構
unsigned int nr_entries;
int slot;
};
linux-5.4.80/arch/x86/include/asm/mmu_context.h
切換任務時進行ldt切換
static inline void switch_ldt(struct mm_struct *prev, struct mm_struct *next)
{
#ifdef CONFIG_MODIFY_LDT_SYSCALL
if (unlikely((unsigned long)prev->context.ldt |
(unsigned long)next->context.ldt))
load_mm_ldt(next);
#endif
}
linux-5.4.80/arch/x86/include/asm/mmu_context.h
static inline void load_mm_ldt(struct mm_struct *mm)
{
#ifdef CONFIG_MODIFY_LDT_SYSCALL
struct ldt_struct *ldt;
ldt = READ_ONCE(mm->context.ldt);
if (unlikely(ldt)) {
set_ldt(ldt->entries, ldt->nr_entries);
} else {
clear_LDT();
}
#else
clear_LDT();
#endif
}
二.分頁管理
記憶體分段管理,可以以段為機關有效利用記憶體,缺點是無法利用碎片,必須搬移記憶體,造成性能損失。Linux通過分頁管理來有效利用記憶體碎片,通過CR0寄存器的PE位來開啟分頁管理。如果不開啟分頁管理,那麼上述分段産生的線性位址就是實體記憶體位址,如果開啟了分頁管理,那麼這個32位線性位址經過頁表轉換以後得到實體位址。
1.頁表和分頁
以32位CPU來看,32位CPU可尋址實體記憶體為4G。那麼如何通過頁目錄和頁表組織4G的尋址空間轉換,首先通過分段産生的線性位址是32位的,這32位線性位址劃分為3段,[31:22]這10位為頁目錄(PGD)項索引,[21:12]這10位為頁表(PTE)項索引,[11:0]這12位為頁内偏移(OFFSET)。由此PGD目錄項總共1024項=2的10次方,PTE表項總共1024項=2的10次方,記憶體頁長度為4K=2的12次方。是以1024 x 1024 x 4K = 4G。32位的線性位址通過分頁打散為4K的記憶體頁。
由上述線性位址由頁目錄頁索引,表索引,頁偏移組成。每個程序都有各自的頁表,CR3寄存器存放目前程序的頁目錄位址。那麼Linux中目錄和頁表,特别是目錄項和表項如何組織的呢。
頁目錄和頁表項是32位的,由于記憶體以4K為一頁的,是以頁表項[12:0]被用作表項屬性标記用,如P位表示該表項的有效性。R/W用于分頁級讀寫保護。
最後,32位CPU,邏輯位址,線性位址,實體位址的轉換如上圖示。
2.32位分頁
linux-5.4.80/arch/x86/include/asm/pgtable-2level_types.h
32位CPU兩級分頁:[31:22] - [21:12] - [11:0]
#ifndef __ASSEMBLY__
#include <linux/types.h>
typedef unsigned long pteval_t;
typedef unsigned long pmdval_t;
typedef unsigned long pudval_t;
typedef unsigned long p4dval_t;
typedef unsigned long pgdval_t;
typedef unsigned long pgprotval_t;
typedef union {
pteval_t pte;
pteval_t pte_low;
} pte_t;
#endif /* !__ASSEMBLY__ */
#define SHARED_KERNEL_PMD 0 //兩級分頁時PMD目錄項為0
#define PGDIR_SHIFT 22 //頁目錄偏移
#define PTRS_PER_PGD 1024 //頁目錄項1024項
#define PTRS_PER_PTE 1024 //頁表項1024項
#define PGD_KERNEL_START (CONFIG_PAGE_OFFSET >> PGDIR_SHIFT) //頁目錄位置[32:22]移動 32 >> 22
#endif /* _ASM_X86_PGTABLE_2LEVEL_DEFS_H */
linux-5.4.80/arch/x86/include/asm/pgtable-3level_types.h
32位CPU三級分頁: [31:30] - [39:21] - [20:12] - [11:0]
#ifndef __ASSEMBLY__
#include <linux/types.h>
typedef u64 pteval_t;
typedef u64 pmdval_t;
typedef u64 pudval_t;
typedef u64 p4dval_t;
typedef u64 pgdval_t;
typedef u64 pgprotval_t;
typedef union {
struct {
unsigned long pte_low, pte_high;
};
pteval_t pte;
} pte_t;
#endif /* !__ASSEMBLY__ */
#ifdef CONFIG_PARAVIRT_XXL
#define SHARED_KERNEL_PMD ((!static_cpu_has(X86_FEATURE_PTI) && \
(pv_info.shared_kernel_pmd)))
#else
#define SHARED_KERNEL_PMD (!static_cpu_has(X86_FEATURE_PTI))
#endif
#define PGDIR_SHIFT 30 //頁目錄偏移
#define PTRS_PER_PGD 4 //頁目錄4項
#define PMD_SHIFT 21 //中間級頁目錄偏移
#define PTRS_PER_PMD 512 //中間級頁目錄512項
#define PTRS_PER_PTE 512 //頁表512項
#define MAX_POSSIBLE_PHYSMEM_BITS 36
#define PGD_KERNEL_START (CONFIG_PAGE_OFFSET >> PGDIR_SHIFT)
#endif /* _ASM_X86_PGTABLE_3LEVEL_DEFS_H */
3.64位分頁
linux-5.4.80/arch/x86/include/asm/pgtable_64_types.h
四級頁表: [48:39] - [38:30] - [29:21] - [20:12] - [11:0]
尋址範圍: 0x000000000000-0xFFFFFFFFFFFF,256TB
五級頁表: [55-48] - [47:39] - [38:30] - [29:21] - [20:12] - [11:0]
尋址範圍0x00000000000000-0xFFFFFFFFFFFFFF,128PB
#ifndef __ASSEMBLY__
#include <linux/types.h>
#include <asm/kaslr.h>
typedef unsigned long pteval_t;
typedef unsigned long pmdval_t;
typedef unsigned long pudval_t;
typedef unsigned long p4dval_t;
typedef unsigned long pgdval_t;
typedef unsigned long pgprotval_t;
typedef struct { pteval_t pte; } pte_t;
extern unsigned int pgdir_shift;
extern unsigned int ptrs_per_p4d;
#endif /* !__ASSEMBLY__ */
#define SHARED_KERNEL_PMD 0
//五級頁表
#ifdef CONFIG_X86_5LEVEL
#define PGDIR_SHIFT pgdir_shift //偏移
#define PTRS_PER_PGD 512 //PGD表512項
//五級頁表下的四級頁表
#define P4D_SHIFT 39 //P4D偏移
#define MAX_PTRS_PER_P4D 512 //P4D表512項
#define PTRS_PER_P4D ptrs_per_p4d
#define P4D_SIZE (_AC(1, UL) << P4D_SHIFT)
#define P4D_MASK (~(P4D_SIZE - 1))
#define MAX_POSSIBLE_PHYSMEM_BITS 52
#else /* CONFIG_X86_5LEVEL */
//四級頁表
#define PGDIR_SHIFT 39
#define PTRS_PER_PGD 512
#define MAX_PTRS_PER_P4D 1
#endif /* CONFIG_X86_5LEVEL */
//三級頁表
#define PUD_SHIFT 30
#define PTRS_PER_PUD 512
//二級頁表
#define PMD_SHIFT 21
#define PTRS_PER_PMD 512
//一級頁表
#define PTRS_PER_PTE 512
#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK (~(PMD_SIZE - 1))
#define PUD_SIZE (_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK (~(PUD_SIZE - 1))
#define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK (~(PGDIR_SIZE - 1))
......
#endif /* _ASM_X86_PGTABLE_64_DEFS_H */
Linux核心通過分段分頁機制來劃分記憶體,将程式的邏輯位址轉換為線性位址,線性位址為多任務模型提供了統一的記憶體模型,再将線性位址轉換為實體位址,使得較小的實體記憶體也可以擁有較大的線性位址空間。這樣上層應用就可以不用考慮多樣化的複雜的記憶體管理問題,同樣這個模型也使得Linux具有極好的相容性,可以适應多樣化的硬體。