本文介紹下linux如何管理內(nèi)存。作為架構(gòu)師,我去做一個(gè)系統(tǒng)時(shí),通常從兩個(gè)方面入手:1)了解上層業(yè)務(wù)和需求勒庄。2)熟悉下層可以使用的工具和能力盐须。本質(zhì)上,做任何系統(tǒng),或者做任何事情吧,縱向上來(lái)說誉简,我們都是做上層和下層之間的樞紐,橫向上來(lái)說烹吵,我們做的是整個(gè)任務(wù)鏈上的一個(gè)節(jié)點(diǎn),也是前琼梆、后節(jié)點(diǎn)之間的傳動(dòng)齒輪艾栋。linux就是這樣蝗砾,是上層業(yè)務(wù)程序和下層體系結(jié)構(gòu)之間的樞紐,其封裝了底層體系結(jié)構(gòu)的復(fù)雜性,以更方便的操作界面供上層操作,用最近流行的一個(gè)詞語(yǔ),”認(rèn)知折疊“,那linux就是折疊了體系結(jié)構(gòu)的認(rèn)知,讓上層不必關(guān)注硬件體系結(jié)構(gòu)彭雾,而專注于自身的業(yè)務(wù)。
內(nèi)存管理的背景
上層業(yè)務(wù)需求
1、save and get:程序?qū)⑦\(yùn)行所需要的指令和數(shù)據(jù)進(jìn)行暫存(不需要持久化),并在需要時(shí)能夠準(zhǔn)確索引之前暫存的任何值,并load入cpu寄存器進(jìn)行運(yùn)算。
2、安全性:要做到進(jìn)程之間的隔離,用戶態(tài)、內(nèi)核態(tài)的隔離谓苟,保證系統(tǒng)級(jí)別的安全,即使一個(gè)進(jìn)程出現(xiàn)內(nèi)存溢出、尋址越界等情況,不能影響系統(tǒng)和其他進(jìn)程的運(yùn)行曲秉。
3、高效:meta信息要少,內(nèi)存操作要快。
底層提供的能力
最底層的能力就是內(nèi)存條,能夠根據(jù)索引存儲(chǔ)數(shù)據(jù)和獲取數(shù)據(jù)塑荒,但是x86體系結(jié)構(gòu)封裝了cpu和內(nèi)存的交互過程拧篮,通過下列x86匯編完成cpu和內(nèi)存交互礁凡。
可以看到,x86提供了基于(段地址 + 段偏移)進(jìn)行存取的能力,x86內(nèi)部會(huì)將(段地址+段偏移)轉(zhuǎn)換為物理地址后進(jìn)行存儲(chǔ)状共,但地址轉(zhuǎn)換時(shí)需要依賴于頁(yè)表機(jī)制,頁(yè)表的內(nèi)容由linux寫入,整個(gè)過程后面會(huì)詳細(xì)聊到(https://zhuanlan.zhihu.com/p/82406447),此處只是了解下底層能力每庆。
可以看到伦籍,上層需要如此豐滿,底層能力如此露骨,我們通常面對(duì)的就是這種情況君珠,但這種情況下恰恰可以大有作為乐导。
1)管理的粒度:linux將內(nèi)存分頁(yè),每頁(yè)默認(rèn)大小為4k沉桌。這里有個(gè)trade off的問題,如果粒度小翠订,比如1 byte,則可以提升空間利用率巩踏,但是管理的meta信息就會(huì)很多,如果分配較大空間時(shí)牵咙,耗時(shí)就會(huì)更長(zhǎng);如果粒度大溪食,則meta信息少,分配大內(nèi)存時(shí)耗時(shí)少,但分配小塊內(nèi)存時(shí)會(huì)有浪費(fèi)。所以一般在線系統(tǒng)或者mysql庫(kù),使用默認(rèn)頁(yè)大小(此處不是只mysql的頁(yè)咖城,而是mysql使用的linux的頁(yè)),olap系統(tǒng)或者離線存儲(chǔ)系統(tǒng)一般使用大頁(yè)他匪,linux提供配置參數(shù)可以配置大頁(yè)絮供。
2)進(jìn)程地址空間:每個(gè)進(jìn)程獨(dú)有的,用字段task_struct -> mm_struct表示亲铡,其也叫線性地址,其轉(zhuǎn)換關(guān)系:線性地址 -> 邏輯地址 -> 物理地址叙谨。
解耦:因?yàn)閯偛盘岬降亩际莤86支持的方式蝠猬,還是arm等體系結(jié)果需要支持兑凿,所以linux通過進(jìn)程地址空間這個(gè)entity進(jìn)行解耦捧请,所以內(nèi)存的操作都在用戶態(tài)完成力麸,真正分配內(nèi)存時(shí)蛮粮,通過缺頁(yè)異常搞定然想。
隔離:由于進(jìn)程地址空間按進(jìn)程單位進(jìn)行隔離蛙卤,保證進(jìn)程訪問時(shí)相互隔離栅屏。同時(shí)空間內(nèi)部也分為kernel space和user space松靡,保證用戶態(tài)和內(nèi)核態(tài)的訪問的內(nèi)存相互隔離。同時(shí)采用分段機(jī)制建椰,將指令雕欺、不同類型的數(shù)據(jù)分開存儲(chǔ),以支持進(jìn)程運(yùn)行模型棉姐。
text segment:存儲(chǔ)代碼指令的區(qū)域
data segment:存儲(chǔ)已初始化的全局或靜態(tài)變量
bss segment:存儲(chǔ)未初始化的全局或靜態(tài)變量
heap:堆屠列,用于動(dòng)態(tài)開辟內(nèi)存空間,brk或malloc開辟的空間
memory mapping space:mmap系統(tǒng)調(diào)用使用的空間伞矩,通常用于文件映射到內(nèi)存或匿名映射(開辟大塊空間)笛洛,當(dāng)malloc大于128k時(shí)(此處依賴于glibc的配置),也使用該區(qū)域乃坤。在進(jìn)程創(chuàng)建時(shí)苛让,會(huì)將程序用到的平臺(tái)、動(dòng)態(tài)鏈接庫(kù)加載到該區(qū)域
stack:進(jìn)程運(yùn)行的棧
空間利用率: 由于32位只支持4G物理內(nèi)存尋址湿诊,一種方式N個(gè)進(jìn)程平分4G內(nèi)存狱杰,這是最簡(jiǎn)單的方式,但有個(gè)問題厅须,有些進(jìn)程占用內(nèi)存仿畸,但一直在sleep,相當(dāng)于很浪費(fèi)朗和。另一種方式就是把內(nèi)存空間給最需要的進(jìn)程颁湖,把sleep進(jìn)程的內(nèi)存swap到硬盤,需要的時(shí)候再swap進(jìn)內(nèi)存例隆。此時(shí)就有一種極端情況甥捺,就是一個(gè)進(jìn)程需要獨(dú)占4G內(nèi)存,linux顯然選擇第二種利用率更高的方式镀层,所以進(jìn)程地址空間能映射4G的物理內(nèi)存镰禾。
3)邏輯地址:邏輯地址 = 段地址 + 段偏移,段地址和段偏移的值由linux進(jìn)行提供唱逢。
4)頁(yè)表:保存邏輯地址到物理地址的映射吴侦,其數(shù)據(jù)由linux初始化,并將熱頁(yè)表項(xiàng)加載TLB快表進(jìn)行緩存坞古,加快轉(zhuǎn)換速度备韧。然后x86體系的硬件依賴頁(yè)表做邏輯地址 -> 物理地址的轉(zhuǎn)換。
5)伙伴系統(tǒng):管理物理內(nèi)存的分配痪枫,其在缺頁(yè)中斷中被調(diào)用织堂,僅負(fù)責(zé)更改頁(yè)表和meta(strcut page)叠艳。當(dāng)邏輯地址和物理地址映射上后,指令使用物理地址寫入內(nèi)存設(shè)備易阳。
進(jìn)程地址空間管理
struct mm_struct {
? ? struct vm_area_struct * mmap;? //指向虛擬區(qū)間(VMA)的鏈表
? ? struct rb_root mm_rb;? ? ? ? ? //指向線性區(qū)對(duì)象紅黑樹的根
? ? pgd_t * pgd;? ? ? ? ? ? ? ? ? //指向頁(yè)全局目錄
? ? unsigned long mmap_base; ? //表示mmap區(qū)域的起始位置
? ? unsigned long total_vm; ? //總共映射的頁(yè)面數(shù)附较,包括映射到內(nèi)存中和已經(jīng)換出到銀盤的
? ? unsigned long locked_vm; ? //表示不能swap到硬盤的頁(yè)數(shù)
? ? unsigned long pinned_vm; ? //表示不能換出,不能移動(dòng)的頁(yè)數(shù)
? ? unsigned long data_vm; ? //存儲(chǔ)數(shù)劇占的總頁(yè)數(shù)
? ? unsigned long exec_vm; ? //存儲(chǔ)指令占的總頁(yè)數(shù)
? ? unsigned long stack_vm; ? //棧所占的總頁(yè)數(shù)
? ? // text潦俺、data segment的開始和結(jié)束地址
? ? unsigned long start_code, end_code, start_data, end_data;
? ? // 堆的開始拒课、當(dāng)前位置。棧的起始位置事示,棧的當(dāng)前地址在esp寄存器中
? ? unsigned long start_brk, brk, start_stack;
? ? // 命令行參數(shù)列表早像、環(huán)境變量的起始、結(jié)束地址肖爵,其都位于棧的高地址
? ? unsigned long arg_start, arg_end, env_start, env_end;
}
task_struct -> mm_struct是對(duì)進(jìn)程地址空間描述的結(jié)構(gòu)體卢鹦,主要包含其統(tǒng)計(jì)信息和各個(gè)segment的起始、結(jié)束地址遏匆,幾個(gè)變量如下圖右側(cè)的標(biāo)注法挨。
進(jìn)程地址空間的地址是向上增長(zhǎng)的》福可以看到有幾個(gè)radom offset凡纳,其在段與段之間縫隙,防止固定的內(nèi)存布局被黑客黑掉帝蒿。
vm_area_struct是描述每個(gè)段具體信息結(jié)構(gòu)體荐糜,其實(shí)一個(gè)單鏈表,通過task_struct- > mm_struct -> mmap表示葛超,由于每次分配內(nèi)存時(shí)會(huì)要到vm_area_struct暴氏,需要快速尋找,所以task_struct- > mm_struct -> mm_rb是根據(jù)vm_area_struct -> vm_start字段绣张,構(gòu)建vm_area_struct的一棵紅黑樹答渔。
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
struct mm_struct *vm_mm; /* The address space we belong to. */
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
? * page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
} __randomize_layout;
每個(gè)字段的具體含義可以參見注釋。其中anon_vma是匿名映射侥涵,即分配大塊內(nèi)存沼撕,vm_file即是指向文件映射映射的文件。vm_ops即是這段內(nèi)存上對(duì)應(yīng)的操作芜飘。
mm_struct和vm_area_struct的初始化是在load_elf_binary的時(shí)候初始化务豺,主要做的幾個(gè)事情如下:
1)調(diào)用 setup_new_exec,設(shè)置內(nèi)存映射區(qū) mmap_base嗦明。
2)調(diào)用setup_arg_pages笼沥,設(shè)置棧的vm_area_struct和current -> mm -> start_stack。
3)調(diào)用elf_map,將elf文件中的代碼映射到對(duì)應(yīng)區(qū)域中奔浅。
4)set_brk馆纳,設(shè)置堆的vm_area_struct,current -> mm -> start_brk = current -> mm -> brk乘凸,此時(shí)棧是空的厕诡。
5)load_elf_interp將依賴的so加載到mmap區(qū)域累榜。
load_elf_binary完成后营勤,線性空間的布局就基本如上圖,當(dāng)進(jìn)程運(yùn)行時(shí)會(huì)修改椧挤#空間的大小葛作,通過malloc申請(qǐng)空間時(shí)會(huì)改變heap、mmmap空間的大小猖凛。
brk & mmap
分配內(nèi)存常使用malloc赂蠢,這是glibc提供的方法,其內(nèi)部也有block的管理辨泳。但是底層都依賴于linux提供的系統(tǒng)調(diào)用:brk和mmap虱岂。若malloc小于128k,則使用brk菠红,大于則使用mmap第岖,可通過M_MMAP_THRESHOLD修改128k這個(gè)邊界值。如下圖试溯,A = malloc(30K)蔑滓,B = malloc(40K),C = malloc(200K)遇绞,D=malloc(100K)键袱。
brk
其通過系統(tǒng)調(diào)用設(shè)置current -> mm -> brk指針,在heap空間來(lái)分配和回收內(nèi)存空間摹闽。
SYSCALL_DEFINE1(brk, unsigned long, brk)
{
unsigned long retval;
unsigned long newbrk, oldbrk;
struct mm_struct *mm = current->mm;
struct vm_area_struct *next;
? ? ? ? // 1蹄咖、如果新的brk和當(dāng)前mm->brk按頁(yè)對(duì)齊后相等,則說明不需要跨頁(yè)分配付鹿,則直接設(shè)置當(dāng)前brk即可
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
if (oldbrk == newbrk)
goto set_brk;
// 2澜汤、如果新的brk小于mm->brk,說明需要進(jìn)行線性空間的回收
if (brk <= mm->brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk, &uf))
goto set_brk;
goto out;
}
// 3倘屹、找到next vma银亲,vm_start_gap可以理解返回的是next.vm_start,然后比較下一個(gè)vma和當(dāng)前vma是否
? ? ? ? // 能容納新申請(qǐng)的空間纽匙。這里有兩個(gè)注意點(diǎn):
? ? ? ? // 1)oldbrk是按頁(yè)對(duì)齊后的brk地址务蝠,此時(shí)通過fina_vma找到的是第一個(gè)滿足vma.vm_end > oldbrk的?
? ? ? ? // vma,所以是下一個(gè)vma烛缔,用next指針表示
? ? ? ? // 2)brk的入?yún)⑹且粋€(gè)addr馏段,所以只能查看當(dāng)前vma和下一個(gè)vma之間是否足夠分配轩拨,不能線性
? ? ? ? // 查找,這一點(diǎn)不同于mmap方式院喜。
next = find_vma(mm, oldbrk);
if (next && newbrk + PAGE_SIZE > vm_start_gap(next))
goto out;
? ? ? ? // 4亡蓉、對(duì)于brk,此處僅設(shè)置vm_area_struct的vm_end和其他相關(guān)指針喷舀,不會(huì)新分配vma
if (do_brk(oldbrk, newbrk-oldbrk, &uf) < 0)
goto out;
// 5砍濒、設(shè)置brk
set_brk:
mm->brk = brk;
return brk;
out:
retval = mm->brk;
return retval
mmap
通過遍歷mmap區(qū)域,找到大小合適的區(qū)域進(jìn)行文件映射或匿名映射硫麻,其入口是SYSCALL_DEFINES6爸邢,主要邏輯在do_mmap中。
unsigned long do_mmap(struct file *file, unsigned long addr,
? ? ? ? ? ? ? ? ? ? ? ? unsigned long len, unsigned long prot,
? ? ? ? ? ? ? ? ? ? ? ? unsigned long flags, vm_flags_t vm_flags,
? ? ? ? ? ? ? ? ? ? ? ? unsigned long pgoff, unsigned long *populate,
? ? ? ? ? ? ? ? ? ? ? ? struct list_head *uf) {
? ? ? ? struct mm_struct *mm = current->mm;
? ? ? ? int pkey = 0;
? ? ? ? *populate = 0;
? ? ? ? if (!len)
? ? ? ? ? ? ? ? return -EINVAL;
.......
/* pang */
? ? ? ? len = PAGE_ALIGN(len);
? ? ? ? if (!len)
? ? ? ? ? ? ? ? return -ENOMEM;
// 判斷該進(jìn)程的地址空間的虛擬區(qū)間數(shù)量是否超過了限制
? ? ? ? if (mm->map_count > sysctl_max_map_count)
? ? ? ? ? ? ? ? return -ENOMEM;
? ? // 從mmap區(qū)域獲取未被映射且length合適的vma addr
? ? ? ? addr = get_unmapped_area(file, addr, len, pgoff, flags);
.......
? ? ? ? /* file指針不為nullptr, 即從文件到虛擬空間的映射 */
? ? if (file) {
.......
? ? ? ? } else {
? ? ? ? ? ? ? ? switch (flags & MAP_TYPE) {
? ? ? ? ? ? ? ? case MAP_SHARED:
.......? ? ? ? ? ? ?
? ? ? ? ? ? ? ? case MAP_PRIVATE:
.......
? ? ? ? ? ? ? ? default:
? ? ? ? ? ? ? ? ? ? ? ? return -EINVAL;
? ? ? ? ? ? ? ? }
? ? ? ? }
? ? ? ? // 映射到vm_area_struct
? ? ? ? addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
? ? ? ? if (!IS_ERR_VALUE(addr) &&
? ? ? ? ? ? ((vm_flags & VM_LOCKED) ||
? ? ? ? ? ? (flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
? ? ? ? ? ? ? ? *populate = len;
? ? ? ? return addr;
get_unmapped_area:從mmap區(qū)域中找到未被映射拿愧,且length滿足要求的vma的起始addr杠河。
若addr != 0,則從指定addr查找浇辜。先調(diào)用fina_vma券敌,找到vma,使其滿足vma.vm_end > addr同時(shí)還需要滿足addr + len < vma.vm.start柳洋,則返回該addr待诅。
若addr == 0或步驟1找不到符合要求的addr,則從mm->free_area_cache從新開始全局搜索膳灶,如果還搜索不到咱士,就返回錯(cuò)誤。mm->free_area_cache在初始化時(shí)被設(shè)置為用戶空間的三分之一(1G的位置轧钓,1G以下是為text序厉、data、bss保留)毕箍。
mmap_region:根據(jù)addr弛房,映射vma,可能是和原有的vma合并而柑,也可能是重新創(chuàng)建vma文捶,邏輯在vma_merge中,具體可以參見http://edsionte.com/techblog/archives/3586媒咳。
線性地址與邏輯地址的映射
邏輯地址 = 段地址 + 段偏移粹排,從上圖可以看出線性地址 = 段偏移,在linux中涩澡,段地址都被初始化為0(也可參見https://zhuanlan.zhihu.com/p/73937048)顽耳。我理解原因有兩個(gè):
1)不是所有體系結(jié)構(gòu)都有段地址的概念,比如arm,linux為了支持多個(gè)體系架構(gòu)射富,所以做了兼容設(shè)計(jì)膝迎。
2)避免了一次地址轉(zhuǎn)換,使得線性地址直接對(duì)應(yīng)段偏移地址胰耗,減少了計(jì)算復(fù)雜度限次。
那為何不直接干掉段地址,linux干嘛還初始化為0柴灯?因?yàn)閤86體系要求啊卖漫,x86會(huì)通過mmu將段地址 + 段偏移 -> 物理地址,如果linux不設(shè)置段地址弛槐,x86就不work了懊亡,就沒法轉(zhuǎn)換為物理地址依啰,這個(gè)后面一篇文章會(huì)詳細(xì)講乎串。
內(nèi)核地址空間
kernel代碼運(yùn)行需要使用的內(nèi)存地址,比如創(chuàng)建task_struct描述符速警,內(nèi)核代碼運(yùn)行棧等等叹誉。
直接映射區(qū):也叫高端映射區(qū),其和物理內(nèi)存一一對(duì)應(yīng)闷旧,這并不是說內(nèi)核可以直接使用物理地址长豁,而是這段區(qū)域和物理地址的映射關(guān)系是一一對(duì)應(yīng)的。虛擬地址空間的3G + 896M直接映射物理地址的低896M忙灼。
內(nèi)核動(dòng)態(tài)映射區(qū):用戶態(tài)malloc會(huì)在堆分配匠襟,那內(nèi)核使用vmalloc函數(shù)分配的空間就在該區(qū)域。該區(qū)域和用戶區(qū)域的映射規(guī)則一樣该园。比如物理內(nèi)存896M~2G被用戶態(tài)使用酸舍,那內(nèi)核態(tài)就需要映射2G以上的物理內(nèi)存,但是內(nèi)核態(tài)的線性地址只有1G里初,此時(shí)就不能用直接映射了啃勉,該區(qū)域的映射可以映射到任何物理內(nèi)存。
永久映射區(qū):分配alloc_pages双妨,用管理物理內(nèi)存淮阐。
很多人到這里有疑問吧,為什么要有高端映射刁品?完全統(tǒng)一為動(dòng)態(tài)映射泣特,用戶態(tài)和內(nèi)核態(tài)保持一致不就ok??jī)?nèi)核需要保證足夠的物理內(nèi)存來(lái)運(yùn)行挑随,如果這段區(qū)域不連續(xù)的話状您,不方便統(tǒng)計(jì),每次用戶態(tài)分配內(nèi)存時(shí)是不是都要檢查?那干脆加一個(gè)直接映射區(qū)竞阐,同時(shí)讓用戶態(tài)無(wú)法映射到該物理區(qū)域就ok了缴饭。