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