虛擬內(nèi)存概念
linux內(nèi)核給每個(gè)進(jìn)程都提供了一個(gè)獨(dú)立的連續(xù)的虛擬地址空間再层。進(jìn)程訪問虛擬內(nèi)存地址時(shí)不需要考慮會不會跟其他進(jìn)程沖突,操作系統(tǒng)負(fù)責(zé)將每個(gè)進(jìn)程的虛擬內(nèi)存映射到物理內(nèi)存叁幢。
虛擬內(nèi)存布局
32位系統(tǒng)內(nèi)核空間占用1G钮追,位于最高處怒炸,用戶空間占用3G隅俘。64位系統(tǒng)內(nèi)核空間和用戶空間各占128T,中間部分未定義酌儒。進(jìn)程在用戶態(tài)時(shí)辜妓,只能訪問用戶空間的內(nèi)存,進(jìn)程在內(nèi)核態(tài)時(shí)忌怎,只能訪問內(nèi)核空間的內(nèi)存籍滴。
每個(gè)進(jìn)程的用戶空間是隔離的(用戶空間的虛擬地址映射到不同的物理地址),但是每個(gè)進(jìn)程的內(nèi)核空間都是一樣的(用戶空間的虛擬地址映射到相同的物理地址)榴啸,雖然每個(gè)進(jìn)程的內(nèi)核棧都是獨(dú)立的异逐,但是如果知道其他進(jìn)程內(nèi)核棧的地址,仍然是可以訪問的插掂。所以在內(nèi)核態(tài),如果要訪問公共的數(shù)據(jù)結(jié)構(gòu)腥例,需要加鎖保護(hù)辅甥。
內(nèi)存映射
操作系統(tǒng)把物理內(nèi)存分成一塊一塊大小相同的頁,這樣更方便管理燎竖。頁面的大小一般為4k璃弄。頁表中記錄了虛擬頁號和物理頁號的對應(yīng)關(guān)系。將一個(gè)虛擬地址分成兩部分:一部分存儲虛擬頁號构回,一部分存儲頁內(nèi)的偏移量夏块。這樣虛擬地址通過查詢頁表可以獲得物理頁號,再根據(jù)偏移量就可以找到映射的物理地址纤掸。
多級頁表和大頁
32位環(huán)境下脐供,虛擬地址空間總共4GB,如果4KB一個(gè)頁借跪,總共需要1M個(gè)頁政己,每個(gè)頁表項(xiàng)需要4個(gè)字節(jié)來存儲。那么總共需要4MB的內(nèi)存來存儲頁表掏愁。
這4MB頁表項(xiàng)必須提前創(chuàng)建好歇由,并且要求是連續(xù)的。為什么有這個(gè)要求果港?為了支持快速查找虛擬頁號到物理頁號的對應(yīng)關(guān)系沦泌,頁表采用了數(shù)組這樣的數(shù)據(jù)結(jié)構(gòu)來存儲,數(shù)組的下標(biāo)就是虛擬頁號辛掠,值為物理頁號谢谦。
如果有100個(gè)進(jìn)程,就需要400M的內(nèi)存,上邊提到過他宛,內(nèi)核空間只有1GB船侧,400MB對于內(nèi)核來說太大了。
如何解決這個(gè)問題呢厅各?第一個(gè)方法是采取多級頁表镜撩,我們將這4MB的頁表繼續(xù)劃分,分成1024個(gè)4KB队塘,這1024個(gè)頁也需要一個(gè)表進(jìn)行管理袁梗,稱為頁目錄表。這個(gè)頁目錄表里有1024項(xiàng)憔古,每項(xiàng)4個(gè)字節(jié)遮怜,頁目錄表的大小為4KB
32位的虛擬地址如下圖所示,根據(jù)前10位定位到頁目錄鸿市,再根據(jù)中間的10位定位頁表锯梁,獲取到物理頁號,根據(jù)頁內(nèi)偏移量來定位到物理地址
多級頁表是如何節(jié)省內(nèi)存的焰情?假設(shè)我們給進(jìn)程分配一個(gè)數(shù)據(jù)頁陌凳,如果只使用頁表,需要1M個(gè)頁表項(xiàng)共計(jì)4MB的空間來存儲頁表内舟。如果使用頁目錄合敦,頁目錄表需要4KB的空間來存儲,里邊只有一項(xiàng)使用了验游,頁表充岛,只需要分配那一個(gè)數(shù)據(jù)頁的頁表項(xiàng)就可以了,總共8KB的內(nèi)存
對于64位系統(tǒng)耕蝉,兩級頁表是不夠的崔梗。64位需要四級目錄,分別是:全局頁目錄項(xiàng)PGD赔硫、上層頁目錄項(xiàng)PUD炒俱、中間頁目錄項(xiàng)PMD、頁表項(xiàng)PTE
再來看大頁爪膊,大頁就是比普通頁更大的內(nèi)存塊权悟,常見的大小有2MB和1GB,采用大頁后推盛,頁表項(xiàng)會顯著減少峦阁,大大降低TLB miss的概率≡懦桑可以為單獨(dú)的應(yīng)用設(shè)置大頁榔昔,比如基于dpdk開發(fā)的應(yīng)用驹闰。
頁表的存儲位置
頁表和task_struct都存儲在內(nèi)核空間
MMU和TLB
MMU是cpu的內(nèi)存管理單元,cpu執(zhí)行單元發(fā)出的內(nèi)存地址(虛擬地址)將會被MMU截獲撒会,MMU將虛擬地址翻譯成物理地址嘹朗,然后將物理地址發(fā)到CPU芯片的外部地址引腳上。
MMU地址翻譯的過程:
1 操作系統(tǒng)在初始化或分配诵肛、釋放內(nèi)存時(shí)會執(zhí)行一些指令在物理內(nèi)存中填寫頁表屹培,然后用指令設(shè)置mmu,告訴mmu頁表在物理內(nèi)存中的具體位置
2 cpu訪問虛擬地址時(shí)怔檩,自動觸發(fā)mmu做查表和地址轉(zhuǎn)換的操作褪秀,地址轉(zhuǎn)換操作有硬件完成,不需要指令控制mmu
頁表一般都很大薛训,只能放在內(nèi)存中媒吗。每次訪問虛擬地址,都需要先訪問內(nèi)存乙埃。為了提高映射速度闸英,引入了TLB(快表)。TLB存儲的數(shù)據(jù)比較少介袜,但是速度比內(nèi)存快的多自阱。TLB就是頁表的緩存。通過減少TLB miss米酬,可以大幅提高cpu訪問內(nèi)存的性能。
用戶態(tài)虛擬空間布局
mm_struct結(jié)構(gòu)
操作系統(tǒng)通過進(jìn)程控制塊task_struct中的mm_struct來管理內(nèi)存趋箩,mm_struct中通過以下這些變量來記錄內(nèi)存區(qū)域的統(tǒng)計(jì)信息和具體位置
unsigned long mmap_base; /* mmap映射的起始地址 */
unsigned long total_vm; /* 總共映射的頁的數(shù)目 */
unsigned long locked_vm; /* 被鎖定不能換出的頁的數(shù)目 */
unsigned long pinned_vm; /* 不能換出赃额,也不能移動的頁的數(shù)目 */
unsigned long data_vm; /* 存放數(shù)據(jù)的頁的數(shù)目 */
unsigned long exec_vm; /* 存放可執(zhí)行文件的頁的數(shù)目 */
unsigned long stack_vm; /* 棧占用的頁的數(shù)目 */
unsigned long start_code, end_code, start_data, end_data; /* 代碼段起始、結(jié)束地址,已經(jīng)初始化的數(shù)據(jù)段起始叫确、結(jié)束地址 */
unsigned long start_brk, brk, start_stack; /*堆的起始地址跳芳、堆的結(jié)束地址、棧的起始地址*/
unsigned long arg_start, arg_end, env_start, env_end; /* 參數(shù)的起始竹勉、結(jié)束地址飞盆,環(huán)境變量的起始、結(jié)束地址次乓,都位于棧中最高地址的地方 */
除了這些位置信息和統(tǒng)計(jì)信息之外吓歇,mm_struct里面還專門有一個(gè)結(jié)構(gòu)vm_area_struct,來描述這些區(qū)域的屬性票腰。
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
mmap是雙向鏈表城看,mm_rb是紅黑樹
vm_area_struct 結(jié)構(gòu)如下:
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;
其中vm_start和vm_end指定了該區(qū)域在用戶空間中的起始地址和結(jié)束地址。vm_next和vm_prev將這個(gè)結(jié)構(gòu)串在鏈表上杏慰。vm_rb將這個(gè)區(qū)域加到紅黑樹上测柠,vm_ops是對這個(gè)區(qū)域可以做的操作的定義炼鞠。anon_vma不為空時(shí),表示這塊虛擬內(nèi)存映射的是物理內(nèi)存轰胁。vm_file不為空時(shí)谒主,表示虛擬內(nèi)存映射的是文件
vm_area_struct映射過程
load_elf_binary會初始化內(nèi)存映射
1 設(shè)置內(nèi)存映射區(qū)mmap_base
2 設(shè)置棧的vm_area_struct
3 設(shè)置arg_start指向棧底,
4 設(shè)置代碼段赃阀、數(shù)據(jù)段霎肯、bss段的vm_area_struct,將elf中的代碼凹耙、已初始化的全局遍歷和未初始化的全局變量映射到內(nèi)存中來
5 設(shè)置堆的vm_area_struct姿现,這時(shí)start_brk和brk相等,堆中是空的
6 設(shè)置mmap動態(tài)映射區(qū)的vm_area_struct肖抱,load_elf_interp將依賴的so映射到內(nèi)存
內(nèi)存的分配與回收
malloc()是c標(biāo)準(zhǔn)庫函數(shù)提供的內(nèi)存分配函數(shù)备典,對于小于128k的小塊內(nèi)存,用brk()來分配意述,對于大于128k的大塊內(nèi)存提佣,用mmap來分配。brk()方式通過移動堆頂?shù)奈恢脕矸峙鋬?nèi)存荤崇,內(nèi)存釋放的時(shí)候并不會立刻歸還給操作系統(tǒng)(只有當(dāng)brk指針下有連續(xù)的內(nèi)存后才縮小brk指針拌屏,釋放內(nèi)存),因此brk()方式的優(yōu)點(diǎn)是可以減少缺頁異常的發(fā)生术荤,缺點(diǎn)是會造成內(nèi)存碎片倚喂。mmap()方式分配內(nèi)存會在釋放時(shí)直接歸還給操作系統(tǒng),優(yōu)點(diǎn)是不會造成內(nèi)存碎片瓣戚,缺點(diǎn)是每次mmap都會發(fā)生缺頁異常端圈。
另外需要注意的一點(diǎn)是,這兩種調(diào)用發(fā)生后子库,并沒有真正的分配內(nèi)存(brk沒有可復(fù)用的內(nèi)存時(shí)舱权,會分配新的頁,mmap每次都分配新的頁)仑嗅,在首次訪問內(nèi)存時(shí)宴倍,通過缺頁異常,由內(nèi)核來分配內(nèi)存
brk處理過程
1 根據(jù)申請內(nèi)存的大小仓技,計(jì)算出新的堆頂?shù)奈恢?/p>
2 將新的堆頂和原來的堆頂按頁對齊鸵贬,比較大小,如果兩者相同脖捻,說明這次增加的內(nèi)存很小恭理,還在一個(gè)頁面里,不需要分配新頁郭变。直接設(shè)置mm->brk為新的堆頂即可
3 如果新舊堆頂不在一個(gè)頁面里颜价,就需要調(diào)用find_vma涯保,這個(gè)函數(shù)是對紅黑樹的查找,找原堆頂所在的vm_area_struct的下一個(gè)vm_area_struct周伦,查看當(dāng)前堆頂和下一個(gè)vm_area_struct之間是否還能分配所需的頁夕春,如果不能,退出返回內(nèi)存空間滿专挪。如果還有空間及志,調(diào)用do_brk分配所需的頁數(shù)。
mmap處理過程
每個(gè)進(jìn)程都有一個(gè)vm_area_struct列表寨腔,這個(gè)變量的名稱就是mmap
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
......
}
內(nèi)存映射不僅僅是物理內(nèi)存和虛擬內(nèi)存之間的映射(匿名映射)速侈,還包括將文件中的內(nèi)容映射到虛擬地址空間。僅有物理內(nèi)存和虛擬內(nèi)存的映射是一種特殊情況迫卢。
mmap處理過程如下:
1 根據(jù)fd的值(匿名映射為-1)和flag(MAP_SHARE|MAP_ANON)來確定是否為匿名映射
2 調(diào)用get_unmapped_area 函數(shù)找到一個(gè)還沒有映射的區(qū)域
3 創(chuàng)建一個(gè)新的vm_area_struct對象倚搬,將它加入紅黑樹,如果是映射文件乾蛤,還需要設(shè)置vm_file為目標(biāo)文件
頁面置換算法
在程序運(yùn)行過程中每界,如果要訪問的頁面不在內(nèi)存中,就發(fā)生缺頁中斷從而將該頁調(diào)入內(nèi)存中家卖。此時(shí)如果內(nèi)存已無空閑空間眨层,系統(tǒng)必須從內(nèi)存中調(diào)出一個(gè)頁面到磁盤對換區(qū)中來騰出空間
Linux并不是在缺頁中斷產(chǎn)生的時(shí)候才進(jìn)行頁面回收。Linux有一個(gè)守護(hù)進(jìn)程kswapd上荡,比較每個(gè)內(nèi)存區(qū)域的高低水位來檢測是否有足夠的空閑頁面來使用趴樱。每次運(yùn)行時(shí),采用lru算法來置換一定數(shù)量的頁面到swap分區(qū)
oom
oom是內(nèi)核的一種保護(hù)機(jī)制酪捡,它監(jiān)控進(jìn)程的內(nèi)存使用情況伊佃,并且使用oom_score為每個(gè)進(jìn)程的內(nèi)存使用情況進(jìn)行評分:進(jìn)程消耗的內(nèi)存越大,oom_score越大沛善。運(yùn)行占用的cpu越多,oom_score越小塞祈。oom_score越大金刁,越容易被殺死。
在實(shí)際工作中议薪,可以手動設(shè)置進(jìn)程的oom_adj尤蛮,oom_adj的取值范圍是[-17, 15],數(shù)值越大越容易被殺死斯议。-17表示禁止oom产捞,設(shè)置進(jìn)程oom_adj的示例如下:
echo -16 > /proc/$(pidof sshd)/oom_adj
高端內(nèi)存
內(nèi)核虛擬地址跟物理地址的對應(yīng)關(guān)系為:
物理地址 = 邏輯地址 – 0xC0000000
那么內(nèi)核邏輯地址空間訪問為0xc0000000 ~ 0xffffffff,那么對應(yīng)的物理內(nèi)存范圍就為0×0 ~ 0×40000000哼御,即只能訪問1G物理內(nèi)存坯临。若機(jī)器中安裝8G物理內(nèi)存焊唬,那么內(nèi)核就只能訪問前1G物理內(nèi)存,后面7G物理內(nèi)存將會無法訪問看靠,因?yàn)閮?nèi)核 的地址空間已經(jīng)全部映射到物理內(nèi)存地址范圍0×0 ~ 0×40000000赶促。即使安裝了8G物理內(nèi)存,那么物理地址為0×40000001的內(nèi)存挟炬,內(nèi)核該怎么去訪問呢鸥滨?
顯 然不能將內(nèi)核地址空間0xc0000000 ~ 0xfffffff全部用來簡單的地址映射。因此x86架構(gòu)中將內(nèi)核地址空間劃分三部分:ZONE_DMA谤祖、ZONE_NORMAL和 ZONE_HIGHMEM婿滓。ZONE_HIGHMEM即為高端內(nèi)存,這就是內(nèi)存高端內(nèi)存概念的由來粥喜。
在x86結(jié)構(gòu)中凸主,三種類型的區(qū)域如下:
ZONE_DMA 內(nèi)存開始的16MB
ZONE_NORMAL? 16MB~896MB
ZONE_HIGHMEM896MB ~ 結(jié)束
內(nèi)核是如何借助128MB高端內(nèi)存地址空間是如何實(shí)現(xiàn)訪問可以所有物理內(nèi)存?
當(dāng)內(nèi)核想訪問高于896MB物理地址內(nèi)存時(shí)容客,從0xF8000000 ~ 0xFFFFFFFF地址空間范圍內(nèi)找一段相應(yīng)大小空閑的邏輯地址空間秕铛,借用一會。借用這段邏輯地址空間缩挑,建立映射到想訪問的那段物理內(nèi)存(即填充內(nèi)核PTE頁面表)但两,臨時(shí)用一會,用完后歸還供置。這樣別人也可以借用這段地址空間訪問其他物理內(nèi)存谨湘,實(shí)現(xiàn)了使用有限的地址空間,訪問所有所有物理內(nèi)存芥丧。
目前現(xiàn)實(shí)中紧阔,64位Linux內(nèi)核不存在高端內(nèi)存,因?yàn)?4位內(nèi)核可以支持超過512GB內(nèi)存续担。若機(jī)器安裝的物理內(nèi)存超過內(nèi)核地址空間范圍擅耽,就會存在高端內(nèi)存
SMP和NUMA
smp(多對稱處理器):最經(jīng)典的使用內(nèi)存的方式是cpu通過總線去訪問內(nèi)存。多個(gè)cpu在總線的一側(cè)物遇,所有的內(nèi)存組成一大片內(nèi)存乖仇,在總線的另一側(cè),所有的cpu訪問內(nèi)存都要經(jīng)過總線询兴,而且距離都是一樣的乃沙,這種模式稱為smp,它的一個(gè)顯著缺點(diǎn)是總線會成為瓶頸
numa(非一致內(nèi)存訪問):這種模式下诗舰,內(nèi)存不是一整塊警儒,每個(gè)cpu都有自己的本地內(nèi)存,cpu訪問本地內(nèi)存不用過總線眶根,速度快很多蜀铲,每個(gè)cpu和內(nèi)存一起稱為一個(gè)numa節(jié)點(diǎn)边琉。缺點(diǎn)是:1 本地內(nèi)存不足時(shí),cpu去另外的numa節(jié)點(diǎn)申請內(nèi)存蝙茶,延時(shí)會比較長艺骂。2 物理內(nèi)存是不連續(xù)的,頁號也不連續(xù)隆夯,內(nèi)存模型變成了非連續(xù)內(nèi)存模型钳恕,管理起來復(fù)雜。