本文是任督二脈之內(nèi)存管理課程第一節(jié)課的總結(jié)說明,由于水平有限,可能無法對宋老師所講完全理解通透,如有錯誤,請及時指證。
本文從5個方面進行說明:
1、 物理/虛擬/總線地址概念說明。
2、 MMU是什么,為什么,怎么做。
3、 內(nèi)存分區(qū)和內(nèi)存映射區(qū)。
4、 Buddy算法是個什么鬼。
5、 CMA的工作原理。
物理/虛擬/總線地址概念說明
所謂一花一世界,一葉一菩提,相同的事物在不同的角度可能會有不同的看法,對于物理地址,虛擬地址,總線地址的概念也是如此。
物理地址是MMU的視角所看到的內(nèi)存地址。
虛擬地址是存在MMU的前提下CPU所看到的內(nèi)存地址,當然我們實際編程的時候操作的也是虛擬地址。
總線地址是設(shè)備的視角所看到的內(nèi)存地址。
比如一塊內(nèi)存,物理地址是0,在設(shè)備端看起來是0x80000000,而物理地址0又通常被映射為虛擬地址0xc0000000,從而同一地址就具備了三個身份,但他們在物理上指的是同一片區(qū)域。
歸根結(jié)底,不論是MMU,CPU或程序員,還是設(shè)備,他們的終極目的是操作內(nèi)存,至于怎么操作,它們又都有各自的比較舒服的操作方式,就是所謂的物理地址,虛擬地址和總線地址,至于為什么要通過這種方式操作內(nèi)存,請參考下一節(jié),MMU是什么,為什么,怎么做。
MMU是什么,為什么,怎么做
通常情況下,應(yīng)用程序并不需要關(guān)心內(nèi)存實際的物理地址,從應(yīng)用程序的角度,“我需要的時候你就要給我,至于你是如何分配的,還有多少空閑,我不管”,MMU使這種需求成為可能。
我們知道應(yīng)用程序的每一個進程都有自己的一張頁表,通常0-3G為用戶空間,3-4G為內(nèi)核空間,每一個進程都傻傻的以為自己獨自擁有4G的內(nèi)存空間,從而使得程序員在寫程序時不需要考慮計算機中物理內(nèi)存的實際容量,但是我們真的沒有這么大的內(nèi)存啊,怎么辦?沒關(guān)系,MMU可以解決。
MMU提供了虛擬地址和物理地址的映射功能,這個功能使每個進程都擁有“4G獨立的內(nèi)存空間”成為可能。另外MMU還提供內(nèi)存權(quán)限保護,用戶權(quán)限保護和Cache緩存控制等功能。
我們使用C語言定義一個const變量,MMU(應(yīng)該是內(nèi)核,而不是MMU)會標記該變量所在的內(nèi)存區(qū)間為readonly,當另外一個文件單元通過虛擬地址嘗試寫這個變量,MMU在把虛擬地址轉(zhuǎn)換為物理地址的過程中發(fā)現(xiàn),這段內(nèi)存區(qū)域是readonly的,那么,不好意思,你無權(quán)寫入,并產(chǎn)生一個fault,內(nèi)核收到這個fault向應(yīng)用程序發(fā)送一個SIGSEGV,應(yīng)用程序產(chǎn)生段錯誤并結(jié)束。
用戶權(quán)限保護,同理,MMU會標記內(nèi)核空間的內(nèi)存,當一個用戶程序嘗試訪問內(nèi)核空間內(nèi)存,也會被拒絕。
另外,MMU還提供Cache緩存控制功能。我們知道設(shè)備可通過DMA直接訪問內(nèi)存,而不需要CPU;另一方面讀寫內(nèi)存是非常耗時的(相對cache來說),如果我們的CPU存在高速緩存,把最近經(jīng)常使用的內(nèi)存緩沖的Cache可以大大的提高程序的效率。但是這時出現(xiàn)了一個問題,如何保證DMA和Cache的一致性問題?MMU提供了Cache是否命中的檢查,從而進一步可以保證DMA與Cache的一致性。
那么MMU是如何實現(xiàn)物理地址到虛擬地址的映射的呢,請看下圖:
對于一個虛擬地址,0x12345670,地址的高20位表示頁表的物理地址,也就是0x12345(對應(yīng)圖中的p),通過這個地址找到頁表所在位置,讀取該位置中的數(shù)據(jù),這個數(shù)據(jù)指向物理地址的索引。虛擬地址的低12位表示物理地址索引的偏移值,也就是0x670(對應(yīng)圖中的d),我們現(xiàn)在有了物理地址的索引值和偏移值,自然就可以找到所對應(yīng)的物理內(nèi)存位置。
另外MMU比較重要的一個組成部分需要介紹一下,TLB(Translation Lookaside Buffer)轉(zhuǎn)換旁路緩存,顧名思義,他是一個物理地址和虛擬地址轉(zhuǎn)換關(guān)系的緩存,是上圖中Page table的cache,也被稱為快表。
最后,附上宋老師的總結(jié):http://mp.weixin.qq.com/s/SdsT6Is0VG84WlzcAkNCJA
內(nèi)存分區(qū)和內(nèi)存映射區(qū)
首先明確內(nèi)存分區(qū)和內(nèi)存映射區(qū)的區(qū)別,內(nèi)存分區(qū)指的是實際的物理內(nèi)存的分區(qū),內(nèi)存映射區(qū)指的是每一個進程所擁有的虛擬地址空間的分區(qū)情況。
對于一個運行了linux的設(shè)備,通常存在如下分區(qū):DMAzone、Normal zone、HighMem zone。
DMAzone存在的原因是有些設(shè)備存在硬件缺陷,無法通過DMA訪問全部的內(nèi)存空間,為了讓這些設(shè)備在需要內(nèi)存的時候能夠每次都申請到訪問能力范圍內(nèi)的內(nèi)存空間,內(nèi)核規(guī)定了一個DMA zone,當這些設(shè)備申請內(nèi)存的時候,指定分配flag為GFP_DMA,它就可以拿到DMA zone的內(nèi)存。也就是說如果我們的設(shè)備不存在這樣的存在缺陷的設(shè)備,我們就不需要DMA zone,或者說整個Normal zone都是DMA zone。一般我們稱DMA zong + Normal zone為Low memory zone。
HighMemzone存在原因是,當內(nèi)存較大時,內(nèi)核空間的3-4G空間無法把所有物理內(nèi)存地址一一映射到內(nèi)核空間,只能映射一部分,那么無法映射的那部分就是高端內(nèi)存。
可以一一映射到內(nèi)核空間的那部分內(nèi)存,除去DMA zone就是Normal zone。
內(nèi)存映射區(qū)可以簡單的理解為,每個進程都擁有的4G空間。其中3-4G為內(nèi)核空間,這部分空間又被劃分為多個區(qū)域,其中與DMA zone和Normal zone存在一一映射關(guān)系的區(qū)域是DMA+常規(guī)內(nèi)存映射區(qū)或者low memory映射區(qū),同時也專門有個區(qū)域可以映射HighMem zone,但是并不存在一一映射的關(guān)系,這個區(qū)域是高端內(nèi)存映射區(qū)。
所謂的一一映射,是指虛擬地址和物理地址只是存在一個物理上的偏移。
在x86系統(tǒng)中,內(nèi)存分區(qū)和內(nèi)存映射區(qū)存在如下的關(guān)系:
在arm linux中,內(nèi)存分區(qū)和內(nèi)存映射區(qū)的關(guān)系請參考內(nèi)核文檔Documentation/arm/memory.txt
Buddy算法是個什么鬼
Linux的最底層的內(nèi)存分配算法叫做Buddy算法,它以2的n次方頁為單位對空閑內(nèi)存進行管理。就是說不管我在應(yīng)用程序中分了多大的空間,1字節(jié),100KB,或者其它任意的大小,底層實際分配的是以2的n次方個頁對齊的空間,當然并不是每一次用戶空間申請內(nèi)存都會引起底層的內(nèi)存分配,slab算法就可以在buddy算法的基礎(chǔ)上對內(nèi)存進行二次管理,分配更小的內(nèi)存空間,當然C庫也可以對分配的空間二次利用,比如指定mallopt函數(shù)的第一個參數(shù)為M_TRIM_THRESHOLD,并設(shè)置真正釋放內(nèi)存給系統(tǒng)的閥值。。
Buddy算法的優(yōu)點是避免了內(nèi)存的外部碎片,但是長期運行后,大片的內(nèi)存會比較少,而1頁,2頁,4頁這種內(nèi)存會非常多,當我們分配大片連續(xù)內(nèi)存的時候就會出問題,具體解決辦法請參考下一節(jié)-CMA的工作原理。
在linux系統(tǒng)中,我們可以通過/proc/buddy文件來查看當前系統(tǒng)空閑的連續(xù)內(nèi)存空間剩余情況。
CMA的工作原理
應(yīng)用程序中申請一塊內(nèi)存,在應(yīng)用程序看來是連續(xù)的,因為虛擬地址本身是連續(xù)的,但實際的內(nèi)存空間中,所申請的這片內(nèi)存未必是連續(xù)的,不過這對應(yīng)用程序來說是沒關(guān)系的,因為應(yīng)用程序不需要關(guān)心實際的內(nèi)存情況,只要MMU把物理地址映射成虛擬地址就好了。但是如果沒有MMU的情況呢,我們又需要一片連續(xù)的內(nèi)存空間,比如設(shè)備通過DMA直接訪問內(nèi)存,這種情況下應(yīng)該怎么辦呢?
CMA機制就是為了解決上面提到的問題而產(chǎn)生的。DMA zone并不是DMA專屬,其它的程序也可以申請該zone的內(nèi)存,如果當設(shè)備要申請DMA zone空間的一大片連續(xù)的內(nèi)存時候,已經(jīng)沒有連續(xù)的大片內(nèi)存了,只有1頁,2頁,4頁的這種連續(xù)的小內(nèi)存。解決辦法就是我們標記某一片連續(xù)區(qū)域為CMA區(qū)域,這部分區(qū)域在沒有大片連續(xù)內(nèi)存申請的時候只給moveable的程序使用,當大片連續(xù)內(nèi)存請求來的時候,我們?nèi)ミ@片區(qū)域,把所有moveable的小片內(nèi)存移動到其它的非CMA區(qū)域,更改對應(yīng)的程序的頁表,然后再把空出來的CMA區(qū)域給設(shè)備,從而實現(xiàn)了DMA大片連續(xù)內(nèi)存的分配。
CMA機制并不是單獨存在的,它通常服務(wù)于DMA設(shè)備,在設(shè)備調(diào)用dma_alloc_coherent函數(shù)申請一塊內(nèi)存后,為了得到一片連續(xù)的內(nèi)存,CMA機制被調(diào)用,它保證了申請的內(nèi)存的連續(xù)性。
另外CMA區(qū)域通常被分配在高端內(nèi)存。
任督二脈之內(nèi)存管理第二節(jié)課總結(jié)
本文是任督二脈之內(nèi)存管理課程第二節(jié)課的總結(jié)說明,由于水平有限,可能無法對宋老師所講完全理解通透,如有錯誤,請及時指證。
本文從4個方面進行說明:
1、 Slab的基本原理以及它的文件接口說明
2、 kmalloc、vmalloc、malloc比較
3、 OOM是什么,為什么,怎么做
4、 FAQ:群里經(jīng)常問到的,也是比較容易誤解的問題
slab的基本原理以及它的文件接口說明
在第一節(jié)課中,我們了解到,Linux的最底層,由Buddy算法管理著所有的空閑頁面,最小單位是2的0次方頁,就是1頁,4K,但是很多時候,我們?yōu)橐粋€結(jié)構(gòu)分配空間,也只需要幾十個字節(jié),按頁分配無疑是浪費空間;另外,當我們頻繁的分配和釋放一個結(jié)構(gòu),我們希望在釋放的時候,這部分內(nèi)存不要立刻還給Buddy,而是提供一種類似C庫的管理機制,在下一次在分配的時候還可以拿到同一塊內(nèi)存且保留著基本的數(shù)據(jù)結(jié)構(gòu)。基于上面兩點,Slab應(yīng)運而生??偨Y(jié)一下,Slab主要提供以下兩個功能:
A.對從Buddy拿到的內(nèi)存進行二次管理,以更小的單位進行分配和回收(注意,是回收而不是釋放),防止了空間的浪費。
B. 讓頻繁使用的對象盡量分配在同一塊內(nèi)存區(qū)間并保留基本數(shù)據(jù)結(jié)構(gòu),提高程序效率。
那么,Slab是如何工作的呢?
如果某個結(jié)構(gòu)被頻繁的使用,內(nèi)核源碼就可以針對這個結(jié)構(gòu)建立一個或者多個Slab分區(qū)(姑且這么叫),每一個Slab分區(qū)從Buddy拿到1頁或者多頁內(nèi)存,并把這些內(nèi)存劃分為多個等分的這個結(jié)構(gòu)大小的小塊內(nèi)存,這些Slab分區(qū)只用于分配給這個結(jié)構(gòu),通常稱這個結(jié)構(gòu)為object,每一次當有該object的分配請求,內(nèi)核就從對應(yīng)的Slab分區(qū)拿一小塊內(nèi)存給object,這樣就實現(xiàn)了在同一片內(nèi)存區(qū)間為頻繁使用的object分配內(nèi)存。請看下圖
黑框表示頻繁使用的結(jié)構(gòu);紅框表示slab分區(qū),一個結(jié)構(gòu)內(nèi)核可能為它分配一個或多個Slab;每個Slab分區(qū)有可能包含多個page,被分隔開的多個紅框表示Slab分區(qū)的多個pages;藍框表示Slab分區(qū)為對應(yīng)的Object劃分的一個一個的小內(nèi)存塊。填充黃色的框表示active的object,灰色填充的框表示未active的object,如果整個Slab分區(qū)的所有藍框都是灰色的,表示這個Slab分區(qū)是未active的。
Linux為用戶提供了Slab的文件查看接口,和命令接口。
文件接口:/proc/slabinfo
上圖所示為slabinfo文件的內(nèi)容,第一行為表頭:
Name:Object名字
Active_objs:已經(jīng)激活的投入使用的object個數(shù)
Num_objs:為這個object分配的小內(nèi)存塊個數(shù)
Objsize:每一個內(nèi)存塊的大小
Objperslab:每一個Slab分區(qū)包含的object個數(shù)
Pagesperslab:每個Slab分區(qū)包含的page的個數(shù)
Active_slabs:已經(jīng)激活的投入使用的Slab分區(qū)個數(shù)
Num_slabs:為這個object分配的Slab分區(qū)個數(shù)
我在查看Slabinfo文件的時候,發(fā)現(xiàn)有的Num_objs為0,正常Active_objs為0是可以理解的,但是總的object數(shù)不應(yīng)該為0啊,然后繼續(xù)看,Active_slabs和Num_slabs也都為0,也就是說,這個時候內(nèi)存還沒有為這個結(jié)構(gòu)分配Slab分區(qū),一切就都解釋的通了。
另外還有一部分slabinfo的內(nèi)容是這樣的:
就是說,除了經(jīng)常頻繁使用的結(jié)構(gòu),內(nèi)核為他們分配了slabs,還同時定義了一些特定的slabs供驅(qū)動使用。
命令接口:slabtop
直接運行slabtop命令(要加sudo,上面查看slabinfo文件同樣),內(nèi)容如下
有點類似top命令,按照使用內(nèi)存的多少進行排序。
最后再說一句,slab只用于分配低端內(nèi)存,所分配的內(nèi)存也只會被映射到物理內(nèi)存映射區(qū),所以vmalloc跟slab一毛錢關(guān)系都沒有。
kmalloc、vmalloc、malloc比較
這部分內(nèi)容牽連太多,也不好區(qū)分,直接上圖:
(此圖有誤:highmem不是映射到vmalloc)
如上圖所示:
如上圖所示:
A.kmalloc函數(shù)是基于slab算法的,從物理內(nèi)存的low mem獲取內(nèi)存,并線性映射到物理內(nèi)存映射區(qū)(映射過程開機就已經(jīng)完成了),由于是線性映射,物理地址和虛擬地址存在簡單的轉(zhuǎn)換關(guān)系(物理地址和虛擬地址的值只是相差了一個固定的偏移),所以使用kmalloc分配內(nèi)存是十分高效的。
B. vmalloc函數(shù)分配內(nèi)存的過程需要先通過alloc_pages函數(shù)獲取內(nèi)存(獲取范圍是整個內(nèi)存條),然后在通過復(fù)雜的邏輯轉(zhuǎn)換(注意,vmalloc并不是簡單的線性映射,它獲取的內(nèi)存并不是連續(xù)的),把物理內(nèi)存映射到vmalloc映射區(qū)。這個過程比較復(fù)雜,所以,如果只是使用vmalloc分配很小的內(nèi)存空間是不合適的。
C. malloc是標準C庫的函數(shù),C庫對申請的內(nèi)存做二次管理,類似Slab。但是注意一點,當我們使用malloc函數(shù)申請一片內(nèi)存時,實際上是從C庫獲取的內(nèi)存,就是說,調(diào)用malloc返回后,系統(tǒng)未必給你一片真正的內(nèi)存,分兩種情況
a. C庫還持有足夠的內(nèi)存,那么malloc就可以直接分配到C庫現(xiàn)有的內(nèi)存
b. C庫沒有足夠的內(nèi)存,malloc返回時,系統(tǒng)只是把要申請的內(nèi)存大小的虛擬地址空間全部映射到同一塊已經(jīng)清零的物理內(nèi)存,當我們實際要寫這片內(nèi)存的時候,才通過brk/mmap系統(tǒng)調(diào)用分配真實的物理內(nèi)存,并改寫進程頁表。
D.另外有一點值得一提,vmalloc映射區(qū),除了vmalloc函數(shù)分配的內(nèi)存會映射在該區(qū)域,設(shè)備的寄存器也同樣會通過ioremap映射到該區(qū)域。
E. 根據(jù)上面要點C的描述,在編寫實時程序時,我們可以通過下面這種方式,減少系統(tǒng)內(nèi)存的頻繁分配,而是基于C庫管理的內(nèi)存。
#include
#include
#define SOMESIZE (100*1024*1024) // 100MB
int main(int argc, char *argv[])
{
unsigned char *buffer;
int i;
if (!mlockall(MCL_CURRENT | MCL_FUTURE))//鎖定進程當前和將來所有的內(nèi)存
mallopt(M_TRIM_THRESHOLD, -1UL);//設(shè)置C庫釋放內(nèi)存的閥值為最大正整數(shù)
mallopt(M_MMAP_MAX, 0);
buffer = malloc(SOMESIZE);
if (!buffer)
exit(-1);
/*
*Touch each page in this piece of memory to get it
*mapped into RAM
*/
for (i = 0; i < SOMESIZE; i += 4 *1024)//由于COW,確保內(nèi)存被真實分配
buffer[i] = 0;
free(buffer);
/*
/* 接下來的所有內(nèi)存分配動作都不是觸發(fā)系統(tǒng)內(nèi)存的真是分配,而是從C庫獲取
*大大提高程序的效率,確保程序?qū)崟r性。
*/
while(1);
return 0;
}
OOM是什么,為什么,怎么做
什么是OOM:上面提到,當我們使用malloc分配內(nèi)存時,系統(tǒng)并沒有真正的分配內(nèi)存,而是采用欺騙性的手段,拖延分配內(nèi)存的時機,防止無謂的內(nèi)存消耗,只有在我們寫入的時候,產(chǎn)生page fault才會拿到真實的內(nèi)存,且是寫多少才分配多少,那么,問題來了,當我們通過malloc獲取一片內(nèi)存并成功返回,然后開始逐步使用內(nèi)存,系統(tǒng)也逐步的分配真實的內(nèi)存給進程,但這個過程中,另外一個耗內(nèi)存的程序快速的拿走所有的內(nèi)存,導(dǎo)致我的進程在逐步寫入的過程發(fā)現(xiàn)剛剛說好給我的內(nèi)存現(xiàn)在沒有了,這種情況就是OOM,out of memory!
在Linux系統(tǒng),每一個進程都有一個oomscore,這個數(shù)值越高,說明進程消耗的內(nèi)存越多,在發(fā)生OOM的情況下,oom score越高的進程就越有可能被系統(tǒng)干掉,從而緩解系統(tǒng)的內(nèi)存壓力。我們可以通過/proc/pid/oom_score文件查看進程的oom score。
那么有沒有什么辦法可以調(diào)整進程的oom score,就算這個進程比較耗內(nèi)存,但是在OOM時候,這個進程仍然不會干掉。系統(tǒng)提供兩個接口文件給用戶:
/proc/pid/oom_ adj:可配置范圍是-17到15,設(shè)置為15,oom score最大,最容易被干掉,設(shè)置-16,oom score最小,設(shè)置-17為禁止使用OOM殺死該進程。
/proc/pid/oom_score_adj:oom score會加上這個值,也可以設(shè)置負數(shù),但如果負數(shù)的絕對值大于oom score,oom score最小為0。
FAQ
Q.kfree和free函數(shù)調(diào)用后,內(nèi)存是否還給了Buddy?
A.kmalloc分配的內(nèi)存是基于slab的,malloc分配的內(nèi)存是基于C庫的,slab和C庫都會對內(nèi)存進行二次管理,實際到底有沒有被釋放,只有Slab和C庫他們自己知道。
Q.kmalloc,vmalloc,malloc他們從哪個zone申請物理內(nèi)存,然后映射到那個映射區(qū)?
A.kmalloc從low mem獲取物理內(nèi)存,然后映射到內(nèi)核空間的物理內(nèi)存映射區(qū),vmalloc和malloc都可以從整個內(nèi)存條獲取內(nèi)存,vmalloc申請的內(nèi)存映射到vmalloc映射區(qū),malloc申請的內(nèi)存映射到進程的用戶空間。
寫到這里,群里已經(jīng)發(fā)出了第二節(jié)課問答集,其它更多內(nèi)容參考該文檔。
任督二脈之內(nèi)存管理第三節(jié)課總結(jié)
本文是任督二脈之內(nèi)存管理課程第三節(jié)課的總結(jié)說明,由于水平有限,可能無法對宋老師所講完全理解通透,如有錯誤,請及時指證。
本文從7個方面進行說明:
1、 VMA到底是個什么鬼?
2、 Linux提供的VMA文件接口和命令接口說明。
3、 Page fault的產(chǎn)生原因分析以及與VMA的關(guān)系。
4、 物理內(nèi)存、頁表、進程之間的愛恨情仇。
5、 VSS、RSS、PSS、USS概念說明和實際的應(yīng)用場景
6、 進程內(nèi)存使用情況命令接口smem。
7、 內(nèi)存泄漏的界定和監(jiān)測辦法。
VMA到底是個什么鬼?
VMA是Virtual MemoryAreas的縮寫,虛擬內(nèi)存區(qū)域,指的是用戶空間0-3G范圍內(nèi)進程所擁有的多個全部零散分布的連續(xù)的虛擬內(nèi)存空間。
注意上面這句話的三個定語:
用戶空間0-3G范圍內(nèi)進程所擁有的:VMA區(qū)域存在于用戶空間,當然“所擁有”并不是獨占,也有可能是共享的。
多個全部零散分布的:進程擁有多個VMA區(qū)域,但是零散分布在0-3G空間。
連續(xù)的:這里說的連續(xù),指的是單個VMA區(qū)域在虛擬地址空間是連續(xù)的。
看完上面的內(nèi)容基本知道VMA是個什么東西了,那么VMA產(chǎn)生的原因是什么,為什么要搞個這個概念出來,VMA區(qū)域是客觀存在的,你不定義VMA的struct,進程的代碼段,數(shù)據(jù)段,堆,棧都是客觀存在的,進程只要在代碼段按序執(zhí)行就好了,我管你叫什么名字,所以進程并不需要關(guān)心這個概念,真正需要VMA概念的是內(nèi)核,通過VMA這個概念方便實現(xiàn)對所有進程內(nèi)存空間的管理。
我們知道一個進程被fork出來,內(nèi)核會維護一個taskstruct,這個結(jié)構(gòu)的mmap成員維護了一個vm_area_struct的鏈表,這就是VMA的結(jié)構(gòu),內(nèi)核通過維護這個結(jié)構(gòu)來實現(xiàn)對進程的內(nèi)存資源的管理、隔離和共享。
舉個例子:進程使用malloc分配內(nèi)存,這時C庫沒有足夠的內(nèi)存,內(nèi)核的Lazy機制采用欺騙性手段,拖延分配內(nèi)存的時機,這時,內(nèi)存并沒有被真正分配,內(nèi)核只是把所有要分配的頁表都映射到同一片已經(jīng)清零的物理地址,并標記頁表權(quán)限為readonly,但是當malloc返回的時候,對應(yīng)的heap的VMA區(qū)域已經(jīng)產(chǎn)生了,且已經(jīng)進入內(nèi)核管理的vm_area_struct鏈表中了,且權(quán)限被標記為讀寫權(quán)限,當我們向這片內(nèi)存寫入的時候,MMU在虛擬地址轉(zhuǎn)換到物理地址的過程,發(fā)現(xiàn)頁表的權(quán)限標記為readonly,而你要寫入,這是不被允許的,于是產(chǎn)生Page fault,內(nèi)核收到Page fault,查看對應(yīng)的VMA鏈表,發(fā)現(xiàn)進程實際是有寫的權(quán)限,于是分配頁面,并改寫頁表,但是這整個過程,進程是不知道的,進程只是傻傻的以為,哈哈,老子又拿到了一片內(nèi)存!
Linux提供的VMA文件接口和命令接口
Linux為用戶提供了VMA查看的命令接口和文件接口。
命令接口:pmap
文件接口:/proc/pid/maps 、 /proc/pid/smaps
這些接口都可以看到進程的VMA區(qū)域分布情況,占用空間大小,和相應(yīng)的權(quán)限
此處不做過多說明。
Page fault的產(chǎn)生原因分析以及與VMA的關(guān)系
其實上面的“VMA到底是個什么鬼”章節(jié),已經(jīng)介紹了一種Pagefault產(chǎn)生的原因,也說明了與VMA的關(guān)系。下面列舉產(chǎn)生Page fault的4中原因。
A.動態(tài)分配內(nèi)存,第一次寫入,由于內(nèi)核的Lazy機制,頁表的權(quán)限為readonly,VMA權(quán)限為r+w,MMU產(chǎn)生Page fault,這種情況是真實的缺頁,下面還會介紹并不是真實的缺頁的情況。這種缺頁也叫做Minor Page fault。
B. 進程訪問自己的VMA區(qū)域以外的空間,這種行為被認為是非法的,同樣會產(chǎn)生Page fault,但不會像A中一樣引起真正的內(nèi)存分配,反而會收到一個segv,程序被干掉。這種情況其實并不是真正的缺頁。
C. 進程訪問自己的VMA區(qū)域,但是并沒有執(zhí)行該操作的權(quán)限,比如進程嘗試寫代碼段或者跳轉(zhuǎn)到數(shù)據(jù)段執(zhí)行,這也被認為是非法的,同樣收到segv,程序被干掉。這種情況也不是真正的缺頁。
D.進程訪問自己的VMA區(qū)域,且權(quán)限正確,但是對應(yīng)的物理內(nèi)存內(nèi)容被swap到硬盤,這種情況毫無疑問才是徹徹底底的缺頁,也會產(chǎn)生Page fault。這種缺頁也叫做Major Page fault。
物理內(nèi)存、頁表、進程之間的愛恨情仇
此部分說起來比較復(fù)雜,直接上圖:
上圖中,process1044,1045,1054是進程的虛擬地址空間,綠色框圖是他們各自的頁表,圖片最中間的是實際的物理內(nèi)存。進程1044,1045,1054在虛擬地址空間各自擁有多個VMA區(qū)域,但是多個進程的VMA可能通過各自頁表指向同一片內(nèi)存區(qū)域,比如上圖中l(wèi)ibc代碼段,三個進程的libc的VMA區(qū)域都通過頁表指向同一片內(nèi)存,就是說這三個進程共享這段內(nèi)存。當然進程的VMA通過頁表指向的內(nèi)存也有可能是被這個進程獨占的,比如上面三個進程的堆,都是各自獨占的。
舉個例子:假設(shè)上圖中的內(nèi)存是女神,女神為了襯托自己漂亮,身邊難免要有幾個丑女閨蜜,丑女閨蜜就是頁表,屌絲process 1044,1045,1054對女神愛慕已久,只是苦于女神高高在上,無法接近,那么怎么辦,曲線救國,先接近她的閨蜜,三個屌絲通過女神的三個閨蜜都輕松的獲取到女神的基本愛好,喜歡吃什么,喜歡什么顏色,三個屌絲雖然都各自獲取到一份信息,但是這個信息是客觀存在的只有一份(這就是上面說的共享的情況),這時,其中一個屌絲1044,辛苦加班12個月,攢錢給閨蜜買了個大金鏈子,這個大金鏈子就是屌絲1044跟女神所擁有的獨家美好記憶(這就是上面說的獨占的情況)。
VSS、RSS、PSS、USS概念說明和實際的應(yīng)用場景
話不多說,還是上圖吧:
如圖所示:
VSS是進程看到的自己在虛擬內(nèi)存空間所占用的內(nèi)存。
RSS是進程實際真正使用的內(nèi)存。
PSS是多進程共享一片內(nèi)存的容量取平均數(shù),在加上自己獨占的內(nèi)存。
USS是進程所獨占的內(nèi)存容量。
通常,VSS≥RSS>PSS>USS,VSS之所以大于等于RSS,是考慮內(nèi)核的Lazy機制并沒有真正的分配內(nèi)存以及內(nèi)存被換出等情況。
好!繼續(xù)舉例子:女神懷了高富帥的娃,苦于腹中胎兒一天天長大,高富帥又不值得托付,所以決定在眾多屌絲中擇一名形象氣質(zhì)佳的男士作為配偶,屌絲1044,1045,1054踴躍報名,由于三人形象上都不分伯仲,所以女神的主要考察點改為:誰更在乎自己多一點。于是:
VSS:屌絲自以為對女神的在乎程度,知道女神的愛好,還給女神買大金鏈子都計算在內(nèi)。
RSS:每個人的感知程度不一樣,屌絲縱然萬般寵愛,可女神沒感覺到也是白搭,這個值是女神感受到的在乎程度。
PSS:請來裁判,考察屌絲日常的在乎程度,知道女神愛好+1分,送大金鏈子+3分,這個分數(shù)是比較客觀的。
USS:單獨考量每個屌絲送多少大金鏈子。
所以,綜上,我們知道PSS值是比較客觀的值,VSS是一個虛擬的值,RSS是一個實際的值,USS是獨占的值。
進程內(nèi)存使用情況命令接口smem
Linux提供命令接口smem來查看系統(tǒng)中進程的VSS、RSS、PSS、USS值。
還可以使用—pie選項和—bar選項進程圖形化顯示,更加一目了然。
內(nèi)存泄漏的界定和監(jiān)測辦法
進程運行時申請的內(nèi)存,在進程結(jié)束后會被全部釋放。內(nèi)存泄漏指的是運行的程序,隨著時間的推移,占用的內(nèi)存容量呈現(xiàn)線性增長,原因是程序中的申請和釋放不成對。
如何監(jiān)測程序是否出現(xiàn)了內(nèi)存泄漏的情況,一方面我們可以通過上文提到的smem命令,連續(xù)的在多個時間點采樣,記錄USS的變化情況,如果這個值在連續(xù)的很長時間里呈現(xiàn)出持續(xù)增長,基本就可以斷定程序存在內(nèi)存泄漏的情況,然后你就可以手動去程序中查找泄漏位置。
另一方面,如果程序代碼量較大,不方便查找定位內(nèi)存泄漏點,可以使用valgrind和addresssanitizer來查找程序的內(nèi)存泄漏。兩種方式各有優(yōu)劣,valgrind在虛擬機中運行程序,所以程序運行效率下降;addresssanitizer則需要改動源碼,在源碼中包含sanitizer/lsan_interface.h文件,然后在需要檢查內(nèi)存泄漏的地方調(diào)用函數(shù)__lsan_do_leak_check。兩種方式都可以定位內(nèi)存泄漏的位置。
任督二脈之內(nèi)存管理第四節(jié)課總結(jié)
本文是任督二脈之內(nèi)存管理課程第四節(jié)課的總結(jié)說明,由于水平有限,可能無法對宋老師所講完全理解通透,如有錯誤,請及時指證。
發(fā)了前幾天的總結(jié)后,有群里的朋友@jeff表示,我這樣大篇幅的文字描述,估計沒幾個人有耐心看下去,想想也是,內(nèi)存管理本身就比較復(fù)雜,枯燥,我聽了宋老師的課,了解個一知半解,轉(zhuǎn)述的過程可能也不到位。所以這一次盡量使用圖片進行說明,然后逐步展開。話不多說,先上圖吧。
如圖所示:有兩條脈絡(luò),分別用黃線和藍線標識,黃線的脈絡(luò)為有文件背景的數(shù)據(jù)交換過程,藍線為無文件背景的數(shù)據(jù)交換過程。
那么何為有文件背景的頁File-backedpage,何為無文件背景的頁,也就是匿名頁anonymous page,直接盜用宋老師課件圖片:
特別說明一下程序的代碼段,程序運行的時候,實際上是把ELF文件的代碼段加載到物理內(nèi)存的page cache,然后映射到內(nèi)核空間的page cache頁,所以程序的代碼段也是file backed的。
接下來分兩條脈絡(luò)來進行說明和擴展。
Filebacked
在硬盤中能對應(yīng)到實際的文件的,歸納為file backed,文件在open后,會被加載到物理內(nèi)存,我們稱這片內(nèi)存為page cache頁,在3.14版本以前的內(nèi)核,page cache又被劃分為buffers和cache,3.14版本以后不做區(qū)分,全部看作page cache。Page cache被映射到內(nèi)核空間的虛擬地址,用戶通過兩種方式訪問磁盤上的文件,直接讀寫和mmap到用戶空間,可能觸發(fā)page cache頁面和磁盤數(shù)據(jù)交換的有LRU,手動同步SYNC和內(nèi)存回收reclaim。
上面的一段話,描述了filebacked的整體脈絡(luò),有一下幾個問題需要單獨說明:
A.3.14版本以前的pagecache劃分為buffers和cache,他們有什么區(qū)別?
要說明這個問題,先明確一個概念,page cache作為磁盤的一個緩沖,它緩沖過來的文件內(nèi)容是可以被犧牲掉的,就是說我內(nèi)存空間不足的時候,我可以回收這部分內(nèi)存資源,所以在Linux操作系統(tǒng)看來,這部分內(nèi)存雖然被占用,但是仍然是available的。
通過free命令,可以看到3.13版本分別列出了buffers和cached的數(shù)值,buffers值是直接操作裸分區(qū)的pagecache的大小,比如我們通過dd命令讀寫SD卡;cached值是操作文件系統(tǒng)上的文件的page cache的大小,比如直接open dev/sda1/hello.c文件,這個文件的page cache被叫做cached。
看上圖,free命令的第三行,有一個-/+buffers/cache,這個值是如何計算的呢?
上面說過,page cache是可犧牲的,這個值就是page cache被回收后的內(nèi)存的used和free的值,盜用宋老師課件again。
其實這種劃分沒有什么特別的意義,他們的區(qū)別是各自的background不同而已,所以3.14版本之后的內(nèi)核不做buffers和cached的區(qū)分,free命令的-/+ buffers/cache這一行也不再需要,只是單獨搞出一個available值,用于表示當前系統(tǒng)中包括已經(jīng)使用的page cache(可犧牲),到底有多少可利用的內(nèi)存。
原諒我的free還不夠給力,并沒有列出available值。
B.mmap和直接讀寫有什么區(qū)別?
Pagecache被映射到內(nèi)核空間后,用戶想要操作對應(yīng)的文件,實際上是對內(nèi)存里page cache的操作,系統(tǒng)會在合適的時機回寫到磁盤中對應(yīng)的文件。操作的方式有兩種,直接讀寫,和通過mmap把page cache在映射到用戶空間一份。
程序調(diào)用read、write函數(shù)的時候,陷入到內(nèi)核空間,實際調(diào)用的是file operation結(jié)構(gòu)的read、write接口,這兩個接口必然要做的一件事就是copy_from_user和copy_to_user,我們知道這兩個函數(shù)是會引起內(nèi)核空間與用戶空間的數(shù)據(jù)拷貝的,就是說每一次read和write都要進行一次拷貝。
mmap則完全不同,它把pagecache直接映射到用戶空間,現(xiàn)在用戶空間和內(nèi)核空間的頁表都可以對應(yīng)到page cache物理內(nèi)存。然后通過mmap返回的指針來讀寫page cache,這個過程是沒有用戶空間和內(nèi)核空間的內(nèi)存拷貝的。
通常在操作顯存設(shè)備的時候會使用mmap,比如我可以通過mmap把/dev/fb0映射到用戶空間,通過讀寫mmap返回的指針來實現(xiàn)對屏幕的顯示。
C.LRU是個什么鬼?
LUR,LeastRecently Used,翻譯過來就是最近最少使用,顧名思義,內(nèi)核把最近最少使用的page cache內(nèi)容或者anonymous頁面交換出去。上圖,盜圖three
現(xiàn)在cache的大小是4頁,前四次,1,2,3,4文件被一次使用,注意第七次,5文件被使用,系統(tǒng)評估最近最少被使用的文件是3,那么不好意思,3被swap出去,5加載進來,依次類推。
所以LRU可能會觸發(fā)pagecache或者anonymous頁與對應(yīng)文件的數(shù)據(jù)交換。
D.SYNC指的什么,如何觸發(fā)pagecache與文件的數(shù)據(jù)交換?
系統(tǒng)為用戶提供了,數(shù)據(jù)從pagecache回寫到文件的接口,sync命令。只要運行sync命令就可以觸發(fā)page cache與文件的數(shù)據(jù)交換。
另外,我們還可以通過向/proc/sys/vm/drop_caches寫入數(shù)字來清空對應(yīng)的caches,一般寫入之前,最好執(zhí)行一下sync命令來同步一下,以便釋放更多的空間,因為drop_caches只回收cleanpages,不回收dirtypages,所以如果想回收更多的cache,應(yīng)該在drop_caches之前先執(zhí)行"sync"命令,把dirtypages變成cleanpages。
echo 1 > /proc/sys/vm/drop_caches //清空 pagecache
echo 2 > /proc/sys/vm/drop_caches //清空 dentries 和 inodes
echo 3 > /proc/sys/vm/drop_caches //清空所有緩存(pagecache、dentries 和 inodes)
anonymous
在進程中定義的堆,棧等沒有文件背景的頁面,如果系統(tǒng)沒有創(chuàng)建swap分區(qū)或者swap文件,那不好意思,匿名頁只能常駐內(nèi)存,直到程序退出,或者發(fā)生OOM程序被干掉。與file backed不同的是,anonymous本身就在內(nèi)存中,而file backed是磁盤中的文件,為了提高效率把內(nèi)存作為磁盤的緩沖區(qū),就是page cache,page cache可以往對應(yīng)文件交換,anonymous頁如果過大的話,可以往swap分區(qū)或者創(chuàng)建的swap文件中交換。要操作這些匿名頁,直接在程序源碼中操作變量和動態(tài)分配內(nèi)存的指針就好了。
那么,對于anonymous頁,在什么情況下會觸發(fā)數(shù)據(jù)交換呢?
除了LRU會產(chǎn)生匿名頁的交換,內(nèi)存回收也會引發(fā)數(shù)據(jù)交換,reclaim,Linux有一個后臺進程kswapd,負責回收page cache和匿名頁,回收速度較慢,但是不會影響程序運行,程序不會被delay,當系統(tǒng)內(nèi)存資源異常緊張時,會觸發(fā)Direct reclaim,這個過程回收速度較快,但是進程會被直接delay,直到回收夠足夠的內(nèi)存。
至于何時kswapd開始回收內(nèi)存,何時Directreclaim,有三個門限值,min,low,hight,當內(nèi)存的水位達到low,說明內(nèi)存緊張,這時kswapd開始工作,慢慢回收內(nèi)存,直到水位達到high,當系統(tǒng)內(nèi)存異常緊張時,達到min水位,Direct reclaim被觸發(fā)。
Swappiness
當系統(tǒng)內(nèi)存不足時,可以從filebacked pages或者anonymous pages回收內(nèi)存,不論哪個被回收,再次被加載進內(nèi)存一定都會影響程序的效率。具體從哪里回收,Linux提供swappiness值作為衡量標準。
Swappiness越大,越傾向于回收匿名頁;swappiness越小,越傾向于回收file-backed的頁面。當然,它們的回收方法都是一樣的LRU算法。盜圖four。
順便附上一個參考鏈接:http://mp.weixin.qq.com/s/BixMISiPz3sR9FDNfVSJ6w
zRamswap
對于嵌入式設(shè)備,它的磁盤是SD卡,MMC,一方面速度較慢,另一方面,有使用壽命的問題,不太適合做swap分區(qū)。嵌入式設(shè)備一般會從內(nèi)存中拿出一小部分當作虛擬內(nèi)存,這個就是zRam。但是這樣直接用又沒有什么意義,因為虛擬內(nèi)存的目的就是在內(nèi)存不足時“擴展內(nèi)存”,現(xiàn)在內(nèi)存還是那片內(nèi)存就是換了個說法,所以為了“擴展內(nèi)存”,當系統(tǒng)把內(nèi)存交換到這個虛擬內(nèi)存,通常是以壓縮的方式存儲,當swap in時在解壓。這樣就某種程度的“擴展了內(nèi)存”,但是缺點是增加了CPU的壓力,需要進行壓縮和解壓縮。
任督二脈之內(nèi)存管理第五節(jié)課總結(jié)
本文是任督二脈之內(nèi)存管理課程第五節(jié)課的總結(jié)說明,由于水平有限,可能無法對宋老師所講完全理解通透,如有錯誤,請及時指證。
第五節(jié)課的內(nèi)容多且雜,其實完全可以合并到前四節(jié)課中。但考慮前四篇總結(jié)已經(jīng)完成,章節(jié)插入不方便,所以還是多寫一篇。
本文分成兩部分來論述
1、DMA與Cache一致性問題。
2、 常用的命令接口和文件接口簡要說明。
DMA與Cache一致性問題
關(guān)于這一部分,宋老師的文章已經(jīng)講解的非常細致,我在寫也無非是畫蛇添足,所以此處只做簡單總結(jié)。附上文章連接,http://mp.weixin.qq.com/s/5K7rlPXo2yIcoIXXgqqLfQ
而實際上,如果你不是在IC公司,大部分時候你只需要在驅(qū)動程序中輕松敲下dma_alloc_coherent來獲取一片能確保DMA和cache一致性的內(nèi)存就可以了,具體實現(xiàn)細節(jié)對你來說可能并不重要。請看下圖:
A.DMA的內(nèi)存分配區(qū)域
不論是應(yīng)用程序還是驅(qū)動程序,在獲取內(nèi)存時都需要獲得一片連續(xù)地址的內(nèi)存,但是由
于DMA設(shè)備訪問內(nèi)存不經(jīng)過MMU,所以也無法把不連續(xù)的物理地址映射為連續(xù)的虛擬地址,解決這個問題有兩種方式:
a.在CMA區(qū)域申請DMA內(nèi)存,因為CMA本身是一片連續(xù)的物理內(nèi)存,CMA通常被分配在高端內(nèi)存,這個時候這片內(nèi)存會被映射到vmalloc映射區(qū),如果CMA在低端內(nèi)存,則不需要重新映射,因為低端內(nèi)存在開機時已經(jīng)與low Memory映射區(qū)建立了一一映射關(guān)系。
b.如果設(shè)備存在IOMMU,那么做DMA內(nèi)存分配時則不需要關(guān)心具體的內(nèi)存分配區(qū)域,IOMMU會讓設(shè)備看到一片連續(xù)的地址范圍,它的功能類似MMU,只不過MMU是把物理地址轉(zhuǎn)換為連續(xù)的虛擬地址供CPU使用,而IOMMU是把物理地址轉(zhuǎn)換為連續(xù)的總線地址供設(shè)備使用。
B.確保DMA和cache一致性的手段
確保DMA和cache一致性的手段有以下三種:
a.頁表設(shè)置uncache
要保證DMA和cache一致性最簡單辦法,在申請到內(nèi)存后,修改對應(yīng)的頁表,將頁表的cache屬性改為uncache,這樣,當CPU在訪問該片內(nèi)存時就不會從cache取數(shù)據(jù)。
b.硬件確保一致性
有的設(shè)備提供了確保一致性的硬件機制,這時我們申請內(nèi)存后則不需要修改頁表的cache屬性,一致性由硬件來保證。
c.代碼手動同步
如果對應(yīng)的內(nèi)存區(qū)域已經(jīng)申請好了,設(shè)備直接使用,那么驅(qū)動就無法更改對應(yīng)頁表的cache屬性,這時解決一致性的手段時在每次訪問這篇內(nèi)存前手動同步cache內(nèi)容到內(nèi)存,同時禁止CPU對這片內(nèi)存的訪問,直到設(shè)備訪問完成。實際上下面提到的DMA streaming mapping就是通過這種方式實現(xiàn)的。
C.API接口
當我們的驅(qū)動自己獲取內(nèi)存,可以使用一致性DMA緩沖區(qū)API接口,就是
dam_alloc_coherent();如果對應(yīng)的內(nèi)存已經(jīng)被成功分配,我們在使用前需要調(diào)用DMA流映射API接口,確保cache的內(nèi)容被成功flush到內(nèi)存,對應(yīng)的函數(shù)有dma_map_sg()和dma_map_single(),他們兩個的區(qū)別是,sg映射的內(nèi)存是分散/聚集的,分散在不同的位置,single映射的內(nèi)存是連續(xù)的一片內(nèi)存,通常是CMA。
常用的命令接口和文件接口簡要說明
A.文件Dirty數(shù)據(jù)寫回配置接口
/proc/sys/vm/dirty_expire_centisecs:設(shè)置Dirty數(shù)據(jù)的寫回時間期限,超過這個時間,在flusher線程下次喚醒后,寫回這部分數(shù)據(jù),單位是百分之一秒,厘秒。
/proc/sys/vm/dirty_writeback_centisecs:flusher線程周期性喚醒的時間,單位是厘秒,設(shè)置為0,表示禁止定期寫回。Flusher線程喚醒后會把超過期限的臟頁和進程超過dirty_background_ratio值的臟頁寫回。
/proc/sys/vm/dirty_background_ratio:進程持有的臟頁的個數(shù)閥值,單位是頁,超過這個值,flusher線程在下次喚醒后會對臟頁進行寫回。
/proc/sys/vm/dirty_ ratio:進程持有的臟頁的個數(shù)閥值,單位是頁,超過這個值,進程delay,無法在進行任何的寫操作,并且進程自行完成臟頁的回寫。
B.Memory Cgroup的使用
控制group的最大使用內(nèi)存示例如下:
$:cd /sys/fs/cgroup/memory
$:mkdir A //創(chuàng)建一個分組
$:cd A/
$echo $((200*1024*1024)) >memory.limit_in_bytes //設(shè)置該組最大可使用內(nèi)存200M
$:cgexec –g memory:A ./a.out //將a.out添加到組A并執(zhí)行
C.內(nèi)存回收接口說明
在內(nèi)存管理四章節(jié)中提到內(nèi)存回收有三個水位,min,low和hight,當內(nèi)存的水位達到low,說明內(nèi)存緊張,這時kswapd開始工作,在后臺慢慢回收內(nèi)存,直到水位達到high,當系統(tǒng)內(nèi)存異常緊張時,達到min水位,程序被堵住,Direct reclaim被觸發(fā)。具體相關(guān)接口如下:
/proc/zoneinfo //該文件可以查看到各個zone的情況,包括各個zone的三個水位設(shè)置
/proc/sys/vm/min_free_kbytes //可用于查看和設(shè)置min水位的值
其中l(wèi)ow水位和high水位沒有對應(yīng)的設(shè)置接口,是通過計算得來的。
Low = min*5/4
High = min*6/4
各個zone的水位標準是按總的水位標準等比例劃分的,比如normal zone是800M,內(nèi)存一共1G,min_free_kbytes被設(shè)置為30720,即30M,那么,normal zone的min水位就等于,800/1024 * 30720,就是24000
D.Swappiness接口
Swappiness越大,越傾向于回收匿名頁;swappiness越小,越傾向于回收file-backed的頁面。當然,它們的回收方法都是一樣的LRU算法。Swappiness的接口有兩個,一個是cgroup里的swappiness,一個是/proc/sys/vm下的swappiness,他們的區(qū)別是作用域不同,cgroup里的swappiness只控制組內(nèi)程序的回收傾向,而/proc/sys/vm/swappiness控制當前整個系統(tǒng),除了在cgroup被重新定義的程序。
E.Getdelays工具
要使用該工具需要打開內(nèi)核選項CONFIG_TASK_DELAY_ACCT和CONFIG_TASKSTATS。該工具的源文件在LinuxKernelSource/Documentation/accounting下。使用getdelays可以查看當前系統(tǒng)或者某個進程調(diào)度的延時,IO的delay情況,swap和內(nèi)存回收的delay情況,幫助用戶查看程序的耗時情況。
F.Vmstat命令
該命令可周期性的查看swap in/out和block in/out的情況。
-
CMA
+關(guān)注
關(guān)注
0文章
28瀏覽量
9941 -
MMU
+關(guān)注
關(guān)注
0文章
92瀏覽量
18575 -
Buddy
+關(guān)注
關(guān)注
0文章
5瀏覽量
7536
原文標題:陳延偉:任督二脈之內(nèi)存管理總結(jié)筆記
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論