1.????x86的物理地址空間布局
物理地址空間的頂部以下一段空間,被PCI設備的I/O內(nèi)存映射占據(jù),它們的大小和布局由PCI規(guī)范所決定。640K~1M這段地址空間被BIOS和VGA適配器所占據(jù)。
由于這兩段地址空間的存在,導致相應的RAM空間不能被CPU所尋址(當CPU訪問該段地址時,北橋會自動將目的物理地址“路由”到相應的I/O設備上,不會發(fā)送給RAM),從而形成RAM空洞。
當開啟分段分頁機制時,典型的x86尋址過程為
內(nèi)存尋址的工作是由Linux內(nèi)核和MMU共同完成的,其中Linux內(nèi)核負責cr3,gdtr等寄存器的設置,頁表的維護,頁面的管理,MMU則進行具體的映射工作。
2.????Linux的內(nèi)存管理
Linux采用了分頁的內(nèi)存管理機制。由于x86體系的分頁機制是基于分段機制的,因此,為了使用分頁機制,分段機制是無法避免的。為了降低復雜性,Linux內(nèi)核將所有段的基址都設為0,段限長設為4G,只是在段類型和段訪問權限上有所區(qū)分,并且Linux內(nèi)核和所有進程共享1個GDT,不使用LDT(即系統(tǒng)中所有的段描述符都保存在同一個GDT中),這是為了應付CPU的分段機制所能做的最少工作。
Linux內(nèi)存管理機制可以分為3個層次,從下而上依次為物理內(nèi)存的管理、頁表的管理、虛擬內(nèi)存的管理。
3.????頁表管理
為了保持兼容性,Linux最多支持4級頁表,而在x86上,實際只用了其中的2級頁表,即PGD(頁全局目錄表)和PT(頁表),中間的PUD和PMD所占的位長都是0,因此對于x86的MMU是不可見的。
在內(nèi)核源碼中,分別為PGD,PUD,PMD,PT定義了相應的頁表項,即
(定義在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;
為了方便的操作頁表項,還定義了以下宏:
(定義在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)核是以物理頁面(也稱為page frame)為單位管理物理內(nèi)存的,為了方便的記錄每個物理頁面的信息,Linux定義了page結構體:
(位于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)在初始化時,會根據(jù)實際的物理內(nèi)存的大小,為每個物理頁面創(chuàng)建一個page對象,所有的page對象構成一個mem_map數(shù)組。
進一步,針對不同的用途,Linux內(nèi)核將所有的物理頁面劃分到3類內(nèi)存管理區(qū)中,如圖,分別為ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA的范圍是0~16M,該區(qū)域的物理頁面專門供I/O設備的DMA使用。之所以需要單獨管理DMA的物理頁面,是因為DMA使用物理地址訪問內(nèi)存,不經(jīng)過MMU,并且需要連續(xù)的緩沖區(qū),所以為了能夠提供物理上連續(xù)的緩沖區(qū),必須從物理地址空間專門劃分一段區(qū)域用于DMA。
ZONE_NORMAL的范圍是16M~896M,該區(qū)域的物理頁面是內(nèi)核能夠直接使用的。
ZONE_HIGHMEM的范圍是896M~結束,該區(qū)域即為高端內(nèi)存,內(nèi)核不能直接使用。
內(nèi)存管理區(qū)
內(nèi)核源碼中,內(nèi)存管理區(qū)的結構體定義為
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)核在分配物理頁面時,通常是一次性分配物理上連續(xù)的多個頁面,為了便于快速的管理,內(nèi)核將連續(xù)的空閑頁面組成空閑區(qū)段,大小是2、4、8、16…等,然后將空閑區(qū)段按大小放在不同隊列里,這樣就構成了MAX_ORDER個隊列,也就是zone里的free_area數(shù)組。這樣在分配物理頁面時,可以快速的定位剛好滿足需求的空閑區(qū)段。這一機制稱為buddy system。
當釋放不用的物理頁面時,內(nèi)核并不會立即將其放入空閑隊列(free_area),而是將其插入非活動隊列l(wèi)ru,便于再次時能夠快速的得到。每個內(nèi)存管理區(qū)都有1個inacitive_clean_list。另外,內(nèi)核中還有3個全局的LRU隊列,分別為active_list,inactive_dirty_list和swapper_space。其中active_list用于記錄所有被映射了的物理頁面,inactive_dirty_list用于記錄所有斷開了映射且未被同步到磁盤交換文件中的物理頁面,swapper_space則用于記錄換入/換出到磁盤交換文件中的物理頁面。
物理頁面分配
分配物理內(nèi)存的函數(shù)主要有
struct page * __alloc_pages(zonelist_t *zonelist, unsigned long order);
參數(shù)zonelist即從哪個內(nèi)存管理區(qū)中分配物理頁面,參數(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ū)域,得到的虛擬地址與物理地址是一一對應的。
void * kmalloc(size_t size,int flags);
該函數(shù)能夠分配物理上連續(xù)的內(nèi)存區(qū)域,得到的虛擬地址與物理地址是一一對應的。
物理頁面回收
當空閑物理頁面不足時,就需要從inactive_clean_list隊列中選擇某些物理頁面插入空閑隊列中,如果仍然不足,就需要把某些物理頁面里的內(nèi)容寫回到磁盤交換文件里,騰出物理頁面,為此內(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ù)組每個元素代表磁盤交換文件中的一個頁面,它記錄相應磁盤交換頁面的信息(如頁面基址、所屬的磁盤交換文件),跟頁表項的作用類似。
回收物理頁面的過程由內(nèi)核中的兩個線程專門負責,kswapd和kreclaimd,它們定期的被內(nèi)核喚醒。kswapd主要通過3個步驟回收物理頁面:
調(diào)用shrink_inactive_list ()掃描inacive_dirty_pages隊列,將非活躍隊列里的頁面寫回到交換文件中,并轉(zhuǎn)移到inactive_clean_pages隊列里。
調(diào)用shrink_slab ()回收slab機制保留的空閑頁面。
調(diào)用shrink_active_list ()掃描active_list隊列,將活躍隊列里可轉(zhuǎn)入非活躍隊列的頁面轉(zhuǎn)移到inactive_dirty_list。
5.????虛擬內(nèi)存管理
Linux虛擬地址空間布局如下
Linux將4G的線性地址空間分為2部分,0~3G為user space,3G~4G為kernel space。
由于開啟了分頁機制,內(nèi)核想要訪問物理地址空間的話,必須先建立映射關系,然后通過虛擬地址來訪問。為了能夠訪問所有的物理地址空間,就要將全部物理地址空間映射到1G的內(nèi)核線性空間中,這顯然不可能。于是,內(nèi)核將0~896M的物理地址空間一對一映射到自己的線性地址空間中,這樣它便可以隨時訪問ZONE_DMA和ZONE_NORMAL里的物理頁面;此時內(nèi)核剩下的128M線性地址空間不足以完全映射所有的ZONE_HIGHMEM,Linux采取了動態(tài)映射的方法,即按需的將ZONE_HIGHMEM里的物理頁面映射到kernel space的最后128M線性地址空間里,使用完之后釋放映射關系,以供其它物理頁面映射。雖然這樣存在效率的問題,但是內(nèi)核畢竟可以正常的訪問所有的物理地址空間了。
內(nèi)核空間布局
下面是內(nèi)核空間布局的詳細內(nèi)容,
在kernel image下面有16M的內(nèi)核空間用于DMA操作。位于內(nèi)核空間高端的128M地址主要由3部分組成,分別為vmalloc area,持久化內(nèi)核映射區(qū),臨時內(nèi)核映射區(qū)。
由于ZONE_NORMAL和內(nèi)核線性空間存在直接映射關系,所以內(nèi)核會將頻繁使用的數(shù)據(jù)如kernel代碼、GDT、IDT、PGD、mem_map數(shù)組等放在ZONE_NORMAL里。而將用戶數(shù)據(jù)、頁表(PT)等不常用數(shù)據(jù)放在ZONE_ HIGHMEM里,只在要訪問這些數(shù)據(jù)時才建立映射關系(kmap())。比如,當內(nèi)核要訪問I/O設備存儲空間時,就使用ioremap()將位于物理地址高端的mmio區(qū)內(nèi)存映射到內(nèi)核空間的vmalloc area中,在使用完之后便斷開映射關系。
用戶空間布局
在用戶空間中,虛擬內(nèi)存和物理內(nèi)存可能的映射關系如下圖
當RAM足夠多時,內(nèi)核會將用戶數(shù)據(jù)保存在ZONE_ HIGHMEM,從而為內(nèi)核騰出內(nèi)存空間。
下面是用戶空間布局的詳細內(nèi)容,
用戶進程的代碼區(qū)一般從虛擬地址空間的0x08048000開始,這是為了便于檢查空指針。代碼區(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定義了訪問權限等。
vm_next構成一個鏈表,保存同一個進程的所有虛擬內(nèi)存區(qū)段。
vm_mm指向進程的mm_struct結構體,它的定義為
(位于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;??????????????
…
};
每個進程只有1個mm_struct結構,保存在task_struct結構體中。
與虛擬內(nèi)存管理相關的結構體關系圖如下
虛擬內(nèi)存相關函數(shù)
創(chuàng)建一個內(nèi)存區(qū)段可以用
unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags);
當給定一個虛擬地址時,可以查找它所屬的虛擬內(nèi)存區(qū)段:
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);
由于所有的vm_area_struct組成了一個RB樹,所以查找的速度很快。
向用戶空間中插入一個內(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里的物理頁面映射到內(nèi)核空間:
static inline void *kmap(struct page*page);
6.????內(nèi)存管理3個層次的關系
下面以擴展用戶堆棧為例,解釋3個層次的關系。
調(diào)用函數(shù)時,會涉及堆棧的操作,當訪問地址超過堆棧的邊界時,便引起page fault,內(nèi)核處理頁面失效的過程中,涉及到內(nèi)存管理的3個層次。
? 調(diào)用expand_stack()修改vm_area_struct結構,即擴展堆棧區(qū)的虛擬地址空間;
? 創(chuàng)建空白頁表項,這一過程會利用mm_struct中的pgd(頁全局目錄表基址)得到頁目錄表項(pgd_offset()),然后計算得到相應的頁表項(pte_alloc())地址;
? 調(diào)用alloc_page()分配物理頁面,它會從指定內(nèi)存管理區(qū)的buddy system中查找一塊合適的free_area,進而得到一個物理頁面;
? 創(chuàng)建映射關系,先調(diào)用mk_pte()產(chǎn)生頁表項內(nèi)容,然后調(diào)用set_pte()寫入頁表項。
? 至此,擴展堆棧基本完成,用戶進程重新訪問堆棧便可以成功。
可以認為,結構體pgd和vm_area_struct,函數(shù)alloc_page()和mk_pte()是連接三者的橋梁。
?
評論