1. 程式人生 > 實用技巧 >Linux記憶體定址和記憶體管理

Linux記憶體定址和記憶體管理

>>> hot3.png

這篇講述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