Linux内核——内存管理

内存管理

内核把物理页作为内存管理的基本单位;内存管理单元(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、映射到“内核动态映射空间”

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

Linux内核——内存管理,布布扣,bubuko.com

时间: 2024-10-03 08:38:51

Linux内核——内存管理的相关文章

linux内核 内存管理

以下内容汇总自网络. 在早期的计算机中,程序是直接运行在物理内存上的.换句话说,就是程序在运行的过程中访问的都是物理地址. 如果这个系统只运行一个程序,那么只要这个程序所需的内存不要超过该机器的物理内存就不会出现问题,我们也就不需要考虑内存管理这个麻烦事了,反正就你一个程序,就这么点内存,吃不吃得饱那是你的事情了. 然而现在的系统都是支持多任务,多进程的,这样CPU以及其他硬件的利用率会更高,这个时候我们就要考虑到将系统内有限的物理内存如何及时有效的分配给多个程序了,这个事情本身我们就称之为内存

linux内核内存管理(zone_dma zone_normal zone_highmem)

Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据,因为Linux使用的虚拟内存机制,用户空间的数据可能被换出,当内核空间使用用户空间指针时,对应的数据可能不在内存中.    Linux内核地址空间划分 通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间.注意这里是32位内核地址空间划分,64位内核地址空间划分是不同的. 1.x86的物理地址空间布局:   物理地址空间的顶部以下一段空间,被PCI设备的I/O内存映射占据,

Linux内核内存管理子系统分析【转】

本文转载自:http://blog.csdn.net/coding__madman/article/details/51298718 版权声明:本文为博主原创文章,未经博主允许不得转载. 还是那张熟悉的老图:Linux内核子系统简介(由七个部分组成) Linux内存管理模型: 1. 内存管子系统职能: 1>  管理虚拟地址与物理地址的映射 2>  管理物理内存的分配 2. 地址映射管理 1> 虚拟地址空间分布: linux采用的是一种虚拟地址的管理方式,对于一个32位的处理器对于的内存空

Linux内核内存管理-内存访问与缺页中断【转】

转自:https://yq.aliyun.com/articles/5865 摘要: 简单描述了x86 32位体系结构下Linux内核的用户进程和内核线程的线性地址空间和物理内存的联系,分析了高端内存的引入与缺页中断的具体处理流程.先介绍了用户态进程的执行流程,然后对比了内核线程,引入高端内存的概念,最后分析了缺页中断的流程. 用户进程 fork之后的用户态进... 简单描述了x86 32位体系结构下Linux内核的用户进程和内核线程的线性地址空间和物理内存的联系,分析了高端内存的引入与缺页中断

LINUX内核内存管理kmalloc,vmalloc

一.kmalloc与vmallco 在设备驱动程序或者内核模块中动态开辟内存,不是用malloc,而是kmalloc ,vmalloc,释放内存用的是kfree,vfree,kmalloc函数返回的是虚拟地址(线性地址). kmalloc特殊之处在于它分配的内存是物理上连续的,这对于要进行DMA的设备十分重要. 而用vmalloc分配的内存只是线性地址连续,物理地址不一定连续,不能直接用于DMA.vmalloc函数的工作方式类似于kmalloc,只不过前者分配的内存虚拟地址是连续的,而物理地址则

linux内核内存分配(三、虚拟内存管理)

在分析虚拟内存管理前要先看下linux内核内存的详细分配我开始就是困在这个地方,对内核内存的分类不是很清晰.我摘录其中的一段: 内核内存地址 =========================================================================================================== 在linux的内存管理中,用户使用0-3GB的地址空间,而内核只是用了3GB-4GB区间的地址空间,共1GB:非连 续空间的物理映射就位于3G

玩转Linux之内存管理-free

玩转Linux之内存管理-free free命令可以显示Linux系统中空闲的.已用的物理内存及swap内存,及被内核使用的buffer.在Linux系统监控的工具中,free命令是最经常使用的命令之一.下面给出一个free命令的栗子: 1 [[email protected] ~]# free 2 total used free shared buffers cached 3 Mem: 8062392 2092832 5969560 0 187132 1498832 4 -/+ buffers

linux内核内存分配(二、struct slab和struct kmem_cache)

前一篇bloglinux内核内存分配(一.基本概念)主要是分析linux内核内存的分配和物理页分配函数接口.但是在实际的操作中,不一定所有内存申请都需要一个物理页,很多只是需要分配几K大小的内存就可以.所以就需要更小的内存分配函数.刚开始看这个有点不懂,不过懂了就很简单了.哈哈. slab思想 摘抄<深入linux设备驱动程序内核机制>的一段话:slab分配器的基本思想是,先利用页面分配器分配出单个或者一组连续的物理页面,然后在此基础上将整块页面分割成多个相等的小内存单元,以满足小内存空间分配

从一道面试题(死循环里分配内存)阐述Linux的内存管理

题目: int cnt = 0; while(1) { ++cnt; ptr = (char *)malloc(1024*1024*128); if(ptr == NULL) { printf("%s\n", "is null"); break; } } printf("%d\n", cnt); 这个程序会有怎样的输出呢? 结果在Linux32位机是 is null 3057 为嘛是3057?? 因为用户态虚拟内存地址空间是3G. 3057M 大