首页 > 技术文章 > (八)内存管理与内存分配

wolfrickwang 2013-09-03 17:33 原文

(1)几个概念

物理地址:芯片级别的内存寻址,由地址总线决定的,一旦连接到CPU 物理地址不会更变。

虚拟内存:抽象出来的虚拟内存地址,真实不存在。进程使用虚拟内存地址,虚拟内存管理单元(MMU)转换为真实的物理地址。

逻辑地址:内存分段机制中使用(早期内存管理机制),逻辑地址是段地址和段内便宜地址组合值。

线性地址:对应页式内存管理中 转换前的地址。线性空间的大小在32-bit平台上为4 GB的固定大小,对于每个进程都是这样(一个应用可以是多进程的,在OS眼中,是以进程为单位的)。也就是说线性空间不是进程共享的,而是进程隔离的,每个进程都有相同大小的4 GB线性空间。一个进程对于某一个内存地址的访问,与其它进程对于同一内存地址的访问绝不冲突。线性地址转化为物理地址需要读取一个转换表进行转换。

CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址,CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。

 

(2)段式内存管理

 1> 找段标识符

  一个逻辑地址由两部份组成,段标识符: 段内偏移量(offset)。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:
Snap1.jpg

通过段标识符中的段索引表可以在段描述符表中取出真实的段标识符(段号)。

段描述符分为全局的段描述符表(GDT)  和局部段描述符表(LDT) 进程私有的,当段选择符中的T1等于0,用GDT,等于1用LDT。

  2>  从段描述符表中取出base基址

  3>  线性地址 =  base + offset


(3)页式内存管理

    每个进程有4GB的线性地址被划分为固定的单位长度(4Kb)成为页。 所以就形成了页表。 进程页表。

    程序在执行过程中需要按页加载线性内存到物理内存,所以物理内存也分成4(KB)的页表 (可以大于4K但不能小于)。与之对应。也有自己的页表,内存页表。

  进程页表中记录了该进程页的详细信息以及对应的内存页表的索引,所以可以映射到物理内存页。

  内存页表采用分级方式,读取内存页,最多可以划分为4级。 其中中间两项只存在64位系统中,32位系统不需要。

  页全局目录(Page Global Directory)、   页上级目录(Page Upper Directory)、 页中间目录(Page Middle Directory)、 页表(Page Table)。

 

(4) 内核常用内存分配函数

   1>   unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)

    __get_free_pages函数是最原始的内存分配方式,直接从伙伴系统中获取原始页框,返回值为第一个页框的起始地址。__get_free_pages在实现上只是封装了alloc_pages函数,从代码分析,alloc_pages函数会分配长度为1<<order的连续页框块。order参数的最大值由include/linux/Mmzone.h文件中的MAX_ORDER宏决定,在默认的2.6.18内核版本中,该宏定义为10。也就是说在理论上__get_free_pages函数一次最多能申请1<<10 * 4KB也就是4MB的连续物理内存。但是在实际应用中,很可能因为不存在这么大量的连续空闲页框而导致分配失败。在测试中,order为10时分配成功,order为11则返回错误。


    2> struct kmem_cache *kmem_cache_create(const char *name, size_t size,size_t align, unsigned long flags,void (*ctor)(void*, struct kmem_cache *, unsigned long), void (*dtor)(void*, struct kmem_cache *, unsigned long) )

    void *kmem_cache_alloc(struct kmem_cache *c, gfp_t flags)
 
    kmem_cache_create/ kmem_cache_alloc是基于slab分配器的一种内存分配方式,适用于反复分配释放同一大小内存块的场合(可以小于页大小)。首先用kmem_cache_create创建一个高速缓存区域,然后用kmem_cache_alloc从该高速缓存区域中获取新的内存块。 kmem_cache_alloc一次能分配的最大内存由mm/slab.c文件中的MAX_OBJ_ORDER宏定义,在默认的2.6.18内核版本中,该宏定义为5,于是一次最多能申请1<<5 * 4KB也就是128KB的连续物理内存。分析内核源码发现,kmem_cache_create函数的size参数大于128KB时会调用BUG()。测试结果验证了分析结果,用kmem_cache_create分配超过128KB的内存时使内核崩溃。

     3> void *mempool_alloc(mempool_t *pool,int gfp_mask)

        为了确保在内存分配不允许失败情况下成功分配内存,内核提供了称为内存池( "mempool" )的抽象,它其实是某种后备高速缓存,mempool的底层通常使用slab。它为了紧急情况下的使用。所以使用时必须注意:mempool会分配一些内存块,使其空闲而不真正使用,所以容易消耗大量内存。而且不要使用mempool处理可失败的分配。应避免在驱动代码中使用mempool。


    4> void *kmalloc(size_t size, gfp_t flags) 

    kmalloc是内核中最常用的一种内存分配方式,它通过调用kmem_cache_alloc函数来实现。kmalloc一次最多能申请的内存大小由include/linux/Kmalloc_size.h的内容来决定,在默认的2.6.18内核版本中,kmalloc一次最多能申请大小为131702B也就是128KB字节的连续物理内存。测试结果表明,如果试图用kmalloc函数分配大于128KB的内存,编译不能通过。


   5> void *vmalloc(unsigned long size)

    前面几种内存分配方式都是物理连续的,能保证较低的平均访问时间。但是在某些场合中,对内存区的请求不是很频繁,较高的内存访问时间也可以接受,这是就可以分配一段线性连续,物理不连续的地址,带来的好处是一次可以分配较大块的内存。图3-1表示的是vmalloc分配的内存使用的地址范围。vmalloc对一次能分配的内存大小没有明确限制。出于性能考虑,应谨慎使用vmalloc函数。在测试过程中,最大能一次分配1GB的空间。

    6> void *dma_alloc_coherent(struct device *dev, size_t size,ma_addr_t *dma_handle, gfp_t gfp)

    DMA是一种硬件机制,允许外围设备和主存之间直接传输IO数据,而不需要CPU的参与,使用DMA机制能大幅提高与设备通信的吞吐量。DMA操作中,涉及到CPU高速缓存和对应的内存数据一致性的问题,必须保证两者的数据一致,在x86_64体系结构中,硬件已经很好的解决了这个问题, dma_alloc_coherent和__get_free_pages函数实现差别不大,前者实际是调用__alloc_pages函数来分配内存,因此一次分配内存的大小限制和后者一样。__get_free_pages分配的内存同样可以用于DMA操作。测试结果证明,dma_alloc_coherent函数一次能分配的最大内存也为4M。

    7> void * ioremap (unsigned long offset, unsigned long size)

    ioremap是一种更直接的内存“分配”方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段物理地址映射到内核地址空间。ioremap用到的物理地址空间都是事先确定的,和上面的几种内存分配方式并不太一样,并不是分配一段新的物理内存。ioremap多用于设备驱动,可以让CPU直接访问外部设备的IO空间。ioremap能映射的内存由原有的物理内存空间决定,所以没有进行测试。

    8> Boot Memory


    如果要分配大量的连续物理内存,上述的分配函数都不能满足,就只能用比较特殊的方式,在Linux内核引导阶段来预留部分内存。


    9> void* alloc_bootmem(unsigned long size)

    可以在Linux内核引导过程中绕过伙伴系统来分配大块内存。使用方法是在Linux内核引导时,调用mem_init函数之前用alloc_bootmem函数申请指定大小的内存。如果需要在其他地方调用这块内存,可以将alloc_bootmem返回的内存首地址通过EXPORT_SYMBOL导出,然后就可以使用这块内存了。这种内存分配方式的缺点是,申请内存的代码必须在链接到内核中的代码里才能使用,因此必须重新编译内核,而且内存管理系统看不到这部分内存,需要用户自行管理。测试结果表明,重新编译内核后重启,能够访问引导时分配的内存块。

   10> 通过内核引导参数预留顶部内存

    在Linux内核引导时,传入参数“mem=size”保留顶部的内存区间。比如系统有256MB内存,参数“mem=248M”会预留顶部的8MB内存,进入系统后可以调用ioremap(0xF800000,0x800000)来申请这段内存。


 

 

推荐阅读