<linux 內存管理模型>
下面這個圖將Linux內存管理基本上描述完了,但是顯得有點復雜,接下來一部分一部分的解析。
內存管理系統可以分為兩部分,分別是內核空間內存管理和用戶空間內存管理:
————內存管理子系統的職責是:進程請求內存時分配可用內存,進程釋放內存后回收內存,以及跟蹤系統內存使用情況。現代操作系統要求能夠使多個程序共享系統資源,同時要求內存限制對於開發者是透明的。在這種情況下,虛擬內存應運而生。虛擬內存可以使得進程可以訪問比實際內存大得多的空間,並且使得多個程序共享內存顯得更加有效。
————當程序從內存中取得數據的時候,需要使用地址指出需要訪問的內存位置(注意:這個地址是虛擬地址,他們組成的進程的虛擬地址空間)。每個進程都有自己的虛擬地址空間,這樣做的好處是可以防止非法讀取或覆蓋其他進程的數據(虛擬地址允許進程使用超過物理內存的內存空間,因此操作系統可以給每個進程提供獨立的虛擬線性地址空間。)
<頁>
a:作為內存管理的基本單元,頁的許多狀態需要被記錄下來(比如,內核需要知道什么時候可以被回收),因此內核為內核中的每個頁都准備了頁描述符struct page{}.系統在初始化時根據物理內存的大小建立起一個page結構數組mem_map,作為物理頁面的“倉庫”。
b:struct page
{
unsigned long flags;//32位的位圖,每一位表示頁面的一個屬性
atomic_t count ;//統計頁面正在被都少個進程使用,為0時表示可以回收。
struct list_head list;//頁表的雙向鏈表
struct address_space *mapping;
unsigned long index;
struct list_head lru;//鏈接最少使用的頁表,可能會被回收
union{
struct pte_chain *chain;
pte_addr_t direct
}pte;
unsigned long private;
#if definde (WANT_PAGE_VIRTUAL)
void *virtual;//指向頁面的虛擬地址
#endif
}
<系統在內存中的分布示意圖>
a:4G進程地址空間解析


其中kmalloc和vmalloc函數申請的空間對應着不同的區域,同時又不同的含義。
<內核空間內存管理>
a:操作系統的生命周期可以分為兩個階段:
————自舉階段:
自舉階段使用臨時內存(系統剛剛啟動的時候)
————正常運行階段:
即熊啟動完成后,系統正常運行的階段
b:正常運行階段又分為兩個部分:
————固定分配部分:
這部分是有固定的內存分配給內核代碼和數據。
————動態請求部分:
為動態內存請求分配內存,動態請求源自於進程的創建和空間的擴張。
c:內存管理區
————並非所有的內核空間的內存區域都會被公平對待,對內核中的不同內存的使用是有限制的。內存管理的區是由頁面組成的,Linux內核將內核空間分為3個內存管理區:
ZONE_DMA:用於分配DMA頁面請求
ZONE_NORMAL:具有虛擬映射的非DMA頁面區間
ZONE_HIHGMEN:高端內存區間
c:內存空間管理區描述符
————與內核管理的的所有對象一樣,每個內存管理區都有一個叫做zone的結構體,其中存放內存管理區的所有信息,記錄這內存管理區的使用情況.
struct zone {
unsigned long watermark[NR_WMARK];
unsigned long percpu_drift_mark;
unsigned longlowmem_reserve[MAX_NR_ZONES];
#ifdef CONFIG_NUMA
int node;
unsigned longmin_unmapped_pages;
unsigned longmin_slab_pages;
#endif
struct per_cpu_pageset __percpu *pageset;
spinlock_tlock;
int all_unreclaimable;
#ifdef CONFIG_MEMORY_HOTPLUG
seqlock_tspan_seqlock;
#endif
struct free_areafree_area[MAX_ORDER];
#ifndef CONFIG_SPARSEMEM
unsigned long*pageblock_flags;
#endif /* CONFIG_SPARSEMEM */
#ifdef CONFIG_COMPACTION
unsigned intcompact_considered;
unsigned intcompact_defer_shift;
#endif
ZONE_PADDING(_pad1_)
spinlock_tlru_lock;
struct zone_lru {
struct list_head list;
} lru[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;
unsigned longpages_scanned;
unsigned longflags;
atomic_long_tvm_stat[NR_VM_ZONE_STAT_ITEMS];
unsigned int inactive_ratio;
ZONE_PADDING(_pad2_)
wait_queue_head_t* wait_table;
unsigned longwait_table_hash_nr_entries;
unsigned longwait_table_bits;
struct pglist_data*zone_pgdat;
unsigned longzone_start_pfn;
unsigned longspanned_pages;
unsigned longpresent_pages;
const char*name;
} ____cacheline_internodealigned_in_smp;
d:內存管理區操作輔助函數
(2)for_each_zone()
遍歷系統中的所有內存管理區
e:頁面請求函數
------頁面是存放頁的基本內存單元(其實就是很多的頁組成了頁面),只要進程請求內存,內核只要滿足要求就會給其分配頁面。同理,只要進程不在需要頁面,內核就會將其回收。
(1)返回指向pages結構體的指針,(返回void* 類型)(該結構體對應分配的請求頁面)
alloc_page()//該函數用於請求單頁
alloc_pages()//該函數用於請求4個頁面
(2)返回32為虛擬地址,該地址是分配頁面的首地址
__get_free_page()/__get_dma_pages()
f:釋放請求頁面
__free_page()/__free_pages()
g:伙伴系統(伙伴算法)
-------每當頁面被分配和回收的時候,系統都會遇到外部碎片或內存碎片的問題(即頁面散布在內存中,即使可用頁面足夠多,但是無法分配大塊的連續頁面)。為了解決這個問題,Linux系統提供了伙伴算法。
h:伙伴算法原理
伙伴系統把內存中空閑塊組成鏈表,將不同大小的空閑內存塊組織起來(我猜測是將相同大小的組織在一起),雖然大小不一樣,但是都是2的冪次方。當系統中有進程釋放沒存的時候,伙伴系統就會搜索與所釋放塊大小相等的可用空閑內存塊,如果找到相鄰的空閑塊,就將其合並成兩倍於自身大小的塊。這種合並的塊稱為伙伴。
i:分配與釋放頁面源代碼
(1)分配頁面

由此可見,slab時間上由許多緩存組成。緩存分為"專用"和"通用"。專用緩存保存特定對象的內存區,比如各種描述符,比如進程描述符"struct task_structs".
注意:關於slab的詳細信息,見《Linux內核編程》P130
<用戶空間/進程內存管理>
-------以上討論了內核如何管理自己的內存空間,接下來討論用戶空間如何讓管理自己的內存空間。用戶進程創建后需要分配一個虛擬地址空間,並且可用通過增加或刪除地址間隔得以擴大或縮小。(地址間隔(一段地址空間):是一種內存單元,也被稱作內存范圍或內存區,把進程地址空間划分為不同的區域是有用的,不同的區域具有不同的保護方案和訪問屬性,比如".text"".data"".bss""棧""棧")。
a:task_struct
(1)每一個任務都有一個
b:mm_struct
(1)每個人物都有一個mm_struct 結構,內核用該結構表示內存地址范圍(所有的mm_struct 描述符統一放在雙向鏈表中,鏈表頭對應於0進程的mm_struct ,可以通過全局變量ini_mm來訪問該描述符)
(2)該結構部分代碼

c:vm_area_struct
----------該結構體定義了虛擬內存區域,因為對於一個進程來將,進程存在於不同的內存區,每個內存區都有對應的vm_area_struct。通常進程所使用到的虛存空間不連續,且各部分虛存空間的訪問屬性也可能不同。所以一個進程的虛存空間需要多個vm_area_struct結構來描述。在vm_area_struct結構的數目較少的時候,各個vm_area_struct按照升序排序,以單鏈表的形式組織數據(通過vm_next指針指向下一個vm_area_struct結構)。但是當vm_area_struct結構的數據較多的時候,仍然采用鏈表組織的化,勢必會影響到它的搜索速度。針對這個問題,vm_area_struct還添加了vm_avl_hight(樹高)、vm_avl_left(左子節點)、vm_avl_right(右子節點)三個成員來實現AVL樹,以提高vm_area_struct的搜索速度。
(2)結構體部分代碼

1) mmap調用實際上就是一個內存對象vma的創建過程, mmap的調用格式是:
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);
2)參數詳解
其中start是映射地址, length是映射長度, 如果flags的MAP_FIXED不被置位, 則該參數通常被忽略, 而查找進程地址空間中第一個長度符合的空閑區域;Fd是映射文件的文件句柄, offset是映射文件中的偏移地址;prot是映射保護權限, 可以是PROT_EXEC, PROT_READ, PROT_WRITE, PROT_NONE, flags則是指映射類型, 可以是MAP_FIXED, MAP_PRIVATE, MAP_SHARED, 該參數必須被指定為MAP_PRIVATE和MAP_SHARED其中之一,MAP_PRIVATE是創建一個寫時拷貝映射(copy-on-write), 也就是說如果有多個進程同時映射到一個文件上,映射建立時只是共享同樣的存儲頁面, 但是某進程企圖修改頁面內容, 則復制一個副本給該進程私用, 它的任何修改對其它進程都不可見. 而MAP_SHARED則無論修改與否都使用同一副本, 任何進程對頁面的修改對其它進程都是可見的.
3)mmap系統調用的實現過程是:
1.先通過文件系統定位要映射的文件;
2.權限檢查, 映射的權限不會超過文件打開的方式, 也就是說如果文件是以只讀方式打開, 那么則不允許建立一個可寫映射;
3.創建一個vma對象, 並對之進行初始化;
4.調用映射文件的mmap函數, 其主要工作是給vm_ops向量表賦值;
5.把該vma鏈入該進程的vma鏈表中, 如果可以和前后的vma合並則合並;
6.如果是要求VM_LOCKED(映射區不被換出)方式映射, 則發出缺頁請求, 把映射頁面讀入內存中.
5)munmap(void * start, size_t length):
該調用可以看作是mmap的一個逆過程. 它將進程中從start開始length長度的一段區域的映射關閉, 如果該區域不是恰好對應一個vma, 則有可能會分割幾個或幾個vma.
該段被稱作代碼段,存放的是程序的執行指令,擁有execute和read屬性,mm_struct 中start_code和end_code保存了text段的起始地址。
<虛擬地址轉化到物理地址>
————處理器只能操作物理地址,虛擬地址和對應的物理地址之間的轉換需要借助內核中的頁表來維護。頁表對內存中的頁面走向進行記錄,在內核運行的整個生命期間,頁表都放在內存中。Linux采用的是三級頁表的分頁機制,分別為:
a:PGD(page globle directory)
由mm_struct 中的pgd_t指定
b:PMD(page middle directory)
由數據類型pmd_t指定
c:PTE(page table)
由數據類型pte_t指定
d:三者之間的關系圖如下

e:x86體系的轉化過程詳細分析

首先將32位的虛擬地址的高10位取出來作為偏移,這個偏移加上CR3寄存器里面的一級也表基地址,就是存儲二級頁表基地址的單元的地址,根據該單元存儲的二級頁表的基地址找到頁表,然后取出32位虛擬地址的中間10位作為偏移,將二級頁表的基地址和偏移相加得到物理頁表的基地址的存儲單元的基地址,從該單元取出物理也表達的基地址加上32位虛擬地址的低12位就是物理頁表的物理地址。
<物理內存分配>
只有實實在在的去訪問虛擬地址所對應的內存時,才會分配內存,如果不訪問,則拿到的只是一個虛擬地址。
<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">
