良许Linux教程网 干货合集 Linux内存管理:如何实现虚拟内存和物理内存的映射和管理

Linux内存管理:如何实现虚拟内存和物理内存的映射和管理

Linux系统中,内存管理是操作系统最重要的部分之一。它负责将有限的物理内存分配给多个进程,并提供虚拟内存的抽象,使得每个进程都有自己的地址空间,并能够保护和共享内存。本文将介绍Linux内存管理的原理和方法,包括Linux内存管理的基本概念、虚拟内存的抽象模型、物理内存的分配和回收、页表和页目录的结构和作用等。

1. x86****的物理地址空间布局

img
img

以x86_32,4G RAM为例:

物理地址空间的顶部以下一段空间,被PCI设备的I/O内存映射占据,它们的大小和布局由PCI规范所决定。640K~1M这段地址空间被BIOS和VGA适配器所占据。

由于这两段地址空间的存在,导致相应的RAM空间不能被CPU所寻址(当CPU访问该段地址时,北桥会自动将目的物理地址“路由”到相应的I/O设备上,不会发送给RAM),从而形成RAM空洞。

当开启分段分页机制时,典型的x86寻址过程为

img
img

内存寻址的工作是由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是不可见的。

img
img

在内核源码中,分别为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。

img
img
  • 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虚拟地址空间布局如下

img
img

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线性地址空间里,使用完之后释放映射关系,以供其它物理页面映射。虽然这样存在效率的问题,但是内核毕竟可以正常的访问所有的物理地址空间了。

内核空间布局

下面是内核空间布局的详细内容,

img
img

在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中,在使用完之后便断开映射关系。

用户空间布局

在用户空间中,虚拟内存和物理内存可能的映射关系如下图

img
img

当RAM足够多时,内核会将用户数据保存在ZONE_ HIGHMEM,从而为内核腾出内存空间。

下面是用户空间布局的详细内容,

img
img

用户进程的代码区一般从虚拟地址空间的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结构体中。

与虚拟内存管理相关的结构体关系图如下

img
img

虚拟内存相关函数

  • 创建一个内存区段可以用

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个层次的关系

img
img

下面以扩展用户堆栈为例,解释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()是连接三者的桥梁。

通过本文,你应该对Linux内存管理有了一个基本的了解,它是一种实现虚拟内存和物理内存的映射和管理的有效方式,可以适应Linux系统的多样化需求。当然,内存管理也不是一成不变的,它需要根据具体的硬件平台和内核版本进行定制和修改。总之,内存管理是Linux系统中不可或缺的一个组件,值得你深入学习和掌握。

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部