zoukankan      html  css  js  c++  java
  • 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()是连接三者的桥梁。

  • 相关阅读:
    eclipse中文乱码问题解决方案
    修改Tomcat的JDK目录
    Tomcat 5.5 修改服务器的侦听端口
    HTML DOM教程 27HTML DOM Button 对象
    HTML DOM教程 24HTML DOM Frameset 对象
    Navicat for MySQL v8.0.27 的注册码
    HTML DOM教程 25HTML DOM IFrame 对象
    Tomcat 5.5 的下载和安装
    android manifest相关属性
    ubuntu10.04 下 eclipse 小结
  • 原文地址:https://www.cnblogs.com/zszmhd/p/2661461.html
Copyright © 2011-2022 走看看