网站地图    收藏   

主页 > 系统 > linux系统 >

Linux内核——内存管理 - Linux操作系统:Ubuntu_Ce

来源:自学PHP网    时间:2015-04-14 10:17 作者: 阅读:

[导读] 内存管理页内核把物理页作为内存管理的基本单位;内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址)通常以页为单位进行处理。MMU以页大小为单位来管理系统中的页表。从...

内存管理

内核把物理页作为内存管理的基本单位;内存管理单元(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

\

<喎"http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+1fu49nNsYWKy47XE1K3A7cjnz8KjujwvcD4KPHA+MS6/ydLU1NrE2rTm1tC9qMGiuPfW1rbUz/O1xLjfy9m7urTmKLHIyOe9+LPMw+jK9s/gudi1xL3hubkgdGFza19zdHJ1Y3QKtcS438vZu7q05ik8L3A+CjxwPjIus/3By9XrttTM2LaottTP87XEuN/L2bu6tObS1M3io6zSstPQzajTw7bUz/O1xLjfy9m7urTmPC9wPgo8cD4zLsO/uPa438vZu7q05tbQsPy6rLbguPYgc2xhYqOsc2xhYtPD09q53MDtu7q05rXEttTP8zwvcD4KPHA+NC5zbGFi1tCw/LqstuC49ru6tOa1xLbUz/OjrM7vwO3Jz9PJ0rvSs7vytuC49sGs0Pi1xNKz1+mzyTwvcD4KPHA+w7+49rjfy9m7urTmtrzKx9PDa21lbV9jYWNoZV9zIL3hubnAtLHtyr6ho9XiuPa94bm5sPy6rMj9uPbBtLHtIHNsYWJzX2Z1bGyjrHNsYWJzX3BhcnRpYWy6zSBzbGFic19lbXB0eaOsvvm05rfF1Noga21lbV9saXN0cyC94bm5xNqho9Xi0KnBtLHtsPy6rLjfy9m7urTm1tC1xMv509BzbGFioaNzbGFiw+jK9rf7IHN0cnVjdHNsYWIg08PAtMPoyvbDv7j2c2xhYqO6PC9wPgo8cD5zdHJ1Y3Qgc2xhYiB7PC9wPgo8cD4gICAgc3RydWN0IGxpc3RfaGVhZCBsaXN0OyAgICAgICAgIC8qIML6oaKyv7fWwvq78r/VwbSx7SAqLzwvcD4KPHA+ICAgIHVuc2lnbmVkIGxvbmcgY29sb3Vyb2ZmOyAgLyogc2xhYiDXxcmrtcTGq9LGwb8gICAqLzwvcD4KPHA+ICAgIHZvaWQgKnNfbWVtOyAgICAgICAgICAgICAgICAgIC8qINTaIHNsYWIg1tC1xLXa0ru49rbUz/MgKi88L3A+CjxwPiAgICB1bnNpZ25lZCBpbnQgaW51c2U7ICAgICAgICAgIC8qINLRt9bF5LXEttTP88r9ICAgICAgICAqLzwvcD4KPHA+ICAgIGttZW1fYnVmY3RsX3QgdHJlZTsgICAgICAgICAvKiC12tK7uPa/1bzkttTP86OoyOe5+9PQtcS7sKOpICovPC9wPgo8cD59OzwvcD4KPGgyPnNsYWK31sXkxve1xL3Tv9o8L2gyPgo8cD7W99Kq09DLxLj2PC9wPgo8cHJlIGNsYXNzPQ=="brush:java;">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、映射到“内核动态映射空间”

这种方式很简单,因为通过 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有自己的数据,所以处理器切换时可以大大减少缓存失效的几率。因为如果一个处理器操作某个数据,而这个数据在另一个处理器的缓存中时,那么存放这个数据的那个处理器必须清理或刷新自己的缓存。持续的缓存失效成为缓存抖动,对系统性能影响很大。

自学PHP网专注网站建设学习,PHP程序学习,平面设计学习,以及操作系统学习

京ICP备14009008号-1@版权所有www.zixuephp.com

网站声明:本站所有视频,教程都由网友上传,站长收集和分享给大家学习使用,如由牵扯版权问题请联系站长邮箱904561283@qq.com

添加评论