前篇文章(https://zhuanlan.zhihu.com/p/81850840/)講了進程地址空間的分配僵控,那本文會繼續(xù)講物理內(nèi)存如何管理燥爷,何時分配等問題。'
物理內(nèi)存meta管理
涉及到物理內(nèi)存的分配犁功、釋放,必然有一個數(shù)據(jù)結(jié)構(gòu)對其進行管理枷踏,同時也要考慮到性能驳规、效率揪胃、安全性等方面的問題斥赋。
前文講了缰猴,linux管理內(nèi)存大小的粒度是4K,我們叫做page疤剑。在單cpu的機器上滑绒,一般采用平臺內(nèi)存模型(flat memroy model)胰舆,就是將page平鋪開供cpu使用,但是在多cpu的情況下蹬挤,就會出現(xiàn)對page的相互競爭、搶占的情況棘幸,導(dǎo)致效率低焰扳,所以本文講NUMA(non-uniform memory access)架構(gòu)下,非一致性內(nèi)存訪問误续。
pglist_data:numa中一個cpu對應(yīng)指定的內(nèi)存節(jié)點吨悍,重要的字段如下:
mem_map數(shù)組:包含了該node上所有的struct page,包括已分配蹋嵌、未分配的育瓜,也用于物理頁號映射。
node_zones數(shù)組:存儲該節(jié)點所有的zone栽烂。
node_zonelist數(shù)組:存儲其他節(jié)點的zone躏仇,numa架構(gòu)下,當(dāng)前cpu的pglist_data -> mem_map的page用完后腺办,可以通過該數(shù)組申請其他cpu的內(nèi)存焰手。
node_zone:存儲該節(jié)點的區(qū)域,用區(qū)域來區(qū)分不同的物理內(nèi)存怀喉。
三種類型的zone:
ZONE_DMA:DMA方式可以操作的內(nèi)存區(qū)域书妻;
ZONE_NORMAL:普通的映射區(qū);
ZONE_HIGHMEM:高端內(nèi)存映射的區(qū)域躬拢。
free_area:空閑page的數(shù)組 + 鏈表的結(jié)構(gòu)躲履,伙伴系統(tǒng)依靠該數(shù)據(jù)結(jié)構(gòu)來分配物理內(nèi)存。每個數(shù)組的item都是一個鏈表聊闯,鏈表中的每個item就是待分配的單元工猜,其大小為2的n次方個頁,n為數(shù)組下標菱蔬。MAX_ORDER = 11域慷,一次可以分配2的11次方個頁,即M內(nèi)存汗销。
page:用于表示一個4k頁面的meta信息犹褒,如果內(nèi)存為4G,則有1M個page弛针,如果page占用空間太大叠骑,則用戶實際可使用的內(nèi)存就會變少,而page的使用方式有多種削茁,需要meta記錄下來宙枷,所以內(nèi)部大量使用union掉房,page目前實際大小為32byte,就是4G的內(nèi)存慰丛,需要使用32M來保存meta信息卓囚。以下是page使用方式:
整頁模式:分配內(nèi)存的單位就是頁,包括匿名映射和文件映射诅病,其重要的字段如下:
struct address_space *mapping:指向映射到該page內(nèi)容的源地址空間哪亿,可能是文件、可能是進程地址空間贤笆。
mapping == 0蝇棉,說明是swap的頁面,其指向swapper_space的地址芥永。
如果mapping != 0篡殷,第0位bit[0] = 0,說明該page屬文件映射埋涧,mapping指向文件的地址空間address_space板辽,此時pgoff_t index則是address_space指向的radix tree的頁號。(可參見文件系統(tǒng):https://zhuanlan.zhihu.com/p/61123802)
如果mapping != 0棘催,第0位bit[0] != 0戳气,說明該page為匿名映射,mapping指向vm_area_struct -> anon_vma對象巧鸭。
_mapcount:被頁表引用的次數(shù)瓶您。
lru:如果page還未被分配,則處于伙伴系統(tǒng)纲仍,lru將其連接在相同階的free_area上呀袱。如果已經(jīng)分配,則連接到zone中郑叠,供回收時使用夜赵。
slab模式:類似于對象池,將一頁分配多個slot乡革,每次分配的單位就是對象大小的內(nèi)存寇僧,基本會小于4k,所以一般1個頁可以包含多個對象沸版。
s_mem:指向正在使用的slab的第一個對象嘁傀。
freelist:池子中可分配的空閑對象。
rcu_head:需要釋放對象的列表视粮。
lru:指向slab的管理結(jié)構(gòu)细办。
更詳細struct page的介紹,可以參考《深入理解Linux內(nèi)核》296頁蕾殴。
伙伴系統(tǒng) (Buddy system)
依據(jù)pgdata_list -> zone -> free_area笑撞,分配物理內(nèi)存岛啸,返回struct page鏈表,其算法如下:
1)將分配空間大小歸一化茴肥,2^n-1 < X < 2^n坚踩,此時從free_area[n]開始查找,若有瓤狐,則直接返回瞬铸。
2)若free_area[n]無法找到,則在n ~ MAX_ORDER 之間遍歷芬首,若有,則將內(nèi)存頁一拆為二逼裆,一部分用于分配郁稍,分配有剩余且大于4k,則尋找free_area掛上去胜宇,另一部分掛在上一階的空閑鏈表上耀怜。
3)如果當(dāng)前zone -> free_area不ok,則遍歷node_zonelists中的其他zone桐愉。
如現(xiàn)在需要分配1個頁财破,free_area[0],free_area[1]都為空从诲,從free_area[2]上取出一個單元(order = 2時左痢,分配item = 4 page),在分配需要使用的1個頁后系洛,剩下的3頁中俊性,1個頁掛在free_area[0],2個頁作為一個單元掛在free_area[1]上描扯。
其函數(shù)調(diào)用鏈:alloc_pages(gfp_t gfp_mask, unsigned int order) -> alloc_pages_current -> __alloc_pages_nodemask定页。
gfp_t的枚舉值:GPF_USER、GPF_KERNAL绽诚、GPF_HIGHMEM典徊,分別對應(yīng)ZONE_NORMAL、ZONE_NORMAL恩够、ZONE_HIGHMEM的空間卒落。
頁表
前面講了進程地址空間,本文前面說了物理內(nèi)存的分配蜂桶,那進程地址空間如何和物理內(nèi)存對應(yīng)呢导绷?就是linux的頁表機制。
段地址 + 段偏移 = 邏輯地址屎飘,然后將邏輯地址拆解為頁號(20位) + 頁內(nèi)偏移(12位)妥曲。頁表維護虛擬頁號和物理頁號的映射贾费。
對于32位的系統(tǒng),支持最大物理尋址空間為4g檐盟。1 page為4k褂萧,4g的空間需要1m個page。由于是32位葵萎,每個page需要32位导犹,即4個字節(jié)來索引,所以4g空間需要4m的頁表羡忘。由于邏輯地址是按進程隔離的谎痢,所以進程之間的邏輯地址可能重合,但都映射了不同的物理地址卷雕,所以頁表也需要按進程隔離节猿。如果有機器上有1000個進程,則頁表就會占用1000 * 4m = 4g的頁表空間漫雕,32位系統(tǒng)的內(nèi)存就撐滿了滨嘱。
為節(jié)省內(nèi)存空間,對頁表進行分級: 4g空間需要4m的頁表浸间,那這4m的頁表需要1k個頁表(4k的空間)來描述太雨,具體如下圖:
有人會說新增了一級頁表,那不就從4m -> 4m + 4k了嗎魁蒜?比原來更大了囊扳。但是大部分情況,1個進程是不會用到4g的地址空間的兜看,對于沒有用到的地址空間宪拥,只要1級頁表缺失4byte,2級頁表就可以省下4k铣减。
頁表是有Linux按照x86規(guī)范構(gòu)造的她君,并將一級頁表的指針通過宏__pa()轉(zhuǎn)換為物理地址,并加載到cr3寄存器(這也是x86體系規(guī)范)葫哗,這個過程就是在context_switch -> switch_mm中發(fā)生的缔刹。
邏輯地址 -> 物理地址的具體轉(zhuǎn)換過程由硬件完成,比如執(zhí)行mov指令時(見前文開頭https://zhuanlan.zhihu.com/p/81850840/)劣针,其傳入的是邏輯地址校镐,硬件訪問時會轉(zhuǎn)換為物理地址訪問,并取出對應(yīng)的值捺典,這個模塊叫MMU(memory mangerment unit)鸟廓。
缺頁中斷
何時分配的物理內(nèi)存呢?是不是上層只要調(diào)用brk、mmap就分配呢引谜?顯然不是的牍陌,上文講brk、mmap時并沒有將分配物理內(nèi)存员咽。實際的物理內(nèi)存是在進程訪問時毒涧,發(fā)現(xiàn)頁表項為空會觸發(fā)缺頁異常,在缺頁異常處理程序中分配內(nèi)存贝室。缺頁異常的注冊中斷門代碼如下:
set_intr_gate(14,&page_fault);
其調(diào)用鏈路是:do_page_fault -> handle_mm_fault -> handle_pte_fault契讲,其中handle_mm_fault主要是創(chuàng)建或找到頁表項pte,handle_pte_fault是完成物理頁的分配滑频,并將物理頁號記錄到頁表項pte中捡偏。主要代碼如下:
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
unsigned long address = read_cr2();? // 缺頁中斷發(fā)生的線性地址通過cr2寄存器傳遞
......
__do_page_fault(regs, error_code, address);
......
}
/*
* This routine handles page faults.? It determines the address,
* and the problem, and then passes it off to one of the appropriate
* routines.
*/
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
{
? ? ? ? // 判斷是否在內(nèi)核態(tài),如果是則調(diào)用內(nèi)核的分配函數(shù)
if (unlikely(fault_in_kernel_space(address))) {
if (vmalloc_fault(address) >= 0)
return;
}
......? // 找到缺頁中斷發(fā)生的線性地址描述符
vma = find_vma(mm, address);
......
fault = handle_mm_fault(vma, address, flags);
......
/*
* 根據(jù)線性地址完成頁表的查詢或分配峡迷,此處代碼是支持64位os的银伟,所以是4層頁表,比
* 前面分析32位的頁表多2層
*/
static int __handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
unsigned int flags)
{
struct vm_fault vmf = {
.vma = vma,
.address = address & PAGE_MASK,
.flags = flags,
.pgoff = linear_page_index(vma, address),
.gfp_mask = __get_fault_gfp_mask(vma),
};
struct mm_struct *mm = vma->vm_mm;
pgd_t *pgd;
p4d_t *p4d;
int ret;
? ? ? ? // 尋找或分配頁表項
? ? ? ? // 全局頁表
pgd = pgd_offset(mm, address);
p4d = p4d_alloc(mm, pgd, address);
......? // 上層頁表
vmf.pud = pud_alloc(mm, p4d, address);
......? // 中間層頁表
vmf.pmd = pmd_alloc(mm, vmf.pud, address);
......
return handle_pte_fault(&vmf);
}
handle_pte_fault會完成真是物理頁的分配和pte頁表項的填充凉当。
1)如果vmf -> pte為null枣申,說明沒有分配售葡,如果是匿名映射看杭,直接調(diào)用do_anonymous_page分配。如果是文件映射挟伙,則調(diào)用do_fault進行分配楼雹,此處涉及到vfs(虛擬文件系統(tǒng)的操作,請參考https://zhuanlan.zhihu.com/p/61123802)
2)如果vmf -> pte存在且不在內(nèi)存中尖阔,說明是swap到硬盤了贮缅,通過do_swap_page swap in就ok了。
static int handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
......
vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
vmf->orig_pte = *vmf->pte;
......
if (!vmf->pte) {
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf);
else
return do_fault(vmf);
}
if (!pte_present(vmf->orig_pte))
return do_swap_page(vmf);
......
}
do_anonymous_page:
1)pte_alloc:分配頁表項介却。
2)alloc_zeroed_user_highpage_movable:分配一個頁谴供,此處最終調(diào)用到伙伴系統(tǒng)的__alloc_pages_nodemask,然后返回一個struct page齿坷。
3)mk_pte:struct page轉(zhuǎn)換為物理頁號桂肌,并保存在pte頁表項中,這是映射物理內(nèi)存的關(guān)鍵永淌,
static int do_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct mem_cgroup *memcg;
struct page *page;
int ret = 0;
pte_t entry;
......
if (pte_alloc(vma->vm_mm, vmf->pmd, vmf->address))
return VM_FAULT_OOM;
......
page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
......
entry = mk_pte(page, vma->vm_page_prot);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry));
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
&vmf->ptl);
......
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
......
}
#define mk_pte(page, pgprot)? pfn_pte(page_to_pfn(page), (pgprot))
#define page_to_pfn(page) ((unsigned long) (page - vmem_map))
static inline pte_t pfn_pte(unsigned long page_nr, pgprot_t pgprot)
{
phys_addr_t pfn = (phys_addr_t)page_nr << PAGE_SHIFT;
pfn ^= protnone_mask(pgprot_val(pgprot));
pfn &= PTE_PFN_MASK;
return __pte(pfn | check_pgprot(pgprot));
}
page_to_pfn:獲取物理頁號的函數(shù)崎场。page實例是前面伙伴系統(tǒng)分配的,同時該page實例存儲在pglist_data -> mem_map中遂蛀,page - vmem_map谭跨,結(jié)構(gòu)體直接想減,得到的是兩個地址之間可以有多少個減數(shù)大小的對象,此處就表示該page是mem_map中的index螃宙,這就是物理頁號蛮瞄。pte結(jié)構(gòu)就是一個long,將該物理頁號和一些控制信息按x86要求記錄就好污呼。
很多人這里可能會困惑裕坊,物理頁號是啥?和硬件相關(guān)嗎燕酷?其實硬件沒有物理頁號這個概念籍凝。只是這個struct page就占用了這個物理頁號,其他的page不能使用苗缩。當(dāng)訪問物理內(nèi)存時饵蒂,真實的物理地址 = 物理頁號 * 4k。而釋放內(nèi)存酱讶,只需要釋放這個pte和page即可退盯,下次再次分配該page時,將物理地址上的內(nèi)存空間覆蓋就好泻肯。