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

一、页

内核把页作为内存管理的基本单位,尽管处理器最小寻址坑是是字或者字节。但是内存管理单元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数据会省去许多数据上锁,唯一的要求是要禁止内核抢占。

十二、分配函数的选择

Linux内核设计与实现 总结笔记(第十二章)内存管理的更多相关文章

  1. 《Linux内核设计与实现》读书笔记(十二)- 内存管理【转】

    转自:http://www.cnblogs.com/wang_yb/archive/2013/05/23/3095907.html 内核的内存使用不像用户空间那样随意,内核的内存出现错误时也只有靠自己 ...

  2. Linux内核设计与实现 总结笔记(第十三章)虚拟文件系统

    一.通用文件系统接口 Linux通过虚拟文件系统,使得用户可以直接使用open().read().write()访问文件系统,这种协作性和泛型存取成为可能. 不管文件系统是什么,也不管文件系统位于何种 ...

  3. Linux内核设计与实现 总结笔记(第六章)内核数据结构

    内核数据结构 Linux内核实现了这些通用数据结构,而且提倡大家在开发时重用. 内核开发者应该尽可能地使用这些数据结构,而不要自作主张的山寨方法. 通用的数据结构有以下几种:链表.队列.映射和二叉树 ...

  4. Linux内核设计与实现 总结笔记(第十一章)定时器和时间管理

    时间管理在内核中占用非常重要的地位,内核中有大量的函数都需要基于时间驱动的,内核对相对时间和绝对时间都非常需要. 一.内核中的时间概念 内核必须在硬件的帮助下才能计算和管理时间,系统定时器以某种频率自 ...

  5. Linux内核设计与实现 总结笔记(第五章)系统调用

    系统调用 内核提供了用户进程和内核交互的接口,使得应用程序可以受限制的访问硬件设备. 提供这些接口主要是为了保证系统稳定可靠,避免应用程序恣意妄行. 一.内核通信 系统调用在用户空间进程和硬件设备之间 ...

  6. Linux内核设计与实现 总结笔记(第七章)中断和中断处理

    中断和中断处理 处理器的速度跟外围硬件设备的速度往往不再一个数量级上,因此,如果内核采取让处理器向硬件发出一个请求. 然后专门等待回应的办法,如果专门等待回应,明显太慢.所以等待期间可以处理其他事务, ...

  7. Linux内核设计与实现 总结笔记(第四章)进程调度

    进程调度 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间. 调度程序没有太复杂的原理,最大限度地利用处理器时间的原则是,只要有可以执行的进程,那么就总会有进程正在执行. 一.多任务 多任 ...

  8. Linux内核设计与实现 总结笔记(第三章)进程

    进程管理 进程:处于执行期的程序. 线程:在进程中活动的对象 虚拟机制 虚拟处理器:多个进程分享一个处理器 虚拟内存:多个线程共享虚拟内存 一.进程描述符和任务结构 进程存放在双向循环链表中(队列), ...

  9. 《Linux内核设计与实现》课本第十八章自学笔记——20135203齐岳

    <Linux内核设计与实现>课本第十八章自学笔记 By20135203齐岳 通过打印来调试 printk()是内核提供的格式化打印函数,除了和C库提供的printf()函数功能相同外还有一 ...

  10. Linux内核设计与实现 读书笔记 转

    Linux内核设计与实现  读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://bl ...

随机推荐

  1. spring(一) IOC 控制反转 、DI 依赖注入

    IOC 控制反转:创建对象的方式  变成了由Spring来主导 IOC底层原理:对象工厂 1.导入jar包:4个核心jar和1个依赖jar spring-beans-4.3.9.RELEASE.jar ...

  2. cocos2dx基础篇(21) 进度条CCProgressTimer

    [3.x] (1)去掉 "CC" (2)CCProgressTimerType 改为强枚举 ProgressTimer::Type:: // RADIAL //扇形进度计时器 BA ...

  3. Mac搭建github Page的Hexo免费个人博客

    1.基础准备 github账号 安装git 安装node.js.npm 2.创建repo 3.配置SSH key 这一步并不重要,配置SSH key与否,并不影响博客的搭建和使用,只是配置了之后,更新 ...

  4. Tensorflow Learning1 模型的保存和恢复

    CKPT->pb Demo 解析 tensor name 和 node name 的区别 Pb 的恢复 CKPT->pb tensorflow的模型保存有两种形式: 1. ckpt:可以恢 ...

  5. notepad++通过调用cmd运行java程序

    notepad++运行java程序方法主要有下面两个: 通过插件NppExec运行(自行百度“notepad++运行java”) 通过运行 调用cmd编译执行java程序(下面详细讲解) 点击上面工具 ...

  6. tarjan算法应用 割点 桥 双连通分量

    tarjan算法的应用. 还需多练习--.遇上题目还是容易傻住 对于tarjan算法中使用到的Dfn和Low数组. low[u]:=min(low[u],dfn[v])--(u,v)为后向边,v不是u ...

  7. Let's encrypt 通配域名DNS验证方式的证书自动更新

    通配符域名不同于一般的单域名证书. 为了解决之前一篇短文中通配域名通过DNS方式验证的证书自动更新问题. 需要使用到第三方域名提供商的API, 用于自动添加域名的TXT记录, 实现自动验证并完成证书更 ...

  8. python学习第五十四天hashlib模块的使用

    hash算法 hash也做散列,也称为哈希,主要用于信息安全领域中加密算法,hash就是找一种数据内容和数据存放地址直接的映射关系. md5算法 md5讯息算法,广泛使用密码函数 md5算法的特点 1 ...

  9. 解决arcgis10.5直连postgresql报错

    软件版本: arcgis10.5 postgresql9.5.9 最近使用desktop直连postgresql,已经拷贝了类库文件到desktop及pgsql配置完成的前提下,但还是报以下错误: 解 ...

  10. Android数据库使用指南(下)

    前言 上面已经说了,对表进行修改,其实就是对数据库进行升级,删除表也算升级啊,反正就是发生变化,数据库就需要升级. 所以老实说其实有个地方决定了数据库的版本 public class DBHelper ...