一文掌握 Linux 内存管理
作者:dengxuanshi,騰訊 IEG 后臺(tái)開發(fā)工程師
以下源代碼來自 linux-5.10.3 內(nèi)核代碼,主要以 x86-32 為例。
Linux 內(nèi)存管理是一個(gè)很復(fù)雜的“工程”,它不僅僅是對(duì)物理內(nèi)存的管理,也涉及到虛擬內(nèi)存管理、內(nèi)存交換和內(nèi)存回收等
物理內(nèi)存的探測(cè)
Linux 內(nèi)核通過 detect_memory()函數(shù)實(shí)現(xiàn)對(duì)物理內(nèi)存的探測(cè)
void?detect_memory(void) {detect_memory_e820();detect_memory_e801();detect_memory_88(); }這里主要介紹一下 detect_memory_e820(),detect_memory_e801()和 detect_memory_88()是針對(duì)較老的電腦進(jìn)行兼容而保留的
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()實(shí)現(xiàn)內(nèi)核從 BIOS 那里獲取到內(nèi)存的基礎(chǔ)布局,之所以叫 e820 是因?yàn)閮?nèi)核是通過 0x15 中斷向量,并在 AX 寄存器中指定 0xE820,中斷調(diào)用后將會(huì)返回被 BIOS 保留的內(nèi)存地址范圍以及系統(tǒng)可以使用的內(nèi)存地址范圍,所有通過中斷獲取的數(shù)據(jù)將會(huì)填充在 boot_params.e820_table 中,具體 0xE820 的詳細(xì)用法感興趣的話可以上網(wǎng)查……這里獲取到的 e820_table 里的數(shù)據(jù)是未經(jīng)過整理,linux 會(huì)通過 setup_memory_map 去整理這些數(shù)據(jù)
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(),會(huì)將 boot_params.e820_table 轉(zhuǎn)換為內(nèi)核自己使用的 e820_table,轉(zhuǎn)換之后的e820 表記錄著所有物理內(nèi)存的起始地址、長(zhǎng)度以及類型,然后通過 memcpy 將 e820_table 復(fù)制到 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; }內(nèi)核使用的e820_table 結(jié)構(gòu)描述如下:
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 內(nèi)存分配器
linux x86 內(nèi)存映射主要存在兩種方式:段式映射和頁(yè)式映射。linux 首次進(jìn)入保護(hù)模式時(shí)會(huì)用到段式映射(加電時(shí),運(yùn)行在實(shí)模式,任意內(nèi)存地址都能執(zhí)行代碼,可以被讀寫,這非常不安全,CPU 為了提供限制/禁止的手段,提出了保護(hù)模式),根據(jù)段寄存器(以 8086 為例,段寄存器有 CS(Code Segment):代碼段寄存器;DS(Data Segment):數(shù)據(jù)段寄存器;SS(Stack Segment):堆棧段寄存器;ES(Extra Segment):附加段寄存器)查找到對(duì)應(yīng)的段描述符(這里其實(shí)就是用到了段描述符表,即段表),段描述符指明了此時(shí)的環(huán)境可以通過段訪問到內(nèi)存基地址、空間大小和訪問權(quán)限。訪問權(quán)限則點(diǎn)明了哪些內(nèi)存可讀、哪些內(nèi)存可寫。
typedef?struct?Descriptor{unsigned?int?base;??//?段基址unsigned?int?limit;?//?段大小unsigned?short?attribute;???//?段屬性、權(quán)限 }linux 在段描述符表準(zhǔn)備完成之后會(huì)通過匯編跳轉(zhuǎn)到保護(hù)模式
事實(shí)上,在上面這個(gè)過程中,linux 并沒有明顯地去區(qū)分每個(gè)段,所以這里并沒有很好地起到保護(hù)作用,linux 最終使用的還是內(nèi)存分頁(yè)管理(開啟頁(yè)式映射可以參考/arch/x86/kernel/head_32.S)
memblock 算法
memblock 是 linux 內(nèi)核初始化階段使用的一個(gè)內(nèi)存分配器,實(shí)現(xiàn)較為簡(jiǎn)單,負(fù)責(zé)頁(yè)分配器初始化之前的內(nèi)存管理和分配請(qǐng)求,memblock 的結(jié)構(gòu)如下
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:用來表示分配器分配內(nèi)存是自低地址向高地址還是自高地址向低地址
current_limit:用來表示用來限制 memblock_alloc()和 memblock_alloc_base()的內(nèi)存申請(qǐng)
memory:用于指向系統(tǒng)可用物理內(nèi)存區(qū),這個(gè)內(nèi)存區(qū)維護(hù)著系統(tǒng)所有可用的物理內(nèi)存,即系統(tǒng) DRAM 對(duì)應(yīng)的物理內(nèi)存
reserved:用于指向系統(tǒng)預(yù)留區(qū),也就是這個(gè)內(nèi)存區(qū)的內(nèi)存已經(jīng)分配,在釋放之前不能再次分配這個(gè)區(qū)內(nèi)的內(nèi)存區(qū)塊
memblock_type中的cnt用于描述該類型內(nèi)存區(qū)中的內(nèi)存區(qū)塊數(shù),這有利于 MEMBLOCK 內(nèi)存分配器動(dòng)態(tài)地知道某種類型的內(nèi)存區(qū)還有多少個(gè)內(nèi)存區(qū)塊
memblock_type 中的max用于描述該類型內(nèi)存區(qū)最大可以含有多少個(gè)內(nèi)存區(qū)塊,當(dāng)往某種類型的內(nèi)存區(qū)添加 內(nèi)存區(qū)塊的時(shí)候,如果內(nèi)存區(qū)的內(nèi)存區(qū)塊數(shù)超過 max 成員,那么 memblock 內(nèi)存分配器就會(huì)增加內(nèi)存區(qū)的容量,以此維護(hù)更多的內(nèi)存區(qū)塊
memblock_type 中的total_size用于統(tǒng)計(jì)內(nèi)存區(qū)總共含有的物理內(nèi)存數(shù)
memblock_type 中的regions是一個(gè)內(nèi)存區(qū)塊鏈表,用于維護(hù)屬于這類型的所有內(nèi)存區(qū)塊(包括基址、大小和內(nèi)存塊標(biāo)記等),
name :用于指明這個(gè)內(nèi)存區(qū)的名字,MEMBLOCK 分配器目前支持的內(nèi)存區(qū)名字有:“memory”, “reserved”, “physmem”
具體關(guān)系可以參考下圖:
內(nèi)核啟動(dòng)后會(huì)為 MEMBLOCK 內(nèi)存分配器創(chuàng)建了一些私有的 p,這些 p 用于存放于 MEMBLOCK 分配器有關(guān)的函數(shù)和數(shù)據(jù),即 init_memblock 和 initdata_memblock。在創(chuàng)建完 init_memblock p 和 initdata_memblock p 之后,memblock 分配器會(huì)開始創(chuàng)建 struct memblock 實(shí)例,這個(gè)實(shí)例此時(shí)作為最原始的 MEMBLOCK 分配器,描述了系統(tǒng)的物理內(nèi)存的初始值
#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, };內(nèi)核在 setup_arch(char **cmdline_p)中會(huì)調(diào)用e820__memblock_setup()對(duì) MEMBLOCK 內(nèi)存分配器進(jìn)行初始化啟動(dòng)
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 則是用于循環(huán)遍歷 e820 的內(nèi)存布局信息,然后進(jìn)行 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(),且它操作對(duì)象是 memblock.memory,即系統(tǒng)可用物理內(nèi)存區(qū)。
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;//調(diào)整size大小,確保不會(huì)越過邊界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 算法管理的內(nèi)存為空時(shí),將當(dāng)前空間添加進(jìn)去
不為空的情況下,for_each_memblock_type(idx, type, rgn)這個(gè)循環(huán)會(huì)檢查是否存在內(nèi)存重疊的情況,如果有的話,則剔除重疊部分,然后將其余非重疊的部分插入 memblock。內(nèi)存塊的添加主要分為四種情況(其余情況與這幾種類似),可以參考下圖:
如果 region 空間不夠,則通過 memblock_double_array()添加新的空間,然后重試。
最后 memblock_merge_regions()會(huì)將緊挨著的內(nèi)存進(jìn)行合并(節(jié)點(diǎn)號(hào)、flag 等必須一致,節(jié)點(diǎn)號(hào)后面再進(jìn)行介紹)。
memblock 內(nèi)存分配與回收
到這里,memblock 內(nèi)存管理的初始化基本完成,后面還有一些關(guān)于 memblock.memory 的修正,這里就不做介紹了,最后也簡(jiǎn)單介紹一下memblock 的內(nèi)存分配和回收,即 memblock_alloc()和 memblock_free()。
//size即分配區(qū)塊的大小,align用于字節(jié)對(duì)齊,表示分配區(qū)塊的對(duì)齊大小 //這里NUMA_NO_NODE指任何節(jié)點(diǎn)(沒有節(jié)點(diǎn)),關(guān)于節(jié)點(diǎn)后面會(huì)介紹,這里節(jié)點(diǎn)還沒初始化 //MEMBLOCK_ALLOC_ACCESSIBLE指分配內(nèi)存塊時(shí)僅受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 返回的是分配到的內(nèi)存塊的虛擬地址,為 NULL 表示分配失敗,關(guān)于 phys_to_virt 的實(shí)現(xiàn)后面再介紹,這里主要看 memblock_alloc_range_nid 的實(shí)現(xiàn)。
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 是一個(gè)檢查內(nèi)存泄漏的工具,這里就不做介紹了。
memblock_alloc_range_nid()首先對(duì) align 參數(shù)進(jìn)行檢測(cè),如果為零,則警告。接著函數(shù)調(diào)用 memblock_find_in_range_node() 函數(shù)從可用內(nèi)存區(qū)中找一塊大小為 size 的物理內(nèi)存區(qū)塊, 然后調(diào)用 memblock_reseve() 函數(shù)在找到的情況下,將這塊物理內(nèi)存區(qū)塊加入到預(yù)留區(qū)內(nèi)。
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 分配器分配內(nèi)存是支持自低地址向高地址和自高地址向低地址的,如果 memblock_bottom_up() 函數(shù)返回 true,表示 MEMBLOCK 從低向上分配,而前面初始化的時(shí)候這個(gè)返回值其實(shí)是 false(某些情況下不一定),所以這里主要看__memblock_find_range_top_down 的實(shí)現(xiàn)(__memblock_find_range_bottom_up 的實(shí)現(xiàn)是類似的)。
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 函數(shù)可以將隨機(jī)變化的數(shù)值限制在一個(gè)給定的區(qū)間[min, max]內(nèi)。
__memblock_find_range_top_down()通過使用 for_each_free_mem_range_reverse 宏封裝調(diào)用__next_free_mem_range_rev()函數(shù),該函數(shù)逐一將 memblock.memory 里面的內(nèi)存塊信息提取出來與 memblock.reserved 的各項(xiàng)信息進(jìn)行檢驗(yàn),確保返回的 this_start 和 this_end 不會(huì)與 reserved 的內(nèi)存存在交叉重疊的情況。判斷大小是否滿足,滿足的情況下,將自末端向前(因?yàn)檫@是 top-down 申請(qǐng)方式)的 size 大小的空間的起始地址(前提該地址不會(huì)超出 this_start)返回回去,至此滿足要求的內(nèi)存塊找到了。
最后看一下memblock_free的實(shí)現(xiàn):
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);} }其主要功能是將指定下標(biāo)索引的內(nèi)存項(xiàng)從 memblock.reserved 中移除。
在__memblock_remove()里面,memblock_isolate_range()主要作用是將要移除的物理內(nèi)存區(qū)從 reserved 內(nèi)存區(qū)中分離出來,將 start_rgn 和 end_rgn(該內(nèi)存區(qū)塊的起始、結(jié)束索引號(hào))返回回去。
memblock_isolate_range()返回后,接著調(diào)用 memblock_remove_region() 函數(shù)將這些索引對(duì)應(yīng)的內(nèi)存區(qū)塊從內(nèi)存區(qū)中移除,這里具體做法為調(diào)用 memmove 函數(shù)將 r 索引之后的內(nèi)存區(qū)塊全部往前挪一個(gè)位置,這樣 r 索引對(duì)應(yīng)的內(nèi)存區(qū)塊就被移除了,如果移除之后,內(nèi)存區(qū)不含有任何內(nèi)存區(qū)塊,那么就初始化該內(nèi)存區(qū)。
memblock 內(nèi)存管理總結(jié)
memblock 內(nèi)存區(qū)管理算法將可用可分配的內(nèi)存用 memblock.memory 進(jìn)行管理,已分配的內(nèi)存用 memblock.reserved 進(jìn)行管理,只要內(nèi)存塊加入到 memblock.reserved 里面就表示該內(nèi)存被申請(qǐng)占用了,另外,在內(nèi)存申請(qǐng)的時(shí)候,memblock 僅是把被申請(qǐng)到的內(nèi)存加入到 memblock.reserved 中,而沒有對(duì) memblock.memory 進(jìn)行任何刪除或改動(dòng)操作,因此申請(qǐng)和釋放的操作都集中在 memblock.reserved。這個(gè)算法效率不高,但是卻是合理的,因?yàn)樵趦?nèi)核初始化階段并沒有太多復(fù)雜的內(nèi)存操作場(chǎng)景,而且很多地方都是申請(qǐng)的內(nèi)存都是永久使用的。
內(nèi)核頁(yè)表
上面有提到,內(nèi)核會(huì)通過/arch/x86/kernel/head_32.S 開啟頁(yè)式映射,不過這里建立的頁(yè)表只是臨時(shí)頁(yè)表,而內(nèi)核真正需要建立的是內(nèi)核頁(yè)表。
內(nèi)核虛擬地址空間與用戶虛擬地址空間
為了合理地利用 4G 的內(nèi)存空間,Linux 采用了 3:1 的策略,即內(nèi)核占用 1G 的線性地址空間,用戶占用 3G 的線性地址空間,由于歷史原因,用戶進(jìn)程的地址范圍從 0~3G,內(nèi)核地址范圍從 3G~4G,而內(nèi)核的那 1GB 地址空間又稱為內(nèi)核虛擬地址(邏輯地址)空間,雖然內(nèi)核在虛擬地址中是在高地址的,但是在物理地址中是從 0 開始的。
內(nèi)核虛擬地址與用戶虛擬地址,這兩者都是虛擬地址,都需要經(jīng)過 MMU 的翻譯轉(zhuǎn)換為物理地址,從硬件層面上來看,所謂的內(nèi)核虛擬地址和用戶虛擬地址只是權(quán)限不一樣而已,但在軟件層面上看,就大不相同了,當(dāng)進(jìn)程需要知道一個(gè)用戶空間虛擬地址對(duì)應(yīng)的物理地址時(shí),linux 內(nèi)核需要通過頁(yè)表來得到它的物理地址,而內(nèi)核空間虛擬地址是所有進(jìn)程共享的,也就是說,內(nèi)核在初始化時(shí),就可以創(chuàng)建內(nèi)核虛擬地址空間的映射,并且這里的映射就是線性映射,它基本等同于物理地址,只是它們之間有一個(gè)固定的偏移,當(dāng)內(nèi)核需要獲取該物理地址時(shí),可以繞開頁(yè)表翻譯直接通過偏移計(jì)算拿到,當(dāng)然這是從軟件層面上來看的,當(dāng)內(nèi)核去訪問該頁(yè)時(shí), 硬件層面仍然走的是 MMU 翻譯的全過程。
至于為什么用戶虛擬地址空間不能也像內(nèi)核虛擬地址空間這么做,原因是用戶地址空間是隨進(jìn)程創(chuàng)建才產(chǎn)生的,無法事先給它分配一塊連續(xù)的內(nèi)存
內(nèi)核通過內(nèi)核虛擬地址可以直接訪問到對(duì)應(yīng)的物理地址,那內(nèi)核如何使用其它的用戶虛擬地址(0~3G)?
Linux 采用的一種折中方案是只對(duì) 1G 內(nèi)核空間的前 896 MB 按線性映射, 剩下的 128 MB 采用動(dòng)態(tài)映射,即走多級(jí)頁(yè)表翻譯,這樣,內(nèi)核態(tài)能訪問空間就更多了。這里 linux 內(nèi)核把這 896M 的空間稱為 NORMAL 內(nèi)存,剩下的 128M 稱為高端內(nèi)存,即 highmem。在 64 位處理器上,內(nèi)核空間大大增加,所以也就不需要高端內(nèi)存了,但是仍然保留了動(dòng)態(tài)映射。
動(dòng)態(tài)映射不全是為了內(nèi)核空間可以訪問更多的物理內(nèi)存,還有一個(gè)重要原因:如果內(nèi)核空間全線性映射,那么很可能就會(huì)出現(xiàn)內(nèi)核空間碎片化而滿足不了很多連續(xù)頁(yè)面分配的需求(這類似于內(nèi)存分段與內(nèi)存分頁(yè))。因此內(nèi)核空間也必須有一部分是非線性映射,從而在這碎片化物理地址空間上,用頁(yè)表構(gòu)造出連續(xù)虛擬地址空間(虛擬地址連續(xù)、物理地址不連續(xù)),這就是所謂的 vmalloc 空間。
到這里,可以大致知道linux 虛擬內(nèi)存的構(gòu)造:
linux 內(nèi)存分頁(yè)
linux 內(nèi)核主要是通過內(nèi)存分頁(yè)來管理內(nèi)存的,這里先介紹兩個(gè)重要的變量:max_pfn 和 max_low_pfn。max_pfn 為最大物理內(nèi)存頁(yè)面幀號(hào),max_low_pfn 為低端內(nèi)存區(qū)的最大可用頁(yè)幀號(hào),它們的初始化如下:
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 的實(shí)現(xiàn)如下,其中 E820_TYPE_RAM 代表可用物理內(nèi)存類型
#define?PAGE_SHIFT??12#ifdef?CONFIG_X86_32 #?ifdef?CONFIG_X86_PAE #??define?MAX_ARCH_PFN??(1ULL<<(36-PAGE_SHIFT)) #?else //32位系統(tǒng),1<<20,即0x100000,代表4G物理內(nèi)存的最大頁(yè)面幀號(hào) #??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其實(shí)就是遍歷e820_table,得到內(nèi)存塊的起始地址以及內(nèi)存塊大小,將起始地址右移 PAGE_SHIFT,算出其起始地址對(duì)應(yīng)的頁(yè)面幀號(hào),同時(shí)根據(jù)內(nèi)存塊大小可以算出結(jié)束地址的頁(yè)號(hào),如果結(jié)束頁(yè)號(hào)大于 limit_pfn,則設(shè)置該頁(yè)號(hào)為為 limit_pfn,然后通過比較得到一個(gè) last_pfn,即系統(tǒng)真正的最大物理頁(yè)號(hào)。
max_low_pfn的計(jì)算則調(diào)用到了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 的最后一個(gè)物理頁(yè)號(hào),PFN_UP(x)是用來返回大于 x 的第一個(gè)物理頁(yè)號(hào),這里 x 即物理地址,而 PFN_PHYS(x)返回的是物理頁(yè)號(hào) x 對(duì)應(yīng)的物理地址。
__pa 其實(shí)就是通過虛擬地址計(jì)算出物理地址,這一塊后面再做講解。
將 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) //進(jìn)一步展開 #define?MAXMEM??????????????__pa(CONSISTENT_BASE?-?PAGE_SIZE?–?PAGE_OFFSET?–?SZ_128M) //再進(jìn)一步展開 #define?MAXMEM??????????????__pa((PKMAP_BASE)?-?(SZ_2M)?-?PAGE_SIZE?–?PAGE_OFFSET?–?SZ_128M)下面這一部分就涉及到高端內(nèi)存的構(gòu)成了,其中PKMAP_BASE 是持久映射空間(KMAP 空間,持久映射區(qū))的起始地址,LAST_PKMAP 則是持久映射空間的映射頁(yè)面數(shù),而 FIXADDR_TOP 是固定映射區(qū)(臨時(shí)內(nèi)核映射區(qū))的末尾,FIXADDR_START 是固定映射區(qū)起始地址,其中的__end_of_permanent_fixed_addresses 是固定映射的一個(gè)標(biāo)志(一個(gè)枚舉值,具體可以參考\arch\x86\include\asm\fixmap.h 里的 enum fixed_addresses)。最后的 VMALLOC_END 即為動(dòng)態(tài)映射區(qū)的末尾。
//臨時(shí)映射 //-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)//持久內(nèi)核映射 #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)//動(dòng)態(tài)映射 //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 代表的是內(nèi)核空間和用戶空間對(duì)虛擬地址空間的劃分,對(duì)不同的體系結(jié)構(gòu)不同。比如在 32 位系統(tǒng)中 3G-4G 屬于內(nèi)核使用的內(nèi)存空間,所以 PAGE_OFFSET = 0xC0000000
內(nèi)核空間如上圖,可分為直接內(nèi)存映射區(qū)和高端內(nèi)存映射區(qū),其中直接內(nèi)存映射區(qū)是指 3G 到 3G+896M 的線性空間,直接對(duì)應(yīng)物理地址就是 0 到 896M(前提是有超過 896M 的物理內(nèi)存),其中 896M 是 high_memory 值,使用 kmalloc()/kfree()接口申請(qǐng)釋放內(nèi)存;而高端內(nèi)存映射區(qū)則是超過 896M 物理內(nèi)存的空間,它又分為動(dòng)態(tài)映射區(qū)、持久映射區(qū)和固定映射區(qū)。
動(dòng)態(tài)內(nèi)存映射區(qū),又稱之為 vmalloc 映射區(qū)或非連續(xù)映射區(qū),是指 VMALLOC_START 到 VMALLOC_END 的地址空間,申請(qǐng)釋放內(nèi)存的接口是 vmalloc()/vfree(),通常用于將非連續(xù)的物理內(nèi)存映射為連續(xù)的線性地址內(nèi)存空間;
而持久映射區(qū),又稱之為 KMAP 區(qū)或永久映射區(qū),是指自 PKMAP_BASE 開始共 LAST_PKMAP 個(gè)頁(yè)面大小的空間,操作接口是 kmap()/kunmap(),用于將高端內(nèi)存長(zhǎng)久映射到內(nèi)存虛擬地址空間中;
最后的固定映射區(qū),有時(shí)候也稱為臨時(shí)映射區(qū),是指 FIXADDR_START 到 FIXADDR_TOP 的地址空間,操作接口是 kmap_atomic()/kummap_atomic(),用于解決持久映射不能用于中斷處理程序而增加的臨時(shí)內(nèi)核映射。
上面的MAXMEM_PFN 其實(shí)就是用來判斷是否初始化(啟用)高端內(nèi)存,當(dāng)內(nèi)存物理頁(yè)數(shù)本來就小于低端內(nèi)存的最大物理頁(yè)數(shù)時(shí),就沒有高端地址映射。
這里接著看 max_low_pfn 的初始化,進(jìn)入 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 的主要工作其實(shí)就是把 max_low_pfn 設(shè)置為 MAXMEM_PFN,將 highmem_pages 設(shè)置為 max_pfn – MAXMEM_PFN,至此,max_pfn 和 max_low_pfn 初始化完畢。
低端內(nèi)存初始化
回到 setup_arch 函數(shù):
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()即申請(qǐng)頁(yè)表緩沖區(qū)
#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:標(biāo)識(shí)該緩沖空間的起始物理內(nèi)存頁(yè)框號(hào);
pgt_buf_end:初始化時(shí)和 pgt_buf_start 是同一個(gè)值,但是它是用于表示該空間未被申請(qǐng)使用的空間起始頁(yè)框號(hào);
pgt_buf_top:則是用來表示緩沖空間的末尾,存放的是該末尾的頁(yè)框號(hào)
INIT_PGT_BUF_SIZE 即 24KB,這里直接看最關(guān)鍵的部分: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()函數(shù)是內(nèi)核標(biāo)記 bug、提供斷言并輸出信息的常用手段
__brk_base 相關(guān)的初始化可以參考 arch\x86\kernel\vmlinux.lds.S
在 setup_arch()中,緊接著 early_alloc_pgt_buf()還有 reserve_brk()函數(shù)
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; }這個(gè)地方主要是將 early_alloc_pgt_buf()申請(qǐng)的空間在 membloc 中做 reserved 保留操作,避免被其它地方申請(qǐng)使用而引發(fā)異常
回到 setup_arch 函數(shù):
void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_ram_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內(nèi)存初始化............early_alloc_pgt_buf();?//頁(yè)表緩沖區(qū)分配reserve_brk();?//緩沖區(qū)加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動(dòng)......init_mem_mapping();?//<-----------------------------...... }init_mem_mapping()即低端內(nèi)存內(nèi)核頁(yè)表初始化的關(guān)鍵函數(shù)
#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()主要作用是初始化直接映射變量(直接映射區(qū)相關(guān))以及根據(jù)配置來控制 CR4 寄存器的置位,用于后面分頁(yè)時(shí)頁(yè)面大小的判定。
上面 init_memory_mapping 的參數(shù) ISA_END_ADDRESS 表示 ISA 總線上設(shè)備的末尾地址。
init_memory_mapping(0, ISA_END_ADDRESS, PAGE_KERNEL)初始化 0 ~ 1MB 的物理地址,一般內(nèi)核啟動(dòng)時(shí)被安裝在 1MB 開始處,這里初始化完成之后會(huì)調(diào)用到 memory_map_bottom_up 或者 memory_map_top_down,后面就是初始化 1MB ~ 內(nèi)核結(jié)束地址 這塊物理地址區(qū)域 ,最后也會(huì)回歸到 init_memory_mapping 的調(diào)用,因此這里不做過多的介紹,直接看 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,該結(jié)構(gòu)是用來保存內(nèi)存段信息,其包含了一個(gè)段的起始地址、結(jié)束地址,以及該段是按多大的頁(yè)面進(jìn)行分頁(yè)(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;//返回小于...的最后一個(gè)物理頁(yè)號(hào)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 用于計(jì)算由頁(yè)中間目錄的一個(gè)單獨(dú)表項(xiàng)所映射的區(qū)域大小,也就是一個(gè)頁(yè)表的大小。
split_mem_range()根據(jù)傳入的內(nèi)存 start 和 end 做四舍五入的對(duì)齊操作(即 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 宏依靠整數(shù)除法來完成這項(xiàng)工作,僅當(dāng)兩個(gè)參數(shù)均為整數(shù)時(shí),它才有效,x 是需要四舍五入的數(shù)字,y 是應(yīng)該四舍五入的間隔,也就是說,round_up(12,5)應(yīng)返回 15,因?yàn)?15 是大于 12 的 5 的第一個(gè)間隔,而 round_down(12,5)應(yīng)返回 10。
split_mem_range()會(huì)根據(jù)對(duì)齊的情況,把開始、末尾的不對(duì)齊部分及中間部分分成了三段,使用 save_mr()將其存放在 init_mem_mapping()的局部變量數(shù)組 mr 中。劃分開來主要是為了讓各部分可以映射到不同大小的頁(yè)面,最后如果相鄰兩部分映射頁(yè)面的大小是一致的,則將其合并。
可以通過 dmesg 得到劃分的情況(以下是我私服的劃分情況,不過是 64 位的……)
初始化完內(nèi)存段信息 mr 之后,就行了進(jìn)入到kernel_physical_mapping_init,這個(gè)函數(shù)是建立內(nèi)核頁(yè)表的最為關(guān)鍵的一步,負(fù)責(zé)處理物理內(nèi)存的映射。
在 2.6.11 后,Linux 采用四級(jí)分頁(yè)模型,這四級(jí)頁(yè)目錄分別為:
頁(yè)全局目錄(Page Global Directory)
頁(yè)上級(jí)目錄(Page Upper Directory)
頁(yè)中間目錄(Page Middle Directory)
頁(yè)表(Page Table)
對(duì)于沒有啟動(dòng) PAE(物理地址擴(kuò)展)的 32 位系統(tǒng),Linux 雖然也采用四級(jí)分頁(yè)模型,但本質(zhì)上只用到了兩級(jí)分頁(yè),Linux 通過將"頁(yè)上級(jí)目錄"位域和“頁(yè)中間目錄”位域全為 0 來達(dá)到使用兩級(jí)分頁(yè)的目的,但為了保證程序能 32 位和 64 系統(tǒng)上都能運(yùn)行,內(nèi)核保留了頁(yè)上級(jí)目錄和頁(yè)中間目錄在指針序列中的位置,它們的頁(yè)目錄數(shù)都被內(nèi)核置為 1,并把這 2 個(gè)頁(yè)目錄項(xiàng)映射到適合的全局目錄項(xiàng)。
開啟 PAE 后,32 位系統(tǒng)尋址方式將大大改變,這時(shí)候使用的是三級(jí)頁(yè)表,即頁(yè)上級(jí)目錄其實(shí)沒有真正用到。
這里不考慮 PAE
PAGE_OFFSET 代表的是內(nèi)核空間和用戶空間對(duì)虛擬地址空間的劃分,對(duì)不同的體系結(jié)構(gòu)不同。比如在 32 位系統(tǒng)中 3G-4G 屬于內(nèi)核使用的內(nèi)存空間,所以 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保存起始頁(yè)框號(hào)pgd_idx?=?pgd_index((pfn<<PAGE_SHIFT)?+?PAGE_OFFSET);?//低端內(nèi)存的起始地址對(duì)應(yīng)的pgd的偏移pgd?=?pgd_base?+?pgd_idx;?//得到起始頁(yè)框?qū)?yīng)的pgd//由pgd開始遍歷for?(;?pgd_idx?<?PTRS_PER_PGD;?pgd++,?pgd_idx++)?{pmd?=?one_md_table_init(pgd);//申請(qǐng)得到一個(gè)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表,對(duì)于未激活PAE的32位系統(tǒng),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);?//創(chuàng)建一個(gè)頁(yè)表//得到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))?//如果處于內(nèi)核代碼段,權(quán)限設(shè)為可執(zhí)行prot?=?PAGE_KERNEL_EXEC;pages_4k++;//設(shè)置pte與pfn關(guān)聯(lián)if?(mapping_iter?==?1)?{set_pte(pte,?pfn_pte(pfn,?init_prot));?//第一次執(zhí)行將權(quán)限位設(shè)為init_protlast_map_addr?=?(pfn?<<?PAGE_SHIFT)?+?PAGE_SIZE;}?elseset_pte(pte,?pfn_pte(pfn,?prot));?//之后的執(zhí)行將權(quán)限位置為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; }內(nèi)核的內(nèi)核頁(yè)全局目錄的基地址保存在 swapper_pg_dir 全局變量中,但需要使用主內(nèi)核頁(yè)表時(shí)系統(tǒng)會(huì)把這個(gè)變量的值放入 cr3 寄存器,詳細(xì)可參考/arch/x86/kernel/head_32.s。
Linux 分別采用 pgd_t、pud_t、pmd_t 和 pte_t 四種數(shù)據(jù)結(jié)構(gòu)來表示頁(yè)全局目錄項(xiàng)、頁(yè)上級(jí)目錄項(xiàng)、頁(yè)中間目錄項(xiàng)和頁(yè)表項(xiàng)。這四種數(shù)據(jù)結(jié)構(gòu)本質(zhì)上都是無符號(hào)長(zhǎng)整型,Linux 為了更嚴(yán)格數(shù)據(jù)類型檢查,將無符號(hào)長(zhǎng)整型分別封裝成四種不同的頁(yè)表項(xiàng)。如果不采用這種方法,那么一個(gè)無符號(hào)長(zhǎng)整型數(shù)據(jù)可以傳入任何一個(gè)與四種頁(yè)表相關(guān)的函數(shù)或宏中,這將大大降低程序的健壯性。下面僅列出 pgd_t 類型的內(nèi)核源碼實(shí)現(xiàn),其他類型與此類似
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; }這里需要區(qū)別指向頁(yè)表項(xiàng)的指針和頁(yè)表項(xiàng)所代表的數(shù)據(jù),如果已知一個(gè) pgd_t 類型的指針 pgd,那么通過 pgd_val(*pgd)即可獲得該頁(yè)表項(xiàng)(也就是一個(gè)無符號(hào)長(zhǎng)整型數(shù)據(jù))
PAGE_SHIFT,PMD_SHIFT,PUD_SHIFT,PGDIR_SHIFT,對(duì)應(yīng)相應(yīng)的頁(yè)目錄所能映射的區(qū)域大小的位數(shù),如 PAGE_SHIFT 為 12,即頁(yè)面大小為 4k。
PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD, PTRS_PER_PGD,對(duì)應(yīng)相應(yīng)頁(yè)目錄中的表項(xiàng)數(shù)。32 位系統(tǒng)下,當(dāng) PAE 被禁止時(shí),他們的值分別為 1024,,1,1 和 1024,也就是說只使用兩級(jí)分頁(yè)
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 對(duì)應(yīng)的 pmd 索引在在 pud 指定的 pmd 表的偏移地址。在兩級(jí)或三級(jí)分頁(yè)系統(tǒng)中,pmd_offset 和 pud_offset 都返回頁(yè)全局目錄的地址
至此,低端內(nèi)存的物理地址和虛擬地址之間的映射關(guān)系已全部建立起來了
回到前面的 init_memory_mapping()函數(shù),它的最后一個(gè)函數(shù)調(diào)用為 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))); }該函數(shù)主要是將前面完成內(nèi)存映射的物理頁(yè)框范圍加入到全局?jǐn)?shù)組pfn_mapped中,其中 nr_pfn_mapped 用于表示數(shù)組中的有效項(xiàng)數(shù)量,之后可以通過內(nèi)核函數(shù) pfn_range_is_mapped 來判斷指定的物理內(nèi)存是否被映射,避免重復(fù)映射的情況
固定映射區(qū)初始化
再回到更前面的 init_mem_mapping()函數(shù),early_ioremap_page_table_range_init()用來建立高端內(nèi)存的固定映射區(qū)頁(yè)表,與低端內(nèi)存的頁(yè)表初始化不同的是,固定映射區(qū)的頁(yè)表只是被分配,相應(yīng)的 PTE 項(xiàng)并未初始化,這個(gè)工作交由后面的set_fixmap()函數(shù)將相關(guān)的固定映射區(qū)頁(yè)表與物理內(nèi)存進(jìn)行關(guān)聯(lián)
#?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 的作用是:從給定地址中提取某些分量,用給定地址與對(duì)應(yīng)的 MASK 位與操作之后即可獲得各個(gè)分量,上面的操作為屏蔽低位
這里可以先具體看一下固定映射區(qū)的組成
每個(gè)固定映射區(qū)索引都以枚舉類型的形式定義在 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//這里即為固定映射區(qū)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))一個(gè)索引對(duì)應(yīng)一個(gè) 4KB 的頁(yè)框,固定映射區(qū)的結(jié)束地址為 FIXADDR_TOP,即 0xfffff000(4G-4K),固定映射區(qū)是反向生長(zhǎng)的,也就是說第一個(gè)索引對(duì)應(yīng)的地址離 FIXADDR_TOP 最近。另外宏__fix_to_virt(idx)可以通過索引來計(jì)算相應(yīng)的固定映射區(qū)域的線性地址
//初始化pgd_base指向的頁(yè)全局目錄中start到end這個(gè)范圍的線性地址,整個(gè)函數(shù)結(jié)束后只是初始化好了頁(yè)中間目錄項(xiàng)對(duì)應(yīng)的頁(yè)表,但是頁(yè)表中的頁(yè)表項(xiàng)并沒有初始化 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對(duì)應(yīng)的pgd索引pmd_idx?=?pmd_index(vaddr);?//得到vaddr對(duì)應(yīng)的pmd索引pgd?=?pgd_base?+?pgd_idx;???//得到pgd項(xiàng)for?(?;?(pgd_idx?<?PTRS_PER_PGD)?&&?(vaddr?!=?end);?pgd++,?pgd_idx++)?{pmd?=?one_md_table_init(pgd);?//得到pmd起始項(xiàng)pmd?=?pmd?+?pmd_index(vaddr);?//得到偏移后的pmdfor?(;?(pmd_idx?<?PTRS_PER_PMD)?&&?(vaddr?!=?end);pmd++,?pmd_idx++)?{//建立pte表并檢查vaddr是否對(duì)應(yīng)內(nèi)核臨時(shí)映射區(qū),若是則重新申請(qǐng)一個(gè)頁(yè)表來保存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 函數(shù)
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()用來計(jì)算臨時(shí)映射區(qū)間的頁(yè)表數(shù)量。FIXADDR_START 到 FIXADDR_TOP 即整個(gè)固定映射區(qū),就如上面所提到的里面有多個(gè)索引標(biāo)識(shí)的不同功能的映射區(qū)間,而其中的一個(gè)區(qū)間FIX_KMAP_BEGIN 到 FIX_KMAP_END 是臨時(shí)映射區(qū)間。這里再看一下兩者的定義:
FIX_KMAP_BEGIN,?/*?reserved?pte's?for?temporary?kernel?mappings?*/ FIX_KMAP_END?=?FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,固定映射是在編譯時(shí)就確定的地址空間,但是它對(duì)應(yīng)的物理頁(yè)可以是任意的,每一個(gè)固定地址中的項(xiàng)都是一頁(yè)范圍,這些地址的用途是固定的,這是該區(qū)間被設(shè)計(jì)的最主要的用途。臨時(shí)映射區(qū)間也叫原子映射,它可以用在不能睡眠的地方,如中斷處理程序中,因?yàn)榕R時(shí)映射獲取映射時(shí)是絕對(duì)不會(huì)阻塞的,上面的 KM_TYPE_NR 可以理解為臨時(shí)內(nèi)核映射的最大數(shù)量,NR_CPUS 則表示 CPU 數(shù)量。
如果返回的頁(yè)表數(shù)量不為 0,則 page_table_range_init()函數(shù)還會(huì)調(diào)用 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函數(shù)根據(jù)前面 early_alloc_pgt_buf()申請(qǐng)保留的頁(yè)表緩沖空間使用情況來判斷是從頁(yè)表緩沖空間中申請(qǐng)內(nèi)存還是通過 memblock 算法申請(qǐng)頁(yè)表內(nèi)存,頁(yè)表緩沖空間空間足夠的話就在頁(yè)表緩沖空間中分配。
回到 page_table_range_init(),其中one_md_table_init()是用于申請(qǐng)新物理頁(yè)作為頁(yè)中間目錄的,不過這里分析的是 x86 非 PAE 環(huán)境,不存在頁(yè)中間目錄,因此實(shí)際上返回的仍是入?yún)?#xff0c;代碼如下:
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(),其入?yún)⒄{(diào)用的one_page_table_init()是用于當(dāng)入?yún)?pmd 沒有頁(yè)表指向時(shí),創(chuàng)建頁(yè)表并使其指向被創(chuàng)建的頁(yè)表。
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()的實(shí)現(xiàn)如下:
#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.*///得到內(nèi)核固定映射區(qū)的臨時(shí)映射區(qū)的起始和結(jié)束虛擬頁(yè)框號(hào)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表進(jìn)行關(guān)聯(lián)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; }檢查當(dāng)前頁(yè)表初始化的地址是否處于該區(qū)間范圍,如果是,則把其 pte 頁(yè)表的內(nèi)容拷貝到 page_table_range_init()的 alloc_low_pages 申請(qǐng)的頁(yè)表空間中(這里的拷貝主要是為了保證連續(xù)性),并將 newpte 新頁(yè)表的地址設(shè)置到 pmd 中(32bit 系統(tǒng)實(shí)際上就是頁(yè)全局目錄),然后調(diào)用__flush_tlb_all()刷新 TLB 緩存;如果不是該區(qū)間,則僅是由入?yún)⒅姓{(diào)用的 one_page_table_init()被分配到了頁(yè)表空間。
至此高端內(nèi)存的固定映射區(qū)的頁(yè)表分配完成,后面的 paging_init()會(huì)負(fù)責(zé)完成剩下的頁(yè)表建立工作。
Linux 內(nèi)存管理框架
傳統(tǒng)的多核運(yùn)算是使用 SMP(Symmetric Multi-Processor )模式:將多個(gè)處理器與一個(gè)集中的存儲(chǔ)器和 I/O 總線相連,所有處理器訪問同一個(gè)物理存儲(chǔ)器,因此 SMP 系統(tǒng)有時(shí)也被稱為一致存儲(chǔ)器訪問(UMA)結(jié)構(gòu)體系,即無論在什么時(shí)候,處理器只能為內(nèi)存的每個(gè)數(shù)據(jù)保持或共享唯一一個(gè)數(shù)值。
而NUMA模式是一種分布式存儲(chǔ)器訪問方式,處理器可以同時(shí)訪問不同的存儲(chǔ)器地址,大幅度提高并行性。NUMA 模式下系統(tǒng)的每個(gè) CPU 都有本地內(nèi)存,可支持快速訪問,各個(gè)處理器之間通過總線連接起來,以支持對(duì)其它 CPU 本地內(nèi)存的訪問,但是這些訪問要比處理器本地內(nèi)存的慢.
Linux 內(nèi)核通過插入一些兼容層,使兩個(gè)不同體系結(jié)構(gòu)的差異被隱藏,兩種模式都使用了同一個(gè)數(shù)據(jù)結(jié)構(gòu)。
在 NUMA 模式下,處理器和內(nèi)存塊被劃分成多個(gè)"節(jié)點(diǎn)"(node),比如機(jī)器上有 2 個(gè)處理器、4 個(gè)內(nèi)存塊,我們可以把 1 個(gè)處理器和 2 個(gè)內(nèi)存塊合起來(GNU/Linux 根據(jù)物理 CPU 的數(shù)量分配 node,一個(gè)物理 CPU 對(duì)應(yīng)一個(gè) node),共同組成一個(gè) NUMA 的節(jié)點(diǎn)。每個(gè)節(jié)點(diǎn)被分配有本地存儲(chǔ)器空間,所有節(jié)點(diǎn)中的處理器都可以訪問系統(tǒng)全部的物理存儲(chǔ)器,但是訪問本節(jié)點(diǎn)內(nèi)的存儲(chǔ)器所需要的時(shí)間比訪問某些遠(yuǎn)程節(jié)點(diǎn)內(nèi)的存儲(chǔ)器所花的時(shí)間要少得多。
與 CPU 類似,內(nèi)存被分割成多個(gè)區(qū)域 BANK,也叫"簇",依據(jù)簇與 CPU 的"距離"的不同,訪問不同簇的方式也會(huì)有所不同,CPU 被劃分為多個(gè)節(jié)點(diǎn),每個(gè) CPU 對(duì)應(yīng)一個(gè)本地物理內(nèi)存, 一般一個(gè) CPU-node 對(duì)應(yīng)一個(gè)內(nèi)存簇,也就是說每個(gè)內(nèi)存簇被認(rèn)為是一個(gè)節(jié)點(diǎn)。
而在 UMA 系統(tǒng)中, 內(nèi)存就相當(dāng)于一個(gè)只使用一個(gè) NUMA 節(jié)點(diǎn)來管理整個(gè)系統(tǒng)的內(nèi)存,這樣在內(nèi)存管理的其它地方可以認(rèn)為他們就是在處理一個(gè)(偽)NUMA 系統(tǒng)。
內(nèi)存管理框架初始化
在 linux 中,每個(gè)物理內(nèi)存節(jié)點(diǎn) node 都被劃分為多個(gè)內(nèi)存管理區(qū)域用于表示不同范圍的內(nèi)存,比如上面提到的 NORMAL 內(nèi)存、高端內(nèi)存,內(nèi)核可以使用不同的映射方式映射物理內(nèi)存。zone 只是內(nèi)核為了管理方便而做的一種邏輯上的劃分,并不存在這種物理硬件單元。
綜上,linux 的物理內(nèi)存管理機(jī)制將物理內(nèi)存劃分為三個(gè)層次來管理,依次是:Node(存儲(chǔ)節(jié)點(diǎn))、Zone(管理區(qū))和 Page(頁(yè)面),它們之間的關(guān)系如下:
其中,zone 的類型如下:
include/linux/mmzone.henum?zone_type?{ #ifdef?CONFIG_ZONE_DMAZONE_DMA,?//通常為內(nèi)存首部16MB,某些工業(yè)標(biāo)準(zhǔn)體系結(jié)構(gòu)設(shè)備需要用到ZONE_DMA(較舊的外設(shè),只能尋址24位內(nèi)存) #endif#ifdef?CONFIG_ZONE_DMA32ZONE_DMA32,?//在64位Linux操作系統(tǒng)上,DMA32為16M~4G(可訪問32位),高于4G的內(nèi)存為Normal?ZONE #endifZONE_NORMAL,?//通常為16MB~896MB,該部分的內(nèi)存由內(nèi)核直接映射到物理地址空間的較高部分 #ifdef?CONFIG_HIGHMEMZONE_HIGHMEM,?//通常為896MB~末尾,將保留給系統(tǒng)使用,是系統(tǒng)中預(yù)留的可用內(nèi)存空間,動(dòng)態(tài)映射 #endifZONE_MOVABLE,?//用于減少內(nèi)存的碎片化,這個(gè)區(qū)域的頁(yè)都是可遷移的#ifdef?CONFIG_ZONE_DEVICEZONE_DEVICE,?//為支持熱插拔設(shè)備而分配的非易失性內(nèi)存 #endif__MAX_NR_ZONES };回到之前的 setup_arch()函數(shù),接著往下走,來到內(nèi)存管理框架初始化的地方
void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_ram_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內(nèi)存初始化............early_alloc_pgt_buf();?//頁(yè)表緩沖區(qū)分配reserve_brk();?//緩沖區(qū)加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動(dòng)......init_mem_mapping();?//低端內(nèi)存內(nèi)核頁(yè)表初始化?高端內(nèi)存固定映射區(qū)中臨時(shí)映射區(qū)頁(yè)表初始化......initmem_init();?//?<----------------------------------------...... }initmem_init的實(shí)現(xiàn)如下:
#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?*/這個(gè)函數(shù)將high_memory初始化為低端內(nèi)存最大頁(yè)框max_low_pfn對(duì)應(yīng)的地址大小,接著調(diào)用 memblock_set_node,通過 memblock 內(nèi)存管理器設(shè)置 node 節(jié)點(diǎn)信息。
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 主要調(diào)用了三個(gè)函數(shù):memblock_isolate_range、memblock_set_region_node 和 memblock_merge_regions,首先看memblock_isolate_range()函數(shù):
/*?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()主要作用是將要移除的物理內(nèi)存區(qū)從 reserved 內(nèi)存區(qū)中分離出來,將 start_rgn 和 end_rgn(該內(nèi)存區(qū)塊的起始、結(jié)束索引號(hào))返回回去,而這里,由于我們傳入的type 是 memblock.memory,該函數(shù)會(huì)根據(jù)入?yún)?base 和 size 標(biāo)記節(jié)點(diǎn)內(nèi)存范圍,將該內(nèi)存從 memory 中劃分開來,同時(shí)返回對(duì)應(yīng)的 start_rgn 和 end_rgn。
1)如果 memblock 中的 region 恰好以在該節(jié)點(diǎn)內(nèi)存范圍內(nèi)的話,那么再未賦值 end_rgn 時(shí)將當(dāng)前 region 的索引記錄至 start_rgn,end_rgn 在此基礎(chǔ)上加 1;
2)如果 memblock 中的 region 跨越了該節(jié)點(diǎn)內(nèi)存末尾分界,那么將會(huì)把當(dāng)前的 region 邊界調(diào)整為 node 節(jié)點(diǎn)內(nèi)存范圍邊界,然后通過 memblock_insert_region()函數(shù)將剩下的部分(即越出內(nèi)存范圍的那一塊內(nèi)存)重新插入 memblock 管理 regions 當(dāng)中,實(shí)現(xiàn)拆分;
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 信息往后移,另外調(diào)用 memblock_set_region_node()將原 region 的 node 節(jié)點(diǎn)號(hào)保留在被拆分出來的 region 當(dāng)中。
回到前面的 memblock_set_node()函數(shù),緊接著 memblock_isolate_range()被調(diào)用的是memblock_set_region_node(),通過這個(gè)函數(shù)把劃分出來的 region 進(jìn)行 node 節(jié)點(diǎn)號(hào)設(shè)置,而后面的memblock_merge_regions()前面 MEMBLOCK 內(nèi)存分配器初始化時(shí)已經(jīng)分析過了,是用于將相鄰的 region 進(jìn)行合并的(節(jié)點(diǎn)號(hào)、flag 等一致才會(huì)合并)。
最后回到 initmem_init()函數(shù)中,memblock_set_node()返回后,接著調(diào)用的函數(shù)為 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); }原來該函數(shù)是用來初始化 bootmem 管理算法的,但現(xiàn)在 x86 的環(huán)境已經(jīng)使用了 memblock 管理算法,所以這里僅作保留,打印部分信息。
bootmem 分配器使用一個(gè) bitmap 來標(biāo)記物理頁(yè)是否被占用,分配的時(shí)候按照第一適應(yīng)的原則,從 bitmap 中進(jìn)行查找,如果這位為 1,表示已經(jīng)被占用,否則表示未被占用。bootmem 分配器每次分配內(nèi)存都會(huì)在 bitmap 中進(jìn)行線性搜索,效率非常低,而且容易在內(nèi)存中留下許多小的空閑碎片,在需要非常大的內(nèi)存塊的時(shí)候,檢查位圖這一過程就顯得代價(jià)很高。bootmem 分配器是用于在啟動(dòng)階段分配內(nèi)存的,對(duì)該分配器的需求集中于簡(jiǎn)單性方面,而不是性能和通用性(和 memblock 管理器一致)。
至此,已完成對(duì)內(nèi)存的節(jié)點(diǎn) node 設(shè)置。
回到 setup_arch()函數(shù):
void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_raCm_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內(nèi)存初始化............early_alloc_pgt_buf();?//頁(yè)表緩沖區(qū)分配reserve_brk();?//緩沖區(qū)加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動(dòng)......init_mem_mapping();?//低端內(nèi)存內(nèi)核頁(yè)表初始化?高端內(nèi)存固定映射區(qū)中臨時(shí)映射區(qū)頁(yè)表初始化......initmem_init();?//high_memory(高端內(nèi)存起始pfn)初始化?通過memblock內(nèi)存管理器設(shè)置node節(jié)點(diǎn)信息......x86_init.paging.pagetable_init();?//?<-----------------------------...... }x86_init 結(jié)構(gòu)體內(nèi)pagetable_init實(shí)際上掛接的是native_pagetable_init()函數(shù):
struct?x86_init_ops?x86_init?__initdata?=?{.......paging?=?{.pagetable_init??=?native_pagetable_init,},...... }native_pagetable_init()函數(shù)內(nèi)容如下:
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;//循環(huán)?低端內(nèi)存最大物理頁(yè)號(hào)~最大物理頁(yè)號(hào)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 代表的是內(nèi)核空間和用戶空間對(duì)虛擬地址空間的劃分,對(duì)不同的體系結(jié)構(gòu)不同。比如在 32 位系統(tǒng)中 3G-4G 屬于內(nèi)核使用的內(nèi)存空間,所以 PAGE_OFFSET = 0xC0000000。
該函數(shù)的 for 循環(huán)主要是用于檢測(cè) max_low_pfn 后面的內(nèi)核空間內(nèi)存直接映射空間后面的物理內(nèi)存是否存在系統(tǒng)啟動(dòng)引導(dǎo)時(shí)創(chuàng)建的頁(yè)表(pfn 通過直接映射的方法得到虛擬地址,然后通過內(nèi)核頁(yè)表得到 pgd、pmd、pte),如果存在,則使用 pte_clear()將其清除。
接著看 native_pagetable_init()調(diào)用的最后一個(gè)函數(shù):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(); }可以對(duì)著這個(gè)圖看:
前面已經(jīng)分析過低端內(nèi)存、固定映射區(qū)中臨時(shí)映射區(qū)的內(nèi)核頁(yè)表的建立,這里 paging_init 將會(huì)完成剩下的工作,首先看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); }該函數(shù)為建立持久映射區(qū)(KMAP 區(qū))的頁(yè)表,page_table_range_init 函數(shù)前面固定映射區(qū)頁(yè)表初始化時(shí)已經(jīng)分析過了(初始化 pgd_base 指向的頁(yè)全局目錄中 start 到 end 這個(gè)范圍的線性地址,整個(gè)函數(shù)結(jié)束后只是初始化好了頁(yè)中間目錄項(xiàng)對(duì)應(yīng)的頁(yè)表,但是頁(yè)表中的頁(yè)表項(xiàng)并沒有初始化),這里建立頁(yè)表范圍為 PKMAP_BASE 到 PKMAP_BASE + PAGE_SIZE*LAST_PKMAP,建好頁(yè)表后將頁(yè)表地址賦值給給持久映射區(qū)頁(yè)表變量 pkmap_page_table。
__flush_tlb_all()為刷新全部 TLB,這里不做介紹,接著看 paging_init()調(diào)用的下一個(gè)函數(shù) 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()主要是獲取到臨時(shí)映射區(qū)間的起始頁(yè)表并往臨時(shí)映射頁(yè)表變量 kmap_pte 置值。
回到 paging_init(),olpc_dt_build_devicetree 這里就不做介紹了,而sparse_init()則涉及到了 Linux 的內(nèi)存模型,這里介紹一下 Linux 的三種內(nèi)存模型,注意,以下都是從 CPU 的角度來看的。
從系統(tǒng)中任意一個(gè) CPU 的角度來看,當(dāng)它訪問物理內(nèi)存的時(shí)候,物理地址空間是一個(gè)連續(xù)的、沒有空洞的地址空間,那么這種計(jì)算機(jī)系統(tǒng)的內(nèi)存模型就是Flat memory。在這種內(nèi)存模型下,物理內(nèi)存的管理比較簡(jiǎn)單,每一個(gè)物理頁(yè)幀都會(huì)有一個(gè) page 數(shù)據(jù)結(jié)構(gòu)來抽象,因此系統(tǒng)中存在一個(gè) struct page 的數(shù)組(位于直接映射區(qū),mem_map,在節(jié)點(diǎn) node 里,后面就會(huì)介紹到),每一個(gè)數(shù)組條目指向一個(gè)實(shí)際的物理頁(yè)幀(page frame)。在 flat memory 的情況下,PFN 和 mem_map 數(shù)組 index 的關(guān)系是線性的(即位于直接映射區(qū),有一個(gè)固定偏移),因此從 PFN 到對(duì)應(yīng)的 page 數(shù)據(jù)結(jié)構(gòu)是非常容易的,反之亦然。
pfn_to_page/page_to_pfn 的作用是 struct page* 和 pfn 頁(yè)幀號(hào)之間的轉(zhuǎn)換,flat memory 內(nèi)存模型的相關(guān)代碼如下:
#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 數(shù)組(mem_map)的 index 是線性關(guān)系,有一個(gè)固定的偏移就是 ARCH_PFN_OFFSET(跟架構(gòu)相關(guān)的物理起始地址的 PFN)。
如果 cpu 在訪問物理內(nèi)存的時(shí)候,其地址空間有一些空洞,是不連續(xù)的,那么這種計(jì)算機(jī)系統(tǒng)的內(nèi)存模型就是Discontiguous memory。一般而言,NUMA 架構(gòu)的計(jì)算機(jī)系統(tǒng)的 memory model 都是選擇 Discontiguous Memory,不過 NUMA 強(qiáng)調(diào)的是 memory 和 processor 的位置關(guān)系,和內(nèi)存模型其實(shí)是沒有關(guān)系的(NUMA 并沒有規(guī)定其內(nèi)存的連續(xù)性,而 Discontiguous memory 系統(tǒng)也并非一定是 NUMA 系統(tǒng),但是這兩種都是多節(jié)點(diǎn)的),只不過,由于同一 node 上的 memory 和 processor 有更緊密的耦合關(guān)系(訪問更快),因此需要多個(gè) node 來管理。Discontiguous memory 本質(zhì)上是 flat memory 內(nèi)存模型的擴(kuò)展,整個(gè)物理內(nèi)存的內(nèi)存空間大部分是成片的大塊內(nèi)存,中間會(huì)有一些空洞,每一個(gè)成片的內(nèi)存地址空間屬于一個(gè) node(如果局限在一個(gè) node 內(nèi)部,其內(nèi)存模型是 flat memory)。
在這種模型下,從 PFN 轉(zhuǎn)換到具體的 struct page 會(huì)稍微復(fù)雜一點(diǎn),首先要從 PFN 得到 node ID,然后根據(jù)這個(gè) ID 找到對(duì)于的節(jié)點(diǎn) node 數(shù)據(jù)結(jié)構(gòu),也就找到了對(duì)應(yīng)的 page 數(shù)組,之后的方法就類似 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 進(jìn)行就 OK 了。對(duì)于__pfn_to_page 的定義,首先通過 arch_pfn_to_nid 將 PFN 轉(zhuǎn)換成 node id,通過 NODE_DATA 宏定義可以找到該 node 對(duì)應(yīng)的 pglist_data 數(shù)據(jù)結(jié)構(gòu),該數(shù)據(jù)結(jié)構(gòu)的 node_start_pfn 記錄了該 node 的第一個(gè) pfn,因此,也就可以得到其對(duì)應(yīng) struct page 在 node_mem_map 的偏移,__page_to_pfn 類似與上述基本類似(pglist_data 數(shù)據(jù)結(jié)構(gòu)后面再進(jìn)行介紹)。
內(nèi)存模型也是一個(gè)演進(jìn)過程,剛開始的時(shí)候,使用 flat memory 去抽象一個(gè)連續(xù)的內(nèi)存地址空間,出現(xiàn) NUMA 之后,整個(gè)不連續(xù)的內(nèi)存空間被分成若干個(gè) node,每個(gè) node 上是連續(xù)的內(nèi)存地址空間,也就是從原來單一的一個(gè) mem_maps[]變成了若干個(gè) mem_maps[]。而現(xiàn)在 memory hotplug 的出現(xiàn)讓原來完美的設(shè)計(jì)變得不完美了(熱插拔,即帶電插拔,熱插拔功能就是允許用戶在不關(guān)閉系統(tǒng),不切斷電源的情況下取出和更換損壞的硬盤、電源或板卡等部件。Linux 內(nèi)核支持熱插拔的部件有 USB 設(shè)備、PCI 設(shè)備甚至 CPU),因?yàn)榧幢闶且粋€(gè) node 中的 mem_maps[]也有可能是不連續(xù)了,因此目前 Discontiguous memory 也逐漸在被 sparse memory 替代。
在sparse memory內(nèi)存模型下,連續(xù)的地址空間按照 SECTION(例如 1G)被分成了一段一段的,其中每一 p 都是 hotplug 的,因此 sparse memory 下,內(nèi)存地址空間可以被切分的更細(xì)。整個(gè)連續(xù)的物理地址空間是按照一個(gè)個(gè)p來切斷的,在每一個(gè) p 內(nèi)部,其 memory 是連續(xù)的(即符合 flat memory 的特點(diǎn)),因此,mem_map 的 page 數(shù)組依附于 p 結(jié)構(gòu)(struct mem_p),而不是 node 結(jié)構(gòu)(struct pglist_data)。在這個(gè)模型下,PFN 轉(zhuǎn) struct page 變?yōu)榱宿D(zhuǎn)換變成了 PFN<--->Section<--->page。
linux 內(nèi)核中靜態(tài)定義了一個(gè) mem_p 的指針數(shù)組,一個(gè) p 中往往包括多個(gè) page,因此需要通過 PFN 得到 p 號(hào),用 p 號(hào)做為 index 在 mem_p 指針數(shù)組可以找到該 PFN 對(duì)應(yīng)的 p 數(shù)據(jù)結(jié)構(gòu)。實(shí)際上PFN 分成兩個(gè)部分:一部分是 p index,另外一個(gè)部分是 page 在該 p 的偏移,找到 p 之后,沿著其 mem_map 就可以找到對(duì)應(yīng)的 page 數(shù)據(jù)結(jié)構(gòu)。
對(duì)于 page 到 p index 的轉(zhuǎn)換,sparse memory 有 2 種方案,先看看經(jīng)典的方案,也就是把 p 號(hào)保存在 page->flags 中(page 的結(jié)構(gòu)同樣在后面再進(jìn)行介紹),這種方法的最大的問題是 page->flags 中的 bit 數(shù)不一定夠用,因?yàn)檫@個(gè) flag 中承載了太多的信息,各種 page flag、node id、zone id,現(xiàn)在又增加一個(gè) p id,在不同的處理器架構(gòu)中無法實(shí)現(xiàn)一致性的算法(上面的圖即為采用經(jīng)典算法的 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?*/對(duì)于經(jīng)典的 sparse memory 模型,一個(gè) p 的 struct page 數(shù)組所占用的內(nèi)存來自直接映射區(qū),頁(yè)表在初始化的時(shí)候就建立好了。但是,對(duì)于 SPARSEMEM_VMEMMAP 而言,虛擬地址一開始就分配好了,是 vmemmap 開始的一段連續(xù)的虛擬地址空間,但是沒有物理地址。因此,當(dāng)一個(gè) p 被發(fā)現(xiàn)后,可以立刻找到對(duì)應(yīng)的 struct page 的虛擬地址,而這時(shí)候還需要分配一個(gè)物理的 page frame,對(duì)于這種 sparse memory,開銷會(huì)稍微大一些,需要建立頁(yè)表,關(guān)聯(lián) 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)內(nèi)存管理區(qū) Zone 初始化
回到 paging_init()的最后一個(gè)函數(shù)調(diào)用: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); }這個(gè)函數(shù)為 zone 的各個(gè)內(nèi)存管理區(qū)域最大物理頁(yè)號(hào)進(jìn)行初始化,并作為參數(shù)調(diào)用 free_area_init_nodes(),其中free_area_init_nodes()函數(shù)實(shí)現(xiàn)如下:
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?*///全局?jǐn)?shù)組arch_zone_lowest_possible_pfn用來存儲(chǔ)各個(gè)內(nèi)存域可使用的最低內(nèi)存頁(yè)幀編號(hào)//全局?jǐn)?shù)組arch_zone_highest_possible_pfn用來存儲(chǔ)各個(gè)內(nèi)存域可使用的最高內(nèi)存頁(yè)幀編號(hào)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));//用于最低內(nèi)存區(qū)域中可用的編號(hào)最小的頁(yè)幀,即memblock.memory.regions[0].basestart_pfn?=?find_min_pfn_with_active_regions();//return?falsedescending?=?arch_has_descending_max_zone_pfns();//根據(jù)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是一個(gè)虛擬內(nèi)存域,不與真正的硬件內(nèi)存域關(guān)聯(lián),該內(nèi)存域的邊界總是設(shè)置為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));//用于計(jì)算ZONE_MOVABLE的內(nèi)存數(shù)量//在內(nèi)存較多的32位系統(tǒng)上,?這通常會(huì)是ZONE_HIGHMEM,?但是對(duì)于64位系統(tǒng),將使用ZONE_NORMAL或ZONE_DMA32,這里計(jì)算也比較復(fù)雜,感興趣的話可以去看一下源碼,這里便不做介紹了find_zone_movable_pfns_for_nodes();......//各種打印....../*?Initialise?every?node?*///為系統(tǒng)中的每個(gè)節(jié)點(diǎn)調(diào)用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()函數(shù)會(huì)計(jì)算每個(gè)節(jié)點(diǎn)中每個(gè)區(qū)域的大小及其空洞的大小,如果兩個(gè)相鄰區(qū)域之間的最大 PFN 匹配,則可以假定后面的區(qū)域?yàn)榭铡@?arch_max_dma_pfn == arch_max_dma32_pfn,則假定 arch_max_dma32_pfn 該區(qū)域?yàn)榭铡?/p>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) {//從全局節(jié)點(diǎn)數(shù)組中獲取一個(gè)節(jié)點(diǎn)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);//根據(jù)節(jié)點(diǎn)id獲取起始pfn和結(jié)束pfn,前面node初始化時(shí),memblock處已經(jīng)設(shè)置好節(jié)點(diǎn)ID了get_pfn_range_for_nid(nid,?&start_pfn,?&end_pfn);//設(shè)置節(jié)點(diǎn)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);//初始化節(jié)點(diǎn)中每個(gè)zone的大小和空洞,同時(shí)計(jì)算節(jié)點(diǎn)的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); }
在進(jìn)入 calculate_node_totalpages 之前,這里還是先簡(jiǎn)單介紹一下node的數(shù)據(jù)結(jié)構(gòu)。
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?{//本節(jié)點(diǎn)的所有zone內(nèi)存管理區(qū)struct?zone?node_zones[MAX_NR_ZONES];//包含了對(duì)所有node的zone的引用,通常第一個(gè)node_zonelists為本節(jié)點(diǎn)自己的zonesstruct?zonelist?node_zonelists[MAX_ZONELISTS];//本節(jié)點(diǎn)zone管理區(qū)數(shù)目int?nr_zones;?/*?number?of?populated?zones?in?this?node?*/ #ifdef?CONFIG_FLAT_NODE_MEM_MAP?/*?means?!SPARSEMEM?*///Discontiguous?Memory內(nèi)存模型,可以找到節(jié)點(diǎn)下的所有pagestruct?page?*node_mem_map; #ifdef?CONFIG_PAGE_EXTENSIONstruct?page_ext?*node_page_ext; #endif #endif......//節(jié)點(diǎn)第一個(gè)頁(yè)的頁(yè)號(hào)unsigned?long?node_start_pfn;//節(jié)點(diǎn)總共的物理頁(yè)數(shù),不含空洞unsigned?long?node_present_pages;?/*?total?number?of?physical?pages?*///節(jié)點(diǎn)物理頁(yè)的范圍大小,含空洞unsigned?long?node_spanned_pages;?/*?total?size?of?physical?pagerange,?including?holes?*/int?node_id;......//內(nèi)存回收相關(guān)的數(shù)據(jù)結(jié)構(gòu)......ZONE_PADDING(_pad1_)......unsigned?long??flags;ZONE_PADDING(_pad2_)...... }?pg_data_t;ZONE_PADDING 的作用,通過添加大量的填充把經(jīng)常被訪問的“熱門”數(shù)據(jù)分到了單獨(dú)的 cache line 上,可以理解為空間換時(shí)間。
calculate_node_totalpages()實(shí)現(xiàn):
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所在區(qū)間*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; }該函數(shù)主要是統(tǒng)計(jì) node 管理節(jié)點(diǎn)的內(nèi)存跨度,該跨度不包括 movable 管理區(qū)的(因?yàn)?movable 就是在其它內(nèi)存管理區(qū)里分配出來的),里面調(diào)用的 adjust_zone_range_for_zone_movable()則是用于剔除 movable 管理區(qū)的部分。
另外的zone_absent_pages_in_node()函數(shù):
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; }該函數(shù)主要用于計(jì)算內(nèi)存空洞頁(yè)面數(shù)的,計(jì)算方法大致為在 zone 區(qū)域范圍內(nèi),遍歷所有 memblock 的內(nèi)存塊,將這些內(nèi)存塊的大小累加,之后兩者做差,zone_absent_pages_in_node 后面是對(duì) ZONE_MOVABLE 的特殊處理了,方法是類似的,這里也不做介紹了。
calculate_node_totalpages()后面就是各種簡(jiǎn)單的賦值操作了,這里也簡(jiǎn)單介紹一下zone的結(jié)構(gòu):
其中 MAX_NR_ZONES 是一個(gè)節(jié)點(diǎn)中所能包容納的 Zones 的最大數(shù)。
struct?zone?{......//保留頁(yè)框池,記錄每個(gè)管理區(qū)中必須保留的物理頁(yè)面數(shù),以用于緊急狀況下的內(nèi)存分配long?lowmem_reserve[MAX_NR_ZONES];//保持對(duì)UMA的兼容(當(dāng)做一個(gè)節(jié)點(diǎn)),NUMA模式下的節(jié)點(diǎn)數(shù) #ifdef?CONFIG_NUMAint?node; #endif//該zone的父節(jié)點(diǎn)struct?pglist_data?*zone_pgdat;......//該zone的第一個(gè)頁(yè)的頁(yè)號(hào)unsigned?long??zone_start_pfn;//伙伴系統(tǒng)管理的page數(shù),這是除去了在初始化階段被申請(qǐng)的頁(yè)面(比如memblock)atomic_long_t??managed_pages;//zone大小,含空洞,即zone_end_pfn?-?zone_start_pfnunsigned?long??spanned_pages;//zone實(shí)際大小,不含空洞unsigned?long??present_pages;//zone的名稱,如“DMA”“Normal”“Highmem”,這些名稱定義于page_alloc.c的zone_names[MAX_NR_ZONES]const?char??*name;ZONE_PADDING(_pad1_)//包含所有空閑頁(yè)面,伙伴系統(tǒng)使用,里面有數(shù)量為MIGRATE_TYPES個(gè)的free_list鏈表,分別用于管理不同遷移類型的內(nèi)存頁(yè)面struct?free_area?free_area[MAX_ORDER];//描述zone的當(dāng)前狀態(tài)unsigned?long??flags;/*?Primarily?protects?free_area?*///與伙伴算法的碎片遷移算法有關(guān)spinlock_t??lock;ZONE_PADDING(_pad2_)......ZONE_PADDING(_pad3_)...... }?____cacheline_internodealigned_in_smp;回到 free_area_init_node()函數(shù),緊接在 calculate_node_totalpages()后的函數(shù)調(diào)用的為alloc_node_mem_map(),這個(gè)函數(shù)是用于申請(qǐng) node 節(jié)點(diǎn)的 node_mem_map 相應(yīng)的內(nèi)存空間,如果是 sparse memory 內(nèi)存模型,則該函數(shù)實(shí)現(xiàn)為空,這里便不做過多介紹了,直接看最后的初始化工作:free_area_init_core()。
static?void?__init?free_area_init_core(struct?pglist_data?*pgdat) {enum?zone_type?j;int?nid?=?pgdat->node_id;//對(duì)節(jié)點(diǎn)的一些鎖和隊(duì)列進(jìn)行初始化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,每一個(gè)4k物理頁(yè)都對(duì)應(yīng)一個(gè)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保留頁(yè)if?(j?==?0?&&?freesize?>?dma_reserve)?{freesize?-=?dma_reserve;printk(KERN_DEBUG?"??%s?zone:?%lu?pages?reserved\n",zone_names[0],?dma_reserve);}//計(jì)算nr_kernel_pages(低端內(nèi)存的頁(yè)數(shù))和nr_all_pages的數(shù)量if?(!is_highmem_idx(j))nr_kernel_pages?+=?freesize;/*?Charge?for?highmem?memmap?if?there?are?enough?kernel?pages?*///如果有足夠的頁(yè),則也為高端內(nèi)存提供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);} }該函數(shù)主要用于向節(jié)點(diǎn)下的每個(gè) zone 填充相關(guān)信息,在 for 循環(huán)內(nèi),循環(huán)遍歷統(tǒng)計(jì)各個(gè)管理區(qū)最大跨度間相差的頁(yè)面數(shù) size 以及除去內(nèi)存“空洞”后的實(shí)際頁(yè)面數(shù) freesize,然后通過 calc_memmap_size()計(jì)算出該管理區(qū)所需的頁(yè)面管理結(jié)構(gòu)占用的頁(yè)面數(shù) memmap_pages,最后可以計(jì)算得出高端內(nèi)存外的系統(tǒng)內(nèi)存共有的內(nèi)存頁(yè)面數(shù)(freesize-memmap_pages)。
nr_kernel_pages 用于統(tǒng)計(jì)低端內(nèi)存的頁(yè)數(shù),此外循環(huán)體內(nèi)的操作則是初始化內(nèi)存管理區(qū)的管理結(jié)構(gòu),例如各類鎖的初始化、隊(duì)列初始化。其中 set_pageblock_order()用于在 CONFIG_HUGETLB_PAGE_SIZE_VARIABLE 下設(shè)置 pageblock_order 的值;setup_usemap()函數(shù)則是為了給 zone 管理結(jié)構(gòu)體中的 pageblock_flags 申請(qǐng)內(nèi)存空間的,pageblock_flags 與伙伴系統(tǒng)的碎片遷移算法有關(guān)。init_currently_empty_zone()則主要是初始化管理區(qū)的等待隊(duì)列哈希表和等待隊(duì)列,同時(shí)還初始化了與伙伴系統(tǒng)相關(guān)的 free_area 列表
nr_kernel_pages、nr_all_pages 和頁(yè)面的關(guān)系可以參考下圖:
這里以我自己的私服為例,看一下我私服 node 和 zone 的情況
首先是 node 的個(gè)數(shù),GNU/Linux 根據(jù)物理 CPU 的數(shù)量分配 node,因此可以直接查物理 CPU 的數(shù)量:
當(dāng)然,用 numactl 會(huì)更加直觀:
我的機(jī)器上只有一個(gè) node,接下來可以用 cat /proc/zoneinfo 查看這個(gè) node 下各個(gè) zone 的情況:
回到 free_area_init_core()函數(shù)的最后,memmap_init()->memmap_init_zone(),該函數(shù)主要是根據(jù) PFN,然后通過 pfn_to_page 找到對(duì)應(yīng)的 struct page 結(jié)構(gòu),并將該結(jié)構(gòu)進(jìn)行初始化處理,并設(shè)置 MIGRATE_MOVABLE 標(biāo)志,表明可移動(dòng)。
//遍歷memblock,找到節(jié)點(diǎn)的內(nèi)存地址范圍,zone的范圍不能大于這個(gè),使用memmap_init_zone對(duì)該zone進(jìn)行處理 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
最后這里簡(jiǎn)單介紹一下 struct page,內(nèi)核會(huì)為每一個(gè)物理頁(yè)幀創(chuàng)建一個(gè) struct page 的結(jié)構(gòu)體,因此要保證 page 結(jié)構(gòu)體足夠的小,否則僅 struct page 就要占用大量的內(nèi)存,該結(jié)構(gòu)有很多 union 結(jié)構(gòu),主要是用于各種算法不同數(shù)據(jù)的空間復(fù)用。
struct page 這個(gè)結(jié)構(gòu)相當(dāng)復(fù)雜,這里我放上網(wǎng)上找到的一個(gè)全局參考,可以比源碼更清晰地了解整個(gè)結(jié)構(gòu)體,這里我也只簡(jiǎn)單介紹里面的幾個(gè)字段。
????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 的狀態(tài)和其它的一些信息,如下圖。
主要分為 4 部分,其中標(biāo)志位 flag 向高位增長(zhǎng),其余位字段向低位增長(zhǎng),中間存在空閑位。
p:主要用于 sparse memory 內(nèi)存模型,即 p 號(hào)。
node:NUMA 節(jié)點(diǎn)號(hào),標(biāo)識(shí)該 page 屬于哪一個(gè)節(jié)點(diǎn)。
zone:內(nèi)存域標(biāo)志,標(biāo)識(shí)該 page 屬于哪一個(gè) zone。
flag:page 的狀態(tài)標(biāo)識(shí),常用的有:
內(nèi)核定義了一些標(biāo)準(zhǔn)宏,用于檢查頁(yè)面是否設(shè)置了某個(gè)特定的標(biāo)志位或者用于操作某些特定的標(biāo)志位,比如
PageXXX()(檢查是否設(shè)置) SetPageXXX() ClearPageXXX()伙伴系統(tǒng),內(nèi)存被分成含有很多頁(yè)面的大塊,每一塊都是 2 個(gè)頁(yè)面大小的方冪。如果找不到想要的塊, 一個(gè)大塊會(huì)被分成兩部分,這兩部分彼此就成為伙伴。其中一半被用來分配,而另一半則空閑。這些塊在以后分配的過程中會(huì)繼續(xù)被二分直至產(chǎn)生一個(gè)所需大小的塊。當(dāng)一個(gè)塊被最終釋放時(shí), 其伙伴將被檢測(cè)出來, 如果伙伴也空閑則合并兩者
slab,Slab 對(duì)小對(duì)象進(jìn)行分配,不用為每個(gè)小對(duì)象去分配頁(yè),節(jié)省了空間。內(nèi)核中一些小對(duì)象在創(chuàng)建析構(gòu)時(shí)很頻繁,Slab 對(duì)這些小對(duì)象做緩存,可以重復(fù)利用一些相同的對(duì)象,減少內(nèi)存分配次數(shù)
接著看struct list_head lru,鏈表頭,具體作用得看 page 處于什么用途中,如果是伙伴系統(tǒng)則用于連接相同的伙伴,通過第一個(gè) page 可以找到伙伴中所有的 page;如果是 slab,page->lru.next 指向 page 駐留的的緩存管理結(jié)構(gòu),page->lru.prec 指向保存該 page 的 slab 管理結(jié)構(gòu);而當(dāng) page 被用戶態(tài)使用或被當(dāng)做頁(yè)緩存使用時(shí),lru 則用于將該 page 連入 zone 中相應(yīng)的 lru 鏈表,供內(nèi)存回收時(shí)使用
struct address_space *mapping,當(dāng) mapping 為 NULL 時(shí),該 page 為交換緩存(swap);當(dāng) mapping 不為 NULL 且第 0 位為 0,該 page 為頁(yè)緩存或文件映射,mapping 指向文件的地址空間;當(dāng) mapping 不為 NULL 且第 0 位為 1,該 page 為匿名頁(yè)(匿名映射),mapping 指向 struct anon_vma 對(duì)象
pgoff_t index,映射虛擬內(nèi)存空間里的地址偏移,一個(gè)文件可能只映射其中的一部分,假設(shè)映射了 1M 的空間,index 指的是在 1M 空間內(nèi)的偏移,而不是在整個(gè)文件內(nèi)的偏移
unsigned long private,私有數(shù)據(jù)指針
atomic_t _mapcount,該 page 被頁(yè)表映射的次數(shù),即這個(gè) page 被多少個(gè)進(jìn)程共享,初始值為-1(非伙伴系統(tǒng),如果是伙伴系統(tǒng)則為 PAGE_BUDDY_MAPCOUNT_VALUE),例如只被一個(gè)進(jìn)程的頁(yè)表映射的話,值為 0
atomic_t _refcount,頁(yè)表引用計(jì)數(shù),內(nèi)核要操作該 page 時(shí),引用計(jì)數(shù)會(huì)+1,操作完成后則-1,當(dāng)引用計(jì)數(shù)為 0 時(shí),表示該 page 沒有被引用到,這時(shí)候就可以解除該 page 的映射(虛擬頁(yè)-物理頁(yè),該物理頁(yè)是占用內(nèi)存的)(用于內(nèi)存回收)
更詳細(xì)的內(nèi)容可以參考源代碼~
ok,回到 memmap_init_zone(),直接看關(guān)鍵函數(shù)**__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初始化,根據(jù)page大小還有一些特殊操作set_page_links(page,?zone,?nid,?pfn);?//flags初始化,將頁(yè)面映射到zone和nodeinit_page_count(page);?//page的_refcount設(shè)置為1page_mapcount_reset(page);?//page的_mapcount設(shè)置為-1INIT_LIST_HEAD(&page->lru);?//初始化lru,指向自身...... }至此,free_area_init_node()的初始化操作執(zhí)行完畢,據(jù)前面分析可以知道該函數(shù)主要是將整個(gè) linux 物理內(nèi)存管理框架進(jìn)行初始化,包括內(nèi)存管理節(jié)點(diǎn) node、管理區(qū) zone 以及頁(yè)面管理 page 等數(shù)據(jù)的初始化。
回到前面的 free_area_init()函數(shù)的循環(huán)體內(nèi)的最后兩個(gè)函數(shù) node_set_state()和 check_for_memory(),node_set_state()主要是對(duì) node 節(jié)點(diǎn)進(jìn)行狀態(tài)設(shè)置,而 check_for_memory()則是做內(nèi)存檢查。
到這里,內(nèi)存管理框架的構(gòu)建基本完畢。
void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_raCm_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內(nèi)存初始化............early_alloc_pgt_buf();?//頁(yè)表緩沖區(qū)分配reserve_brk();?//緩沖區(qū)加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動(dòng)......init_mem_mapping();?//低端內(nèi)存內(nèi)核頁(yè)表初始化?高端內(nèi)存固定映射區(qū)中臨時(shí)映射區(qū)頁(yè)表初始化......initmem_init();?//high_memory初始化?通過memblock內(nèi)存管理器設(shè)置node節(jié)點(diǎn)信息......x86_init.paging.pagetable_init();?//?節(jié)點(diǎn)node、內(nèi)存管理區(qū)zone、page初始化...... }最后補(bǔ)充一下 pfn 和物理地址以及 pfn 和虛擬地址的轉(zhuǎn)換。
//物理地址->物理頁(yè) #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 可以參考上面的內(nèi)存模型,不同的模型實(shí)現(xiàn)的細(xì)節(jié)不一樣。
這里可以看出物理頁(yè)的大小是 4096,即 4kb,雖然內(nèi)核在虛擬地址中是在高地址的,但是在物理地址中是從 0 開始的。
在 linux 內(nèi)核直接映射區(qū)里內(nèi)核邏輯地址與物理頁(yè)的轉(zhuǎn)換關(guān)系如下:
#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 與具體的架構(gòu)有關(guān),在 x86_32 中,PAGE_OFFSET 是 0xC0000000,即 32 位系統(tǒng)中,內(nèi)核的邏輯地址只有高位的 1GB
總結(jié)
Linux 內(nèi)存管理是一個(gè)很復(fù)雜的“工程”,Linux 會(huì)通過中斷調(diào)用獲取被 BIOS 保留的內(nèi)存地址范圍以及系統(tǒng)可以使用的內(nèi)存地址范圍。在內(nèi)核初始化階段,通過 memblock 內(nèi)存分配器,實(shí)現(xiàn)頁(yè)分配器初始化之前的內(nèi)存管理和分配請(qǐng)求,memblock 內(nèi)存區(qū)管理算法將可用可分配的內(nèi)存用 memblock.memory 進(jìn)行管理,已分配的內(nèi)存用 memblock.reserved 進(jìn)行管理,只要內(nèi)存塊加入到 memblock.reserved 里面就表示該內(nèi)存被申請(qǐng)占用了,申請(qǐng)和釋放的操作都集中在 memblock.reserved,這個(gè)算法效率不高,但是卻是合理的,因?yàn)樵趦?nèi)核初始化階段并沒有太多復(fù)雜的內(nèi)存操作場(chǎng)景,而且很多地方都是申請(qǐng)的內(nèi)存都是永久使用的。為了合理地利用 4G 的內(nèi)存空間,Linux 采用了 3:1 的策略,即內(nèi)核占用 1G 的線性地址空間,用戶占用 3G 的線性地址空間,且 Linux 采用了一種折中方案是只對(duì) 1G 內(nèi)核空間的前 896 MB 按線性映射, 剩下的 128 MB 采用動(dòng)態(tài)映射,即走多級(jí)頁(yè)表翻譯,這樣,內(nèi)核態(tài)能訪問空間就更多了。
傳統(tǒng)的多核運(yùn)算是使用 SMP(Symmetric Multi-Processor )模式,將多個(gè)處理器與一個(gè)集中的存儲(chǔ)器和 I/O 總線相連,所有處理器訪問同一個(gè)物理存儲(chǔ)器,因此 SMP 系統(tǒng)有時(shí)也被稱為一致存儲(chǔ)器訪問(UMA)結(jié)構(gòu)體系。而 NUMA 模式是一種分布式存儲(chǔ)器訪問方式,處理器可以同時(shí)訪問不同的存儲(chǔ)器地址,大幅度提高并行性。NUMA 模式下系統(tǒng)的每個(gè) CPU 都有本地內(nèi)存,可支持快速訪問,各個(gè)處理器之間通過總線連接起來,以支持對(duì)其它 CPU 本地內(nèi)存的訪問,但是這些訪問要比處理器本地內(nèi)存的慢。Linux 內(nèi)核通過插入一些兼容層,使兩個(gè)不同體系結(jié)構(gòu)的差異被隱藏,兩種模式都使用了同一個(gè)數(shù)據(jù)結(jié)構(gòu),另外 linux 的物理內(nèi)存管理機(jī)制將物理內(nèi)存劃分為三個(gè)層次來管理,依次是:Node(存儲(chǔ)節(jié)點(diǎn))、Zone(管理區(qū))和 Page(頁(yè)面)。
Linux 內(nèi)存管理的內(nèi)容十分多且復(fù)雜,上面介紹到的也只是其中的一部分,如果感興趣的話可以下載一份源代碼,然后細(xì)細(xì)品味。
參考文獻(xiàn)
Linux 內(nèi)存管理-Zone:https://blog.csdn.net/wyy4045/article/details/81776277
Linux 內(nèi)存管理-Node:https://blog.csdn.net/wyy4045/article/details/81708895
內(nèi)存管理框架: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
內(nèi)核頁(yè)表:https://www.daimajiaoliu.com/daima/47db35735100402
linux 內(nèi)存模型:http://www.wowotech.net/memory_management/memory_model.html
linux 中的分頁(yè)機(jī)制:http://edsionte.com/techblog/archives/3435
linux 內(nèi)核介紹:https://richardweiyang-2.gitbook.io/kernel-exploring
struct page:https://blog.csdn.net/gatieme/article/details/52384636
頁(yè)表初始化:https://www.cnblogs.com/tolimit/p/4585803.html
視頻號(hào)最新視頻
總結(jié)
以上是生活随笔為你收集整理的一文掌握 Linux 内存管理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 混沌工程将成标配?落地腾讯游戏后带来了哪
- 下一篇: 5月18发布会,这次TDSQL又有什么大