关于linux下内存管理内容(详细)总结

一、简介

内存管理子系统可能是linux内核中最为复杂的一个子系统,其支持的功能需求众多,如页面映射、页面分配、页面回收、页面交换、冷热页面、紧急页面、页面碎片管理、页面缓存、页面统计等,而且对性能也有很高的要求。本文从内存分配、内存回收、页面映射、页面交换等方面进行整体说明。
Linux将32位设备4G的线性地址空间分为2部分,0~3G为userspace,0~4G为kernel space。由于开启了分页机制,内核想要访问物理地址空间的话,必须先建立映射关系,然后通过虚拟地址来访问。为了能够访问所有的物理地址空间,就要将全部物理地址空间映射到1G的内核线性空间中,这显然不可能。于是,内核将0~896M的物理地址空间一对一映射到自己的线性地址空间中,这样它便可以随时访问ZONE_DMA和ZONE_NORMAL里的物理页面;此时内核剩下的128M 线性地址空间不足以完全映射所有的 ZONE_HIGHMEM,Linux 采取了动态映射的方法,即按需的将 ZONE_HIGHMEM 里的物理页面映射到 kernel space 的最后 128M。 线性地址空间里,使用完之后释放映射关系,以供其它物理页面映射。虽然这样存在效率的问题,但是内核毕竟可以正常的访问所有的物理地址空间了。
在这里插入图片描述

1、ZONE_DMA 的范围是 0~16M,该区域的物理页面专门供 I/O 设备的 DMA 使用。之所以需要单独管理 DMA 的物理页面,是因为 DMA 使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于
2、DMA,ZONE_NORMAL的范围是16M~896M,该区域的物理页面是内核能够直接使用的。
3、ZONE_HIGHMEM的范围是896M~结束,该区域即为高端内存,内核不能直接使用。
ZONE_DMA是DMA使用的页(DMA是直接路径访问,不经过cpu缓存而直接访问内存)ZONE_NORMAL是正常可寻址的页。ZONE_HIGHMEM是动态映射的页。Linux把系统的页划分为不同的区,形成不同的内存池,这样就可以根据用途进行分配了。其中还有ZONE_DMA32,他只能被32位设备访问。
其中低端内存实现了对物理内存一对一的线性映射(只有1G),高端内存用来临时存放指向其余物理内存空间的地址。

DMA内存动态分配地址空间:
一些DMA设备因为其自身寻址能力的限制,不能访问所有内存空间。如早期的ISA设备只能在24位地址空间执行DMA,即只能访问前16MB内存。所以需要划分出DMA内存动态分配空间,即DMA zone。其分配通过加上GFP_ATOMIC控制符的kmalloc接口来申请。
直接内存动态分配地址空间:
因为访问效率等原因,内核对内存采用简单的线性映射,但是因为32位CPU的寻址能力(4G大小)和内核地址空间起始的设置(3G开始),会导致内核的地址空间资源不足,当内存大于1GB时,就无法直接映射所有内存。无法直接映射的地址空间部分,即highmem zone。在DMA zone和highmem zone中间的区域即normal zone,主要用于内核的动态内存分配。其分配通过kmalloc接口来申请。
高端内存动态分配地址空间:
高端内存分配的内存是虚拟地址连续而物理地址不连续的内存,一般用于内核动态加载的模块和驱动,因为内核可能运行了很久,内存页面碎片情况严重,如果要申请大的连续地址的内存页会比较困难,容易导致分配失败。根据应用需要,高端内存分配提供多个接口:
vmalloc:指定分配大小,page位置和虚拟地址隐式分配;
vmap:指定page位置数组,虚拟地址隐式分配;
ioremap:指定物理地址和大小,虚拟地址隐式分配。
持久映射地址空间:
内核上下文切换会伴随着TLB刷新,这会导致性能下降。但一些使用高端内存的模块对性能也有很高要求。持久映射空间在内核上下文切换时,其TLB不刷新,所以它们映射的高端地址空间寻址效率较高。其分配通过kmap接口来申请。kmap与vmap的区别是:vmap可以映射一组page,即page不连续,但虚拟地址连续,而kmap只能映射一个page到虚拟地址空间。kmap主要用于fs、net等对高端内存访问有较高性能要求的模块中。
固定映射地址空间:
持久映射的问题是可能会休眠,在中断上下文、自旋锁临界区等不能阻塞的场景中不可用。为了解决这个问题,内核又划分出固定映射,其接口不会休眠。固定映射空间通过kmap_atomic接口来映射。kmap_atomic的使用场景与kmap较为相似,主要用于mm、fs、net等对高端内存访问有较高性能要求而且不能休眠的模块中。
在这里插入图片描述

二、内存申请

2.1 页分配与释放
Linux通过struct page结构来表示物理页,是系统内存的最小单位

struct page {
       page_flags_t flags;  //页标志符,flag用来表示这些页的存放状态,比如是否为脏页(总共能够表示32位,在page-flags.h中可以看到)
       atomic_t _count;    页引用计数,表明这个物理页的计数,若为-1表示内核还没有用到这一页,他是调用page_count()函数来检查这个域(返回0表示空闲,正整数表示正在使用)
       atomic_t _mapcount;     //页映射计数
       unsigned long private;    //私有数据指针
       struct address_space *mapping;    //该页所在地址空间描述结构指针,用于内容为文件的页帧
//       struct list_head lru;        //最近最久未使用struct slab结构指针链表头变量
       void *virtual;               //这个用来指向该页在虚拟内存中的地址,常用作高端内存的动态映射
};

下面列举所有的页为单位进行连续物理内存分配
页分配函数描述

alloc_pages(gfp_mask, order) 	分配2^order个页,返回指向第一页的指针
alloc_pages(gfp_mask) 	分配一页,返回指向页的指针
__get_free_pages(gfp_mask, order) 	分配2^order个页,返回指向其逻辑地址的指针
__get_free_pages(gfp_mask) 	分配一页,返回指向其逻辑地址的指针
get_zeroed_page(gfp_mask) 	分配一页,并填充内容为0,返回指向其逻辑地址的指针
get_zeroed_page:对于用户空间,这个方法能保障系统敏感数据不会泄露
page_address: 把给定的页转换成逻辑地址

页释放函数描述

__free_pages(page, order) 	从page开始,释放2^order个页
free_pages(addr, order) 	从地址addr开始,释放2^order个页
free_page(addr) 	释放addr所在的那一页
kmalloc()__get_free_pages()不能分配高端内存,因为这两个函数返回的是物理地址上的逻辑地址,可能还没有映射到虚拟地址上,并非page结构。只有alloc_pages()才能分配高端内存

2.2 字节分配与释放
kmalloc,vmalloc分配都是以字节为单位
内核中申请内存的方法有:kmalloc()、kzalloc()、vmalloc()、alloc_page()

//kmalloc用于申请物理内存,地址上无须连续,因此性能较快。对应的内存释放函数是 kfree()。
void * kmalloc(size_t size, gfp_t flags)

该函数返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存在物理内存中连续且保持原有的数据(不清零)
其中部分flags取值说明:

GFP_USER: 用于用户空间的分配内存,可能休眠;
GFP_KERNEL:用于内核空间的内存分配,可能休眠;
GFP_ATOMIC:用于原子性的内存分配,不会休眠;典型原子性场景有中断处理程序,软中断,tasklet等

kmalloc内存分配最终总是调用__get_free_pages 来进行实际的分配,故前缀都是GFP_开头。 kmalloc分最多只能分配32个page大小的内存,每个page=4k,也就是128K大小,其中16个字节用来记录页描述结构。kmalloc分配的是常驻内存,不会被交换到文件中。最小分配单位是32或64字节。

2、kzalloc相对于kmalloc多了一个__GFP_ZERO标志,这个标志位会对申请到的内存内容清零。对应的内存释放函数也是 kfree()。get_zeroed_page()也是同样的效果。
kzalloc

//kzalloc()等价于先用 kmalloc() 申请空间, 再用memset()来初始化,所有申请的元素都被初始化为0。
static inline void *kzalloc(size_t size, gfp_t flags)
{
    return kmalloc(size, flags | __GFP_ZERO); //通过或标志位__GFP_ZERO,初始化元素为0
}

3、vmalloc用于申请虚拟内存,由于虚拟内存需要保证一定的连续性,因此性能较慢(可能造成比较大的TLB抖动)。由于对象是虚拟内存,所以申请的内存大小没有限制,通常被用来申请大内存空间。对应的内存释放函数是 vfree()。

void * vmalloc(unsigned long size)

该函数返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存是逻辑上连续的。
kmalloc不同,该函数乜有flags,默认是可以休眠的。

分配函数区域连续性大小释放函数优势
kmalloc内核空间物理地址连续最大值128K-16kfree性能更佳
vmalloc内核空间虚拟地址连续更大vfree更易分配大内存
malloc用户空间虚拟地址连续更大free

2.3 Slab分配器
以页为最小单位分配内存对于内核管理系统中的物理内存来说的确比较方便,但内核自身最常使用的内存却往往是很小(远远小于一页)的内存块——比如存放文件描述符、进程描述符、虚拟内存区域描述符等行为所需的内存都不足一页。这些用来存放描述符的内存相比页面而言,就好比是面包屑与面包。一个整页中可以聚集多个这些小块内存;而且这些小块内存块也和面包屑一样频繁地生成/销毁。
Slab技术不但避免了内存内部分片(下文将解释)带来的不便(引入Slab分配器的主要目的是为了减少对伙伴系统分配算法的调用次数——频繁分配和回收必然会导致内存碎片——难以找到大块连续的可用内存),而且可以很好地利用硬件缓存提高访问速度。
slab分配器专门用来分配小内存。其中,slab分配器将SLAB分为两大类:专用SLAB和普通SLAB。
专用SLAB用于特定的场合(比如TCP有自己专用的SLAB,当TCP模块需要小内存时,会从自己的SLAB中分配);
普通SLAB就是用于常规分配的时候。我们可以通过查看/proc/slabinfo看到slab的状态。
在slab中,可分配的内存块叫做对象。不同的slab所包含对象的大小也不同。如kmalloc-8这个普通SLAB,里面所有的对象都是8B大小,同理,kmalloc-16中的对象都是以16B为大小。申请内存时就会依据这些对象来划分,这样做可以减小内存碎片化。其中申请的对象释放后也会回到他的slab中。
Slab并非是脱离伙伴关系而独立存在的一种内存分配方式,slab仍然是建立在页面基础之上,换句话说,Slab将页面(来自于伙伴关系管理的空闲页面链表)撕碎成众多小内存块以供分配,slab中的对象分配和销毁使用kmem_cache_alloc与kmem_cache_free。

三、内存回收

内核内存管理的核心工作就是内存的分配回收管理,其内部分为2个体系:页管理和对象管理。页管理体系是一个两级的层次结构,对象管理体系是一个三级的层次结构,分配成本和操作对CPU cache和TLB的负面影响,从上而下逐渐升高。
页管理层次结构:由冷热缓存、伙伴系统组成的两级结构。负责内存页的缓存、分配、回收。
对象管理层次结构:由per-cpu高速缓存、slab缓存、伙伴系统组成的三级结构。负责对象的缓存、分配、回收。这里的对象指小于一页大小的内存块。
除了内存分配,内存释放也是按照此层次结构操作。如释放对象,先释放到per-cpu缓存,再释放到slab缓存,最后再释放到伙伴系统。
在这里插入图片描述在这里插入图片描述
具体内存回收机制:
当系统内存不足时,内核会启动内存回收。内存回收的时机有以下三种,
1、内存紧缺回收:在内存分配失败时,会直接调用try_to_free_pages()进行页面回收,以便尽快释放内存。这种方式被称作“直接页面回收”。
2、睡眠回收:在进入suspend-to-disk状态时,需要释放内存。
3、周期回收:守护进程 kswapd会定期检查系统可用内存,当剩余内存低于预定水位线时就会进行回收。另一个定期回收是reap_work,用来回收slab空闲页面。

在这里插入图片描述
3.1 shrink_zone函数回收
shrink_zone() 函数是 Linux 操作系统实现页面回收的最核心的函数之一,它实现了对一个内存区域的页面进行回收的功能,该函数主要做了两件事情:
将某些页面从 active 链表移到 inactive 链表,这是由函数 shrink_active_list() 实现的。
从 inactive 链表中选定一定数目的页面,将其放到一个临时链表中,这由函数 shrink_inactive_list() 完成。该函数最终会调用 shrink_page_list() 去回收这些页面。
在这里插入图片描述
shrink_page_list() 返回的是回收成功的页面数目,对于可进行回收的页面,该函数主要做了这样几件事情:
对于匿名页面来说,在回收此类页面时,需要将其数据写入到交换区。如果尚未为该页面分配交换区槽位,则先分配一个槽位,并将该页面添加到交换缓存。同时,将相关的 page 实例加入到交换区,这样,对该页面的处理就可以跟其他已经建立映射的页面一样;
如果该页面已经被映射到一个或者多个进程的页表项中,那么必须找到所有引用该页面的进程,并更新页表中与这些进程相关的所有页表项。在这里,Linux 操作系统会利用反向映射机制去检查哪些页表项引用了该页面;
如果该页面中的数据是脏的,那么数据必须要被回写,释放页缓存中的干净页面。

3.2 shrink_slab函数回收
shrink_slab() 是用来回收磁盘缓存所占用的页面的。Linux操作系统并不清楚这类页面是如何使用的,所以如果希望操作系统回收磁盘缓存所占用的页面,那么必须要向操作系统内核注册 shrinker 函数,shrinker 函数会在内存较少的时候主动释放一些该磁盘缓存占用的空间。函数 shrink_slab() 会遍历shrinker链表,从而对所有注册了 shrinker函数的磁盘缓存进行处理。注册shrinker是通过函数 register_shrinker() 实现的,解除shrinker注册是通过函数unregister_shrinker()实现的。从实现上来看,shrinker函数和slab分配器并没有固定的联系,只是当前主要是slab缓存使用shrinker函数最多。shrinker的用法不仅限于回收磁盘缓存。

3.3 kswapd内存回收机制
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,选择合适的时机启动后台内存回收。

四、内存访问

高速缓冲存储器是存在于主存与CPU之间的一级存储器, 由静态存储芯片(SRAM)组成,容量比较小但速度比主存高得多,接近于CPU的速度。 Cache的功能是用来存放那些近期需要运行的指令与数据。目的是提高CPU对存储器的访问速度。为此需要解决2个技术问题:
一是主存地址与缓存地址的映象及转换;
二是按一定原则对Cache的内容进行替换。
Cache主要由三大部分组成:

  1. Cache存储体:存放由主存调入的指令与数据块。
  2. 地址转换部件:建立目录表以实现主存地址到缓存地址的转换。
  3. 替换部件:在缓存已满时按一定策略进行数据块替换,并修改地址转换部件。

4.1 内存映射
内存映射(mmap)是Linux操作系统的一个很大特色,它可以将系统内存映射到一个文件(设备)上,以便可以通过访问文件内容来达到访问内存的目的。这样做的最大好处是提高了内存访问速度,并且可以利用文件系统的接口编程(设备在Linux中作为特殊文件处理)访问内存,降低了开发难度。许多设备驱动程序便是利用内存映射功能将用户空间的一段地址关联到设备内存上,无论何时,只要内存在分配的地址范围内进行读写,实际上就是对设备内存的访问。同时对设备文件的访问也等同于对内存区域的访问,也就是说,通过文件操作接口可以访问内存。Linux中的X服务器就是一个利用内存映射达到直接高速访问视频卡内存的例子。

熟悉文件操作的朋友一定会知道file_operations结构中有mmap方法,在用户执行mmap系统调用时,便会调用该方法来通过文件访问内存——不过在调用文件系统mmap方法前,内核还需要处理分配内存区域(vma_struct)、建立页表等工作。对于具体映射细节不作介绍了,需要强调的是,建立页表可以采用remap_page_range方法一次建立起所有映射区的页表,或利用vma_struct的nopage方法在缺页时现场一页一页的建立页表。第一种方法相比第二种方法简单方便、速度快, 但是灵活性不高。一次调用所有页表便定型了,不适用于那些需要现场建立页表的场合——比如映射区需要扩展或下面我们例子中的情况。
1、直接映射
一个内存地址能被映射到的Cache line是固定的。就如每个人的停车位是固定分配好的,可以直接找到。缺点是:因为人多车位少,很可能几个人争用同一个车位,导致Cache淘汰换出频繁,需要频繁的从主存读取数据到Cache,这个代价也较高。
在这里插入图片描述

地址变换过程:用主存地址中的块号B去访问目录存储器, 把读出来的区号与主存地址中的区号E进行比较, 比较结果相等,有效位为1,则Cache命中,可以直接用块号及块内地址组成的缓冲地址到缓存中取数;比较结果不相等,有效位为1, 可以进行替换,如果有效位为0,可以直接调入所需块。
优点:地址映象方式简单,数据访问时,只需检查区号是否相等即可,因而可以得到比较快的访问速度,硬件设备简单。
缺点:替换操作频繁,命中率比较低。

2、全相联映射
主存中的一个地址可被映射进任意cache line,问题是:当寻找一个地址是否已经被cache时,需要遍历每一个cache line来寻找,这个代价很高。就像停车位可以大家随便停一样,停的时候简单,找车的时候需要一个一个停车位的找了。
主存中任何一块都可以映射到Cache中的任何一块位置上。
在这里插入图片描述

全相联映射方式比较灵活,主存的各块可以映射到Cache的任一块中,Cache的利用率高,块冲突概率低,只要淘汰Cache中的某一块,即可调入主存的任一块。但是,由于Cache比较电路的设计和实现比较困难,这种方式只适合于小容量Cache采用。
优点:命中率比较高,Cache存储空间利用率高。
缺点:访问相关存储器时,每次都要与全部内容比较,速度低,成本高,因而应用少。

3、组相联映射
组相联映射实际上是直接映射和全相联映射的折中方案,其组织结构如图所示。
在这里插入图片描述

主存和Cache都分组,主存中一个组内的块数与Cache中的分组数相同,组间采用直接映射,组内采用全相联映射。主存块存放到哪个组是固定的,至于存到该组哪一块则是灵活的。即主存的某块只能映射到Cache的特定组中的任意一块。
优点:块的冲突概率比较低,块的利用率大幅度提高,块失效率明显降低。
缺点:实现难度和造价要比直接映象方式高。

4.2 替换策略
根据程序局部性规律可知:程序在运行中,总是频繁地使用那些最近被使用过的指令和数据。这就提供了替换策略的理论依据。综合命中率、实现的难易及速度的快慢各种因素,替换策略可有随机法、先进先出法、最近最少使用法等。
1.随机法(RAND法)
随机法是随机地确定替换的存储块。设置一个随机数产生器,依据所产生的随机数,确定替换块。这种方法简单、易于实现,但命中率比较低。
2.先进先出法(FIFO法)
先进先出法是选择那个最先调入的那个块进行替换。当最先调入并被多次命中的块,很可能被优先替换,因而不符合局部性规律。这种方法的命中率比随机法好 些,但还不满足要求。先进先出方法易于实现,例如Solar-16/65机Cache采用组相联方式,每组4块,每块都设定一个两位的计数器,当某块被装 入或被替换时该块的计数器清为0,而同组的其它各块的计数器均加1,当需要替换时就选择计数值最大的块被替换掉。
3.最近最少使用法(LRU法)
LRU法是依据各块使用的情况, 总是选择那个最近最少使用的块被替换。这种方法比较好地反映了程序局部性规律。
实现LRU策略的方法有多种。 下面简单介绍计数器法、寄存器栈法及硬件逻辑比较对法的设计思路。
计数器方法:缓存的每一块都设置一个计数器,计数器的操作规则是:
(1) 被调入或者被替换的块, 其计数器清“0”,而其它的计数器则加“1”。
(2) 当访问命中时,所有块的计数值与命中块的计数值要进行比较,如果计数值小于命中块的计数值, 则该块的计数值加“1”;如果块的计数值大于命中块的计数值,则数值不变。最后将命中块的计数器清为0。
(3) 需要替换时,则选择计数值最大的块被替换。
例如IBM 370/65机的Cache用组相联方式,每组4块,每一块设置一个2位的计数器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值