3atv精品不卡视频,97人人超碰国产精品最新,中文字幕av一区二区三区人妻少妇,久久久精品波多野结衣,日韩一区二区三区精品

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

一文掌握 Linux 内存管理

發布時間:2024/2/28 linux 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 一文掌握 Linux 内存管理 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

作者:dengxuanshi,騰訊 IEG 后臺開發工程師

以下源代碼來自 linux-5.10.3 內核代碼,主要以 x86-32 為例。

Linux 內存管理是一個很復雜的“工程”,它不僅僅是對物理內存的管理,也涉及到虛擬內存管理、內存交換和內存回收等

物理內存的探測

Linux 內核通過 detect_memory()函數實現對物理內存的探測

void?detect_memory(void) {detect_memory_e820();detect_memory_e801();detect_memory_88(); }

這里主要介紹一下 detect_memory_e820(),detect_memory_e801()和 detect_memory_88()是針對較老的電腦進行兼容而保留的

static?void?detect_memory_e820(void) {int?count?=?0;struct?biosregs?ireg,?oreg;struct?boot_e820_entry?*desc?=?boot_params.e820_table;static?struct?boot_e820_entry?buf;?/*?static?so?it?is?zeroed?*/initregs(&ireg);ireg.ax??=?0xe820;ireg.cx??=?sizeof(buf);ireg.edx?=?SMAP;ireg.di??=?(size_t)&buf;do?{intcall(0x15,?&ireg,?&oreg);ireg.ebx?=?oreg.ebx;?/*?for?next?iteration...?*/if?(oreg.eflags?&?X86_EFLAGS_CF)break;if?(oreg.eax?!=?SMAP)?{count?=?0;break;}*desc++?=?buf;count++;}?while?(ireg.ebx?&&?count?<?ARRAY_SIZE(boot_params.e820_table));boot_params.e820_entries?=?count; }

detect_memory_e820()實現內核從 BIOS 那里獲取到內存的基礎布局,之所以叫 e820 是因為內核是通過 0x15 中斷向量,并在 AX 寄存器中指定 0xE820,中斷調用后將會返回被 BIOS 保留的內存地址范圍以及系統可以使用的內存地址范圍,所有通過中斷獲取的數據將會填充在 boot_params.e820_table 中,具體 0xE820 的詳細用法感興趣的話可以上網查……這里獲取到的 e820_table 里的數據是未經過整理,linux 會通過 setup_memory_map 去整理這些數據

start_kernel()?->?setup_arch()?->?setup_memory_map() void?__init?e820__memory_setup(void) {char?*who;BUILD_BUG_ON(sizeof(struct?boot_e820_entry)?!=?20);who?=?x86_init.resources.memory_setup();memcpy(e820_table_kexec,?e820_table,?sizeof(*e820_table_kexec));memcpy(e820_table_firmware,?e820_table,?sizeof(*e820_table_firmware));pr_info("BIOS-provided?physical?RAM?map:\n");e820__print_table(who); }

x86_init.resources.memory_setup()指向了 e820__memory_setup_default(),會將 boot_params.e820_table 轉換為內核自己使用的 e820_table,轉換之后的e820 表記錄著所有物理內存的起始地址、長度以及類型,然后通過 memcpy 將 e820_table 復制到 e820_table_kexec、e820_table_firmware

struct?x86_init_ops?x86_init?__initdata?=?{.resources?=?{.probe_roms??=?probe_roms,.reserve_resources?=?reserve_standard_io_resources,.memory_setup??=?e820__memory_setup_default,},...... }char?*__init?e820__memory_setup_default(void) {char?*who?=?"BIOS-e820";/**?Try?to?copy?the?BIOS-supplied?E820-map.**?Otherwise?fake?a?memory?map;?one?p?from?0k->640k,*?the?next?p?from?1mb->appropriate_mem_k*/if?(append_e820_table(boot_params.e820_table,?boot_params.e820_entries)?<?0)?{u64?mem_size;/*?Compare?results?from?other?methods?and?take?the?one?that?gives?more?RAM:?*/if?(boot_params.alt_mem_k?<?boot_params.screen_info.ext_mem_k)?{mem_size?=?boot_params.screen_info.ext_mem_k;who?=?"BIOS-88";}?else?{mem_size?=?boot_params.alt_mem_k;who?=?"BIOS-e801";}e820_table->nr_entries?=?0;e820__range_add(0,?LOWMEMSIZE(),?E820_TYPE_RAM);e820__range_add(HIGH_MEMORY,?mem_size?<<?10,?E820_TYPE_RAM);}/*?We?just?appended?a?lot?of?ranges,?sanitize?the?table:?*/e820__update_table(e820_table);return?who; }

內核使用的e820_table 結構描述如下:

enum?e820_type?{E820_TYPE_RAM??=?1,E820_TYPE_RESERVED?=?2,E820_TYPE_ACPI??=?3,E820_TYPE_NVS??=?4,E820_TYPE_UNUSABLE?=?5,E820_TYPE_PMEM??=?7,E820_TYPE_PRAM??=?12,E820_TYPE_SOFT_RESERVED?=?0xefffffff,E820_TYPE_RESERVED_KERN?=?128, };struct?e820_entry?{u64???addr;u64???size;enum?e820_type??type; }?__attribute__((packed));struct?e820_table?{__u32?nr_entries;struct?e820_entry?entries[E820_MAX_ENTRIES]; };

memblock 內存分配器

linux x86 內存映射主要存在兩種方式:段式映射和頁式映射。linux 首次進入保護模式時會用到段式映射(加電時,運行在實模式,任意內存地址都能執行代碼,可以被讀寫,這非常不安全,CPU 為了提供限制/禁止的手段,提出了保護模式),根據段寄存器(以 8086 為例,段寄存器有 CS(Code Segment):代碼段寄存器;DS(Data Segment):數據段寄存器;SS(Stack Segment):堆棧段寄存器;ES(Extra Segment):附加段寄存器)查找到對應的段描述符(這里其實就是用到了段描述符表,即段表),段描述符指明了此時的環境可以通過段訪問到內存基地址、空間大小和訪問權限。訪問權限則點明了哪些內存可讀、哪些內存可寫。

typedef?struct?Descriptor{unsigned?int?base;??//?段基址unsigned?int?limit;?//?段大小unsigned?short?attribute;???//?段屬性、權限 }

linux 在段描述符表準備完成之后會通過匯編跳轉到保護模式

事實上,在上面這個過程中,linux 并沒有明顯地去區分每個段,所以這里并沒有很好地起到保護作用,linux 最終使用的還是內存分頁管理(開啟頁式映射可以參考/arch/x86/kernel/head_32.S)

memblock 算法

memblock 是 linux 內核初始化階段使用的一個內存分配器,實現較為簡單,負責頁分配器初始化之前的內存管理和分配請求,memblock 的結構如下

struct?memblock_region?{phys_addr_t?base;phys_addr_t?size;enum?memblock_flags?flags; #ifdef?CONFIG_NEED_MULTIPLE_NODESint?nid; #endif };struct?memblock_type?{unsigned?long?cnt;unsigned?long?max;phys_addr_t?total_size;struct?memblock_region?*regions;char?*name; };struct?memblock?{bool?bottom_up;??/*?is?bottom?up?direction??*/phys_addr_t?current_limit;struct?memblock_type?memory;struct?memblock_type?reserved; };

bottom_up:用來表示分配器分配內存是自低地址向高地址還是自高地址向低地址

current_limit:用來表示用來限制 memblock_alloc()和 memblock_alloc_base()的內存申請

memory:用于指向系統可用物理內存區,這個內存區維護著系統所有可用的物理內存,即系統 DRAM 對應的物理內存

reserved:用于指向系統預留區,也就是這個內存區的內存已經分配,在釋放之前不能再次分配這個區內的內存區塊

memblock_type中的cnt用于描述該類型內存區中的內存區塊數,這有利于 MEMBLOCK 內存分配器動態地知道某種類型的內存區還有多少個內存區塊

memblock_type 中的max用于描述該類型內存區最大可以含有多少個內存區塊,當往某種類型的內存區添加 內存區塊的時候,如果內存區的內存區塊數超過 max 成員,那么 memblock 內存分配器就會增加內存區的容量,以此維護更多的內存區塊

memblock_type 中的total_size用于統計內存區總共含有的物理內存數

memblock_type 中的regions是一個內存區塊鏈表,用于維護屬于這類型的所有內存區塊(包括基址、大小和內存塊標記等),

name :用于指明這個內存區的名字,MEMBLOCK 分配器目前支持的內存區名字有:“memory”, “reserved”, “physmem”

具體關系可以參考下圖:

內核啟動后會為 MEMBLOCK 內存分配器創建了一些私有的 p,這些 p 用于存放于 MEMBLOCK 分配器有關的函數和數據,即 init_memblockinitdata_memblock。在創建完 init_memblock p 和 initdata_memblock p 之后,memblock 分配器會開始創建 struct memblock 實例,這個實例此時作為最原始的 MEMBLOCK 分配器,描述了系統的物理內存的初始值

#define?MEMBLOCK_ALLOC_ANYWHERE?(~(phys_addr_t)0)?//即0xFFFFFFFF #define?INIT_MEMBLOCK_REGIONS???128#ifndef?INIT_MEMBLOCK_RESERVED_REGIONS #?define?INIT_MEMBLOCK_RESERVED_REGIONS??INIT_MEMBLOCK_REGIONS #endifstatic?struct?memblock_region?memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS]?__initdata_memblock; static?struct?memblock_region?memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS]?__initdata_memblock;struct?memblock?memblock?__initdata_memblock?=?{.memory.regions??=?memblock_memory_init_regions,.memory.cnt??=?1,?/*?empty?dummy?entry?*/.memory.max??=?INIT_MEMBLOCK_REGIONS,.memory.name??=?"memory",.reserved.regions?=?memblock_reserved_init_regions,.reserved.cnt??=?1,?/*?empty?dummy?entry?*/.reserved.max??=?INIT_MEMBLOCK_RESERVED_REGIONS,.reserved.name??=?"reserved",.bottom_up??=?false,.current_limit??=?MEMBLOCK_ALLOC_ANYWHERE, };

內核在 setup_arch(char **cmdline_p)中會調用e820__memblock_setup()對 MEMBLOCK 內存分配器進行初始化啟動

void?__init?e820__memblock_setup(void) {int?i;u64?end;memblock_allow_resize();for?(i?=?0;?i?<?e820_table->nr_entries;?i++)?{struct?e820_entry?*entry?=?&e820_table->entries[i];end?=?entry->addr?+?entry->size;if?(end?!=?(resource_size_t)end)continue;if?(entry->type?==?E820_TYPE_SOFT_RESERVED)memblock_reserve(entry->addr,?entry->size);if?(entry->type?!=?E820_TYPE_RAM?&&?entry->type?!=?E820_TYPE_RESERVED_KERN)continue;memblock_add(entry->addr,?entry->size);}/*?Throw?away?partial?pages:?*/memblock_trim_memory(PAGE_SIZE);memblock_dump_all(); }

memblock_allow_resize() 僅是用于置 memblock_can_resize 的值,里面的 for 則是用于循環遍歷 e820 的內存布局信息,然后進行 memblock_add 的操作

int?__init_memblock?memblock_add(phys_addr_t?base,?phys_addr_t?size) {phys_addr_t?end?=?base?+?size?-?1;memblock_dbg("%s:?[%pa-%pa]?%pS\n",?__func__,&base,?&end,?(void?*)_RET_IP_);return?memblock_add_range(&memblock.memory,?base,?size,?MAX_NUMNODES,?0); }

memblock_add()主要封裝了 memblock_add_region(),且它操作對象是 memblock.memory,即系統可用物理內存區。

static?int?__init_memblock?memblock_add_range(struct?memblock_type?*type,phys_addr_t?base,?phys_addr_t?size,int?nid,?enum?memblock_flags?flags) {bool?insert?=?false;phys_addr_t?obase?=?base;//調整size大小,確保不會越過邊界phys_addr_t?end?=?base?+?memblock_cap_size(base,?&size);int?idx,?nr_new;struct?memblock_region?*rgn;if?(!size)return?0;/*?special?case?for?empty?array?*/if?(type->regions[0].size?==?0)?{WARN_ON(type->cnt?!=?1?||?type->total_size);type->regions[0].base?=?base;type->regions[0].size?=?size;type->regions[0].flags?=?flags;memblock_set_region_node(&type->regions[0],?nid);type->total_size?=?size;return?0;} repeat:/**?The?following?is?executed?twice.??Once?with?%false?@insert?and*?then?with?%true.??The?first?counts?the?number?of?regions?needed*?to?accommodate?the?new?area.??The?second?actually?inserts?them.*/base?=?obase;nr_new?=?0;for_each_memblock_type(idx,?type,?rgn)?{phys_addr_t?rbase?=?rgn->base;phys_addr_t?rend?=?rbase?+?rgn->size;if?(rbase?>=?end)break;if?(rend?<=?base)continue;/**?@rgn?overlaps.??If?it?separates?the?lower?part?of?new*?area,?insert?that?portion.*/if?(rbase?>?base)?{ #ifdef?CONFIG_NEED_MULTIPLE_NODESWARN_ON(nid?!=?memblock_get_region_node(rgn)); #endifWARN_ON(flags?!=?rgn->flags);nr_new++;if?(insert)memblock_insert_region(type,?idx++,?base,rbase?-?base,?nid,flags);}/*?area?below?@rend?is?dealt?with,?forget?about?it?*/base?=?min(rend,?end);}/*?insert?the?remaining?portion?*/if?(base?<?end)?{nr_new++;if?(insert)memblock_insert_region(type,?idx,?base,?end?-?base,nid,?flags);}if?(!nr_new)return?0;/**?If?this?was?the?first?round,?resize?array?and?repeat?for?actual*?insertions;?otherwise,?merge?and?return.*/if?(!insert)?{while?(type->cnt?+?nr_new?>?type->max)if?(memblock_double_array(type,?obase,?size)?<?0)return?-ENOMEM;insert?=?true;goto?repeat;}?else?{memblock_merge_regions(type);return?0;} }

如果 memblock 算法管理的內存為空時,將當前空間添加進去

不為空的情況下,for_each_memblock_type(idx, type, rgn)這個循環會檢查是否存在內存重疊的情況,如果有的話,則剔除重疊部分,然后將其余非重疊的部分插入 memblock。內存塊的添加主要分為四種情況(其余情況與這幾種類似),可以參考下圖:

如果 region 空間不夠,則通過 memblock_double_array()添加新的空間,然后重試。

最后 memblock_merge_regions()會將緊挨著的內存進行合并(節點號、flag 等必須一致,節點號后面再進行介紹)。

memblock 內存分配與回收

到這里,memblock 內存管理的初始化基本完成,后面還有一些關于 memblock.memory 的修正,這里就不做介紹了,最后也簡單介紹一下memblock 的內存分配和回收,即 memblock_alloc()和 memblock_free()。

//size即分配區塊的大小,align用于字節對齊,表示分配區塊的對齊大小 //這里NUMA_NO_NODE指任何節點(沒有節點),關于節點后面會介紹,這里節點還沒初始化 //MEMBLOCK_ALLOC_ACCESSIBLE指分配內存塊時僅受memblock.current_limit的限制 #define?NUMA_NO_NODE?(-1) #define?MEMBLOCK_LOW_LIMIT?0 #define?MEMBLOCK_ALLOC_ACCESSIBLE?0static?inline?void?*?__init?memblock_alloc(phys_addr_t?size,??phys_addr_t?align) {return?memblock_alloc_try_nid(size,?align,?MEMBLOCK_LOW_LIMIT,MEMBLOCK_ALLOC_ACCESSIBLE,?NUMA_NO_NODE); }void?*?__init?memblock_alloc_try_nid(phys_addr_t?size,?phys_addr_t?align,phys_addr_t?min_addr,?phys_addr_t?max_addr,int?nid) {void?*ptr;memblock_dbg("%s:?%llu?bytes?align=0x%llx?nid=%d?from=%pa?max_addr=%pa?%pS\n",__func__,?(u64)size,?(u64)align,?nid,?&min_addr,&max_addr,?(void?*)_RET_IP_);ptr?=?memblock_alloc_internal(size,?align,min_addr,?max_addr,?nid,?false);if?(ptr)memset(ptr,?0,?size);return?ptr; }static?void?*?__init?memblock_alloc_internal(phys_addr_t?size,?phys_addr_t?align,phys_addr_t?min_addr,?phys_addr_t?max_addr,int?nid,?bool?exact_nid) {phys_addr_t?alloc;/**?Detect?any?accidental?use?of?these?APIs?after?slab?is?ready,?as?at*?this?moment?memblock?may?be?deinitialized?already?and?its*?internal?data?may?be?destroyed?(after?execution?of?memblock_free_all)*/if?(WARN_ON_ONCE(slab_is_available()))return?kzalloc_node(size,?GFP_NOWAIT,?nid);if?(max_addr?>?memblock.current_limit)max_addr?=?memblock.current_limit;alloc?=?memblock_alloc_range_nid(size,?align,?min_addr,?max_addr,?nid,exact_nid);/*?retry?allocation?without?lower?limit?*/if?(!alloc?&&?min_addr)alloc?=?memblock_alloc_range_nid(size,?align,?0,?max_addr,?nid,exact_nid);if?(!alloc)return?NULL;return?phys_to_virt(alloc); }

memblock_alloc_internal 返回的是分配到的內存塊的虛擬地址,為 NULL 表示分配失敗,關于 phys_to_virt 的實現后面再介紹,這里主要看 memblock_alloc_range_nid 的實現。

phys_addr_t?__init?memblock_alloc_range_nid(phys_addr_t?size,phys_addr_t?align,?phys_addr_t?start,phys_addr_t?end,?int?nid,bool?exact_nid) {enum?memblock_flags?flags?=?choose_memblock_flags();phys_addr_t?found;if?(WARN_ONCE(nid?==?MAX_NUMNODES,?"Usage?of?MAX_NUMNODES?is?deprecated.?Use?NUMA_NO_NODE?instead\n"))nid?=?NUMA_NO_NODE;if?(!align)?{/*?Can't?use?WARNs?this?early?in?boot?on?powerpc?*/dump_stack();align?=?SMP_CACHE_BYTES;}again:found?=?memblock_find_in_range_node(size,?align,?start,?end,?nid,flags);if?(found?&&?!memblock_reserve(found,?size))goto?done;if?(nid?!=?NUMA_NO_NODE?&&?!exact_nid)?{found?=?memblock_find_in_range_node(size,?align,?start,end,?NUMA_NO_NODE,flags);if?(found?&&?!memblock_reserve(found,?size))goto?done;}if?(flags?&?MEMBLOCK_MIRROR)?{flags?&=?~MEMBLOCK_MIRROR;pr_warn("Could?not?allocate?%pap?bytes?of?mirrored?memory\n",&size);goto?again;}return?0;done:if?(end?!=?MEMBLOCK_ALLOC_KASAN)kmemleak_alloc_phys(found,?size,?0,?0);return?found; }

kmemleak 是一個檢查內存泄漏的工具,這里就不做介紹了。

memblock_alloc_range_nid()首先對 align 參數進行檢測,如果為零,則警告。接著函數調用 memblock_find_in_range_node() 函數從可用內存區中找一塊大小為 size 的物理內存區塊, 然后調用 memblock_reseve() 函數在找到的情況下,將這塊物理內存區塊加入到預留區內。

static?phys_addr_t?__init_memblock?memblock_find_in_range_node(phys_addr_t?size,phys_addr_t?align,?phys_addr_t?start,phys_addr_t?end,?int?nid,enum?memblock_flags?flags) {phys_addr_t?kernel_end,?ret;/*?pump?up?@end?*/if?(end?==?MEMBLOCK_ALLOC_ACCESSIBLE?||end?==?MEMBLOCK_ALLOC_KASAN)end?=?memblock.current_limit;/*?avoid?allocating?the?first?page?*/start?=?max_t(phys_addr_t,?start,?PAGE_SIZE);end?=?max(start,?end);kernel_end?=?__pa_symbol(_end);if?(memblock_bottom_up()?&&?end?>?kernel_end)?{phys_addr_t?bottom_up_start;/*?make?sure?we?will?allocate?above?the?kernel?*/bottom_up_start?=?max(start,?kernel_end);/*?ok,?try?bottom-up?allocation?first?*/ret?=?__memblock_find_range_bottom_up(bottom_up_start,?end,size,?align,?nid,?flags);if?(ret)return?ret;WARN_ONCE(IS_ENABLED(CONFIG_MEMORY_HOTREMOVE),"memblock:?bottom-up?allocation?failed,?memory?hotremove?may?be?affected\n");}return?__memblock_find_range_top_down(start,?end,?size,?align,?nid,flags); }

memblock 分配器分配內存是支持自低地址向高地址和自高地址向低地址的,如果 memblock_bottom_up() 函數返回 true,表示 MEMBLOCK 從低向上分配,而前面初始化的時候這個返回值其實是 false(某些情況下不一定),所以這里主要看__memblock_find_range_top_down 的實現(__memblock_find_range_bottom_up 的實現是類似的)。

static?phys_addr_t?__init_memblock __memblock_find_range_top_down(phys_addr_t?start,?phys_addr_t?end,phys_addr_t?size,?phys_addr_t?align,?int?nid,enum?memblock_flags?flags) {phys_addr_t?this_start,?this_end,?cand;u64?i;for_each_free_mem_range_reverse(i,?nid,?flags,?&this_start,?&this_end,NULL)?{this_start?=?clamp(this_start,?start,?end);this_end?=?clamp(this_end,?start,?end);if?(this_end?<?size)continue;cand?=?round_down(this_end?-?size,?align);if?(cand?>=?this_start)return?cand;}return?0; }

Clamp 函數可以將隨機變化的數值限制在一個給定的區間[min, max]內。

__memblock_find_range_top_down()通過使用 for_each_free_mem_range_reverse 宏封裝調用__next_free_mem_range_rev()函數,該函數逐一將 memblock.memory 里面的內存塊信息提取出來與 memblock.reserved 的各項信息進行檢驗,確保返回的 this_start 和 this_end 不會與 reserved 的內存存在交叉重疊的情況。判斷大小是否滿足,滿足的情況下,將自末端向前(因為這是 top-down 申請方式)的 size 大小的空間的起始地址(前提該地址不會超出 this_start)返回回去,至此滿足要求的內存塊找到了。

最后看一下memblock_free的實現:

int?__init_memblock?memblock_free(phys_addr_t?base,?phys_addr_t?size) {phys_addr_t?end?=?base?+?size?-?1;memblock_dbg("%s:?[%pa-%pa]?%pS\n",?__func__,&base,?&end,?(void?*)_RET_IP_);kmemleak_free_part_phys(base,?size);return?memblock_remove_range(&memblock.reserved,?base,?size); }

這里直接看最核心的部分:

static?int?__init_memblock?memblock_remove_range(struct?memblock_type?*type,phys_addr_t?base,?phys_addr_t?size) {int?start_rgn,?end_rgn;int?i,?ret;ret?=?memblock_isolate_range(type,?base,?size,?&start_rgn,?&end_rgn);if?(ret)return?ret;for?(i?=?end_rgn?-?1;?i?>=?start_rgn;?i--)memblock_remove_region(type,?i);return?0; } static?void?__init_memblock?memblock_remove_region(struct?memblock_type?*type,?unsigned?long?r) {type->total_size?-=?type->regions[r].size;memmove(&type->regions[r],?&type->regions[r?+?1],(type->cnt?-?(r?+?1))?*?sizeof(type->regions[r]));type->cnt--;/*?Special?case?for?empty?arrays?*/if?(type->cnt?==?0)?{WARN_ON(type->total_size?!=?0);type->cnt?=?1;type->regions[0].base?=?0;type->regions[0].size?=?0;type->regions[0].flags?=?0;memblock_set_region_node(&type->regions[0],?MAX_NUMNODES);} }

其主要功能是將指定下標索引的內存項從 memblock.reserved 中移除

在__memblock_remove()里面,memblock_isolate_range()主要作用是將要移除的物理內存區從 reserved 內存區中分離出來,將 start_rgn 和 end_rgn(該內存區塊的起始、結束索引號)返回回去。

memblock_isolate_range()返回后,接著調用 memblock_remove_region() 函數將這些索引對應的內存區塊從內存區中移除,這里具體做法為調用 memmove 函數將 r 索引之后的內存區塊全部往前挪一個位置,這樣 r 索引對應的內存區塊就被移除了,如果移除之后,內存區不含有任何內存區塊,那么就初始化該內存區。

memblock 內存管理總結

memblock 內存區管理算法將可用可分配的內存用 memblock.memory 進行管理,已分配的內存用 memblock.reserved 進行管理,只要內存塊加入到 memblock.reserved 里面就表示該內存被申請占用了,另外,在內存申請的時候,memblock 僅是把被申請到的內存加入到 memblock.reserved 中,而沒有對 memblock.memory 進行任何刪除或改動操作,因此申請和釋放的操作都集中在 memblock.reserved。這個算法效率不高,但是卻是合理的,因為在內核初始化階段并沒有太多復雜的內存操作場景,而且很多地方都是申請的內存都是永久使用的。



內核頁表

上面有提到,內核會通過/arch/x86/kernel/head_32.S 開啟頁式映射,不過這里建立的頁表只是臨時頁表,而內核真正需要建立的是內核頁表。

內核虛擬地址空間與用戶虛擬地址空間

為了合理地利用 4G 的內存空間,Linux 采用了 3:1 的策略,即內核占用 1G 的線性地址空間,用戶占用 3G 的線性地址空間,由于歷史原因,用戶進程的地址范圍從 0~3G,內核地址范圍從 3G~4G,而內核的那 1GB 地址空間又稱為內核虛擬地址(邏輯地址)空間,雖然內核在虛擬地址中是在高地址的,但是在物理地址中是從 0 開始的。

內核虛擬地址與用戶虛擬地址,這兩者都是虛擬地址,都需要經過 MMU 的翻譯轉換為物理地址,從硬件層面上來看,所謂的內核虛擬地址和用戶虛擬地址只是權限不一樣而已,但在軟件層面上看,就大不相同了,當進程需要知道一個用戶空間虛擬地址對應的物理地址時,linux 內核需要通過頁表來得到它的物理地址,而內核空間虛擬地址是所有進程共享的,也就是說,內核在初始化時,就可以創建內核虛擬地址空間的映射,并且這里的映射就是線性映射,它基本等同于物理地址,只是它們之間有一個固定的偏移,當內核需要獲取該物理地址時,可以繞開頁表翻譯直接通過偏移計算拿到,當然這是從軟件層面上來看的,當內核去訪問該頁時, 硬件層面仍然走的是 MMU 翻譯的全過程。

至于為什么用戶虛擬地址空間不能也像內核虛擬地址空間這么做,原因是用戶地址空間是隨進程創建才產生的,無法事先給它分配一塊連續的內存

內核通過內核虛擬地址可以直接訪問到對應的物理地址,那內核如何使用其它的用戶虛擬地址(0~3G)?

Linux 采用的一種折中方案是只對 1G 內核空間的前 896 MB 按線性映射, 剩下的 128 MB 采用動態映射,即走多級頁表翻譯,這樣,內核態能訪問空間就更多了。這里 linux 內核把這 896M 的空間稱為 NORMAL 內存,剩下的 128M 稱為高端內存,即 highmem。在 64 位處理器上,內核空間大大增加,所以也就不需要高端內存了,但是仍然保留了動態映射。

動態映射不全是為了內核空間可以訪問更多的物理內存,還有一個重要原因:如果內核空間全線性映射,那么很可能就會出現內核空間碎片化而滿足不了很多連續頁面分配的需求(這類似于內存分段與內存分頁)。因此內核空間也必須有一部分是非線性映射,從而在這碎片化物理地址空間上,用頁表構造出連續虛擬地址空間(虛擬地址連續、物理地址不連續),這就是所謂的 vmalloc 空間。

到這里,可以大致知道linux 虛擬內存的構造

linux 內存分頁

linux 內核主要是通過內存分頁來管理內存的,這里先介紹兩個重要的變量:max_pfn 和 max_low_pfn。max_pfn 為最大物理內存頁面幀號,max_low_pfn 為低端內存區的最大可用頁幀號,它們的初始化如下:

void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_ram_pfn();..... #ifdef?CONFIG_X86_32/*?max_low_pfn?get?updated?here?*/find_low_pfn_range(); #elsecheck_x2apic();/*?How?many?end-of-memory?variables?you?have,?grandma!?*//*?need?this?before?calling?reserve_initrd?*/if?(max_pfn?>?(1UL<<(32?-?PAGE_SHIFT)))max_low_pfn?=?e820__end_of_low_ram_pfn();elsemax_low_pfn?=?max_pfn;high_memory?=?(void?*)__va(max_pfn?*?PAGE_SIZE?-?1)?+?1; #endif......e820__memblock_setup();...... }

其中 e820__end_of_ram_pfn 的實現如下,其中 E820_TYPE_RAM 代表可用物理內存類型

#define?PAGE_SHIFT??12#ifdef?CONFIG_X86_32 #?ifdef?CONFIG_X86_PAE #??define?MAX_ARCH_PFN??(1ULL<<(36-PAGE_SHIFT)) #?else //32位系統,1<<20,即0x100000,代表4G物理內存的最大頁面幀號 #??define?MAX_ARCH_PFN??(1ULL<<(32-PAGE_SHIFT)) #?endif #else?/*?CONFIG_X86_32?*/ #?define?MAX_ARCH_PFN?MAXMEM>>PAGE_SHIFT #endifunsigned?long?__init?e820__end_of_ram_pfn(void) {return?e820_end_pfn(MAX_ARCH_PFN,?E820_TYPE_RAM); }/**?Find?the?highest?page?frame?number?we?have?available*/ static?unsigned?long?__init?e820_end_pfn(unsigned?long?limit_pfn,?enum?e820_type?type) {int?i;unsigned?long?last_pfn?=?0;unsigned?long?max_arch_pfn?=?MAX_ARCH_PFN;for?(i?=?0;?i?<?e820_table->nr_entries;?i++)?{struct?e820_entry?*entry?=?&e820_table->entries[i];unsigned?long?start_pfn;unsigned?long?end_pfn;if?(entry->type?!=?type)continue;start_pfn?=?entry->addr?>>?PAGE_SHIFT;end_pfn?=?(entry->addr?+?entry->size)?>>?PAGE_SHIFT;if?(start_pfn?>=?limit_pfn)continue;if?(end_pfn?>?limit_pfn)?{last_pfn?=?limit_pfn;break;}if?(end_pfn?>?last_pfn)last_pfn?=?end_pfn;}if?(last_pfn?>?max_arch_pfn)last_pfn?=?max_arch_pfn;pr_info("last_pfn?=?%#lx?max_arch_pfn?=?%#lx\n",last_pfn,?max_arch_pfn);return?last_pfn; }

e820__end_of_ram_pfn其實就是遍歷e820_table,得到內存塊的起始地址以及內存塊大小,將起始地址右移 PAGE_SHIFT,算出其起始地址對應的頁面幀號,同時根據內存塊大小可以算出結束地址的頁號,如果結束頁號大于 limit_pfn,則設置該頁號為為 limit_pfn,然后通過比較得到一個 last_pfn,即系統真正的最大物理頁號。

max_low_pfn的計算則調用到了find_low_pfn_range

#define?PFN_UP(x)?(((x)?+?PAGE_SIZE-1)?>>?PAGE_SHIFT) #define?PFN_DOWN(x)?((x)?>>?PAGE_SHIFT) #define?PFN_PHYS(x)?((phys_addr_t)(x)?<<?PAGE_SHIFT)#ifndef?__pa #define?__pa(x)??__phys_addr((unsigned?long)(x)) #endif#define?VMALLOC_RESERVE??SZ_128M #define?VMALLOC_END??(CONSISTENT_BASE?-?PAGE_SIZE) #define?VMALLOC_START??((VMALLOC_END)?-?VMALLOC_RESERVE) #define?VMALLOC_VMADDR(x)?((unsigned?long)(x)) #define?MAXMEM???__pa(VMALLOC_START) #define?MAXMEM_PFN??PFN_DOWN(MAXMEM)void?__init?find_low_pfn_range(void) {/*?it?could?update?max_pfn?*/if?(max_pfn?<=?MAXMEM_PFN)lowmem_pfn_init();elsehighmem_pfn_init(); }

PFN_DOWN(x)是用來返回小于 x 的最后一個物理頁號,PFN_UP(x)是用來返回大于 x 的第一個物理頁號,這里 x 即物理地址,而 PFN_PHYS(x)返回的是物理頁號 x 對應的物理地址

__pa 其實就是通過虛擬地址計算出物理地址,這一塊后面再做講解。

將 MAXMEM 展開一下可得

#ifdef?CONFIG_HIGHMEM #define?CONSISTENT_BASE??((PKMAP_BASE)?-?(SZ_2M)) #define?CONSISTENT_END??(PKMAP_BASE) #else #define?CONSISTENT_BASE??(FIXADDR_START?-?SZ_2M) #define?CONSISTENT_END??(FIXADDR_START) #endif#define?SZ_2M????0x00200000 #define?SZ_128M????0x08000000#define?MAXMEM??????????????__pa(VMALLOC_END?–?PAGE_OFFSET?–?__VMALLOC_RESERVE) //進一步展開 #define?MAXMEM??????????????__pa(CONSISTENT_BASE?-?PAGE_SIZE?–?PAGE_OFFSET?–?SZ_128M) //再進一步展開 #define?MAXMEM??????????????__pa((PKMAP_BASE)?-?(SZ_2M)?-?PAGE_SIZE?–?PAGE_OFFSET?–?SZ_128M)

下面這一部分就涉及到高端內存的構成了,其中PKMAP_BASE 是持久映射空間(KMAP 空間,持久映射區)的起始地址,LAST_PKMAP 則是持久映射空間的映射頁面數,而 FIXADDR_TOP 是固定映射區(臨時內核映射區)的末尾,FIXADDR_START 是固定映射區起始地址,其中的__end_of_permanent_fixed_addresses 是固定映射的一個標志(一個枚舉值,具體可以參考\arch\x86\include\asm\fixmap.h 里的 enum fixed_addresses)。最后的 VMALLOC_END 即為動態映射區的末尾。

//臨時映射 //-4096(4KB)?->?0xfffff000 #define?__FIXADDR_TOP?(-PAGE_SIZE)#define?FIXADDR_TOP?((unsigned?long)__FIXADDR_TOP) #define?FIXADDR_SIZE??(__end_of_permanent_fixed_addresses?<<?PAGE_SHIFT) #define?FIXADDR_START??(FIXADDR_TOP?-?FIXADDR_SIZE) #define?FIXADDR_TOT_SIZE?(__end_of_fixed_addresses?<<?PAGE_SHIFT) #define?FIXADDR_TOT_START?(FIXADDR_TOP?-?FIXADDR_TOT_SIZE)//持久內核映射 #ifdef?CONFIG_X86_PAE #define?LAST_PKMAP?512 #else #define?LAST_PKMAP?1024 #endif#define?CPU_ENTRY_AREA_BASE?\((FIXADDR_TOT_START?-?PAGE_SIZE*(CPU_ENTRY_AREA_PAGES+1))?&?PMD_MASK)#define?LDT_BASE_ADDR??\((CPU_ENTRY_AREA_BASE?-?PAGE_SIZE)?&?PMD_MASK)#define?LDT_END_ADDR??(LDT_BASE_ADDR?+?PMD_SIZE)#define?PKMAP_BASE??\((LDT_BASE_ADDR?-?PAGE_SIZE)?&?PMD_MASK)//動態映射 //0xffffff80<<20?->?0xf8000000?->?4,160,749,568?->?3948MB?->?3GB+896MB?與上述一致 #define?high_memory?(-128UL?<<?20) //8MB #define?VMALLOC_OFFSET?(8?*?1024?*?1024) #define?VMALLOC_START?((unsigned?long)high_memory?+?VMALLOC_OFFSET)#ifdef?CONFIG_HIGHMEM #?define?VMALLOC_END?(PKMAP_BASE?-?2?*?PAGE_SIZE) #else #?define?VMALLOC_END?(LDT_BASE_ADDR?-?2?*?PAGE_SIZE) #endif

直接看圖~

PAGE_OFFSET 代表的是內核空間和用戶空間對虛擬地址空間的劃分,對不同的體系結構不同。比如在 32 位系統中 3G-4G 屬于內核使用的內存空間,所以 PAGE_OFFSET = 0xC0000000

內核空間如上圖,可分為直接內存映射區和高端內存映射區,其中直接內存映射區是指 3G 到 3G+896M 的線性空間,直接對應物理地址就是 0 到 896M(前提是有超過 896M 的物理內存),其中 896M 是 high_memory 值,使用 kmalloc()/kfree()接口申請釋放內存;而高端內存映射區則是超過 896M 物理內存的空間,它又分為動態映射區、持久映射區和固定映射區

動態內存映射區,又稱之為 vmalloc 映射區或非連續映射區,是指 VMALLOC_START 到 VMALLOC_END 的地址空間,申請釋放內存的接口是 vmalloc()/vfree(),通常用于將非連續的物理內存映射為連續的線性地址內存空間;

而持久映射區,又稱之為 KMAP 區或永久映射區,是指自 PKMAP_BASE 開始共 LAST_PKMAP 個頁面大小的空間,操作接口是 kmap()/kunmap(),用于將高端內存長久映射到內存虛擬地址空間中;

最后的固定映射區,有時候也稱為臨時映射區,是指 FIXADDR_START 到 FIXADDR_TOP 的地址空間,操作接口是 kmap_atomic()/kummap_atomic(),用于解決持久映射不能用于中斷處理程序而增加的臨時內核映射。

上面的MAXMEM_PFN 其實就是用來判斷是否初始化(啟用)高端內存,當內存物理頁數本來就小于低端內存的最大物理頁數時,就沒有高端地址映射

這里接著看 max_low_pfn 的初始化,進入 highmem_pfn_init(void)。

static?void?__init?highmem_pfn_init(void) {max_low_pfn?=?MAXMEM_PFN;if?(highmem_pages?==?-1)highmem_pages?=?max_pfn?-?MAXMEM_PFN;if?(highmem_pages?+?MAXMEM_PFN?<?max_pfn)max_pfn?=?MAXMEM_PFN?+?highmem_pages;if?(highmem_pages?+?MAXMEM_PFN?>?max_pfn)?{printk(KERN_WARNING?MSG_HIGHMEM_TOO_SMALL,pages_to_mb(max_pfn?-?MAXMEM_PFN),pages_to_mb(highmem_pages));highmem_pages?=?0;} #ifndef?CONFIG_HIGHMEM/*?Maximum?memory?usable?is?what?is?directly?addressable?*/printk(KERN_WARNING?"Warning?only?%ldMB?will?be?used.\n",?MAXMEM>>20);if?(max_pfn?>?MAX_NONPAE_PFN)printk(KERN_WARNING?"Use?a?HIGHMEM64G?enabled?kernel.\n");elseprintk(KERN_WARNING?"Use?a?HIGHMEM?enabled?kernel.\n");max_pfn?=?MAXMEM_PFN; #else?/*?!CONFIG_HIGHMEM?*/ #ifndef?CONFIG_HIGHMEM64Gif?(max_pfn?>?MAX_NONPAE_PFN)?{max_pfn?=?MAX_NONPAE_PFN;printk(KERN_WARNING?MSG_HIGHMEM_TRIMMED);} #endif?/*?!CONFIG_HIGHMEM64G?*/ #endif?/*?!CONFIG_HIGHMEM?*/ }

highmem_pfn_init 的主要工作其實就是把 max_low_pfn 設置為 MAXMEM_PFN,將 highmem_pages 設置為 max_pfn – MAXMEM_PFN,至此,max_pfn 和 max_low_pfn 初始化完畢。

低端內存初始化

回到 setup_arch 函數:

void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_ram_pfn();..... #ifdef?CONFIG_X86_32/*?max_low_pfn?get?updated?here?*/find_low_pfn_range(); #elsecheck_x2apic();/*?How?many?end-of-memory?variables?you?have,?grandma!?*//*?need?this?before?calling?reserve_initrd?*/if?(max_pfn?>?(1UL<<(32?-?PAGE_SHIFT)))max_low_pfn?=?e820__end_of_low_ram_pfn();elsemax_low_pfn?=?max_pfn;high_memory?=?(void?*)__va(max_pfn?*?PAGE_SIZE?-?1)?+?1; #endif......early_alloc_pgt_buf();?//<-------------------------------------/**?Need?to?conclude?brk,?before?e820__memblock_setup()*??it?could?use?memblock_find_in_range,?could?overlap?with*??brk?area.*/reserve_brk();?//<-------------------------------------------......e820__memblock_setup();...... }

early_alloc_pgt_buf()即申請頁表緩沖區

#define?INIT_PGD_PAGE_COUNT??????6 #define?INIT_PGT_BUF_SIZE?(INIT_PGD_PAGE_COUNT?*?PAGE_SIZE) RESERVE_BRK(early_pgt_alloc,?INIT_PGT_BUF_SIZE); void??__init?early_alloc_pgt_buf(void) {unsigned?long?tables?=?INIT_PGT_BUF_SIZE;phys_addr_t?base;base?=?__pa(extend_brk(tables,?PAGE_SIZE));pgt_buf_start?=?base?>>?PAGE_SHIFT;pgt_buf_end?=?pgt_buf_start;pgt_buf_top?=?pgt_buf_start?+?(tables?>>?PAGE_SHIFT); }

pgt_buf_start:標識該緩沖空間的起始物理內存頁框號;

pgt_buf_end:初始化時和 pgt_buf_start 是同一個值,但是它是用于表示該空間未被申請使用的空間起始頁框號;

pgt_buf_top:則是用來表示緩沖空間的末尾,存放的是該末尾的頁框號

INIT_PGT_BUF_SIZE 即 24KB,這里直接看最關鍵的部分:extend_brk

unsigned?long?_brk_start?=?(unsigned?long)__brk_base; unsigned?long?_brk_end???=?(unsigned?long)__brk_base;void?*?__init?extend_brk(size_t?size,?size_t?align) {size_t?mask?=?align?-?1;void?*ret;BUG_ON(_brk_start?==?0);BUG_ON(align?&?mask);_brk_end?=?(_brk_end?+?mask)?&?~mask;BUG_ON((char?*)(_brk_end?+?size)?>?__brk_limit);ret?=?(void?*)_brk_end;_brk_end?+=?size;memset(ret,?0,?size);return?ret; }

BUG_ON()函數是內核標記 bug、提供斷言并輸出信息的常用手段

__brk_base 相關的初始化可以參考 arch\x86\kernel\vmlinux.lds.S

在 setup_arch()中,緊接著 early_alloc_pgt_buf()還有 reserve_brk()函數

static?void?__init?reserve_brk(void) {if?(_brk_end?>?_brk_start)memblock_reserve(__pa_symbol(_brk_start),_brk_end?-?_brk_start);/*?Mark?brk?area?as?locked?down?and?no?longer?taking?anynew?allocations?*/_brk_start?=?0; }

這個地方主要是將 early_alloc_pgt_buf()申請的空間在 membloc 中做 reserved 保留操作,避免被其它地方申請使用而引發異常

回到 setup_arch 函數:

void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_ram_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內存初始化............early_alloc_pgt_buf();?//頁表緩沖區分配reserve_brk();?//緩沖區加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動......init_mem_mapping();?//<-----------------------------...... }

init_mem_mapping()即低端內存內核頁表初始化的關鍵函數

#define?ISA_END_ADDRESS??0x00100000?//1MBvoid?__init?init_mem_mapping(void) {unsigned?long?end;pti_check_boottime_disable();probe_page_size_mask();setup_pcid();#ifdef?CONFIG_X86_64end?=?max_pfn?<<?PAGE_SHIFT; #elseend?=?max_low_pfn?<<?PAGE_SHIFT; #endif/*?the?ISA?range?is?always?mapped?regardless?of?memory?holes?*/init_memory_mapping(0,?ISA_END_ADDRESS,?PAGE_KERNEL);/*?Init?the?trampoline,?possibly?with?KASLR?memory?offset?*/init_trampoline();/**?If?the?allocation?is?in?bottom-up?direction,?we?setup?direct?mapping*?in?bottom-up,?otherwise?we?setup?direct?mapping?in?top-down.*/if?(memblock_bottom_up())?{unsigned?long?kernel_end?=?__pa_symbol(_end);memory_map_bottom_up(kernel_end,?end);memory_map_bottom_up(ISA_END_ADDRESS,?kernel_end);}?else?{memory_map_top_down(ISA_END_ADDRESS,?end);}#ifdef?CONFIG_X86_64if?(max_pfn?>?max_low_pfn)?{/*?can?we?preseve?max_low_pfn??*/max_low_pfn?=?max_pfn;} #elseearly_ioremap_page_table_range_init(); #endifload_cr3(swapper_pg_dir);__flush_tlb_all();x86_init.hyper.init_mem_mapping();early_memtest(0,?max_pfn_mapped?<<?PAGE_SHIFT); }

probe_page_size_mask()主要作用是初始化直接映射變量(直接映射區相關)以及根據配置來控制 CR4 寄存器的置位,用于后面分頁時頁面大小的判定。

上面 init_memory_mapping 的參數 ISA_END_ADDRESS 表示 ISA 總線上設備的末尾地址。

init_memory_mapping(0, ISA_END_ADDRESS, PAGE_KERNEL)初始化 0 ~ 1MB 的物理地址,一般內核啟動時被安裝在 1MB 開始處,這里初始化完成之后會調用到 memory_map_bottom_up 或者 memory_map_top_down,后面就是初始化 1MB ~ 內核結束地址 這塊物理地址區域 ,最后也會回歸到 init_memory_mapping 的調用,因此這里不做過多的介紹,直接看 init_memory_mapping():

#ifdef?CONFIG_X86_32 #define?NR_RANGE_MR?3 #else?/*?CONFIG_X86_64?*/ #define?NR_RANGE_MR?5 #endifstruct?map_range?{unsigned?long?start;unsigned?long?end;unsigned?page_size_mask; };unsigned?long?__ref?init_memory_mapping(unsigned?long?start,unsigned?long?end,?pgprot_t?prot) {struct?map_range?mr[NR_RANGE_MR];unsigned?long?ret?=?0;int?nr_range,?i;pr_debug("init_memory_mapping:?[mem?%#010lx-%#010lx]\n",start,?end?-?1);memset(mr,?0,?sizeof(mr));nr_range?=?split_mem_range(mr,?0,?start,?end);for?(i?=?0;?i?<?nr_range;?i++)ret?=?kernel_physical_mapping_init(mr[i].start,?mr[i].end,mr[i].page_size_mask,prot);add_pfn_range_mapped(start?>>?PAGE_SHIFT,?ret?>>?PAGE_SHIFT);return?ret?>>?PAGE_SHIFT; }

struct map_range,該結構是用來保存內存段信息,其包含了一個段的起始地址、結束地址,以及該段是按多大的頁面進行分頁(4K、2M、1G,1G 是 64 位的,所以這里不提及)。

static?int?__meminit?split_mem_range(struct?map_range?*mr,?int?nr_range,unsigned?long?start,unsigned?long?end) {unsigned?long?start_pfn,?end_pfn,?limit_pfn;unsigned?long?pfn;int?i;//返回小于...的最后一個物理頁號limit_pfn?=?PFN_DOWN(end);pfn?=?start_pfn?=?PFN_DOWN(start);if?(pfn?==?0)end_pfn?=?PFN_DOWN(PMD_SIZE);elseend_pfn?=?round_up(pfn,?PFN_DOWN(PMD_SIZE));if?(end_pfn?>?limit_pfn)end_pfn?=?limit_pfn;if?(start_pfn?<?end_pfn)?{nr_range?=?save_mr(mr,?nr_range,?start_pfn,?end_pfn,?0);pfn?=?end_pfn;}/*?big?page?(2M)?range?*/start_pfn?=?round_up(pfn,?PFN_DOWN(PMD_SIZE));end_pfn?=?round_down(limit_pfn,?PFN_DOWN(PMD_SIZE));if?(start_pfn?<?end_pfn)?{nr_range?=?save_mr(mr,?nr_range,?start_pfn,?end_pfn,page_size_mask?&?(1<<PG_LEVEL_2M));pfn?=?end_pfn;}/*?tail?is?not?big?page?(2M)?alignment?*/start_pfn?=?pfn;end_pfn?=?limit_pfn;nr_range?=?save_mr(mr,?nr_range,?start_pfn,?end_pfn,?0);if?(!after_bootmem)adjust_range_page_size_mask(mr,?nr_range);/*?try?to?merge?same?page?size?and?continuous?*/for?(i?=?0;?nr_range?>?1?&&?i?<?nr_range?-?1;?i++)?{unsigned?long?old_start;if?(mr[i].end?!=?mr[i+1].start?||mr[i].page_size_mask?!=?mr[i+1].page_size_mask)continue;/*?move?it?*/old_start?=?mr[i].start;memmove(&mr[i],?&mr[i+1],(nr_range?-?1?-?i)?*?sizeof(struct?map_range));mr[i--].start?=?old_start;nr_range--;}for?(i?=?0;?i?<?nr_range;?i++)pr_debug("?[mem?%#010lx-%#010lx]?page?%s\n",mr[i].start,?mr[i].end?-?1,page_size_string(&mr[i]));return?nr_range; }

PMD_SIZE 用于計算由頁中間目錄的一個單獨表項所映射的區域大小,也就是一個頁表的大小。

split_mem_range()根據傳入的內存 start 和 end 做四舍五入的對齊操作(即 round_up 和 round_down)

#define?__round_mask(x,?y)?((__typeof__(x))((y)-1)) #define?round_up(x,?y)?((((x)-1)?|?__round_mask(x,?y))+1) //可以理解為:#define round_up(x, y)?(((x)+(y)?- 1)/(y))*(y)) #define?round_down(x,?y)?((x)?&?~__round_mask(x,?y)) //可以理解為:#define round_down(x, y)?((x/y)?* y)

round_up 宏依靠整數除法來完成這項工作,僅當兩個參數均為整數時,它才有效,x 是需要四舍五入的數字,y 是應該四舍五入的間隔,也就是說,round_up(12,5)應返回 15,因為 15 是大于 12 的 5 的第一個間隔,而 round_down(12,5)應返回 10。

split_mem_range()會根據對齊的情況,把開始、末尾的不對齊部分及中間部分分成了三段,使用 save_mr()將其存放在 init_mem_mapping()的局部變量數組 mr 中。劃分開來主要是為了讓各部分可以映射到不同大小的頁面,最后如果相鄰兩部分映射頁面的大小是一致的,則將其合并。

可以通過 dmesg 得到劃分的情況(以下是我私服的劃分情況,不過是 64 位的……)

初始化完內存段信息 mr 之后,就行了進入到kernel_physical_mapping_init,這個函數是建立內核頁表的最為關鍵的一步,負責處理物理內存的映射

在 2.6.11 后,Linux 采用四級分頁模型,這四級頁目錄分別為:

  • 頁全局目錄(Page Global Directory)

  • 頁上級目錄(Page Upper Directory)

  • 頁中間目錄(Page Middle Directory)

  • 頁表(Page Table)

對于沒有啟動 PAE(物理地址擴展)的 32 位系統,Linux 雖然也采用四級分頁模型,但本質上只用到了兩級分頁,Linux 通過將"頁上級目錄"位域和“頁中間目錄”位域全為 0 來達到使用兩級分頁的目的,但為了保證程序能 32 位和 64 系統上都能運行,內核保留了頁上級目錄和頁中間目錄在指針序列中的位置,它們的頁目錄數都被內核置為 1,并把這 2 個頁目錄項映射到適合的全局目錄項。

開啟 PAE 后,32 位系統尋址方式將大大改變,這時候使用的是三級頁表,即頁上級目錄其實沒有真正用到。

這里不考慮 PAE

PAGE_OFFSET 代表的是內核空間和用戶空間對虛擬地址空間的劃分,對不同的體系結構不同。比如在 32 位系統中 3G-4G 屬于內核使用的內存空間,所以 PAGE_OFFSET = 0xC0000000

unsigned?long?__init kernel_physical_mapping_init(unsigned?long?start,unsigned?long?end,unsigned?long?page_size_mask,pgprot_t?prot) {int?use_pse?=?page_size_mask?==?(1<<PG_LEVEL_2M);unsigned?long?last_map_addr?=?end;unsigned?long?start_pfn,?end_pfn;pgd_t?*pgd_base?=?swapper_pg_dir;int?pgd_idx,?pmd_idx,?pte_ofs;unsigned?long?pfn;pgd_t?*pgd;pmd_t?*pmd;pte_t?*pte;unsigned?pages_2m,?pages_4k;int?mapping_iter;start_pfn?=?start?>>?PAGE_SHIFT;end_pfn?=?end?>>?PAGE_SHIFT;mapping_iter?=?1;if?(!boot_cpu_has(X86_FEATURE_PSE))use_pse?=?0;repeat:pages_2m?=?pages_4k?=?0;pfn?=?start_pfn;?//pfn保存起始頁框號pgd_idx?=?pgd_index((pfn<<PAGE_SHIFT)?+?PAGE_OFFSET);?//低端內存的起始地址對應的pgd的偏移pgd?=?pgd_base?+?pgd_idx;?//得到起始頁框對應的pgd//由pgd開始遍歷for?(;?pgd_idx?<?PTRS_PER_PGD;?pgd++,?pgd_idx++)?{pmd?=?one_md_table_init(pgd);//申請得到一個pmd表if?(pfn?>=?end_pfn)continue; #ifdef?CONFIG_X86_PAEpmd_idx?=?pmd_index((pfn<<PAGE_SHIFT)?+?PAGE_OFFSET);pmd?+=?pmd_idx; #elsepmd_idx?=?0; #endif//遍歷pmd表,對于未激活PAE的32位系統,PTRS_PER_PMD為1,激活PAE則為512for?(;?pmd_idx?<?PTRS_PER_PMD?&&?pfn?<?end_pfn;pmd++,?pmd_idx++)?{unsigned?int?addr?=?pfn?*?PAGE_SIZE?+?PAGE_OFFSET;/**?Map?with?big?pages?if?possible,?otherwise*?create?normal?page?tables:*/if?(use_pse)?{unsigned?int?addr2;pgprot_t?prot?=?PAGE_KERNEL_LARGE;/**?first?pass?will?use?the?same?initial*?identity?mapping?attribute?+?_PAGE_PSE.*/pgprot_t?init_prot?=__pgprot(PTE_IDENT_ATTR?|_PAGE_PSE);pfn?&=?PMD_MASK?>>?PAGE_SHIFT;addr2?=?(pfn?+?PTRS_PER_PTE-1)?*?PAGE_SIZE?+PAGE_OFFSET?+?PAGE_SIZE-1;if?(__is_kernel_text(addr)?||__is_kernel_text(addr2))prot?=?PAGE_KERNEL_LARGE_EXEC;pages_2m++;if?(mapping_iter?==?1)set_pmd(pmd,?pfn_pmd(pfn,?init_prot));elseset_pmd(pmd,?pfn_pmd(pfn,?prot));pfn?+=?PTRS_PER_PTE;continue;}pte?=?one_page_table_init(pmd);?//創建一個頁表//得到pfn在page?table中的偏移并定位到具體的ptepte_ofs?=?pte_index((pfn<<PAGE_SHIFT)?+?PAGE_OFFSET);pte?+=?pte_ofs;//由pte開始遍歷page?tablefor?(;?pte_ofs?<?PTRS_PER_PTE?&&?pfn?<?end_pfn;pte++,?pfn++,?pte_ofs++,?addr?+=?PAGE_SIZE)?{pgprot_t?prot?=?PAGE_KERNEL;/**?first?pass?will?use?the?same?initial*?identity?mapping?attribute.*/pgprot_t?init_prot?=?__pgprot(PTE_IDENT_ATTR);if?(__is_kernel_text(addr))?//如果處于內核代碼段,權限設為可執行prot?=?PAGE_KERNEL_EXEC;pages_4k++;//設置pte與pfn關聯if?(mapping_iter?==?1)?{set_pte(pte,?pfn_pte(pfn,?init_prot));?//第一次執行將權限位設為init_protlast_map_addr?=?(pfn?<<?PAGE_SHIFT)?+?PAGE_SIZE;}?elseset_pte(pte,?pfn_pte(pfn,?prot));?//之后的執行將權限位置為prot}}}if?(mapping_iter?==?1)?{/**?update?direct?mapping?page?count?only?in?the?first*?iteration.*/update_page_count(PG_LEVEL_2M,?pages_2m);update_page_count(PG_LEVEL_4K,?pages_4k);/**?local?global?flush?tlb,?which?will?flush?the?previous*?mappings?present?in?both?small?and?large?page?TLB's.*/__flush_tlb_all();?//TLB全部刷新/**?Second?iteration?will?set?the?actual?desired?PTE?attributes.*/mapping_iter?=?2;goto?repeat;}return?last_map_addr; }

內核的內核頁全局目錄的基地址保存在 swapper_pg_dir 全局變量中,但需要使用主內核頁表時系統會把這個變量的值放入 cr3 寄存器,詳細可參考/arch/x86/kernel/head_32.s。

Linux 分別采用 pgd_t、pud_t、pmd_t 和 pte_t 四種數據結構來表示頁全局目錄項、頁上級目錄項、頁中間目錄項和頁表項。這四種數據結構本質上都是無符號長整型,Linux 為了更嚴格數據類型檢查,將無符號長整型分別封裝成四種不同的頁表項。如果不采用這種方法,那么一個無符號長整型數據可以傳入任何一個與四種頁表相關的函數或宏中,這將大大降低程序的健壯性。下面僅列出 pgd_t 類型的內核源碼實現,其他類型與此類似

typedef?unsigned?long???pgdval_t;typedef?struct?{?pgdval_t?pgd;?}?pgd_t;#define?pgd_val(x)??????native_pgd_val(x)static?inline?pgdval_t?native_pgd_val(pgd_t?pgd) {return?pgd.pgd; }

這里需要區別指向頁表項的指針和頁表項所代表的數據,如果已知一個 pgd_t 類型的指針 pgd,那么通過 pgd_val(*pgd)即可獲得該頁表項(也就是一個無符號長整型數據)

PAGE_SHIFT,PMD_SHIFT,PUD_SHIFT,PGDIR_SHIFT,對應相應的頁目錄所能映射的區域大小的位數,如 PAGE_SHIFT 為 12,即頁面大小為 4k。

PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD, PTRS_PER_PGD,對應相應頁目錄中的表項數。32 位系統下,當 PAE 被禁止時,他們的值分別為 1024,,1,1 和 1024,也就是說只使用兩級分頁

pgd_index(addr),pud_index,(addr),pmd_index(addr),pte_index(addr),取 addr 在該目錄中的索引

pud_offset(pgd,addr), pmd_offset(pud,addr), pte_offset(pmd,addr),以 pmd_offset 為例,線性地址 addr 對應的 pmd 索引在在 pud 指定的 pmd 表的偏移地址。在兩級或三級分頁系統中,pmd_offset 和 pud_offset 都返回頁全局目錄的地址

至此,低端內存的物理地址和虛擬地址之間的映射關系已全部建立起來了

回到前面的 init_memory_mapping()函數,它的最后一個函數調用為 add_pfn_range_mapped()

struct?range?pfn_mapped[E820_MAX_ENTRIES]; int?nr_pfn_mapped;static?void?add_pfn_range_mapped(unsigned?long?start_pfn,?unsigned?long?end_pfn) {nr_pfn_mapped?=?add_range_with_merge(pfn_mapped,?E820_MAX_ENTRIES,nr_pfn_mapped,?start_pfn,?end_pfn);nr_pfn_mapped?=?clean_sort_range(pfn_mapped,?E820_MAX_ENTRIES);max_pfn_mapped?=?max(max_pfn_mapped,?end_pfn);if?(start_pfn?<?(1UL<<(32-PAGE_SHIFT)))max_low_pfn_mapped?=?max(max_low_pfn_mapped,min(end_pfn,?1UL<<(32-PAGE_SHIFT))); }

該函數主要是將前面完成內存映射的物理頁框范圍加入到全局數組pfn_mapped中,其中 nr_pfn_mapped 用于表示數組中的有效項數量,之后可以通過內核函數 pfn_range_is_mapped 來判斷指定的物理內存是否被映射,避免重復映射的情況

固定映射區初始化

再回到更前面的 init_mem_mapping()函數,early_ioremap_page_table_range_init()用來建立高端內存的固定映射區頁表,與低端內存的頁表初始化不同的是,固定映射區的頁表只是被分配,相應的 PTE 項并未初始化,這個工作交由后面的set_fixmap()函數將相關的固定映射區頁表與物理內存進行關聯

#?define?PMD_MASK?(~(PMD_SIZE?-?1)) void?__init?early_ioremap_page_table_range_init(void) {pgd_t?*pgd_base?=?swapper_pg_dir;unsigned?long?vaddr,?end;/**?Fixed?mappings,?only?the?page?table?structure?has?to?be*?created?-?mappings?will?be?set?by?set_fixmap():*/vaddr?=?__fix_to_virt(__end_of_fixed_addresses?-?1)?&?PMD_MASK;end?=?(FIXADDR_TOP?+?PMD_SIZE?-?1)?&?PMD_MASK;page_table_range_init(vaddr,?end,?pgd_base);early_ioremap_reset(); }

PUD_MASK、PMD_MASK、PGDIR_MASK,這些 MASK 的作用是:從給定地址中提取某些分量,用給定地址與對應的 MASK 位與操作之后即可獲得各個分量,上面的操作為屏蔽低位

這里可以先具體看一下固定映射區的組成

每個固定映射區索引都以枚舉類型的形式定義在 enum fixed_addresses 中

enum?fixed_addresses?{ #ifdef?CONFIG_X86_32FIX_HOLE, #else #ifdef?CONFIG_X86_VSYSCALL_EMULATIONVSYSCALL_PAGE?=?(FIXADDR_TOP?-?VSYSCALL_ADDR)?>>?PAGE_SHIFT, #endif #endifFIX_DBGP_BASE,FIX_EARLYCON_MEM_BASE, #ifdef?CONFIG_PROVIDE_OHCI1394_DMA_INITFIX_OHCI1394_BASE, #endif #ifdef?CONFIG_X86_LOCAL_APICFIX_APIC_BASE,?/*?local?(CPU)?APIC)?--?required?for?SMP?or?not?*/ #endif #ifdef?CONFIG_X86_IO_APICFIX_IO_APIC_BASE_0,FIX_IO_APIC_BASE_END?=?FIX_IO_APIC_BASE_0?+?MAX_IO_APICS?-?1, #endif #ifdef?CONFIG_X86_32//這里即為固定映射區FIX_KMAP_BEGIN,?/*?reserved?pte's?for?temporary?kernel?mappings?*/FIX_KMAP_END?=?FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1, #ifdef?CONFIG_PCI_MMCONFIGFIX_PCIE_MCFG, #endif #endif #ifdef?CONFIG_PARAVIRT_XXLFIX_PARAVIRT_BOOTMAP, #endif #ifdef?CONFIG_X86_INTEL_MIDFIX_LNW_VRTC, #endif#ifdef?CONFIG_ACPI_APEI_GHES/*?Used?for?GHES?mapping?from?assorted?contexts?*/FIX_APEI_GHES_IRQ,FIX_APEI_GHES_NMI, #endif__end_of_permanent_fixed_addresses,/**?512?temporary?boot-time?mappings,?used?by?early_ioremap(),*?before?ioremap()?is?functional.**?If?necessary?we?round?it?up?to?the?next?512?pages?boundary?so*?that?we?can?have?a?single?pmd?entry?and?a?single?pte?table:*/ #define?NR_FIX_BTMAPS??64 #define?FIX_BTMAPS_SLOTS?8 #define?TOTAL_FIX_BTMAPS?(NR_FIX_BTMAPS?*?FIX_BTMAPS_SLOTS)FIX_BTMAP_END?=(__end_of_permanent_fixed_addresses?^(__end_of_permanent_fixed_addresses?+?TOTAL_FIX_BTMAPS?-?1))?&-PTRS_PER_PTE??__end_of_permanent_fixed_addresses?+?TOTAL_FIX_BTMAPS?-(__end_of_permanent_fixed_addresses?&?(TOTAL_FIX_BTMAPS?-?1)):?__end_of_permanent_fixed_addresses,FIX_BTMAP_BEGIN?=?FIX_BTMAP_END?+?TOTAL_FIX_BTMAPS?-?1, #ifdef?CONFIG_X86_32FIX_WP_TEST, #endif #ifdef?CONFIG_INTEL_TXTFIX_TBOOT_BASE, #endif__end_of_fixed_addresses };#define?__fix_to_virt(x)?(FIXADDR_TOP?-?((x)?<<?PAGE_SHIFT))

一個索引對應一個 4KB 的頁框,固定映射區的結束地址為 FIXADDR_TOP,即 0xfffff000(4G-4K),固定映射區是反向生長的,也就是說第一個索引對應的地址離 FIXADDR_TOP 最近。另外宏__fix_to_virt(idx)可以通過索引來計算相應的固定映射區域的線性地址

//初始化pgd_base指向的頁全局目錄中start到end這個范圍的線性地址,整個函數結束后只是初始化好了頁中間目錄項對應的頁表,但是頁表中的頁表項并沒有初始化 static?void?__init page_table_range_init(unsigned?long?start,?unsigned?long?end,?pgd_t?*pgd_base) {int?pgd_idx,?pmd_idx;unsigned?long?vaddr;pgd_t?*pgd;pmd_t?*pmd;pte_t?*pte?=?NULL;unsigned?long?count?=?page_table_range_init_count(start,?end);void?*adr?=?NULL;if?(count)adr?=?alloc_low_pages(count);vaddr?=?start;pgd_idx?=?pgd_index(vaddr);?//得到vaddr對應的pgd索引pmd_idx?=?pmd_index(vaddr);?//得到vaddr對應的pmd索引pgd?=?pgd_base?+?pgd_idx;???//得到pgd項for?(?;?(pgd_idx?<?PTRS_PER_PGD)?&&?(vaddr?!=?end);?pgd++,?pgd_idx++)?{pmd?=?one_md_table_init(pgd);?//得到pmd起始項pmd?=?pmd?+?pmd_index(vaddr);?//得到偏移后的pmdfor?(;?(pmd_idx?<?PTRS_PER_PMD)?&&?(vaddr?!=?end);pmd++,?pmd_idx++)?{//建立pte表并檢查vaddr是否對應內核臨時映射區,若是則重新申請一個頁表來保存pte表pte?=?page_table_kmap_check(one_page_table_init(pmd),pmd,?vaddr,?pte,?&adr);vaddr?+=?PMD_SIZE;}pmd_idx?=?0;} }

先看 page_table_range_init_count 函數

static?unsigned?long?__init page_table_range_init_count(unsigned?long?start,?unsigned?long?end) {unsigned?long?count?=?0; #ifdef?CONFIG_HIGHMEMint?pmd_idx_kmap_begin?=?fix_to_virt(FIX_KMAP_END)?>>?PMD_SHIFT;int?pmd_idx_kmap_end?=?fix_to_virt(FIX_KMAP_BEGIN)?>>?PMD_SHIFT;int?pgd_idx,?pmd_idx;unsigned?long?vaddr;if?(pmd_idx_kmap_begin?==?pmd_idx_kmap_end)return?0;vaddr?=?start;pgd_idx?=?pgd_index(vaddr);pmd_idx?=?pmd_index(vaddr);for?(?;?(pgd_idx?<?PTRS_PER_PGD)?&&?(vaddr?!=?end);?pgd_idx++)?{for?(;?(pmd_idx?<?PTRS_PER_PMD)?&&?(vaddr?!=?end);pmd_idx++)?{if?((vaddr?>>?PMD_SHIFT)?>=?pmd_idx_kmap_begin?&&(vaddr?>>?PMD_SHIFT)?<=?pmd_idx_kmap_end)count++;vaddr?+=?PMD_SIZE;}pmd_idx?=?0;} #endifreturn?count; }

page_table_range_init_count()用來計算臨時映射區間的頁表數量。FIXADDR_START 到 FIXADDR_TOP 即整個固定映射區,就如上面所提到的里面有多個索引標識的不同功能的映射區間,而其中的一個區間FIX_KMAP_BEGIN 到 FIX_KMAP_END 是臨時映射區間。這里再看一下兩者的定義:

FIX_KMAP_BEGIN,?/*?reserved?pte's?for?temporary?kernel?mappings?*/ FIX_KMAP_END?=?FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,

固定映射是在編譯時就確定的地址空間,但是它對應的物理頁可以是任意的,每一個固定地址中的項都是一頁范圍,這些地址的用途是固定的,這是該區間被設計的最主要的用途。臨時映射區間也叫原子映射,它可以用在不能睡眠的地方,如中斷處理程序中,因為臨時映射獲取映射時是絕對不會阻塞的,上面的 KM_TYPE_NR 可以理解為臨時內核映射的最大數量,NR_CPUS 則表示 CPU 數量。

如果返回的頁表數量不為 0,則 page_table_range_init()函數還會調用 alloc_low_pages():

__ref?void?*alloc_low_pages(unsigned?int?num) {unsigned?long?pfn;int?i;if?(after_bootmem)?{unsigned?int?order;order?=?get_order((unsigned?long)num?<<?PAGE_SHIFT);return?(void?*)__get_free_pages(GFP_ATOMIC?|?__GFP_ZERO,?order);}if?((pgt_buf_end?+?num)?>?pgt_buf_top?||?!can_use_brk_pgt)?{unsigned?long?ret?=?0;if?(min_pfn_mapped?<?max_pfn_mapped)?{ret?=?memblock_find_in_range(min_pfn_mapped?<<?PAGE_SHIFT,max_pfn_mapped?<<?PAGE_SHIFT,PAGE_SIZE?*?num?,?PAGE_SIZE);}if?(ret)memblock_reserve(ret,?PAGE_SIZE?*?num);else?if?(can_use_brk_pgt)ret?=?__pa(extend_brk(PAGE_SIZE?*?num,?PAGE_SIZE));if?(!ret)panic("alloc_low_pages:?can?not?alloc?memory");pfn?=?ret?>>?PAGE_SHIFT;}?else?{pfn?=?pgt_buf_end;pgt_buf_end?+=?num;}for?(i?=?0;?i?<?num;?i++)?{void?*adr;adr?=?__va((pfn?+?i)?<<?PAGE_SHIFT);clear_page(adr);}return?__va(pfn?<<?PAGE_SHIFT); }

alloc_low_pages函數根據前面 early_alloc_pgt_buf()申請保留的頁表緩沖空間使用情況來判斷是從頁表緩沖空間中申請內存還是通過 memblock 算法申請頁表內存,頁表緩沖空間空間足夠的話就在頁表緩沖空間中分配。

回到 page_table_range_init(),其中one_md_table_init()是用于申請新物理頁作為頁中間目錄的,不過這里分析的是 x86 非 PAE 環境,不存在頁中間目錄,因此實際上返回的仍是入參,代碼如下:

static?pmd_t?*?__init?one_md_table_init(pgd_t?*pgd) {p4d_t?*p4d;pud_t?*pud;pmd_t?*pmd_table;#ifdef?CONFIG_X86_PAEif?(!(pgd_val(*pgd)?&?_PAGE_PRESENT))?{pmd_table?=?(pmd_t?*)alloc_low_page();paravirt_alloc_pmd(&init_mm,?__pa(pmd_table)?>>?PAGE_SHIFT);set_pgd(pgd,?__pgd(__pa(pmd_table)?|?_PAGE_PRESENT));p4d?=?p4d_offset(pgd,?0);pud?=?pud_offset(p4d,?0);BUG_ON(pmd_table?!=?pmd_offset(pud,?0));return?pmd_table;} #endifp4d?=?p4d_offset(pgd,?0);pud?=?pud_offset(p4d,?0);pmd_table?=?pmd_offset(pud,?0);return?pmd_table; }

接著的是page_table_kmap_check(),其入參調用的one_page_table_init()是用于當入參 pmd 沒有頁表指向時,創建頁表并使其指向被創建的頁表。

static?pte_t?*?__init?one_page_table_init(pmd_t?*pmd) {if?(!(pmd_val(*pmd)?&?_PAGE_PRESENT))?{pte_t?*page_table?=?(pte_t?*)alloc_low_page();paravirt_alloc_pte(&init_mm,?__pa(page_table)?>>?PAGE_SHIFT);set_pmd(pmd,?__pmd(__pa(page_table)?|?_PAGE_TABLE));BUG_ON(page_table?!=?pte_offset_kernel(pmd,?0));}return?pte_offset_kernel(pmd,?0); }

page_table_kmap_check()的實現如下:

#define?PTRS_PER_PTE?512 static?pte_t?*__init?page_table_kmap_check(pte_t?*pte,?pmd_t?*pmd,unsigned?long?vaddr,?pte_t?*lastpte,void?**adr) { #ifdef?CONFIG_HIGHMEM/**?Something?(early?fixmap)?may?already?have?put?a?pte*?page?here,?which?causes?the?page?table?allocation*?to?become?nonlinear.?Attempt?to?fix?it,?and?if?it*?is?still?nonlinear?then?we?have?to?bug.*///得到內核固定映射區的臨時映射區的起始和結束虛擬頁框號int?pmd_idx_kmap_begin?=?fix_to_virt(FIX_KMAP_END)?>>?PMD_SHIFT;int?pmd_idx_kmap_end?=?fix_to_virt(FIX_KMAP_BEGIN)?>>?PMD_SHIFT;if?(pmd_idx_kmap_begin?!=?pmd_idx_kmap_end&&?(vaddr?>>?PMD_SHIFT)?>=?pmd_idx_kmap_begin&&?(vaddr?>>?PMD_SHIFT)?<=?pmd_idx_kmap_end)?{pte_t?*newpte;int?i;BUG_ON(after_bootmem);newpte?=?*adr;//拷貝操作for?(i?=?0;?i?<?PTRS_PER_PTE;?i++)set_pte(newpte?+?i,?pte[i]);*adr?=?(void?*)(((unsigned?long)(*adr))?+?PAGE_SIZE);paravirt_alloc_pte(&init_mm,?__pa(newpte)?>>?PAGE_SHIFT);set_pmd(pmd,?__pmd(__pa(newpte)|_PAGE_TABLE));?//pmd與newpte表進行關聯BUG_ON(newpte?!=?pte_offset_kernel(pmd,?0));__flush_tlb_all();paravirt_release_pte(__pa(pte)?>>?PAGE_SHIFT);pte?=?newpte;}BUG_ON(vaddr?<?fix_to_virt(FIX_KMAP_BEGIN?-?1)&&?vaddr?>?fix_to_virt(FIX_KMAP_END)&&?lastpte?&&?lastpte?+?PTRS_PER_PTE?!=?pte); #endifreturn?pte; }

檢查當前頁表初始化的地址是否處于該區間范圍,如果是,則把其 pte 頁表的內容拷貝到 page_table_range_init()的 alloc_low_pages 申請的頁表空間中(這里的拷貝主要是為了保證連續性),并將 newpte 新頁表的地址設置到 pmd 中(32bit 系統實際上就是頁全局目錄),然后調用__flush_tlb_all()刷新 TLB 緩存;如果不是該區間,則僅是由入參中調用的 one_page_table_init()被分配到了頁表空間。

至此高端內存的固定映射區的頁表分配完成,后面的 paging_init()會負責完成剩下的頁表建立工作。

Linux 內存管理框架

傳統的多核運算是使用 SMP(Symmetric Multi-Processor )模式:將多個處理器與一個集中的存儲器和 I/O 總線相連,所有處理器訪問同一個物理存儲器,因此 SMP 系統有時也被稱為一致存儲器訪問(UMA)結構體系,即無論在什么時候,處理器只能為內存的每個數據保持或共享唯一一個數值。

NUMA模式是一種分布式存儲器訪問方式,處理器可以同時訪問不同的存儲器地址,大幅度提高并行性。NUMA 模式下系統的每個 CPU 都有本地內存,可支持快速訪問,各個處理器之間通過總線連接起來,以支持對其它 CPU 本地內存的訪問,但是這些訪問要比處理器本地內存的慢.

Linux 內核通過插入一些兼容層,使兩個不同體系結構的差異被隱藏,兩種模式都使用了同一個數據結構。

在 NUMA 模式下,處理器和內存塊被劃分成多個"節點"(node),比如機器上有 2 個處理器、4 個內存塊,我們可以把 1 個處理器和 2 個內存塊合起來(GNU/Linux 根據物理 CPU 的數量分配 node,一個物理 CPU 對應一個 node),共同組成一個 NUMA 的節點。每個節點被分配有本地存儲器空間,所有節點中的處理器都可以訪問系統全部的物理存儲器,但是訪問本節點內的存儲器所需要的時間比訪問某些遠程節點內的存儲器所花的時間要少得多。

與 CPU 類似,內存被分割成多個區域 BANK,也叫"簇",依據簇與 CPU 的"距離"的不同,訪問不同簇的方式也會有所不同,CPU 被劃分為多個節點,每個 CPU 對應一個本地物理內存, 一般一個 CPU-node 對應一個內存簇,也就是說每個內存簇被認為是一個節點。

而在 UMA 系統中, 內存就相當于一個只使用一個 NUMA 節點來管理整個系統的內存,這樣在內存管理的其它地方可以認為他們就是在處理一個(偽)NUMA 系統。

內存管理框架初始化

在 linux 中,每個物理內存節點 node 都被劃分為多個內存管理區域用于表示不同范圍的內存,比如上面提到的 NORMAL 內存、高端內存,內核可以使用不同的映射方式映射物理內存。zone 只是內核為了管理方便而做的一種邏輯上的劃分,并不存在這種物理硬件單元。

綜上,linux 的物理內存管理機制將物理內存劃分為三個層次來管理,依次是:Node(存儲節點)、Zone(管理區)和 Page(頁面),它們之間的關系如下:

其中,zone 的類型如下:

include/linux/mmzone.henum?zone_type?{ #ifdef?CONFIG_ZONE_DMAZONE_DMA,?//通常為內存首部16MB,某些工業標準體系結構設備需要用到ZONE_DMA(較舊的外設,只能尋址24位內存) #endif#ifdef?CONFIG_ZONE_DMA32ZONE_DMA32,?//在64位Linux操作系統上,DMA32為16M~4G(可訪問32位),高于4G的內存為Normal?ZONE #endifZONE_NORMAL,?//通常為16MB~896MB,該部分的內存由內核直接映射到物理地址空間的較高部分 #ifdef?CONFIG_HIGHMEMZONE_HIGHMEM,?//通常為896MB~末尾,將保留給系統使用,是系統中預留的可用內存空間,動態映射 #endifZONE_MOVABLE,?//用于減少內存的碎片化,這個區域的頁都是可遷移的#ifdef?CONFIG_ZONE_DEVICEZONE_DEVICE,?//為支持熱插拔設備而分配的非易失性內存 #endif__MAX_NR_ZONES };

回到之前的 setup_arch()函數,接著往下走,來到內存管理框架初始化的地方

void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_ram_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內存初始化............early_alloc_pgt_buf();?//頁表緩沖區分配reserve_brk();?//緩沖區加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動......init_mem_mapping();?//低端內存內核頁表初始化?高端內存固定映射區中臨時映射區頁表初始化......initmem_init();?//?<----------------------------------------...... }

initmem_init的實現如下:

#define?PHYS_ADDR_MAX?(~(phys_addr_t)0)#ifndef?CONFIG_NEED_MULTIPLE_NODES void?__init?initmem_init(void) { #ifdef?CONFIG_HIGHMEMhighstart_pfn?=?highend_pfn?=?max_pfn;if?(max_pfn?>?max_low_pfn)highstart_pfn?=?max_low_pfn;printk(KERN_NOTICE?"%ldMB?HIGHMEM?available.\n",pages_to_mb(highend_pfn?-?highstart_pfn));high_memory?=?(void?*)?__va(highstart_pfn?*?PAGE_SIZE?-?1)?+?1; #elsehigh_memory?=?(void?*)?__va(max_low_pfn?*?PAGE_SIZE?-?1)?+?1; #endifmemblock_set_node(0,?PHYS_ADDR_MAX,?&memblock.memory,?0);#ifdef?CONFIG_FLATMEMmax_mapnr?=?IS_ENABLED(CONFIG_HIGHMEM)???highend_pfn?:?max_low_pfn; #endif__vmalloc_start_set?=?true;printk(KERN_NOTICE?"%ldMB?LOWMEM?available.\n",pages_to_mb(max_low_pfn));setup_bootmem_allocator(); } #endif?/*?!CONFIG_NEED_MULTIPLE_NODES?*/

這個函數將high_memory初始化為低端內存最大頁框max_low_pfn對應的地址大小,接著調用 memblock_set_node,通過 memblock 內存管理器設置 node 節點信息

int?__init_memblock?memblock_set_node(phys_addr_t?base,?phys_addr_t?size,struct?memblock_type?*type,?int?nid) { #ifdef?CONFIG_NEED_MULTIPLE_NODESint?start_rgn,?end_rgn;int?i,?ret;ret?=?memblock_isolate_range(type,?base,?size,?&start_rgn,?&end_rgn);if?(ret)return?ret;for?(i?=?start_rgn;?i?<?end_rgn;?i++)memblock_set_region_node(&type->regions[i],?nid);memblock_merge_regions(type); #endifreturn?0; }

memblock_set_node 主要調用了三個函數:memblock_isolate_range、memblock_set_region_node 和 memblock_merge_regions,首先看memblock_isolate_range()函數:

/*?adjust?*@size?so?that?(@base?+?*@size)?doesn't?overflow,?return?new?size?*/ static?inline?phys_addr_t?memblock_cap_size(phys_addr_t?base,?phys_addr_t?*size) {return?*size?=?min(*size,?PHYS_ADDR_MAX?-?base); }static?int?__init_memblock?memblock_isolate_range(struct?memblock_type?*type,phys_addr_t?base,?phys_addr_t?size,int?*start_rgn,?int?*end_rgn) {phys_addr_t?end?=?base?+?memblock_cap_size(base,?&size);int?idx;struct?memblock_region?*rgn;*start_rgn?=?*end_rgn?=?0;if?(!size)return?0;/*?we'll?create?at?most?two?more?regions?*/while?(type->cnt?+?2?>?type->max)if?(memblock_double_array(type,?base,?size)?<?0)return?-ENOMEM;for_each_memblock_type(idx,?type,?rgn)?{phys_addr_t?rbase?=?rgn->base;phys_addr_t?rend?=?rbase?+?rgn->size;if?(rbase?>=?end)break;if?(rend?<=?base)continue;if?(rbase?<?base)?{/**?@rgn?intersects?from?below.??Split?and?continue*?to?process?the?next?region?-?the?new?top?half.*/rgn->base?=?base;rgn->size?-=?base?-?rbase;type->total_size?-=?base?-?rbase;memblock_insert_region(type,?idx,?rbase,?base?-?rbase,memblock_get_region_node(rgn),rgn->flags);}?else?if?(rend?>?end)?{/**?@rgn?intersects?from?above.??Split?and?redo?the*?current?region?-?the?new?bottom?half.*/rgn->base?=?end;rgn->size?-=?end?-?rbase;type->total_size?-=?end?-?rbase;memblock_insert_region(type,?idx--,?rbase,?end?-?rbase,memblock_get_region_node(rgn),rgn->flags);}?else?{/*?@rgn?is?fully?contained,?record?it?*/if?(!*end_rgn)*start_rgn?=?idx;*end_rgn?=?idx?+?1;}}return?0; }

在__memblock_remove()中有提到,memblock_isolate_range()主要作用是將要移除的物理內存區從 reserved 內存區中分離出來,將 start_rgn 和 end_rgn(該內存區塊的起始、結束索引號)返回回去,而這里,由于我們傳入的type 是 memblock.memory,該函數會根據入參 base 和 size 標記節點內存范圍,將該內存從 memory 中劃分開來,同時返回對應的 start_rgn 和 end_rgn。

1)如果 memblock 中的 region 恰好以在該節點內存范圍內的話,那么再未賦值 end_rgn 時將當前 region 的索引記錄至 start_rgn,end_rgn 在此基礎上加 1;

2)如果 memblock 中的 region 跨越了該節點內存末尾分界,那么將會把當前的 region 邊界調整為 node 節點內存范圍邊界,然后通過 memblock_insert_region()函數將剩下的部分(即越出內存范圍的那一塊內存)重新插入 memblock 管理 regions 當中,實現拆分;

static?inline?void?memblock_set_region_node(struct?memblock_region?*r,?int?nid) {r->nid?=?nid; } static?inline?int?memblock_get_region_node(const?struct?memblock_region?*r) {return?r->nid; }static?void?__init_memblock?memblock_insert_region(struct?memblock_type?*type,int?idx,?phys_addr_t?base,phys_addr_t?size,int?nid,?unsigned?long?flags) {struct?memblock_region?*rgn?=?&type->regions[idx];BUG_ON(type->cnt?>=?type->max);memmove(rgn?+?1,?rgn,?(type->cnt?-?idx)?*?sizeof(*rgn));rgn->base?=?base;rgn->size?=?size;rgn->flags?=?flags;memblock_set_region_node(rgn,?nid);type->cnt++;type->total_size?+=?size; }

上面的 memmove()將后面的 region 信息往后移,另外調用 memblock_set_region_node()將原 region 的 node 節點號保留在被拆分出來的 region 當中。

回到前面的 memblock_set_node()函數,緊接著 memblock_isolate_range()被調用的是memblock_set_region_node(),通過這個函數把劃分出來的 region 進行 node 節點號設置,而后面的memblock_merge_regions()前面 MEMBLOCK 內存分配器初始化時已經分析過了,是用于將相鄰的 region 進行合并的(節點號、flag 等一致才會合并)。

最后回到 initmem_init()函數中,memblock_set_node()返回后,接著調用的函數為 setup_bootmem_allocator()。

void?__init?setup_bootmem_allocator(void) {printk(KERN_INFO?"??mapped?low?ram:?0?-?%08lx\n",max_pfn_mapped<<PAGE_SHIFT);printk(KERN_INFO?"??low?ram:?0?-?%08lx\n",?max_low_pfn<<PAGE_SHIFT); }

原來該函數是用來初始化 bootmem 管理算法的,但現在 x86 的環境已經使用了 memblock 管理算法,所以這里僅作保留,打印部分信息。

bootmem 分配器使用一個 bitmap 來標記物理頁是否被占用,分配的時候按照第一適應的原則,從 bitmap 中進行查找,如果這位為 1,表示已經被占用,否則表示未被占用。bootmem 分配器每次分配內存都會在 bitmap 中進行線性搜索,效率非常低,而且容易在內存中留下許多小的空閑碎片,在需要非常大的內存塊的時候,檢查位圖這一過程就顯得代價很高。bootmem 分配器是用于在啟動階段分配內存的,對該分配器的需求集中于簡單性方面,而不是性能和通用性(和 memblock 管理器一致)。

至此,已完成對內存的節點 node 設置。

回到 setup_arch()函數:

void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_raCm_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內存初始化............early_alloc_pgt_buf();?//頁表緩沖區分配reserve_brk();?//緩沖區加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動......init_mem_mapping();?//低端內存內核頁表初始化?高端內存固定映射區中臨時映射區頁表初始化......initmem_init();?//high_memory(高端內存起始pfn)初始化?通過memblock內存管理器設置node節點信息......x86_init.paging.pagetable_init();?//?<-----------------------------...... }

x86_init 結構體內pagetable_init實際上掛接的是native_pagetable_init()函數:

struct?x86_init_ops?x86_init?__initdata?=?{.......paging?=?{.pagetable_init??=?native_pagetable_init,},...... }

native_pagetable_init()函數內容如下:

void?__init?native_pagetable_init(void) {unsigned?long?pfn,?va;pgd_t?*pgd,?*base?=?swapper_pg_dir;p4d_t?*p4d;pud_t?*pud;pmd_t?*pmd;pte_t?*pte;//循環?低端內存最大物理頁號~最大物理頁號for?(pfn?=?max_low_pfn;?pfn?<?1<<(32-PAGE_SHIFT);?pfn++)?{va?=?PAGE_OFFSET?+?(pfn<<PAGE_SHIFT);pgd?=?base?+?pgd_index(va);if?(!pgd_present(*pgd))break;p4d?=?p4d_offset(pgd,?va);pud?=?pud_offset(p4d,?va);pmd?=?pmd_offset(pud,?va);if?(!pmd_present(*pmd))break;/*?should?not?be?large?page?here?*/if?(pmd_large(*pmd))?{pr_warn("try?to?clear?pte?for?ram?above?max_low_pfn:?pfn:?%lx?pmd:?%p?pmd?phys:?%lx,?but?pmd?is?big?page?and?is?not?using?pte?!\n",pfn,?pmd,?__pa(pmd));BUG_ON(1);}pte?=?pte_offset_kernel(pmd,?va);if?(!pte_present(*pte))break;printk(KERN_DEBUG?"clearing?pte?for?ram?above?max_low_pfn:?pfn:?%lx?pmd:?%p?pmd?phys:?%lx?pte:?%p?pte?phys:?%lx\n",pfn,?pmd,?__pa(pmd),?pte,?__pa(pte));pte_clear(NULL,?va,?pte);}paravirt_alloc_pmd(&init_mm,?__pa(base)?>>?PAGE_SHIFT);paging_init(); }

PAGE_OFFSET 代表的是內核空間和用戶空間對虛擬地址空間的劃分,對不同的體系結構不同。比如在 32 位系統中 3G-4G 屬于內核使用的內存空間,所以 PAGE_OFFSET = 0xC0000000

該函數的 for 循環主要是用于檢測 max_low_pfn 后面的內核空間內存直接映射空間后面的物理內存是否存在系統啟動引導時創建的頁表(pfn 通過直接映射的方法得到虛擬地址,然后通過內核頁表得到 pgd、pmd、pte),如果存在,則使用 pte_clear()將其清除。

接著看 native_pagetable_init()調用的最后一個函數:paging_init()。

void?__init?paging_init(void) {pagetable_init();__flush_tlb_all();kmap_init();/**?NOTE:?at?this?point?the?bootmem?allocator?is?fully?available.*/olpc_dt_build_devicetree();sparse_init();zone_sizes_init(); }

可以對著這個圖看:

前面已經分析過低端內存、固定映射區中臨時映射區的內核頁表的建立,這里 paging_init 將會完成剩下的工作,首先看pagetable_init()

static?void?__init?pagetable_init(void) {pgd_t?*pgd_base?=?swapper_pg_dir;permanent_kmaps_init(pgd_base); }static?void?__init?permanent_kmaps_init(pgd_t?*pgd_base) {unsigned?long?vaddr?=?PKMAP_BASE;page_table_range_init(vaddr,?vaddr?+?PAGE_SIZE*LAST_PKMAP,?pgd_base);pkmap_page_table?=?virt_to_kpte(vaddr); }

該函數為建立持久映射區(KMAP 區)的頁表,page_table_range_init 函數前面固定映射區頁表初始化時已經分析過了(初始化 pgd_base 指向的頁全局目錄中 start 到 end 這個范圍的線性地址,整個函數結束后只是初始化好了頁中間目錄項對應的頁表,但是頁表中的頁表項并沒有初始化),這里建立頁表范圍為 PKMAP_BASE 到 PKMAP_BASE + PAGE_SIZE*LAST_PKMAP,建好頁表后將頁表地址賦值給給持久映射區頁表變量 pkmap_page_table

__flush_tlb_all()為刷新全部 TLB,這里不做介紹,接著看 paging_init()調用的下一個函數 kmap_init()。

static?void?__init?kmap_init(void) {unsigned?long?kmap_vstart;/**?Cache?the?first?kmap?pte:*/kmap_vstart?=?__fix_to_virt(FIX_KMAP_BEGIN);kmap_pte?=?virt_to_kpte(kmap_vstart); }

可以很容易看到 kmap_init()主要是獲取到臨時映射區間的起始頁表并往臨時映射頁表變量 kmap_pte 置值

回到 paging_init(),olpc_dt_build_devicetree 這里就不做介紹了,而sparse_init()則涉及到了 Linux 的內存模型,這里介紹一下 Linux 的三種內存模型,注意,以下都是從 CPU 的角度來看的。

從系統中任意一個 CPU 的角度來看,當它訪問物理內存的時候,物理地址空間是一個連續的、沒有空洞的地址空間,那么這種計算機系統的內存模型就是Flat memory。在這種內存模型下,物理內存的管理比較簡單,每一個物理頁幀都會有一個 page 數據結構來抽象,因此系統中存在一個 struct page 的數組(位于直接映射區,mem_map,在節點 node 里,后面就會介紹到),每一個數組條目指向一個實際的物理頁幀(page frame)。在 flat memory 的情況下,PFN 和 mem_map 數組 index 的關系是線性的(即位于直接映射區,有一個固定偏移),因此從 PFN 到對應的 page 數據結構是非常容易的,反之亦然。

pfn_to_page/page_to_pfn 的作用是 struct page* 和 pfn 頁幀號之間的轉換,flat memory 內存模型的相關代碼如下:

#if?defined(CONFIG_FLATMEM)#define?__pfn_to_page(pfn)?(mem_map?+?((pfn)?-?ARCH_PFN_OFFSET)) #define?__page_to_pfn(page)?((unsigned?long)((page)?-?mem_map)?+?\ARCH_PFN_OFFSET)

PFN 和 struct page 數組(mem_map)的 index 是線性關系,有一個固定的偏移就是 ARCH_PFN_OFFSET(跟架構相關的物理起始地址的 PFN)

如果 cpu 在訪問物理內存的時候,其地址空間有一些空洞,是不連續的,那么這種計算機系統的內存模型就是Discontiguous memory。一般而言,NUMA 架構的計算機系統的 memory model 都是選擇 Discontiguous Memory,不過 NUMA 強調的是 memory 和 processor 的位置關系,和內存模型其實是沒有關系的(NUMA 并沒有規定其內存的連續性,而 Discontiguous memory 系統也并非一定是 NUMA 系統,但是這兩種都是多節點的),只不過,由于同一 node 上的 memory 和 processor 有更緊密的耦合關系(訪問更快),因此需要多個 node 來管理。Discontiguous memory 本質上是 flat memory 內存模型的擴展,整個物理內存的內存空間大部分是成片的大塊內存,中間會有一些空洞,每一個成片的內存地址空間屬于一個 node(如果局限在一個 node 內部,其內存模型是 flat memory)

在這種模型下,從 PFN 轉換到具體的 struct page 會稍微復雜一點,首先要從 PFN 得到 node ID,然后根據這個 ID 找到對于的節點 node 數據結構,也就找到了對應的 page 數組,之后的方法就類似 flat memory 了。

#elif?defined(CONFIG_DISCONTIGMEM)#define?__pfn_to_page(pfn)???\ ({?unsigned?long?__pfn?=?(pfn);??\unsigned?long?__nid?=?arch_pfn_to_nid(__pfn);??\NODE_DATA(__nid)->node_mem_map?+?arch_local_page_offset(__pfn,?__nid);\ })#define?__page_to_pfn(pg)??????\ ({?const?struct?page?*__pg?=?(pg);?????\struct?pglist_data?*__pgdat?=?NODE_DATA(page_to_nid(__pg));?\(unsigned?long)(__pg?-?__pgdat->node_mem_map)?+???\__pgdat->node_start_pfn;?????\ })

Discontiguous memory 模型需要獲取 node id,只要找到 node id,一切都好辦了,后面類比 flat memory model 進行就 OK 了。對于__pfn_to_page 的定義,首先通過 arch_pfn_to_nid 將 PFN 轉換成 node id,通過 NODE_DATA 宏定義可以找到該 node 對應的 pglist_data 數據結構,該數據結構的 node_start_pfn 記錄了該 node 的第一個 pfn,因此,也就可以得到其對應 struct page 在 node_mem_map 的偏移,__page_to_pfn 類似與上述基本類似(pglist_data 數據結構后面再進行介紹)。

內存模型也是一個演進過程,剛開始的時候,使用 flat memory 去抽象一個連續的內存地址空間,出現 NUMA 之后,整個不連續的內存空間被分成若干個 node,每個 node 上是連續的內存地址空間,也就是從原來單一的一個 mem_maps[]變成了若干個 mem_maps[]。而現在 memory hotplug 的出現讓原來完美的設計變得不完美了(熱插拔,即帶電插拔,熱插拔功能就是允許用戶在不關閉系統,不切斷電源的情況下取出和更換損壞的硬盤、電源或板卡等部件。Linux 內核支持熱插拔的部件有 USB 設備、PCI 設備甚至 CPU),因為即便是一個 node 中的 mem_maps[]也有可能是不連續了,因此目前 Discontiguous memory 也逐漸在被 sparse memory 替代。

sparse memory內存模型下,連續的地址空間按照 SECTION(例如 1G)被分成了一段一段的,其中每一 p 都是 hotplug 的,因此 sparse memory 下,內存地址空間可以被切分的更細。整個連續的物理地址空間是按照一個個p來切斷的,在每一個 p 內部,其 memory 是連續的(即符合 flat memory 的特點),因此,mem_map 的 page 數組依附于 p 結構(struct mem_p),而不是 node 結構(struct pglist_data)。在這個模型下,PFN 轉 struct page 變為了轉換變成了 PFN<--->Section<--->page。

linux 內核中靜態定義了一個 mem_p 的指針數組,一個 p 中往往包括多個 page,因此需要通過 PFN 得到 p 號,用 p 號做為 index 在 mem_p 指針數組可以找到該 PFN 對應的 p 數據結構。實際上PFN 分成兩個部分:一部分是 p index,另外一個部分是 page 在該 p 的偏移,找到 p 之后,沿著其 mem_map 就可以找到對應的 page 數據結構。

對于 page 到 p index 的轉換,sparse memory 有 2 種方案,先看看經典的方案,也就是把 p 號保存在 page->flags 中(page 的結構同樣在后面再進行介紹),這種方法的最大的問題是 page->flags 中的 bit 數不一定夠用,因為這個 flag 中承載了太多的信息,各種 page flag、node id、zone id,現在又增加一個 p id,在不同的處理器架構中無法實現一致性的算法(上面的圖即為采用經典算法的 sparse memory)。

#elif?defined(CONFIG_SPARSEMEM) /**?Note:?p's?mem_map?is?encoded?to?reflect?its?start_pfn.*?p[i].p_mem_map?==?mem_map's?address?-?start_pfn;*/ #define?__page_to_pfn(pg)?????\ ({?const?struct?page?*__pg?=?(pg);????\int?__sec?=?page_to_p(__pg);???\(unsigned?long)(__pg?-?__p_mem_map_addr(__nr_to_p(__sec)));?\ })#define?__pfn_to_page(pfn)????\ ({?unsigned?long?__pfn?=?(pfn);???\struct?mem_p?*__sec?=?__pfn_to_p(__pfn);?\__p_mem_map_addr(__sec)?+?__pfn;??\ }) #endif?/*?CONFIG_FLATMEM/DISCONTIGMEM/SPARSEMEM?*/

對于經典的 sparse memory 模型,一個 p 的 struct page 數組所占用的內存來自直接映射區,頁表在初始化的時候就建立好了。但是,對于 SPARSEMEM_VMEMMAP 而言,虛擬地址一開始就分配好了,是 vmemmap 開始的一段連續的虛擬地址空間,但是沒有物理地址。因此,當一個 p 被發現后,可以立刻找到對應的 struct page 的虛擬地址,而這時候還需要分配一個物理的 page frame,對于這種 sparse memory,開銷會稍微大一些,需要建立頁表,關聯 page 跟物理地址。

#elif?defined(CONFIG_SPARSEMEM_VMEMMAP)/*?memmap?is?virtually?contiguous.??*/ #define?__pfn_to_page(pfn)?(vmemmap?+?(pfn)) #define?__page_to_pfn(page)?(unsigned?long)((page)?-?vmemmap)
內存管理區 Zone 初始化

回到 paging_init()的最后一個函數調用:zone_sizes_init()。

void?__init?zone_sizes_init(void) {unsigned?long?max_zone_pfns[MAX_NR_ZONES];memset(max_zone_pfns,?0,?sizeof(max_zone_pfns));#ifdef?CONFIG_ZONE_DMAmax_zone_pfns[ZONE_DMA]??=?min(MAX_DMA_PFN,?max_low_pfn); #endif #ifdef?CONFIG_ZONE_DMA32max_zone_pfns[ZONE_DMA32]?=?min(MAX_DMA32_PFN,?max_low_pfn); #endifmax_zone_pfns[ZONE_NORMAL]?=?max_low_pfn; #ifdef?CONFIG_HIGHMEMmax_zone_pfns[ZONE_HIGHMEM]?=?max_pfn; #endiffree_area_init(max_zone_pfns); }

這個函數為 zone 的各個內存管理區域最大物理頁號進行初始化,并作為參數調用 free_area_init_nodes(),其中free_area_init_nodes()函數實現如下:

void?__init?free_area_init(unsigned?long?*max_zone_pfn) {unsigned?long?start_pfn,?end_pfn;int?i,?nid,?zone;bool?descending;/*?Record?where?the?zone?boundaries?are?*///全局數組arch_zone_lowest_possible_pfn用來存儲各個內存域可使用的最低內存頁幀編號//全局數組arch_zone_highest_possible_pfn用來存儲各個內存域可使用的最高內存頁幀編號memset(arch_zone_lowest_possible_pfn,?0,sizeof(arch_zone_lowest_possible_pfn));memset(arch_zone_highest_possible_pfn,?0,sizeof(arch_zone_highest_possible_pfn));//用于最低內存區域中可用的編號最小的頁幀,即memblock.memory.regions[0].basestart_pfn?=?find_min_pfn_with_active_regions();//return?falsedescending?=?arch_has_descending_max_zone_pfns();//根據max_zone_pfn和start_pfn初始化arch_zone_lowest_possible_pfn和arch_zone_highest_possible_pfnfor?(i?=?0;?i?<?MAX_NR_ZONES;?i++)?{if?(descending)zone?=?MAX_NR_ZONES?-?i?-?1;elsezone?=?i;//由于ZONE_MOVABLE是一個虛擬內存域,不與真正的硬件內存域關聯,該內存域的邊界總是設置為0if?(zone?==?ZONE_MOVABLE)continue;end_pfn?=?max(max_zone_pfn[zone],?start_pfn);arch_zone_lowest_possible_pfn[zone]?=?start_pfn;arch_zone_highest_possible_pfn[zone]?=?end_pfn;start_pfn?=?end_pfn;}/*?Find?the?PFNs?that?ZONE_MOVABLE?begins?at?in?each?node?*/memset(zone_movable_pfn,?0,?sizeof(zone_movable_pfn));//用于計算ZONE_MOVABLE的內存數量//在內存較多的32位系統上,?這通常會是ZONE_HIGHMEM,?但是對于64位系統,將使用ZONE_NORMAL或ZONE_DMA32,這里計算也比較復雜,感興趣的話可以去看一下源碼,這里便不做介紹了find_zone_movable_pfns_for_nodes();......//各種打印....../*?Initialise?every?node?*///為系統中的每個節點調用free_area_init_node()mminit_verify_pageflags_layout();setup_nr_node_ids();init_unavailable_mem();for_each_online_node(nid)?{pg_data_t?*pgdat?=?NODE_DATA(nid);free_area_init_node(nid);/*?Any?memory?on?that?node?*/if?(pgdat->node_present_pages)node_set_state(nid,?N_MEMORY);check_for_memory(pgdat,?nid);} }

free_area_init_node()函數會計算每個節點中每個區域的大小及其空洞的大小,如果兩個相鄰區域之間的最大 PFN 匹配,則可以假定后面的區域為空。例如 arch_max_dma_pfn == arch_max_dma32_pfn,則假定 arch_max_dma32_pfn 該區域為空。

pg_data_t?node_data[MAX_NUMNODES]; EXPORT_SYMBOL(node_data);#ifdef?CONFIG_NUMA extern?struct?pglist_data?*node_data[]; #define?NODE_DATA(nid)?(node_data[nid]) #endif?/*?CONFIG_NUMA?*/static?void?__init?free_area_init_node(int?nid) {//從全局節點數組中獲取一個節點pg_data_t?*pgdat?=?NODE_DATA(nid);unsigned?long?start_pfn?=?0;unsigned?long?end_pfn?=?0;/*?pg_data_t?should?be?reset?to?zero?when?it's?allocated?*/WARN_ON(pgdat->nr_zones?||?pgdat->kswapd_highest_zoneidx);//根據節點id獲取起始pfn和結束pfn,前面node初始化時,memblock處已經設置好節點ID了get_pfn_range_for_nid(nid,?&start_pfn,?&end_pfn);//設置節點ID以及起始pfnpgdat->node_id?=?nid;pgdat->node_start_pfn?=?start_pfn;pgdat->per_cpu_nodestats?=?NULL;pr_info("Initmem?setup?node?%d?[mem?%#018Lx-%#018Lx]\n",?nid,(u64)start_pfn?<<?PAGE_SHIFT,end_pfn???((u64)end_pfn?<<?PAGE_SHIFT)?-?1?:?0);//初始化節點中每個zone的大小和空洞,同時計算節點的spanned_pages和present_pagescalculate_node_totalpages(pgdat,?start_pfn,?end_pfn);alloc_node_mem_map(pgdat);pgdat_set_deferred_range(pgdat);free_area_init_core(pgdat); }

在進入 calculate_node_totalpages 之前,這里還是先簡單介紹一下node的數據結構。

struct?zoneref?{struct?zone?*zone;?/*?Pointer?to?actual?zone?*/int?zone_idx;??/*?zone_idx(zoneref->zone)?*/ }; struct?zonelist?{struct?zoneref?_zonerefs[MAX_ZONES_PER_ZONELIST?+?1]; }; typedef?struct?pglist_data?{//本節點的所有zone內存管理區struct?zone?node_zones[MAX_NR_ZONES];//包含了對所有node的zone的引用,通常第一個node_zonelists為本節點自己的zonesstruct?zonelist?node_zonelists[MAX_ZONELISTS];//本節點zone管理區數目int?nr_zones;?/*?number?of?populated?zones?in?this?node?*/ #ifdef?CONFIG_FLAT_NODE_MEM_MAP?/*?means?!SPARSEMEM?*///Discontiguous?Memory內存模型,可以找到節點下的所有pagestruct?page?*node_mem_map; #ifdef?CONFIG_PAGE_EXTENSIONstruct?page_ext?*node_page_ext; #endif #endif......//節點第一個頁的頁號unsigned?long?node_start_pfn;//節點總共的物理頁數,不含空洞unsigned?long?node_present_pages;?/*?total?number?of?physical?pages?*///節點物理頁的范圍大小,含空洞unsigned?long?node_spanned_pages;?/*?total?size?of?physical?pagerange,?including?holes?*/int?node_id;......//內存回收相關的數據結構......ZONE_PADDING(_pad1_)......unsigned?long??flags;ZONE_PADDING(_pad2_)...... }?pg_data_t;

ZONE_PADDING 的作用,通過添加大量的填充把經常被訪問的“熱門”數據分到了單獨的 cache line 上,可以理解為空間換時間。

calculate_node_totalpages()實現:

static?void?__init?calculate_node_totalpages(struct?pglist_data?*pgdat,unsigned?long?node_start_pfn,unsigned?long?node_end_pfn) {unsigned?long?realtotalpages?=?0,?totalpages?=?0;enum?zone_type?i;for?(i?=?0;?i?<?MAX_NR_ZONES;?i++)?{struct?zone?*zone?=?pgdat->node_zones?+?i;unsigned?long?zone_start_pfn,?zone_end_pfn;unsigned?long?spanned,?absent;unsigned?long?size,?real_size;spanned?=?zone_spanned_pages_in_node(pgdat->node_id,?i,node_start_pfn,node_end_pfn,&zone_start_pfn,&zone_end_pfn);absent?=?zone_absent_pages_in_node(pgdat->node_id,?i,node_start_pfn,node_end_pfn);size?=?spanned;real_size?=?size?-?absent;if?(size)zone->zone_start_pfn?=?zone_start_pfn;elsezone->zone_start_pfn?=?0;zone->spanned_pages?=?size;zone->present_pages?=?real_size;totalpages?+=?size;realtotalpages?+=?real_size;}pgdat->node_spanned_pages?=?totalpages;pgdat->node_present_pages?=?realtotalpages;printk(KERN_DEBUG?"On?node?%d?totalpages:?%lu\n",?pgdat->node_id,realtotalpages); }

其中zone_spanned_pages_in_node():

static?unsigned?long?__init?zone_spanned_pages_in_node(int?nid,unsigned?long?zone_type,unsigned?long?node_start_pfn,unsigned?long?node_end_pfn,unsigned?long?*zone_start_pfn,unsigned?long?*zone_end_pfn) {unsigned?long?zone_low?=?arch_zone_lowest_possible_pfn[zone_type];unsigned?long?zone_high?=?arch_zone_highest_possible_pfn[zone_type];/*?When?hotadd?a?new?node?from?cpu_up(),?the?node?should?be?empty?*/if?(!node_start_pfn?&&?!node_end_pfn)return?0;/*?Get?the?start?and?end?of?the?zone?*///限制zone_start_pfn和zone_end_pfn所在區間*zone_start_pfn?=?clamp(node_start_pfn,?zone_low,?zone_high);*zone_end_pfn?=?clamp(node_end_pfn,?zone_low,?zone_high);adjust_zone_range_for_zone_movable(nid,?zone_type,node_start_pfn,?node_end_pfn,zone_start_pfn,?zone_end_pfn);/*?Check?that?this?node?has?pages?within?the?zone's?required?range?*/if?(*zone_end_pfn?<?node_start_pfn?||?*zone_start_pfn?>?node_end_pfn)return?0;/*?Move?the?zone?boundaries?inside?the?node?if?necessary?*/*zone_end_pfn?=?min(*zone_end_pfn,?node_end_pfn);*zone_start_pfn?=?max(*zone_start_pfn,?node_start_pfn);/*?Return?the?spanned?pages?*/return?*zone_end_pfn?-?*zone_start_pfn; }

該函數主要是統計 node 管理節點的內存跨度,該跨度不包括 movable 管理區的(因為 movable 就是在其它內存管理區里分配出來的),里面調用的 adjust_zone_range_for_zone_movable()則是用于剔除 movable 管理區的部分。

另外的zone_absent_pages_in_node()函數:

unsigned?long?__init?__absent_pages_in_range(int?nid,unsigned?long?range_start_pfn,unsigned?long?range_end_pfn) {unsigned?long?nr_absent?=?range_end_pfn?-?range_start_pfn;unsigned?long?start_pfn,?end_pfn;int?i;for_each_mem_pfn_range(i,?nid,?&start_pfn,?&end_pfn,?NULL)?{start_pfn?=?clamp(start_pfn,?range_start_pfn,?range_end_pfn);end_pfn?=?clamp(end_pfn,?range_start_pfn,?range_end_pfn);nr_absent?-=?end_pfn?-?start_pfn;}return?nr_absent; }static?unsigned?long?__init?zone_absent_pages_in_node(int?nid,unsigned?long?zone_type,unsigned?long?node_start_pfn,unsigned?long?node_end_pfn) {unsigned?long?zone_low?=?arch_zone_lowest_possible_pfn[zone_type];unsigned?long?zone_high?=?arch_zone_highest_possible_pfn[zone_type];unsigned?long?zone_start_pfn,?zone_end_pfn;unsigned?long?nr_absent;/*?When?hotadd?a?new?node?from?cpu_up(),?the?node?should?be?empty?*/if?(!node_start_pfn?&&?!node_end_pfn)return?0;zone_start_pfn?=?clamp(node_start_pfn,?zone_low,?zone_high);zone_end_pfn?=?clamp(node_end_pfn,?zone_low,?zone_high);adjust_zone_range_for_zone_movable(nid,?zone_type,node_start_pfn,?node_end_pfn,&zone_start_pfn,?&zone_end_pfn);nr_absent?=?__absent_pages_in_range(nid,?zone_start_pfn,?zone_end_pfn);/**?ZONE_MOVABLE?handling.*?Treat?pages?to?be?ZONE_MOVABLE?in?ZONE_NORMAL?as?absent?pages*?and?vice?versa.*/if?(mirrored_kernelcore?&&?zone_movable_pfn[nid])?{unsigned?long?start_pfn,?end_pfn;struct?memblock_region?*r;for_each_mem_region(r)?{start_pfn?=?clamp(memblock_region_memory_base_pfn(r),zone_start_pfn,?zone_end_pfn);end_pfn?=?clamp(memblock_region_memory_end_pfn(r),zone_start_pfn,?zone_end_pfn);if?(zone_type?==?ZONE_MOVABLE?&&memblock_is_mirror(r))nr_absent?+=?end_pfn?-?start_pfn;if?(zone_type?==?ZONE_NORMAL?&&!memblock_is_mirror(r))nr_absent?+=?end_pfn?-?start_pfn;}}return?nr_absent; }

該函數主要用于計算內存空洞頁面數的,計算方法大致為在 zone 區域范圍內,遍歷所有 memblock 的內存塊,將這些內存塊的大小累加,之后兩者做差,zone_absent_pages_in_node 后面是對 ZONE_MOVABLE 的特殊處理了,方法是類似的,這里也不做介紹了。

calculate_node_totalpages()后面就是各種簡單的賦值操作了,這里也簡單介紹一下zone的結構:

其中 MAX_NR_ZONES 是一個節點中所能包容納的 Zones 的最大數。

struct?zone?{......//保留頁框池,記錄每個管理區中必須保留的物理頁面數,以用于緊急狀況下的內存分配long?lowmem_reserve[MAX_NR_ZONES];//保持對UMA的兼容(當做一個節點),NUMA模式下的節點數 #ifdef?CONFIG_NUMAint?node; #endif//該zone的父節點struct?pglist_data?*zone_pgdat;......//該zone的第一個頁的頁號unsigned?long??zone_start_pfn;//伙伴系統管理的page數,這是除去了在初始化階段被申請的頁面(比如memblock)atomic_long_t??managed_pages;//zone大小,含空洞,即zone_end_pfn?-?zone_start_pfnunsigned?long??spanned_pages;//zone實際大小,不含空洞unsigned?long??present_pages;//zone的名稱,如“DMA”“Normal”“Highmem”,這些名稱定義于page_alloc.c的zone_names[MAX_NR_ZONES]const?char??*name;ZONE_PADDING(_pad1_)//包含所有空閑頁面,伙伴系統使用,里面有數量為MIGRATE_TYPES個的free_list鏈表,分別用于管理不同遷移類型的內存頁面struct?free_area?free_area[MAX_ORDER];//描述zone的當前狀態unsigned?long??flags;/*?Primarily?protects?free_area?*///與伙伴算法的碎片遷移算法有關spinlock_t??lock;ZONE_PADDING(_pad2_)......ZONE_PADDING(_pad3_)...... }?____cacheline_internodealigned_in_smp;

回到 free_area_init_node()函數,緊接在 calculate_node_totalpages()后的函數調用的為alloc_node_mem_map(),這個函數是用于申請 node 節點的 node_mem_map 相應的內存空間,如果是 sparse memory 內存模型,則該函數實現為空,這里便不做過多介紹了,直接看最后的初始化工作:free_area_init_core()。

static?void?__init?free_area_init_core(struct?pglist_data?*pgdat) {enum?zone_type?j;int?nid?=?pgdat->node_id;//對節點的一些鎖和隊列進行初始化pgdat_init_internals(pgdat);pgdat->per_cpu_nodestats?=?&boot_nodestats;for?(j?=?0;?j?<?MAX_NR_ZONES;?j++)?{struct?zone?*zone?=?pgdat->node_zones?+?j;unsigned?long?size,?freesize,?memmap_pages;unsigned?long?zone_start_pfn?=?zone->zone_start_pfn;size?=?zone->spanned_pages;freesize?=?zone->present_pages;//memmap_pages,每一個4k物理頁都對應一個mem_map_t來管理memmap_pages?=?calc_memmap_size(size,?freesize);if?(!is_highmem_idx(j))?{if?(freesize?>=?memmap_pages)?{freesize?-=?memmap_pages;if?(memmap_pages)printk(KERN_DEBUG"??%s?zone:?%lu?pages?used?for?memmap\n",zone_names[j],?memmap_pages);}?elsepr_warn("??%s?zone:?%lu?pages?exceeds?freesize?%lu\n",zone_names[j],?memmap_pages,?freesize);}//dma保留頁if?(j?==?0?&&?freesize?>?dma_reserve)?{freesize?-=?dma_reserve;printk(KERN_DEBUG?"??%s?zone:?%lu?pages?reserved\n",zone_names[0],?dma_reserve);}//計算nr_kernel_pages(低端內存的頁數)和nr_all_pages的數量if?(!is_highmem_idx(j))nr_kernel_pages?+=?freesize;/*?Charge?for?highmem?memmap?if?there?are?enough?kernel?pages?*///如果有足夠的頁,則也為高端內存提供memmap_pageselse?if?(nr_kernel_pages?>?memmap_pages?*?2)nr_kernel_pages?-=?memmap_pages;nr_all_pages?+=?freesize;/**?Set?an?approximate?value?for?lowmem?here,?it?will?be?adjusted*?when?the?bootmem?allocator?frees?pages?into?the?buddy?system.*?And?all?highmem?pages?will?be?managed?by?the?buddy?system.*///初始化zone使用的各類鎖zone_init_internals(zone,?j,?nid,?freesize);if?(!size)continue;set_pageblock_order();setup_usemap(pgdat,?zone,?zone_start_pfn,?size);init_currently_empty_zone(zone,?zone_start_pfn,?size);memmap_init(size,?nid,?j,?zone_start_pfn);} }

該函數主要用于向節點下的每個 zone 填充相關信息,在 for 循環內,循環遍歷統計各個管理區最大跨度間相差的頁面數 size 以及除去內存“空洞”后的實際頁面數 freesize,然后通過 calc_memmap_size()計算出該管理區所需的頁面管理結構占用的頁面數 memmap_pages,最后可以計算得出高端內存外的系統內存共有的內存頁面數(freesize-memmap_pages)。

nr_kernel_pages 用于統計低端內存的頁數,此外循環體內的操作則是初始化內存管理區的管理結構,例如各類鎖的初始化、隊列初始化。其中 set_pageblock_order()用于在 CONFIG_HUGETLB_PAGE_SIZE_VARIABLE 下設置 pageblock_order 的值;setup_usemap()函數則是為了給 zone 管理結構體中的 pageblock_flags 申請內存空間的,pageblock_flags 與伙伴系統的碎片遷移算法有關。init_currently_empty_zone()則主要是初始化管理區的等待隊列哈希表和等待隊列,同時還初始化了與伙伴系統相關的 free_area 列表

nr_kernel_pages、nr_all_pages 和頁面的關系可以參考下圖:

這里以我自己的私服為例,看一下我私服 node 和 zone 的情況

首先是 node 的個數,GNU/Linux 根據物理 CPU 的數量分配 node,因此可以直接查物理 CPU 的數量:

當然,用 numactl 會更加直觀:

我的機器上只有一個 node,接下來可以用 cat /proc/zoneinfo 查看這個 node 下各個 zone 的情況:

回到 free_area_init_core()函數的最后,memmap_init()->memmap_init_zone(),該函數主要是根據 PFN,然后通過 pfn_to_page 找到對應的 struct page 結構,并將該結構進行初始化處理,并設置 MIGRATE_MOVABLE 標志,表明可移動。

//遍歷memblock,找到節點的內存地址范圍,zone的范圍不能大于這個,使用memmap_init_zone對該zone進行處理 void?__meminit?__weak?memmap_init(unsigned?long?size,?int?nid,unsigned?long?zone,unsigned?long?range_start_pfn) {unsigned?long?start_pfn,?end_pfn;unsigned?long?range_end_pfn?=?range_start_pfn?+?size;int?i;for_each_mem_pfn_range(i,?nid,?&start_pfn,?&end_pfn,?NULL)?{start_pfn?=?clamp(start_pfn,?range_start_pfn,?range_end_pfn);end_pfn?=?clamp(end_pfn,?range_start_pfn,?range_end_pfn);if?(end_pfn?>?start_pfn)?{size?=?end_pfn?-?start_pfn;memmap_init_zone(size,?nid,?zone,?start_pfn,MEMINIT_EARLY,?NULL,?MIGRATE_MOVABLE);}} }void?__meminit?memmap_init_zone(unsigned?long?size,?int?nid,?unsigned?long?zone,unsigned?long?start_pfn,enum?meminit_context?context,struct?vmem_altmap?*altmap,?int?migratetype) {unsigned?long?pfn,?end_pfn?=?start_pfn?+?size;struct?page?*page;if?(highest_memmap_pfn?<?end_pfn?-?1)highest_memmap_pfn?=?end_pfn?-?1;#ifdef?CONFIG_ZONE_DEVICE/**?Honor?reservation?requested?by?the?driver?for?this?ZONE_DEVICE*?memory.?We?limit?the?total?number?of?pages?to?initialize?to?just*?those?that?might?contain?the?memory?mapping.?We?will?defer?the*?ZONE_DEVICE?page?initialization?until?after?we?have?released*?the?hotplug?lock.*/if?(zone?==?ZONE_DEVICE)?{if?(!altmap)return;if?(start_pfn?==?altmap->base_pfn)start_pfn?+=?altmap->reserve;end_pfn?=?altmap->base_pfn?+?vmem_altmap_offset(altmap);} #endiffor?(pfn?=?start_pfn;?pfn?<?end_pfn;?)?{/**?There?can?be?holes?in?boot-time?mem_map[]s?handed?to?this*?function.??They?do?not?exist?on?hotplugged?memory.*/if?(context?==?MEMINIT_EARLY)?{if?(overlap_memmap_init(zone,?&pfn))continue;if?(defer_init(nid,?pfn,?end_pfn))break;}page?=?pfn_to_page(pfn);__init_single_page(page,?pfn,?zone,?nid);if?(context?==?MEMINIT_HOTPLUG)__SetPageReserved(page);/**?Usually,?we?want?to?mark?the?pageblock?MIGRATE_MOVABLE,*?such?that?unmovable?allocations?won't?be?scattered?all*?over?the?place?during?system?boot.*/if?(IS_ALIGNED(pfn,?pageblock_nr_pages))?{set_pageblock_migratetype(page,?migratetype);cond_resched();}pfn++;} }
struct page

最后這里簡單介紹一下 struct page,內核會為每一個物理頁幀創建一個 struct page 的結構體,因此要保證 page 結構體足夠的小,否則僅 struct page 就要占用大量的內存,該結構有很多 union 結構,主要是用于各種算法不同數據的空間復用。

struct page 這個結構相當復雜,這里我放上網上找到的一個全局參考,可以比源碼更清晰地了解整個結構體,這里我也只簡單介紹里面的幾個字段。

????struct?page?(include/linux/mm_types.h)page+--------------------------------------------------------------+|flags?????????????????????????????????????????????????????????||???(unsigned?long)????????????????????????????????????????????|--+--???????+==============================================================+|?????????|..............................................................||?????????|page?cache?and?anonymous?pages????????????????????????????????||?????????|????+---------------------------------------------------------+|?????????|????|lru??????????????????????????????????????????????????????||?????????|????|????(struct?list_head)???????????????????????????????????||????|mapping??????????????????????????????????????????????????||????|????(struct?address_space*)??????????????????????????????||????|index????????????????????????????????????????????????????||????|????(pgoff_t)????????????????????????????????????????????|5?words??????|????|private??????????????????????????????????????????????????|union???????|????|????(unsigned?long)??????????????????????????????????????||????+---------------------------------------------------------+|..............................................................|has????????|slab,?slob,?slub??????????????????????????????????????????????||????+---------------------------------------------------------+7?usage??????|????|.........................................................||????|????????????????????????????.+---------------------------||????|slab_list???????????????????.|next???????????????????????||????|????(struct?list_head)??????.|???(struct?page*)??????????||????|????????????????????????????.|pages??????????????????????||????|????????????????????????????.|pobjects???????????????????||????|????????????????????????????.|???(int)???????????????????||????|????????????????????????????.+---------------------------||????|.........................................................||????+---------------------------------------------------------+|????|slab_cache???????????????????????????????????????????????||????|????(struct?kmem_cache*)?????????????????????????????????||????|freelist?????????????????????????????????????????????????||????|????(void*)??????????????????????????????????????????????||????+---------------------------------------------------------+|????|.........................................................||????|s_mem????.counters??????????.+---------------------------||????|?(void*)?.?(unsigned?long)??.|inuse??????????????????????||????|?????????.??????????????????.|objects????????????????????||????|?????????.??????????????????.|frozen?????????????????????||????|?????????.??????????????????.|????(unsigned)?????????????||????|?????????.??????????????????.+---------------------------||????|.........................................................||????+---------------------------------------------------------+|..............................................................||Tail?pages?of?compound?page???????????????????????????????????||????+---------------------------------------------------------+|????|compound_head????????????????????????????????????????????||????|????(unsigned?long)??????????????????????????????????????||????|compound_dtor????????????????????????????????????????????||????|compound_order???????????????????????????????????????????||????|????(unsigned?char)??????????????????????????????????????||????|compound_mapcount????????????????????????????????????????||????|????(atomic_t)???????????????????????????????????????????||????+---------------------------------------------------------+|..............................................................||Second?tail?page?of?compound?page?????????????????????????????||????+---------------------------------------------------------+|????|_compound_pad_1??????????????????????????????????????????||????|_compound_pad_2??????????????????????????????????????????||????|????(unsigned?long)??????????????????????????????????????||????|deferred_list????????????????????????????????????????????||????|????(struct?list_head)???????????????????????????????????||????+---------------------------------------------------------+|..............................................................||Page?table?pages??????????????????????????????????????????????||????+---------------------------------------------------------+|????|_pt_pad_1????????????????????????????????????????????????||????|????(unsigned?long)??????????????????????????????????????||????|pmd_huge_pte?????????????????????????????????????????????||????|????(pgtable_t)??????????????????????????????????????????||????|_pt_pad_2????????????????????????????????????????????????||????|????(unsigned?long)??????????????????????????????????????||????|.........................................................||????|pt_mm?????????????????????????.pt_frag_refcount??????????||????|????(struct?mm_struct*)???????.????(atomic_t)????????????||????|.........................................................||????|ptl??????????????????????????????????????????????????????||????|????(spinlock_t/spinlock_t?*)????????????????????????????||????+---------------------------------------------------------+|..............................................................||ZONE_DEVICE?pages?????????????????????????????????????????????||????+---------------------------------------------------------+|????|pgmap????????????????????????????????????????????????????||????|????(struct?dev_pagemap*)????????????????????????????????||????|hmm_data?????????????????????????????????????????????????||????|_zd_pad_1????????????????????????????????????????????????||?????????|????|????(unsigned?long)??????????????????????????????????????||?????????|????+---------------------------------------------------------+|?????????|..............................................................||?????????|rcu_head??????????????????????????????????????????????????????||?????????|????(struct?rcu_head)?????????????????????????????????????????||?????????|..............................................................|--+--???????+==============================================================+|?????????|..............................................................||????????????.?????????????????.????????????????.??????????????|4?bytes?????|_mapcount???.page_type????????.active??????????.units?????????|union??????|??(atomic_t).???(unsigned?int).??(unsigned?int).???(int)??????||????????????.?????????????????.????????????????.??????????????||?????????|..............................................................|--+--???????+==============================================================+|_refcount?????????????????????????????????????????????????????||?????(atomic_t)???????????????????????????????????????????????||mem_cgroup????????????????????????????????????????????????????||?????(struct?mem_cgroup)??????????????????????????????????????||virtual???????????????????????????????????????????????????????||?????(void?*)?????????????????????????????????????????????????||_last_cpupid??????????????????????????????????????????????????||?????(int)????????????????????????????????????????????????????|+--------------------------------------------------------------+

首先介紹一下flags,它描述 page 的狀態和其它的一些信息,如下圖。

主要分為 4 部分,其中標志位 flag 向高位增長,其余位字段向低位增長,中間存在空閑位。

  • p:主要用于 sparse memory 內存模型,即 p 號。

  • node:NUMA 節點號,標識該 page 屬于哪一個節點。

  • zone:內存域標志,標識該 page 屬于哪一個 zone。

  • flag:page 的狀態標識,常用的有:

page-flags.h enum?pageflags?{PG_locked,??/*?表示頁面已上鎖,不要訪問?*/PG_error,?????/*?表示頁面發生IO錯誤?*/PG_referenced,?/*?用于RCU算法?*/PG_uptodate,???/*?表示頁面內容有效,當該頁面上讀操作完成后,設置該標志位?*/PG_dirty,??????/*?表示頁面是臟頁,內容被修改過?*/PG_lru,????????/*?表示該頁面在lru鏈表中?*/PG_active,?????/*?表示該頁面在活躍lru鏈表中?*/PG_slab,???????/*?表示該頁面是屬于slab分配器創建的slab?*/PG_owner_priv_1,?/*?頁面的所有者使用,如果是pagecache頁面,文件系統可能使用*/PG_arch_1,???????/*?與體系架構相關的頁面狀態位?*/PG_reserved,?????/*?表示該頁面不可被換出,防止該page被交換到swap?*/PG_private,??/*?如果page中的private成員非空,則需要設置該標志,如果是pagecache,?包含fs-private?data?*/PG_writeback,??/*?頁面正在回寫?*/PG_head,??/*?A?head?page?*/PG_swapcache,??/*?表示該page處于swap?cache中?*/PG_reclaim,??/*?表示該page要被回收,決定要回收某個page后,需要設置該標志?*/PG_swapbacked,??/*?該page的后備存儲器是swap/ram,一般匿名頁才可以回寫swap分區?*/PG_unevictable,??/*?該page被鎖住,不能回收,并會出現在LRU_UNEVICTABLE鏈表中?*/...... }

內核定義了一些標準宏,用于檢查頁面是否設置了某個特定的標志位或者用于操作某些特定的標志位,比如

PageXXX()(檢查是否設置) SetPageXXX() ClearPageXXX()

伙伴系統,內存被分成含有很多頁面的大塊,每一塊都是 2 個頁面大小的方冪。如果找不到想要的塊, 一個大塊會被分成兩部分,這兩部分彼此就成為伙伴。其中一半被用來分配,而另一半則空閑。這些塊在以后分配的過程中會繼續被二分直至產生一個所需大小的塊。當一個塊被最終釋放時, 其伙伴將被檢測出來, 如果伙伴也空閑則合并兩者

slab,Slab 對小對象進行分配,不用為每個小對象去分配頁,節省了空間。內核中一些小對象在創建析構時很頻繁,Slab 對這些小對象做緩存,可以重復利用一些相同的對象,減少內存分配次數

接著看struct list_head lru,鏈表頭,具體作用得看 page 處于什么用途中,如果是伙伴系統則用于連接相同的伙伴,通過第一個 page 可以找到伙伴中所有的 page;如果是 slab,page->lru.next 指向 page 駐留的的緩存管理結構,page->lru.prec 指向保存該 page 的 slab 管理結構;而當 page 被用戶態使用或被當做頁緩存使用時,lru 則用于將該 page 連入 zone 中相應的 lru 鏈表,供內存回收時使用

struct address_space *mapping,當 mapping 為 NULL 時,該 page 為交換緩存(swap);當 mapping 不為 NULL 且第 0 位為 0,該 page 為頁緩存或文件映射,mapping 指向文件的地址空間;當 mapping 不為 NULL 且第 0 位為 1,該 page 為匿名頁(匿名映射),mapping 指向 struct anon_vma 對象

pgoff_t index,映射虛擬內存空間里的地址偏移,一個文件可能只映射其中的一部分,假設映射了 1M 的空間,index 指的是在 1M 空間內的偏移,而不是在整個文件內的偏移

unsigned long private,私有數據指針

atomic_t _mapcount,該 page 被頁表映射的次數,即這個 page 被多少個進程共享,初始值為-1(非伙伴系統,如果是伙伴系統則為 PAGE_BUDDY_MAPCOUNT_VALUE),例如只被一個進程的頁表映射的話,值為 0

atomic_t _refcount,頁表引用計數,內核要操作該 page 時,引用計數會+1,操作完成后則-1,當引用計數為 0 時,表示該 page 沒有被引用到,這時候就可以解除該 page 的映射(虛擬頁-物理頁,該物理頁是占用內存的)(用于內存回收)

更詳細的內容可以參考源代碼~

ok,回到 memmap_init_zone(),直接看關鍵函數**__init_single_page**()

static?void?__meminit?__init_single_page(struct?page?*page,?unsigned?long?pfn,unsigned?long?zone,?int?nid) {mm_zero_struct_page(page);?//page初始化,根據page大小還有一些特殊操作set_page_links(page,?zone,?nid,?pfn);?//flags初始化,將頁面映射到zone和nodeinit_page_count(page);?//page的_refcount設置為1page_mapcount_reset(page);?//page的_mapcount設置為-1INIT_LIST_HEAD(&page->lru);?//初始化lru,指向自身...... }

至此,free_area_init_node()的初始化操作執行完畢,據前面分析可以知道該函數主要是將整個 linux 物理內存管理框架進行初始化,包括內存管理節點 node、管理區 zone 以及頁面管理 page 等數據的初始化

回到前面的 free_area_init()函數的循環體內的最后兩個函數 node_set_state()和 check_for_memory(),node_set_state()主要是對 node 節點進行狀態設置,而 check_for_memory()則是做內存檢查。

到這里,內存管理框架的構建基本完畢。

void?__init?setup_arch(char?**cmdline_p) {......max_pfn?=?e820__end_of_raCm_pfn();?//max_pfn初始化......find_low_pfn_range();?//max_low_pfn、高端內存初始化............early_alloc_pgt_buf();?//頁表緩沖區分配reserve_brk();?//緩沖區加入memblock.reserve......e820__memblock_setup();?//memblock.memory空間初始化?啟動......init_mem_mapping();?//低端內存內核頁表初始化?高端內存固定映射區中臨時映射區頁表初始化......initmem_init();?//high_memory初始化?通過memblock內存管理器設置node節點信息......x86_init.paging.pagetable_init();?//?節點node、內存管理區zone、page初始化...... }

最后補充一下 pfn 和物理地址以及 pfn 和虛擬地址的轉換。

//物理地址->物理頁 #define?PAGE_SHIFT?_PAGE_SHIFT #define?_PAGE_SHIFT?12 #define?phys_to_pfn(phys)?((phys)?>>?PAGE_SHIFT) #define?pfn_to_phys(pfn)?((pfn)?<<?PAGE_SHIFT)#define?phys_to_page(phys)?pfn_to_page(phys_to_pfn(phys)) #define?page_to_phys(page)?pfn_to_phys(page_to_pfn(page))

pfn_to_page、page_to_pfn 可以參考上面的內存模型,不同的模型實現的細節不一樣。

這里可以看出物理頁的大小是 4096,即 4kb,雖然內核在虛擬地址中是在高地址的,但是在物理地址中是從 0 開始的。

在 linux 內核直接映射區里內核邏輯地址與物理頁的轉換關系如下:

#define?pfn_to_virt(pfn)?__va(pfn_to_phys(pfn)) #define?virt_to_pfn(kaddr)?(phys_to_pfn(__pa(kaddr))) #define?virt_to_page(kaddr)?pfn_to_page(virt_to_pfn(kaddr)) #define?page_to_virt(page)?pfn_to_virt(page_to_pfn(page))#define?__pa(x)?????????((unsigned?long)?(x)?-?PAGE_OFFSET) #define?__va(x)?????????((void?*)((unsigned?long)?(x)?+?PAGE_OFFSET))

PAGE_OFFSET 與具體的架構有關,在 x86_32 中,PAGE_OFFSET 是 0xC0000000,即 32 位系統中,內核的邏輯地址只有高位的 1GB

總結

Linux 內存管理是一個很復雜的“工程”,Linux 會通過中斷調用獲取被 BIOS 保留的內存地址范圍以及系統可以使用的內存地址范圍。在內核初始化階段,通過 memblock 內存分配器,實現頁分配器初始化之前的內存管理和分配請求,memblock 內存區管理算法將可用可分配的內存用 memblock.memory 進行管理,已分配的內存用 memblock.reserved 進行管理,只要內存塊加入到 memblock.reserved 里面就表示該內存被申請占用了,申請和釋放的操作都集中在 memblock.reserved,這個算法效率不高,但是卻是合理的,因為在內核初始化階段并沒有太多復雜的內存操作場景,而且很多地方都是申請的內存都是永久使用的。為了合理地利用 4G 的內存空間,Linux 采用了 3:1 的策略,即內核占用 1G 的線性地址空間,用戶占用 3G 的線性地址空間,且 Linux 采用了一種折中方案是只對 1G 內核空間的前 896 MB 按線性映射, 剩下的 128 MB 采用動態映射,即走多級頁表翻譯,這樣,內核態能訪問空間就更多了。

傳統的多核運算是使用 SMP(Symmetric Multi-Processor )模式,將多個處理器與一個集中的存儲器和 I/O 總線相連,所有處理器訪問同一個物理存儲器,因此 SMP 系統有時也被稱為一致存儲器訪問(UMA)結構體系。而 NUMA 模式是一種分布式存儲器訪問方式,處理器可以同時訪問不同的存儲器地址,大幅度提高并行性。NUMA 模式下系統的每個 CPU 都有本地內存,可支持快速訪問,各個處理器之間通過總線連接起來,以支持對其它 CPU 本地內存的訪問,但是這些訪問要比處理器本地內存的慢。Linux 內核通過插入一些兼容層,使兩個不同體系結構的差異被隱藏,兩種模式都使用了同一個數據結構,另外 linux 的物理內存管理機制將物理內存劃分為三個層次來管理,依次是:Node(存儲節點)、Zone(管理區)和 Page(頁面)。

Linux 內存管理的內容十分多且復雜,上面介紹到的也只是其中的一部分,如果感興趣的話可以下載一份源代碼,然后細細品味。

參考文獻

Linux 內存管理-Zone:https://blog.csdn.net/wyy4045/article/details/81776277

Linux 內存管理-Node:https://blog.csdn.net/wyy4045/article/details/81708895

內存管理框架:https://www.jeanleo.com/

memblock:https://biscuitos.github.io/blog/MMU-ARM32-MEMBLOCK-index/

Zone_sizes_init:http://www.soolco.com/post/19152_1_1.html

內核頁表:https://www.daimajiaoliu.com/daima/47db35735100402

linux 內存模型:http://www.wowotech.net/memory_management/memory_model.html

linux 中的分頁機制:http://edsionte.com/techblog/archives/3435

linux 內核介紹:https://richardweiyang-2.gitbook.io/kernel-exploring

struct page:https://blog.csdn.net/gatieme/article/details/52384636

頁表初始化:https://www.cnblogs.com/tolimit/p/4585803.html

視頻號最新視頻

總結

以上是生活随笔為你收集整理的一文掌握 Linux 内存管理的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

久久五月精品中文字幕 | 在线播放亚洲第一字幕 | 男人的天堂av网站 | 欧美乱妇无乱码大黄a片 | 六月丁香婷婷色狠狠久久 | 精品无码一区二区三区爱欲 | 捆绑白丝粉色jk震动捧喷白浆 | 国产精品久久久久影院嫩草 | 无码av最新清无码专区吞精 | 国产综合久久久久鬼色 | 亚洲精品午夜无码电影网 | 清纯唯美经典一区二区 | 国产超碰人人爽人人做人人添 | 欧美 日韩 人妻 高清 中文 | 日韩av激情在线观看 | 国产精品99久久精品爆乳 | 久久精品视频在线看15 | 亚洲中文字幕无码一久久区 | 人人妻人人藻人人爽欧美一区 | 亚洲成a人片在线观看无码3d | 牲欲强的熟妇农村老妇女视频 | 欧美黑人巨大xxxxx | 1000部夫妻午夜免费 | 国产精品香蕉在线观看 | 无码国模国产在线观看 | 国产亚洲精品精品国产亚洲综合 | 亚洲精品无码国产 | 亚洲日韩中文字幕在线播放 | 露脸叫床粗话东北少妇 | 色婷婷综合激情综在线播放 | 中文字幕精品av一区二区五区 | 一本大道伊人av久久综合 | 精品亚洲韩国一区二区三区 | 麻豆精品国产精华精华液好用吗 | yw尤物av无码国产在线观看 | 欧洲精品码一区二区三区免费看 | 国产精品理论片在线观看 | 一本大道久久东京热无码av | √天堂中文官网8在线 | 国产精品久久国产精品99 | 国产高清av在线播放 | 久久久久成人片免费观看蜜芽 | 精品久久久久久亚洲精品 | 国内精品人妻无码久久久影院 | 国产日产欧产精品精品app | 中国女人内谢69xxxx | 无码乱肉视频免费大全合集 | 亚洲一区二区三区在线观看网站 | 大色综合色综合网站 | 欧美精品免费观看二区 | 国产一精品一av一免费 | 成人欧美一区二区三区黑人免费 | 清纯唯美经典一区二区 | 国产色视频一区二区三区 | √天堂中文官网8在线 | 午夜嘿嘿嘿影院 | www国产精品内射老师 | 亚洲欧洲日本无在线码 | 欧美丰满熟妇xxxx性ppx人交 | 久久久精品国产sm最大网站 | 精品国产一区二区三区四区 | 青青草原综合久久大伊人精品 | 久久久国产精品无码免费专区 | 午夜精品久久久久久久 | 性色av无码免费一区二区三区 | 国产乱人偷精品人妻a片 | 久久久久成人精品免费播放动漫 | 狠狠躁日日躁夜夜躁2020 | 人人澡人人透人人爽 | 国产亚洲精品久久久久久 | 午夜精品一区二区三区在线观看 | 欧美日本免费一区二区三区 | 国产真实伦对白全集 | 国产亚洲精品久久久久久久 | 偷窥日本少妇撒尿chinese | 午夜免费福利小电影 | 成人无码影片精品久久久 | 国产精品高潮呻吟av久久 | 中文无码伦av中文字幕 | 福利一区二区三区视频在线观看 | 乱中年女人伦av三区 | www一区二区www免费 | 免费无码一区二区三区蜜桃大 | 婷婷丁香六月激情综合啪 | 国产精品国产三级国产专播 | 国产精品久久久一区二区三区 | 精品夜夜澡人妻无码av蜜桃 | 精品一二三区久久aaa片 | 国产午夜亚洲精品不卡下载 | 亚洲国产一区二区三区在线观看 | 女人高潮内射99精品 | 久久精品人人做人人综合 | 欧美人与动性行为视频 | 少妇久久久久久人妻无码 | 人妻无码久久精品人妻 | 狠狠综合久久久久综合网 | 欧美xxxx黑人又粗又长 | 欧洲vodafone精品性 | 少妇无码av无码专区在线观看 | 国产亚洲欧美在线专区 | 日本肉体xxxx裸交 | 最新国产麻豆aⅴ精品无码 | 97资源共享在线视频 | 日本爽爽爽爽爽爽在线观看免 | 国产97色在线 | 免 | 中文字幕av无码一区二区三区电影 | 久久天天躁夜夜躁狠狠 | 精品无码国产一区二区三区av | 亚洲自偷自偷在线制服 | 精品国精品国产自在久国产87 | 国产卡一卡二卡三 | 亚洲国产精品一区二区美利坚 | 欧美激情一区二区三区成人 | 99久久久国产精品无码免费 | 日本丰满护士爆乳xxxx | 午夜精品一区二区三区的区别 | 一区二区三区乱码在线 | 欧洲 | 欧美性生交xxxxx久久久 | 成人片黄网站色大片免费观看 | 黑人玩弄人妻中文在线 | 夜夜躁日日躁狠狠久久av | 亚洲熟妇色xxxxx欧美老妇 | 台湾无码一区二区 | 未满小14洗澡无码视频网站 | 无码av免费一区二区三区试看 | 亚洲综合另类小说色区 | 精品国产精品久久一区免费式 | 久久精品国产精品国产精品污 | 久久久久亚洲精品男人的天堂 | 国产麻豆精品一区二区三区v视界 | 亚洲熟妇色xxxxx欧美老妇 | 好爽又高潮了毛片免费下载 | 亚洲精品无码人妻无码 | 国产精品久久国产三级国 | 伊人久久大香线蕉午夜 | 蜜桃无码一区二区三区 | 精品一区二区三区波多野结衣 | 成人精品视频一区二区 | 欧美人与禽猛交狂配 | 成人毛片一区二区 | 无码乱肉视频免费大全合集 | 色欲av亚洲一区无码少妇 | 久久精品无码一区二区三区 | 久久视频在线观看精品 | 波多野结衣av一区二区全免费观看 | 国产美女极度色诱视频www | 成人性做爰aaa片免费看不忠 | 东京无码熟妇人妻av在线网址 | 俺去俺来也www色官网 | 无码精品人妻一区二区三区av | 国产午夜无码视频在线观看 | 国产内射爽爽大片视频社区在线 | 3d动漫精品啪啪一区二区中 | 精品国产青草久久久久福利 | 激情内射日本一区二区三区 | 波多野结衣av在线观看 | 丝袜足控一区二区三区 | 色一情一乱一伦一区二区三欧美 | 搡女人真爽免费视频大全 | 红桃av一区二区三区在线无码av | www国产亚洲精品久久网站 | 欧美三级a做爰在线观看 | 国色天香社区在线视频 | 久久www免费人成人片 | 激情亚洲一区国产精品 | av无码久久久久不卡免费网站 | 2019午夜福利不卡片在线 | 一本久久伊人热热精品中文字幕 | 国产国语老龄妇女a片 | 欧美一区二区三区 | 人人爽人人澡人人人妻 | 精品人妻人人做人人爽 | 国产女主播喷水视频在线观看 | 又紧又大又爽精品一区二区 | 黑人巨大精品欧美一区二区 | 国产亲子乱弄免费视频 | 成人一区二区免费视频 | 人妻少妇精品视频专区 | 无码一区二区三区在线观看 | 天天燥日日燥 | 欧美丰满老熟妇xxxxx性 | 亚洲の无码国产の无码影院 | 色婷婷综合激情综在线播放 | 国产成人精品久久亚洲高清不卡 | 色欲久久久天天天综合网精品 | 99久久精品无码一区二区毛片 | 又粗又大又硬毛片免费看 | 国产成人精品视频ⅴa片软件竹菊 | 国产成人无码专区 | 国产卡一卡二卡三 | av人摸人人人澡人人超碰下载 | 久久久久免费看成人影片 | 欧美人与动性行为视频 | 欧美黑人性暴力猛交喷水 | 久久99热只有频精品8 | 国产两女互慰高潮视频在线观看 | 兔费看少妇性l交大片免费 | 欧美激情内射喷水高潮 | 国产精品国产三级国产专播 | 日本饥渴人妻欲求不满 | 午夜福利试看120秒体验区 | 18禁黄网站男男禁片免费观看 | 丰满少妇熟乱xxxxx视频 | 欧美性色19p | 精品国产一区二区三区四区 | 亚洲欧洲中文日韩av乱码 | 又大又紧又粉嫩18p少妇 | 日本一卡2卡3卡四卡精品网站 | 亚洲午夜无码久久 | 成人欧美一区二区三区 | 欧美精品国产综合久久 | 国产精品毛多多水多 | 国产精品嫩草久久久久 | 欧美刺激性大交 | 免费播放一区二区三区 | 中文字幕乱码人妻无码久久 | 国产精品久久久久9999小说 | 国产精品内射视频免费 | 午夜精品久久久久久久久 | 亚洲人交乣女bbw | 亚洲色成人中文字幕网站 | 学生妹亚洲一区二区 | 欧美 日韩 人妻 高清 中文 | 老头边吃奶边弄进去呻吟 | 欧美日本免费一区二区三区 | 露脸叫床粗话东北少妇 | 亚洲大尺度无码无码专区 | 中文字幕精品av一区二区五区 | 精品无人国产偷自产在线 | 日本精品人妻无码免费大全 | 波多野结衣aⅴ在线 | 九九综合va免费看 | 美女张开腿让人桶 | 国产成人久久精品流白浆 | 中文字幕av无码一区二区三区电影 | 中文字幕无码人妻少妇免费 | 亚洲日韩一区二区 | 国产精品手机免费 | 欧美激情一区二区三区成人 | 国产色视频一区二区三区 | 99在线 | 亚洲 | 色综合视频一区二区三区 | 丰满少妇人妻久久久久久 | 搡女人真爽免费视频大全 | 无套内谢的新婚少妇国语播放 | 欧美35页视频在线观看 | 国产精品高潮呻吟av久久 | 国产另类ts人妖一区二区 | 婷婷色婷婷开心五月四房播播 | 狠狠色欧美亚洲狠狠色www | 日韩av无码一区二区三区 | 国产一区二区三区精品视频 | 国产疯狂伦交大片 | 99久久精品午夜一区二区 | 国产成人精品视频ⅴa片软件竹菊 | 麻豆精品国产精华精华液好用吗 | 2019nv天堂香蕉在线观看 | 国产精品人人爽人人做我的可爱 | 久久久精品欧美一区二区免费 | 精品人妻人人做人人爽夜夜爽 | 伊在人天堂亚洲香蕉精品区 | 亚洲日韩精品欧美一区二区 | 亚洲一区二区三区偷拍女厕 | 国产极品美女高潮无套在线观看 | 成人片黄网站色大片免费观看 | 熟妇人妻无乱码中文字幕 | 精品 日韩 国产 欧美 视频 | 中文字幕人妻丝袜二区 | 欧美丰满熟妇xxxx | 亚洲成a人片在线观看无码 | 亚洲中文字幕久久无码 | 久久综合给久久狠狠97色 | 成熟妇人a片免费看网站 | 人人超人人超碰超国产 | 亚洲精品久久久久久一区二区 | 免费播放一区二区三区 | 亚洲日韩av一区二区三区四区 | 国产精品久久久久久亚洲影视内衣 | 亚洲人成无码网www | 欧美freesex黑人又粗又大 | 国产精品18久久久久久麻辣 | 水蜜桃色314在线观看 | 帮老师解开蕾丝奶罩吸乳网站 | 131美女爱做视频 | 色 综合 欧美 亚洲 国产 | 桃花色综合影院 | 熟妇女人妻丰满少妇中文字幕 | 亚洲成av人片在线观看无码不卡 | 中文字幕日韩精品一区二区三区 | 无套内谢的新婚少妇国语播放 | 桃花色综合影院 | 狠狠色色综合网站 | аⅴ资源天堂资源库在线 | 国产亚洲精品久久久久久久久动漫 | 乌克兰少妇性做爰 | 成人性做爰aaa片免费看不忠 | 久久人人爽人人爽人人片av高清 | 中文字幕无码热在线视频 | 中文字幕无码日韩欧毛 | 亚洲欧美日韩综合久久久 | 久热国产vs视频在线观看 | 亚洲中文字幕va福利 | 日本大乳高潮视频在线观看 | 国产欧美亚洲精品a | 国产精品久久久久久久9999 | 亚洲欧洲日本综合aⅴ在线 | 亚洲中文字幕va福利 | 成人欧美一区二区三区黑人 | 国产超碰人人爽人人做人人添 | 一区二区三区乱码在线 | 欧洲 | 国产精品亚洲а∨无码播放麻豆 | 国产亚洲tv在线观看 | 成人动漫在线观看 | 国产电影无码午夜在线播放 | 亚洲一区二区三区无码久久 | 乌克兰少妇xxxx做受 | 国产又爽又黄又刺激的视频 | 久久人人爽人人爽人人片ⅴ | 精品国产精品久久一区免费式 | 亚洲成av人影院在线观看 | 成人无码影片精品久久久 | 丰满妇女强制高潮18xxxx | 亚洲中文字幕成人无码 | 巨爆乳无码视频在线观看 | 亚拍精品一区二区三区探花 | 婷婷综合久久中文字幕蜜桃三电影 | 老司机亚洲精品影院 | 牛和人交xxxx欧美 | 熟女俱乐部五十路六十路av | 欧美性猛交xxxx富婆 | 日本肉体xxxx裸交 | 四十如虎的丰满熟妇啪啪 | 久久久久se色偷偷亚洲精品av | 国产无遮挡又黄又爽免费视频 | 一区二区三区乱码在线 | 欧洲 | 无套内谢老熟女 | 成熟人妻av无码专区 | 国产性生交xxxxx无码 | 乱人伦中文视频在线观看 | 性色欲情网站iwww九文堂 | 久久国产精品萌白酱免费 | 国产超碰人人爽人人做人人添 | 色窝窝无码一区二区三区色欲 | 一区二区传媒有限公司 | 国产农村妇女高潮大叫 | 国产无av码在线观看 | 久久国产精品偷任你爽任你 | 76少妇精品导航 | 亚洲综合另类小说色区 | 国产肉丝袜在线观看 | 疯狂三人交性欧美 | 成人免费视频在线观看 | 欧美激情内射喷水高潮 | 色诱久久久久综合网ywww | 熟女体下毛毛黑森林 | 亚洲国产欧美国产综合一区 | 精品久久久久久亚洲精品 | 任你躁在线精品免费 | 99er热精品视频 | 精品人妻中文字幕有码在线 | 久久久中文字幕日本无吗 | 久久久久99精品成人片 | 国产网红无码精品视频 | 日韩在线不卡免费视频一区 | 夜精品a片一区二区三区无码白浆 | 久久成人a毛片免费观看网站 | 国产精品美女久久久 | 欧美成人高清在线播放 | 内射欧美老妇wbb | 久久人人爽人人爽人人片ⅴ | 亚洲人成影院在线无码按摩店 | 蜜桃av蜜臀av色欲av麻 999久久久国产精品消防器材 | 老子影院午夜精品无码 | 国产在线aaa片一区二区99 | 东京无码熟妇人妻av在线网址 | 国产区女主播在线观看 | 人人澡人人透人人爽 | 极品尤物被啪到呻吟喷水 | 精品国产乱码久久久久乱码 | 国产精品a成v人在线播放 | 成人性做爰aaa片免费看 | 亚洲欧美精品伊人久久 | 性开放的女人aaa片 | 国产精品丝袜黑色高跟鞋 | 亚洲aⅴ无码成人网站国产app | 日日橹狠狠爱欧美视频 | 亚洲 欧美 激情 小说 另类 | 亚洲欧洲日本综合aⅴ在线 | 国产成人无码av一区二区 | 欧洲欧美人成视频在线 | 亚洲成a人片在线观看无码 | 久久久久免费精品国产 | 中文字幕人成乱码熟女app | 黑人玩弄人妻中文在线 | aa片在线观看视频在线播放 | 欧美三级a做爰在线观看 | 亚洲精品一区二区三区在线 | 青草视频在线播放 | 免费无码肉片在线观看 | 亚洲成av人在线观看网址 | 88国产精品欧美一区二区三区 | 久久综合狠狠综合久久综合88 | 亚洲中文字幕在线观看 | 51国偷自产一区二区三区 | 骚片av蜜桃精品一区 | 亚洲中文字幕无码一久久区 | 亚洲国产精品久久久天堂 | 免费播放一区二区三区 | 嫩b人妻精品一区二区三区 | 在线看片无码永久免费视频 | 国产精品嫩草久久久久 | 黄网在线观看免费网站 | 欧美日韩综合一区二区三区 | 久久午夜无码鲁丝片午夜精品 | 一二三四在线观看免费视频 | 亚洲色欲色欲欲www在线 | 377p欧洲日本亚洲大胆 | 久久99精品国产麻豆 | 亚洲毛片av日韩av无码 | 中文字幕人妻丝袜二区 | 亚洲日韩精品欧美一区二区 | 亚洲精品一区二区三区在线观看 | 色综合久久久久综合一本到桃花网 | 欧美性生交活xxxxxdddd | 国产av剧情md精品麻豆 | 麻豆成人精品国产免费 | 性欧美videos高清精品 | 中文字幕亚洲情99在线 | 欧美激情综合亚洲一二区 | 丰满人妻被黑人猛烈进入 | 亚洲经典千人经典日产 | 蜜桃视频插满18在线观看 | 日韩少妇内射免费播放 | 成人女人看片免费视频放人 | 国产真实夫妇视频 | 一本一道久久综合久久 | 中文字幕无码视频专区 | 人妻插b视频一区二区三区 | 日日干夜夜干 | 未满成年国产在线观看 | 日韩av无码一区二区三区不卡 | 东京热男人av天堂 | 国产成人精品必看 | 俺去俺来也在线www色官网 | 国产精品va在线观看无码 | 亚洲人交乣女bbw | 六十路熟妇乱子伦 | 亚洲精品一区二区三区婷婷月 | 好爽又高潮了毛片免费下载 | 久久精品中文字幕大胸 | 国产成人综合美国十次 | 一本大道久久东京热无码av | 精品无码国产一区二区三区av | 无码av最新清无码专区吞精 | 久久久av男人的天堂 | 人人爽人人澡人人人妻 | 亚洲国产欧美国产综合一区 | 亚洲欧美综合区丁香五月小说 | 日本大乳高潮视频在线观看 | 久久人人爽人人爽人人片ⅴ | 熟妇女人妻丰满少妇中文字幕 | 欧美丰满少妇xxxx性 | 欧美日韩一区二区免费视频 | 亚洲欧美日韩综合久久久 | 丰满少妇弄高潮了www | 国产免费久久久久久无码 | 成人免费无码大片a毛片 | 一个人免费观看的www视频 | 久久精品女人的天堂av | 亚洲一区二区三区播放 | 成人免费视频视频在线观看 免费 | 国产成人无码av片在线观看不卡 | ass日本丰满熟妇pics | 久久久中文字幕日本无吗 | 亚洲国产精品无码一区二区三区 | 一区二区传媒有限公司 | 中文字幕av伊人av无码av | 欧美日本免费一区二区三区 | 纯爱无遮挡h肉动漫在线播放 | 人人爽人人爽人人片av亚洲 | 亚洲精品久久久久久久久久久 | 国产无遮挡又黄又爽免费视频 | 一本无码人妻在中文字幕免费 | 国语自产偷拍精品视频偷 | 久久精品国产大片免费观看 | 精品国产一区二区三区av 性色 | 中文字幕av日韩精品一区二区 | 四虎4hu永久免费 | 性做久久久久久久久 | 精品国产一区二区三区四区 | 色婷婷综合中文久久一本 | v一区无码内射国产 | 成 人 网 站国产免费观看 | 久久久久人妻一区精品色欧美 | 国产精品多人p群无码 | 一本色道久久综合亚洲精品不卡 | 99精品国产综合久久久久五月天 | 精品国产福利一区二区 | 性生交大片免费看l | 无码av免费一区二区三区试看 | 久久五月精品中文字幕 | 日韩人妻系列无码专区 | 天堂亚洲2017在线观看 | 中国女人内谢69xxxx | 欧洲美熟女乱又伦 | 亚洲精品久久久久avwww潮水 | 亚洲阿v天堂在线 | 熟妇女人妻丰满少妇中文字幕 | 国产亚av手机在线观看 | 国产精品资源一区二区 | 未满成年国产在线观看 | 色欲久久久天天天综合网精品 | 国产成人综合在线女婷五月99播放 | 88国产精品欧美一区二区三区 | 国产精品久免费的黄网站 | 日韩av无码中文无码电影 | 中文字幕无码日韩欧毛 | 人人超人人超碰超国产 | 久久综合九色综合97网 | 精品人妻人人做人人爽夜夜爽 | 亚洲欧美日韩综合久久久 | 国产人妖乱国产精品人妖 | 国产精品美女久久久网av | 国产亚洲精品久久久久久久久动漫 | 亚洲综合精品香蕉久久网 | 无码av免费一区二区三区试看 | 无码国产乱人伦偷精品视频 | 97精品人妻一区二区三区香蕉 | 高潮毛片无遮挡高清免费 | 亚洲小说图区综合在线 | 国产精品久久久久7777 | 红桃av一区二区三区在线无码av | 亚洲精品中文字幕久久久久 | 夫妻免费无码v看片 | 国产精品99久久精品爆乳 | 伊人久久大香线蕉av一区二区 | 妺妺窝人体色www婷婷 | 亚洲 日韩 欧美 成人 在线观看 | √天堂中文官网8在线 | 在线播放亚洲第一字幕 | 久久国产劲爆∧v内射 | 欧美日韩视频无码一区二区三 | 无码免费一区二区三区 | 婷婷五月综合激情中文字幕 | 欧洲极品少妇 | 国产精品久久久 | 熟女体下毛毛黑森林 | 狠狠色欧美亚洲狠狠色www | 国产成人综合美国十次 | 无码毛片视频一区二区本码 | 天堂亚洲2017在线观看 | 熟妇女人妻丰满少妇中文字幕 | 国产免费久久久久久无码 | 欧美一区二区三区视频在线观看 | a在线观看免费网站大全 | 大色综合色综合网站 | 美女极度色诱视频国产 | 精品无码成人片一区二区98 | 亚洲中文字幕成人无码 | 久久精品人人做人人综合试看 | 国产成人无码区免费内射一片色欲 | 天天综合网天天综合色 | 永久免费观看国产裸体美女 | 久久精品无码一区二区三区 | 在线亚洲高清揄拍自拍一品区 | 老司机亚洲精品影院 | 内射巨臀欧美在线视频 | 粗大的内捧猛烈进出视频 | 国产精品无码一区二区桃花视频 | 欧美亚洲日韩国产人成在线播放 | 九月婷婷人人澡人人添人人爽 | 乱人伦人妻中文字幕无码 | 波多野结衣aⅴ在线 | 亚洲欧洲中文日韩av乱码 | 国产激情艳情在线看视频 | 小鲜肉自慰网站xnxx | 成人亚洲精品久久久久 | 久久视频在线观看精品 | 国产 浪潮av性色四虎 | 少妇无码av无码专区在线观看 | 亚洲爆乳大丰满无码专区 | 国产人妻久久精品二区三区老狼 | 在线亚洲高清揄拍自拍一品区 | 亚洲国产午夜精品理论片 | 天堂一区人妻无码 | 无码人妻精品一区二区三区下载 | 伦伦影院午夜理论片 | 亚洲综合无码久久精品综合 | 亚洲精品一区二区三区婷婷月 | 丰腴饱满的极品熟妇 | 欧美喷潮久久久xxxxx | 久久久精品456亚洲影院 | 无套内谢的新婚少妇国语播放 | av无码电影一区二区三区 | 色综合久久久无码中文字幕 | 欧美三级a做爰在线观看 | 亚洲a无码综合a国产av中文 | 久久综合网欧美色妞网 | 人妻夜夜爽天天爽三区 | 在线精品国产一区二区三区 | 欧美丰满熟妇xxxx性ppx人交 | 日韩亚洲欧美精品综合 | 特级做a爰片毛片免费69 | 日本高清一区免费中文视频 | 九九热爱视频精品 | 丁香花在线影院观看在线播放 | 日日躁夜夜躁狠狠躁 | 内射后入在线观看一区 | 亚洲成a人片在线观看日本 | 亚洲成av人在线观看网址 | 欧美日韩人成综合在线播放 | 成熟女人特级毛片www免费 | 成年女人永久免费看片 | 巨爆乳无码视频在线观看 | 又色又爽又黄的美女裸体网站 | 亚洲午夜福利在线观看 | 欧美人与动性行为视频 | 色综合久久久无码中文字幕 | 2019午夜福利不卡片在线 | 国产人妻大战黑人第1集 | 色综合久久88色综合天天 | 亚洲成a人一区二区三区 | 亚洲综合久久一区二区 | 鲁鲁鲁爽爽爽在线视频观看 | 欧美成人家庭影院 | 又黄又爽又色的视频 | 极品尤物被啪到呻吟喷水 | 精品久久综合1区2区3区激情 | 国产av一区二区三区最新精品 | 国产精品久久久久久久影院 | 久久久久免费看成人影片 | 未满小14洗澡无码视频网站 | а天堂中文在线官网 | 亚洲国产成人av在线观看 | 精品国产一区av天美传媒 | 中文毛片无遮挡高清免费 | 久久精品人人做人人综合试看 | 日本一卡2卡3卡四卡精品网站 | 国产亚洲人成a在线v网站 | 乱人伦中文视频在线观看 | 九九在线中文字幕无码 | 亚洲а∨天堂久久精品2021 | 亚洲乱码国产乱码精品精 | 亚洲无人区午夜福利码高清完整版 | 中文字幕 亚洲精品 第1页 | aⅴ亚洲 日韩 色 图网站 播放 | 亚洲春色在线视频 | 国产美女精品一区二区三区 | 国产精品沙发午睡系列 | 97色伦图片97综合影院 | 国产一区二区三区四区五区加勒比 | 少妇激情av一区二区 | 国产成人一区二区三区在线观看 | 日韩少妇白浆无码系列 | 少妇性l交大片欧洲热妇乱xxx | 暴力强奷在线播放无码 | 色综合天天综合狠狠爱 | 精品水蜜桃久久久久久久 | 国产精品无码成人午夜电影 | 精品成人av一区二区三区 | 蜜桃无码一区二区三区 | 亚洲の无码国产の无码影院 | 欧美熟妇另类久久久久久多毛 | 国产成人无码a区在线观看视频app | 午夜福利电影 | 小sao货水好多真紧h无码视频 | 粗大的内捧猛烈进出视频 | 国产又爽又黄又刺激的视频 | 久久久久成人片免费观看蜜芽 | 国产麻豆精品一区二区三区v视界 | 亚洲成av人影院在线观看 | 久久精品无码一区二区三区 | 日韩视频 中文字幕 视频一区 | 波多野结衣aⅴ在线 | 一二三四社区在线中文视频 | 性色av无码免费一区二区三区 | 欧美国产日产一区二区 | 日本一区二区三区免费播放 | 亚洲国精产品一二二线 | 人妻尝试又大又粗久久 | 99麻豆久久久国产精品免费 | 少妇高潮一区二区三区99 | 国产免费久久久久久无码 | 欧美三级不卡在线观看 | 欧美freesex黑人又粗又大 | 久久久亚洲欧洲日产国码αv | 色综合久久久无码中文字幕 | aⅴ亚洲 日韩 色 图网站 播放 | 老司机亚洲精品影院无码 | 99久久99久久免费精品蜜桃 | 疯狂三人交性欧美 | 亚洲色大成网站www | 精品久久久久久人妻无码中文字幕 | 亚洲中文字幕无码中文字在线 | 亚洲人亚洲人成电影网站色 | 老太婆性杂交欧美肥老太 | 7777奇米四色成人眼影 | 欧美野外疯狂做受xxxx高潮 | 久久zyz资源站无码中文动漫 | 性欧美熟妇videofreesex | 又黄又爽又色的视频 | 亚洲精品一区二区三区四区五区 | 亚洲一区二区三区无码久久 | 亚洲国产精品一区二区美利坚 | 亚洲精品综合一区二区三区在线 | 久久国产精品_国产精品 | 精品国产av色一区二区深夜久久 | 人妻少妇精品久久 | 亚洲精品成a人在线观看 | 国产精品无套呻吟在线 | 久久午夜夜伦鲁鲁片无码免费 | 国产人妻大战黑人第1集 | 免费视频欧美无人区码 | 亚洲国产高清在线观看视频 | 国产成人无码午夜视频在线观看 | 无码av中文字幕免费放 | 国产精品久久久久久无码 | 亚洲色欲色欲天天天www | 久久久久久av无码免费看大片 | 天天拍夜夜添久久精品大 | 丰满少妇高潮惨叫视频 | 蜜桃无码一区二区三区 | 成人无码视频在线观看网站 | 特级做a爰片毛片免费69 | 亚洲国产一区二区三区在线观看 | 一个人看的www免费视频在线观看 | 99久久99久久免费精品蜜桃 | 成人精品视频一区二区三区尤物 | 999久久久国产精品消防器材 | 2020久久香蕉国产线看观看 | 色综合久久88色综合天天 | 欧美 亚洲 国产 另类 | 女人被男人躁得好爽免费视频 | 亚洲经典千人经典日产 | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | 熟妇人妻无乱码中文字幕 | 欧美熟妇另类久久久久久不卡 | 2020最新国产自产精品 | 无码国产乱人伦偷精品视频 | 日本爽爽爽爽爽爽在线观看免 | 中文字幕乱码亚洲无线三区 | 精品午夜福利在线观看 | 精品人妻av区 | 亚洲国产精品美女久久久久 | 日本一卡2卡3卡四卡精品网站 | 亚洲精品国偷拍自产在线麻豆 | 国产精品久久久久9999小说 | 国产疯狂伦交大片 | 国产九九九九九九九a片 | аⅴ资源天堂资源库在线 | 日本乱偷人妻中文字幕 | 色狠狠av一区二区三区 | 国产精品爱久久久久久久 | www国产亚洲精品久久久日本 | 久久久久久久人妻无码中文字幕爆 | 午夜福利不卡在线视频 | 亚洲国产精品一区二区第一页 | 日本www一道久久久免费榴莲 | 无码av岛国片在线播放 | 人妻体内射精一区二区三四 | 又大又黄又粗又爽的免费视频 | 国内综合精品午夜久久资源 | 色综合视频一区二区三区 | 18禁黄网站男男禁片免费观看 | 青草青草久热国产精品 | 18无码粉嫩小泬无套在线观看 | 色一情一乱一伦 | 精品久久8x国产免费观看 | 丰满诱人的人妻3 | 日本高清一区免费中文视频 | 亚洲综合无码一区二区三区 | 亚洲成在人网站无码天堂 | 国产人妻人伦精品 | 美女极度色诱视频国产 | 曰韩少妇内射免费播放 | 无码精品人妻一区二区三区av | 精品一区二区不卡无码av | 亚洲一区二区观看播放 | 大色综合色综合网站 | 精品人妻人人做人人爽 | 四虎国产精品一区二区 | 亚洲中文字幕在线观看 | 久久综合九色综合欧美狠狠 | 99精品无人区乱码1区2区3区 | 亚洲毛片av日韩av无码 | 性色欲情网站iwww九文堂 | 国产成人综合在线女婷五月99播放 | 日本大乳高潮视频在线观看 | 鲁一鲁av2019在线 | 青春草在线视频免费观看 | 久久久国产一区二区三区 | 午夜成人1000部免费视频 | 麻花豆传媒剧国产免费mv在线 | 亚洲综合色区中文字幕 | 国产精品对白交换视频 | 人妻无码久久精品人妻 | 国产精品无码成人午夜电影 | 中文久久乱码一区二区 | 国产精华av午夜在线观看 | 国产精品高潮呻吟av久久4虎 | 亚洲熟妇色xxxxx欧美老妇y | 国产精品久久久久久久影院 | 欧美人与禽猛交狂配 | 国产精品人人妻人人爽 | 天天综合网天天综合色 | 国产精品亚洲综合色区韩国 | 国产精品99爱免费视频 | 国产在线一区二区三区四区五区 | 精品一区二区不卡无码av | 色综合天天综合狠狠爱 | 精品欧洲av无码一区二区三区 | 亚洲精品www久久久 | 俺去俺来也在线www色官网 | 久久久久成人精品免费播放动漫 | 成在人线av无码免观看麻豆 | 狠狠cao日日穞夜夜穞av | 免费观看激色视频网站 | 色窝窝无码一区二区三区色欲 | 永久免费观看国产裸体美女 | 成人无码视频免费播放 | 国产免费观看黄av片 | 亚洲小说图区综合在线 | 亚洲无人区午夜福利码高清完整版 | 精品乱码久久久久久久 | 久久人人爽人人人人片 | 国产色xx群视频射精 | 亚洲精品中文字幕久久久久 | 日韩精品成人一区二区三区 | 国精产品一品二品国精品69xx | 国产av无码专区亚洲awww | 国产亚洲精品久久久久久久久动漫 | 久久国产精品精品国产色婷婷 | 一二三四在线观看免费视频 | 无码av中文字幕免费放 | av无码电影一区二区三区 | 亚洲欧美日韩综合久久久 | 亚洲精品久久久久avwww潮水 | 99精品国产综合久久久久五月天 | 亚洲中文字幕久久无码 | 免费人成在线视频无码 | 大肉大捧一进一出视频出来呀 | 人妻aⅴ无码一区二区三区 | 无码国产乱人伦偷精品视频 | 国产精品18久久久久久麻辣 | 国产精品久久久av久久久 | 久久久久久av无码免费看大片 | 亚洲一区av无码专区在线观看 | av人摸人人人澡人人超碰下载 | 欧美变态另类xxxx | 欧美freesex黑人又粗又大 | 夜精品a片一区二区三区无码白浆 | 成人无码影片精品久久久 | 精品亚洲韩国一区二区三区 | 久久综合给合久久狠狠狠97色 | 久久久久亚洲精品中文字幕 | 久久久久成人精品免费播放动漫 | 成人一区二区免费视频 | 国产在线精品一区二区高清不卡 | 国内精品九九久久久精品 | 天堂久久天堂av色综合 | 亚洲人成影院在线无码按摩店 | 76少妇精品导航 | 精品无码一区二区三区的天堂 | 狠狠躁日日躁夜夜躁2020 | 国产精品久久久久久久9999 | www国产亚洲精品久久久日本 | 亚洲精品午夜无码电影网 | 久久天天躁狠狠躁夜夜免费观看 | 日本一区二区三区免费播放 | 中文字幕精品av一区二区五区 | 久久99国产综合精品 | 欧美性猛交xxxx富婆 | 久久熟妇人妻午夜寂寞影院 | 精品一区二区三区无码免费视频 | 四虎国产精品免费久久 | 青草视频在线播放 | 无码午夜成人1000部免费视频 | 无码人妻av免费一区二区三区 | 国产性生大片免费观看性 | 精品无码国产自产拍在线观看蜜 | 丰满少妇人妻久久久久久 | 少妇性荡欲午夜性开放视频剧场 | 欧美午夜特黄aaaaaa片 | 久久久久免费看成人影片 | 少妇人妻av毛片在线看 | 99精品无人区乱码1区2区3区 | aⅴ在线视频男人的天堂 | 精品 日韩 国产 欧美 视频 | 少妇高潮一区二区三区99 | 人妻人人添人妻人人爱 | 性欧美大战久久久久久久 | 欧美三级a做爰在线观看 | 最近的中文字幕在线看视频 | 亚洲一区av无码专区在线观看 | 亚洲国产精品无码久久久久高潮 | 国产做国产爱免费视频 | 蜜桃无码一区二区三区 | 波多野结衣 黑人 | 国产精品沙发午睡系列 | 55夜色66夜色国产精品视频 | 亚洲国产精品一区二区第一页 | 人妻夜夜爽天天爽三区 | 红桃av一区二区三区在线无码av | 国产人妻人伦精品1国产丝袜 | 久久99精品久久久久婷婷 | 免费播放一区二区三区 | 国产精品福利视频导航 | 男女爱爱好爽视频免费看 | 97久久国产亚洲精品超碰热 | 国产精品人人爽人人做我的可爱 | 国产精品久久国产三级国 | 国产特级毛片aaaaaaa高清 | 图片区 小说区 区 亚洲五月 | 日日摸夜夜摸狠狠摸婷婷 | 99国产精品白浆在线观看免费 | 国产亚洲精品久久久久久大师 | 噜噜噜亚洲色成人网站 | 亚洲 a v无 码免 费 成 人 a v | 日日摸天天摸爽爽狠狠97 | 亚洲中文字幕乱码av波多ji | 亚洲码国产精品高潮在线 | 久热国产vs视频在线观看 | 国产精品丝袜黑色高跟鞋 | 国产疯狂伦交大片 | 亚洲日韩一区二区三区 | 97精品人妻一区二区三区香蕉 | 亚洲精品一区三区三区在线观看 | 鲁一鲁av2019在线 | 丰满人妻翻云覆雨呻吟视频 | 丝袜人妻一区二区三区 | 帮老师解开蕾丝奶罩吸乳网站 | 国产一区二区三区精品视频 | 四虎永久在线精品免费网址 | 亚洲 另类 在线 欧美 制服 | 成人亚洲精品久久久久 | 久久精品人妻少妇一区二区三区 | 国产精品亚洲а∨无码播放麻豆 | 波多野结衣乳巨码无在线观看 | 18禁止看的免费污网站 | 国内精品一区二区三区不卡 | 色婷婷欧美在线播放内射 | 久久综合激激的五月天 | 97久久精品无码一区二区 | 国产精品亚洲综合色区韩国 | 在线观看国产午夜福利片 | 国产性生大片免费观看性 | 漂亮人妻洗澡被公强 日日躁 | 亚洲色在线无码国产精品不卡 | 午夜成人1000部免费视频 | 人妻无码久久精品人妻 | 人人妻人人澡人人爽欧美一区 | 亚洲精品综合一区二区三区在线 | 色婷婷久久一区二区三区麻豆 | 中文字幕人成乱码熟女app | 亚洲欧美日韩综合久久久 | 欧美熟妇另类久久久久久不卡 | 国产疯狂伦交大片 | 国内精品一区二区三区不卡 | 自拍偷自拍亚洲精品10p | 国产精品国产自线拍免费软件 | 一本色道久久综合亚洲精品不卡 | 中文字幕av日韩精品一区二区 | 国产深夜福利视频在线 | 国产在线精品一区二区高清不卡 | 激情内射亚州一区二区三区爱妻 | 国产农村妇女高潮大叫 | 久久 国产 尿 小便 嘘嘘 | 国产69精品久久久久app下载 | 玩弄少妇高潮ⅹxxxyw | 18无码粉嫩小泬无套在线观看 | 丰满岳乱妇在线观看中字无码 | 国产精品久久久av久久久 | 日韩精品a片一区二区三区妖精 | www国产精品内射老师 | 国产激情无码一区二区app | 日本免费一区二区三区最新 | 荫蒂被男人添的好舒服爽免费视频 | 无码人妻丰满熟妇区毛片18 | 国产在线一区二区三区四区五区 | 最近免费中文字幕中文高清百度 | 欧美刺激性大交 | 曰韩少妇内射免费播放 | 一本久久伊人热热精品中文字幕 | 亚洲国产精品久久久天堂 | 少妇激情av一区二区 | 亚洲精品鲁一鲁一区二区三区 | 国产亚洲精品久久久闺蜜 | 亚洲乱亚洲乱妇50p | 日韩少妇内射免费播放 | 熟女体下毛毛黑森林 | 亚洲中文字幕av在天堂 | 人人爽人人爽人人片av亚洲 | 欧美freesex黑人又粗又大 | 国产一精品一av一免费 | 日日橹狠狠爱欧美视频 | 久久午夜夜伦鲁鲁片无码免费 | 最新版天堂资源中文官网 | 久久人人爽人人人人片 | 成人免费无码大片a毛片 | 岛国片人妻三上悠亚 | 一个人看的www免费视频在线观看 | 亚洲七七久久桃花影院 | 人人妻人人藻人人爽欧美一区 | 国产午夜亚洲精品不卡下载 | 丁香啪啪综合成人亚洲 | 国产精品无码一区二区桃花视频 | 少妇愉情理伦片bd | 初尝人妻少妇中文字幕 | 日日碰狠狠丁香久燥 | 国产精品亚洲lv粉色 | 欧美精品在线观看 | 国产精品久久久久久亚洲影视内衣 | 中文字幕中文有码在线 | 国内精品人妻无码久久久影院 | 天天摸天天碰天天添 | 无码精品人妻一区二区三区av | 一二三四社区在线中文视频 | 中文字幕日产无线码一区 | 日日碰狠狠躁久久躁蜜桃 | 亚洲色无码一区二区三区 | 精品乱子伦一区二区三区 | 久久伊人色av天堂九九小黄鸭 | 亚洲伊人久久精品影院 | 国产偷国产偷精品高清尤物 | 日韩人妻无码中文字幕视频 | 久久久成人毛片无码 | 国产乱人伦偷精品视频 | 日韩无套无码精品 | aa片在线观看视频在线播放 | 亚洲 高清 成人 动漫 | 欧美日韩久久久精品a片 | 国产激情艳情在线看视频 | 性生交大片免费看女人按摩摩 | 少女韩国电视剧在线观看完整 | 网友自拍区视频精品 | 国产精品久久久久9999小说 | 无码免费一区二区三区 | 国产激情精品一区二区三区 | 波多野结衣一区二区三区av免费 | 少妇被粗大的猛进出69影院 | 色偷偷av老熟女 久久精品人妻少妇一区二区三区 | 久久综合色之久久综合 | 欧美人与禽zoz0性伦交 | 中文字幕无码免费久久9一区9 | 亚洲色欲色欲欲www在线 | 性生交大片免费看女人按摩摩 | 日韩精品a片一区二区三区妖精 | 国产激情无码一区二区app | 久久久久久久久蜜桃 | 无码人妻精品一区二区三区下载 | 国产精品久久久av久久久 | 久久精品女人天堂av免费观看 | 沈阳熟女露脸对白视频 | 亚欧洲精品在线视频免费观看 | 女人色极品影院 | 久久久久成人片免费观看蜜芽 | 亚洲中文字幕va福利 | 久久99精品国产麻豆 | 亚洲自偷自拍另类第1页 | 国产精品久久精品三级 | 成人av无码一区二区三区 | 中文字幕精品av一区二区五区 | 日韩av无码中文无码电影 | 无码国产色欲xxxxx视频 | 色五月五月丁香亚洲综合网 | 日韩欧美群交p片內射中文 | 亚洲最大成人网站 | 欧美日韩久久久精品a片 | 在线精品亚洲一区二区 | 中文字幕亚洲情99在线 | 天堂一区人妻无码 | 精品国产成人一区二区三区 | 18禁止看的免费污网站 | 麻豆人妻少妇精品无码专区 | 日韩欧美中文字幕在线三区 | 亚洲成色www久久网站 | 爆乳一区二区三区无码 | 麻豆蜜桃av蜜臀av色欲av | 奇米影视7777久久精品 | 国产在线精品一区二区三区直播 | 欧美freesex黑人又粗又大 | 久久无码中文字幕免费影院蜜桃 | www国产精品内射老师 | 国产激情无码一区二区 | 国产精品久久久午夜夜伦鲁鲁 | 欧美日本精品一区二区三区 | 精品一区二区不卡无码av | 无码一区二区三区在线观看 | 色情久久久av熟女人妻网站 | 国产情侣作爱视频免费观看 | 国产人成高清在线视频99最全资源 | 福利一区二区三区视频在线观看 | 大色综合色综合网站 | 国产偷抇久久精品a片69 | 丝袜美腿亚洲一区二区 | 国产精品-区区久久久狼 | 51国偷自产一区二区三区 | 亲嘴扒胸摸屁股激烈网站 | 欧洲精品码一区二区三区免费看 | 2020久久香蕉国产线看观看 | 久久99精品久久久久婷婷 | 伊人久久大香线焦av综合影院 | 在线观看免费人成视频 | 成人性做爰aaa片免费看 | 55夜色66夜色国产精品视频 | 日本一本二本三区免费 | 国产乡下妇女做爰 | 日韩欧美群交p片內射中文 | 人人妻人人澡人人爽人人精品浪潮 | 国内少妇偷人精品视频免费 | 久久综合色之久久综合 | 伊在人天堂亚洲香蕉精品区 | 国产亚av手机在线观看 | 久久综合色之久久综合 | 伊人久久大香线蕉av一区二区 | 日韩亚洲欧美中文高清在线 | 国产成人一区二区三区在线观看 | 国产欧美精品一区二区三区 | 大乳丰满人妻中文字幕日本 | 精品成在人线av无码免费看 | av在线亚洲欧洲日产一区二区 | 任你躁国产自任一区二区三区 | 东北女人啪啪对白 | 精品久久8x国产免费观看 | 成人欧美一区二区三区黑人 | 久久精品国产一区二区三区肥胖 | 精品久久综合1区2区3区激情 | 国产精品高潮呻吟av久久 | 久久天天躁狠狠躁夜夜免费观看 | 少妇太爽了在线观看 | 一本久道高清无码视频 | 高清国产亚洲精品自在久久 | 国产性生大片免费观看性 | 久久久久国色av免费观看性色 | 欧美一区二区三区视频在线观看 | 天天做天天爱天天爽综合网 | 久久 国产 尿 小便 嘘嘘 | 性欧美牲交xxxxx视频 | 欧美日韩在线亚洲综合国产人 | 欧美高清在线精品一区 | 国产激情无码一区二区app | 日本精品少妇一区二区三区 | 亚洲va中文字幕无码久久不卡 | 少妇性俱乐部纵欲狂欢电影 | 77777熟女视频在线观看 а天堂中文在线官网 | 人人超人人超碰超国产 | 欧美日韩一区二区三区自拍 | 伊人久久大香线蕉av一区二区 | 欧美国产亚洲日韩在线二区 | 妺妺窝人体色www婷婷 | 夜精品a片一区二区三区无码白浆 | www国产亚洲精品久久网站 | 日产国产精品亚洲系列 | 欧美猛少妇色xxxxx | 国模大胆一区二区三区 | 超碰97人人射妻 | 性做久久久久久久免费看 | 国产乱人伦av在线无码 | 亚洲の无码国产の无码步美 | 综合网日日天干夜夜久久 | 亚洲成a人片在线观看日本 | 国产精品自产拍在线观看 | 人人妻人人澡人人爽人人精品浪潮 | 中文字幕无线码免费人妻 | 日韩av激情在线观看 | 51国偷自产一区二区三区 | 无遮无挡爽爽免费视频 | 人妻尝试又大又粗久久 | 人人妻人人澡人人爽人人精品浪潮 | 日本欧美一区二区三区乱码 | 中文字幕人妻无码一区二区三区 | 成年美女黄网站色大免费视频 | 影音先锋中文字幕无码 | 永久黄网站色视频免费直播 | 久久久久av无码免费网 | 亚洲欧洲日本无在线码 | 人妻夜夜爽天天爽三区 | 99麻豆久久久国产精品免费 | 精品国产一区av天美传媒 | 亚洲一区二区三区偷拍女厕 | 97久久国产亚洲精品超碰热 | 亚洲一区二区三区在线观看网站 | 欧美xxxx黑人又粗又长 | 人妻中文无码久热丝袜 | 亚洲国精产品一二二线 | 国产精品资源一区二区 | 日韩欧美中文字幕公布 | 色婷婷av一区二区三区之红樱桃 | 一本无码人妻在中文字幕免费 | 国产真实乱对白精彩久久 | 国产成人综合在线女婷五月99播放 | 自拍偷自拍亚洲精品被多人伦好爽 | 伊在人天堂亚洲香蕉精品区 | 久久综合激激的五月天 | 精品偷拍一区二区三区在线看 | 欧洲精品码一区二区三区免费看 | 扒开双腿吃奶呻吟做受视频 | 4hu四虎永久在线观看 | 少妇被粗大的猛进出69影院 | 乱人伦人妻中文字幕无码 | 国产亚洲精品久久久久久 | 亚洲爆乳精品无码一区二区三区 | 六十路熟妇乱子伦 | 国产特级毛片aaaaaa高潮流水 | 国产av一区二区精品久久凹凸 | 无码人妻精品一区二区三区不卡 | 亚洲狠狠色丁香婷婷综合 | 狠狠躁日日躁夜夜躁2020 | 巨爆乳无码视频在线观看 | 色一情一乱一伦一区二区三欧美 | 7777奇米四色成人眼影 | 久精品国产欧美亚洲色aⅴ大片 | 亚洲国产精品毛片av不卡在线 | 亚洲午夜无码久久 | 国产成人无码区免费内射一片色欲 | 成在人线av无码免观看麻豆 | 日韩av无码中文无码电影 | 亚洲精品久久久久久一区二区 | 久热国产vs视频在线观看 | 色婷婷香蕉在线一区二区 | 亚洲色成人中文字幕网站 | 久久久亚洲欧洲日产国码αv | 窝窝午夜理论片影院 | 久久伊人色av天堂九九小黄鸭 | 激情爆乳一区二区三区 | 成人欧美一区二区三区黑人免费 | 十八禁真人啪啪免费网站 | 精品国产福利一区二区 | 少妇激情av一区二区 | 久久99久久99精品中文字幕 | 日本高清一区免费中文视频 | 欧美午夜特黄aaaaaa片 | 成人亚洲精品久久久久软件 | 亚洲精品综合五月久久小说 | 99精品国产综合久久久久五月天 | 亚洲人成人无码网www国产 | 麻花豆传媒剧国产免费mv在线 | 欧美兽交xxxx×视频 | 亚洲国产午夜精品理论片 | 国产精品人妻一区二区三区四 | 日韩人妻无码中文字幕视频 | 亚洲日韩一区二区三区 | 国产午夜福利100集发布 | 国产精品igao视频网 | 中文字幕无码热在线视频 | 97久久精品无码一区二区 | 日本高清一区免费中文视频 | 亚洲热妇无码av在线播放 | 亚洲成a人片在线观看日本 | 亚洲精品综合五月久久小说 | 午夜福利一区二区三区在线观看 | 国产成人无码a区在线观看视频app | 国产在线无码精品电影网 | 99久久精品国产一区二区蜜芽 | 性欧美videos高清精品 | 熟女俱乐部五十路六十路av | 午夜理论片yy44880影院 | 狠狠cao日日穞夜夜穞av | √8天堂资源地址中文在线 | 成人aaa片一区国产精品 | 亚洲成av人在线观看网址 | 无套内谢的新婚少妇国语播放 | 久久久久亚洲精品中文字幕 | 给我免费的视频在线观看 | 欧美兽交xxxx×视频 | 人人妻人人澡人人爽欧美一区 | 国产亚洲精品久久久ai换 | 亚洲色大成网站www国产 | 国产欧美熟妇另类久久久 | 国产色在线 | 国产 | 中文字幕无线码 | 日韩 欧美 动漫 国产 制服 | 久久成人a毛片免费观看网站 | 久久www免费人成人片 | 欧美阿v高清资源不卡在线播放 | 国产一精品一av一免费 | 久久精品国产一区二区三区肥胖 | 无码一区二区三区在线 | 少妇无码一区二区二三区 | 午夜精品一区二区三区的区别 | 久久久精品456亚洲影院 | 小鲜肉自慰网站xnxx | 爽爽影院免费观看 | 老熟妇仑乱视频一区二区 | 亚洲欧洲日本无在线码 | 曰韩少妇内射免费播放 | 欧美变态另类xxxx | 国产精品嫩草久久久久 | 熟妇人妻无乱码中文字幕 | 久久久久亚洲精品中文字幕 | 亚洲国产精品久久人人爱 | 成人无码视频在线观看网站 | 中文字幕无码乱人伦 | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | 又粗又大又硬又长又爽 | 无套内谢的新婚少妇国语播放 | 精品少妇爆乳无码av无码专区 | 帮老师解开蕾丝奶罩吸乳网站 | 欧美日本免费一区二区三区 | 久久国语露脸国产精品电影 | 欧美性猛交内射兽交老熟妇 | 超碰97人人射妻 | 色欲人妻aaaaaaa无码 | 亚洲精品一区二区三区四区五区 | 亚洲精品一区二区三区在线 | 中文字幕日韩精品一区二区三区 | 久久精品女人天堂av免费观看 | 色爱情人网站 | 国内综合精品午夜久久资源 | 一本久道高清无码视频 | 风流少妇按摩来高潮 | 久久99精品久久久久久动态图 | 亚洲成av人片在线观看无码不卡 | 小sao货水好多真紧h无码视频 | 国产办公室秘书无码精品99 | 亚洲欧美中文字幕5发布 | 一本加勒比波多野结衣 | 亚洲狠狠婷婷综合久久 | 国产极品美女高潮无套在线观看 | 日本熟妇人妻xxxxx人hd | 欧美性黑人极品hd | 大色综合色综合网站 | 妺妺窝人体色www在线小说 | 久久久久人妻一区精品色欧美 | 国产九九九九九九九a片 | 免费无码一区二区三区蜜桃大 | 粉嫩少妇内射浓精videos | 大乳丰满人妻中文字幕日本 | 国产特级毛片aaaaaaa高清 | 鲁一鲁av2019在线 | 国产成人亚洲综合无码 | 久久久久久国产精品无码下载 | 中文无码精品a∨在线观看不卡 | 精品熟女少妇av免费观看 | 国产女主播喷水视频在线观看 | 亚洲成av人影院在线观看 | 久久综合给合久久狠狠狠97色 | 麻豆成人精品国产免费 | 人妻尝试又大又粗久久 | 日韩亚洲欧美精品综合 | 理论片87福利理论电影 | 日韩欧美中文字幕公布 | 亚洲啪av永久无码精品放毛片 | 精品人妻人人做人人爽夜夜爽 | 中文字幕乱妇无码av在线 | 又色又爽又黄的美女裸体网站 | 丰满人妻精品国产99aⅴ | 日韩欧美成人免费观看 | 老熟妇仑乱视频一区二区 | 久久午夜无码鲁丝片午夜精品 | 熟妇人妻无乱码中文字幕 | 日本丰满护士爆乳xxxx | 伊人久久大香线蕉午夜 | 双乳奶水饱满少妇呻吟 | 色窝窝无码一区二区三区色欲 | 久久综合给久久狠狠97色 | 成人免费视频视频在线观看 免费 | 好男人社区资源 | 欧美 丝袜 自拍 制服 另类 | 亚洲国产精品一区二区美利坚 | 久久午夜无码鲁丝片秋霞 | 国产成人精品三级麻豆 | 免费乱码人妻系列无码专区 | 久久久婷婷五月亚洲97号色 | 国产成人精品久久亚洲高清不卡 | 国产乱人伦av在线无码 | 青青草原综合久久大伊人精品 | 亚洲日韩av片在线观看 | 18无码粉嫩小泬无套在线观看 | 婷婷丁香五月天综合东京热 | 最新国产乱人伦偷精品免费网站 | 内射后入在线观看一区 | 亚洲无人区午夜福利码高清完整版 | 亚洲午夜福利在线观看 | 1000部夫妻午夜免费 | 国产极品美女高潮无套在线观看 | 久激情内射婷内射蜜桃人妖 | 精品久久久久久人妻无码中文字幕 | 日产国产精品亚洲系列 | 亚洲日本va中文字幕 | 欧美激情一区二区三区成人 | 国产成人无码一二三区视频 | 国产精品无码永久免费888 | 亚洲狠狠婷婷综合久久 | 俺去俺来也www色官网 | 国产精品久久久久久亚洲影视内衣 | 色五月丁香五月综合五月 | 成人片黄网站色大片免费观看 | 日本精品高清一区二区 | 色综合视频一区二区三区 | 国产亚洲精品久久久久久大师 | 黑人巨大精品欧美一区二区 | 内射后入在线观看一区 | 99久久婷婷国产综合精品青草免费 | 国产色视频一区二区三区 | 男女下面进入的视频免费午夜 | 精品国产aⅴ无码一区二区 | 国产亲子乱弄免费视频 | 人人超人人超碰超国产 | 草草网站影院白丝内射 | 国产在热线精品视频 | 大肉大捧一进一出视频出来呀 | 又色又爽又黄的美女裸体网站 | 国产性生大片免费观看性 | 十八禁真人啪啪免费网站 | 国产国产精品人在线视 | 久久久中文久久久无码 | 久久99精品国产麻豆蜜芽 | 香蕉久久久久久av成人 | 激情亚洲一区国产精品 | 欧美性猛交xxxx富婆 | 蜜桃无码一区二区三区 | 久久人人爽人人爽人人片ⅴ | 亚洲呦女专区 | 亚洲一区二区三区偷拍女厕 | 国产激情无码一区二区app | 久久精品人人做人人综合试看 | 久久久精品人妻久久影视 | √天堂中文官网8在线 | 人妻aⅴ无码一区二区三区 | 高清国产亚洲精品自在久久 | 亚洲最大成人网站 | 国产亚洲日韩欧美另类第八页 | 老熟妇乱子伦牲交视频 | 国产97人人超碰caoprom | 51国偷自产一区二区三区 | 国产精品va在线观看无码 | 国产电影无码午夜在线播放 | 国产成人精品视频ⅴa片软件竹菊 | 麻花豆传媒剧国产免费mv在线 | 亚洲国产高清在线观看视频 | 性欧美牲交在线视频 | 久久久久se色偷偷亚洲精品av | 小鲜肉自慰网站xnxx | 亚洲精品国偷拍自产在线观看蜜桃 | 国产激情一区二区三区 | 免费男性肉肉影院 | 国产午夜无码视频在线观看 | 久青草影院在线观看国产 | 麻豆精产国品 | 亚洲欧美色中文字幕在线 | 亚洲の无码国产の无码步美 | 久久人人97超碰a片精品 | 国产明星裸体无码xxxx视频 | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | 亚洲中文字幕无码一久久区 | 亚洲欧美色中文字幕在线 | а√资源新版在线天堂 | 久久精品国产精品国产精品污 | 精品无码一区二区三区的天堂 | 国产精品99久久精品爆乳 | 欧美日本免费一区二区三区 | 伊在人天堂亚洲香蕉精品区 | 精品人妻人人做人人爽 | 国产亚洲日韩欧美另类第八页 | 人人妻人人澡人人爽欧美一区九九 | 亚洲熟妇色xxxxx欧美老妇 | 激情内射亚州一区二区三区爱妻 | 色一情一乱一伦一视频免费看 | 国产xxx69麻豆国语对白 | 精品成人av一区二区三区 | 我要看www免费看插插视频 | 性色欲网站人妻丰满中文久久不卡 | 精品 日韩 国产 欧美 视频 | 亚洲欧洲无卡二区视頻 | 久久国产劲爆∧v内射 | 国产成人无码av一区二区 | 福利一区二区三区视频在线观看 | 国产明星裸体无码xxxx视频 | 国产精品99久久精品爆乳 | 中文字幕无码热在线视频 | 76少妇精品导航 | 乱人伦人妻中文字幕无码 | 无码成人精品区在线观看 | 男女下面进入的视频免费午夜 | 欧美老熟妇乱xxxxx | 国内精品九九久久久精品 | 亚洲国产精品无码一区二区三区 | 国产熟妇另类久久久久 | 亚洲精品久久久久avwww潮水 | 荫蒂添的好舒服视频囗交 | 国产成人精品久久亚洲高清不卡 | 成在人线av无码免观看麻豆 | 一本一道久久综合久久 | 中文字幕久久久久人妻 | 国产又爽又黄又刺激的视频 | 国产精品无码一区二区桃花视频 | 久久精品女人的天堂av | 无遮无挡爽爽免费视频 | 人妻人人添人妻人人爱 | a在线观看免费网站大全 | 沈阳熟女露脸对白视频 | 久久久中文久久久无码 | 成人一在线视频日韩国产 | 国产av剧情md精品麻豆 | 任你躁国产自任一区二区三区 | 日韩欧美成人免费观看 | 中文字幕无码免费久久9一区9 | 无码人妻av免费一区二区三区 | 亚洲精品久久久久久一区二区 | 国产农村乱对白刺激视频 | 国产亚洲精品久久久久久久久动漫 | 亚洲日本在线电影 | 欧美激情一区二区三区成人 | 久久综合九色综合欧美狠狠 | 精品久久久久香蕉网 | 中文字幕+乱码+中文字幕一区 | 欧美老妇与禽交 | 在线观看国产午夜福利片 | 久久人妻内射无码一区三区 | ass日本丰满熟妇pics | 漂亮人妻洗澡被公强 日日躁 | 高清不卡一区二区三区 | 欧美zoozzooz性欧美 | 激情内射日本一区二区三区 | 国产亚洲精品久久久久久国模美 | 久久精品视频在线看15 | 午夜熟女插插xx免费视频 | 偷窥村妇洗澡毛毛多 | 久久久精品人妻久久影视 | 在线播放免费人成毛片乱码 | 人妻天天爽夜夜爽一区二区 | 国产精品99爱免费视频 | 18禁黄网站男男禁片免费观看 | 人妻尝试又大又粗久久 | 中文字幕人妻无码一区二区三区 | 日韩人妻无码中文字幕视频 | 日本精品高清一区二区 | 最新国产乱人伦偷精品免费网站 | 中文字幕+乱码+中文字幕一区 | 久久99精品国产麻豆蜜芽 | 51国偷自产一区二区三区 | 伊人久久大香线蕉av一区二区 | 精品久久久无码人妻字幂 | 亚洲自偷精品视频自拍 | 熟女少妇人妻中文字幕 | 日韩人妻少妇一区二区三区 | 动漫av网站免费观看 | 国产成人无码a区在线观看视频app | 国产成人无码午夜视频在线观看 | 无码精品国产va在线观看dvd | 婷婷六月久久综合丁香 | 精品一区二区三区无码免费视频 | 欧洲欧美人成视频在线 | 中文字幕人妻无码一夲道 | 理论片87福利理论电影 | 亚洲国产精品美女久久久久 | 欧美日韩综合一区二区三区 | 午夜精品一区二区三区的区别 | 2020最新国产自产精品 | 日日碰狠狠躁久久躁蜜桃 | 国产精品-区区久久久狼 | 色一情一乱一伦一区二区三欧美 | 在线天堂新版最新版在线8 | 亚洲啪av永久无码精品放毛片 | 一本精品99久久精品77 | 九九热爱视频精品 | 国产内射爽爽大片视频社区在线 | 亚洲午夜无码久久 | 一本久久a久久精品亚洲 | 国产精品内射视频免费 | 日日摸天天摸爽爽狠狠97 | 免费国产黄网站在线观看 | 国产成人无码av在线影院 | 激情爆乳一区二区三区 | 欧美兽交xxxx×视频 | 久久久精品人妻久久影视 | 老司机亚洲精品影院 | 乱码午夜-极国产极内射 | 亚洲一区二区三区播放 | 亚洲中文字幕av在天堂 | 99er热精品视频 | 中文毛片无遮挡高清免费 | 狠狠综合久久久久综合网 | 亚洲精品一区三区三区在线观看 | 亚洲自偷自拍另类第1页 | 无码人妻少妇伦在线电影 | 久久精品国产99久久6动漫 | 粉嫩少妇内射浓精videos | 亚洲热妇无码av在线播放 | 又大又硬又爽免费视频 | 国内老熟妇对白xxxxhd | 中文字幕无码av激情不卡 | 丰满妇女强制高潮18xxxx | 国产成人精品三级麻豆 | 国内老熟妇对白xxxxhd | 伊人久久大香线蕉av一区二区 | 国产精华av午夜在线观看 | 丰满人妻被黑人猛烈进入 | 欧美三级不卡在线观看 | 亚洲欧美综合区丁香五月小说 | 亚洲欧洲日本综合aⅴ在线 | 丝袜 中出 制服 人妻 美腿 | a国产一区二区免费入口 | 日本又色又爽又黄的a片18禁 | 亚洲日韩一区二区三区 | 久久综合久久自在自线精品自 | 色情久久久av熟女人妻网站 | 国产精品福利视频导航 | 东北女人啪啪对白 | 久久婷婷五月综合色国产香蕉 | 乱人伦中文视频在线观看 | 欧美日韩亚洲国产精品 | 熟女少妇在线视频播放 | 精品水蜜桃久久久久久久 | 亚洲一区av无码专区在线观看 | 亚洲一区二区三区四区 | 国产高清不卡无码视频 | 久久亚洲精品成人无码 | 国产精品久久久久久久9999 | 成人性做爰aaa片免费看 | 亚洲一区二区三区 | 亚洲一区二区三区在线观看网站 | 偷窥日本少妇撒尿chinese | 国产亚av手机在线观看 | 国产成人精品优优av | 少妇性l交大片欧洲热妇乱xxx | 99视频精品全部免费免费观看 | 亚洲七七久久桃花影院 | 中文字幕乱妇无码av在线 | 暴力强奷在线播放无码 | 欧美亚洲日韩国产人成在线播放 | 人妻体内射精一区二区三四 | 久久国产自偷自偷免费一区调 | 国产熟妇高潮叫床视频播放 | 老熟女重囗味hdxx69 | 中国女人内谢69xxxx | 亚洲国产欧美国产综合一区 | 精品厕所偷拍各类美女tp嘘嘘 | 日韩欧美成人免费观看 | av无码久久久久不卡免费网站 | 国产又粗又硬又大爽黄老大爷视 | 色狠狠av一区二区三区 | 精品无码一区二区三区爱欲 | 免费无码肉片在线观看 | 人妻天天爽夜夜爽一区二区 | 99久久久无码国产aaa精品 | 国产成人无码av一区二区 | 在线看片无码永久免费视频 | 色综合久久久无码中文字幕 | 狠狠亚洲超碰狼人久久 | 一本加勒比波多野结衣 | 欧美一区二区三区视频在线观看 | 亚洲精品国产品国语在线观看 | 最新版天堂资源中文官网 | 性生交大片免费看l | 国产精华av午夜在线观看 | 亚洲阿v天堂在线 | 伊人久久大香线蕉午夜 | 国产一区二区三区四区五区加勒比 | 老头边吃奶边弄进去呻吟 | 日日碰狠狠丁香久燥 | 最新国产乱人伦偷精品免费网站 | 日韩欧美中文字幕在线三区 | 亚洲伊人久久精品影院 | 粗大的内捧猛烈进出视频 | 免费人成在线观看网站 | 初尝人妻少妇中文字幕 | 人妻天天爽夜夜爽一区二区 | 麻豆md0077饥渴少妇 | 牲欲强的熟妇农村老妇女 | 无码国模国产在线观看 | 国产精品成人av在线观看 | 天下第一社区视频www日本 | 免费观看的无遮挡av | 对白脏话肉麻粗话av | 国产午夜精品一区二区三区嫩草 | 国产午夜福利亚洲第一 | 精品久久综合1区2区3区激情 | 1000部夫妻午夜免费 | 亚拍精品一区二区三区探花 | 国产精品久久久久久无码 | 2019午夜福利不卡片在线 | 精品无码av一区二区三区 | 少妇性l交大片欧洲热妇乱xxx | 久在线观看福利视频 |