1.????x86的物理地址空間布局
物理地址空間的頂部以下一段空間,被PCI設(shè)備的I/O內(nèi)存映射占據(jù),它們的大小和布局由PCI規(guī)范所決定。640K~1M這段地址空間被BIOS和VGA適配器所占據(jù)。
由于這兩段地址空間的存在,導(dǎo)致相應(yīng)的RAM空間不能被CPU所尋址(當(dāng)CPU訪問(wèn)該段地址時(shí),北橋會(huì)自動(dòng)將目的物理地址“路由”到相應(yīng)的I/O設(shè)備上,不會(huì)發(fā)送給RAM),從而形成RAM空洞。
當(dāng)開(kāi)啟分段分頁(yè)機(jī)制時(shí),典型的x86尋址過(guò)程為
內(nèi)存尋址的工作是由Linux內(nèi)核和MMU共同完成的,其中Linux內(nèi)核負(fù)責(zé)cr3,gdtr等寄存器的設(shè)置,頁(yè)表的維護(hù),頁(yè)面的管理,MMU則進(jìn)行具體的映射工作。
2.????Linux的內(nèi)存管理
Linux采用了分頁(yè)的內(nèi)存管理機(jī)制。由于x86體系的分頁(yè)機(jī)制是基于分段機(jī)制的,因此,為了使用分頁(yè)機(jī)制,分段機(jī)制是無(wú)法避免的。為了降低復(fù)雜性,Linux內(nèi)核將所有段的基址都設(shè)為0,段限長(zhǎng)設(shè)為4G,只是在段類型和段訪問(wèn)權(quán)限上有所區(qū)分,并且Linux內(nèi)核和所有進(jìn)程共享1個(gè)GDT,不使用LDT(即系統(tǒng)中所有的段描述符都保存在同一個(gè)GDT中),這是為了應(yīng)付CPU的分段機(jī)制所能做的最少工作。
Linux內(nèi)存管理機(jī)制可以分為3個(gè)層次,從下而上依次為物理內(nèi)存的管理、頁(yè)表的管理、虛擬內(nèi)存的管理。
3.????頁(yè)表管理
為了保持兼容性,Linux最多支持4級(jí)頁(yè)表,而在x86上,實(shí)際只用了其中的2級(jí)頁(yè)表,即PGD(頁(yè)全局目錄表)和PT(頁(yè)表),中間的PUD和PMD所占的位長(zhǎng)都是0,因此對(duì)于x86的MMU是不可見(jiàn)的。
在內(nèi)核源碼中,分別為PGD,PUD,PMD,PT定義了相應(yīng)的頁(yè)表項(xiàng),即
(定義在include/asm-generic/page.h中)
typedef struct {unsigned long pgd;} pgd_t;
typedef struct {unsigned long pud;} pud_t;
typedef struct {unsigned long pmd;} pmd_t;
typedef struct {unsigned long pte;} pte_t;
為了方便的操作頁(yè)表項(xiàng),還定義了以下宏:
(定義在arch/x86/include/asm/pgtable.h中)
mk_pte
pgd_page/pud_page/pmd_page/pte_page
pgd_alloc/pud_alloc/pmd_alloc/pte_alloc
pgd_free/pud_free/pmd_free/pte_free
set_pgd/ set_pud/ set_pmd/ set_pte
…
4.????物理內(nèi)存管理
Linux內(nèi)核是以物理頁(yè)面(也稱為page frame)為單位管理物理內(nèi)存的,為了方便的記錄每個(gè)物理頁(yè)面的信息,Linux定義了page結(jié)構(gòu)體:
(位于include/linux/mm_types.h)
struct page {
unsigned long flags;?????????
atomic_t _count;???????
union {
atomic_t _mapcount;??????
struct {????????? /* SLUB */
u16 inuse;
u16 objects;
};
};
union {
struct {
unsigned long private;????????????
struct address_space *mapping;???
};
struct kmem_cache *slab;????? /* SLUB: Pointer to slab */
struct page *first_page;? /* Compound tail pages */
};
union {
pgoff_t index;???????????? /* Our offset within mapping. */
void *freelist;???????????? /* SLUB: freelist req. slab lock */
};
struct list_head lru;??????????
…
};
Linux系統(tǒng)在初始化時(shí),會(huì)根據(jù)實(shí)際的物理內(nèi)存的大小,為每個(gè)物理頁(yè)面創(chuàng)建一個(gè)page對(duì)象,所有的page對(duì)象構(gòu)成一個(gè)mem_map數(shù)組。
進(jìn)一步,針對(duì)不同的用途,Linux內(nèi)核將所有的物理頁(yè)面劃分到3類內(nèi)存管理區(qū)中,如圖,分別為ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA的范圍是0~16M,該區(qū)域的物理頁(yè)面專門供I/O設(shè)備的DMA使用。之所以需要單獨(dú)管理DMA的物理頁(yè)面,是因?yàn)镈MA使用物理地址訪問(wèn)內(nèi)存,不經(jīng)過(guò)MMU,并且需要連續(xù)的緩沖區(qū),所以為了能夠提供物理上連續(xù)的緩沖區(qū),必須從物理地址空間專門劃分一段區(qū)域用于DMA。
ZONE_NORMAL的范圍是16M~896M,該區(qū)域的物理頁(yè)面是內(nèi)核能夠直接使用的。
ZONE_HIGHMEM的范圍是896M~結(jié)束,該區(qū)域即為高端內(nèi)存,內(nèi)核不能直接使用。
內(nèi)存管理區(qū)
內(nèi)核源碼中,內(nèi)存管理區(qū)的結(jié)構(gòu)體定義為
struct zone {
...
struct free_area? free_area[MAX_ORDER];
...
spinlock_t??????????? lru_lock;??????
struct zone_lru {
struct list_head list;
} lru[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;
unsigned long???????????? pages_scanned;? ?? /* since last reclaim */
unsigned long???????????? flags;???? ?????? ?? /* zone flags, see below */
atomic_long_t??????????? vm_stat[NR_VM_ZONE_STAT_ITEMS];
unsigned int inactive_ratio;
...
wait_queue_head_t?? * wait_table;
unsigned long???????????? wait_table_hash_nr_entries;
unsigned long???????????? wait_table_bits;
...
struct pglist_data?????? *zone_pgdat;
unsigned long???????????? zone_start_pfn;
...
};
其中zone_start_pfn表示該內(nèi)存管理區(qū)在mem_map數(shù)組中的索引。
內(nèi)核在分配物理頁(yè)面時(shí),通常是一次性分配物理上連續(xù)的多個(gè)頁(yè)面,為了便于快速的管理,內(nèi)核將連續(xù)的空閑頁(yè)面組成空閑區(qū)段,大小是2、4、8、16…等,然后將空閑區(qū)段按大小放在不同隊(duì)列里,這樣就構(gòu)成了MAX_ORDER個(gè)隊(duì)列,也就是zone里的free_area數(shù)組。這樣在分配物理頁(yè)面時(shí),可以快速的定位剛好滿足需求的空閑區(qū)段。這一機(jī)制稱為buddy system。
當(dāng)釋放不用的物理頁(yè)面時(shí),內(nèi)核并不會(huì)立即將其放入空閑隊(duì)列(free_area),而是將其插入非活動(dòng)隊(duì)列l(wèi)ru,便于再次時(shí)能夠快速的得到。每個(gè)內(nèi)存管理區(qū)都有1個(gè)inacitive_clean_list。另外,內(nèi)核中還有3個(gè)全局的LRU隊(duì)列,分別為active_list,inactive_dirty_list和swapper_space。其中active_list用于記錄所有被映射了的物理頁(yè)面,inactive_dirty_list用于記錄所有斷開(kāi)了映射且未被同步到磁盤交換文件中的物理頁(yè)面,swapper_space則用于記錄換入/換出到磁盤交換文件中的物理頁(yè)面。
物理頁(yè)面分配
分配物理內(nèi)存的函數(shù)主要有
struct page * __alloc_pages(zonelist_t *zonelist, unsigned long order);
參數(shù)zonelist即從哪個(gè)內(nèi)存管理區(qū)中分配物理頁(yè)面,參數(shù)order即分配的內(nèi)存大小。
__get_free_pages(unsigned int flags,unsigned int order);
參數(shù)flags可選GFP_KERNEL或__GFP_DMA等,參數(shù)order同上。
該函數(shù)能夠分配物理上連續(xù)的內(nèi)存區(qū)域,得到的虛擬地址與物理地址是一一對(duì)應(yīng)的。
void * kmalloc(size_t size,int flags);
該函數(shù)能夠分配物理上連續(xù)的內(nèi)存區(qū)域,得到的虛擬地址與物理地址是一一對(duì)應(yīng)的。
物理頁(yè)面回收
當(dāng)空閑物理頁(yè)面不足時(shí),就需要從inactive_clean_list隊(duì)列中選擇某些物理頁(yè)面插入空閑隊(duì)列中,如果仍然不足,就需要把某些物理頁(yè)面里的內(nèi)容寫回到磁盤交換文件里,騰出物理頁(yè)面,為此內(nèi)核源碼中為磁盤交換文件定義了:
(位于include/linux/swap.h)
struct swap_info_struct {
unsigned long????? flags;??????????? /* SWP_USED etc: see above */
signed short prio;????????????? /* swap priority of this type */
signed char? type;???????????? /* strange name for an index */
signed char? next;???????????? /* next type on the swap list */
…
unsigned char *swap_map;???? /* vmalloc'ed array of usage counts */
…
struct block_device *bdev;????? /* swap device or bdev of swap file */
struct file *swap_file;????????????? /* seldom referenced */
…
};
其中swap_map數(shù)組每個(gè)元素代表磁盤交換文件中的一個(gè)頁(yè)面,它記錄相應(yīng)磁盤交換頁(yè)面的信息(如頁(yè)面基址、所屬的磁盤交換文件),跟頁(yè)表項(xiàng)的作用類似。
回收物理頁(yè)面的過(guò)程由內(nèi)核中的兩個(gè)線程專門負(fù)責(zé),kswapd和kreclaimd,它們定期的被內(nèi)核喚醒。kswapd主要通過(guò)3個(gè)步驟回收物理頁(yè)面:
調(diào)用shrink_inactive_list ()掃描inacive_dirty_pages隊(duì)列,將非活躍隊(duì)列里的頁(yè)面寫回到交換文件中,并轉(zhuǎn)移到inactive_clean_pages隊(duì)列里。
調(diào)用shrink_slab ()回收slab機(jī)制保留的空閑頁(yè)面。
調(diào)用shrink_active_list ()掃描active_list隊(duì)列,將活躍隊(duì)列里可轉(zhuǎn)入非活躍隊(duì)列的頁(yè)面轉(zhuǎn)移到inactive_dirty_list。
5.????虛擬內(nèi)存管理
Linux虛擬地址空間布局如下
Linux將4G的線性地址空間分為2部分,0~3G為user space,3G~4G為kernel space。
由于開(kāi)啟了分頁(yè)機(jī)制,內(nèi)核想要訪問(wèn)物理地址空間的話,必須先建立映射關(guān)系,然后通過(guò)虛擬地址來(lái)訪問(wèn)。為了能夠訪問(wèn)所有的物理地址空間,就要將全部物理地址空間映射到1G的內(nèi)核線性空間中,這顯然不可能。于是,內(nèi)核將0~896M的物理地址空間一對(duì)一映射到自己的線性地址空間中,這樣它便可以隨時(shí)訪問(wèn)ZONE_DMA和ZONE_NORMAL里的物理頁(yè)面;此時(shí)內(nèi)核剩下的128M線性地址空間不足以完全映射所有的ZONE_HIGHMEM,Linux采取了動(dòng)態(tài)映射的方法,即按需的將ZONE_HIGHMEM里的物理頁(yè)面映射到kernel space的最后128M線性地址空間里,使用完之后釋放映射關(guān)系,以供其它物理頁(yè)面映射。雖然這樣存在效率的問(wèn)題,但是內(nèi)核畢竟可以正常的訪問(wèn)所有的物理地址空間了。
內(nèi)核空間布局
下面是內(nèi)核空間布局的詳細(xì)內(nèi)容,
在kernel image下面有16M的內(nèi)核空間用于DMA操作。位于內(nèi)核空間高端的128M地址主要由3部分組成,分別為vmalloc area,持久化內(nèi)核映射區(qū),臨時(shí)內(nèi)核映射區(qū)。
由于ZONE_NORMAL和內(nèi)核線性空間存在直接映射關(guān)系,所以內(nèi)核會(huì)將頻繁使用的數(shù)據(jù)如kernel代碼、GDT、IDT、PGD、mem_map數(shù)組等放在ZONE_NORMAL里。而將用戶數(shù)據(jù)、頁(yè)表(PT)等不常用數(shù)據(jù)放在ZONE_ HIGHMEM里,只在要訪問(wèn)這些數(shù)據(jù)時(shí)才建立映射關(guān)系(kmap())。比如,當(dāng)內(nèi)核要訪問(wèn)I/O設(shè)備存儲(chǔ)空間時(shí),就使用ioremap()將位于物理地址高端的mmio區(qū)內(nèi)存映射到內(nèi)核空間的vmalloc area中,在使用完之后便斷開(kāi)映射關(guān)系。
用戶空間布局
在用戶空間中,虛擬內(nèi)存和物理內(nèi)存可能的映射關(guān)系如下圖
當(dāng)RAM足夠多時(shí),內(nèi)核會(huì)將用戶數(shù)據(jù)保存在ZONE_ HIGHMEM,從而為內(nèi)核騰出內(nèi)存空間。
下面是用戶空間布局的詳細(xì)內(nèi)容,
用戶進(jìn)程的代碼區(qū)一般從虛擬地址空間的0x08048000開(kāi)始,這是為了便于檢查空指針。代碼區(qū)之上便是數(shù)據(jù)區(qū),未初始化數(shù)據(jù)區(qū),堆區(qū),棧區(qū),以及參數(shù)、全局環(huán)境變量。
虛擬內(nèi)存區(qū)段
為了管理不同的虛擬內(nèi)存區(qū)段,Linux代碼中定義了
(位于include/linux/mm_types.h)
struct vm_area_struct {
struct mm_struct * vm_mm;?? /* The address space we belong to. */
unsigned long vm_start;?? ?????? /* Our start address within vm_mm. */
unsigned long vm_end;??????????? /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
pgprot_t vm_page_prot;???????? /* Access permissions of this VMA. */
unsigned long vm_flags;????????? /* Flags, see mm.h. */
…
};
其中vm_start,vm_end定義了虛擬內(nèi)存區(qū)段的起始位置,vm_page_prot和vm_flags定義了訪問(wèn)權(quán)限等。
vm_next構(gòu)成一個(gè)鏈表,保存同一個(gè)進(jìn)程的所有虛擬內(nèi)存區(qū)段。
vm_mm指向進(jìn)程的mm_struct結(jié)構(gòu)體,它的定義為
(位于include/linux/mm_types.h)
struct mm_struct {
struct vm_area_struct * mmap;??????????? /* list of VMAs */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache;?????? /* last find_vma result */
unsigned long mmap_base;??????????? /* base of mmap area */
unsigned long task_size;????????? /* size of task vm space */
unsigned long cached_hole_size;
unsigned long free_area_cache;??????????
pgd_t * pgd;
atomic_t mm_users;??????????????? /* How many users with user space? */
atomic_t mm_count;??????????????
…
};
每個(gè)進(jìn)程只有1個(gè)mm_struct結(jié)構(gòu),保存在task_struct結(jié)構(gòu)體中。
與虛擬內(nèi)存管理相關(guān)的結(jié)構(gòu)體關(guān)系圖如下
虛擬內(nèi)存相關(guān)函數(shù)
創(chuàng)建一個(gè)內(nèi)存區(qū)段可以用
unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags);
當(dāng)給定一個(gè)虛擬地址時(shí),可以查找它所屬的虛擬內(nèi)存區(qū)段:
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);
由于所有的vm_area_struct組成了一個(gè)RB樹(shù),所以查找的速度很快。
向用戶空間中插入一個(gè)內(nèi)存區(qū)段可以用
void insert_vm_struct (struct mm_struct *mm, struct vm_area_struct *vmp);
使用以下函數(shù)可以在內(nèi)核空間分配一段連續(xù)的內(nèi)存(但在物理地址空間上不一定連續(xù)):
void *vmalloc(unsigned long size);
使用以下函數(shù)可以將ZONE_HIGHMEM里的物理頁(yè)面映射到內(nèi)核空間:
static inline void *kmap(struct page*page);
6.????內(nèi)存管理3個(gè)層次的關(guān)系
下面以擴(kuò)展用戶堆棧為例,解釋3個(gè)層次的關(guān)系。
調(diào)用函數(shù)時(shí),會(huì)涉及堆棧的操作,當(dāng)訪問(wèn)地址超過(guò)堆棧的邊界時(shí),便引起page fault,內(nèi)核處理頁(yè)面失效的過(guò)程中,涉及到內(nèi)存管理的3個(gè)層次。
? 調(diào)用expand_stack()修改vm_area_struct結(jié)構(gòu),即擴(kuò)展堆棧區(qū)的虛擬地址空間;
? 創(chuàng)建空白頁(yè)表項(xiàng),這一過(guò)程會(huì)利用mm_struct中的pgd(頁(yè)全局目錄表基址)得到頁(yè)目錄表項(xiàng)(pgd_offset()),然后計(jì)算得到相應(yīng)的頁(yè)表項(xiàng)(pte_alloc())地址;
? 調(diào)用alloc_page()分配物理頁(yè)面,它會(huì)從指定內(nèi)存管理區(qū)的buddy system中查找一塊合適的free_area,進(jìn)而得到一個(gè)物理頁(yè)面;
? 創(chuàng)建映射關(guān)系,先調(diào)用mk_pte()產(chǎn)生頁(yè)表項(xiàng)內(nèi)容,然后調(diào)用set_pte()寫入頁(yè)表項(xiàng)。
? 至此,擴(kuò)展堆?;就瓿桑脩暨M(jìn)程重新訪問(wèn)堆棧便可以成功。
可以認(rèn)為,結(jié)構(gòu)體pgd和vm_area_struct,函數(shù)alloc_page()和mk_pte()是連接三者的橋梁。
?
評(píng)論