内存管理
页
内核把物理页作为内存管理的基本单位。内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址)通常以页为单位进行处理。MMU以页大小为单位来管理系统中的页表。
从虚拟内存的角度看,页就是最小单位。
32位系统:页大小4KB
64位系统:页大小8KB
在支持4KB页大小并有1GB物理内存的机器上。物理内存会被划分为262144个页。
内核用 struct page 结构表示系统中的每一个物理页。
struct page {
page_flags_t flags; /* 表示页的状态。每一位表示一种状态*/
atomic_t _count; /* 存放页的引用计数,0代表没有被引用 */
atomic_t _mapcount;
unsigned long private;
strcut address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual; /* 页在虚拟内存中的地址,动态映射物理页 */
}
以下,我们来解释下当中的重要字段。
flags:这个字段用于存放页的状态。这些状态包含页是不是脏的,是不是被锁定在内存中等。 flag 的每一位单独表示一种状态。所以,它至少能够同一时候表示出32种不同的状态。
_count:这个字段存放页的使用计数,也就是这个页被引用了多少次。非常奇怪。技术值变为 -1 时,就说明当前内核并没有引用这一页。于是,在新的分配中就能够使用它,注意,这个字段使用的是 -1 代表未使用,而不是 0 。
virtual:这个字段是页的虚拟地址。
mapping:这个域指向和这个页关联的address_space 对象。
private:这个依据名字就能够看得出,它指向私有数据。
内核通过这种数据结构管理系统中全部的页。由于内核须要知道一个页是否空暇,谁有拥有这个页。拥有者可能是:用户空间进程、动态分配的内核数据、静态内核代码、页快速缓存等等。系统中每个物理页都要分配这样一个结构体,进行内存管理。
区
因为硬件的限制,内核并不能对全部的页一视同仁。Linux必须处理例如以下两种因为硬件存在缺陷而引起的内存寻址问题:
1)一些硬件仅仅能用某些特定的内存地址来运行DMA(直接内存訪问)。
2)一些体系结构其内存的物理寻址范围比虚拟寻址范围大得多。
这样,就有一些内存不能永久地映射到内核空间上。
因为存在这样的限制,内核把具有相似特性的页划分为不同的区(ZONE):
1)ZONE_DMA——这个区包括的页能用来运行DMA操作。
2)ZONE_NORMAL——这个区包括的都是能正常地映射网页。
3)ZONE_DMA32——同上,只是仅仅能被32位设备訪问
4)ZONE_HIGHMEM——这个区包括“高端内存”,当中的页并能不永久地映射到内核地址空间。
Linux把系统的页划分为区,形成不同的内存池,这样就能够依据用途进行分配。
注意。区的划分没有不论什么物理意义。这仅仅是内核为了管理页而採取的一种逻辑上的分组。用于DMA的内存必须从ZONE_DMA中进行分配。可是一般用途的内存却既能从ZONE_DMA分配,也能从ZONE_NORMAL分配。
获得页
内核提供了一种请求内存的底层机制,并提供了对它进行訪问的几个接口。全部这些接口都以页为单位分配内存。定义于<linux/gfp.h>。
最核心的函数是:
structpage *alloc_pages( unsigned int gfp_mask, unsigned int order );
该函数分配 2order 个连续的物理页,并返回一个指向第一页的 page 结构体指针,假设出错就返回NULL。
void*page_address( struct page *page );
把给定的页转换成它的逻辑地址。假设无须用到 struct page。能够调用:
unsignedlong __get_free_pages( unsigned int gfp_mask, unsigned int order );
这个函数与alloc_pages 作用同样,只是它直接返回所请求的第一个页的逻辑地址。由于页是连续的,因此其它页也会紧随其后。
假设仅仅须要一页,能够用下面两个函数:
structpage *alloc_page( unsigned int gfp_mask );
unsignedlong _get_free_page( unsigned int gfp_mask );
假设须要让返回页的内容全为0,能够使用以下这个函数
unsignedlong get_zeroed_page(unsigned int gfp_mask );
方法 |
描写叙述 |
alloc_page(gfp_mask) |
仅仅分配一页,返回指向页结构的指针 |
alloc_pages(gfp_mask, order) |
分配 2^order 个页,返回指向第一页页结构的指针 |
__get_free_page(gfp_mask) |
仅仅分配一页,返回指向其逻辑地址的指针 |
__get_free_pages(gfp_mask, order) |
分配 2^order 个页,返回指向第一页逻辑地址的指针 |
get_zeroed_page(gfp_mask) |
仅仅分配一页,让其内容填充为0,返回指向其逻辑地址的指针 |
当不再须要页时能够使用下面函数来释放它。
void__free_pages( struct page *page, unsigned int order );
voidfree_pages( unsigned long addr, unsigned int order );
voidfree_page( unsigned long addr );
释放页时要慎重,仅仅能释放属于你的页。传递了错误的 struct page 或地址,用了错误的 order 值都可能导致系统崩溃。请记住,内核是全然依赖自己的。
kmalloc()
kmalloc 与 malloc 一族函数很类似,仅仅只是它多了一个 flags 參数。kmalloc在<linux/slab.h>中声明:
void*kmalloc( size_t size, int flags );
这个函数返回一个指向内存块的指针,其内存块至少要有 size 大小。所分配的内存正在物理上是连续的。
在出错时,它返回 NULL。除非没有足够的内存可用。否则内核总能分配成功。
在对 kmalloc 调用之后,你必须检查返回的是不是 NULL,假设是,要适当地处理错误。
在低级页分配函数还是 kmalloc 中,都用到了gfp_mask(分配器标志)。这些标志可分为三类:行为修饰符、区修饰符及类型。
1)行为修饰符表示内核应当怎样分配所需的内存。在某些特定情况下,仅仅能使用某些特定的方法分配内存。
比如。中断处理程序就要求内核在分配内存的过程中不能睡眠(由于中断处理程序不能被又一次调度)。
2)区修饰符指明究竟从哪一区中进行分配。
3)类型标志组合了行为修饰符和区修饰符。将各种可能用到的组合归纳为不同类型。简化了修饰符的使用。
kmalloc 的还有一端就是 kfree,kfree声明于<linux/slab.h>中
voidkfree( const
void *ptr );
kfree 函数释放由 kmalloc分配出来的内存块。
调用 kfree( NULL ) 是安全的。
vmalloc()
vmalloc 的工作方式是类似于 kmalloc。仅仅只是前者分配的内存虚拟地址是连续的,而物理地址则无需连续。这也是用户空间分配函数的工作方式:由malloc()返回的页在进程的虚拟地址空间内是连续的,可是这并不保证他们在物理RAM中也是连续的。kmalloc()函数确保页在物理地址上是连续。vmalloc函数值确保在虚拟地址空间内是连续的。它通过分配非连续的物理内存块,在修订页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。
大多数情况下,唯独硬件设备须要得到物理地址连续的内存,由于硬件设备存在内存管理单元以外,它根本不理解什么是虚拟地址。虽然只在某些情况下才须要物理上连续的内存块,可是非常多内核都有kmalloc()来获取内存。而不是vmalloc()。这主要出于性能方面的考虑。vmalloc()函数为了把物理上不连续的页转换成虚拟地址空间上连续的页。必须专门建立页表项。糟糕的是,通过vmalloc()获得的页必须一个一个地进行映射。由于这些原因,通常是在为了获得大块内存时。比如当模块被动态插入内核时。就把模块装载到由vmalloc()分配的内存上。
void *vmalloc(unsigned long size)
该函数返回一个指针。指向逻辑上连续的一块内存。其大小至少为size。在错误发生时。函数返回NULL。
函数可能睡眠,因此么不能从中断上下文中进行调用。也不能从其它不同意堵塞的情况下进行调用。
释放通过vfree()函数
void vfree(const void *addr)
slab层
为了便于数据的频繁分配和回收,Linux内核提供了slab层(也就是所谓的slab分配器)。slab分配器扮演了通用数据结构缓存层的角色。
slab层把不同的对象划分为快速缓存。当中每一个快速缓存组中存放的都是不同类型的数据结构对象。比如,一个快速缓存用于存放进程描写叙述符,还有一个快速缓存用于存放i节点。
这些快速缓存又被划分为slab。slab由一个或多个物理上连续的页组成。普通情况下,slab也就只由一页组成。每一个快速缓存能够由多个slab组成。
每一个slab都包括一些对象成员。这里的对象指的是被缓存的数据结构。每一个slab处于三种状态之中的一个:满、部分满或空。当内核的某一部分须要一个对象时。就要由slab分配了,首先考虑的是部分满的slab。假设不存在部分满的slab则去空的slab分配,假设也不存在空的slab。则内核须要申请页又一次分配快速缓存。下图描写叙述了快速缓存、slab及对象之间的关系。来自http://www.cnblogs.com/wang_yb/archive/2013/05/23/3095907.html
整个slab层的原理例如以下:
1.能够在内存中建立各种对象的快速缓存(比方进程描写叙述相关的结构 task_struct
的快速缓存)
2.除了针对特定对象的快速缓存以外,也有通用对象的快速缓存
3.每一个快速缓存中包括多个 slab。slab用于管理缓存的对象
4.slab中包括多个缓存的对象,物理上由一页或多个连续的页组成
每一个快速缓存都是用kmem_cache_s 结构来表示。这个结构包括三个链表 slabs_full。slabs_partial和 slabs_empty。均存放在 kmem_lists 结构内。这些链表包括快速缓存中的全部slab。slab描写叙述符 structslab 用来描写叙述每一个slab:
struct slab {
struct list_head list; /* 满、部分满或空链表 */
unsigned long colouroff; /* slab 着色的偏移量 */
void *s_mem; /* 在 slab 中的第一个对象 */
unsigned int inuse; /* 已分配的对象数 */
kmem_bufctl_t tree; /* 第一个空间对象(假设有的话) */
};
slab分配器的接口
主要有四个
1. 快速缓存的创建
struct kmem_cache * kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))
2. 从快速缓存中分配对象
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
3. 释放对象。返回给原先的slab
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
4.快速缓存的销毁
void kmem_cache_destroy(struct kmem_cache *cachep)
slab解决内存碎片
内存碎片存在的方式有两种:a.内部碎片 b.外部碎片
内部碎片的产生:由于全部的内存分配必须起始于可被 4、8
或 16 整除(视处理器体系结构而定)的地址或者由于MMU的分页机制的限制,决定内存分配算法仅能把预定大小的内存块分配给客户。如果当某个客户请求一个 43 字节的内存块时,由于没有适合大小的内存,所以它可能会获得 44字节、48字节等稍大一点的字节,因此由所需大小四舍五入而产生的多余空间就叫内部碎片。
外部碎片的产生:
频繁的分配与回收物理页面会导致大量的、连续且小的页面块夹杂在已分配的页面中间。就会产生外部碎片。
假设有一块一共同拥有100个单位的连续空暇内存空间。范围是0~99。假设你从中申请一块内存,如10个单位,那么申请出来的内存块就为0~9区间。
这时候你继续申请一块内存。比方说5个单位大。第二块得到的内存块就应该为10~14区间。假设你把第一块内存块释放。然后再申请一块大于10个单位的内存块。比方说20个单位。由于刚被释放的内存块不能满足新的请求,所以仅仅能从15開始分配出20个单位的内存块。如今整个内存空间的状态是0~9空暇。10~14被占用。15~24被占用,25~99空暇。当中0~9就是一个内存碎片了。
假设10~14一直被占用,而以后申请的空间都大于10个单位,那么0~9就永远用不上了。变成外部碎片。
解决方法:
slab机制,由于slab预先分配了特定数据结构大小的内存,所以没有内部碎片或者外部碎片。
slab与传统内存管理模式比較:
与传统的内存管理模式相比。 slab 缓存分配器提供了非常多长处。
首先。内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。slab 缓存分配器通过对类似大小的对象进行缓存而提供这样的功能。从而避免了常见的碎片问题。slab 分配器还支持通用对象的初始化,从而避免了为同一目而对一个对象反复进行初始化。
最后。slab 分配器还能够支持硬件缓存对齐和着色,这防止错误的共享(两个或两个对象虽然位于不同的内存地址,但映射到同样的告诉缓冲行),这能够提高性能。但以添加内存浪费为代价。
在栈上的静态分配
内核栈大小固定。我们在进程时要注意节省栈资源,要控制函数内的局部变量。尽量不要出现大型数组或大型结构体。
尤其对于内核栈,一旦造成溢出,就会影响到内核数据(如thread_info)。所以应当优先考虑动态分配。另外一个进程的内核栈和中断栈是分开的,这样能够减轻内核栈的负担(一个内核栈仅仅占1页或2页)。
高端内存的映射
由于32位的处理器可以寻址达到4GB。一旦这些页被分配。就必须映射到内核的虚拟内存空间上。
高于896MB的全部物理内存的范围大都是高端内存,它不会永久或自己主动的映射到内核虚拟地址空间。
内核地址的虚拟内存大小为1G。当中0-896M的内存与物理内存一一映射,即线性映射。而896MB~1024MB的虚拟内存假设也与物理内存线性映射。那么内核态仅仅能使用1G的物理内存。即使物理内存大于1G(比方4G),这种话就没有充分利用物理内存了。所以内核虚拟内存中的896MB~1024MB与高端内存不会一一映射。详细的映射方式例如以下:
当内核态须要訪问高端物理内存时。在内核虚拟内存空间中的896-1024MB找一段对应大小空暇的逻辑地址空间。借用一会。借用这段逻辑地址空间,建立映射到想要訪问的那段物理内存,暂时用一会,用完后归还。
这样当进程后面又须要訪问其它的高端物理内存时。仍然能够用这段逻辑地址空间。
高端内存的最基本思想:在内核虚拟空间896MB~1024MB的内存中借一段地址空间,建立与高端物理内存的暂时地址映射,用完后释放虚拟空间。达到这段虚拟地址空间能够循环使用,訪问全部物理内存。
高端内存映射有三种方式:
1、映射到“内核动态映射空间”
这样的方式非常easy。由于通过 vmalloc() ,在”内核动态映射空间“申请内存的时候,就可能从高端内存获得页面(參看 vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间“ 中。
2、永久内核映射
假设是通过alloc_page() 获得了高端内存相应的 page,怎样给它找个线性空间?
内核专门为此留出一块线性空间。从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。在 2.4 内核上,这个地址范围是 4G-8M 到 4G-4M 之间。
这个空间起叫“内核永久映射空间”或者“永久内核映射空间”。
这个空间和其他空间使用相同的页文件夹表,对于内核来说,就是 swapper_pg_dir,对普通进程来说。通过 CR3 寄存器指向。
通常情况下,这个空间是 4M 大小,因此只须要一个页表就可以,内核通过来 pkmap_page_table 寻找这个页表。
3、暂时映射
当必须创建一个映射而当前的上下文又不能睡眠时。内核提供了暂时映射(也就是原子映射)。有一组保留的映射。他们能够存放新创建的暂时映射。内核能够原子地把高端内存中的一个页映射到某个保留的映射中。因此,暂时映射能够用在不能睡眠的地方,比方中断处理程序中,由于获取映射时绝不会堵塞。
每一个CPU数据
SMP环境下加锁过多的话,会严重影响并行的效率,假设是自旋锁的话。还会浪费其它CPU的运行时间。
所以内核中才有了按CPU分配数据的接口。按CPU分配数据之后。每一个CPU自己的数据不会被其它CPU訪问。尽管浪费了一点内存,可是会使系统更加的简洁高效。
按CPU来分配数据主要有2个长处:
1.最直接的效果就是降低了对数据的锁,提高了系统的性能
2.由于每一个CPU有自己的数据,所以处理器切换时能够大大降低缓存失效的几率。由于假设一个处理器操作某个数据。而这个数据在还有一个处理器的缓存中时,那么存放这个数据的那个处理器必须清理或刷新自己的缓存。持续的缓存失效成为缓存抖动。对系统性能影响非常大。