Linux内核设计与实现——内存管理

内核的内存使用不像用户空间那样随意,内核的内存出现错误时也只有靠自己来解决(用户空间的内存错误可以抛给内核来解决)。
所有内核的内存管理必须要简洁而且高效。

主要内容

  1. 内存的管理单元
  2. 获取内存的方法
  3. 获取高端内存
  4. 内核内存的分配方式

1. 页

内核把物理页作为内存管理的基本单位,内存管理单元MMU以页为单位进行处理,从虚拟内存的角度,页就是最小单位。

struct page {
    unsigned long flags;    /* 存放页的状态,脏页?被锁定?等各种状态参见<linux/page-flags.h> */
    atomic_t _count;        /* 页的引用计数 */
    atomic_t _mapcount;    /* 已经映射到mms的pte的个数 *
    unsigned long private;        /* 此page作为私有数据时,指向私有数据 */
    struct address_space *mapping;    /* 此page作为页缓存时,指向关联的address_space */
    pgoff_t index;        /* Our offset within mapping. */
    struct list_head lru;    /* 将页关联起来的链表项 */
    void *virtual;            /* 页的虚拟地址 */
};

如果页的大小是 8KB 的话,消耗的管理page内存只有 20MB 左右。相对于 4GB 来说并不算很多。

2. 区

页是内存管理的最小单元,但是并不是所有的页对于内核都一样。
内核将内存按地址的顺序分成了不同的区,有的硬件只能访问有专门的区。

在x86(32位系统)上

描述 物理内存
ZONE_DMA DMA使用的页 <16MB
ZONE_NORMAL 正常可寻址的页 16~896MB
ZONE_HIGHMEM 动态映射的页 >896MB

某些硬件只能直接访问内存地址,不支持内存映射,对于这些硬件内核会分配 ZONE_DMA 区的内存。
某些硬件的内存寻址范围很广,比虚拟寻址范围还要大的多,那么就会用到 ZONE_HIGHMEM 区的内存。

3. 获得页

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

alloc** 方法和 get** 方法的区别在于,一个返回的是内存的物理地址,一个返回内存物理地址映射后的逻辑地址。
如果无须直接操作物理页结构体的话,一般使用 get** 方法。

对应的释放函数:

extern void __free_pages(struct page *page, unsigned int order);
extern void free_pages(unsigned long addr, unsigned int order);
extern void free_hot_page(struct page *page);

4. kmalloc()

获得以字节为单位的一块内核内存。

/**
 * @size  - 申请分配的字节数
 * @flags - 上面讨论的各种 gfp_mask
 */
static __always_inline void *kmalloc(size_t size, gfp_t flags)
#+end_src

vmalloc的定义在 mm/vmalloc.c 中
#+begin_src C
/**
 * @size - 申请分配的字节数
 */
void *vmalloc(unsigned long size)

kmalloc 和 vmalloc 区别在于:

  • kmalloc 分配的内存物理地址是连续的,虚拟地址也是连续的
  • vmalloc 分配的内存物理地址是不连续的,虚拟地址是连续的

对应的释放方法:kfree, vfree

gfp_mask标志

请求内存时,参数中有个标志位,控制分配内存时必须遵守的一些规则。

  • 行为标志 :控制分配内存时,分配器的一些行为
  • 区标志 :控制内存分配在那个区(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM 之类)
  • 类型标志 :由上面2种标志组合而成的一些常用的场景

5. slab层实现原理

频繁的分配/释放内存必然导致系统性能的下降,所以有必要为频繁分配/释放的对象内心建立缓存。

linux中的高速缓存是用所谓的slab层来实现的

  1. 可以在内存中建立各种对象的高速缓存(比如进程描述相关的结构 task_struct 的高速缓存)
  2. 除了针对特定对象的高速缓存以外,也有通用对象的高速缓存
  3. 每个高速缓存中包含多个 slab,slab用于管理缓存的对象
  4. slab中包含多个缓存的对象,物理上由一页或多个连续的页组成
高速缓存-slab-对象.png

高速缓存被划分为slab,每个slab都包含一些对象成员(被缓存的数据结构),slab处于三种状态之一(满、部分满、空)
每个高速缓存都使用kmem_cache结构来表示,包含三个链表:slabs_full、slabs_partial和slabs_empty
,这些链表包含高速缓存中的所有slab。

struct slab {
    struct list_head list;   /* 存放缓存对象,这个链表有 满,部分满,空 3种状态  */
    unsigned long colouroff; /* slab 着色的偏移量 */
    void *s_mem;             /* 在 slab 中的第一个对象 */
    unsigned int inuse;         /* slab 中已分配的对象数 */
    kmem_bufctl_t free;      /* 第一个空闲对象(如果有的话) */
    unsigned short nodeid;   /* 应该是在 NUMA 环境下使用 */
};

slab层的应用主要有四个方法:

  1. 高速缓存的创建
  2. 从高速缓存中分配对象
  3. 向高速缓存释放对象
  4. 高速缓存的销毁
/**
 * 创建高速缓存
 * 参见文件: mm/slab.c
 * 这个函数的注释很详细,这里就不多说了。
 */
struct kmem_cache *
kmem_cache_create (const char *name, size_t size, size_t align,
    unsigned long flags, void (*ctor)(void *))

/**
 * 从高速缓存中分配对象也很简单
 * 函数参见文件:mm/slab.c
 * @cachep - 指向高速缓存指针
 * @flags  - 之前讨论的 gfp_mask 标志,只有在高速缓存中所有slab都没有空闲对象时,
 *           需要申请新的空间时,这个标志才会起作用。
 *
 * 分配成功时,返回指向对象的指针
 */
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)

/**
 * 向高速缓存释放对象
 * @cachep - 指向高速缓存指针
 * @objp   - 要释放的对象的指针
 */
void kmem_cache_free(struct kmem_cache *cachep, void *objp)

/**
 * 销毁高速缓存
 * @cachep - 指向高速缓存指针 
 */
void kmem_cache_destroy(struct kmem_cache *cachep)

6. 在栈上的静态分配

内核空间的栈小而且固定
在x86体系结构中,内核栈的大小一般就是1页或2页,即 4KB ~ 8KB

内核栈可以在编译内核时通过配置选项将内核栈配置为1页,配置为1页的好处是分配时比较简单,只有一页,不存在内存碎片的情况,因为一页是本就是分配的最小单位。

当有中断发生时,如果共享内核栈,中断程序和被中断程序共享一个内核栈会可能导致空间不足,
于是,每个进程除了有个内核栈之外,还有一个中断栈,中断栈一般也就1页大小。

7. 高端内存的映射

x86体系中,高于896MB的所有物理内存的范围大都是高端呢村,不会永久或自动地映射到内核地址空间。

  • 永久映射:void kmap(struct page page)
  • 临时映射:void *kmap_atomic(struct page *page, enum km_type type)

8. 按CPU分配

按CPU来分配数据主要有2个优点:

  1. 最直接的效果就是减少了对数据的锁,提高了系统的性能
  2. 由于每个CPU有自己的数据,所以处理器切换时可以大大减少缓存失效的几率

如果一个处理器操作某个数据,而这个数据在另一个处理器的缓存中时,那么存放这个数据的那个处理器必须清理或刷新自己的缓存。持续的缓存失效称为缓存抖动,对系统性能影响很大。

percpu接口:

  • 编译时分配
  • 运行时分配,通过指针
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容