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()是連接三者的橋梁。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM