本篇開始總結(jié)內(nèi)存問題的分析,在分析之前先簡單梳理下內(nèi)存的基礎(chǔ)知識(shí)樟结。
一汽抚、虛擬內(nèi)存
在早期的計(jì)算機(jī)中,程序是直接運(yùn)行在物理內(nèi)存上的枚抵。這樣帶來不少問題:
地址空間不隔離存在安全性問題线欲、超過物理內(nèi)存大小的內(nèi)存需求無法得到更好滿足,分配空閑內(nèi)存的位置無法確定帶來了重定位問題等汽摹。
為解決以上問題李丰,引入了虛擬內(nèi)存
概念:
它是程序和物理內(nèi)存中引入的一個(gè)中間層,屬于內(nèi)存管理策略的范疇逼泣。
虛擬內(nèi)存:
程序都有自己獨(dú)立的進(jìn)程地址空間趴泌,且程序認(rèn)為它擁有連續(xù)的可用的內(nèi)存(一個(gè)連續(xù)完整的虛擬地址空間,但不保證物理內(nèi)存連續(xù)拉庶,物理內(nèi)存不夠的情況下嗜憔,部分?jǐn)?shù)據(jù)還會(huì)暫時(shí)存儲(chǔ)在外部磁盤存儲(chǔ)器上,在需要時(shí)進(jìn)行數(shù)據(jù)交換)氏仗,虛擬內(nèi)存與物理內(nèi)存直接建立映射關(guān)系來一一對(duì)應(yīng)吉捶。
而Linux的內(nèi)存管理就是建立在虛擬內(nèi)存概念之上。
1.1 虛擬內(nèi)存劃分
從Linux操作系統(tǒng)層次上,可將Linux虛擬內(nèi)存劃分為用戶空間和內(nèi)核空間呐舔。以32位操作系統(tǒng)為例币励,最大尋址范圍是4G,也就是整個(gè)虛擬地址空間是4G珊拼,Linux簡化了分段機(jī)制食呻,使得虛擬地址與線性地址總是一致的。Linux一般把這個(gè)4G的地址空間劃分為兩個(gè)部分:其中 0~3G為用戶程序地址空間澎现,虛地址0x00000000到0xBFFFFFFF,供各個(gè)進(jìn)程使用搁进;3G~4G為內(nèi)核的地址空間,虛擬地址 0xC0000000到0xFFFFFFFF, 供內(nèi)核使用昔头。
這里有兩點(diǎn):
用戶進(jìn)程通常情況下只能訪問用戶空間( 0~3G)的虛擬地址饼问,不能訪問內(nèi)核空間的虛擬地址。例外情況只有用戶進(jìn)程進(jìn)行系統(tǒng)調(diào)用(代表用戶進(jìn)程在內(nèi)核態(tài)執(zhí)行)等時(shí)刻可以訪問到內(nèi)核空間揭斧。
每個(gè)進(jìn)程的用戶空間(0-3G)完全獨(dú)立莱革、互不相干,內(nèi)核空間(3G-4G)則由則由所有進(jìn)程以及內(nèi)核共享讹开。
1.2 地址介紹
物理地址
:內(nèi)存條的單元地址盅视。
邏輯地址
:機(jī)器語言指令中用來指定一個(gè)操作數(shù)或者是一條指令的地址。
線性地址(虛擬地址)
:內(nèi)存管理創(chuàng)造的一種地址旦万。
流程:
1.3 地址間映射方案
從上面流程可知闹击,地址轉(zhuǎn)換之間存在兩種映射方案:分段
與分頁
。
分段
:使用了大小可變的塊來管理內(nèi)存成艘。適合處理復(fù)雜系統(tǒng)的邏輯分區(qū)赏半,映射的段表存儲(chǔ)在線性地址空間。
分頁
:使用了大小不變的塊來管理內(nèi)存淆两。適合管理物理內(nèi)存断箫,映射的頁表保存在物理地址空間。
這里重點(diǎn)再看看虛擬地址查詢物理地址過程:
虛擬地址與物理地址通過頁表建立映射關(guān)系秋冰,CPU通過MMU(Memory Management Unit :內(nèi)存管理單元)訪問頁表來查詢虛擬地址對(duì)應(yīng)的物理地址仲义。
頁表結(jié)構(gòu):
依次按順序判斷:是否命中(命中:想要的數(shù)據(jù)在內(nèi)存中)、是否滿足RWX權(quán)限剑勾、是否滿足User/Kernel權(quán)限埃撵,只要一項(xiàng)不滿足,MMU會(huì)給CPU發(fā)出page fault虽另,CPU自動(dòng)跳到fault的代碼去處理fault暂刘。全滿足,那么MMU就去訪問內(nèi)存條上對(duì)應(yīng)的地址洲赵。
二鸳惯、內(nèi)存組織與劃分
2.1 頁(page)
內(nèi)核把頁作為內(nèi)存管理的基本單位。MMU也是以頁為單位來管理頁表叠萍。大多數(shù)32位體系結(jié)構(gòu)支持4KB的頁芝发,而64位支持8KB的頁(可通過命令來查看系統(tǒng)page大小:getconf -a | grep -i 'page')苛谷。內(nèi)核中用struct page來表示系統(tǒng)中的每個(gè)物理頁辅鲸。
2.2 區(qū)(zone)
由于硬件限制,內(nèi)核對(duì)特性不同的頁是區(qū)別對(duì)待的腹殿,內(nèi)核將內(nèi)存按地址的順序分成了不同的區(qū)独悴,有的硬件只能訪問有專門的區(qū)。區(qū)的劃分本身沒有任何物理意義锣尉,只不過是內(nèi)核為了管理頁而采取的一種邏輯上的分組刻炒。
主要關(guān)注的區(qū)有3個(gè):
區(qū) | 描述 | 物理內(nèi)存 |
---|---|---|
ZONE_DMA | 直接內(nèi)存訪問,無需映射 | <16MB |
ZONE_NORMAL | 一一對(duì)應(yīng)映射頁 | 16~896MB |
ZONE_HIGHMEM | 動(dòng)態(tài)映射頁 | >896MB |
Linux將4G的線性地址空間分為2部分自沧,0-3G為user space坟奥,3G-4G為kernel space。以上三個(gè)區(qū)都是針對(duì)這1G的kernel space而言的拇厢。
總結(jié):
對(duì)0-3G的用戶空間來說爱谁,其實(shí)不太關(guān)注物理地址是否連續(xù),連續(xù)不連續(xù)都是在虛擬地址層面上談的孝偎,區(qū)別也就是查詢和插入的效率差別访敌。
對(duì)3G-4G的內(nèi)核空間來說,詳細(xì)劃分了三個(gè)區(qū)來滿足各種物理內(nèi)存需求衣盾。
DMA zone
:直接訪問物理內(nèi)存寺旺,不需要映射,可以滿足某些硬件設(shè)備的內(nèi)存需求势决。
Normal zone
:虛擬地址與物理地址是一一映射關(guān)系迅涮,如果需要連續(xù)物理內(nèi)存這部分能滿足。
High zone
:虛擬地址與物理地址是動(dòng)態(tài)映射關(guān)系徽龟,它的意義是為了能夠訪問所有的物理地址空間(1G空間顯然無法滿足叮姑,所以需要出一塊動(dòng)態(tài)映射區(qū)域),因此這部分內(nèi)存不一定能滿足連續(xù)物理內(nèi)存需求据悔,但是它提升了物理地址空間訪問范圍传透。
注:供硬件設(shè)備使用的物理內(nèi)存地址必須是連續(xù)的,而供軟件使用的物理內(nèi)存地址則不要求必須是連續(xù)的极颓。
三朱盐、內(nèi)存分配
內(nèi)存按page組織按zone劃分之后,接下來看看如何分配內(nèi)存菠隆。
3.1內(nèi)存分配算法
1)Buddy算法
把空閑的頁以2的n次方為單位進(jìn)行管理兵琳,Buddy算法最主要的的特點(diǎn)任何時(shí)候區(qū)域里的空閑內(nèi)存都能以2的n次方進(jìn)行拆分或合并狂秘。整個(gè)kernel space都采用buddy算法進(jìn)行管理,因此Linux最底層的內(nèi)存申請(qǐng)都是以2n 為單位的(page)躯肌。
例如者春,假設(shè)ZONE_NORMAL有16頁內(nèi)存(24),此時(shí)有人申請(qǐng)一頁內(nèi)存清女,Buddy算法會(huì)把剩下的15頁拆分成8+4+2+1钱烟,放到不同的鏈表中去。此時(shí)再申請(qǐng)4頁嫡丙,直接給4頁拴袭,若再申請(qǐng)4頁,則從8頁中給4頁曙博,正好剩下4頁拥刻。Buddy算法的精髓在于任何正整數(shù)都可以拆分成2的n次方之和。
通過/proc/buddyinfo可以看到內(nèi)存空閑的一些情況:Buddy算法的優(yōu)點(diǎn)是避免了內(nèi)存的外部碎片父泳,但是長期運(yùn)行后泰佳,大片的內(nèi)存會(huì)比較少,而1頁尘吗,2頁逝她,4頁這種內(nèi)存會(huì)非常多,當(dāng)我們分配大片連續(xù)內(nèi)存的時(shí)候就會(huì)出問題睬捶。換句話說就是以產(chǎn)生內(nèi)部碎片為代價(jià)來避免外部碎片的產(chǎn)生黔宛。 Linux針對(duì)大內(nèi)存的物理地址分配,采用Buddy伙伴算法擒贸,如果是針對(duì)小于一個(gè)page的內(nèi)存臀晃,頻繁的分配和釋放,則不宜用Buddy伙伴算法介劫。
注:所謂“內(nèi)部碎片”徽惋,是指系統(tǒng)已經(jīng)分配給用戶使用、用戶自己沒有用到的那部分存儲(chǔ)空間座韵;所謂“外部碎片”险绘,是指系統(tǒng)無法把它分配出去供用戶使用的那部分存儲(chǔ)空間。
2)slab算法
頻繁的分配/釋放內(nèi)存必然導(dǎo)致系統(tǒng)性能的下降誉碴,所以有必要為頻繁分配/釋放的對(duì)象建立高速緩存宦棺。linux中的高速緩存是用所謂 slab 層來實(shí)現(xiàn)的,slab層即內(nèi)核中管理高速緩存的機(jī)制黔帕。
整個(gè)slab層的原理如下:
- 可以在內(nèi)存中建立各種對(duì)象的高速緩存(比如進(jìn)程描述相關(guān)的結(jié)構(gòu) task_struct 的高速緩存)代咸。
- 除了針對(duì)特定對(duì)象的高速緩存以外,也有通用對(duì)象的高速緩存成黄。
- 每個(gè)高速緩存中包含多個(gè) slab呐芥,slab用于管理緩存的對(duì)象逻杖。
- slab中包含多個(gè)緩存的對(duì)象,物理上由一頁或多個(gè)連續(xù)的頁組成思瘟。
文件接口:/proc/slabinfo
上圖所示為slabinfo文件的內(nèi)容荸百,第一行為表頭:
Name | Object name |
---|---|
Active_objs | 已經(jīng)激活的投入使用的object個(gè)數(shù) |
Num_objs | 為這個(gè)object分配的小內(nèi)存塊個(gè)數(shù) |
Objsize | 每一個(gè)內(nèi)存塊的大小 |
Objperslab | 每一個(gè)Slab分區(qū)包含的object個(gè)數(shù) |
Pagesperslab | 每個(gè)Slab分區(qū)包含的page的個(gè)數(shù) |
Active_slabs | 已經(jīng)激活的投入使用的Slab分區(qū)個(gè)數(shù) |
Num_slabs | 為這個(gè)object分配的Slab分區(qū)個(gè)數(shù) |
最后再說一句,slab只用于分配低端內(nèi)存潮太,所分配的內(nèi)存也只會(huì)被映射到物理內(nèi)存映射區(qū)管搪,所以vmalloc跟slab一毛錢關(guān)系都沒有虾攻。
3.2 內(nèi)存分配函數(shù)
1)按頁獲日÷颉(最原始方法):
以下分配內(nèi)存的方法參見:<linux/gfp.h>
方法 | 描述 |
---|---|
alloc_page(gfp_mask) | 只分配一頁,返回指向頁結(jié)構(gòu)的指針 |
alloc_pages(gfp_mask, order) | 分配 2^order 個(gè)頁霎箍,返回指向第一頁頁結(jié)構(gòu)的指針 |
__get_free_page(gfp_mask) | 只分配一頁奇钞,返回指向其邏輯地址的指針 |
__get_free_pages(gfp_mask, order) | 分配 2^order 個(gè)頁,返回指向第一頁邏輯地址的指針 |
get_zeroed_page(gfp_mask) | 只分配一頁漂坏,讓其內(nèi)容填充為0景埃,返回指向其邏輯地址的指針 |
alloc** 方法和 get** 方法的區(qū)別在于,一個(gè)返回的是內(nèi)存的物理地址顶别,一個(gè)返回內(nèi)存物理地址映射后的邏輯地址谷徙。
如果無須直接操作物理頁結(jié)構(gòu)體的話,一般使用 get** 方法驯绎。
2)按字節(jié)獲取(用的最多的獲取方法)
方法 | 描述 |
---|---|
kmalloc | 分配的內(nèi)存物理地址是連續(xù)的完慧,虛擬地址也是連續(xù)的。分配小塊內(nèi)存剩失,分配效率高屈尼。 |
vmalloc | 分配的內(nèi)存物理地址是不連續(xù)的,虛擬地址是連續(xù)的拴孤。分配大塊內(nèi)存脾歧,分配效率低。 |
盡管只有很少的硬件設(shè)備使用內(nèi)存的場(chǎng)合需要用到連續(xù)的物理內(nèi)存演熟,但是很多內(nèi)核代碼還是使用kmalloc來分配內(nèi)存而不是vmalloc主要還是出于性能考慮鞭执。在映射效率上,kmalloc明顯高于vmalloc芒粹。kmalloc的物理地址和虛擬地址之間的映射比較簡單蚕冬,只需要將物理地址的第一頁和虛擬地址的第一頁關(guān)聯(lián)起來即可。而vmalloc由于物理地址是不連續(xù)的是辕,所以要將物理地址的每一頁都和虛擬地址關(guān)聯(lián)起來才行囤热。當(dāng)然除非是不得已需要大塊內(nèi)存時(shí)會(huì)考慮使用vmalloc。
3)slab層獲然袢(效率最高的獲取方法)
這里主要是針對(duì)高速緩存來處理旁蔼。
方法 | 描述 |
---|---|
kmem_cache_create | 高速緩存的創(chuàng)建 |
kmem_cache_alloc | 從高速緩存中分配對(duì)象 |
kmem_cache_free | 向高速緩存釋放對(duì)象 |
kmem_cache_destroy | 高速緩存的銷毀 |
總結(jié):
在眾多的內(nèi)存分配函數(shù)中锨苏,如何選擇合適的內(nèi)存分配函數(shù)很重要,下面總結(jié)了一些選擇的原則:
應(yīng)用場(chǎng)景 | 分配函數(shù)選擇 |
---|---|
如果需要物理上連續(xù)的頁 | 選擇低級(jí)頁分配器或者 kmalloc 函數(shù) |
如果kmalloc分配是可以睡眠 | 指定 GFP_KERNEL 標(biāo)志 |
如果kmalloc分配是不能睡眠 | 指定 GFP_ATOMIC 標(biāo)志 |
如果不需要物理上連續(xù)的頁 | vmalloc 函數(shù) (vmalloc 的性能不如 kmalloc) |
如果需要高端內(nèi)存 | alloc_pages 函數(shù)獲取 page 的地址棺聊,在用 kmap 之類的函數(shù)進(jìn)行映射 |
如果頻繁撤銷/創(chuàng)建教導(dǎo)的數(shù)據(jù)結(jié)構(gòu) | 建立slab高速緩存 |
3.3 用戶態(tài)函數(shù)
函數(shù) | 描述 |
---|---|
malloc | 動(dòng)態(tài)內(nèi)存分配伞租,用于在堆上申請(qǐng)一塊連續(xù)的指定大小的內(nèi)存塊區(qū)域 |
mmap | 通過映射同一個(gè)普通文件實(shí)現(xiàn)共享內(nèi)存。普通文件被映射到進(jìn)程地址空間后限佩,進(jìn)程可以像訪問普通內(nèi)存一樣對(duì)文件進(jìn)行訪問葵诈,不必再調(diào)用read(),write()等操作祟同。 |
四作喘、缺頁中斷
在執(zhí)行一條指令時(shí),如果發(fā)現(xiàn)他要訪問的頁沒有在內(nèi)存中(即存在位為0)晕城,那么停止該指令的執(zhí)行泞坦,并產(chǎn)生一個(gè)頁不存在的異常,然后進(jìn)行故障處理砖顷,排除異常之后原先引起的異常的指令就可以繼續(xù)執(zhí)行贰锁,而不再產(chǎn)生異常。
缺頁中斷處理:
do_page_fault是缺頁中斷的核心函數(shù)滤蝠,主要工作交給__do_page_fault處理豌熄,然后進(jìn)行一些異常處理__do_kernel_fault和__do_user_fault。__do_page_fault主要工作交給handle_mm_fault物咳;handle_mm_fault的核心又是handle_pte_fault锣险。
handle_pte_fault()函數(shù)根據(jù)頁表項(xiàng)pte所描述的物理頁框是否在物理內(nèi)存中,分為兩大類:
請(qǐng)求調(diào)頁:被訪問的頁框不在主存中所森,那么此時(shí)必須分配一個(gè)頁框囱持,分為線性映射、非線性映射焕济、swap情況下映射纷妆。
寫實(shí)復(fù)制:被訪問的頁存在,但是該頁是只讀的晴弃,內(nèi)核需要對(duì)該頁進(jìn)行寫操作掩幢,此時(shí)內(nèi)核將這個(gè)已存在的只讀頁中的數(shù)據(jù)復(fù)制到一個(gè)新的頁框中。
把缺頁中斷處理當(dāng)成一個(gè)黑盒上鞠,就是采取一切手段讓你需要訪問的頁面存在于內(nèi)存中际邻,并且能正常讀寫,顯然這個(gè)過程是耗時(shí)的芍阎。
內(nèi)容有點(diǎn)多世曾,打算分上下兩篇來總結(jié),上篇就先總結(jié)到這吧谴咸。這里主要是梳理了下基本概念轮听,對(duì)細(xì)節(jié)感興趣的可以自己去擼Linux內(nèi)核骗露。
參考:
《Linux內(nèi)核設(shè)計(jì)與實(shí)現(xiàn)》
《奔跑吧Linux內(nèi)核 基于Linux4.x內(nèi)核源代碼問題分析》
https://www.cnblogs.com/wang_yb/archive/2013/05/23/3095907.html
https://www.cnblogs.com/wuchanming/p/4756911.html
http://www.wowotech.net/memory_management/233.html