linux下內存

MMU由一個或一組芯片組成,其功能是把邏輯地址映射爲物理地址,進行地址轉換(MMU是CPU的一部分)

機器指令仍然用邏輯地址指定一個操作數的地址或一條指令的地址

每一個邏輯地址都由一個段選擇符(16位)和段內的相對偏移量(32位)組成。段寄存器的唯一目的是存放段選擇符。

MMU包含兩個部件:分段部件和分頁部件,分段機制將邏輯地址轉換爲線性地址,分頁機制把線性地址轉換爲物理地址。

在RAM芯片上的讀或寫必須串行地執行,因此一種內存仲裁器的硬件電路插在總線和每個RAM芯片之間。

在整個系統中全局描述符(GDT)只有一張,可以被存放在內存的任何位置,,但CPU必須知道GDT的入口,GDT不僅存放了段描述符,還有其它描述符,都是64bit長,它是全局可見的,對任何一個任務都是這樣。

GDT的第一項總是設爲0,這就確保空段選擇符的邏輯地址會被認爲是無效的,因此引起一個處理器異常。????? 

分段可以給每個進程分配不同的線性地址空間,而分頁可以把同一線性地址空間映射到不同的物理地址空間。

運行在用戶態的所有進程都使用一對相同的段來對指令和數據尋址,這兩個段就是所謂的用戶代碼段和用戶數據段。

所有段都是從0x00000000 開始,可以得出一個重要的結論,就是在linux邏輯地址與線性地址是一致的,即邏輯地址的偏移量子段與相應的線性地址的值總是一致的。

當指向指令或者數據結構的指針進行保存時,內核根本不需要爲其設置邏輯地址的段選擇符,因爲CS寄存器就含有當前的段選擇符。

每個處理器有一個任務狀態段(tss),相應的線性地址空間都是內核數據段相應的線性地址空間的小子集。

GDT中只有少數項可能依賴CPU正在執行的進程(LDT和TLS段描述符)。

分頁單元的一個關鍵任務是所請求的訪問類型與線性地址的訪問權限相比較,如果這次內存訪問是無效的,就產生缺頁異常。

線性地址被分成以固定長度爲單位的組,稱爲頁。頁內部連續的線性地址被映射到連續的物理地址中。

分頁單元把所有的RAM分成固定長度的頁框。

把線性地址映射到物理地址的數據結構稱爲頁表。頁表存放在主存中,並在啓動分頁單元之前必須由內核對頁表進行適當的初始化。

二級模式通過只爲進程實際使用的那些虛擬內存區域請求頁表來減少內存使用量。

擴展分頁用於把大段連續的線性地址轉換成相應的物理地址,在這些情況下,內核可以不用中間頁表進行地址轉換,從而節省內存並保留TLB項。

與段的3種存取權限不同(讀寫執行)不同的是,頁的存取權限只有兩種(讀寫)。

 由於用戶進程線性地址空間的需要,內核不能直接對1GB以上的RAM進行尋址。

只有內核能夠修改進程的頁表,所以在用戶態下運行的進程不能使用物理地址。

使用的級別數量取決於CPU類型。

硬件高速緩存基於著名的局部性原理,該原理既使用於程序結構也適用於數據結構。

高速緩存單元插在分頁單元和主內存之間。它包含一個硬件高速緩存內存和一個高速緩存控制器。高速緩存內存中存放內存中真正的行,高速緩存控制器存放一個標項數組。  

系統的運行速度一般是被CPU從內存中取得指令和數據速率限制的。

當命中一個高速緩存時,高速緩存控制器進行不同的操作,具體取決於存取類型。

cache訪問:1 寫回操作(write back) 2 寫穿操作(write through)。回寫方式只更新高速緩存行,不改變RAM的內容,提供了更快的功效。只有當CPU執行一條要求刷新高速緩存表項的指令時,或者當一個FLUSH硬件信號產生時(通常在高速緩存不命中之後),高速控制器才把高速緩存行寫回道RAM中。

多處理器系統的每一個處理器都有一個單獨的硬件高速緩存,因此需要額外的硬件電路用於保持高速緩存內容的同步。

TLB(translation lookaside buffer)的高速緩存用於加快線性地址的轉換。當線性地址被第一次使用時,通過慢速訪問RAM中的頁表計算出相應的物理地址。同時物理地址被存放在一個TLB表項中,以便以後對同一個線性地址的引用快速的得到轉換。

在多處理器系統中,每個CPU都有自己的TLB。

Linux的進程處理很大程度上依賴於分頁。

在初始化階段,內核必須建立一個物理地址映射來指定哪些物理地址範圍對內核可用而哪些不可用(或者因爲他們映射硬件設備I/O的共享內存,或者因爲相應的頁框含有BIOS數據)。

宏PAGE_OFFSET的值是區分用戶空間和內核空間的範圍大小,也是進程在線性地址中偏移量。PAGE_SHIFT是指頁大小。****

內核維持着一組自己使用的頁表,駐留在所謂的主內核全局目錄中,系統初始化後,該組頁表還從未被任何進程貨任何內核線程直接使用。

在內核剛剛被裝入內存後,CPU仍然運行於實模式,所以分頁功能沒有被啓用。

臨時頁的全局目錄放在swapper_pg_dir變量中。

由內核頁表提供的最終映射必須把從PAGE_OFFSET開始線性地址轉化爲從0開始的物理地址。

主內核全局目錄仍然保存在swapper_pg_dir變量中,由paging_init()函數初始化。

內核線性地址第四個GB的初始部分映射系統的物理內存至少有128MB的線性地址總是留作他用,因爲內核使用這些線性地址實現非連續內存分配和固定映射的線性地址。

間接飲用一個指針變量比間接引用一個立即常量地址要多一次內存訪問。


RAM的某些部分永久的分配給內核,並用來存放內核代碼及靜態內核數據結構。RAM的其餘部分稱爲動態內存,這不僅是進程所需要的寶貴資源,也是內核本身所要的寶貴資源。

內核必須記錄每個頁框當前的狀態。

在一下情況下頁框是不空閒的,包含用戶態進程的數據,某個軟件高速緩存的數據,動態分配的內核數據結構,設備驅動程序緩衝的數據,內核模塊的代碼等。

頁框的狀態信息保存在一個類型爲page的頁描述符中,所有的頁描述符存放在mem_map數組中。

在NUMA模型中,給定CPU對不同內存單元的訪問時間可能不一樣,系統的物理內存被劃分爲幾個節點。在一個單獨的節點內,任意給定CPU訪問頁面所需的時間都是相同的。每個節點都有一個類型爲gd_data_t的描述符。

每個頁描述符都有到內存節點和到節點管理區(包含相應框)的鏈接。

當內核調用一個內存分配函數時,必須指明請求頁框所在的管理區。

原子請求(GFP_ATOMIC)從不被阻塞,如果沒有足夠的空間,則僅僅是分配失敗而已。

保留內存的數量(KB)存放在min_free_kbytes變量中。它的初始值在內核初始化時設置。

參數gfp_mask是一組標誌,它指明瞭如何尋找空閒的頁框。

與直接映射的物理內存末端,高端內存的始端所對應的線性地址存放在high_memory變量中。

返回所分配頁框線性地址的頁分配器不適用於高端內存,即不適用ZPONE_HIGHMEM內存管理區內的頁框。

高端內存頁框分配只能通過alloc_pages()函數和它的快捷函數alloc_page()。這些函數不返回第一個被分配頁框的線性地址,因爲如果該頁框屬於高端內存,那麼這樣的線性地址根本不存在。取而代之,這些函數返回第一個被分配頁框的頁描述符的線性地址。

所有頁描述符一旦被分配在低端內存中,他們在內核初始化階段就不會改變。

內核可以採用三種不同的機制將頁框映射到高端內存,分別叫做永久內核映射,臨時內核映射及非連續內存分配。

建立永久內核映射可能阻塞當前的進程,這發生在空閒頁表項不存在時。因此,永久內核映射不能用於中斷處理程序和可延遲函數。

建立臨時內核映射絕不會要求阻塞當前進程,但是缺點是隻有很少的臨時內核映射可以同時建立起來。

永久內核映射使用內核頁表中i 個專門的頁表,其地址被存放在pkmap_page_table變量中,頁表中的表項數由LAST_PKMAP宏產生,該表映射的線性地址從PKMAP_BASE開始。

爲了記錄該段內存頁框與永久內核映射包含的線性地址之間的聯繫,內核使用了page_address_htable散列表。

page_address()函數返回頁框對應的線性地址,如果頁框在高端內存中並且沒有被映射,則返回NULL。

kmap()函數建立永久內核映射,如果頁框確實屬於高端內存,則調用kmap_high();

在高端內存的任一頁框都可以通過一個“窗口”映射到內核地址空間。留給臨時內核映射的窗口數是很少的。

內核必須確保同一窗口永遠不會被兩個不同的控制路徑同時使用。因此,km_type結構中的那個符號只能由一種內核成分使用,並以該成分命名。最後一個符號KM_TYPE_NR本身並不表示一個線性地址,但由每個CPU用來產生不同的可用窗口數。

爲了建立臨時內核映射,內核調用kmap_atomic()函數。

Linux採用著名的夥伴系統算法來解決外碎片問題,把所有的空閒頁框分組爲11塊鏈表,每個塊鏈表分別包含大小1,2,4,8,16,32,64,128,256,512,1024個連續的頁框。對1024個頁框的最大請求對應着4MB大小的連續RAM塊。

LRU鏈表是統稱:細分爲活動鏈表,非活動鏈表;鏈表中存放的是進程用戶態地址空間或者頁高速緩存的所有頁。前者是最近被訪問過的頁,後者是一段時間內未曾被訪問過的頁。

每個夥伴系統使用的主要數據結構:mem_map數組,free_area數組(該元素的free_list數組的第k個元素標識所有大小爲2^k的空閒塊,該鏈表包含每個空閒頁框塊的起始頁框的頁描述符,指向鏈表中相鄰元素的指針存放在頁描述符的lru字段中)。最後,一個2^k的空閒頁塊的第一個頁描述符的private字段存放了塊的order,也就是數字k。

__rmqueue();函數用來在管理區中找到一個空閒塊。該函數需要兩個參數,管理區描述符的地址和order。

__rmqueue()函數假設調用者已經禁止了本地中斷並獲得了保護夥伴系統數據結構的zone->lock自旋鎖。

__free_pcppages_bulk()函數按照夥伴系統的策略釋放頁框。

爲了提升系統性能,每個內存管理區定義了一個“每CPU”頁框高速緩存。所有“每CPU”高速緩存包含一些預先分配的頁框,他們被用於滿足本地CPU發出的單一內存請求。

實現每CPU頁框高速緩存的主要數據結構是存放在(zone)內存管理區描述符pageset字段中的一個per_cpu_pageset數組數據結構。

buffered_rmqueue()函數在指定的內存管理區中分配頁框,它使用每CPU頁框高速緩存來處理單一頁框請求。

爲了釋放單個頁框到每CPU頁框高速緩存,內核使用free_hot_page()和free_cold_page()函數。

管理區分配器是內核頁框分配器的前端,該構件必須分配必須分配一個包含足夠多空閒頁框的內存區。

夥伴系統算法採用頁框作爲基本內存區,這適合於對大塊內存。

內核函數傾向於反覆請求同一類型的內存區。

slab分配器吧對象分組放進高速緩存,每個高速緩存都是同類線性對象的一種“儲備”。

包含高速緩存的主內存區被劃分爲多個slab,每個slab由一個多多個連續的頁框組成,這些頁框中既包含已分配的對象,也包含空閒的對象。

每個高速緩存都是由kmem_cache類型的數據結構來描述。

高速緩存被分爲兩種類型,普通和專用,普通高速緩存只由slab分配器用於自己的目的(kmem_cache),而專用高速緩存由內核的其餘部分使用。

在系統初始化期間調用kmem_cache_init()和kmem_sizes_init()來建立普通高速緩存。

專用高速緩存是由kmem_cache_create()函數創建的。

爲了避免浪費內存空間,內核必須在撤銷高速緩存本身之前就撤銷其所有的slab。kmem_cache_shrink()函數通過反覆調用slab_destroy()撤銷高速緩存中所有的slab.

所有普通和專用高速緩存的名字都可以在運行期間通過讀取/proc/slabinfo文件得到。

當slab分配器創建新的slab時,它依賴頁框分配器來獲得一組連續的空閒頁框,爲了達到此目的,它調用kmem_getpages()函數。

在相反的操作中,通過調用kmem_freepages()函數可以釋放分配給slab的頁框。

一個新創建的高速緩存沒有包含任何slab,因此也沒有空閒的對象。

只由當條件都爲真(1.已發出一個分配對象的請求 2.高速緩存不包含任何空閒對象)纔給高速緩存分配slab。

slab分配器通過調用cache_grow()函數給高速緩存分配一個新的slab,而這個函數調用kmem_getpages()從分區頁表分配器獲得一組頁框來存放一個單獨的slab,然後又調用alloc_slabmgmt()獲得一個新的slab描述符。

只有當頁框空閒時夥伴系統的函數纔會使用lru字段,而只要涉及夥伴系統,slab分配器函數所處理的頁框就不空閒並將PG_slab標誌置位。

每個對象都有類似kmem_bufctl_t的一個描述符。對象描述符存放在一個數組中,位於相應的slab描述符後。

對象描述符只不過是一個無符號整數,只由在對象空閒時纔有意義。它包含的是下一個空閒對象在slab中的下標,因此實現了slab內部空閒對象的一個簡單鏈表。

slab分配器所管理的對象可以在內存中進行對齊,也就是存放他們的內存單元的起始物理地址是一個給定常量的倍數,通常是2的倍數,該常量也叫對齊因子。

slab分配器所允許的最大對齊因子是4096,即頁框大小。

同一硬件高速緩存行可以映射RAM中很多不同塊。

相同大小的對象傾向於存放在高速緩存內相同的偏移量處。因此,不同的slab內具有相同偏移量的對象最終很可能映射在同一高速緩存行中。

高速緩存描述符的array字段是一組向array_cache數據結構的指針,系統中的每個CPU對應於一個元素,每個array_cache數據結構是空閒對象的本地高速緩存的一個描述符。

本地高速緩存描述符並不包含本地高速緩存本身的地址,本地高速緩存存放的是指向釋放對象的指針,而不是對象本身。

當創建一個新的slab高速緩存時,kmem_cache_create()函數決定本地高速緩存的大小,分配本地高速緩存,並將它們的指針存放在高速緩存的array字段。

通過調用kmem_cache_alloc()函數就可以獲得新對象。

如果對存儲區的請求訪問不頻繁,就用一組普通高速緩存來處理,普通高速緩存中的對象具有幾何分佈的大小。調用kmalloc()函數就可以得到這種類型的對象。

保留的內存池只能用於滿足中斷處理程序或者內存臨界區發出的原子內存分配請求,而內存池是動態內存的設備,只能被特定的內核成分(池的擁有者)使用。

一個內存池常常疊加在slab分配器之上,內存池能被用來分配任何一種類型的動態內存,從整個頁框到使用kmalloc()分配的小內存區。

當內存元素是slab對象時,alloc和free對象一般由mempool_alloc_slab()和mempool_free_slab()函數實現。在這種情況下,mempool_t對象的pool_data字段存放了slab高速緩存描述符的地址。

mempool_create()函數創建一個新的內存池,它接受的參數爲內存元素的個數min_nr,實現alloc和free方法的函數地址和賦給pol_data字段的任意值。

爲了從內存池分配一個元素,內核調用mempool_alloc()函數,將mmepool_t對象的地址和內存分配標誌傳遞給它。

把內存映射到一組連續的頁框是最好的選擇,這樣會充分利用高速緩存並獲得較低的平均訪問時間。

在物理內存映射的末尾與第一個內存區域之間插入一個大小爲VMALLOC_OFFSET的安全區,目的是爲了捕獲對內存的越界訪問。


每個非連續內存區都對應着一個類型爲vm_struct的描述符。

get_vm_area()函數在線性地址VMALLOC_START和VMALLOC_END之間查找一個空閒區域,該函數使用兩個參數,被創建內存區的字節大小和指定空閒區類型。

map_vm_area()並不觸及當前進程的頁表。因此,當內核態的進程訪問非連續內存區域時,缺頁發生,因爲該內存區所對應的進程頁表的表項爲空。然而,缺頁處理程序要檢查這個缺頁線程地址是否在主內核頁表中(init_mm.pgd頁全局目和它的子頁表)。一旦處理程序發現一個主內核頁表包含有這個線性地址的非空項,就把它的值拷貝到相應的進程頁表項中,並恢復進程的正常執行。

內核永遠也不會收回紮根於在內核頁全局目錄中的頁上級目錄,頁中間目錄和頁表。


DMA忽略分頁單元而直接訪問地址總線,因此,所請求的緩衝區就必須位於連續的頁框中。

頻繁的修改頁表勢必導致平均訪問內存次數的增加,因爲這會使CPU頻繁地刷新轉換後援緩衝器TLB的內容。


__get_free_page()或_alloc_page()從分區頁框分配器中獲得頁框,kmem_cache_alloc()或kmem_alloc()使用slab分配器爲專用或通用對象分配塊,而vmallc()或vmalloc_32()獲得一塊非連續的內存區域,如果所請求的內存區得到滿足,這些函數都返回一個頁描述符地址或線性地址(即所分配動態內存的起始地址)。


發佈了41 篇原創文章 · 獲贊 8 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章