Linux記憶體定址和記憶體管理
這篇講述linux記憶體定址與記憶體管理的文章,講解的非常好。
1.x86的實體地址空間佈局
以x86_32,4G RAM為例:
實體地址空間的頂部以下一段空間,被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()是連線三者的橋樑。
轉載於:https://my.oschina.net/qingwa/blog/172950