内核里的内存分配不像其他地方分配内存那么容易,内核的内存分配不能简单便捷的使用,分配机制也不能太复杂。

一、页

内核把页作为内存管理的基本单位,尽管处理器最小寻址坑是是字或者字节。但是内存管理单元MMU通常以页为单位进行处理。

从虚拟内存的角度来看,页就是最小单位。大多数32位系统支持4KB的页,而64位系统结构一般会支持8KB的页。

内核用struct page结构表示系统中每个物理页,在<linux/mm_types.h>中

struct page {
unsigned long flags;
atomic_t _count;
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual;
};

简化的struct page

flag域用来存放页的状态,flag的每一位单独表示一种状态,这些标志位定义于<linux/page-flags.h>中

_count存放页的引用计数,也就是一页被引用了多少次。如果是-1时,就说明当前内核并没有引用这一页。内核代码不应直接检查该域,而是使用page_count()函数进行检查,当页空闲时,返回0表示页空闲。

virtual域是页的虚拟地址,通常情况下,它就是页在虚拟内存中的地址。

page结构与物理页相关,而并非与虚拟页相关。

二、区

有些页位于内存中特定的物理地址上,不能用于其他特定的任务。由于这种限制,内核把页划分为不同的地区。

Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:

  • 一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)
  • 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。这样就有一些内存不能永久地映射到内核空间上。

因为上面的制约条件,Linux使用了四种区:

  • ZONE_DMA:这个区包含的页能用来执行DMA操作
  • ZONE_DMA32:和ZOME_DMA类似,该区包含的页面可用来执行DMA操作;不过只能被32位设备访问。
  • ZONE_NORMAL:这个区包含的都是能正常映射的页。
  • ZONE_HIGHEM:这个区包含“高端内存”并不能永久地映射到内核地址空间,在<linux/mmzone.h>中定义。

区          描述        物理内存

ZONE_DMA    DMA使用的页    <16MB

ZONE_NORMAL  正常可寻址的页     16~896MB

ZONE_HIGHMEM  动态映射的页    >896MB

Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。

比如需要DMA分配所需的内存,可以在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 long nr_saved_scan;
}lru[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;
unsigned long pages_scanned;
unsigned long flags;
atomic_long_t vm_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;
unsgined 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;
}

struct zone

lock域是一个自旋锁,它防止该结构被并发访问。这个域只保护结构,而不保护驻留在这个区中的所有页。

watermark数组持有该区的最小值、最低和最高水位值。内核使用水位为每个内存区设置合适的内存消耗基准。

name域是一个以NULL结束的字符串表示这个区的名字。在mm/page_alloc.c中,有名字“DMA”、“Normal”和“HighMem”

三、获得页

内核使用如下接口在内核内分配和释放内存,定义于<linux/gfp.h>中。

struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);

alloc_pages

函数分配2order(1<<order)个连续的物理页,并返回一个指针,指向第一个页的page结构体;如果出错,就返回NULL。

返回的页指针,可以用下面函数转换成逻辑地址。该函数返回一个指针,指向给定物理页当前所在的逻辑地址。

void *page_address(struct page *page);

page_address

如果你无须用到struct page,可以调用:

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

__get_free_pages

这个函数与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。

如果只需要一页,有两个封装好的函数,只不过传递给order的值为0:

struct page *alloc_page(gfp_t gfp_mask)
unsigned long __get_free_page(gfp_t gfp_mask)

alloc_page

3.1 获得填充为0的页

函数可以让返回的页内容全为0

unsigned long get_zeroed_page(unsigned int gfp_mask);

get_zeroed_page

此函数与__get_free_pages()工作方式相同,只不过把分配好的页都填充成了0。

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,返回指向其逻辑地址的指针

底层页分配方法表

3.2 释放页

当你不再需要页时可以用下面的函数释放它们:

void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr, unsigned int order)
void free_page(unsigned long addr) 例子:
unsigned long page: page = __get_free_pages(GFP_KERNEL, );
if(!page) {
/* 没有足够的内存:你必须处理这种错误 */
return -ENOMEM;
}
/* "page"现在指向8个连续页中第1个页的地址... */
用完这8个页后需要释放它们:
free_pages(page, ); /* 页现在已经被释放了,我们不应该再访问存放在"page"中的地址了 */

free_pages

四、kmalloc()

kmalloc和malloc类似,但是比malloc多了一个flags。kmalloc()函数是一个简单的接口,用它可以获得字节为单位的一块内核内存,如果需要整页,那么,前面的页分配接口可能更好的选择。

kmalloc()在<linux/slab.h>中声明:

void *kmalloc(size_t size, gfp_t flags)
size:内存块至少要有size大小,所分配的内存区在物理上是连续的。
出错返回NULL
例子:
struct dog *p;
p=kamlloc(sizeof(struct dog), GFP_KERNEL);
if(!p)
/* 处理错误 ... */

kmalloc原型

4.1 gfp_mask标志

这些标志分为三类:行为修饰符、区修饰符及类型。

行为修饰符:内核应当如何分配所需的内存。在某些特定情况下,只能使用某些特定的方法分配内存。如:中断要求分配内存过程不能睡眠。

包括行为描述符都是在<linux/gfp.h>中声明的。不过,在<linux/slab.h>中包含有这个头文件,因此一般不必直接包含引用。

__GFP_WAIT:分配器可以睡眠
__GFP_HIGH:分配器可以访问紧急事件缓冲池
__GFP_IO:分配器可以启动磁盘I/O
__GFP_FS:分配器可以启动文件系统I/O
__GFP_COLD:分配器应该使用告诉缓存中快要淘汰出去的页
__GFP_NOWARN:分配器将不打印失败警告
__GFP_REPEAT:分配器在分配失败时重复进行分配,但是这次分配还存在失败的可能
__GFP_NOFALL:分配器将无限的重复进行分配,分配不能失败
__GFP_NORETRY:分配器在分配失败时绝不会重新分配
__GFP_NO_GROW:由slab层内部使用
__GFP_COMP:添加混合页元数据,在hugetlb的代码内部使用

行为修饰符列表

区修饰符:从哪儿分配内存。内核把物理内存分为多个区。

类型标志符:组合行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型,简化了修饰符的使用。

4.2 kfree()

kamlloc的另一端就是kfree(),kfree在<linux/slab.h>中:

void kfree(const void *ptr)

kfree原型

中断处理程序中分配内存的例子:

char *buf;
buf = kmalloc(BUF_SIZE, GFP_ATOMIC);
if(!buf)
/* 内存分配错误 */
kfree(buf); /* 用完释放 */

kmallco例子

五、vmalloc()

vmalloc相对于kmalloc,分配的虚拟内存地址是连续的,物理地址则无需连续。

kmalloc确保页在物理地址上是连续的。

vmalloc函数声明在<linux/vmalloc.h>中,定义在<mm/vmalloc.c>中。用法与用户空间相同。

void *vmalloc(unsgined long size);
/* 该函数返回指针,指向逻辑上连续的一块内存区,大小至少为size。 */
/* 错误时返回NULL。函数可能睡眠 */ void vfree(const void *addr)
/* 释放vmalloc获得的内存 */ /* 例子 */
char *buf;
buf = vmalloc(*PAGE_SIZE); /* get 16 pages */
if(!buf)
/* 错误!不能分配内存 */
/*
* buf现在指向虚拟地址连续的一块内存区,其大小至少为16*PAGE_SIZE
*/ vfree(buf);
/* 释放 */

vmalloc

六、slab层

分配和释放数据结构是所有内核中最普遍的操作之一。为了全局控制频繁的数据分配和回收,有了slba分配器。

  • 频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。
  • 频繁分配和回收必然会导致内存碎片,所以空闲链表缓存会连续地存放。因为已释放地数据接哦古又会放回空闲链表,因此不会导致碎片。
  • 回收地对象可以立即投入下一次分配,因此,对于频繁分批和释放,空闲链表能够提高其性能。
  • 如果分配器知道对象大小、页大小和总的高速缓存的大小这样的概念,它会做出更明智的决策。
  • 如果让部分缓存专属于单个处理器,那么,分配和释放就可以在不加SMP锁的情况下进行。
  • 如果分配器是与NUMA相关的,它就可以从相同的内粗节点为请求者进行分配。
  • 对存放的对象进行着色,以防止多个对象映射到相同的告诉缓存行。

6.1 slab层的设计

slab把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象。比如一个存放进程描述符,一个存放索引节点。

每个高速缓存都是用kmem_cache结构来表示。包括三个链表:slabs_full、slabs_partial和slabs_empty。都存放在kmem_list3结构内,该结构在mm/slab.c中。

struct slab {
struct list_head list; /* 满、部分满或空链表 */
unsigned long colouroff; /* slab着色的偏移量 */
void *s_mem; /* 在slab中的第一个对象 */
unsigned int inuse; /* slab中已分配的对象数 */
kmem_bufctl_t free; /* 第一个空闲对象(如果有的话) */
};

struct slab

6.2 slab分配器的接口

一个新的高速缓存通过以下函数创建:

struct kmem_cache *kmem_cache_create(const char *name,
size_t size,
size_t align,
unsigned long flags,
void (*ctor)(void *));
name:高速缓存的名字
size:高速缓存每个元素的大小
align:slab内第一个对象的偏移,它用来确保在页内进行特定的对齐
falgs:参数是可选的设置项
ctor:高速缓存的构造函数。基本抛弃,设NULL

kmem_cache_create

falgs有各种参数:

  • SLAB_HWCACHE_ALIGN:这个标志命令slab层把一个slab内所有对象按高速缓存行对齐。对齐越严格,浪费内存越多
  • SLAB_POISON:slab层用已知的值(a5a5a5a5)填充slab
  • SLAB_RED_ZONE:导致slab层在已分配的内存周围插入“红色警戒区”以探测缓冲越界
  • SLAB_PANIC:当分配失败时提醒slab层。
  • SLAB_CACHE_DMA:slab层可以执行DMA的内存给每个slab分配空间。

要撤销一个高速缓存,则调用:

int kmem_cache_destroy(struct kmem_cache *cachep);

kmem_cache_destroy

1.从缓存中分配

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t *flags)
cachep:返回一个指向对象的指针
flags:_get_free_pages()

创建缓存

释放一个对象,

void kmem_cache_free(struct kmem_cache *cachep, void *objp)
cachep:对象objp标记为空闲

释放分配的对象

2.slab分配器的使用实例

在kernel/fork.c中,

struct kmem_cache *task_struct_cachep;
task_struct_cachep = kmem_cache_create("task_struct",
sizeof(struct task_struct),
ARCH_MIN_TASKALIGN,
SLAB_PANIC | SLAB_NOTRACK,
NULL); struct task_struct *tsk;
tsk = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL);
if(!tsk)
return NULL; kmem_cache_free(task_struct_cachep, tsk); int err;
err = kmem_cache_destroy(task_struct_cachep);
if(err)
/* 出错,撤销高速缓存 */

例子

七、在栈上的静态分配

历史上,每个进程都有两页的内核栈。因为32位和64位体系结构的页面大小分别是4KB和8KB,所以通常它们的内核栈的带线啊哦分别是8KB和16KB

7.1 单页内核栈

中断处理程序也要放在内核栈中,但同时会把更严格的约束台哦见加在这可怜的内核栈上。

所以有一个中断栈,可以为每个进程提供一个用于中断处理程序的栈。

7.2 正大光明的工作

大量的静态分配是很危险的,因此动态分配是一种明智的选择。

八、高端村内的映射

高端内存的页被映射到3GB~4GB

8.1 永久映射

要映射一个给定page结构到内核地址空间,可以使用定义在文件<linux/highmem.h>中的函数:

void *kmap(struct page *page)
这个函数在高端内存或低端内存上都能用。

kmap

8.2 临时映射

建立一个临时映射:

void *kmap_atomic(struct page *page, enum km_type type)
type是枚举类型之一
enum km_type {
KM_BOUNCE_READ,
KM_SKB_SUNRPC_DATA,
KM_USER0,
KM_USER1,
KM_BIO_SRC_IRQ,
KM_BIO_DST_IRQ,
KM_PTE0,
KM_PTE1,
KM_IRQ0,
KM_IRQ1,
KM_SOFTIRQ0,
KM_SOFTIRQ1,
KM_SYNC_ICACHE,
KM_UML_USERCOPY,
KM_IRQ_PTE,
KM_NMI,
KM_NMI_PTE,
KM_TYPE_NR
};

kmap_atomic

通过下列取消映射:

void kunmap_atomic(void *kvaddr, enum km_type type)

kunmap_atomic

九、每个CPU的分配

这个不知道干什么:

unsigned long my_percpu[NR_CPUS]'

int cpu;
cpu = get_cpu() /* 获得当前处理器,并进制内核抢占 */
my_percpu[cpu]++; /* ...或者无论什么 */
printk("my_percpu on cpu=%d is %lu\n", cpu, my_percpu[cpu]);
put_cpu(); /* 激活内核抢占 */

cpu例子

十、新的每个CPU接口

为了方便创建和操作每个CPU数据,而引进了新的操作接口,称作percpu。

10.1 编译时的每个CPU数据

在编译时设置每个CPU的变量很简单:

DEFINE_PER_CPU(type,name);

10.2 运行时的每个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()

内核提供了两个宏来利用指针获取每个CPU数据

get_cpu_var(ptr);    /* 返回一个void类型指针,该指针指向处理器的ptr拷贝 */
put_cpu_var(ptr); /* 完成:重新激活内核抢占 */

获取每个CPU数据

使用这些函数的例子:

void *percpu_ptr;
unsigned long *foo; percpu_ptr = alloc_percpu(unsigned long);
if(!ptr)
/* 内存分配错误... */ foo = get_cpu_var(percpu_ptr);
/* 操作foo ... */
put_cpu_var(percpu_ptr);

get_cpu_var例子

十一、使用每个CPU数据的原因

使用每个CPU数据有很多好处,减少了数据锁定。

第二个好处是使用每个CPU数据可以大大减少缓存失效。

每个CPU数据会省去许多数据上锁,唯一的要求是要禁止内核抢占。

十二、分配函数的选择

最新文章

  1. 无法向会话状态服务器发出会话状态请求。请确保 ASP.NET State Service (ASP.NET 状态服务)已启动,并且客户端端口与服务器端口相同。如果服务器位于远程计算机上,请检查。。。
  2. 微服务和SOA服务
  3. 【Alpha版本】 第一天 11.7
  4. 【PC网站前端架构探讨系列】关于中小型PC网站前端架构方案的讨论与实践
  5. 使用HTML.ActionLink实现一个图片链接
  6. CS193P学习笔记(一)
  7. html checkbox 全选与反选
  8. C语言strchr()函数:查找某字符在字符串中首次出现的位置
  9. java---相亲练习
  10. Insert BLOB &amp;&amp; CLOB from PL/SQL and JDBC
  11. python_way ,day1 编译安装python3、基础及流程控制
  12. objective-C运算符和表达式
  13. Context 之我见
  14. Linux 性能监测工具总结
  15. javascript face ++
  16. python【第十五篇】JavaScript
  17. Android -----ArrayAdapter的重写 .
  18. ASP.NET MVC 项目直接预览PDF文件
  19. Python open()
  20. jsp页面遍历List&lt;Array&gt;

热门文章

  1. 【ABAP系列】SAP ABAP 关于四舍五入算法
  2. Our growth depends not on how many experiences we devour, but on how manywe digest.
  3. 机器学习实战-K-近邻算法(kNN)
  4. DataAdapter的Fill方法(转)
  5. java基础笔记(8)
  6. SCUT - 486 - 无向图上的点 - Dijkstra
  7. neo4j 初探
  8. htpwdScan — 一个简单的HTTP暴力破解、撞库攻击脚本
  9. linux中的文件类型以及查看文件类型的方法
  10. 锋利的jQuery ——jQuery中的事件和动画(四)