1、页
- 内核把物理页作为内存管理的基本单元
- 内存管理单元(MMU)以页为单位来管理系统中的页表
- 从虚拟内存的角度看,页就是最小单位。
- 体系结构不同,支持页的大小也不尽相同。大多数32位体系结构支持4KB的页,而64位体系结构一般会支持8KB的页。
- 内核用
struct page
结构表示系统中的每个物理页,该结构位于<linux/mm.h>
中。下面是简化后的结构体:struct page { unsigned long flags;//存放页的状态,每一位单独表示一种状态,至少可以同时表示出32种状态 atomic_t _count ;//存放这一页的引用次数,当页空闲时,就可以在新的分配中使用它 atomic_t _mapcount; unsigned long private;//作为私有数据使用 struct address_space *mapping;//一个页可以由页缓存使用,这时,mapping域指向和这个页关联的address_sapce对象。 pgoff_t index; struct list_head lru; void *virtual;//页的虚拟地址,通常情况下他就是页在虚拟内存种的地址。 };
page结构
与物理页相关,而并非与虚拟页相关,内核仅仅用这个数据结构来描述当前时刻在相关的物理页中存放的东西,系统中的每个物理页都要分配一个这样的结构。
2、区
- 由于硬件的限制,内核并不能对所有的页一视同仁,内核把页划分为不同的区。Linux使用了三种区:
ZONE_DMA
:这个区包含的页能用来执行DMA
操作ZONE_NOEMAL
:这个区包含的都是能正常映射的页ZONE_HIGHMEM
:这个区包含高端内存,其中的页不能永久映射到内核地址空间。
- 这些区定义位于
linux/mmzone.h
。区的实际使用和分布是与体系结构相关的。在x86
上的区分布如下表:
- Linux通过区的划分形成不同的内存池,这样就可以根据用途分配,注意这样的划分是逻辑意义上的划分;
- 某些特定的分配需要特定的区域,但某些一般用途的分配可以从两个不同的类型的任意一种分配区域(注意不能同时分配两种区的页,即存在区限界)。比如:一般用途的内存在可
ZONE_NOEMAL
不够用的情况下会访问ZONE_DMA
。 - 每个区都用
struct zone
表示,定义在linux/mmzone.h
中struct zone { unsigned long watermark [NR_WMARK] ; unsigned long lowmem_reserve [MAX_NR__ZONES] ; struct per_cpu_pageset pageset [NR_CPUS] ; spinlock_t lock; struct free_area free_area [MAX_ORDER] spinlock_t lru_lock ; struct zone_lru { struct list_head list; unsigned longnr_saved_scan; } lru [NR_LRU_LISTS] ; struct zone_reclaim_stat reclain_stat; unsigned long pages_scanned; unsigned long flags; atomic_long_t vmn_stat [NR_VM_ZONE_STAT_ITEMS]; int prev_priority; unsigned int inactive_ratio; wait_queue_head_t *wait_table; unsigned long wait_table_hash_nr_entries; unsigned long wait__table_bits; struct pglist_data *zone_pgdat; unsigned long zone_start _pfn; unsigned long spanned pages; unsigned long present_pages; const char *name; };
- 区的结构表示
lock
:表示自旋锁,方式该结构被并发访问,但这个锁保护不了这个区的所有页,只单单保护这个结构watermark
:该数组有本区的最小值、最低和最高水位值,水位被内核设置每个区的内存消耗基准,随空间大小的改变而改变name
:表示该区的名字,分别为:DMA
、Normal
和HighMem
。
3、获得页
- 分配2的order次方个页,返回第一页的页结构指针。
使用下面的函数把给定的页转换为它的逻辑地址:struct page *alloc_pages(unsigned int gfp_mask, unsigned int order);
void *page_address(struct page *page);
- 分配2的order次方个页,直接返回指向其逻辑地址的指针(注意与前者之间的区别):
unsigned long __get_free_pages(unsigned int gfp_make,unsigned int order);
- 分配1页,返回页结构指针:
struct page *alloc_page(gfp_t gfp_mask);
- 分配1页,返回它的逻辑地址:
unsigned long __get_free_page(gfp_t gfp_mask);
- 获得填充为0的页:
采取这种方式的理由很简单,避免将敏感信息传递给客户。unsigned long get_zeroed_page(unsigned int gfp_mask)
- 释放页
注意:释放页的时候需要谨慎,只能释放属于自己的页。传递错误的参数可能会导致系统崩溃。void __free_pages(struct page *page,unsigend int order); void free_pages(unsigned long addr,unsigend int order); void free_page(unsigned long addr);
4、kmalloc()
kmalloc()
函数与用户空间的malloc()
一族函数非常类似,只不过它多了一个flags
参数。kmalloc
函数是一个简单的接口,用它可以获得以字节为单位的一块内核内存。注意:第3节重点讲述的是以页为单位的分配。kmalloc()
在linux/slab.h
中声明:
这个函数返回一个指向内存块的指针,其内存块至少要有void *kmalloc(size_t size, gfp_t flags);//size:字节数 flags:分配标志
size
大小,所分配的内存区在物理上是连续的(第5节介绍的vmalloc就是非连续的),在出错时,它返回NULL
。
-
gfp_mask
标志
不管在低级页分配函数中,还是在kmalloc()
中,都用到了分配器标志。这些标志可分为三类:行为修饰符、区修饰符及类型。行为修饰符表示内核应当如何分配所需的内存,例如,中断处理程序就要求内核在分配内存的过程中不能睡眠。区修饰符表示从哪儿分配内存。类型标志组合了行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型。所有这些标志都是在linux/gfp.h
中申明的。- 行为修饰符
- 区修饰符
- 类型标志
每种类型标志后隐含的修饰符列表:
- 标志的使用情景
- 行为修饰符
-
kfree()
kfree
释放由kmalloc
分配出来的内存块。如果想要释放的内存不是由kmalloc()
分配的,或者想要释放的内存早就被释放完了,调用这个函数会导致严重的后果。
5、vmalloc()
vmalloc()
是用户空间分配函数的工作方式vmalloc()
与kmalloc()
很像,他们的区别是:vmalloc()
确保虚拟地址空间内连续,分配非连续的物理内存块kmalloc()
确保虚拟地址空间内连续,分配连续的物理内存块
- 硬件设备根本不理解什么是虚拟地址。硬件设备需要得到连续的物理内存块,而软件就可以使用仅虚拟地址空间连续的内存块。
- 出于性能考虑,很多内核代码都使用
kmalloc()
来获得内存,而非vmalloc()
。 vmalloc()
函数通过分配非连续的物理内存块,再修正页表,把内存映射到逻辑地址空间的连续区域中vmalloc()
函数在linux/vmalloc.h中
声明,在mm/vmalloc.c
中定义。
该函数返回一个指针,指向逻辑上连续的一块内存区,其大小至少为size。在发生错误时,函数返回NULL。函数可能睡眠,因此,不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下使用。void *vmalloc(unsigned long size);
- 释放通过
vmalloc()
所获得的内存。
这个函数会释放从void vfree(void *addr);
addr
开始的内存块,addr
是由vmalloc
分配的内存块地址。这个函数也可以睡眠,因此,不能从中断上下文中调用,它没有返回值。
6、slab层
- 背景
空闲链表在很多的地方都出现过,为了应对频繁的分配和回收,链表中存放预先设置好的数据结构,需要就抓取一个,不用分配内存,不需要就回收而不是释放;所以空闲链表相当于对象高速缓存——快速存储频繁使用的对象类型。 - Linux中的空闲链表
在内核中,空闲链表面临的挑战就是不能全局控制,当可用内存紧缺时,内核无法通知每个链表,让其收缩缓存的大小来补充一些内存出来,所以Linux内核提供slab层(slab分配器),扮演着通用数据结构缓存层的角色 - slab需要满足的基本原则
- 频繁分配和释放的数据结构应当缓存
- 频繁分配和释放会造成内存碎片,空闲链表的缓存会连续存放
- 回收的对象可以立即投入下一次分配,空闲链表提高性能
- 分配器知道对象大小、页大小和缓存的大小可以做出更明确的选择
- 如果部分缓存只属于某个单处理器则不需要加锁
- 若分配器是基于非统一内存访问(NUMA:在NUMA下,处理器访问它自己的本地存储器的速度比非本地存储器(存储器的地方到另一个处理器之间共享的处理器或存储器)快一些),可以从相同的内存节点进行分配
- 对存放在缓存中的对象进行标记,防止将多个对象映射到同一个缓存行中
- slab层的设计
- 概述
slab层把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象。每种对象类型对应一个高速缓存。例如,一个高速缓存用于存放进程描述符(task_struct结构的一个空闲链表),而另一个高速缓存存放索引节点对象(struct inode)。 - slab
每个缓存又被划分为多个slab,slab由一个或多个物理上连续的页组成(一般由一个页组成),每个slab都包含一些被缓存的数据结构等,每个slab处于三种状态之一:满、部分满或空,当内核分配时先从部分满开始寻找,如果没有则再去空的slab,如果没有创建空的则创建一个。下面是高速缓存、slab和对象之间的关系。
- slab的结构
缓存由kmen_cache
所表示,里面包含上面提到的三种链表:满、部分满和空,链表包含缓存中的所有slab。struct slab { struct list_head list; /*满、部分满或空链表*/ unsigned long co1ouroff ; /*slab着色的偏移量*/ void s_mem; /*在slab中的第一个对象*/ unsigned int inuse; /*slab中已分配的对象数*/ kmem_bufctl_t free; /*第一个空闲对象(如果有的话)*/ } ;
- 创建新的slab函数
通过__get_free_pages()
低级内核页分配器进行static inline void * kmem_getpages(struct kmem_cache *cachep,gfp_t flags){ void *addr; flags |= cachep->gfpflags;//把高速缓存需要的缺省标志加到flags参数上 addr = (void*) _get_free_pages(flags,cachep->gfporder) ;//分配页的大小为2的幂次方 return addr; }
- slab释放函数
当内存变得紧缺时,系统试图释放出更多的内存以供使用,或者当高速缓存显示地被取消地时候,就会调用slab释放函数。调用方法:kmem_freepages()
释放内存,最终实际上调用的是free_pages()
- 概述
- slab分配器的接口
- 这一节中讲的是高速缓存和对象 的 创建和销毁,也就是slab的接口。有关slab的操作在上一节有介绍到。
- 高速缓存的创建
kmem_cache_t *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *, kmem_cache_t *, unsigned long), void (*dtor)(void *, kmem_cache_t *, unsigned long));
- 第一个参数是字符串,存放着高速缓存的名字。第二个参数是高速缓存中每个元素的大小,第三个参数就是高速缓存内第一个对象的偏移,这用来确保在页内进行特定的对齐,通常情况,
0
就可以满足要求,也就是标准对齐。flags是可选的设置项,用来控制高速缓存的行为。 - 最后两个参数
ctor
和dtor
分别是高速缓存的构造和析构函数。只有在新的页追加到高速缓存时,构造函数才被调用。只有从高速缓存中删去页时,析构函数才被调用。有一个析构函数就要有一个构造函数,实际上,Linux内核的高速缓存不使用析构函数或构造函数,可以将这两个参数都赋值为NULL
。 kmem_cache_create()
在成功时会返回一个指向所创建高速缓存的指针,否则,返回NULL。这个函数不能在中断上下文中调用,因为它可能会睡眠。
- 第一个参数是字符串,存放着高速缓存的名字。第二个参数是高速缓存中每个元素的大小,第三个参数就是高速缓存内第一个对象的偏移,这用来确保在页内进行特定的对齐,通常情况,
- 销毁一个高速缓存
同样,也不能从中断上下文中调用这个函数,因为它也可能会睡眠。调用该函数之前必须确保以下两个条件:int kmem_cache_destroy(kmem_cache_t *cachep);
- 高速缓存中的所有
slab
都必须为空 - 在调用
kmem_cache_destroy()
期间,不能再访问这个高速缓存
- 高速缓存中的所有
- 创建高速缓存之后,就可以通过下列函数从中获取对象:(注意slab和对象的关系)
该函数从给定的高速缓存void *kmem_cache_alloc(kmem_cache_t *cachep,int flags);
cachep
中返回一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmem_getpages()
获取新的页,flags的值传递给__get_free_pages()
。 - 最后释放一个对象,并把它返回给原先的slab,可以使用下面的函数:
这样就能把高速缓存void kmem_cache_free(kmem_cache_t *cachep,void *objp);
cachep
中的对象objp
标记为空闲了。
7、在栈上静态分配
- 内核栈可以是1页,也可以是两页,这取决于编译时配置选项,栈大小因此在4KB-16KB的范围内。历史上,中断处理程序和被中断进程共享一个内核栈,当1页栈的选项被激活时,中断处理程序就获得了自己的栈。
8、高端内核的映射
- 根据定义,在高端内存中的页不能永久映射到内核地址空间上。在x86体系结构上,高于896的所有物理内存的范围大都是高端内存,它并不会永久地或自动映射到内核地址空间。
- 永久映射
- 要映射一个给定的
page
结构到内核地址空间,可以使用:
这个函数在高端内存或低端内存都能用。如果page结构对应的是低端内存中的一页,函数只会单纯地返回该页的虚拟地址。如果页位于高端内存,则会建立一个永久映射,再返回地址。这个函数可以睡眠。因此void *kmap(struct page *page);
kmap()
只能用在进程上下文中。 - 当不再需要高端内存时,应该解除映射,可以通过
kunmap
函数完成:void kunmap(struct page *page);
- 要映射一个给定的
- 临时映射
当必须创建一个映射而当前上下文又不能睡眠时,内核提供了临时映射(也就是所谓的原子映射)。有一组保留的映射,它们可以存放新创建的临时映射,内核可以原子地把高端内存中的一个页映射到某个保留的映射中。因此,临时映射可以用在不能睡眠的地方,比如中断处理程序中,因为获取映射时绝不会阻塞。- 可以通过下列函数创建一个临时映射:
void *kmap_atomic(struct page *page,enum km_type type);
- 可通过下列函数取消映射:
void kunmap_atomic(void *kvaddr.enum km_type type);
- 可以通过下列函数创建一个临时映射:
9、CPU的接口
- 为了方便创建和操作每个CPU数据,从而引进了新的操作接口,称作
percpu
。该接口使每个CPU数据的创建和操作得以简化。头文件linux/percpu.h
声明了所有的接口操作例程,可以在文件mm/slab.c
和asm/percput.h
中找到它们的定义。 - 编译时的每个CPU数据
- 在编译时定义每个CPU变量很容易:
DEFINE_PER_CPU(type,name);
- 这个语句为系统中的每个处理器都创建一个类型为
type
,名字为name
的变量实例。如果你需要在别处声明变量,以防编译时警告,那么下面的宏将是你的好帮手:DECLARE_PER_CPU(type,name);
- 你可以用
get_cpu_var()
和put_cpu_var()
例程操作变量。调用get_cpu_var()
返回当前处理器上的指定变量,同时它将禁止抢占,另一方面put_cpu_var()
将相应地重新激活抢占(看到代码并没有进行加锁,这时因为操作的数据对于当前的处理器是唯一的,不存在并发问题,唯一会产生同步问题的因素是内核抢占):get_cpu_var(name)++; /*增加该处理器上的name变量的值*/ put_cpu_var(name); /*完成;重新激活内核抢占*/
- 在编译时定义每个CPU变量很容易:
- 运行时每个CPU数据
- 内核实现每个CPU数据的动态分配方法类似于
kmalloc()
。这些方法原型在文件linux/percpu.h
中
宏void alloc_percpu(type); /*一个宏*/ void *__alloc_percpu(size_t size,size_t align); void free_percpu(const void *);
alloc_percpu()
给系统中的每个处理器分配一个指定类型对象的实例。它其实是宏__alloc_percpu()
的一个封装,这个原始宏接受的参数有两个,一个是要分配的实际字节数,一个是分配时按多少字节对齐。而封装后的alloc_percpu()
按照给定类型的自然边界对齐。调用free_percpu()
将释放所有处理器上指定的每个CPU数据。 - 无论是
alloc_percpu()
还是__alloc_percpu()
都会返回一个指针,它用来间接引用动态创建的每个CPU数据,内核提供了两个宏来利用指针获取每个CPU数据:get_cpu_ptr(ptr); put_cpu_ptr(ptr);
get_cpu_ptr()
宏返回一个指向当前处理器数据的实例,它同时会禁止内核抢占,而在put_cpu_ptr()
宏中重新激活内核抢占。
- 内核实现每个CPU数据的动态分配方法类似于
10、使用每个CPU数据的原因
- 减少了数据锁定:因为按照每个处理器访问每个CPU数据的逻辑,你可以不在需要任何锁。
- 使用每个CPU数据可以大大减少缓存失效。失效发生在处理器试图使它们的缓存保持同步时,如果一个处理器操作某个数据,而该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须清理或刷新它自己的缓存。使用每个CPU数据将使缓存影响降至最低,因为理想情况下只会访问它自己的数据。
percpu
接口缓存对齐所有数据,以便确保在访问一个处理器的数据时,不会将另一个处理器的数据带入同一个缓存线上。
11、分配函数的选择
- 如果你需要连续的物理页,就可以使用某个低级页分配器或
kmalloc
。 - 如果你想从高端内存进行分配,就可以
alloc_pages()
。该函数返回一个指向struct page
结构的指针,而不是一个指向某个逻辑地址的指针。 - 如果你不需要物理上的连续的页,而仅仅需要虚拟地址上连续的页,那么就使用
vmalloc()
。 - 如果你需要创建和销毁很多较大的数据结构,那么应该考虑建立
slab
高速缓存。