提要:本系列文章主要參考MIT 6.828課程
以及兩本書籍《深入理解Linux內核》
《深入Linux內核架構》
對Linux內核內容進行總結。
內存管理的實現覆蓋了多個領域:
- 內存中的物理內存頁的管理
- 分配大塊內存的伙伴系統(tǒng)
- 分配較小內存的slab、slub、slob分配器
- 分配非連續(xù)內存塊的vmalloc分配器
- 進程的地址空間
內存管理實際分配的是物理內存頁,因此,了解物理內存分布是十分必要的。
物理內存布局
在初始化階段,內核必須建立一個物理地址映射來指定哪些物理地址范圍對內核可用而哪些不可用(或者因為它們映射硬件設備I/O的共享內存,或者因為相應的頁框含有BIOS數據)。
內核將下列頁框記為保留:
- 在不可用的物理地址范圍內的頁框
- 含有內核代碼和已初始化的數據結構的頁框。
保留頁框中的頁決不能被動態(tài)分配或交換到磁盤上。
例如,MIT 6.828 Lab2 -> Part 1: Physical Page Management -> page_init() 方法的實現中,注釋如下:
// The example code here marks all physical pages as free.
// However this is not truly the case. What memory is free?
// 1) Mark physical page 0 as in use.
// This way we preserve the real-mode IDT and BIOS structures
// in case we ever need them. (Currently we don't, but...)
// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
// is free.
// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
// never be allocated.
// 4) Then extended memory [EXTPHYSMEM, ...).
// Some of it is in use, some is free. Where is the kernel
// in physical memory? Which pages are already in use for
// page tables and other data structures?
MIT 6.828中,主機的物理內存布局如下:
+------------------+ <- 0xFFFFFFFF (4GB)
| 32-bit |
| memory mapped |
| devices |
| |
/\/\/\/\/\/\/\/\/\/\
/\/\/\/\/\/\/\/\/\/\
| |
| Unused |
| |
+------------------+ <- depends on amount of RAM
| |
| |
| Extended Memory |
| |
| |
+------------------+ <- 0x00100000 (1MB)
| BIOS ROM |
+------------------+ <- 0x000F0000 (960KB)
| 16-bit devices, |
| expansion ROMs |
+------------------+ <- 0x000C0000 (768KB)
| VGA Display |
+------------------+ <- 0x000A0000 (640KB)
| |
| Low Memory |
| |
+------------------+ <- 0x00000000
注釋中的(1)(3)
這兩項就是不可用的物理地址范圍內的頁框
,而(4)
就指的是含有內核代碼和已初始化的數據結構的頁框
。一般來說,Linux內核安裝在RAM中從物理地址0x00100000
(1MB)開始的地方(詳見https://www.cnblogs.com/yanlishao/p/17557668.html),當然這只是一個固定的地址。那么為何內核沒有安裝在RAM的第一MB開始的地方呢?由上圖可以看出,第一個MB中很多內存由BIOS和硬件使用,因此不是連續(xù)的,會被切分成很多小塊。具體描述如下:
- 頁框0由BIOS使用,存放加電自檢(Power-On Self-Test,POST)期間檢查到的系統(tǒng)硬件配置。因此,很多膝上型電腦的BIOS甚至在系統(tǒng)初始化后還將數據寫到該頁框。
- 物理地址從0x000a0000到0x000ffff的范圍通常留給BIOS例程,并且映射ISA圖形卡上的內部內存。這個區(qū)域就是所有IBM兼容PC上從640KB到1MB之間著名的洞:物理地址存在但被保留,對應的頁框不能由操作系統(tǒng)使用。
- 第一個MB內的其他頁框可能由特定計算機模型保留。例如,IBM Thinkpnd把0xa0頁框映射到0x9f頁框。
但是由于各種硬件和BIOS不同,因此,保留內存也有差異,因此新近的計算機中,內核調用BIOS過程建立一組物理地址范圍和其對應的內存類型,最后映射成一張[start,end, TYPE]的表,TYPE表示內存是否可用。在系統(tǒng)啟動時,找到的內存區(qū)由內核函數print_memory_map
顯示。形如下表:
最后,下圖給出物理內存最低幾M字節(jié)的布局:
前1MB的內存布局在前面已經介紹過,為了避免把內核裝入一組不連續(xù)的頁框里,Linux更愿跳過RAM的第一個MB。因此,內核會裝載在物理地址0x00100000
開始的地方。
- _text和 _etext是代碼段的起始和結束地址,包含了編譯后的內核代碼。
- 數據段位于 _etext和 _edata之間,保存了大部分內核變量。
- 初始化數據在內核啟動過程結束后不再需要(例如,包含初始化為0的所有靜態(tài)全局變量的BSS段)保存在最后一段,從_edata到_end。在內核初始化完成后,其中的大部分數據都可以從內存刪除,給應用程序留出更多空間。這一段內存區(qū)劃分為更小的子區(qū)間,以控制哪些可以刪除,哪些不能刪除,但這對于我們現在的討論沒多大意義。
在運行內核時,可以通過/proc/iomem
獲得內核的相關信息:
共享存儲型多處理機模型
本部分引用知乎大佬的文章,原鏈接
進行內存管理,一個很重要的因素就是CPU對內存的訪問方式,所以難免要對此部分進行介紹。
共享存儲型多處理機有兩種模型:
- 均勻存儲器存?。║niform-Memory-Access,簡稱UMA)模型 (一致存儲器訪問結構)
- 非均勻存儲器存取(Nonuniform-Memory-Access,簡稱NUMA)模型 (非一致存儲器訪問結構)
UMA模型
UMA模型將多個處理機與一個集中的存儲器和I/O總線相連,物理存儲器被所有處理機均勻共享,所有處理機對所有的存儲單元都具有相同的存取時間
。SMP(對稱型多處理機)系統(tǒng)有時也被稱之為一致存儲器訪問(UMA)結構體系。
UMA模型的最大特點就是共享
。在該模型下,所有資源都是共享的,包括CPU、內存、I/O等。也正是由于這種特性,導致了UMA模型可伸縮性非常有限
,因為內存是共享的,CPUs都會通過一條內存總線連接到內存上,這時,當多個CPU同時訪問同一個內存塊時就會產生沖突,因此當存儲器和I/O接口達到飽和的時候,增加處理器并不能獲得更高的性能。
NUMA模型
NUMA模型的基本特征是具有多個CPU模塊
,每個CPU模塊
又由多個CPU core(如4個)組成
,并具有本地內存、I/O接口等,所以可以支持CPU對本地內存的快速訪問
。這里我們把CPU模塊稱為節(jié)點,每個節(jié)點被分配有本地存儲器,各個節(jié)點之間通過總線連接起來,這樣可以支持對其他節(jié)點中的本地內存的訪問,當然這時訪問遠的內存就要比訪問本地內存慢些。所有節(jié)點中的處理器都能夠訪問全部的物理存儲器。
NUMA模型的最大優(yōu)勢是伸縮性
。與UMA不同的是,NUMA具有多條內存總線,可以通過限制任何一條內存總線上的CPU數量以及依靠高速互連來連接各個節(jié)點,從而緩解UMA的瓶頸。NUMA理論上可以無限擴展的,但由于訪問遠地內存的延時遠遠超過訪問本地內存,所以當CPU數量增加時,系統(tǒng)性能無法線性增加。
內核處理
內核對一致和非一致內存訪問系統(tǒng)使用相同的數據結構,因此針對各種不同形式的內存布局,各個算法幾乎沒有什么差別。在UMA系統(tǒng)上,只使用一個NUMA結點來管理整個系統(tǒng)內存。而內存管理的其他部分則相信它們是在處理一個偽NUMA系統(tǒng)。
根據對NUMA模型的描述,Linux將內存進行了如下劃分:
- 存儲節(jié)點(Node):是每個CPU對應的一個本地內存,在內核中表示為
pg_data_t
的實例。因為CPU被劃分為多個節(jié)點,內存被劃分為簇,每個CPU都對應一個本地物理內存,即一個CPU Node對應一個內存簇bank,即每個內存簇被認為是一個存儲節(jié)點。在UMA結構下,只存在一個存儲節(jié)點。 - 內存域(Zone):由于硬件制約,每個物理內存節(jié)點Node被劃分為多個內存域, 用于表示不同范圍的內存, 內核可以使用不同的映射方式映射物理內存。
- 頁面(Page):各個內存域都關聯(lián)一個數組,用來組織屬于該內存域的物理內存頁(頁幀)。頁面是最基本的頁面分配的單位
接下來對這3層數據結構進行簡單介紹。
Node — pg_data_t
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
struct page *node_mem_map;
struct bootmem_data *bdata;
unsigned long node_start_pfn;
unsigned long node_present_pages; /* 物理內存頁的總數 */
unsigned long node_spanned_pages; /* 物理內存頁的總長度,包含洞在內 */
int node_id;
struct pglist_data *pgdat_next;
wait_queue_head_t kswapd_wait;
struct task_struct *kswapd;
int kswapd_max_order;
} pg_data_t;
屬性名 | 描述 |
---|---|
node_zones | 包含了結點中各內存域的數據結構 |
node_zonelists | 指定了備用結點及其內存域的列表,以便在當前結點沒有可用空間時,在備用結點分配內存(這個字段將在后面介紹頁分配器 時再提到) |
nr_zones | 結點中管理區(qū)的個數 |
node_mem_map | 指向page實例數組的指針,用于?述結點的所有物理內存頁。它包含了結點中所有內存域的頁。 |
bdata | 在系統(tǒng)啟動期間,內存管理子系統(tǒng)初始化之前,內核也需要使用內存(另外,還必須保留部分內存用于初始化內存管理子系統(tǒng))。為解決這個問題,內核使用了自舉內存分配(boot memory allocator)。bdata指向自舉內存分配器數據結構的實例 |
node_start_pfn | 該NUMA結點第一個頁幀的邏輯編號。系統(tǒng)中 結點的頁幀是依次編號的,每個頁幀的號碼都是全局唯一的(不只是結點內唯一)。node_start_pfn在UMA系統(tǒng)中總是0,因為其中只有一個結點,因此其第一個頁幀編號總是0。 node_present_pages指定了結點中頁幀的數目,而node_spanned_pages則給出了該結點以頁幀為單位計算的長度。二者的值不一定相同,因為結點中可能有一些空洞,并不對應真正的頁幀 |
node_id | 全局結點ID。系統(tǒng)中的NUMA結點都從0開始編號 |
pgdat_next | 連接到下一個內存結點,系統(tǒng)中所有結點都通過單鏈表連接起來,其末尾通過空指針標記。 |
kswapd_wait | 交換守護進程(swap daemon)的等待隊列,在將頁幀換出結點時會用到。kswapd指向負責該結點的交換守護進程的task_struct。kswapd_max_order用于頁交換子系統(tǒng)的實現,來定義需要釋放的區(qū)域的長度(我們當前不感興趣)。 |
注意:
- 所有結點的描述符存放在一個單向鏈表中,其首結點為pgdat_list。如果采用的是UMA模型,那么這唯一一個元素還會被放在contig_page_data變量中。
- 結點的內存域保存在node_zones[MAX_NR_ZONES]。該數組總是有3個項(這3個項是什么,看下一小節(jié)),即使結點沒有那么多內存域,也是如此。如果不足3個,則其余的數組項用0填充
如果系統(tǒng)中結點多于一個,內核會維護一個位圖,用以?供各個結點的狀態(tài)信息。狀態(tài)是用位掩碼指定的,可使用下列值:
enum node_states {
N_POSSIBLE, /* 結點在某個時候可能變?yōu)槁?lián)機 */
N_ONLINE, /* 結點是聯(lián)機的 */
N_NORMAL_MEMORY, /* 結點有普通內存域 */
#ifdef CONFIG_HIGHMEM
N_HIGH_MEMORY, /* 結點有普通或高端內存域 */
#else
N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
N_CPU, /* 結點有一個或多個CPU */
NR_NODE_STATES
};
Zone - zone
在之前討論了,由于計算機體系結構有硬件的制約,這限制了頁框可以使用的方式,尤其是,Linux內核必須處理80x86體系結構的兩種硬件約束:
- ISA總線的直接內存存取(DMA)處理器有一個嚴格的限制:它們只能對RAM的前16MB尋址。
- 在具有大容量RAM的現代32位計算機中,CPU不能直接訪問所有的物理內存,因為線性地址空間太小。
為了應對這兩種限制,Linux2.6把每個內存節(jié)點的物理內存劃分為3個管理區(qū)(zone),在80x86UMA體系結構中的管理區(qū)為:
名稱 | 描述 |
---|---|
ZONE_DMA | 包含低于16MB的內存頁框 |
ZONE_DMA32 | 使用32位地址字可尋址、適合DMA的內存域。顯然,只有在64位系統(tǒng)上,兩種DMA內存域才有差別 |
ZONE_NORMAL | 包含高于16MB且低于896MB的內存頁框 |
ZONE_HIGHMEM | 包含從896MB開始高于896MB的內存頁框 |
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
MAX_NR_ZONES
};
這里我們了解了16MB這個界限的來歷,那么896MB這個界限是怎么回事呢??
Linux默認按照3:1的比例將地址空間劃分給用戶態(tài)和內核態(tài),32位操作系統(tǒng)上內核空間的大小就是1GB(4GB按照3:1劃分),內核空間又分為各個段,如下圖:
直接映射區(qū)域從0xC0000000到high_memory地址,high_memory通常為896MB
。high_memory由下面的宏具體決定,該
//arch/x86/kernel/setup_32.c
static unsigned long __init setup_memory(void)
{
...
#ifdef CONFIG_HIGHMEM
high_memory = (void *) __va(highstart_pfn * PAGE_SIZE -1) + 1;
#else
high_memory = (void *) __va(max_low_pfn * PAGE_SIZE -1) + 1;
#endif
...
}
max_low_pfn
指定了物理內存數量小于896 MiB的系統(tǒng)上內存頁的數目。該值的上界受限于896 MiB可容納的最大頁數(具體的計算在find_max_low_pfn給出),因此high_memory通常為896MB。剩下的128MB通常用作如下用途(如下三種用途的具體介紹有機會會仔細講解):
- 虛擬內存中連續(xù)、但物理內存中不連續(xù)的內存區(qū),可以在vmalloc區(qū)域分配
- 持久映射用于將高端內存域中的非持久頁映射到內核中
- 固定映射是與物理地址空間中的固定頁關聯(lián)的虛擬地址空間項,但具體關聯(lián)的頁幀可以自由選擇。
可以看到直接映射的最大空間長度為896MB,如果物理內存超過896MB,則內核無法直接映射全部內存。
最后看一下內存中表示Zone的數據結構zone,其各個字段所代表的含義:
struct zone {
/*通常由頁分配器訪問的字段 */
unsigned long pages_min, pages_low, pages_high;
unsigned long lowmem_reserve[MAX_NR_ZONES];
struct per_cpu_pageset pageset[NR_CPUS];
/*
* 不同長度的空閑區(qū)域
*/
spinlock_t lock;
struct free_area free_area[MAX_ORDER];
ZONE_PADDING(_pad1_)
/* 通常由頁面收回掃描程序訪問的字段 */
spinlock_t ru_lock;
struct list_head active_list;
struct list_head inactive_list;
unsigned long nr_scan_active;
unsigned long nr_scan_inactive;
unsigned long pages_scanned; /* 上一次回收以來掃? 過的頁 */
unsigned long flags; /* 內存域標志,見下文 */
/* 內存域統(tǒng)計量 */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
int prev_priority;
ZONE_PADDING(_pad2_)
/* 很少使用或大多數情況下只讀的字段 */
wait_queue_head_t * wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
/* 支持不連續(xù)內存模型的字段。 */
struct pglist_data *zone_pgdat;
unsigned long zone_start_pfn;
unsigned long spanned_pages; /* 總長度,包含空洞 */
unsigned long present_pages; /* 內存數量(除去空洞) */
/*
* 很少使用的字段:
*/
char *name;
} ____cacheline_maxaligned_in_smp;
這里主要介紹內存管理的核心字段,將在后面比較頻繁的出現,對于其他的字段,我們使用到的時候再進行介紹。
- pages_min、pages_high、pages_low是頁換出時使用的
watermark
,也就是一些界限。如果內存不足,內核可以將頁寫到硬盤。這3個成員會影響交換守護進程的行為。- 如果空閑頁多于pages_high,則內存域的狀態(tài)是理想的。
- 如果空閑頁的數目低于pages_low,則內核開始將頁換出到硬盤。
- 如果空閑頁的數目低于pages_min,那么頁回收工作的壓力就比較大,因為內存域中急需空閑頁。
- lowmem_reserve數組分別為各種內存域指定了若干頁,
用于一些無論如何都不能失敗的關鍵性內存分配
。 - pageset是一個數組,
用于實現每個CPU的熱/冷頁幀列表
。內核使用這些列表來保存可用于滿足實現的“新鮮”頁。但冷熱頁幀對應的高速緩存狀態(tài)不同:- 有些頁幀也很可能仍然在高速緩存中,因此可以快速訪問,故稱之為熱的;
- 未緩存的頁幀與此相對,故稱之為冷的。
- free_area是同名數據結構的數組,用于實現伙伴系統(tǒng)
Page - page
由于我們現在討論的都是對物理內存的管理,即頁框的管理,那么記錄頁框的狀態(tài)是無法避免的,因此,操作系統(tǒng)提供了頁描述符用于完成該任務,即struct page
結構體。
struct page {
unsigned long flags; /* 原子標志,有些情況下會異步更新 */
atomic_t _count; /* 使用計數,見下文。 */
union {
atomic_t _mapcount; /* 內存管理子系統(tǒng)中映射的頁表項計數,
* 用于表示頁是否已經映射,還用于限制逆向映射搜索。
*/
unsigned int inuse; /* 用于SLUB分配器:對象的數目 */
};
union {
struct {
unsigned long private; /* 由映射私有,不透明數據:
* 如果設置了PagePrivate,通常用于
buffer_heads;
* 如果設置了PageSwapCache,則用于
swp_entry_t;
* 如果設置了PG_buddy,則用于表示伙伴系統(tǒng)中的
階。
*/
struct address_space *mapping; /* 如果最低位為0,則指向inode
* address_space,或為NULL。
* 如果頁映射為匿名內存,最低位置位,
* 而且該指針指向anon_vma對象:
* 參見下文的PAGE_MAPPING_ANON。
*/
};
...
struct kmem_cache *slab; /* 用于SLUB分配器:指向slab的指針 */
struct page *first_page; /* 用于復合頁的尾頁,指向首頁 */
};
union {
pgoff_t index; /* 在映射內的偏移量 */
void *freelist; /* SLUB: freelist req. slab lock */
};
struct list_head lru; /* 換出頁列表,例如由zone->lru_lock保護的active_list!
*/
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* 內核虛擬地址(如果沒有映射則為NULL,即高端內存) */
#endif /* WANT_PAGE_VIRTUAL */
};
這里我們介紹一些對我們討論的內容比較重要的屬性。
屬性名 | 描述 |
---|---|
flags | 包含多達32個用來描述頁框狀態(tài)的標志 |
_count | 頁的引用計數器(這里兩本參考書有分歧,不做具體描述) |
_mapcount | 表示在頁表中有多少項指向該頁 |
lru | 是一個表頭,用于在各種鏈表上維護該頁,以便將頁按不同類別分組,最重要的類別是活動和不活動頁。 |
private | 指向“私有”數據的指針,虛擬內存管理會忽略該數據。根據頁的用途,可以用不同的方式使用該指針 |
virtual | l用于高端內存區(qū)域中的頁,換言之,即無法直接映射到內核內存中的頁。virtual用于存儲該頁的虛擬地址 |
在本部分的最后,給出一個flags標志中可選標志的表,方便后期查看。
文章來源:http://www.zghlxwxcb.cn/news/detail-639950.html
總結
本節(jié)主要講解了物理內存的布局以及內存管理模塊的主要數據結構,下一節(jié)我們會描述內核在啟動時是如何初始化這些結構的,為后面講解內存分配算法做準備。文章來源地址http://www.zghlxwxcb.cn/news/detail-639950.html
到了這里,關于深入理解Linux內核——內存管理(2)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!