作者:Diting0x
CSysSec注: 本文来自Diting0x的个人博客,主要介绍了Linux内核中一系列的内存分配函数及其原理
转载本文请务必注明,文章出处:《深入理解Linux内核分配》与作者信息:Diting0x
为了写一个用户层程序,你也许会声明一个全局变量,这个全局变量可能是一个int类型也可能是一个数组,而声明之后你有可能会先初始化它,也有可能放在之后用到它的时候再初始化。除此之外,你有可能会选择在函数内部去声明局部变量,又或者为变量动态申请内存。
不管你在用户程序中采取哪种方式申请内存,这些都对应着不同的内存分配方式以及不同的数据段,如果再加上代码段,就构成了一个完整的进程。由此可见,一个完整的进程在内存空间中对应着不同的数据区,具体来说,对应着五种不同的数据区:
代码段,存放操作指令;数据段,存放已初始化的全局变量;BSS段,存放未初始化的全局变量;堆,存放动态分配的内存(e.g.,malloc());栈,存放临时创建的局部变量。
当你习惯写用户程序时,你不会去太多考虑你声明的变量最后都放到内存哪里去了,如果你仍然觉得这不是你应该了解的事情,后面的内容你就可以不用浪费时间继续阅读了。下文更多的是关注内核空间的分配,但至少也会把用户空间的分配情况说清楚。
对于x86系统来说,4G的内存空间被分为用户空间(e.g.,0-3G,0xC0000000)与内核空间(e.g.,3G-4G),用户程序只能在进行系统调用时才能访问到内核空间。此外,当进程切换时,用户空间会随着进程的变化切换到对应进程的用户空间,而内核空间不会随着进程而改变,由内核负责映射。内核空间有自己对应的页表,而用户进程各自有不同的页表。
从用户层向内核看,内存的地址形式依次是,逻辑地址--线性地址--物理地址,但Linux并没有充分利用段机制,而是将所有程序的段地址固定在0-4G,因此逻辑地址就等于线性地址。
了解这些基本知识之后,来看看进程的虚拟地址是如何组织的。
一个进程的虚拟地址空间主要由两个数据结构来描述,mm_struct与vm_area_struct。 来具体说说这两个结构体用来做什么
每个进程有一个mm_struct结构,在进程的task_struct结构体中有一个指针指向mm_struct。 mm_struct的定义如下:
|
|
简单来说,mm_struct是对整个进程的用户空间的描述,而进程的虚拟空间可能有多个虚拟区间(这里的区间就是由vm_area_struct来描述). vm_area_struct是描述进程虚拟空间的基本单元,那这些基本单元又是如何管理组织的呢?内核采取两种方式来组织这些基本单元,第一,正如mm_struct中的mmap指针指向vm_area_struct,以链表形式存储,这种结构主要用来遍历节点;第二,以红黑树来组织vm_area_struct,这种结构主要在定位特定
内存区域时用来搜索,以降低耗时。
了解了这些关联之后,回到最前面,当你写的用户程序在申请内存时(e.g., int i =0; malloc()),注意这里申请的内存还是虚拟内存,可以说是“内存区域”(vm_area_struct),并非实际物理内存。 这些虚拟内存除了malloc()方式(由专门的brk()系统调用实现),最终都是通过系统调用mmap来完成的,而mmap系统调用对应的服务例程是do_mmap()函数,有关do_mmap()函数,可参考do_mmap().
说了这么多用户空间,该把重心来看看内核空间了。
用户空间有malloc内存分配函数,内核空间同样有类似的内存分配函数,只是种类多一些(e.g.,*kmalloc/kfree,vmalloc/vfree,kmem_cache_alloc/kmem_cache_free,get_free_page).
在具体解释内核空间层的内存分配函数之前,先来看看,物理内存是如何组织的。Linux通过分页机制来管理物理内存,页面是物理内存的基本单位,每个页面占4kB。页面在系统中由struct page结构来描述,而所有的struct page结构体都存储在数组mem_map[]中,因此只要能找到mem_map[]数组的物理地址,就能遍历所有页面的地址。可以来大致看一下struct page*的定义:
|
|
其中,flag用来存放页的状态,count记录该页面被引用了多少次,mapping指向该页面相关的地址空间对象… 这里只是一个简化的定义,真实情况会复杂一些,要把page说清楚,需要写一篇新的博客了,之后的文章会专门介绍。需要注意的是,page描述的是物理内存本身,而并非包含在里面的数据。
那这些page又和内核空间的内存分配有什么关系呢?
内核空间有一系列的页面分配函数:
|
|
以_ _get_free_pages为例看看其函数间调用关系:
|
|
|
|
|
|
|
最终_ _get_free_page会调用_ _alloc_pages函数分配页面。_ _alloc_pages是所有页面分配函数的核心函数,最终都会调用到这个函数,它会返回一个struct page结构。
在了解与其它内存分配函数的区别前,先说明下面这个概念
前文说过3G-4G属于内核空间,然后在内核空间中又有进一步划分。
3G~vmalloc_start这段地址是物理内存映射区域,该区域包括了内核镜像,mem_map数组等等。在vmalloc_start~vmalloc_end属于vmalloc区域(vmalloc下文会说),vmalloc_end的位置接近4G(最后系统会保留一片128KB大小的区域专用页面映射). 那这个vmalloc_start的位置又在哪呢?假设我们使用的系统内存是512M,vmalloc_start就在应在3G+512M附近(说”附近”因为是在物理内存映射区与vmalloc_start期间还会存在一个8M大小的gap来防止跃界).当然实际情况都比这个大,甚至都4G,8G,16G..但我们使用的CPU都是64位的,可寻址空间就不止4G了,这个理论仍然有效。
_ _get_free_page系列函数申请的内存位于物理内存映射区域,在物理上是连续的,注意,函数返回的是虚拟地址,其与物理地址有一个固定的偏移,存在比较简单的转换关系,virt_to_phys()函数做的就是这件事:
|
|
注意,这里的PAGE_OFFSET指的就是3G(针对x86位系统).
与页面分配系函数一样,kmalloc函数申请的内存也处于物理内存映射区域,在物理上是连续的。Kmalloc函数是slab分配器提供的分配内存的接口,slab是什么?这里不去具体讲slab分配原理,想详细了解的slab可以参考这里. 简单说明一下:slab是为了避免内部碎片使得一个页面内包含的众多小块内存可独立被分配使用,是为分配小内存提供的一种高效机制。追踪kmalloc函数,可以发现,它最终还是调用前面提到的
_ _alloc_pages()函数。既然kmalloc基于slab实现,而slab分配机制又不是独立的,本身也是在以页面为单位分配的基础上来划分更细粒度的内存供调用者使用。就是说系统先用页分配器分配以页为最小单位的连续物理地 址,然后kmalloc再在这上面根据调用者的需要进行切分。
既然slab是为了解决内部碎片的问题,那想必也有一个解决外部碎片的机制(注:外部分片是指系统虽有足够的内存,但却是分散的碎片,无法满足对大块“连续内存”的需求)。没错,伙伴关系系统就是这么一个机制。伙伴关系系统提供vmalloc来分配非连续内存,其分配的地址限于上述说的vmalloc_start~vmalloc_end之间。这些虚拟地址与物理内存没有简单的位移关系,必须通过内核页表才可转换为物理地址或物理页。它们有可能尚未被映射,在发生缺页时才真正分配物理页面。
说到这里,还有一个关键函数没提,kmem_cache_alloc。 kmem_cache_alloc也是基于slab分配器的一种内存分配方式,适用于反复分配同一大小内存块的场合。首先用kmem_cache_create创建一个高速缓存区域,然后用kmem_cache_alloc从该高速缓存区域获取新的内存块。kmem_cache_alloc分配固定大小的内存块。kmalloc则是在kmem_cache_create的基础实现的,其分配动态大小的内存块,查看源码可以发现kmalloc函数中会有一段代码块转向调用kmem_cache_alloc:
|
|
内核空间常用的内存分配函数就此说完了,实际除了这些常用的,还有其它的分配函数,在此简单说明一下。如,dma_alloc_coherent,基于_ _alloc_pages实现,适用于DMA操作;ioremap,实现已知物理地址到虚拟地址的映射,适用于物理地址已经的场合,如设备驱动;alloc_bootmem,在启动内核时,预留一段内存,内核看不见,对内存管理要求较高。
完
转载本文请务必注明,文章出处:《深入理解Linux内核分配》与作者信息:Diting0x