告別“一頁(yè)障目”
本文轉(zhuǎn)載自微信公眾號(hào)「人人都是極客 」,作者布道師Peter。轉(zhuǎn)載本文請(qǐng)聯(lián)系人人都是極客 公眾號(hào)。
沒有宏觀概念,上來(lái)通過(guò)擼代碼來(lái)理解簡(jiǎn)直就是耍流氓,效率極低。為了更有效的理解內(nèi)存管理的來(lái)龍去脈很有必要先了解一些基礎(chǔ)概念,然后再去擼代碼。來(lái),先一起看看那些內(nèi)存里的各種頁(yè)的含義和應(yīng)用場(chǎng)景。
用戶進(jìn)程的內(nèi)存頁(yè)分為兩種:
- file-backed pages(文件背景頁(yè))
- anonymous pages(匿名頁(yè))
比如進(jìn)程的代碼段、映射的文件都是file-backed,而進(jìn)程的堆、棧都是不與文件相對(duì)應(yīng)的、就屬于匿名頁(yè)。
file-backed pages在內(nèi)存不足的時(shí)候可以直接寫回對(duì)應(yīng)的硬盤文件里,稱為page-out,不需要用到交換區(qū)(swap);而anonymous pages在內(nèi)存不足時(shí)就只能寫到硬盤上的交換區(qū)(swap)里,稱為swap-out。
file-backed pages(文件背景頁(yè))
對(duì)于有文件背景的頁(yè)面,程序去讀文件時(shí),可以通過(guò)read也可以通過(guò)mmap去讀。當(dāng)你通過(guò)任何一種方式從磁盤讀文件時(shí),內(nèi)核都會(huì)給你申請(qǐng)一個(gè)page cache,來(lái)緩存硬盤上的內(nèi)容。這樣的話,讀過(guò)一遍的數(shù)據(jù),本進(jìn)程或其他進(jìn)程下次再讀的時(shí)候就直接從page cache里去拿,就很快了,提升系統(tǒng)的整體性能。因此用戶的read/write實(shí)際上是跟page cache的相互拷貝。
而用戶的mmap則會(huì)將一段虛擬地址(3G)以下映射到page cache上,這樣的話,用戶就可以通過(guò)讀寫這段虛擬地址來(lái)修改文件內(nèi)容,省去了內(nèi)核和用戶之間的拷貝。
所以文件對(duì)于用戶程序來(lái)講其實(shí)只是內(nèi)存,page cache就是磁盤中文件的一個(gè)副本。可以通過(guò) “echo 3 > /proc/sys/vm/drop_cache” 來(lái)清cache。清掉之后,進(jìn)程第一次讀文件就會(huì)變慢。
通過(guò)free命令可以看到當(dāng)前page cache占用內(nèi)存的大小,free命令中會(huì)打印buffers和cached。通過(guò)文件系統(tǒng)來(lái)訪問文件(掛載文件系統(tǒng),通過(guò)文件名打開文件)產(chǎn)生的緩存就由cached記錄,而直接操作裸盤(打開/dev/sda設(shè)備去讀寫)產(chǎn)生的緩存就由buffers記錄。
實(shí)際上文件系統(tǒng)本身再讀寫文件就是操作裸分區(qū)的方式,用戶態(tài)也可以直接操作裸盤,像dd命令操作一個(gè)設(shè)備名也是直接訪問裸分區(qū)。那么,通過(guò)文件系統(tǒng)讀寫的時(shí)候,就會(huì)既有cached又有buffers。從圖中可以看到,文件名等元數(shù)據(jù)和文件系統(tǒng)相關(guān),是進(jìn)cached,實(shí)際的數(shù)據(jù)緩存還是在buffers。例如,read一個(gè)文件(如ext4文件系統(tǒng))的時(shí)候,如果文件cache命中了,就不用走到ext4層,從vfs層就返回了。
當(dāng)然,還可以在open的時(shí)候加上O_DIRECT標(biāo)記,做直接IO,就連buffers都不進(jìn)了,直接讀寫磁盤。
anonymous pages(匿名頁(yè))
沒有文件背景的頁(yè)面,即匿名頁(yè)(anonymous page),如堆,棧,數(shù)據(jù)段等,不是以文件形式存在,因此無(wú)法和磁盤文件交換,但可以通過(guò)硬盤上劃分額外的swap分區(qū)或使用swap文件進(jìn)行交換。swap分區(qū)可以將不活躍的頁(yè)交換到硬盤中,緩解內(nèi)存緊張。swap分區(qū)可以當(dāng)做針對(duì)匿名頁(yè)偽造的文件背景。
頁(yè)面回收(reclaim)
- 有文件背景的數(shù)據(jù)實(shí)際上就是page cache,但page cache不能無(wú)限增加,不能說(shuō)慢慢的所有文件都緩存到內(nèi)存了??隙ㄒ幸粋€(gè)機(jī)制,讓不常用的文件數(shù)據(jù)從page cache刷出去。內(nèi)核中有一個(gè)水位控制的機(jī)制,在系統(tǒng)內(nèi)存不夠用的時(shí)候,會(huì)觸發(fā)頁(yè)面回收。
- 對(duì)于沒有文件背景的頁(yè)面即匿名頁(yè),比如堆、棧、數(shù)據(jù)段,如果沒有swap分區(qū),不能與磁盤交換,就要常駐內(nèi)存了。但是常駐內(nèi)存的話,就會(huì)吃內(nèi)存,可以通過(guò)給硬盤搞一個(gè)swap分區(qū)或硬盤中創(chuàng)建一個(gè)swap文件讓匿名頁(yè)也能交換到磁盤上??烧J(rèn)為是為匿名頁(yè)偽造的文件背景。swap分區(qū)或swap文件實(shí)際上最終是到達(dá)了增大內(nèi)存的效果。當(dāng)然,如果頻繁交換的話,被交換出去的數(shù)據(jù)的訪問就會(huì)慢一些,因?yàn)橐蠭O操作了。
1. 水位(watermark)控制:
內(nèi)核中有三個(gè)水位:
- min:如果剩余內(nèi)存減少到觸及這個(gè)水位,可認(rèn)為內(nèi)存嚴(yán)重不足,當(dāng)前進(jìn)程就會(huì)被堵住,kernel會(huì)直接在這個(gè)進(jìn)程的進(jìn)程上下文里面做內(nèi)存回收(direct reclaim)。
- low:當(dāng)剩余內(nèi)存慢慢減少,觸到這個(gè)水位時(shí),就會(huì)觸發(fā)kswapd線程的內(nèi)存回收。
- high: 進(jìn)行內(nèi)存回收時(shí),內(nèi)存慢慢增加,觸到這個(gè)水位時(shí),就停止回收。
由于每個(gè)ZONE是分別管理各自內(nèi)存的,因此每個(gè)ZONE都有這三個(gè)水位
2. swapness:
回收的時(shí)候,是回收有文件背景的頁(yè)還是匿名頁(yè)還是都會(huì)回收呢,可通過(guò)/proc/sys/vm/swapness來(lái)控制讓誰(shuí)回收多一點(diǎn)點(diǎn)。swappiness越大,越傾向于回收匿名頁(yè);swappiness越小,越傾向于回收f(shuō)ile-backed的頁(yè)面。當(dāng)然,它們的回收方法都是一樣的LRU算法,即最近最少使用的頁(yè)會(huì)被回收。
3. 如何計(jì)算水位:
/proc/sys/vm/min_free_kbytes 是一個(gè)用戶可配置的值,默認(rèn)值是min_free_kbytes = 4 * sqrt(lowmem_kbytes)。然后根據(jù)min算出來(lái)low和high水位的值:low=5/4*min,high=6/4*min。
臟頁(yè)的寫回
sync是用來(lái)回寫臟頁(yè)的,臟頁(yè)不能在內(nèi)存中呆的太久,因?yàn)槿绻蝗粩嚯姏]有寫到硬盤的話臟數(shù)據(jù)就丟了,另一方面如果攢了很多一起寫回也會(huì)明顯占用CPU時(shí)間。
那么臟頁(yè)時(shí)候?qū)懟啬?臟頁(yè)回寫的時(shí)機(jī)由時(shí)間和空間兩方面共同控制:
時(shí)間:
- dirty_expire_centisecs: 臟頁(yè)的到期時(shí)間,或理解為老化時(shí)間,單位是1/100s,內(nèi)核中的flusher thread會(huì)檢查駐留內(nèi)存的時(shí)間超過(guò)dirty_expire_centisecs的臟頁(yè),超過(guò)的就回寫。
- dirty_writeback_centisecs:內(nèi)核的flusher thread周期性被喚醒(wakeup_flusher_threads())的時(shí)間間隔,每次被喚醒都會(huì)去檢查是否有臟頁(yè)老化了。如果將這個(gè)值置為0,則flusher線程就完全不會(huì)被喚醒了。
空間:
- dirty_ratio: 一個(gè)寫磁盤的進(jìn)程所產(chǎn)生的臟頁(yè)到達(dá)這個(gè)比例時(shí),這個(gè)進(jìn)程自己就會(huì)去回寫臟頁(yè)。
- dirty_background_ratio: 如果臟頁(yè)的數(shù)量超過(guò)這個(gè)比例時(shí),flusher線程就會(huì)啟動(dòng)臟頁(yè)回寫。
所以:
- 即使只有一個(gè)臟頁(yè),那如果它超時(shí)了,也會(huì)被寫回。防止臟頁(yè)在內(nèi)存駐留太久。dirty_expire_centisecs這個(gè)值默認(rèn)是3000,即30s,可以將其設(shè)置得短一些,這樣掉電后丟失的數(shù)據(jù)會(huì)更少,但磁盤寫操作也更密集。
- 不能有太多的臟頁(yè),否則會(huì)給磁盤IO造成很大壓力,例如在內(nèi)存不夠做內(nèi)存回收時(shí),還要先回寫臟頁(yè),也會(huì)明顯耗時(shí)。
需要注意的是,在達(dá)到dirty_background_ratio后,flusher線程(名為“[flush-devname]”)開始回寫,但由于寫磁盤速度慢,如果此時(shí)應(yīng)用進(jìn)程還在不停地寫磁盤,flusher線程回寫沒那么快,那么就會(huì)導(dǎo)致進(jìn)程的臟頁(yè)達(dá)到dirty_ratio,這時(shí)這個(gè)進(jìn)程就會(huì)去回寫臟頁(yè)而導(dǎo)致write被堵住。也就是說(shuō)dirty_background_ratio通常是比dirty_ratio小的。
臟頁(yè)都是指有文件背景的頁(yè)面,匿名頁(yè)不會(huì)存在臟頁(yè)。從/proc/meminfo的’Dirty’一行可以看到當(dāng)前系統(tǒng)的臟頁(yè)有多少,用sync命令可以刷掉。
zRAM機(jī)制
不用swap分區(qū),也可以用zRAM機(jī)制來(lái)緩解內(nèi)存緊張:從內(nèi)存里拿出一段內(nèi)存空間(compressed block),作為交換空間模擬硬盤的交換分區(qū),用來(lái)交換匿名頁(yè),并且讓kernel看到的物理內(nèi)存大小不包括這段內(nèi)存。而這段交換空間自帶透明壓縮功能,即交換到這塊zRAM分區(qū)時(shí),Linux會(huì)自動(dòng)將這塊匿名頁(yè)壓縮存放。系統(tǒng)訪問這塊頁(yè)面的內(nèi)容時(shí),產(chǎn)生page fault后從交換分區(qū)去拿,這時(shí)Linux給你透明解壓再交換出來(lái)。
使用zRAM的好處,就是訪存比訪問硬盤或flash的速度提高很多,且不用考慮壽命問題,并且由于這段內(nèi)存是壓縮后存儲(chǔ)的,因此可以存更多的數(shù)據(jù),雖然占用了一段內(nèi)存,但實(shí)際可以存更多的數(shù)據(jù),也達(dá)到了增加內(nèi)存的效果。缺點(diǎn)就是壓縮要占用CPU時(shí)間。
Android里面普遍使用了zRAM技術(shù),由于zRAM犧牲了CPU時(shí)間,所以交換次數(shù)還是越少越好。像Android和windows,內(nèi)存越大越好,因?yàn)榘l(fā)生交換的幾率就小。這樣兩個(gè)進(jìn)程相互切換(如微博和微信)時(shí)就會(huì)變得流暢,因?yàn)閮?nèi)存足夠的話,后臺(tái)進(jìn)程無(wú)需被換進(jìn)swap分區(qū)或被OOM殺掉。當(dāng)然如果你只打打電話,就沒必要大內(nèi)存啦。