linux記憶體管理淺析

來源:互聯網
上載者:User



[地址映射](圖:左中)
linux核心使用頁式記憶體管理,應用程式給出的記憶體位址是虛擬位址,它需要經過若干級頁表一級一級的變換,才變成真正的物理地址。
想一下,地址映射還是一件很恐怖的事情。當訪問一個由虛擬位址表示的記憶體空間時,需要先經過若干次的記憶體訪問,得到每一級頁表中用於轉換的頁表項(頁表是存放在記憶體裡面的),才能完成映射。也就是說,要實現一次記憶體訪問,實際上記憶體被訪問了N+1次(N=頁表級數),並且還需要做N次加法運算。
所以,地址映射必須要有硬體支援,mmu(記憶體管理單元)就是這個硬體。並且需要有cache來儲存頁表,這個cache就是TLB(Translation lookaside buffer)。
儘管如此,地址映射還是有著不小的開銷。假設cache的訪存速度是記憶體的10倍,命中率是40%,頁表有三級,那麼平均一次虛擬位址訪問大概就消耗了兩次實體記憶體訪問的時間。
於是,一些嵌入式硬體上可能會放棄使用mmu,這樣的硬體能夠運行VxWorks(一個很高效的嵌入式即時作業系統)、linux(linux也有禁用mmu的編譯選項)、等系統。
但是使用mmu的優勢也是很大的,最主要的是出於安全性考慮。各個進程都是相互獨立的虛擬位址空間,互不干擾。而放棄地址映射之後,所有程式將運行在同一個地址空間。於是,在沒有mmu的機器上,一個進程越界訪存,可能引起其他進程莫名其妙的錯誤,甚至導致核心崩潰。
在地址映射這個問題上,核心只提供頁表,實際的轉換是由硬體去完成的。那麼核心如何產生這些頁表呢?這就有兩方面的內容,虛擬位址空間的管理和實體記憶體的管理。(實際上只有使用者態的地址映射才需要管理,核心態的地址映射是寫死的。)

[虛擬位址管理](圖:左下)
每個進程對應一個task結構,它指向一個mm結構,這就是該進程的記憶體管理器。(對於線程來說,每個線程也都有一個task結構,但是它們都指向同一個mm,所以地址空間是共用的。)
mm->pgd指向容納頁表的記憶體,每個進程有自已的mm,每個mm有自己的頁表。於是,進程調度時,頁表被切換(一般會有一個CPU寄存器來儲存頁表的地址,比如X86下的CR3,頁表切換就是改變該寄存器的值)。所以,各個進程的地址空間互不影響(因為頁表都不一樣了,當然無法訪問到別人的地址空間上。但是共用記憶體除外,這是故意讓不同的頁表能夠訪問到相同的物理地址上)。
使用者程式對記憶體的操作(分配、回收、映射、等)都是對mm的操作,具體來說是對mm上的vma(虛擬記憶體空間)的操作。這些vma代表著進程空間的各個地區,比如堆、棧、代碼區、資料區、各種映射區、等等。
使用者程式對記憶體的操作並不會直接影響到頁表,更不會直接影響到實體記憶體的分配。比如malloc成功,僅僅是改變了某個vma,頁表不會變,實體記憶體的分配也不會變。
假設使用者指派了記憶體,然後訪問這塊記憶體。由於頁表裡面並沒有記錄相關的映射,CPU產生一次缺頁異常。核心捕捉異常,檢查產生異常的地址是不是存在於一個合法的vma中。如果不是,則給進程一個"段錯誤",讓其崩潰;如果是,則分配一個物理頁,並為之建立映射。

[實體記憶體管理](圖:右上)
那麼實體記憶體是如何分配的呢?
首先,linux支援NUMA(非均質儲存結構),實體記憶體管理的第一個層次就是介質的管理。pg_data_t結構就描述了介質。一般而言,我們的記憶體管理介質只有記憶體,並且它是均勻的,所以可以簡單地認為系統中只有一個pg_data_t對象。
每一種介質下面有若干個zone。一般是三個,DMA、NORMAL和HIGH。
DMA:因為有些硬體系統的DMA匯流排比系統匯流排窄,所以只有一部分地址空間能夠用作DMA,這部分地址被管理在DMA地區(這屬於是進階貨了);
HIGH:高端記憶體。在32位系統中,地址空間是4G,其中核心規定3~4G的範圍是核心空間,0~3G是使用者空間(每個使用者進程都有這麼大的虛擬空間)(圖:中下)。前面提到過核心的地址映射是寫死的,就是指這3~4G的對應的頁表是寫死的,它映射到了物理地址的0~1G上。(實際上沒有映射1G,只映射了896M。剩下的空間留下來映射大於1G的物理地址,而這一部分顯然不是寫死的)。所以,大於896M的物理地址是沒有寫死的頁表來對應的,核心不能直接存取它們(必須要建立映射),稱它們為高端記憶體(當然,如果機器記憶體不足896M,就不存在高端記憶體。如果是64位機器,也不存在高端記憶體,因為地址空間很大很大,屬於核心的空間也不止1G了);
NORMAL:不屬於DMA或HIGH的記憶體就叫NORMAL。
在zone之上的zone_list代表了分配策略,即記憶體配置時的zone優先順序。一種記憶體配置往往不是只能在一個zone裡進行分配的,比如分配一個頁給核心使用時,最優先是從NORMAL裡面分配,不行的話就分配DMA裡面的好了(HIGH就不行,因為還沒建立映射),這就是一種分配策略。
每個記憶體介質維護了一個mem_map,為介質中的每一個物理頁面建立了一個page結構與之對應,以便管理實體記憶體。
每個zone記錄著它在mem_map上的起始位置。並且通過free_area串聯著這個zone上閒置page。實體記憶體的分配就是從這裡來的,從 free_area上把page摘下,就算是分配了。(核心的記憶體配置與使用者進程不同,使用者使用記憶體會被核心監督,使用不當就"段錯誤";而核心則無人監督,只能靠自覺,不是自己從free_area摘下的page就不要亂用。)

[建立地址映射]
核心需要實體記憶體時,很多情況是整頁分配的,這在上面的mem_map中摘一個page下來就好了。比如前面說到的核心捕捉缺頁異常,然後需要分配一個page以建立映射。
說到這裡,會有一個疑問,核心在分配page、建立地址映射的過程中,使用的是虛擬位址還是物理地址呢?首先,核心代碼所訪問的地址都是虛擬位址,因為CPU指令接收的就是虛擬位址(地址映射對於CPU指令是透明的)。但是,建立地址映射時,核心在頁表裡面填寫的內容卻是物理地址,因為地址映射的目標就是要得到物理地址。
那麼,核心怎麼得到這個物理地址呢?其實,上面也提到了,mem_map中的page就是根據實體記憶體來建立的,每一個page就對應了一個物理頁。
於是我們可以說,虛擬位址的映射是靠這裡page結構來完成的,是它們給出了最終的物理地址。然而,page結構顯然是通過虛擬位址來管理的(前面已經說過,CPU指令接收的就是虛擬位址)。那麼,page結構實現了別人的虛擬位址映射,誰又來實現page結構自己的虛擬位址映射呢?沒人能夠實現。
這就引出了前面提到的一個問題,核心空間的頁表項是寫死的。在核心初始化時,核心的地址空間就已經把地址映射寫死了。page結構顯然存在於核心空間,所以它的地址映射問題已經通過“寫死”解決了。
由於核心空間的頁表項是寫死的,又引出另一個問題,NORMAL(或DMA)地區的記憶體可能被同時映射到核心空間和使用者空間。被映射到核心空間是顯然的,因為這個映射已經寫死了。而這些頁面也可能被映射到使用者空間的,在前面提到的缺頁異常的情境裡面就有這樣的可能。映射到使用者空間的頁面應該優先從HIGH地區擷取,因為這些記憶體被核心訪問起來很不方便,拿給使用者空間再合適不過了。但是HIGH地區可能會耗盡,或者可能因為裝置上實體記憶體不足導致系統裡面根本就沒有HIGH地區,所以,將NORMAL區域對應給使用者空間是必然存在的。
但是NORMAL地區的記憶體被同時映射到核心空間和使用者空間並沒有問題,因為如果某個頁面正在被核心使用,對應的page應該已經從free_area被摘下,於是缺頁異常處理代碼中不會再將該頁映射到使用者空間。反過來也一樣,被映射到使用者空間的page自然已經從free_area被摘下,核心不會再去使用這個頁面。

[核心空間管理](圖:右下)
除了對記憶體整頁的使用,有些時候,核心也需要像使用者程式使用malloc一樣,分配一塊任意大小的空間。這個功能是由slab系統來實現的。
slab相當於為核心中常用的一些結構體對象建立了對象池,比如對應task結構的池、對應mm結構的池、等等。
而slab也維護有通用的對象池,比如"32位元組大小"的對象池、"64位元組大小"的對象池、等等。核心中常用的kmalloc函數(類似於使用者態的malloc)就是在這些通用的對象池中實現分配的。
slab除了對象實際使用的記憶體空間外,還有其對應的控制結構。有兩種組織方式,如果對象較大,則控制結構使用專門的頁面來儲存;如果對象較小,控制結構與對象空間使用相同的頁面。
除了slab,linux 2.6還引入了mempool(記憶體池)。其意圖是:某些對象我們不希望它會因為記憶體不足而分配失敗,於是我們預先分配若干個,放在mempool中存起來。正常情況下,指派至時是不會去動mempool裡面的資源的,照常通過slab去分配。到系統記憶體緊缺,已經無法通過slab分配記憶體時,才會使用 mempool中的內容。

[頁面換入換出](圖:左上)(圖:右上)
頁面換入換出又是一個很複雜的系統。記憶體頁面被換出到磁碟,與磁碟檔案被映射到記憶體,是很相似的兩個過程(記憶體頁被換出到磁碟的動機,就是今後還要從磁碟將其載回記憶體)。所以swap複用了檔案子系統的一些機制。
頁面換入換出是一件很費CPU和IO的事情,但是由於記憶體昂貴這一曆史原因,我們只好拿磁碟來擴充記憶體。但是現在記憶體越來越便宜了,我們可以輕鬆安裝數G的記憶體,然後將swap系統關閉。於是swap的實現實在讓人難有探索的慾望,在這裡就不贅述了。(另見:《linux核心頁面回收淺析》)

[使用者空間記憶體管理]
malloc是libc的庫函數,使用者程式一般通過它(或類似函數)來分配記憶體空間。
libc對記憶體的分配有兩種途徑,一是調整堆的大小,二是mmap一個新的虛擬記憶體地區(堆也是一個vma)。
在核心中,堆是一個一端固定、一端可伸縮的vma(圖:左中)。可伸縮的一端通過系統調用brk來調整。libc管理著堆的空間,使用者調用malloc分配記憶體時,libc盡量從現有的堆中去分配。如果堆空間不夠,則通過brk增大堆空間。
當使用者將已指派的空間free時,libc可能會通過brk減小堆空間。但是堆空間增大容易減小卻難,考慮這樣一種情況,使用者空間連續分配了10塊記憶體,前9塊已經free。這時,未free的第10塊哪怕只有1位元組大,libc也不能夠去減小堆的大小。因為堆只有一端可伸縮,並且中間不能掏空。而第10塊記憶體就死死地佔據著堆可伸縮的那一端,堆的大小沒法減小,相關資源也沒法歸還核心。
當使用者malloc一塊很大的記憶體時,libc會通過mmap系統調用映射一個新的vma。因為對於堆的大小調整和空間管理還是比較麻煩的,重建立一個vma會更方便(上面提到的free的問題也是原因之一)。
那麼為什麼不總是在malloc的時候去mmap一個新的vma呢?第一,對於小空間的分配與回收,被libc管理的堆空間已經能夠滿足需要,不必每次都去進行系統調用。並且vma是以page為單位的,最小就是分配一個頁;第二,太多的vma會降低系統效能。缺頁異常、vma的建立與銷毀、堆空間的大小調整、等等情況下,都需要對vma進行操作,需要在當前進程的所有vma中找到需要被操作的那個(或那些)vma。vma數目太多,必然導致效能下降。(在進程的vma較少時,核心採用鏈表來管理vma;vma較多時,改用紅/黑樹狀結構來管理。)

[使用者的棧]
與堆一樣,棧也是一個vma(圖:左中),這個vma是一端固定、一端可伸(注意,不能縮)的。這個vma比較特殊,沒有類似brk的系統調用讓這個vma伸展,它是自動伸展的。
當使用者訪問的虛擬位址越過這個vma時,核心會在處理缺頁異常的時候將自動將這個vma增大。核心會檢查當時的棧寄存器(如:ESP),訪問的虛擬位址不能超過ESP加n(n為CPU壓棧指令一次性壓棧的最大位元組數)。也就是說,核心是以ESP為基準來檢查訪問是否越界。
但是,ESP的值是可以由使用者態程式自由讀寫的,使用者程式如果調整ESP,將棧劃得很大很大怎麼辦呢?核心中有一套關於進程限制的配置,其中就有棧大小的配置,棧只能這麼大,再大就出錯。
對於一個進程來說,棧一般是可以被伸展得比較大(如:8MB)。然而對於線程呢?
首先線程的棧是怎麼回事?前面說過,線程的mm是共用其父進程的。雖然棧是mm中的一個vma,但是線程不能與其父進程共用這個vma(兩個運行實體顯然不用共用一個棧)。於是,線上程建立時,線程庫通過mmap建立了一個vma,以此作為線程的棧(大於一般為:2M)。
可見,線程的棧在某種意義上並不是真正棧,它是一個固定的地區,並且容量很有限。

原文地址:http://hi.baidu.com/_kouu/item/4c73532902a05299b73263d0

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.