Linux内核内存管理

内存地址

Memory Zone

Linux使用虚拟内存技术,因此在应用层所能看到的、访问的都是虚拟地址。对于32位系统来讲(本文涉及的都是32位系统),每个进程能够寻址的地址空间都是4G,不管物理内存有多大。应用开发者实际上是能够不用关系内存空间的划分,仅仅使用封装后的接口就能够完成开发。但在工做中,若是对地址空间没有基本的了解,在程序设计和解决问题时可能会引发方向性错误。这里对地址空间进行简单介绍,下图时网上常见的x86架构的内存区域划分。linux

linux_address_2.jpg

  • 物理内存被分为三个区域:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。DMA和HIGHMEM区域不是必须存在的,在Kernel编译选项中可控制开关。
  • 在ARM架构中,一般是不存在ZONE_DMA区域的,ZONE_HIGHMEM的起始地址默认是760M,而不是x86的896M。
  • ZONE_DMA和ZONE_NORMAL是直接映射到Kernel地址空间的,因此它们的物理地址能够经过一个偏移量直接转化为Kernel地址。
  • ZONE_HIGHMEM区域没有直接映射,Kernel没法直接访问,须要经过vmalloc映射到Kernel空间。vmalloc的用途是将非连续的物理空间映射为连续的Kernel地址空间,vmalloc会优先使用highmem。
  • 在64位系统中,Kernel可寻址的空间超过512G,因此也就不须要ZONE_HIGHMEM。

物理内存是映射到Kernel地址空间的,这个空间也是虚拟的,分布在进程虚拟地址空间的3G~4G,0~3G为用户地址空间。下图也是从网络上得到的。编程

linux_address_1.jpg

  • 每一个进程都会有这样的4G空间,用户地址空间本身分配使用,内核地址空间实际上都是相同的。
  • 0~3G的用户地址空间被划分为代码段(程序执行代码,只读)、数据段(已初始化的全局变量)、BSS段(未初始化的全局变量,清零)、堆(动态分配的内存)、栈(局部变量,函数传参,返回值)等。
  • 用户进程仅能访问0~3G的用户地址空间,Kernel能够访问所有地址空间。
  • 物理内存分配须要同时映射到用户地址空间和内核地址空间。
  • 内核地址空间中,直接映射区域与vmalloc区域存在一个8M的hole,用作内存保护。每个vmalloc间又留有一个4K的hole用来保护。

地址转换

Linux中的用户空间看到的是连续的虚拟地址,在被真正使用时仍是须要转换为真实的物理地址。根据虚拟地址来查询物理地址的过程被称为walking page tables。由于Linux使用分页机制来管理内存,基本的结构以下图。缓存

linux_address_3.jpg

当前的Linux设计了四级页表,分别是PGD -> PUD -> PMD -> PTE。其中PUD和PMD不是必须的,因此系统能够根据硬件构架使用二级、三级或四级的页表模式。一般在32位系统中,二级页表就能够知足需求。下图是一个二级页表的查找过程。网络

linux_address_4.png

能够看到,一个虚拟地址被分为一级查找索引,二级查找索引和页索引三部分。根据一级页表PGD的基地址(保存在协处理器CP15:C2中),结合一级查找索引能够获取到二级页表PTE的基地址。再将二级页表基址与二级查找索引结合,能够获取到物理页的基地址。物理页基址与页索引结合就是须要查找的物理页。架构

页表的查找过程在系统中很是频繁,所以须要经过硬件来完成,这个硬件就是MMU。MMU被称为内存管理单元,它不只仅负责虚拟地址到物理地址的转换,还负责内存访问权限和高速缓存的管理。编程语言

TLB

上文已经说到了内存管理单元MMU,其最主要的工做就是进行地址转换。为了加快地址转换的速度,一般将最近访问的虚拟地址和转换后的物理地址的对应保存在一个高速缓存中,这个缓存就是TLB(Translation Lookaside Buffer)。由于在程序运行时,其访问过的地址被再次访问的几率很高,当TLB中保存了须要访问的地址时,就能够免去页表查找的过程,能够大大提升系统性能。ide

linux_address_5.png

在进行地址转换时, 首先访问的是TLB,在TLB查找是否有该虚拟地址的缓存。若是找到(a TLB hit),直接返回物理地址。若是在TLB中没法找到(a TLB miss),则须要经过MMU进行页表查找得到物理地址,同时将新的查找更新到TLB中。某些状况下,例如使用磁盘作为swap时,须要访问的内存页可能不存在page table中。这时须要将访问页回写到物理内存中,同时更新page table和TLB。函数

内存分配

内核中常见的内存分配函数有vmalloc、kmalloc、__get_free_pages等,分别介绍一下。工具

kmalloc

内核中最经常使用的内存分配函数就是kmalloc(),当须要申请小块内存时,优先考虑使用kmalloc。kmalloc是基于slab分配器进行工做的,先简单介绍一下slab。性能

linux_address_6.png

  • slab是为了解决小块内存分配产生的内部碎片而产生的。
  • slab又有不一样的实现方式,如slab、slob、slub。最新的内核中使用slub。
  • slab会缓存频繁使用的对象,减小分配、释放的时间。
  • slab分配的内存在物理上是连续的。
  • slab高速缓存经过kmem_cache来描述,每个kmem_cache中存有一种对象的高速缓存。
  • kmem_cache经过三个链表来管理slab缓存:slabs_full(彻底分配),slabs_partial(部分分配的),slabs_empty(未分配)。
  • slab可分为普通高速缓存和专用高速缓存两类。专用缓存为具体的对象建立,根据对象命名。普通缓存不指定特定对象,根据大小命名。

kmalloc就是经过slab的普通高速缓存来分配内存的。kmalloc的实现也很简单,就是在slab的普通高速缓存中寻找一个大小最匹配缓存。由于kmalloc分配的内存是物理连续的,而物理连续的内存是很是珍贵的,因此除非必要,不然大块内存(以页为单位)分配应该使用vmalloc和get_free_pages。kmalloc能够分配的最大值在不一样的硬件架构上是不一样的,而且使用的分配器类型也有影响。例如在ARM32上使用slub分配器时,kmalloc()可接受的最大size为8M。可是当size大于8K时,kmalloc()的内部实现调用了__get_free_pages()。

vmalloc

上面讲到过,highmem中有一块vmalloc区域,这个地址空间就是经过vmalloc()分配使用的。vmalloc的特色以下,

  • vmalloc分配的内存被映射到内核地址空间的vmalloc区域,在分配时优先使用highmem。
  • vmalloc分配的内存在内核地址空间上是连续的,但在物理内存上不必定连续。
  • vmalloc效率较低,在运行过程当中新的页表须要被创建,因此效率低于kmalloc和__get_free_page

__get_free_pages

__get_free_pages从Buddy系统中分配的页面,其分配的页数是2的幂数。Buddy系统是Linux用来组织和管理内存页面的方法,用来解决内存外部碎片问题。Buddy系统把全部的空闲页框分组为11个块链表,每一个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大能够申请1024个连续页框,对应4MB大小的连续内存。每一个页框块的第一个页框的物理地址是该块大小的整数倍。

linux_address_7.png

假设要申请一个256个页框的块,先从256个页框的链表中查找空闲块,若是没有,就去512个页框的链表中找,找到了则将页框块分为2个256个页框的块,一个分配给应用,另一个移到256个页框的链表中。若是512个页框的链表中仍没有空闲块,继续向1024个页框的链表查找,若是仍然没有,则返回错误。页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块。

__get_free_pages分配的是物理内存上连续的页面,因此没有特殊需求时也不该该一次性分配较大的内存。在须要分配大内存时,若是不需求物理上连续,能够一页一页分配,而后映射到连续的虚拟地址空间上。

DMA

开发驱动时,有时须要为硬件设备分配一段内存用于DMA传输,这时可能就会用到dma_alloc_coherent()。为DMA分配的内存会有如下特色,

  • 内存必须是物理上连续的。
  • 内存是不能被cache的,不然可能会产生数据的不一致。
  • DMA映射的区域能够同时被CPU和外围设备访问。
  • 使用DMA时须要注意cache,根据须要使用dma_cache_sync()来保证内存一致性。

ION

ION是google为了解决不一样Android设备的内存碎片问题,提出的一种内存管理器。它支持不一样的内存分配机制,如CARVOUT(PMEM),物理连续内存(kmalloc),虚拟地址连续但物理不连续内存(vmalloc), IOMMU等。

linux_address_8.png

ION经过Heap来管理不一样的内存空间,每一个Heap须要实现本身的操做内存方法,好比allocate, free, map等。对ION的使用须要经过Client完成,用户空间和内核空间均可以成为Client。内核空间经过ion_client_create()获取Client,用户空间经过打开/dev/ion来获取Client的fd。

内存释放

主动释放

本着谁分配谁释放的原则,不管时应用中经过malloc分配的内存仍是内核中经过kmalloc分配的,在使用完成时必定记得使用free类函数进行释放。代码上很简单,重要的是养成良好的编程习惯。同时也要注意不要对分配的内存屡次释放,这样一样会致使异常。内存的分配释放提及来很简单,但也是最容易出问题的地方。因此就出现了大量的内存检测工具。若是软件开发已经开始使用内存检测工具了,就已经太晚了。但咱们又没法避免内存问题,即便带有自动回收的编程语言也是不能彻底避免。内存问题在软件开发中一直是个难题。

Cache回收

Linux系统中使用free命令来查看内存时,能够看到有两项是“buffers”和“cached”。这两项的意义是,

  • buffers:表示块设备 (block device) 所占用的缓存页,包括了直接读写块设备以及文件系统元数据 (metadata) 好比 SuperBlock 所使用的缓存页。
  • cached:表示普通文件系统中数据所占用的缓存页。

buff/cache是用来缓存磁盘文件数据的,用来提升IO访问速度。当系统运行一段时间后,会发现buff/cache占用的内存不少,但这些内存被认为是能够available的。当内存短缺时,系统会触发内存回收,这部份内存就会释放。还有一种手动清理Cache的方法,经过下面这个命令。

$ echo 3 > /proc/sys/vm/drop_caches

但咱们不建议用命令行来强行回收Cache,这样会破坏系统内存管理。Cache回收更多仍是应该依赖系统的内存回收机制。也能够扩展原生的内存回收,增长本身的回收机制,像Android的lowmemorykiller那样。若是内存回收仍然没法解决内存短缺问题(在嵌入式系统中常常发生),能够试图去调整vm的一些参数,在“/proc/sys/vm/”下,这就须要对内存管理有一个正确的理解。也能够试图去调整磁盘挂载的参数,这也可能影响到Cache。具体的调整方法不在这里详细说明,若是之后写到内存优化时会单独写一篇。

内存回收机制

当系统内存不足时,内核会启动内存回收。内存回收的时机有如下三种,

  • 内存紧缺回收:在内存分配失败时,会直接调用try_to_free_pages()进行页面回收,以便尽快释放内存。这种方式被称做“直接页面回收”。
  • 睡眠回收:在进入suspend-to-disk状态时,须要释放内存。
  • 周期回收:守护进程 kswapd会按期检查系统可用内存,当剩余内存低于预约水位线时就会进行回收。另外一个按期回收是reap_work,用来回收slab空闲页面。

linux_address_9.jpg

kswapd是内存回收机制中最重要的方式,它做为守护进程在后台周期运行,根据预约的水位进行回收。Linux系统中每个内存区域(zone)都会存在一个kswapd,同时每一个区域也定义了一组watermark来作为参考水位。

  • WMARK_MIN:最低水位线。低于该水位表示系统没法工做,必须进行页面回收。kswapd检查到剩余内存低于该水位时,会发起直接页面回收,并且可能会引发OOM。
  • WMARK_LOW:低水位线。kswapd检查剩余内存低于该水位时开始启动回收,直到剩余内存高于WMARK_HIGH时中止回收。
  • WMARK_HIGH:高水位线。kswapd认为这时系统内存充足,不须要回收。

内存回收的目的是为了保证系统有足够的内存能够正常运行,但当kswapd频繁回收时也会对系统形成压力,有时能够看到kswapd的CPU占用率很高,就是由于回收过于频繁。这种状况下就须要根据系统状态来进行内存调优,主要是调整watermark。基本原则是避免内存低于WMARK_MIN,根据系统运行状态设置合理的WMARK_HIGH,选择合适的时机启动后台内存回收。这些又是内存调优的话题,须要另外一篇来阐述。

说明

本文只是对内核的内存管理作简单的介绍,目的是对其有一个总体的认识。其中的每一部分展开来都是很大的课题,本人水平有限再也不深刻分析。文章中的图片所有来自网络,源头也不是很清楚。

相关文章
相关标签/搜索