一文掌握 Linux 内存管理
作者:dengxuanshi,騰訊 IEG 后臺開發工程師
以下源代碼來自 linux-5.10.3 內核代碼,主要以 x86-32 為例。
Linux 內存管理是一個很復雜的“工程”,它不僅僅是對物理內存的管理,也涉及到虛擬內存管理、內存交換和內存回收等
物理內存的探測
Linux 內核通過 detect_memory()函數實現對物理內存的探測
void?detect_memory(void) {detect_memory_e820();detect_memory_e801();detect_memory_88(); }這里主要介紹一下 detect_memory_e820(),detect_memory_e801()和 detect_memory_88()是針對較老的電腦進行兼容而保留的
static?void?detect_memory_e820(void) {int?count?=?0;struct?biosregs?ireg,?oreg;struct?boot_e820_entry?*desc?=?boot_params.e820_table;static?struct?boot_e820_entry?buf;?/*?static?so?it?is?zeroed?*/initregs(&ireg);ireg.ax??=?0xe820;ireg.cx??=?sizeof(buf);ireg.edx?=?SMAP;ireg.di??=?(size_t)&buf;do?{intcall(0x15,?&ireg,?&oreg);ireg.ebx?=?oreg.ebx;?/*?for?next?iteration...?*/if?(oreg.eflags?&?X86_EFLAGS_CF)break;if?(oreg.eax?!=?SMAP)?{count?=?0;break;}*desc++?=?buf;count++;}?while?(ireg.ebx?&&?count?<?ARRAY_SIZE(boot_params.e820_table));boot_params.e820_entries?=?count; }detect_memory_e820()實現內核從 BIOS 那里獲取到內存的基礎布局,之所以叫 e820 是因為內核是通過 0x15 中斷向量,并在 AX 寄存器中指定 0xE820,中斷調用后將會返回被 BIOS 保留的內存地址范圍以及系統可以使用的內存地址范圍,所有通過中斷獲取的數據將會填充在 boot_params.e820_table 中,具體 0xE820 的詳細用法感興趣的話可以上網查……這里獲取到的 e820_table 里的數據是未經過整理,linux 會通過 setup_memory_map 去整理這些數據
start_kernel()?->?setup_arch()?->?setup_memory_map() void?__init?e820__memory_setup(void) {char?*who;BUILD_BUG_ON(sizeof(struct?boot_e820_entry)?!=?20);who?=?x86_init.resources.memory_setup();memcpy(e820_table_kexec,?e820_table,?sizeof(*e820_table_kexec));memcpy(e820_table_firmware,?e820_table,?sizeof(*e820_table_firmware));pr_info("BIOS-provided?physical?RAM?map:\n");e820__print_table(who); }x86_init.resources.memory_setup()指向了 e820__memory_setup_default(),會將 boot_params.e820_table 轉換為內核自己使用的 e820_table,轉換之后的e820 表記錄著所有物理內存的起始地址、長度以及類型,然后通過 memcpy 將 e820_table 復制到 e820_table_kexec、e820_table_firmware
struct?x86_init_ops?x86_init?__initdata?=?{.resources?=?{.probe_roms??=?probe_roms,.reserve_resources?=?reserve_standard_io_resources,.memory_setup??=?e820__memory_setup_default,},...... }char?*__init?e820__memory_setup_default(void) {char?*who?=?"BIOS-e820";/**?Try?to?copy?the?BIOS-supplied?E820-map.**?Otherwise?fake?a?memory?map;?one?p?from?0k->640k,*?the?next?p?from?1mb->appropriate_mem_k*/if?(append_e820_table(boot_params.e820_table,?boot_params.e820_entries)?<?0)?{u64?mem_size;/*?Compare?results?from?other?methods?and?take?the?one?that?gives?more?RAM:?*/if?(boot_params.alt_mem_k?<?boot_params.screen_info.ext_mem_k)?{mem_size?=?boot_params.screen_info.ext_mem_k;who?=?"BIOS-88";}?else?{mem_size?=?boot_params.alt_mem_k;who?=?"BIOS-e801";}e820_table->nr_entries?=?0;e820__range_add(0,?LOWMEMSIZE(),?E820_TYPE_RAM);e820__range_add(HIGH_MEMORY,?mem_size?<<?10,?E820_TYPE_RAM);}/*?We?just?appended?a?lot?of?ranges,?sanitize?the?table:?*/e820__update_table(e820_table);return?who; }內核使用的e820_table 結構描述如下:
enum?e820_type?{E820_TYPE_RAM??=?1,E820_TYPE_RESERVED?=?2,E820_TYPE_ACPI??=?3,E820_TYPE_NVS??=?4,E820_TYPE_UNUSABLE?=?5,E820_TYPE_PMEM??=?7,E820_TYPE_PRAM??=?12,E820_TYPE_SOFT_RESERVED?=?0xefffffff,E820_TYPE_RESERVED_KERN?=?128, };struct?e820_entry?{u64???addr;u64???size;enum?e820_type??type; }?__attribute__((packed));struct?e820_table?{__u32?nr_entries;struct?e820_entry?entries[E820_MAX_ENTRIES]; };memblock 內存分配器
linux x86 內存映射主要存在兩種方式:段式映射和頁式映射。linux 首次進入保護模式時會用到段式映射(加電時,運行在實模式,任意內存地址都能執行代碼,可以被讀寫,這非常不安全,CPU 為了提供限制/禁止的手段,提出了保護模式),根據段寄存器(以 8086 為例,段寄存器有 CS(Code Segment):代碼段寄存器;DS(Data Segment):數據段寄存器;SS(Stack Segment):堆棧段寄存器;ES(Extra Segment):附加段寄存器)查找到對應的段描述符(這里其實就是用到了段描述符表,即段表),段描述符指明了此時的環境可以通過段訪問到內存基地址、空間大小和訪問權限。訪問權限則點明了哪些內存可讀、哪些內存可寫。
typedef?struct?Descriptor{unsigned?int?base;??//?段基址unsigned?int?limit;?//?段大小unsigned?short?attribute;???//?段屬性、權限 }linux 在段描述符表準備完成之后會通過匯編跳轉到保護模式
事實上,在上面這個過程中,linux 并沒有明顯地去區分每個段,所以這里并沒有很好地起到保護作用,linux 最終使用的還是內存分頁管理(開啟頁式映射可以參考/arch/x86/kernel/head_32.S)
memblock 算法
memblock 是 linux 內核初始化階段使用的一個內存分配器,實現較為簡單,負責頁分配器初始化之前的內存管理和分配請求,memblock 的結構如下
struct?memblock_region?{phys_addr_t?base;phys_addr_t?size;enum?memblock_flags?flags; #ifdef?CONFIG_NEED_MULTIPLE_NODESint?nid; #endif };struct?memblock_type?{unsigned?long?cnt;unsigned?long?max;phys_addr_t?total_size;struct?memblock_region?*regions;char?*name; };struct?memblock?{bool?bottom_up;??/*?is?bottom?up?direction??*/phys_addr_t?current_limit;struct?memblock_type?memory;struct?memblock_type?reserved; };bottom_up:用來表示分配器分配內存是自低地址向高地址還是自高地址向低地址
current_limit:用來表示用來限制 memblock_alloc()和 memblock_alloc_base()的內存申請
memory:用于指向系統可用物理內存區,這個內存區維護著系統所有可用的物理內存,即系統 DRAM 對應的物理內存
reserved:用于指向系統預留區,也就是這個內存區的內存已經分配,在釋放之前不能再次分配這個區內的內存區塊
memblock_type中的cnt用于描述該類型內存區中的內存區塊數,這有利于 MEMBLOCK 內存分配器動態地知道某種類型的內存區還有多少個內存區塊
memblock_type 中的max用于描述該類型內存區最大可以含有多少個內存區塊,當往某種類型的內存區添加 內存區塊的時候,如果內存區的內存區塊數超過 max 成員,那么 memblock 內存分配器就會增加內存區的容量,以此維護更多的內存區塊
memblock_type 中的total_size用于統計內存區總共含有的物理內存數
memblock_type 中的regions是一個內存區塊鏈表,用于維護屬于這類型的所有內存區塊(包括基址、大小和內存塊標記等),
name :用于指明這個內存區的名字,MEMBLOCK 分配器目前支持的內存區名字有:“memory”, “reserved”, “physmem”
具體關系可以參考下圖:
內核啟動后會為 MEMBLOCK 內存分配器創建了一些私有的 p,這些 p 用于存放于 MEMBLOCK 分配器有關的函數和數據,即 init_memblock 和 initdata_memblock。在創建完 init_memblock p 和 initdata_memblock p 之后,memblock 分配器會開始創建 struct memblock 實例,這個實例此時作為最原始的 MEMBLOCK 分配器,描述了系統的物理內存的初始值
#define?MEMBLOCK_ALLOC_ANYWHERE?(~(phys_addr_t)0)?//即0xFFFFFFFF #define?INIT_MEMBLOCK_REGIONS???128#ifndef?INIT_MEMBLOCK_RESERVED_REGIONS #?define?INIT_MEMBLOCK_RESERVED_REGIONS??INIT_MEMBLOCK_REGIONS #endifstatic?struct?memblock_region?memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS]?__initdata_memblock; static?struct?memblock_region?memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS]?__initdata_memblock;struct?memblock?memblock?__initdata_memblock?=?{.memory.regions??=?memblock_memory_init_regions,.memory.cnt??=?1,?/*?empty?dummy?entry?*/.memory.max??=?INIT_MEMBLOCK_REGIONS,.memory.name??=?"memory",.reserved.regions?=?memblock_reserved_init_regions,.reserved.cnt??=?1,?/*?empty?dummy?entry?*/.reserved.max??=?INIT_MEMBLOCK_RESERVED_REGIONS,.reserved.name??=?"reserved",.bottom_up??=?false,.current_limit??=?MEMBLOCK_ALLOC_ANYWHERE, };內核在 setup_arch(char **cmdline_p)中會調用e820__memblock_setup()對 MEMBLOCK 內存分配器進行初始化啟動
void?__init?e820__memblock_setup(void) {int?i;u64?end;memblock_allow_resize();for?(i?=?0;?i?<?e820_table->nr_entries;?i++)?{struct?e820_entry?*entry?=?&e820_table->entries[i];end?=?entry->addr?+?entry->size;if?(end?!=?(resource_size_t)end)continue;if?(entry->type?==?E820_TYPE_SOFT_RESERVED)memblock_reserve(entry->addr,?entry->size);if?(entry->type?!=?E820_TYPE_RAM?&&?entry->type?!=?E820_TYPE_RESERVED_KERN)continue;memblock_add(entry->addr,?entry->size);}/*?Throw?away?partial?pages:?*/memblock_trim_memory(PAGE_SIZE);memblock_dump_all(); }memblock_allow_resize() 僅是用于置 memblock_can_resize 的值,里面的 for 則是用于循環遍歷 e820 的內存布局信息,然后進行 memblock_add 的操作
int?__init_memblock?memblock_add(phys_addr_t?base,?phys_addr_t?size) {phys_addr_t?end?=?base?+?size?-?1;memblock_dbg("%s:?[%pa-%pa]?%pS\n",?__func__,&base,?&end,?(void?*)_RET_IP_);return?memblock_add_range(&memblock.memory,?base,?size,?MAX_NUMNODES,?0); }memblock_add()主要封裝了 memblock_add_region(),且它操作對象是 memblock.memory,即系統可用物理內存區。
static?int?__init_memblock?memblock_add_range(struct?memblock_type?*type,phys_addr_t?base,?phys_addr_t?size,int?nid,?enum?memblock_flags?flags) {bool?insert?=?false;phys_addr_t?obase?=?base;//調整size大小,確保不會越過邊界phys_addr_t?end?=?base?+?memblock_cap_size(base,?&size);int?idx,?nr_new;struct?memblock_region?*rgn;if?(!size)return?0;/*?special?case?for?empty?array?*/if?(type->regions[0].size?==?0)?{WARN_ON(type->cnt?!=?1?||?type->total_size);type->regions[0].base?=?base;type->regions[0].size?=?size;type->regions[0].flags?=?flags;memblock_set_region_node(&type->regions[0],?nid);type->total_size?=?size;return?0;} repeat:/**?The?following?is?executed?twice.??Once?with?%false?@insert?and*?then?with?%true.??The?first?counts?the?number?of?regions?needed*?to?accommodate?the?new?area.??The?second?actually?inserts?them.*/base?=?obase;nr_new?=?0;for_each_memblock_type(idx,?type,?rgn)?{phys_addr_t?rbase?=?rgn->base;phys_addr_t?rend?=?rbase?+?rgn->size;if?(rbase?>=?end)break;if?(rend?<=?base)continue;/**?@rgn?overlaps.??If?it?separates?the?lower?part?of?new*?area,?insert?that?portion.*/if?(rbase?>?base)?{ #ifdef?CONFIG_NEED_MULTIPLE_NODESWARN_ON(nid?!=?memblock_get_region_node(rgn)); #endifWARN_ON(flags?!=?rgn->flags);nr_new++;if?(insert)memblock_insert_region(type,?idx++,?base,rbase?-?base,?nid,flags);}/*?area?below?@rend?is?dealt?with,?forget?about?it?*/base?=?min(rend,?end);}/*?insert?the?remaining?portion?*/if?(base?<?end)?{nr_new++;if?(insert)memblock_insert_region(type,?idx,?base,?end?-?base,nid,?flags);}if?(!nr_new)return?0;/**?If?this?was?the?first?round,?resize?array?and?repeat?for?actual*?insertions;?otherwise,?merge?and?return.*/if?(!insert)?{while?(type->cnt?+?nr_new?>?type->max)if?(memblock_double_array(type,?obase,?size)?<?0)return?-ENOMEM;insert?=?true;goto?repeat;}?else?{memblock_merge_regions(type);return?0;} }如果 memblock 算法管理的內存為空時,將當前空間添加進去
不為空的情況下,for_each_memblock_type(idx, type, rgn)這個循環會檢查是否存在內存重疊的情況,如果有的話,則剔除重疊部分,然后將其余非重疊的部分插入 memblock。內存塊的添加主要分為四種情況(其余情況與這幾種類似),可以參考下圖:
如果 region 空間不夠,則通過 memblock_double_array()添加新的空間,然后重試。
最后 memblock_merge_regions()會將緊挨著的內存進行合并(節點號、flag 等必須一致,節點號后面再進行介紹)。
memblock 內存分配與回收
到這里,memblock 內存管理的初始化基本完成,后面還有一些關于 memblock.memory 的修正,這里就不做介紹了,最后也簡單介紹一下memblock 的內存分配和回收,即 memblock_alloc()和 memblock_free()。
//size即分配區塊的大小,align用于字節對齊,表示分配區塊的對齊大小 //這里NUMA_NO_NODE指任何節點(沒有節點),關于節點后面會介紹,這里節點還沒初始化 //MEMBLOCK_ALLOC_ACCESSIBLE指分配內存塊時僅受memblock.current_limit的限制 #define?NUMA_NO_NODE?(-1) #define?MEMBLOCK_LOW_LIMIT?0 #define?MEMBLOCK_ALLOC_ACCESSIBLE?0static?inline?void?*?__init?memblock_alloc(phys_addr_t?size,??phys_addr_t?align) {return?memblock_alloc_try_nid(size,?align,?MEMBLOCK_LOW_LIMIT,MEMBLOCK_ALLOC_ACCESSIBLE,?NUMA_NO_NODE); }void?*?__init?memblock_alloc_try_nid(phys_addr_t?size,?phys_addr_t?align,phys_addr_t?min_addr,?phys_addr_t?max_addr,int?nid) {void?*ptr;memblock_dbg("%s:?%llu?bytes?align=0x%llx?nid=%d?from=%pa?max_addr=%pa?%pS\n",__func__,?(u64)size,?(u64)align,?nid,?&min_addr,&max_addr,?(void?*)_RET_IP_);ptr?=?memblock_alloc_internal(size,?align,min_addr,?max_addr,?nid,?false);if?(ptr)memset(ptr,?0,?size);return?ptr; }static?void?*?__init?memblock_alloc_internal(phys_addr_t?size,?phys_addr_t?align,phys_addr_t?min_addr,?phys_addr_t?max_addr,int?nid,?bool?exact_nid) {phys_addr_t?alloc;/**?Detect?any?accidental?use?of?these?APIs?after?slab?is?ready,?as?at*?this?moment?memblock?may?be?deinitialized?already?and?its*?internal?data?may?be?destroyed?(after?execution?of?memblock_free_all)*/if?(WARN_ON_ONCE(slab_is_available()))return?kzalloc_node(size,?GFP_NOWAIT,?nid);if?(max_addr?>?memblock.current_limit)max_addr?=?memblock.current_limit;alloc?=?memblock_alloc_range_nid(size,?align,?min_addr,?max_addr,?nid,exact_nid);/*?retry?allocation?without?lower?limit?*/if?(!alloc?&&?min_addr)alloc?=?memblock_alloc_range_nid(size,?align,?0,?max_addr,?nid,exact_nid);if?(!alloc)return?NULL;return?phys_to_virt(alloc); }memblock_alloc_internal 返回的是分配到的內存塊的虛擬地址,為 NULL 表示分配失敗,關于 phys_to_virt 的實現后面再介紹,這里主要看 memblock_alloc_range_nid 的實現。
phys_addr_t?__init?memblock_alloc_range_nid(phys_addr_t?size,phys_addr_t?align,?phys_addr_t?start,phys_addr_t?end,?int?nid,bool?exact_nid) {enum?memblock_flags?flags?=?choose_memblock_flags();phys_addr_t?found;if?(WARN_ONCE(nid?==?MAX_NUMNODES,?"Usage?of?MAX_NUMNODES?is?deprecated.?Use?NUMA_NO_NODE?instead\n"))nid?=?NUMA_NO_NODE;if?(!align)?{/*?Can't?use?WARNs?this?early?in?boot?on?powerpc?*/dump_stack();align?=?SMP_CACHE_BYTES;}again:found?=?memblock_find_in_range_node(size,?align,?start,?end,?nid,flags);if?(found?&&?!memblock_reserve(found,?size))goto?done;if?(nid?!=?NUMA_NO_NODE?&&?!exact_nid)?{found?=?memblock_find_in_range_node(size,?align,?start,end,?NUMA_NO_NODE,flags);if?(found?&&?!memblock_reserve(found,?size))goto?done;}if?(flags?&?MEMBLOCK_MIRROR)?{flags?&=?~MEMBLOCK_MIRROR;pr_warn("Could?not?allocate?%pap?bytes?of?mirrored?memory\n",&size);goto?again;}return?0;done:if?(end?!=?MEMBLOCK_ALLOC_KASAN)kmemleak_alloc_phys(found,?size,?0,?0);return?found; }kmemleak 是一個檢查內存泄漏的工具,這里就不做介紹了。
memblock_alloc_range_nid()首先對 align 參數進行檢測,如果為零,則警告。接著函數調用 memblock_find_in_range_node() 函數從可用內存區中找一塊大小為 size 的物理內存區塊, 然后調用 memblock_reseve() 函數在找到的情況下,將這塊物理內存區塊加入到預留區內。
static?phys_addr_t?__init_memblock?memblock_find_in_range_node(phys_addr_t?size,phys_addr_t?align,?phys_addr_t?start,phys_addr_t?end,?int?nid,enum?memblock_flags?flags) {phys_addr_t?kernel_end,?ret;/*?pump?up?@end?*/if?(end?==?MEMBLOCK_ALLOC_ACCESSIBLE?||end?==?MEMBLOCK_ALLOC_KASAN)end?=?memblock.current_limit;/*?avoid?allocating?the?first?page?*/start?=?max_t(phys_addr_t,?start,?PAGE_SIZE);end?=?max(start,?end);kernel_end?=?__pa_symbol(_end);if?(memblock_bottom_up()?&&?end?>?kernel_end)?{phys_addr_t?bottom_up_start;/*?make?sure?we?will?allocate?above?the?kernel?*/bottom_up_start?=?max(start,?kernel_end);/*?ok,?try?bottom-up?allocation?first?*/ret?=?__memblock_find_range_bottom_up(bottom_up_start,?end,size,?align,?nid,?flags);if?(ret)return?ret;WARN_ONCE(IS_ENABLED(CONFIG_MEMORY_HOTREMOVE),"memblock:?bottom-up?allocation?failed,?memory?hotremove?may?be?affected\n");}return?__memblock_find_range_top_down(start,?end,?size,?align,?nid,flags); }memblock 分配器分配內存是支持自低地址向高地址和自高地址向低地址的,如果 memblock_bottom_up() 函數返回 true,表示 MEMBLOCK 從低向上分配,而前面初始化的時候這個返回值其實是 false(某些情況下不一定),所以這里主要看__memblock_find_range_top_down 的實現(__memblock_find_range_bottom_up 的實現是類似的)。
static?phys_addr_t?__init_memblock __memblock_find_range_top_down(phys_addr_t?start,?phys_addr_t?end,phys_addr_t?size,?phys_addr_t?align,?int?nid,enum?memblock_flags?flags) {phys_addr_t?this_start,?this_end,?cand;u64?i;for_each_free_mem_range_reverse(i,?nid,?flags,?&this_start,?&this_end,NULL)?{this_start?=?clamp(this_start,?start,?end);this_end?=?clamp(this_end,?start,?end);if?(this_end?<?size)continue;cand?=?round_down(this_end?-?size,?align);if?(cand?>=?this_start)return?cand;}return?0; }Clamp 函數可以將隨機變化的數值限制在一個給定的區間[min, max]內。
__memblock_find_range_top_down()通過使用 for_each_free_mem_range_reverse 宏封裝調用__next_free_mem_range_rev()函數,該函數逐一將 memblock.memory 里面的內存塊信息提取出來與 memblock.reserved 的各項信息進行檢驗,確保返回的 this_start 和 this_end 不會與 reserved 的內存存在交叉重疊的情況。判斷大小是否滿足,滿足的情況下,將自末端向前(因為這是 top-down 申請方式)的 size 大小的空間的起始地址(前提該地址不會超出 this_start)返回回去,至此滿足要求的內存塊找到了。
最后看一下memblock_free的實現:
int?__init_memblock?memblock_free(phys_addr_t?base,?phys_addr_t?size) {phys_addr_t?end?=?base?+?size?-?1;memblock_dbg("%s:?[%pa-%pa]?%pS\n",?__func__,&base,?&end,?(void?*)_RET_IP_);kmemleak_free_part_phys(base,?size);return?memblock_remove_range(&memblock.reserved,?base,?size); }這里直接看最核心的部分:
static?int?__init_memblock?memblock_remove_range(struct?memblock_type?*type,phys_addr_t?base,?phys_addr_t?size) {int?start_rgn,?end_rgn;int?i,?ret;ret?=?memblock_isolate_range(type,?base,?size,?&start_rgn,?&end_rgn);if?(ret)return?ret;for?(i?=?end_rgn?-?1;?i?>=?start_rgn;?i--)memblock_remove_region(type,?i);return?0; } static?void?__init_memblock?memblock_remove_region(struct?memblock_type?*type,?unsigned?long?r) {type->total_size?-=?type->regions[r].size;memmove(&type->regions[r],?&type->regions[r?+?1],(type->cnt?-?(r?+?1))?*?sizeof(type->regions[r]));type->cnt--;/*?Special?case?for?empty?arrays?*/if?(type->cnt?==?0)?{WARN_ON(type->total_size?!=?0);type->cnt?=?1;type->regions[0].base?=?0;type->regions[0].size?=?0;type->regions[0].flags?=?0;memblock_set_region_node(&type->regions[0],?MAX_NUMNODES);} }其主要功能是將指定下標索引的內存項從 memblock.reserved 中移除。
在__memblock_remove()里面,memblock_isolate_range()主要作用是將要移除的物理內存區從 reserved 內存區中分離出來,將 start_rgn 和 end_rgn(該內存區塊的起始、結束索引號)返回回去。
memblock_isolate_range()返回后,接著調用 memblock_remove_region() 函數將這些索引對應的內存區塊從內存區中移除,這里具體做法為調用 memmove 函數將 r 索引之后的內存區塊全部往前挪一個位置,這樣 r 索引對應的內存區塊就被移除了,如果移除之后,內存區不含有任何內存區塊,那么就初始化該內存區。
memblock 內存管理總結
memblock 內存區管理算法將可用可分配的內存用 memblock.memory 進行管理,已分配的內存用 memblock.reserved 進行管理,只要內存塊加入到 memblock.reserved 里面就表示該內存被申請占用了,另外,在內存申請的時候,memblock 僅是把被申請到的內存加入到 memblock.reserved 中,而沒有對 memblock.memory 進行任何刪除或改動操作,因此申請和釋放的操作都集中在 memblock.reserved。這個算法效率不高,但是卻是合理的,因為在內核初始化階段并沒有太多復雜的內存操作場景,而且很多地方都是申請的內存都是永久使用的。
內核頁表
上面有提到,內核會通過/arch/x86/kernel/head_32.S 開啟頁式映射,不過這里建立的頁表只是臨時頁表,而內核真正需要建立的是內核頁表。
內核虛擬地址空間與用戶虛擬地址空間
為了合理地利用 4G 的內存空間,Linux 采用了 3:1 的策略,即內核占用 1G 的線性地址空間,用戶占用 3G 的線性地址空間,由于歷史原因,用戶進程的地址范圍從 0~3G,內核地址范圍從 3G~4G,而內核的那 1GB 地址空間又稱為內核虛擬地址(邏輯地址)空間,雖然內核在虛擬地址中是在高地址的,但是在物理地址中是從 0 開始的。
內核虛擬地址與用戶虛擬地址,這兩者都是虛擬地址,都需要經過 MMU 的翻譯轉換為物理地址,從硬件層面上來看,所謂的內核虛擬地址和用戶虛擬地址只是權限不一樣而已,但在軟件層面上看,就大不相同了,當進程需要知道一個用戶空間虛擬地址對應的物理地址時,linux 內核需要通過頁表來得到它的物理地址,而內核空間虛擬地址是所有進程共享的,也就是說,內核在初始化時,就可以創建內核虛擬地址空間的映射,并且這里的映射就是線性映射,它基本等同于物理地址,只是它們之間有一個固定的偏移,當內核需要獲取該物理地址時,可以繞開頁表翻譯直接通過偏移計算拿到,當然這是從軟件層面上來看的,當內核去訪問該頁時, 硬件層面仍然走的是 MMU 翻譯的全過程。
至于為什么用戶虛擬地址空間不能也像內核虛擬地址空間這么做,原因是用戶地址空間是隨進程創建才產生的,無法事先給它分配一塊連續的內存
內核通過內核虛擬地址可以直接訪問到對應的物理地址,那內核如何使用其它的用戶虛擬地址(0~3G)?
Linux 采用的一種折中方案是只對 1G 內核空間的前 896 MB 按線性映射, 剩下的 128 MB 采用動態映射,即走多級頁表翻譯,這樣,內核態能訪問空間就更多了。這里 linux 內核把這 896M 的空間稱為 NORMAL 內存,剩下的 128M 稱為高端內存,即 highmem。在 64 位處理器上,內核空間大大增加,所以也就不需要高端內存了,但是仍然保留了動態映射。
動態映射不全是為了內核空間可以訪問更多的物理內存,還有一個重要原因:如果內核空間全線性映射,那么很可能就會出現內核空間碎片化而滿足不了很多連續頁面分配的需求(這類似于內存分段與內存分頁)。因此內核空間也必須有一部分是非線性映射,從而在這碎片化物理地址空間上,用頁表構造出連續虛擬地址空間(虛擬地址連續、物理地址不連續),這就是所謂的 vmalloc 空間。
到這里,可以大致知道linux 虛擬內存的構造:
linux 內存分頁
linux 內核主要是通過內存分頁來管理內存的,這里先介紹兩個重要的變量:max_pfn 和 max_low_pfn。max_pfn 為最大物理內存頁面幀號,max_low_pfn 為低端內存區的最大可用頁幀號,它們的初始化如下:
void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_ram_pfn();..... #ifdef?CONFIG_X86_32/*?max_low_pfn?get?updated?here?*/find_low_pfn_range(); #elsecheck_x2apic();/*?How?many?end-of-memory?variables?you?have,?grandma!?*//*?need?this?before?calling?reserve_initrd?*/if?(max_pfn?>?(1UL<<(32?-?PAGE_SHIFT)))max_low_pfn?=?e820__end_of_low_ram_pfn();elsemax_low_pfn?=?max_pfn;high_memory?=?(void?*)__va(max_pfn?*?PAGE_SIZE?-?1)?+?1; #endif......e820__memblock_setup();...... }其中 e820__end_of_ram_pfn 的實現如下,其中 E820_TYPE_RAM 代表可用物理內存類型
#define?PAGE_SHIFT??12#ifdef?CONFIG_X86_32 #?ifdef?CONFIG_X86_PAE #??define?MAX_ARCH_PFN??(1ULL<<(36-PAGE_SHIFT)) #?else //32位系統,1<<20,即0x100000,代表4G物理內存的最大頁面幀號 #??define?MAX_ARCH_PFN??(1ULL<<(32-PAGE_SHIFT)) #?endif #else?/*?CONFIG_X86_32?*/ #?define?MAX_ARCH_PFN?MAXMEM>>PAGE_SHIFT #endifunsigned?long?__init?e820__end_of_ram_pfn(void) {return?e820_end_pfn(MAX_ARCH_PFN,?E820_TYPE_RAM); }/**?Find?the?highest?page?frame?number?we?have?available*/ static?unsigned?long?__init?e820_end_pfn(unsigned?long?limit_pfn,?enum?e820_type?type) {int?i;unsigned?long?last_pfn?=?0;unsigned?long?max_arch_pfn?=?MAX_ARCH_PFN;for?(i?=?0;?i?<?e820_table->nr_entries;?i++)?{struct?e820_entry?*entry?=?&e820_table->entries[i];unsigned?long?start_pfn;unsigned?long?end_pfn;if?(entry->type?!=?type)continue;start_pfn?=?entry->addr?>>?PAGE_SHIFT;end_pfn?=?(entry->addr?+?entry->size)?>>?PAGE_SHIFT;if?(start_pfn?>=?limit_pfn)continue;if?(end_pfn?>?limit_pfn)?{last_pfn?=?limit_pfn;break;}if?(end_pfn?>?last_pfn)last_pfn?=?end_pfn;}if?(last_pfn?>?max_arch_pfn)last_pfn?=?max_arch_pfn;pr_info("last_pfn?=?%#lx?max_arch_pfn?=?%#lx\n",last_pfn,?max_arch_pfn);return?last_pfn; }e820__end_of_ram_pfn其實就是遍歷e820_table,得到內存塊的起始地址以及內存塊大小,將起始地址右移 PAGE_SHIFT,算出其起始地址對應的頁面幀號,同時根據內存塊大小可以算出結束地址的頁號,如果結束頁號大于 limit_pfn,則設置該頁號為為 limit_pfn,然后通過比較得到一個 last_pfn,即系統真正的最大物理頁號。
max_low_pfn的計算則調用到了find_low_pfn_range:
#define?PFN_UP(x)?(((x)?+?PAGE_SIZE-1)?>>?PAGE_SHIFT) #define?PFN_DOWN(x)?((x)?>>?PAGE_SHIFT) #define?PFN_PHYS(x)?((phys_addr_t)(x)?<<?PAGE_SHIFT)#ifndef?__pa #define?__pa(x)??__phys_addr((unsigned?long)(x)) #endif#define?VMALLOC_RESERVE??SZ_128M #define?VMALLOC_END??(CONSISTENT_BASE?-?PAGE_SIZE) #define?VMALLOC_START??((VMALLOC_END)?-?VMALLOC_RESERVE) #define?VMALLOC_VMADDR(x)?((unsigned?long)(x)) #define?MAXMEM???__pa(VMALLOC_START) #define?MAXMEM_PFN??PFN_DOWN(MAXMEM)void?__init?find_low_pfn_range(void) {/*?it?could?update?max_pfn?*/if?(max_pfn?<=?MAXMEM_PFN)lowmem_pfn_init();elsehighmem_pfn_init(); }PFN_DOWN(x)是用來返回小于 x 的最后一個物理頁號,PFN_UP(x)是用來返回大于 x 的第一個物理頁號,這里 x 即物理地址,而 PFN_PHYS(x)返回的是物理頁號 x 對應的物理地址。
__pa 其實就是通過虛擬地址計算出物理地址,這一塊后面再做講解。
將 MAXMEM 展開一下可得
#ifdef?CONFIG_HIGHMEM #define?CONSISTENT_BASE??((PKMAP_BASE)?-?(SZ_2M)) #define?CONSISTENT_END??(PKMAP_BASE) #else #define?CONSISTENT_BASE??(FIXADDR_START?-?SZ_2M) #define?CONSISTENT_END??(FIXADDR_START) #endif#define?SZ_2M????0x00200000 #define?SZ_128M????0x08000000#define?MAXMEM??????????????__pa(VMALLOC_END?–?PAGE_OFFSET?–?__VMALLOC_RESERVE) //進一步展開 #define?MAXMEM??????????????__pa(CONSISTENT_BASE?-?PAGE_SIZE?–?PAGE_OFFSET?–?SZ_128M) //再進一步展開 #define?MAXMEM??????????????__pa((PKMAP_BASE)?-?(SZ_2M)?-?PAGE_SIZE?–?PAGE_OFFSET?–?SZ_128M)下面這一部分就涉及到高端內存的構成了,其中PKMAP_BASE 是持久映射空間(KMAP 空間,持久映射區)的起始地址,LAST_PKMAP 則是持久映射空間的映射頁面數,而 FIXADDR_TOP 是固定映射區(臨時內核映射區)的末尾,FIXADDR_START 是固定映射區起始地址,其中的__end_of_permanent_fixed_addresses 是固定映射的一個標志(一個枚舉值,具體可以參考\arch\x86\include\asm\fixmap.h 里的 enum fixed_addresses)。最后的 VMALLOC_END 即為動態映射區的末尾。
//臨時映射 //-4096(4KB)?->?0xfffff000 #define?__FIXADDR_TOP?(-PAGE_SIZE)#define?FIXADDR_TOP?((unsigned?long)__FIXADDR_TOP) #define?FIXADDR_SIZE??(__end_of_permanent_fixed_addresses?<<?PAGE_SHIFT) #define?FIXADDR_START??(FIXADDR_TOP?-?FIXADDR_SIZE) #define?FIXADDR_TOT_SIZE?(__end_of_fixed_addresses?<<?PAGE_SHIFT) #define?FIXADDR_TOT_START?(FIXADDR_TOP?-?FIXADDR_TOT_SIZE)//持久內核映射 #ifdef?CONFIG_X86_PAE #define?LAST_PKMAP?512 #else #define?LAST_PKMAP?1024 #endif#define?CPU_ENTRY_AREA_BASE?\((FIXADDR_TOT_START?-?PAGE_SIZE*(CPU_ENTRY_AREA_PAGES+1))?&?PMD_MASK)#define?LDT_BASE_ADDR??\((CPU_ENTRY_AREA_BASE?-?PAGE_SIZE)?&?PMD_MASK)#define?LDT_END_ADDR??(LDT_BASE_ADDR?+?PMD_SIZE)#define?PKMAP_BASE??\((LDT_BASE_ADDR?-?PAGE_SIZE)?&?PMD_MASK)//動態映射 //0xffffff80<<20?->?0xf8000000?->?4,160,749,568?->?3948MB?->?3GB+896MB?與上述一致 #define?high_memory?(-128UL?<<?20) //8MB #define?VMALLOC_OFFSET?(8?*?1024?*?1024) #define?VMALLOC_START?((unsigned?long)high_memory?+?VMALLOC_OFFSET)#ifdef?CONFIG_HIGHMEM #?define?VMALLOC_END?(PKMAP_BASE?-?2?*?PAGE_SIZE) #else #?define?VMALLOC_END?(LDT_BASE_ADDR?-?2?*?PAGE_SIZE) #endif直接看圖~
PAGE_OFFSET 代表的是內核空間和用戶空間對虛擬地址空間的劃分,對不同的體系結構不同。比如在 32 位系統中 3G-4G 屬于內核使用的內存空間,所以 PAGE_OFFSET = 0xC0000000
內核空間如上圖,可分為直接內存映射區和高端內存映射區,其中直接內存映射區是指 3G 到 3G+896M 的線性空間,直接對應物理地址就是 0 到 896M(前提是有超過 896M 的物理內存),其中 896M 是 high_memory 值,使用 kmalloc()/kfree()接口申請釋放內存;而高端內存映射區則是超過 896M 物理內存的空間,它又分為動態映射區、持久映射區和固定映射區。
動態內存映射區,又稱之為 vmalloc 映射區或非連續映射區,是指 VMALLOC_START 到 VMALLOC_END 的地址空間,申請釋放內存的接口是 vmalloc()/vfree(),通常用于將非連續的物理內存映射為連續的線性地址內存空間;
而持久映射區,又稱之為 KMAP 區或永久映射區,是指自 PKMAP_BASE 開始共 LAST_PKMAP 個頁面大小的空間,操作接口是 kmap()/kunmap(),用于將高端內存長久映射到內存虛擬地址空間中;
最后的固定映射區,有時候也稱為臨時映射區,是指 FIXADDR_START 到 FIXADDR_TOP 的地址空間,操作接口是 kmap_atomic()/kummap_atomic(),用于解決持久映射不能用于中斷處理程序而增加的臨時內核映射。
上面的MAXMEM_PFN 其實就是用來判斷是否初始化(啟用)高端內存,當內存物理頁數本來就小于低端內存的最大物理頁數時,就沒有高端地址映射。
這里接著看 max_low_pfn 的初始化,進入 highmem_pfn_init(void)。
static?void?__init?highmem_pfn_init(void) {max_low_pfn?=?MAXMEM_PFN;if?(highmem_pages?==?-1)highmem_pages?=?max_pfn?-?MAXMEM_PFN;if?(highmem_pages?+?MAXMEM_PFN?<?max_pfn)max_pfn?=?MAXMEM_PFN?+?highmem_pages;if?(highmem_pages?+?MAXMEM_PFN?>?max_pfn)?{printk(KERN_WARNING?MSG_HIGHMEM_TOO_SMALL,pages_to_mb(max_pfn?-?MAXMEM_PFN),pages_to_mb(highmem_pages));highmem_pages?=?0;} #ifndef?CONFIG_HIGHMEM/*?Maximum?memory?usable?is?what?is?directly?addressable?*/printk(KERN_WARNING?"Warning?only?%ldMB?will?be?used.\n",?MAXMEM>>20);if?(max_pfn?>?MAX_NONPAE_PFN)printk(KERN_WARNING?"Use?a?HIGHMEM64G?enabled?kernel.\n");elseprintk(KERN_WARNING?"Use?a?HIGHMEM?enabled?kernel.\n");max_pfn?=?MAXMEM_PFN; #else?/*?!CONFIG_HIGHMEM?*/ #ifndef?CONFIG_HIGHMEM64Gif?(max_pfn?>?MAX_NONPAE_PFN)?{max_pfn?=?MAX_NONPAE_PFN;printk(KERN_WARNING?MSG_HIGHMEM_TRIMMED);} #endif?/*?!CONFIG_HIGHMEM64G?*/ #endif?/*?!CONFIG_HIGHMEM?*/ }highmem_pfn_init 的主要工作其實就是把 max_low_pfn 設置為 MAXMEM_PFN,將 highmem_pages 設置為 max_pfn – MAXMEM_PFN,至此,max_pfn 和 max_low_pfn 初始化完畢。
低端內存初始化
回到 setup_arch 函數:
void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_ram_pfn();..... #ifdef?CONFIG_X86_32/*?max_low_pfn?get?updated?here?*/find_low_pfn_range(); #elsecheck_x2apic();/*?How?many?end-of-memory?variables?you?have,?grandma!?*//*?need?this?before?calling?reserve_initrd?*/if?(max_pfn?>?(1UL<<(32?-?PAGE_SHIFT)))max_low_pfn?=?e820__end_of_low_ram_pfn();elsemax_low_pfn?=?max_pfn;high_memory?=?(void?*)__va(max_pfn?*?PAGE_SIZE?-?1)?+?1; #endif......early_alloc_pgt_buf();?//<-------------------------------------/**?Need?to?conclude?brk,?before?e820__memblock_setup()*??it?could?use?memblock_find_in_range,?could?overlap?with*??brk?area.*/reserve_brk();?//<-------------------------------------------......e820__memblock_setup();...... }early_alloc_pgt_buf()即申請頁表緩沖區
#define?INIT_PGD_PAGE_COUNT??????6 #define?INIT_PGT_BUF_SIZE?(INIT_PGD_PAGE_COUNT?*?PAGE_SIZE) RESERVE_BRK(early_pgt_alloc,?INIT_PGT_BUF_SIZE); void??__init?early_alloc_pgt_buf(void) {unsigned?long?tables?=?INIT_PGT_BUF_SIZE;phys_addr_t?base;base?=?__pa(extend_brk(tables,?PAGE_SIZE));pgt_buf_start?=?base?>>?PAGE_SHIFT;pgt_buf_end?=?pgt_buf_start;pgt_buf_top?=?pgt_buf_start?+?(tables?>>?PAGE_SHIFT); }pgt_buf_start:標識該緩沖空間的起始物理內存頁框號;
pgt_buf_end:初始化時和 pgt_buf_start 是同一個值,但是它是用于表示該空間未被申請使用的空間起始頁框號;
pgt_buf_top:則是用來表示緩沖空間的末尾,存放的是該末尾的頁框號
INIT_PGT_BUF_SIZE 即 24KB,這里直接看最關鍵的部分:extend_brk
unsigned?long?_brk_start?=?(unsigned?long)__brk_base; unsigned?long?_brk_end???=?(unsigned?long)__brk_base;void?*?__init?extend_brk(size_t?size,?size_t?align) {size_t?mask?=?align?-?1;void?*ret;BUG_ON(_brk_start?==?0);BUG_ON(align?&?mask);_brk_end?=?(_brk_end?+?mask)?&?~mask;BUG_ON((char?*)(_brk_end?+?size)?>?__brk_limit);ret?=?(void?*)_brk_end;_brk_end?+=?size;memset(ret,?0,?size);return?ret; }BUG_ON()函數是內核標記 bug、提供斷言并輸出信息的常用手段
__brk_base 相關的初始化可以參考 arch\x86\kernel\vmlinux.lds.S
在 setup_arch()中,緊接著 early_alloc_pgt_buf()還有 reserve_brk()函數
static?void?__init?reserve_brk(void) {if?(_brk_end?>?_brk_start)memblock_reserve(__pa_symbol(_brk_start),_brk_end?-?_brk_start);/*?Mark?brk?area?as?locked?down?and?no?longer?taking?anynew?allocations?*/_brk_start?=?0; }這個地方主要是將 early_alloc_pgt_buf()申請的空間在 membloc 中做 reserved 保留操作,避免被其它地方申請使用而引發異常
回到 setup_arch 函數:
void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_ram_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內存初始化............early_alloc_pgt_buf();?//頁表緩沖區分配reserve_brk();?//緩沖區加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動......init_mem_mapping();?//<-----------------------------...... }init_mem_mapping()即低端內存內核頁表初始化的關鍵函數
#define?ISA_END_ADDRESS??0x00100000?//1MBvoid?__init?init_mem_mapping(void) {unsigned?long?end;pti_check_boottime_disable();probe_page_size_mask();setup_pcid();#ifdef?CONFIG_X86_64end?=?max_pfn?<<?PAGE_SHIFT; #elseend?=?max_low_pfn?<<?PAGE_SHIFT; #endif/*?the?ISA?range?is?always?mapped?regardless?of?memory?holes?*/init_memory_mapping(0,?ISA_END_ADDRESS,?PAGE_KERNEL);/*?Init?the?trampoline,?possibly?with?KASLR?memory?offset?*/init_trampoline();/**?If?the?allocation?is?in?bottom-up?direction,?we?setup?direct?mapping*?in?bottom-up,?otherwise?we?setup?direct?mapping?in?top-down.*/if?(memblock_bottom_up())?{unsigned?long?kernel_end?=?__pa_symbol(_end);memory_map_bottom_up(kernel_end,?end);memory_map_bottom_up(ISA_END_ADDRESS,?kernel_end);}?else?{memory_map_top_down(ISA_END_ADDRESS,?end);}#ifdef?CONFIG_X86_64if?(max_pfn?>?max_low_pfn)?{/*?can?we?preseve?max_low_pfn??*/max_low_pfn?=?max_pfn;} #elseearly_ioremap_page_table_range_init(); #endifload_cr3(swapper_pg_dir);__flush_tlb_all();x86_init.hyper.init_mem_mapping();early_memtest(0,?max_pfn_mapped?<<?PAGE_SHIFT); }probe_page_size_mask()主要作用是初始化直接映射變量(直接映射區相關)以及根據配置來控制 CR4 寄存器的置位,用于后面分頁時頁面大小的判定。
上面 init_memory_mapping 的參數 ISA_END_ADDRESS 表示 ISA 總線上設備的末尾地址。
init_memory_mapping(0, ISA_END_ADDRESS, PAGE_KERNEL)初始化 0 ~ 1MB 的物理地址,一般內核啟動時被安裝在 1MB 開始處,這里初始化完成之后會調用到 memory_map_bottom_up 或者 memory_map_top_down,后面就是初始化 1MB ~ 內核結束地址 這塊物理地址區域 ,最后也會回歸到 init_memory_mapping 的調用,因此這里不做過多的介紹,直接看 init_memory_mapping():
#ifdef?CONFIG_X86_32 #define?NR_RANGE_MR?3 #else?/*?CONFIG_X86_64?*/ #define?NR_RANGE_MR?5 #endifstruct?map_range?{unsigned?long?start;unsigned?long?end;unsigned?page_size_mask; };unsigned?long?__ref?init_memory_mapping(unsigned?long?start,unsigned?long?end,?pgprot_t?prot) {struct?map_range?mr[NR_RANGE_MR];unsigned?long?ret?=?0;int?nr_range,?i;pr_debug("init_memory_mapping:?[mem?%#010lx-%#010lx]\n",start,?end?-?1);memset(mr,?0,?sizeof(mr));nr_range?=?split_mem_range(mr,?0,?start,?end);for?(i?=?0;?i?<?nr_range;?i++)ret?=?kernel_physical_mapping_init(mr[i].start,?mr[i].end,mr[i].page_size_mask,prot);add_pfn_range_mapped(start?>>?PAGE_SHIFT,?ret?>>?PAGE_SHIFT);return?ret?>>?PAGE_SHIFT; }struct map_range,該結構是用來保存內存段信息,其包含了一個段的起始地址、結束地址,以及該段是按多大的頁面進行分頁(4K、2M、1G,1G 是 64 位的,所以這里不提及)。
static?int?__meminit?split_mem_range(struct?map_range?*mr,?int?nr_range,unsigned?long?start,unsigned?long?end) {unsigned?long?start_pfn,?end_pfn,?limit_pfn;unsigned?long?pfn;int?i;//返回小于...的最后一個物理頁號limit_pfn?=?PFN_DOWN(end);pfn?=?start_pfn?=?PFN_DOWN(start);if?(pfn?==?0)end_pfn?=?PFN_DOWN(PMD_SIZE);elseend_pfn?=?round_up(pfn,?PFN_DOWN(PMD_SIZE));if?(end_pfn?>?limit_pfn)end_pfn?=?limit_pfn;if?(start_pfn?<?end_pfn)?{nr_range?=?save_mr(mr,?nr_range,?start_pfn,?end_pfn,?0);pfn?=?end_pfn;}/*?big?page?(2M)?range?*/start_pfn?=?round_up(pfn,?PFN_DOWN(PMD_SIZE));end_pfn?=?round_down(limit_pfn,?PFN_DOWN(PMD_SIZE));if?(start_pfn?<?end_pfn)?{nr_range?=?save_mr(mr,?nr_range,?start_pfn,?end_pfn,page_size_mask?&?(1<<PG_LEVEL_2M));pfn?=?end_pfn;}/*?tail?is?not?big?page?(2M)?alignment?*/start_pfn?=?pfn;end_pfn?=?limit_pfn;nr_range?=?save_mr(mr,?nr_range,?start_pfn,?end_pfn,?0);if?(!after_bootmem)adjust_range_page_size_mask(mr,?nr_range);/*?try?to?merge?same?page?size?and?continuous?*/for?(i?=?0;?nr_range?>?1?&&?i?<?nr_range?-?1;?i++)?{unsigned?long?old_start;if?(mr[i].end?!=?mr[i+1].start?||mr[i].page_size_mask?!=?mr[i+1].page_size_mask)continue;/*?move?it?*/old_start?=?mr[i].start;memmove(&mr[i],?&mr[i+1],(nr_range?-?1?-?i)?*?sizeof(struct?map_range));mr[i--].start?=?old_start;nr_range--;}for?(i?=?0;?i?<?nr_range;?i++)pr_debug("?[mem?%#010lx-%#010lx]?page?%s\n",mr[i].start,?mr[i].end?-?1,page_size_string(&mr[i]));return?nr_range; }PMD_SIZE 用于計算由頁中間目錄的一個單獨表項所映射的區域大小,也就是一個頁表的大小。
split_mem_range()根據傳入的內存 start 和 end 做四舍五入的對齊操作(即 round_up 和 round_down)
#define?__round_mask(x,?y)?((__typeof__(x))((y)-1)) #define?round_up(x,?y)?((((x)-1)?|?__round_mask(x,?y))+1) //可以理解為:#define round_up(x, y)?(((x)+(y)?- 1)/(y))*(y)) #define?round_down(x,?y)?((x)?&?~__round_mask(x,?y)) //可以理解為:#define round_down(x, y)?((x/y)?* y)round_up 宏依靠整數除法來完成這項工作,僅當兩個參數均為整數時,它才有效,x 是需要四舍五入的數字,y 是應該四舍五入的間隔,也就是說,round_up(12,5)應返回 15,因為 15 是大于 12 的 5 的第一個間隔,而 round_down(12,5)應返回 10。
split_mem_range()會根據對齊的情況,把開始、末尾的不對齊部分及中間部分分成了三段,使用 save_mr()將其存放在 init_mem_mapping()的局部變量數組 mr 中。劃分開來主要是為了讓各部分可以映射到不同大小的頁面,最后如果相鄰兩部分映射頁面的大小是一致的,則將其合并。
可以通過 dmesg 得到劃分的情況(以下是我私服的劃分情況,不過是 64 位的……)
初始化完內存段信息 mr 之后,就行了進入到kernel_physical_mapping_init,這個函數是建立內核頁表的最為關鍵的一步,負責處理物理內存的映射。
在 2.6.11 后,Linux 采用四級分頁模型,這四級頁目錄分別為:
頁全局目錄(Page Global Directory)
頁上級目錄(Page Upper Directory)
頁中間目錄(Page Middle Directory)
頁表(Page Table)
對于沒有啟動 PAE(物理地址擴展)的 32 位系統,Linux 雖然也采用四級分頁模型,但本質上只用到了兩級分頁,Linux 通過將"頁上級目錄"位域和“頁中間目錄”位域全為 0 來達到使用兩級分頁的目的,但為了保證程序能 32 位和 64 系統上都能運行,內核保留了頁上級目錄和頁中間目錄在指針序列中的位置,它們的頁目錄數都被內核置為 1,并把這 2 個頁目錄項映射到適合的全局目錄項。
開啟 PAE 后,32 位系統尋址方式將大大改變,這時候使用的是三級頁表,即頁上級目錄其實沒有真正用到。
這里不考慮 PAE
PAGE_OFFSET 代表的是內核空間和用戶空間對虛擬地址空間的劃分,對不同的體系結構不同。比如在 32 位系統中 3G-4G 屬于內核使用的內存空間,所以 PAGE_OFFSET = 0xC0000000
unsigned?long?__init kernel_physical_mapping_init(unsigned?long?start,unsigned?long?end,unsigned?long?page_size_mask,pgprot_t?prot) {int?use_pse?=?page_size_mask?==?(1<<PG_LEVEL_2M);unsigned?long?last_map_addr?=?end;unsigned?long?start_pfn,?end_pfn;pgd_t?*pgd_base?=?swapper_pg_dir;int?pgd_idx,?pmd_idx,?pte_ofs;unsigned?long?pfn;pgd_t?*pgd;pmd_t?*pmd;pte_t?*pte;unsigned?pages_2m,?pages_4k;int?mapping_iter;start_pfn?=?start?>>?PAGE_SHIFT;end_pfn?=?end?>>?PAGE_SHIFT;mapping_iter?=?1;if?(!boot_cpu_has(X86_FEATURE_PSE))use_pse?=?0;repeat:pages_2m?=?pages_4k?=?0;pfn?=?start_pfn;?//pfn保存起始頁框號pgd_idx?=?pgd_index((pfn<<PAGE_SHIFT)?+?PAGE_OFFSET);?//低端內存的起始地址對應的pgd的偏移pgd?=?pgd_base?+?pgd_idx;?//得到起始頁框對應的pgd//由pgd開始遍歷for?(;?pgd_idx?<?PTRS_PER_PGD;?pgd++,?pgd_idx++)?{pmd?=?one_md_table_init(pgd);//申請得到一個pmd表if?(pfn?>=?end_pfn)continue; #ifdef?CONFIG_X86_PAEpmd_idx?=?pmd_index((pfn<<PAGE_SHIFT)?+?PAGE_OFFSET);pmd?+=?pmd_idx; #elsepmd_idx?=?0; #endif//遍歷pmd表,對于未激活PAE的32位系統,PTRS_PER_PMD為1,激活PAE則為512for?(;?pmd_idx?<?PTRS_PER_PMD?&&?pfn?<?end_pfn;pmd++,?pmd_idx++)?{unsigned?int?addr?=?pfn?*?PAGE_SIZE?+?PAGE_OFFSET;/**?Map?with?big?pages?if?possible,?otherwise*?create?normal?page?tables:*/if?(use_pse)?{unsigned?int?addr2;pgprot_t?prot?=?PAGE_KERNEL_LARGE;/**?first?pass?will?use?the?same?initial*?identity?mapping?attribute?+?_PAGE_PSE.*/pgprot_t?init_prot?=__pgprot(PTE_IDENT_ATTR?|_PAGE_PSE);pfn?&=?PMD_MASK?>>?PAGE_SHIFT;addr2?=?(pfn?+?PTRS_PER_PTE-1)?*?PAGE_SIZE?+PAGE_OFFSET?+?PAGE_SIZE-1;if?(__is_kernel_text(addr)?||__is_kernel_text(addr2))prot?=?PAGE_KERNEL_LARGE_EXEC;pages_2m++;if?(mapping_iter?==?1)set_pmd(pmd,?pfn_pmd(pfn,?init_prot));elseset_pmd(pmd,?pfn_pmd(pfn,?prot));pfn?+=?PTRS_PER_PTE;continue;}pte?=?one_page_table_init(pmd);?//創建一個頁表//得到pfn在page?table中的偏移并定位到具體的ptepte_ofs?=?pte_index((pfn<<PAGE_SHIFT)?+?PAGE_OFFSET);pte?+=?pte_ofs;//由pte開始遍歷page?tablefor?(;?pte_ofs?<?PTRS_PER_PTE?&&?pfn?<?end_pfn;pte++,?pfn++,?pte_ofs++,?addr?+=?PAGE_SIZE)?{pgprot_t?prot?=?PAGE_KERNEL;/**?first?pass?will?use?the?same?initial*?identity?mapping?attribute.*/pgprot_t?init_prot?=?__pgprot(PTE_IDENT_ATTR);if?(__is_kernel_text(addr))?//如果處于內核代碼段,權限設為可執行prot?=?PAGE_KERNEL_EXEC;pages_4k++;//設置pte與pfn關聯if?(mapping_iter?==?1)?{set_pte(pte,?pfn_pte(pfn,?init_prot));?//第一次執行將權限位設為init_protlast_map_addr?=?(pfn?<<?PAGE_SHIFT)?+?PAGE_SIZE;}?elseset_pte(pte,?pfn_pte(pfn,?prot));?//之后的執行將權限位置為prot}}}if?(mapping_iter?==?1)?{/**?update?direct?mapping?page?count?only?in?the?first*?iteration.*/update_page_count(PG_LEVEL_2M,?pages_2m);update_page_count(PG_LEVEL_4K,?pages_4k);/**?local?global?flush?tlb,?which?will?flush?the?previous*?mappings?present?in?both?small?and?large?page?TLB's.*/__flush_tlb_all();?//TLB全部刷新/**?Second?iteration?will?set?the?actual?desired?PTE?attributes.*/mapping_iter?=?2;goto?repeat;}return?last_map_addr; }內核的內核頁全局目錄的基地址保存在 swapper_pg_dir 全局變量中,但需要使用主內核頁表時系統會把這個變量的值放入 cr3 寄存器,詳細可參考/arch/x86/kernel/head_32.s。
Linux 分別采用 pgd_t、pud_t、pmd_t 和 pte_t 四種數據結構來表示頁全局目錄項、頁上級目錄項、頁中間目錄項和頁表項。這四種數據結構本質上都是無符號長整型,Linux 為了更嚴格數據類型檢查,將無符號長整型分別封裝成四種不同的頁表項。如果不采用這種方法,那么一個無符號長整型數據可以傳入任何一個與四種頁表相關的函數或宏中,這將大大降低程序的健壯性。下面僅列出 pgd_t 類型的內核源碼實現,其他類型與此類似
typedef?unsigned?long???pgdval_t;typedef?struct?{?pgdval_t?pgd;?}?pgd_t;#define?pgd_val(x)??????native_pgd_val(x)static?inline?pgdval_t?native_pgd_val(pgd_t?pgd) {return?pgd.pgd; }這里需要區別指向頁表項的指針和頁表項所代表的數據,如果已知一個 pgd_t 類型的指針 pgd,那么通過 pgd_val(*pgd)即可獲得該頁表項(也就是一個無符號長整型數據)
PAGE_SHIFT,PMD_SHIFT,PUD_SHIFT,PGDIR_SHIFT,對應相應的頁目錄所能映射的區域大小的位數,如 PAGE_SHIFT 為 12,即頁面大小為 4k。
PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD, PTRS_PER_PGD,對應相應頁目錄中的表項數。32 位系統下,當 PAE 被禁止時,他們的值分別為 1024,,1,1 和 1024,也就是說只使用兩級分頁
pgd_index(addr),pud_index,(addr),pmd_index(addr),pte_index(addr),取 addr 在該目錄中的索引
pud_offset(pgd,addr), pmd_offset(pud,addr), pte_offset(pmd,addr),以 pmd_offset 為例,線性地址 addr 對應的 pmd 索引在在 pud 指定的 pmd 表的偏移地址。在兩級或三級分頁系統中,pmd_offset 和 pud_offset 都返回頁全局目錄的地址
至此,低端內存的物理地址和虛擬地址之間的映射關系已全部建立起來了
回到前面的 init_memory_mapping()函數,它的最后一個函數調用為 add_pfn_range_mapped()
struct?range?pfn_mapped[E820_MAX_ENTRIES]; int?nr_pfn_mapped;static?void?add_pfn_range_mapped(unsigned?long?start_pfn,?unsigned?long?end_pfn) {nr_pfn_mapped?=?add_range_with_merge(pfn_mapped,?E820_MAX_ENTRIES,nr_pfn_mapped,?start_pfn,?end_pfn);nr_pfn_mapped?=?clean_sort_range(pfn_mapped,?E820_MAX_ENTRIES);max_pfn_mapped?=?max(max_pfn_mapped,?end_pfn);if?(start_pfn?<?(1UL<<(32-PAGE_SHIFT)))max_low_pfn_mapped?=?max(max_low_pfn_mapped,min(end_pfn,?1UL<<(32-PAGE_SHIFT))); }該函數主要是將前面完成內存映射的物理頁框范圍加入到全局數組pfn_mapped中,其中 nr_pfn_mapped 用于表示數組中的有效項數量,之后可以通過內核函數 pfn_range_is_mapped 來判斷指定的物理內存是否被映射,避免重復映射的情況
固定映射區初始化
再回到更前面的 init_mem_mapping()函數,early_ioremap_page_table_range_init()用來建立高端內存的固定映射區頁表,與低端內存的頁表初始化不同的是,固定映射區的頁表只是被分配,相應的 PTE 項并未初始化,這個工作交由后面的set_fixmap()函數將相關的固定映射區頁表與物理內存進行關聯
#?define?PMD_MASK?(~(PMD_SIZE?-?1)) void?__init?early_ioremap_page_table_range_init(void) {pgd_t?*pgd_base?=?swapper_pg_dir;unsigned?long?vaddr,?end;/**?Fixed?mappings,?only?the?page?table?structure?has?to?be*?created?-?mappings?will?be?set?by?set_fixmap():*/vaddr?=?__fix_to_virt(__end_of_fixed_addresses?-?1)?&?PMD_MASK;end?=?(FIXADDR_TOP?+?PMD_SIZE?-?1)?&?PMD_MASK;page_table_range_init(vaddr,?end,?pgd_base);early_ioremap_reset(); }PUD_MASK、PMD_MASK、PGDIR_MASK,這些 MASK 的作用是:從給定地址中提取某些分量,用給定地址與對應的 MASK 位與操作之后即可獲得各個分量,上面的操作為屏蔽低位
這里可以先具體看一下固定映射區的組成
每個固定映射區索引都以枚舉類型的形式定義在 enum fixed_addresses 中
enum?fixed_addresses?{ #ifdef?CONFIG_X86_32FIX_HOLE, #else #ifdef?CONFIG_X86_VSYSCALL_EMULATIONVSYSCALL_PAGE?=?(FIXADDR_TOP?-?VSYSCALL_ADDR)?>>?PAGE_SHIFT, #endif #endifFIX_DBGP_BASE,FIX_EARLYCON_MEM_BASE, #ifdef?CONFIG_PROVIDE_OHCI1394_DMA_INITFIX_OHCI1394_BASE, #endif #ifdef?CONFIG_X86_LOCAL_APICFIX_APIC_BASE,?/*?local?(CPU)?APIC)?--?required?for?SMP?or?not?*/ #endif #ifdef?CONFIG_X86_IO_APICFIX_IO_APIC_BASE_0,FIX_IO_APIC_BASE_END?=?FIX_IO_APIC_BASE_0?+?MAX_IO_APICS?-?1, #endif #ifdef?CONFIG_X86_32//這里即為固定映射區FIX_KMAP_BEGIN,?/*?reserved?pte's?for?temporary?kernel?mappings?*/FIX_KMAP_END?=?FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1, #ifdef?CONFIG_PCI_MMCONFIGFIX_PCIE_MCFG, #endif #endif #ifdef?CONFIG_PARAVIRT_XXLFIX_PARAVIRT_BOOTMAP, #endif #ifdef?CONFIG_X86_INTEL_MIDFIX_LNW_VRTC, #endif#ifdef?CONFIG_ACPI_APEI_GHES/*?Used?for?GHES?mapping?from?assorted?contexts?*/FIX_APEI_GHES_IRQ,FIX_APEI_GHES_NMI, #endif__end_of_permanent_fixed_addresses,/**?512?temporary?boot-time?mappings,?used?by?early_ioremap(),*?before?ioremap()?is?functional.**?If?necessary?we?round?it?up?to?the?next?512?pages?boundary?so*?that?we?can?have?a?single?pmd?entry?and?a?single?pte?table:*/ #define?NR_FIX_BTMAPS??64 #define?FIX_BTMAPS_SLOTS?8 #define?TOTAL_FIX_BTMAPS?(NR_FIX_BTMAPS?*?FIX_BTMAPS_SLOTS)FIX_BTMAP_END?=(__end_of_permanent_fixed_addresses?^(__end_of_permanent_fixed_addresses?+?TOTAL_FIX_BTMAPS?-?1))?&-PTRS_PER_PTE??__end_of_permanent_fixed_addresses?+?TOTAL_FIX_BTMAPS?-(__end_of_permanent_fixed_addresses?&?(TOTAL_FIX_BTMAPS?-?1)):?__end_of_permanent_fixed_addresses,FIX_BTMAP_BEGIN?=?FIX_BTMAP_END?+?TOTAL_FIX_BTMAPS?-?1, #ifdef?CONFIG_X86_32FIX_WP_TEST, #endif #ifdef?CONFIG_INTEL_TXTFIX_TBOOT_BASE, #endif__end_of_fixed_addresses };#define?__fix_to_virt(x)?(FIXADDR_TOP?-?((x)?<<?PAGE_SHIFT))一個索引對應一個 4KB 的頁框,固定映射區的結束地址為 FIXADDR_TOP,即 0xfffff000(4G-4K),固定映射區是反向生長的,也就是說第一個索引對應的地址離 FIXADDR_TOP 最近。另外宏__fix_to_virt(idx)可以通過索引來計算相應的固定映射區域的線性地址
//初始化pgd_base指向的頁全局目錄中start到end這個范圍的線性地址,整個函數結束后只是初始化好了頁中間目錄項對應的頁表,但是頁表中的頁表項并沒有初始化 static?void?__init page_table_range_init(unsigned?long?start,?unsigned?long?end,?pgd_t?*pgd_base) {int?pgd_idx,?pmd_idx;unsigned?long?vaddr;pgd_t?*pgd;pmd_t?*pmd;pte_t?*pte?=?NULL;unsigned?long?count?=?page_table_range_init_count(start,?end);void?*adr?=?NULL;if?(count)adr?=?alloc_low_pages(count);vaddr?=?start;pgd_idx?=?pgd_index(vaddr);?//得到vaddr對應的pgd索引pmd_idx?=?pmd_index(vaddr);?//得到vaddr對應的pmd索引pgd?=?pgd_base?+?pgd_idx;???//得到pgd項for?(?;?(pgd_idx?<?PTRS_PER_PGD)?&&?(vaddr?!=?end);?pgd++,?pgd_idx++)?{pmd?=?one_md_table_init(pgd);?//得到pmd起始項pmd?=?pmd?+?pmd_index(vaddr);?//得到偏移后的pmdfor?(;?(pmd_idx?<?PTRS_PER_PMD)?&&?(vaddr?!=?end);pmd++,?pmd_idx++)?{//建立pte表并檢查vaddr是否對應內核臨時映射區,若是則重新申請一個頁表來保存pte表pte?=?page_table_kmap_check(one_page_table_init(pmd),pmd,?vaddr,?pte,?&adr);vaddr?+=?PMD_SIZE;}pmd_idx?=?0;} }先看 page_table_range_init_count 函數
static?unsigned?long?__init page_table_range_init_count(unsigned?long?start,?unsigned?long?end) {unsigned?long?count?=?0; #ifdef?CONFIG_HIGHMEMint?pmd_idx_kmap_begin?=?fix_to_virt(FIX_KMAP_END)?>>?PMD_SHIFT;int?pmd_idx_kmap_end?=?fix_to_virt(FIX_KMAP_BEGIN)?>>?PMD_SHIFT;int?pgd_idx,?pmd_idx;unsigned?long?vaddr;if?(pmd_idx_kmap_begin?==?pmd_idx_kmap_end)return?0;vaddr?=?start;pgd_idx?=?pgd_index(vaddr);pmd_idx?=?pmd_index(vaddr);for?(?;?(pgd_idx?<?PTRS_PER_PGD)?&&?(vaddr?!=?end);?pgd_idx++)?{for?(;?(pmd_idx?<?PTRS_PER_PMD)?&&?(vaddr?!=?end);pmd_idx++)?{if?((vaddr?>>?PMD_SHIFT)?>=?pmd_idx_kmap_begin?&&(vaddr?>>?PMD_SHIFT)?<=?pmd_idx_kmap_end)count++;vaddr?+=?PMD_SIZE;}pmd_idx?=?0;} #endifreturn?count; }page_table_range_init_count()用來計算臨時映射區間的頁表數量。FIXADDR_START 到 FIXADDR_TOP 即整個固定映射區,就如上面所提到的里面有多個索引標識的不同功能的映射區間,而其中的一個區間FIX_KMAP_BEGIN 到 FIX_KMAP_END 是臨時映射區間。這里再看一下兩者的定義:
FIX_KMAP_BEGIN,?/*?reserved?pte's?for?temporary?kernel?mappings?*/ FIX_KMAP_END?=?FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,固定映射是在編譯時就確定的地址空間,但是它對應的物理頁可以是任意的,每一個固定地址中的項都是一頁范圍,這些地址的用途是固定的,這是該區間被設計的最主要的用途。臨時映射區間也叫原子映射,它可以用在不能睡眠的地方,如中斷處理程序中,因為臨時映射獲取映射時是絕對不會阻塞的,上面的 KM_TYPE_NR 可以理解為臨時內核映射的最大數量,NR_CPUS 則表示 CPU 數量。
如果返回的頁表數量不為 0,則 page_table_range_init()函數還會調用 alloc_low_pages():
__ref?void?*alloc_low_pages(unsigned?int?num) {unsigned?long?pfn;int?i;if?(after_bootmem)?{unsigned?int?order;order?=?get_order((unsigned?long)num?<<?PAGE_SHIFT);return?(void?*)__get_free_pages(GFP_ATOMIC?|?__GFP_ZERO,?order);}if?((pgt_buf_end?+?num)?>?pgt_buf_top?||?!can_use_brk_pgt)?{unsigned?long?ret?=?0;if?(min_pfn_mapped?<?max_pfn_mapped)?{ret?=?memblock_find_in_range(min_pfn_mapped?<<?PAGE_SHIFT,max_pfn_mapped?<<?PAGE_SHIFT,PAGE_SIZE?*?num?,?PAGE_SIZE);}if?(ret)memblock_reserve(ret,?PAGE_SIZE?*?num);else?if?(can_use_brk_pgt)ret?=?__pa(extend_brk(PAGE_SIZE?*?num,?PAGE_SIZE));if?(!ret)panic("alloc_low_pages:?can?not?alloc?memory");pfn?=?ret?>>?PAGE_SHIFT;}?else?{pfn?=?pgt_buf_end;pgt_buf_end?+=?num;}for?(i?=?0;?i?<?num;?i++)?{void?*adr;adr?=?__va((pfn?+?i)?<<?PAGE_SHIFT);clear_page(adr);}return?__va(pfn?<<?PAGE_SHIFT); }alloc_low_pages函數根據前面 early_alloc_pgt_buf()申請保留的頁表緩沖空間使用情況來判斷是從頁表緩沖空間中申請內存還是通過 memblock 算法申請頁表內存,頁表緩沖空間空間足夠的話就在頁表緩沖空間中分配。
回到 page_table_range_init(),其中one_md_table_init()是用于申請新物理頁作為頁中間目錄的,不過這里分析的是 x86 非 PAE 環境,不存在頁中間目錄,因此實際上返回的仍是入參,代碼如下:
static?pmd_t?*?__init?one_md_table_init(pgd_t?*pgd) {p4d_t?*p4d;pud_t?*pud;pmd_t?*pmd_table;#ifdef?CONFIG_X86_PAEif?(!(pgd_val(*pgd)?&?_PAGE_PRESENT))?{pmd_table?=?(pmd_t?*)alloc_low_page();paravirt_alloc_pmd(&init_mm,?__pa(pmd_table)?>>?PAGE_SHIFT);set_pgd(pgd,?__pgd(__pa(pmd_table)?|?_PAGE_PRESENT));p4d?=?p4d_offset(pgd,?0);pud?=?pud_offset(p4d,?0);BUG_ON(pmd_table?!=?pmd_offset(pud,?0));return?pmd_table;} #endifp4d?=?p4d_offset(pgd,?0);pud?=?pud_offset(p4d,?0);pmd_table?=?pmd_offset(pud,?0);return?pmd_table; }接著的是page_table_kmap_check(),其入參調用的one_page_table_init()是用于當入參 pmd 沒有頁表指向時,創建頁表并使其指向被創建的頁表。
static?pte_t?*?__init?one_page_table_init(pmd_t?*pmd) {if?(!(pmd_val(*pmd)?&?_PAGE_PRESENT))?{pte_t?*page_table?=?(pte_t?*)alloc_low_page();paravirt_alloc_pte(&init_mm,?__pa(page_table)?>>?PAGE_SHIFT);set_pmd(pmd,?__pmd(__pa(page_table)?|?_PAGE_TABLE));BUG_ON(page_table?!=?pte_offset_kernel(pmd,?0));}return?pte_offset_kernel(pmd,?0); }page_table_kmap_check()的實現如下:
#define?PTRS_PER_PTE?512 static?pte_t?*__init?page_table_kmap_check(pte_t?*pte,?pmd_t?*pmd,unsigned?long?vaddr,?pte_t?*lastpte,void?**adr) { #ifdef?CONFIG_HIGHMEM/**?Something?(early?fixmap)?may?already?have?put?a?pte*?page?here,?which?causes?the?page?table?allocation*?to?become?nonlinear.?Attempt?to?fix?it,?and?if?it*?is?still?nonlinear?then?we?have?to?bug.*///得到內核固定映射區的臨時映射區的起始和結束虛擬頁框號int?pmd_idx_kmap_begin?=?fix_to_virt(FIX_KMAP_END)?>>?PMD_SHIFT;int?pmd_idx_kmap_end?=?fix_to_virt(FIX_KMAP_BEGIN)?>>?PMD_SHIFT;if?(pmd_idx_kmap_begin?!=?pmd_idx_kmap_end&&?(vaddr?>>?PMD_SHIFT)?>=?pmd_idx_kmap_begin&&?(vaddr?>>?PMD_SHIFT)?<=?pmd_idx_kmap_end)?{pte_t?*newpte;int?i;BUG_ON(after_bootmem);newpte?=?*adr;//拷貝操作for?(i?=?0;?i?<?PTRS_PER_PTE;?i++)set_pte(newpte?+?i,?pte[i]);*adr?=?(void?*)(((unsigned?long)(*adr))?+?PAGE_SIZE);paravirt_alloc_pte(&init_mm,?__pa(newpte)?>>?PAGE_SHIFT);set_pmd(pmd,?__pmd(__pa(newpte)|_PAGE_TABLE));?//pmd與newpte表進行關聯BUG_ON(newpte?!=?pte_offset_kernel(pmd,?0));__flush_tlb_all();paravirt_release_pte(__pa(pte)?>>?PAGE_SHIFT);pte?=?newpte;}BUG_ON(vaddr?<?fix_to_virt(FIX_KMAP_BEGIN?-?1)&&?vaddr?>?fix_to_virt(FIX_KMAP_END)&&?lastpte?&&?lastpte?+?PTRS_PER_PTE?!=?pte); #endifreturn?pte; }檢查當前頁表初始化的地址是否處于該區間范圍,如果是,則把其 pte 頁表的內容拷貝到 page_table_range_init()的 alloc_low_pages 申請的頁表空間中(這里的拷貝主要是為了保證連續性),并將 newpte 新頁表的地址設置到 pmd 中(32bit 系統實際上就是頁全局目錄),然后調用__flush_tlb_all()刷新 TLB 緩存;如果不是該區間,則僅是由入參中調用的 one_page_table_init()被分配到了頁表空間。
至此高端內存的固定映射區的頁表分配完成,后面的 paging_init()會負責完成剩下的頁表建立工作。
Linux 內存管理框架
傳統的多核運算是使用 SMP(Symmetric Multi-Processor )模式:將多個處理器與一個集中的存儲器和 I/O 總線相連,所有處理器訪問同一個物理存儲器,因此 SMP 系統有時也被稱為一致存儲器訪問(UMA)結構體系,即無論在什么時候,處理器只能為內存的每個數據保持或共享唯一一個數值。
而NUMA模式是一種分布式存儲器訪問方式,處理器可以同時訪問不同的存儲器地址,大幅度提高并行性。NUMA 模式下系統的每個 CPU 都有本地內存,可支持快速訪問,各個處理器之間通過總線連接起來,以支持對其它 CPU 本地內存的訪問,但是這些訪問要比處理器本地內存的慢.
Linux 內核通過插入一些兼容層,使兩個不同體系結構的差異被隱藏,兩種模式都使用了同一個數據結構。
在 NUMA 模式下,處理器和內存塊被劃分成多個"節點"(node),比如機器上有 2 個處理器、4 個內存塊,我們可以把 1 個處理器和 2 個內存塊合起來(GNU/Linux 根據物理 CPU 的數量分配 node,一個物理 CPU 對應一個 node),共同組成一個 NUMA 的節點。每個節點被分配有本地存儲器空間,所有節點中的處理器都可以訪問系統全部的物理存儲器,但是訪問本節點內的存儲器所需要的時間比訪問某些遠程節點內的存儲器所花的時間要少得多。
與 CPU 類似,內存被分割成多個區域 BANK,也叫"簇",依據簇與 CPU 的"距離"的不同,訪問不同簇的方式也會有所不同,CPU 被劃分為多個節點,每個 CPU 對應一個本地物理內存, 一般一個 CPU-node 對應一個內存簇,也就是說每個內存簇被認為是一個節點。
而在 UMA 系統中, 內存就相當于一個只使用一個 NUMA 節點來管理整個系統的內存,這樣在內存管理的其它地方可以認為他們就是在處理一個(偽)NUMA 系統。
內存管理框架初始化
在 linux 中,每個物理內存節點 node 都被劃分為多個內存管理區域用于表示不同范圍的內存,比如上面提到的 NORMAL 內存、高端內存,內核可以使用不同的映射方式映射物理內存。zone 只是內核為了管理方便而做的一種邏輯上的劃分,并不存在這種物理硬件單元。
綜上,linux 的物理內存管理機制將物理內存劃分為三個層次來管理,依次是:Node(存儲節點)、Zone(管理區)和 Page(頁面),它們之間的關系如下:
其中,zone 的類型如下:
include/linux/mmzone.henum?zone_type?{ #ifdef?CONFIG_ZONE_DMAZONE_DMA,?//通常為內存首部16MB,某些工業標準體系結構設備需要用到ZONE_DMA(較舊的外設,只能尋址24位內存) #endif#ifdef?CONFIG_ZONE_DMA32ZONE_DMA32,?//在64位Linux操作系統上,DMA32為16M~4G(可訪問32位),高于4G的內存為Normal?ZONE #endifZONE_NORMAL,?//通常為16MB~896MB,該部分的內存由內核直接映射到物理地址空間的較高部分 #ifdef?CONFIG_HIGHMEMZONE_HIGHMEM,?//通常為896MB~末尾,將保留給系統使用,是系統中預留的可用內存空間,動態映射 #endifZONE_MOVABLE,?//用于減少內存的碎片化,這個區域的頁都是可遷移的#ifdef?CONFIG_ZONE_DEVICEZONE_DEVICE,?//為支持熱插拔設備而分配的非易失性內存 #endif__MAX_NR_ZONES };回到之前的 setup_arch()函數,接著往下走,來到內存管理框架初始化的地方
void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_ram_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內存初始化............early_alloc_pgt_buf();?//頁表緩沖區分配reserve_brk();?//緩沖區加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動......init_mem_mapping();?//低端內存內核頁表初始化?高端內存固定映射區中臨時映射區頁表初始化......initmem_init();?//?<----------------------------------------...... }initmem_init的實現如下:
#define?PHYS_ADDR_MAX?(~(phys_addr_t)0)#ifndef?CONFIG_NEED_MULTIPLE_NODES void?__init?initmem_init(void) { #ifdef?CONFIG_HIGHMEMhighstart_pfn?=?highend_pfn?=?max_pfn;if?(max_pfn?>?max_low_pfn)highstart_pfn?=?max_low_pfn;printk(KERN_NOTICE?"%ldMB?HIGHMEM?available.\n",pages_to_mb(highend_pfn?-?highstart_pfn));high_memory?=?(void?*)?__va(highstart_pfn?*?PAGE_SIZE?-?1)?+?1; #elsehigh_memory?=?(void?*)?__va(max_low_pfn?*?PAGE_SIZE?-?1)?+?1; #endifmemblock_set_node(0,?PHYS_ADDR_MAX,?&memblock.memory,?0);#ifdef?CONFIG_FLATMEMmax_mapnr?=?IS_ENABLED(CONFIG_HIGHMEM)???highend_pfn?:?max_low_pfn; #endif__vmalloc_start_set?=?true;printk(KERN_NOTICE?"%ldMB?LOWMEM?available.\n",pages_to_mb(max_low_pfn));setup_bootmem_allocator(); } #endif?/*?!CONFIG_NEED_MULTIPLE_NODES?*/這個函數將high_memory初始化為低端內存最大頁框max_low_pfn對應的地址大小,接著調用 memblock_set_node,通過 memblock 內存管理器設置 node 節點信息。
int?__init_memblock?memblock_set_node(phys_addr_t?base,?phys_addr_t?size,struct?memblock_type?*type,?int?nid) { #ifdef?CONFIG_NEED_MULTIPLE_NODESint?start_rgn,?end_rgn;int?i,?ret;ret?=?memblock_isolate_range(type,?base,?size,?&start_rgn,?&end_rgn);if?(ret)return?ret;for?(i?=?start_rgn;?i?<?end_rgn;?i++)memblock_set_region_node(&type->regions[i],?nid);memblock_merge_regions(type); #endifreturn?0; }memblock_set_node 主要調用了三個函數:memblock_isolate_range、memblock_set_region_node 和 memblock_merge_regions,首先看memblock_isolate_range()函數:
/*?adjust?*@size?so?that?(@base?+?*@size)?doesn't?overflow,?return?new?size?*/ static?inline?phys_addr_t?memblock_cap_size(phys_addr_t?base,?phys_addr_t?*size) {return?*size?=?min(*size,?PHYS_ADDR_MAX?-?base); }static?int?__init_memblock?memblock_isolate_range(struct?memblock_type?*type,phys_addr_t?base,?phys_addr_t?size,int?*start_rgn,?int?*end_rgn) {phys_addr_t?end?=?base?+?memblock_cap_size(base,?&size);int?idx;struct?memblock_region?*rgn;*start_rgn?=?*end_rgn?=?0;if?(!size)return?0;/*?we'll?create?at?most?two?more?regions?*/while?(type->cnt?+?2?>?type->max)if?(memblock_double_array(type,?base,?size)?<?0)return?-ENOMEM;for_each_memblock_type(idx,?type,?rgn)?{phys_addr_t?rbase?=?rgn->base;phys_addr_t?rend?=?rbase?+?rgn->size;if?(rbase?>=?end)break;if?(rend?<=?base)continue;if?(rbase?<?base)?{/**?@rgn?intersects?from?below.??Split?and?continue*?to?process?the?next?region?-?the?new?top?half.*/rgn->base?=?base;rgn->size?-=?base?-?rbase;type->total_size?-=?base?-?rbase;memblock_insert_region(type,?idx,?rbase,?base?-?rbase,memblock_get_region_node(rgn),rgn->flags);}?else?if?(rend?>?end)?{/**?@rgn?intersects?from?above.??Split?and?redo?the*?current?region?-?the?new?bottom?half.*/rgn->base?=?end;rgn->size?-=?end?-?rbase;type->total_size?-=?end?-?rbase;memblock_insert_region(type,?idx--,?rbase,?end?-?rbase,memblock_get_region_node(rgn),rgn->flags);}?else?{/*?@rgn?is?fully?contained,?record?it?*/if?(!*end_rgn)*start_rgn?=?idx;*end_rgn?=?idx?+?1;}}return?0; }在__memblock_remove()中有提到,memblock_isolate_range()主要作用是將要移除的物理內存區從 reserved 內存區中分離出來,將 start_rgn 和 end_rgn(該內存區塊的起始、結束索引號)返回回去,而這里,由于我們傳入的type 是 memblock.memory,該函數會根據入參 base 和 size 標記節點內存范圍,將該內存從 memory 中劃分開來,同時返回對應的 start_rgn 和 end_rgn。
1)如果 memblock 中的 region 恰好以在該節點內存范圍內的話,那么再未賦值 end_rgn 時將當前 region 的索引記錄至 start_rgn,end_rgn 在此基礎上加 1;
2)如果 memblock 中的 region 跨越了該節點內存末尾分界,那么將會把當前的 region 邊界調整為 node 節點內存范圍邊界,然后通過 memblock_insert_region()函數將剩下的部分(即越出內存范圍的那一塊內存)重新插入 memblock 管理 regions 當中,實現拆分;
static?inline?void?memblock_set_region_node(struct?memblock_region?*r,?int?nid) {r->nid?=?nid; } static?inline?int?memblock_get_region_node(const?struct?memblock_region?*r) {return?r->nid; }static?void?__init_memblock?memblock_insert_region(struct?memblock_type?*type,int?idx,?phys_addr_t?base,phys_addr_t?size,int?nid,?unsigned?long?flags) {struct?memblock_region?*rgn?=?&type->regions[idx];BUG_ON(type->cnt?>=?type->max);memmove(rgn?+?1,?rgn,?(type->cnt?-?idx)?*?sizeof(*rgn));rgn->base?=?base;rgn->size?=?size;rgn->flags?=?flags;memblock_set_region_node(rgn,?nid);type->cnt++;type->total_size?+=?size; }上面的 memmove()將后面的 region 信息往后移,另外調用 memblock_set_region_node()將原 region 的 node 節點號保留在被拆分出來的 region 當中。
回到前面的 memblock_set_node()函數,緊接著 memblock_isolate_range()被調用的是memblock_set_region_node(),通過這個函數把劃分出來的 region 進行 node 節點號設置,而后面的memblock_merge_regions()前面 MEMBLOCK 內存分配器初始化時已經分析過了,是用于將相鄰的 region 進行合并的(節點號、flag 等一致才會合并)。
最后回到 initmem_init()函數中,memblock_set_node()返回后,接著調用的函數為 setup_bootmem_allocator()。
void?__init?setup_bootmem_allocator(void) {printk(KERN_INFO?"??mapped?low?ram:?0?-?%08lx\n",max_pfn_mapped<<PAGE_SHIFT);printk(KERN_INFO?"??low?ram:?0?-?%08lx\n",?max_low_pfn<<PAGE_SHIFT); }原來該函數是用來初始化 bootmem 管理算法的,但現在 x86 的環境已經使用了 memblock 管理算法,所以這里僅作保留,打印部分信息。
bootmem 分配器使用一個 bitmap 來標記物理頁是否被占用,分配的時候按照第一適應的原則,從 bitmap 中進行查找,如果這位為 1,表示已經被占用,否則表示未被占用。bootmem 分配器每次分配內存都會在 bitmap 中進行線性搜索,效率非常低,而且容易在內存中留下許多小的空閑碎片,在需要非常大的內存塊的時候,檢查位圖這一過程就顯得代價很高。bootmem 分配器是用于在啟動階段分配內存的,對該分配器的需求集中于簡單性方面,而不是性能和通用性(和 memblock 管理器一致)。
至此,已完成對內存的節點 node 設置。
回到 setup_arch()函數:
void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_raCm_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內存初始化............early_alloc_pgt_buf();?//頁表緩沖區分配reserve_brk();?//緩沖區加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動......init_mem_mapping();?//低端內存內核頁表初始化?高端內存固定映射區中臨時映射區頁表初始化......initmem_init();?//high_memory(高端內存起始pfn)初始化?通過memblock內存管理器設置node節點信息......x86_init.paging.pagetable_init();?//?<-----------------------------...... }x86_init 結構體內pagetable_init實際上掛接的是native_pagetable_init()函數:
struct?x86_init_ops?x86_init?__initdata?=?{.......paging?=?{.pagetable_init??=?native_pagetable_init,},...... }native_pagetable_init()函數內容如下:
void?__init?native_pagetable_init(void) {unsigned?long?pfn,?va;pgd_t?*pgd,?*base?=?swapper_pg_dir;p4d_t?*p4d;pud_t?*pud;pmd_t?*pmd;pte_t?*pte;//循環?低端內存最大物理頁號~最大物理頁號for?(pfn?=?max_low_pfn;?pfn?<?1<<(32-PAGE_SHIFT);?pfn++)?{va?=?PAGE_OFFSET?+?(pfn<<PAGE_SHIFT);pgd?=?base?+?pgd_index(va);if?(!pgd_present(*pgd))break;p4d?=?p4d_offset(pgd,?va);pud?=?pud_offset(p4d,?va);pmd?=?pmd_offset(pud,?va);if?(!pmd_present(*pmd))break;/*?should?not?be?large?page?here?*/if?(pmd_large(*pmd))?{pr_warn("try?to?clear?pte?for?ram?above?max_low_pfn:?pfn:?%lx?pmd:?%p?pmd?phys:?%lx,?but?pmd?is?big?page?and?is?not?using?pte?!\n",pfn,?pmd,?__pa(pmd));BUG_ON(1);}pte?=?pte_offset_kernel(pmd,?va);if?(!pte_present(*pte))break;printk(KERN_DEBUG?"clearing?pte?for?ram?above?max_low_pfn:?pfn:?%lx?pmd:?%p?pmd?phys:?%lx?pte:?%p?pte?phys:?%lx\n",pfn,?pmd,?__pa(pmd),?pte,?__pa(pte));pte_clear(NULL,?va,?pte);}paravirt_alloc_pmd(&init_mm,?__pa(base)?>>?PAGE_SHIFT);paging_init(); }PAGE_OFFSET 代表的是內核空間和用戶空間對虛擬地址空間的劃分,對不同的體系結構不同。比如在 32 位系統中 3G-4G 屬于內核使用的內存空間,所以 PAGE_OFFSET = 0xC0000000。
該函數的 for 循環主要是用于檢測 max_low_pfn 后面的內核空間內存直接映射空間后面的物理內存是否存在系統啟動引導時創建的頁表(pfn 通過直接映射的方法得到虛擬地址,然后通過內核頁表得到 pgd、pmd、pte),如果存在,則使用 pte_clear()將其清除。
接著看 native_pagetable_init()調用的最后一個函數:paging_init()。
void?__init?paging_init(void) {pagetable_init();__flush_tlb_all();kmap_init();/**?NOTE:?at?this?point?the?bootmem?allocator?is?fully?available.*/olpc_dt_build_devicetree();sparse_init();zone_sizes_init(); }可以對著這個圖看:
前面已經分析過低端內存、固定映射區中臨時映射區的內核頁表的建立,這里 paging_init 將會完成剩下的工作,首先看pagetable_init()
static?void?__init?pagetable_init(void) {pgd_t?*pgd_base?=?swapper_pg_dir;permanent_kmaps_init(pgd_base); }static?void?__init?permanent_kmaps_init(pgd_t?*pgd_base) {unsigned?long?vaddr?=?PKMAP_BASE;page_table_range_init(vaddr,?vaddr?+?PAGE_SIZE*LAST_PKMAP,?pgd_base);pkmap_page_table?=?virt_to_kpte(vaddr); }該函數為建立持久映射區(KMAP 區)的頁表,page_table_range_init 函數前面固定映射區頁表初始化時已經分析過了(初始化 pgd_base 指向的頁全局目錄中 start 到 end 這個范圍的線性地址,整個函數結束后只是初始化好了頁中間目錄項對應的頁表,但是頁表中的頁表項并沒有初始化),這里建立頁表范圍為 PKMAP_BASE 到 PKMAP_BASE + PAGE_SIZE*LAST_PKMAP,建好頁表后將頁表地址賦值給給持久映射區頁表變量 pkmap_page_table。
__flush_tlb_all()為刷新全部 TLB,這里不做介紹,接著看 paging_init()調用的下一個函數 kmap_init()。
static?void?__init?kmap_init(void) {unsigned?long?kmap_vstart;/**?Cache?the?first?kmap?pte:*/kmap_vstart?=?__fix_to_virt(FIX_KMAP_BEGIN);kmap_pte?=?virt_to_kpte(kmap_vstart); }可以很容易看到 kmap_init()主要是獲取到臨時映射區間的起始頁表并往臨時映射頁表變量 kmap_pte 置值。
回到 paging_init(),olpc_dt_build_devicetree 這里就不做介紹了,而sparse_init()則涉及到了 Linux 的內存模型,這里介紹一下 Linux 的三種內存模型,注意,以下都是從 CPU 的角度來看的。
從系統中任意一個 CPU 的角度來看,當它訪問物理內存的時候,物理地址空間是一個連續的、沒有空洞的地址空間,那么這種計算機系統的內存模型就是Flat memory。在這種內存模型下,物理內存的管理比較簡單,每一個物理頁幀都會有一個 page 數據結構來抽象,因此系統中存在一個 struct page 的數組(位于直接映射區,mem_map,在節點 node 里,后面就會介紹到),每一個數組條目指向一個實際的物理頁幀(page frame)。在 flat memory 的情況下,PFN 和 mem_map 數組 index 的關系是線性的(即位于直接映射區,有一個固定偏移),因此從 PFN 到對應的 page 數據結構是非常容易的,反之亦然。
pfn_to_page/page_to_pfn 的作用是 struct page* 和 pfn 頁幀號之間的轉換,flat memory 內存模型的相關代碼如下:
#if?defined(CONFIG_FLATMEM)#define?__pfn_to_page(pfn)?(mem_map?+?((pfn)?-?ARCH_PFN_OFFSET)) #define?__page_to_pfn(page)?((unsigned?long)((page)?-?mem_map)?+?\ARCH_PFN_OFFSET)PFN 和 struct page 數組(mem_map)的 index 是線性關系,有一個固定的偏移就是 ARCH_PFN_OFFSET(跟架構相關的物理起始地址的 PFN)。
如果 cpu 在訪問物理內存的時候,其地址空間有一些空洞,是不連續的,那么這種計算機系統的內存模型就是Discontiguous memory。一般而言,NUMA 架構的計算機系統的 memory model 都是選擇 Discontiguous Memory,不過 NUMA 強調的是 memory 和 processor 的位置關系,和內存模型其實是沒有關系的(NUMA 并沒有規定其內存的連續性,而 Discontiguous memory 系統也并非一定是 NUMA 系統,但是這兩種都是多節點的),只不過,由于同一 node 上的 memory 和 processor 有更緊密的耦合關系(訪問更快),因此需要多個 node 來管理。Discontiguous memory 本質上是 flat memory 內存模型的擴展,整個物理內存的內存空間大部分是成片的大塊內存,中間會有一些空洞,每一個成片的內存地址空間屬于一個 node(如果局限在一個 node 內部,其內存模型是 flat memory)。
在這種模型下,從 PFN 轉換到具體的 struct page 會稍微復雜一點,首先要從 PFN 得到 node ID,然后根據這個 ID 找到對于的節點 node 數據結構,也就找到了對應的 page 數組,之后的方法就類似 flat memory 了。
#elif?defined(CONFIG_DISCONTIGMEM)#define?__pfn_to_page(pfn)???\ ({?unsigned?long?__pfn?=?(pfn);??\unsigned?long?__nid?=?arch_pfn_to_nid(__pfn);??\NODE_DATA(__nid)->node_mem_map?+?arch_local_page_offset(__pfn,?__nid);\ })#define?__page_to_pfn(pg)??????\ ({?const?struct?page?*__pg?=?(pg);?????\struct?pglist_data?*__pgdat?=?NODE_DATA(page_to_nid(__pg));?\(unsigned?long)(__pg?-?__pgdat->node_mem_map)?+???\__pgdat->node_start_pfn;?????\ })Discontiguous memory 模型需要獲取 node id,只要找到 node id,一切都好辦了,后面類比 flat memory model 進行就 OK 了。對于__pfn_to_page 的定義,首先通過 arch_pfn_to_nid 將 PFN 轉換成 node id,通過 NODE_DATA 宏定義可以找到該 node 對應的 pglist_data 數據結構,該數據結構的 node_start_pfn 記錄了該 node 的第一個 pfn,因此,也就可以得到其對應 struct page 在 node_mem_map 的偏移,__page_to_pfn 類似與上述基本類似(pglist_data 數據結構后面再進行介紹)。
內存模型也是一個演進過程,剛開始的時候,使用 flat memory 去抽象一個連續的內存地址空間,出現 NUMA 之后,整個不連續的內存空間被分成若干個 node,每個 node 上是連續的內存地址空間,也就是從原來單一的一個 mem_maps[]變成了若干個 mem_maps[]。而現在 memory hotplug 的出現讓原來完美的設計變得不完美了(熱插拔,即帶電插拔,熱插拔功能就是允許用戶在不關閉系統,不切斷電源的情況下取出和更換損壞的硬盤、電源或板卡等部件。Linux 內核支持熱插拔的部件有 USB 設備、PCI 設備甚至 CPU),因為即便是一個 node 中的 mem_maps[]也有可能是不連續了,因此目前 Discontiguous memory 也逐漸在被 sparse memory 替代。
在sparse memory內存模型下,連續的地址空間按照 SECTION(例如 1G)被分成了一段一段的,其中每一 p 都是 hotplug 的,因此 sparse memory 下,內存地址空間可以被切分的更細。整個連續的物理地址空間是按照一個個p來切斷的,在每一個 p 內部,其 memory 是連續的(即符合 flat memory 的特點),因此,mem_map 的 page 數組依附于 p 結構(struct mem_p),而不是 node 結構(struct pglist_data)。在這個模型下,PFN 轉 struct page 變為了轉換變成了 PFN<--->Section<--->page。
linux 內核中靜態定義了一個 mem_p 的指針數組,一個 p 中往往包括多個 page,因此需要通過 PFN 得到 p 號,用 p 號做為 index 在 mem_p 指針數組可以找到該 PFN 對應的 p 數據結構。實際上PFN 分成兩個部分:一部分是 p index,另外一個部分是 page 在該 p 的偏移,找到 p 之后,沿著其 mem_map 就可以找到對應的 page 數據結構。
對于 page 到 p index 的轉換,sparse memory 有 2 種方案,先看看經典的方案,也就是把 p 號保存在 page->flags 中(page 的結構同樣在后面再進行介紹),這種方法的最大的問題是 page->flags 中的 bit 數不一定夠用,因為這個 flag 中承載了太多的信息,各種 page flag、node id、zone id,現在又增加一個 p id,在不同的處理器架構中無法實現一致性的算法(上面的圖即為采用經典算法的 sparse memory)。
#elif?defined(CONFIG_SPARSEMEM) /**?Note:?p's?mem_map?is?encoded?to?reflect?its?start_pfn.*?p[i].p_mem_map?==?mem_map's?address?-?start_pfn;*/ #define?__page_to_pfn(pg)?????\ ({?const?struct?page?*__pg?=?(pg);????\int?__sec?=?page_to_p(__pg);???\(unsigned?long)(__pg?-?__p_mem_map_addr(__nr_to_p(__sec)));?\ })#define?__pfn_to_page(pfn)????\ ({?unsigned?long?__pfn?=?(pfn);???\struct?mem_p?*__sec?=?__pfn_to_p(__pfn);?\__p_mem_map_addr(__sec)?+?__pfn;??\ }) #endif?/*?CONFIG_FLATMEM/DISCONTIGMEM/SPARSEMEM?*/對于經典的 sparse memory 模型,一個 p 的 struct page 數組所占用的內存來自直接映射區,頁表在初始化的時候就建立好了。但是,對于 SPARSEMEM_VMEMMAP 而言,虛擬地址一開始就分配好了,是 vmemmap 開始的一段連續的虛擬地址空間,但是沒有物理地址。因此,當一個 p 被發現后,可以立刻找到對應的 struct page 的虛擬地址,而這時候還需要分配一個物理的 page frame,對于這種 sparse memory,開銷會稍微大一些,需要建立頁表,關聯 page 跟物理地址。
#elif?defined(CONFIG_SPARSEMEM_VMEMMAP)/*?memmap?is?virtually?contiguous.??*/ #define?__pfn_to_page(pfn)?(vmemmap?+?(pfn)) #define?__page_to_pfn(page)?(unsigned?long)((page)?-?vmemmap)內存管理區 Zone 初始化
回到 paging_init()的最后一個函數調用:zone_sizes_init()。
void?__init?zone_sizes_init(void) {unsigned?long?max_zone_pfns[MAX_NR_ZONES];memset(max_zone_pfns,?0,?sizeof(max_zone_pfns));#ifdef?CONFIG_ZONE_DMAmax_zone_pfns[ZONE_DMA]??=?min(MAX_DMA_PFN,?max_low_pfn); #endif #ifdef?CONFIG_ZONE_DMA32max_zone_pfns[ZONE_DMA32]?=?min(MAX_DMA32_PFN,?max_low_pfn); #endifmax_zone_pfns[ZONE_NORMAL]?=?max_low_pfn; #ifdef?CONFIG_HIGHMEMmax_zone_pfns[ZONE_HIGHMEM]?=?max_pfn; #endiffree_area_init(max_zone_pfns); }這個函數為 zone 的各個內存管理區域最大物理頁號進行初始化,并作為參數調用 free_area_init_nodes(),其中free_area_init_nodes()函數實現如下:
void?__init?free_area_init(unsigned?long?*max_zone_pfn) {unsigned?long?start_pfn,?end_pfn;int?i,?nid,?zone;bool?descending;/*?Record?where?the?zone?boundaries?are?*///全局數組arch_zone_lowest_possible_pfn用來存儲各個內存域可使用的最低內存頁幀編號//全局數組arch_zone_highest_possible_pfn用來存儲各個內存域可使用的最高內存頁幀編號memset(arch_zone_lowest_possible_pfn,?0,sizeof(arch_zone_lowest_possible_pfn));memset(arch_zone_highest_possible_pfn,?0,sizeof(arch_zone_highest_possible_pfn));//用于最低內存區域中可用的編號最小的頁幀,即memblock.memory.regions[0].basestart_pfn?=?find_min_pfn_with_active_regions();//return?falsedescending?=?arch_has_descending_max_zone_pfns();//根據max_zone_pfn和start_pfn初始化arch_zone_lowest_possible_pfn和arch_zone_highest_possible_pfnfor?(i?=?0;?i?<?MAX_NR_ZONES;?i++)?{if?(descending)zone?=?MAX_NR_ZONES?-?i?-?1;elsezone?=?i;//由于ZONE_MOVABLE是一個虛擬內存域,不與真正的硬件內存域關聯,該內存域的邊界總是設置為0if?(zone?==?ZONE_MOVABLE)continue;end_pfn?=?max(max_zone_pfn[zone],?start_pfn);arch_zone_lowest_possible_pfn[zone]?=?start_pfn;arch_zone_highest_possible_pfn[zone]?=?end_pfn;start_pfn?=?end_pfn;}/*?Find?the?PFNs?that?ZONE_MOVABLE?begins?at?in?each?node?*/memset(zone_movable_pfn,?0,?sizeof(zone_movable_pfn));//用于計算ZONE_MOVABLE的內存數量//在內存較多的32位系統上,?這通常會是ZONE_HIGHMEM,?但是對于64位系統,將使用ZONE_NORMAL或ZONE_DMA32,這里計算也比較復雜,感興趣的話可以去看一下源碼,這里便不做介紹了find_zone_movable_pfns_for_nodes();......//各種打印....../*?Initialise?every?node?*///為系統中的每個節點調用free_area_init_node()mminit_verify_pageflags_layout();setup_nr_node_ids();init_unavailable_mem();for_each_online_node(nid)?{pg_data_t?*pgdat?=?NODE_DATA(nid);free_area_init_node(nid);/*?Any?memory?on?that?node?*/if?(pgdat->node_present_pages)node_set_state(nid,?N_MEMORY);check_for_memory(pgdat,?nid);} }free_area_init_node()函數會計算每個節點中每個區域的大小及其空洞的大小,如果兩個相鄰區域之間的最大 PFN 匹配,則可以假定后面的區域為空。例如 arch_max_dma_pfn == arch_max_dma32_pfn,則假定 arch_max_dma32_pfn 該區域為空。
pg_data_t?node_data[MAX_NUMNODES]; EXPORT_SYMBOL(node_data);#ifdef?CONFIG_NUMA extern?struct?pglist_data?*node_data[]; #define?NODE_DATA(nid)?(node_data[nid]) #endif?/*?CONFIG_NUMA?*/static?void?__init?free_area_init_node(int?nid) {//從全局節點數組中獲取一個節點pg_data_t?*pgdat?=?NODE_DATA(nid);unsigned?long?start_pfn?=?0;unsigned?long?end_pfn?=?0;/*?pg_data_t?should?be?reset?to?zero?when?it's?allocated?*/WARN_ON(pgdat->nr_zones?||?pgdat->kswapd_highest_zoneidx);//根據節點id獲取起始pfn和結束pfn,前面node初始化時,memblock處已經設置好節點ID了get_pfn_range_for_nid(nid,?&start_pfn,?&end_pfn);//設置節點ID以及起始pfnpgdat->node_id?=?nid;pgdat->node_start_pfn?=?start_pfn;pgdat->per_cpu_nodestats?=?NULL;pr_info("Initmem?setup?node?%d?[mem?%#018Lx-%#018Lx]\n",?nid,(u64)start_pfn?<<?PAGE_SHIFT,end_pfn???((u64)end_pfn?<<?PAGE_SHIFT)?-?1?:?0);//初始化節點中每個zone的大小和空洞,同時計算節點的spanned_pages和present_pagescalculate_node_totalpages(pgdat,?start_pfn,?end_pfn);alloc_node_mem_map(pgdat);pgdat_set_deferred_range(pgdat);free_area_init_core(pgdat); }在進入 calculate_node_totalpages 之前,這里還是先簡單介紹一下node的數據結構。
struct?zoneref?{struct?zone?*zone;?/*?Pointer?to?actual?zone?*/int?zone_idx;??/*?zone_idx(zoneref->zone)?*/ }; struct?zonelist?{struct?zoneref?_zonerefs[MAX_ZONES_PER_ZONELIST?+?1]; }; typedef?struct?pglist_data?{//本節點的所有zone內存管理區struct?zone?node_zones[MAX_NR_ZONES];//包含了對所有node的zone的引用,通常第一個node_zonelists為本節點自己的zonesstruct?zonelist?node_zonelists[MAX_ZONELISTS];//本節點zone管理區數目int?nr_zones;?/*?number?of?populated?zones?in?this?node?*/ #ifdef?CONFIG_FLAT_NODE_MEM_MAP?/*?means?!SPARSEMEM?*///Discontiguous?Memory內存模型,可以找到節點下的所有pagestruct?page?*node_mem_map; #ifdef?CONFIG_PAGE_EXTENSIONstruct?page_ext?*node_page_ext; #endif #endif......//節點第一個頁的頁號unsigned?long?node_start_pfn;//節點總共的物理頁數,不含空洞unsigned?long?node_present_pages;?/*?total?number?of?physical?pages?*///節點物理頁的范圍大小,含空洞unsigned?long?node_spanned_pages;?/*?total?size?of?physical?pagerange,?including?holes?*/int?node_id;......//內存回收相關的數據結構......ZONE_PADDING(_pad1_)......unsigned?long??flags;ZONE_PADDING(_pad2_)...... }?pg_data_t;ZONE_PADDING 的作用,通過添加大量的填充把經常被訪問的“熱門”數據分到了單獨的 cache line 上,可以理解為空間換時間。
calculate_node_totalpages()實現:
static?void?__init?calculate_node_totalpages(struct?pglist_data?*pgdat,unsigned?long?node_start_pfn,unsigned?long?node_end_pfn) {unsigned?long?realtotalpages?=?0,?totalpages?=?0;enum?zone_type?i;for?(i?=?0;?i?<?MAX_NR_ZONES;?i++)?{struct?zone?*zone?=?pgdat->node_zones?+?i;unsigned?long?zone_start_pfn,?zone_end_pfn;unsigned?long?spanned,?absent;unsigned?long?size,?real_size;spanned?=?zone_spanned_pages_in_node(pgdat->node_id,?i,node_start_pfn,node_end_pfn,&zone_start_pfn,&zone_end_pfn);absent?=?zone_absent_pages_in_node(pgdat->node_id,?i,node_start_pfn,node_end_pfn);size?=?spanned;real_size?=?size?-?absent;if?(size)zone->zone_start_pfn?=?zone_start_pfn;elsezone->zone_start_pfn?=?0;zone->spanned_pages?=?size;zone->present_pages?=?real_size;totalpages?+=?size;realtotalpages?+=?real_size;}pgdat->node_spanned_pages?=?totalpages;pgdat->node_present_pages?=?realtotalpages;printk(KERN_DEBUG?"On?node?%d?totalpages:?%lu\n",?pgdat->node_id,realtotalpages); }其中zone_spanned_pages_in_node():
static?unsigned?long?__init?zone_spanned_pages_in_node(int?nid,unsigned?long?zone_type,unsigned?long?node_start_pfn,unsigned?long?node_end_pfn,unsigned?long?*zone_start_pfn,unsigned?long?*zone_end_pfn) {unsigned?long?zone_low?=?arch_zone_lowest_possible_pfn[zone_type];unsigned?long?zone_high?=?arch_zone_highest_possible_pfn[zone_type];/*?When?hotadd?a?new?node?from?cpu_up(),?the?node?should?be?empty?*/if?(!node_start_pfn?&&?!node_end_pfn)return?0;/*?Get?the?start?and?end?of?the?zone?*///限制zone_start_pfn和zone_end_pfn所在區間*zone_start_pfn?=?clamp(node_start_pfn,?zone_low,?zone_high);*zone_end_pfn?=?clamp(node_end_pfn,?zone_low,?zone_high);adjust_zone_range_for_zone_movable(nid,?zone_type,node_start_pfn,?node_end_pfn,zone_start_pfn,?zone_end_pfn);/*?Check?that?this?node?has?pages?within?the?zone's?required?range?*/if?(*zone_end_pfn?<?node_start_pfn?||?*zone_start_pfn?>?node_end_pfn)return?0;/*?Move?the?zone?boundaries?inside?the?node?if?necessary?*/*zone_end_pfn?=?min(*zone_end_pfn,?node_end_pfn);*zone_start_pfn?=?max(*zone_start_pfn,?node_start_pfn);/*?Return?the?spanned?pages?*/return?*zone_end_pfn?-?*zone_start_pfn; }該函數主要是統計 node 管理節點的內存跨度,該跨度不包括 movable 管理區的(因為 movable 就是在其它內存管理區里分配出來的),里面調用的 adjust_zone_range_for_zone_movable()則是用于剔除 movable 管理區的部分。
另外的zone_absent_pages_in_node()函數:
unsigned?long?__init?__absent_pages_in_range(int?nid,unsigned?long?range_start_pfn,unsigned?long?range_end_pfn) {unsigned?long?nr_absent?=?range_end_pfn?-?range_start_pfn;unsigned?long?start_pfn,?end_pfn;int?i;for_each_mem_pfn_range(i,?nid,?&start_pfn,?&end_pfn,?NULL)?{start_pfn?=?clamp(start_pfn,?range_start_pfn,?range_end_pfn);end_pfn?=?clamp(end_pfn,?range_start_pfn,?range_end_pfn);nr_absent?-=?end_pfn?-?start_pfn;}return?nr_absent; }static?unsigned?long?__init?zone_absent_pages_in_node(int?nid,unsigned?long?zone_type,unsigned?long?node_start_pfn,unsigned?long?node_end_pfn) {unsigned?long?zone_low?=?arch_zone_lowest_possible_pfn[zone_type];unsigned?long?zone_high?=?arch_zone_highest_possible_pfn[zone_type];unsigned?long?zone_start_pfn,?zone_end_pfn;unsigned?long?nr_absent;/*?When?hotadd?a?new?node?from?cpu_up(),?the?node?should?be?empty?*/if?(!node_start_pfn?&&?!node_end_pfn)return?0;zone_start_pfn?=?clamp(node_start_pfn,?zone_low,?zone_high);zone_end_pfn?=?clamp(node_end_pfn,?zone_low,?zone_high);adjust_zone_range_for_zone_movable(nid,?zone_type,node_start_pfn,?node_end_pfn,&zone_start_pfn,?&zone_end_pfn);nr_absent?=?__absent_pages_in_range(nid,?zone_start_pfn,?zone_end_pfn);/**?ZONE_MOVABLE?handling.*?Treat?pages?to?be?ZONE_MOVABLE?in?ZONE_NORMAL?as?absent?pages*?and?vice?versa.*/if?(mirrored_kernelcore?&&?zone_movable_pfn[nid])?{unsigned?long?start_pfn,?end_pfn;struct?memblock_region?*r;for_each_mem_region(r)?{start_pfn?=?clamp(memblock_region_memory_base_pfn(r),zone_start_pfn,?zone_end_pfn);end_pfn?=?clamp(memblock_region_memory_end_pfn(r),zone_start_pfn,?zone_end_pfn);if?(zone_type?==?ZONE_MOVABLE?&&memblock_is_mirror(r))nr_absent?+=?end_pfn?-?start_pfn;if?(zone_type?==?ZONE_NORMAL?&&!memblock_is_mirror(r))nr_absent?+=?end_pfn?-?start_pfn;}}return?nr_absent; }該函數主要用于計算內存空洞頁面數的,計算方法大致為在 zone 區域范圍內,遍歷所有 memblock 的內存塊,將這些內存塊的大小累加,之后兩者做差,zone_absent_pages_in_node 后面是對 ZONE_MOVABLE 的特殊處理了,方法是類似的,這里也不做介紹了。
calculate_node_totalpages()后面就是各種簡單的賦值操作了,這里也簡單介紹一下zone的結構:
其中 MAX_NR_ZONES 是一個節點中所能包容納的 Zones 的最大數。
struct?zone?{......//保留頁框池,記錄每個管理區中必須保留的物理頁面數,以用于緊急狀況下的內存分配long?lowmem_reserve[MAX_NR_ZONES];//保持對UMA的兼容(當做一個節點),NUMA模式下的節點數 #ifdef?CONFIG_NUMAint?node; #endif//該zone的父節點struct?pglist_data?*zone_pgdat;......//該zone的第一個頁的頁號unsigned?long??zone_start_pfn;//伙伴系統管理的page數,這是除去了在初始化階段被申請的頁面(比如memblock)atomic_long_t??managed_pages;//zone大小,含空洞,即zone_end_pfn?-?zone_start_pfnunsigned?long??spanned_pages;//zone實際大小,不含空洞unsigned?long??present_pages;//zone的名稱,如“DMA”“Normal”“Highmem”,這些名稱定義于page_alloc.c的zone_names[MAX_NR_ZONES]const?char??*name;ZONE_PADDING(_pad1_)//包含所有空閑頁面,伙伴系統使用,里面有數量為MIGRATE_TYPES個的free_list鏈表,分別用于管理不同遷移類型的內存頁面struct?free_area?free_area[MAX_ORDER];//描述zone的當前狀態unsigned?long??flags;/*?Primarily?protects?free_area?*///與伙伴算法的碎片遷移算法有關spinlock_t??lock;ZONE_PADDING(_pad2_)......ZONE_PADDING(_pad3_)...... }?____cacheline_internodealigned_in_smp;回到 free_area_init_node()函數,緊接在 calculate_node_totalpages()后的函數調用的為alloc_node_mem_map(),這個函數是用于申請 node 節點的 node_mem_map 相應的內存空間,如果是 sparse memory 內存模型,則該函數實現為空,這里便不做過多介紹了,直接看最后的初始化工作:free_area_init_core()。
static?void?__init?free_area_init_core(struct?pglist_data?*pgdat) {enum?zone_type?j;int?nid?=?pgdat->node_id;//對節點的一些鎖和隊列進行初始化pgdat_init_internals(pgdat);pgdat->per_cpu_nodestats?=?&boot_nodestats;for?(j?=?0;?j?<?MAX_NR_ZONES;?j++)?{struct?zone?*zone?=?pgdat->node_zones?+?j;unsigned?long?size,?freesize,?memmap_pages;unsigned?long?zone_start_pfn?=?zone->zone_start_pfn;size?=?zone->spanned_pages;freesize?=?zone->present_pages;//memmap_pages,每一個4k物理頁都對應一個mem_map_t來管理memmap_pages?=?calc_memmap_size(size,?freesize);if?(!is_highmem_idx(j))?{if?(freesize?>=?memmap_pages)?{freesize?-=?memmap_pages;if?(memmap_pages)printk(KERN_DEBUG"??%s?zone:?%lu?pages?used?for?memmap\n",zone_names[j],?memmap_pages);}?elsepr_warn("??%s?zone:?%lu?pages?exceeds?freesize?%lu\n",zone_names[j],?memmap_pages,?freesize);}//dma保留頁if?(j?==?0?&&?freesize?>?dma_reserve)?{freesize?-=?dma_reserve;printk(KERN_DEBUG?"??%s?zone:?%lu?pages?reserved\n",zone_names[0],?dma_reserve);}//計算nr_kernel_pages(低端內存的頁數)和nr_all_pages的數量if?(!is_highmem_idx(j))nr_kernel_pages?+=?freesize;/*?Charge?for?highmem?memmap?if?there?are?enough?kernel?pages?*///如果有足夠的頁,則也為高端內存提供memmap_pageselse?if?(nr_kernel_pages?>?memmap_pages?*?2)nr_kernel_pages?-=?memmap_pages;nr_all_pages?+=?freesize;/**?Set?an?approximate?value?for?lowmem?here,?it?will?be?adjusted*?when?the?bootmem?allocator?frees?pages?into?the?buddy?system.*?And?all?highmem?pages?will?be?managed?by?the?buddy?system.*///初始化zone使用的各類鎖zone_init_internals(zone,?j,?nid,?freesize);if?(!size)continue;set_pageblock_order();setup_usemap(pgdat,?zone,?zone_start_pfn,?size);init_currently_empty_zone(zone,?zone_start_pfn,?size);memmap_init(size,?nid,?j,?zone_start_pfn);} }該函數主要用于向節點下的每個 zone 填充相關信息,在 for 循環內,循環遍歷統計各個管理區最大跨度間相差的頁面數 size 以及除去內存“空洞”后的實際頁面數 freesize,然后通過 calc_memmap_size()計算出該管理區所需的頁面管理結構占用的頁面數 memmap_pages,最后可以計算得出高端內存外的系統內存共有的內存頁面數(freesize-memmap_pages)。
nr_kernel_pages 用于統計低端內存的頁數,此外循環體內的操作則是初始化內存管理區的管理結構,例如各類鎖的初始化、隊列初始化。其中 set_pageblock_order()用于在 CONFIG_HUGETLB_PAGE_SIZE_VARIABLE 下設置 pageblock_order 的值;setup_usemap()函數則是為了給 zone 管理結構體中的 pageblock_flags 申請內存空間的,pageblock_flags 與伙伴系統的碎片遷移算法有關。init_currently_empty_zone()則主要是初始化管理區的等待隊列哈希表和等待隊列,同時還初始化了與伙伴系統相關的 free_area 列表
nr_kernel_pages、nr_all_pages 和頁面的關系可以參考下圖:
這里以我自己的私服為例,看一下我私服 node 和 zone 的情況
首先是 node 的個數,GNU/Linux 根據物理 CPU 的數量分配 node,因此可以直接查物理 CPU 的數量:
當然,用 numactl 會更加直觀:
我的機器上只有一個 node,接下來可以用 cat /proc/zoneinfo 查看這個 node 下各個 zone 的情況:
回到 free_area_init_core()函數的最后,memmap_init()->memmap_init_zone(),該函數主要是根據 PFN,然后通過 pfn_to_page 找到對應的 struct page 結構,并將該結構進行初始化處理,并設置 MIGRATE_MOVABLE 標志,表明可移動。
//遍歷memblock,找到節點的內存地址范圍,zone的范圍不能大于這個,使用memmap_init_zone對該zone進行處理 void?__meminit?__weak?memmap_init(unsigned?long?size,?int?nid,unsigned?long?zone,unsigned?long?range_start_pfn) {unsigned?long?start_pfn,?end_pfn;unsigned?long?range_end_pfn?=?range_start_pfn?+?size;int?i;for_each_mem_pfn_range(i,?nid,?&start_pfn,?&end_pfn,?NULL)?{start_pfn?=?clamp(start_pfn,?range_start_pfn,?range_end_pfn);end_pfn?=?clamp(end_pfn,?range_start_pfn,?range_end_pfn);if?(end_pfn?>?start_pfn)?{size?=?end_pfn?-?start_pfn;memmap_init_zone(size,?nid,?zone,?start_pfn,MEMINIT_EARLY,?NULL,?MIGRATE_MOVABLE);}} }void?__meminit?memmap_init_zone(unsigned?long?size,?int?nid,?unsigned?long?zone,unsigned?long?start_pfn,enum?meminit_context?context,struct?vmem_altmap?*altmap,?int?migratetype) {unsigned?long?pfn,?end_pfn?=?start_pfn?+?size;struct?page?*page;if?(highest_memmap_pfn?<?end_pfn?-?1)highest_memmap_pfn?=?end_pfn?-?1;#ifdef?CONFIG_ZONE_DEVICE/**?Honor?reservation?requested?by?the?driver?for?this?ZONE_DEVICE*?memory.?We?limit?the?total?number?of?pages?to?initialize?to?just*?those?that?might?contain?the?memory?mapping.?We?will?defer?the*?ZONE_DEVICE?page?initialization?until?after?we?have?released*?the?hotplug?lock.*/if?(zone?==?ZONE_DEVICE)?{if?(!altmap)return;if?(start_pfn?==?altmap->base_pfn)start_pfn?+=?altmap->reserve;end_pfn?=?altmap->base_pfn?+?vmem_altmap_offset(altmap);} #endiffor?(pfn?=?start_pfn;?pfn?<?end_pfn;?)?{/**?There?can?be?holes?in?boot-time?mem_map[]s?handed?to?this*?function.??They?do?not?exist?on?hotplugged?memory.*/if?(context?==?MEMINIT_EARLY)?{if?(overlap_memmap_init(zone,?&pfn))continue;if?(defer_init(nid,?pfn,?end_pfn))break;}page?=?pfn_to_page(pfn);__init_single_page(page,?pfn,?zone,?nid);if?(context?==?MEMINIT_HOTPLUG)__SetPageReserved(page);/**?Usually,?we?want?to?mark?the?pageblock?MIGRATE_MOVABLE,*?such?that?unmovable?allocations?won't?be?scattered?all*?over?the?place?during?system?boot.*/if?(IS_ALIGNED(pfn,?pageblock_nr_pages))?{set_pageblock_migratetype(page,?migratetype);cond_resched();}pfn++;} }struct page
最后這里簡單介紹一下 struct page,內核會為每一個物理頁幀創建一個 struct page 的結構體,因此要保證 page 結構體足夠的小,否則僅 struct page 就要占用大量的內存,該結構有很多 union 結構,主要是用于各種算法不同數據的空間復用。
struct page 這個結構相當復雜,這里我放上網上找到的一個全局參考,可以比源碼更清晰地了解整個結構體,這里我也只簡單介紹里面的幾個字段。
????struct?page?(include/linux/mm_types.h)page+--------------------------------------------------------------+|flags?????????????????????????????????????????????????????????||???(unsigned?long)????????????????????????????????????????????|--+--???????+==============================================================+|?????????|..............................................................||?????????|page?cache?and?anonymous?pages????????????????????????????????||?????????|????+---------------------------------------------------------+|?????????|????|lru??????????????????????????????????????????????????????||?????????|????|????(struct?list_head)???????????????????????????????????||????|mapping??????????????????????????????????????????????????||????|????(struct?address_space*)??????????????????????????????||????|index????????????????????????????????????????????????????||????|????(pgoff_t)????????????????????????????????????????????|5?words??????|????|private??????????????????????????????????????????????????|union???????|????|????(unsigned?long)??????????????????????????????????????||????+---------------------------------------------------------+|..............................................................|has????????|slab,?slob,?slub??????????????????????????????????????????????||????+---------------------------------------------------------+7?usage??????|????|.........................................................||????|????????????????????????????.+---------------------------||????|slab_list???????????????????.|next???????????????????????||????|????(struct?list_head)??????.|???(struct?page*)??????????||????|????????????????????????????.|pages??????????????????????||????|????????????????????????????.|pobjects???????????????????||????|????????????????????????????.|???(int)???????????????????||????|????????????????????????????.+---------------------------||????|.........................................................||????+---------------------------------------------------------+|????|slab_cache???????????????????????????????????????????????||????|????(struct?kmem_cache*)?????????????????????????????????||????|freelist?????????????????????????????????????????????????||????|????(void*)??????????????????????????????????????????????||????+---------------------------------------------------------+|????|.........................................................||????|s_mem????.counters??????????.+---------------------------||????|?(void*)?.?(unsigned?long)??.|inuse??????????????????????||????|?????????.??????????????????.|objects????????????????????||????|?????????.??????????????????.|frozen?????????????????????||????|?????????.??????????????????.|????(unsigned)?????????????||????|?????????.??????????????????.+---------------------------||????|.........................................................||????+---------------------------------------------------------+|..............................................................||Tail?pages?of?compound?page???????????????????????????????????||????+---------------------------------------------------------+|????|compound_head????????????????????????????????????????????||????|????(unsigned?long)??????????????????????????????????????||????|compound_dtor????????????????????????????????????????????||????|compound_order???????????????????????????????????????????||????|????(unsigned?char)??????????????????????????????????????||????|compound_mapcount????????????????????????????????????????||????|????(atomic_t)???????????????????????????????????????????||????+---------------------------------------------------------+|..............................................................||Second?tail?page?of?compound?page?????????????????????????????||????+---------------------------------------------------------+|????|_compound_pad_1??????????????????????????????????????????||????|_compound_pad_2??????????????????????????????????????????||????|????(unsigned?long)??????????????????????????????????????||????|deferred_list????????????????????????????????????????????||????|????(struct?list_head)???????????????????????????????????||????+---------------------------------------------------------+|..............................................................||Page?table?pages??????????????????????????????????????????????||????+---------------------------------------------------------+|????|_pt_pad_1????????????????????????????????????????????????||????|????(unsigned?long)??????????????????????????????????????||????|pmd_huge_pte?????????????????????????????????????????????||????|????(pgtable_t)??????????????????????????????????????????||????|_pt_pad_2????????????????????????????????????????????????||????|????(unsigned?long)??????????????????????????????????????||????|.........................................................||????|pt_mm?????????????????????????.pt_frag_refcount??????????||????|????(struct?mm_struct*)???????.????(atomic_t)????????????||????|.........................................................||????|ptl??????????????????????????????????????????????????????||????|????(spinlock_t/spinlock_t?*)????????????????????????????||????+---------------------------------------------------------+|..............................................................||ZONE_DEVICE?pages?????????????????????????????????????????????||????+---------------------------------------------------------+|????|pgmap????????????????????????????????????????????????????||????|????(struct?dev_pagemap*)????????????????????????????????||????|hmm_data?????????????????????????????????????????????????||????|_zd_pad_1????????????????????????????????????????????????||?????????|????|????(unsigned?long)??????????????????????????????????????||?????????|????+---------------------------------------------------------+|?????????|..............................................................||?????????|rcu_head??????????????????????????????????????????????????????||?????????|????(struct?rcu_head)?????????????????????????????????????????||?????????|..............................................................|--+--???????+==============================================================+|?????????|..............................................................||????????????.?????????????????.????????????????.??????????????|4?bytes?????|_mapcount???.page_type????????.active??????????.units?????????|union??????|??(atomic_t).???(unsigned?int).??(unsigned?int).???(int)??????||????????????.?????????????????.????????????????.??????????????||?????????|..............................................................|--+--???????+==============================================================+|_refcount?????????????????????????????????????????????????????||?????(atomic_t)???????????????????????????????????????????????||mem_cgroup????????????????????????????????????????????????????||?????(struct?mem_cgroup)??????????????????????????????????????||virtual???????????????????????????????????????????????????????||?????(void?*)?????????????????????????????????????????????????||_last_cpupid??????????????????????????????????????????????????||?????(int)????????????????????????????????????????????????????|+--------------------------------------------------------------+首先介紹一下flags,它描述 page 的狀態和其它的一些信息,如下圖。
主要分為 4 部分,其中標志位 flag 向高位增長,其余位字段向低位增長,中間存在空閑位。
p:主要用于 sparse memory 內存模型,即 p 號。
node:NUMA 節點號,標識該 page 屬于哪一個節點。
zone:內存域標志,標識該 page 屬于哪一個 zone。
flag:page 的狀態標識,常用的有:
內核定義了一些標準宏,用于檢查頁面是否設置了某個特定的標志位或者用于操作某些特定的標志位,比如
PageXXX()(檢查是否設置) SetPageXXX() ClearPageXXX()伙伴系統,內存被分成含有很多頁面的大塊,每一塊都是 2 個頁面大小的方冪。如果找不到想要的塊, 一個大塊會被分成兩部分,這兩部分彼此就成為伙伴。其中一半被用來分配,而另一半則空閑。這些塊在以后分配的過程中會繼續被二分直至產生一個所需大小的塊。當一個塊被最終釋放時, 其伙伴將被檢測出來, 如果伙伴也空閑則合并兩者
slab,Slab 對小對象進行分配,不用為每個小對象去分配頁,節省了空間。內核中一些小對象在創建析構時很頻繁,Slab 對這些小對象做緩存,可以重復利用一些相同的對象,減少內存分配次數
接著看struct list_head lru,鏈表頭,具體作用得看 page 處于什么用途中,如果是伙伴系統則用于連接相同的伙伴,通過第一個 page 可以找到伙伴中所有的 page;如果是 slab,page->lru.next 指向 page 駐留的的緩存管理結構,page->lru.prec 指向保存該 page 的 slab 管理結構;而當 page 被用戶態使用或被當做頁緩存使用時,lru 則用于將該 page 連入 zone 中相應的 lru 鏈表,供內存回收時使用
struct address_space *mapping,當 mapping 為 NULL 時,該 page 為交換緩存(swap);當 mapping 不為 NULL 且第 0 位為 0,該 page 為頁緩存或文件映射,mapping 指向文件的地址空間;當 mapping 不為 NULL 且第 0 位為 1,該 page 為匿名頁(匿名映射),mapping 指向 struct anon_vma 對象
pgoff_t index,映射虛擬內存空間里的地址偏移,一個文件可能只映射其中的一部分,假設映射了 1M 的空間,index 指的是在 1M 空間內的偏移,而不是在整個文件內的偏移
unsigned long private,私有數據指針
atomic_t _mapcount,該 page 被頁表映射的次數,即這個 page 被多少個進程共享,初始值為-1(非伙伴系統,如果是伙伴系統則為 PAGE_BUDDY_MAPCOUNT_VALUE),例如只被一個進程的頁表映射的話,值為 0
atomic_t _refcount,頁表引用計數,內核要操作該 page 時,引用計數會+1,操作完成后則-1,當引用計數為 0 時,表示該 page 沒有被引用到,這時候就可以解除該 page 的映射(虛擬頁-物理頁,該物理頁是占用內存的)(用于內存回收)
更詳細的內容可以參考源代碼~
ok,回到 memmap_init_zone(),直接看關鍵函數**__init_single_page**()
static?void?__meminit?__init_single_page(struct?page?*page,?unsigned?long?pfn,unsigned?long?zone,?int?nid) {mm_zero_struct_page(page);?//page初始化,根據page大小還有一些特殊操作set_page_links(page,?zone,?nid,?pfn);?//flags初始化,將頁面映射到zone和nodeinit_page_count(page);?//page的_refcount設置為1page_mapcount_reset(page);?//page的_mapcount設置為-1INIT_LIST_HEAD(&page->lru);?//初始化lru,指向自身...... }至此,free_area_init_node()的初始化操作執行完畢,據前面分析可以知道該函數主要是將整個 linux 物理內存管理框架進行初始化,包括內存管理節點 node、管理區 zone 以及頁面管理 page 等數據的初始化。
回到前面的 free_area_init()函數的循環體內的最后兩個函數 node_set_state()和 check_for_memory(),node_set_state()主要是對 node 節點進行狀態設置,而 check_for_memory()則是做內存檢查。
到這里,內存管理框架的構建基本完畢。
void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_raCm_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內存初始化............early_alloc_pgt_buf();?//頁表緩沖區分配reserve_brk();?//緩沖區加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動......init_mem_mapping();?//低端內存內核頁表初始化?高端內存固定映射區中臨時映射區頁表初始化......initmem_init();?//high_memory初始化?通過memblock內存管理器設置node節點信息......x86_init.paging.pagetable_init();?//?節點node、內存管理區zone、page初始化...... }最后補充一下 pfn 和物理地址以及 pfn 和虛擬地址的轉換。
//物理地址->物理頁 #define?PAGE_SHIFT?_PAGE_SHIFT #define?_PAGE_SHIFT?12 #define?phys_to_pfn(phys)?((phys)?>>?PAGE_SHIFT) #define?pfn_to_phys(pfn)?((pfn)?<<?PAGE_SHIFT)#define?phys_to_page(phys)?pfn_to_page(phys_to_pfn(phys)) #define?page_to_phys(page)?pfn_to_phys(page_to_pfn(page))pfn_to_page、page_to_pfn 可以參考上面的內存模型,不同的模型實現的細節不一樣。
這里可以看出物理頁的大小是 4096,即 4kb,雖然內核在虛擬地址中是在高地址的,但是在物理地址中是從 0 開始的。
在 linux 內核直接映射區里內核邏輯地址與物理頁的轉換關系如下:
#define?pfn_to_virt(pfn)?__va(pfn_to_phys(pfn)) #define?virt_to_pfn(kaddr)?(phys_to_pfn(__pa(kaddr))) #define?virt_to_page(kaddr)?pfn_to_page(virt_to_pfn(kaddr)) #define?page_to_virt(page)?pfn_to_virt(page_to_pfn(page))#define?__pa(x)?????????((unsigned?long)?(x)?-?PAGE_OFFSET) #define?__va(x)?????????((void?*)((unsigned?long)?(x)?+?PAGE_OFFSET))PAGE_OFFSET 與具體的架構有關,在 x86_32 中,PAGE_OFFSET 是 0xC0000000,即 32 位系統中,內核的邏輯地址只有高位的 1GB
總結
Linux 內存管理是一個很復雜的“工程”,Linux 會通過中斷調用獲取被 BIOS 保留的內存地址范圍以及系統可以使用的內存地址范圍。在內核初始化階段,通過 memblock 內存分配器,實現頁分配器初始化之前的內存管理和分配請求,memblock 內存區管理算法將可用可分配的內存用 memblock.memory 進行管理,已分配的內存用 memblock.reserved 進行管理,只要內存塊加入到 memblock.reserved 里面就表示該內存被申請占用了,申請和釋放的操作都集中在 memblock.reserved,這個算法效率不高,但是卻是合理的,因為在內核初始化階段并沒有太多復雜的內存操作場景,而且很多地方都是申請的內存都是永久使用的。為了合理地利用 4G 的內存空間,Linux 采用了 3:1 的策略,即內核占用 1G 的線性地址空間,用戶占用 3G 的線性地址空間,且 Linux 采用了一種折中方案是只對 1G 內核空間的前 896 MB 按線性映射, 剩下的 128 MB 采用動態映射,即走多級頁表翻譯,這樣,內核態能訪問空間就更多了。
傳統的多核運算是使用 SMP(Symmetric Multi-Processor )模式,將多個處理器與一個集中的存儲器和 I/O 總線相連,所有處理器訪問同一個物理存儲器,因此 SMP 系統有時也被稱為一致存儲器訪問(UMA)結構體系。而 NUMA 模式是一種分布式存儲器訪問方式,處理器可以同時訪問不同的存儲器地址,大幅度提高并行性。NUMA 模式下系統的每個 CPU 都有本地內存,可支持快速訪問,各個處理器之間通過總線連接起來,以支持對其它 CPU 本地內存的訪問,但是這些訪問要比處理器本地內存的慢。Linux 內核通過插入一些兼容層,使兩個不同體系結構的差異被隱藏,兩種模式都使用了同一個數據結構,另外 linux 的物理內存管理機制將物理內存劃分為三個層次來管理,依次是:Node(存儲節點)、Zone(管理區)和 Page(頁面)。
Linux 內存管理的內容十分多且復雜,上面介紹到的也只是其中的一部分,如果感興趣的話可以下載一份源代碼,然后細細品味。
參考文獻
Linux 內存管理-Zone:https://blog.csdn.net/wyy4045/article/details/81776277
Linux 內存管理-Node:https://blog.csdn.net/wyy4045/article/details/81708895
內存管理框架:https://www.jeanleo.com/
memblock:https://biscuitos.github.io/blog/MMU-ARM32-MEMBLOCK-index/
Zone_sizes_init:http://www.soolco.com/post/19152_1_1.html
內核頁表:https://www.daimajiaoliu.com/daima/47db35735100402
linux 內存模型:http://www.wowotech.net/memory_management/memory_model.html
linux 中的分頁機制:http://edsionte.com/techblog/archives/3435
linux 內核介紹:https://richardweiyang-2.gitbook.io/kernel-exploring
struct page:https://blog.csdn.net/gatieme/article/details/52384636
頁表初始化:https://www.cnblogs.com/tolimit/p/4585803.html
視頻號最新視頻
總結
以上是生活随笔為你收集整理的一文掌握 Linux 内存管理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 混沌工程将成标配?落地腾讯游戏后带来了哪
- 下一篇: 5月18发布会,这次TDSQL又有什么大