Linux内核原理之内存管理

本文详细介绍了Linux内核的内存管理机制,包括页、区的管理,内存分配如kmalloc()、vmalloc(),slab层的工作原理,以及高端内存的映射策略。讨论了页的引用计数、状态和页缓存,以及针对不同场景的内存分配和释放。还涉及到了内核栈的管理和SMP系统中每个CPU的内存分配问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

内存管理

内核不能像用户空间那样奢侈的使用内存,获取内存币用户空间复杂很多

内核把物理页作为内核管理的基本单元,内存管理单元(MMU)是管理内存并将虚拟内存转换为物理内存的硬件,它以页为单位来管理系统中的页表

结构体struct page表示系统中的每个物理页

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
};
  • flags域,用来存放页的状态(包括是不是脏的,是不是锁定在内存),每一位单独表示一种状态,至少可以表示32中不同的状态
  • _count域,存放页的引用计数,-1时内核没有引用该页,在新的内存分配中可以使用。内核调用page_count()检查该域,返回0表示页空闲,返回正整数表示正在使用
  • 页可以由页缓存使用(此时,mapping域指向页关联的address_space对象),或者作为私有数据(private指向),或者作为进程页表中的映射
  • virtual域,页的虚拟地址,当页在高端内存(不会永久映射到内核空间)中时,这个域为NULL

注意

  1. page结构与物理页相关,并非与虚拟页相关,它仅仅描述当前时刻在相关物理中存放的数据(由于交换等原因,关联的数据继续存在,但是和当前物理页不再关联),它对于页的描述是短暂的
  2. 页的拥有者可能是用户空间进程、动态分配的内核数据,静态内核数据或者页高速缓存等

Linux主要使用四种区:

  • ZONE_DMA,其中包含的页只能进行DMA操作(直接内存访问)
  • ZONE_DMA32,和ZONE_DMA类似,不同之处是只能被32位设备访问
  • ZONE_NORMAL,包含能够正常映射的页
  • ZONE_HIGHMEM,包含“高端内存”,其中的页不能永久地映射到内核空间

高端内存,由于一些体系结构的物理内存比虚拟内存大的多,为了充分利用物理内存,将物理内存中的部分区域划分为高端内存,他们不能永久地映射到内核空间,而是动态的映射

在32位x86体系中,ZONE_HIGHMEM为高于896MB的所有物理内存,其余内存为低端内存,其中ZONE_NORMAL为16MB到896MB的物理内存,ZONE_DMA为小于16MB的物理内存

x86-64系统没有高端内存区

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

每个区使用结构体zone表示,具体结构详见 Linux内核设计与实现 P189

域说明:

  • lock域,是一个自旋锁,防止结构被并发访问,这个域只保护结构,不保护驻留在这个区中的页
  • watermark域,水位值,为每个内存区设置合理的内存消耗基准
  • name域,表示区的名字,三个区的名字分别为"DMA",“Normal”,“HighMem”

获得页

  • 分配2order(1<<order)个连续的物理内存页,返回的指针指向第一个页的page结构体
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
  • 将指定的物理页转换为它的逻辑地址(虚拟内存地址),返回的指针指向物理页所在的逻辑地址
void *page_address(struct page *page);
  • alloc_pages()功能类似,不过它直接返回请求的第一个页的逻辑地址
unsigned long __get_free_pages(gfp_t fp_mask, unsigned int order)
  • 只分配一页的函数
struct page *alloc_page(gfp_t gfp_mask);
unsigned long __get_free_page(gfp_t fp_mask)
获得填充为0的页
  • 分配的所有页内容全为0,返回执行逻辑地址的指针
unsigned long get_zeroed_page(unsigned int gfp_mask)

注意:为了防止页中留下一般随机的垃圾信息包含一些敏感信息,一般用户空间在获取页的时候,内容最好全部填充为0

释放页
void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
void free_page(unsigned long addr)

注意:传递了错误的struct page、地址或者order参数,都可能导致系统崩溃,因为内核是完全相信自己的

kmalloc()

  • 以字节为单位分配内存
  • 可以获得以字节为单位的一块内核内存
void *kmalloc(size_t size, gfp_t flags);
gfp_mask标志

标志分为三类:

  • 行为修饰符
  • 区修饰符
  • 类型修饰符

标志具体说明详见P192

kfree()
  • 释放由kmalloc()分配的内存
void kfree(const void *ptr);

vmalloc()

kmalloc()类似,不同之处在于vmalloc()分配的内存虚拟内存连续,但是物理内存不一定连续,而kmalloc()分配的物理内存也是连续的

vmalloc()正是用户空间分配内存的方式:有malloc()分配的内存页在进程的虚拟内存中是连续的,但是物理内存不保证连续。大多情况下,只有硬件设备才需要连续的物理内存,他们不理解什么是虚拟内存

slab层

Linux内核提供slab层(即slab分配器),作为通用数据结构缓存层

slab的设计

slab层将不同的对象划分为高速缓存组,每个高速缓存组存放不同类型的对象,例如,分别存放进程描述符(task_struct结构的空闲链表),索引节点对象(struct inode)

  • kmalloc()建立在slab层之上,使用了一组通用高速缓存
  • 一般slab仅仅由一页组成,每个高速缓存由多个slab组成
  • 每个slab包含一些对象成员,对象指的是被缓存的数据结构
  • slab包含三种状态:满、部分满或空

高速缓存使用结构体kmem_cache表示,这个结构包括三个链表:slabs_full, slabs_partialslabs_empty,这些链表包含高速缓存中的所有slab

slab使用slab描述符表示,详见P216

struct slab {
    ...
};

slab分配器使用__get_free_pages()创建新的slab

slab分配器的接口
  • 创建一个新的高速缓存
    • name:高速缓存的名字
    • size:高速缓存中每个元素的大小
    • align:slab内第一个对象的偏移量,用来确保在页内进行特定的对齐
    • flags:可选的设置项,控制高速缓存的行为,详见P218
    • ctor:高速缓存的构造函数(Linux的高速缓存不使用构造函数)
    • 返回指向高速缓存的指针
    • 函数调用可能会睡眠,不能再中断上下文使用
struct kmem_cache *kmem_cache_create(const char *name, 
                                    size_t size,
                                    size_t align,
                                    unsigned long flags,
                                    void (*ctor)(void *));
  • 撤销一个高速缓存(可能睡眠,不能再中断上下文使用)
int kmem_cache_destroy(struct kmem_cache *cachep);

注意:调用kmem_cache_destroy之前要确保两个条件:

  1. 高速缓存中的所有slab为空
  2. 在调用此函数过程中不在访问这个高速缓存
  • 从已经创建的缓存中分配释放对象,使用示例详见P219
void kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
void kmem_cache_free(struct kmem_cache *cachep, void *objp);

栈上的静态分配

不同于用户栈,内核栈小而且固定,内核栈一般是两个页大小

单页内核栈

2.6内核之后,引入选项可以设置单页内核栈,激活这个选项,每个进程的内核栈只有一页大小

引入的原因有两点:

  1. 可以让每个进程减少内存消耗,另外随着机器运行时间增加,寻找两个连续的物理页变得越来越困难
  2. 当内核栈使用两页时,中断处理程序使用它所中断进程的内核栈,而当进程使用单页的内核栈时,中断处理程序不放在进程内核栈中,而是放在中段栈中。

中断栈:为每个进程提供运行中断处理程序的栈,一页大小。

总之,历史上,进程和中断处理程序共享一个栈空间,当1页栈的选项激活之后,中断处理程序获得了自己的栈。

在栈上工作

在任何函数,都要尽量节省内核栈的使用,让所有局部变量大小不要超过几百字节。栈溢出非常危险,所出的数据会直接覆盖紧邻堆栈末端的数据(例如thread_info结构就是紧邻进程堆栈末端)。

因此,推荐使用动态分配。

高端内存的映射

永久映射

映射给定的page结构到内核地址空间,使用如下函数

void *kmap(struct page *page);

函数在对于高端内存或者低端内存都能使用:

  1. 如果page对应低端内存的一页,函数会单纯返回该物理页对应的虚拟地址
  2. 如果page对应高端内存页,函数会建立一个永久映射,在返回对应的虚拟地址
  3. 函数可以睡眠,只能在进程上下文中使用

当不再需要高端内存中的这一个页时,使用如下函数解除映射

void kunmap(struct page *page);
临时映射

当必须创建映射而上下文不能睡眠是,内核提供了临时映射(原子映射)

临时映射可以用在像中断上下文一样的不能睡眠的地方,使用如下函数建立心是映射:

void *kmap_atomic(struct page *page, enum km_type type);
  • 函数禁止了内核抢占(因为映射对每个处理器都是唯一的???)

每个CPU的分配

SMP定义:一个操作系统的实例可以同时管理所有CPU内核,且应用并不绑定某一个内核。

支持SMP的操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的,每个CPU的数据存放在一个数组中,数组的每一个元素对应一个存在的处理器

unsigned long my_percpu[NR_CPUS];

访问cpu数据过程:

int cpu;

cpu = get_cpu();  //获取当前CPU,并且禁止内核抢占
data = my_percpu[cpu];
....       // 使用data的过程
put_cpu();

代码中没有出现锁,因为数据对当前处理器是唯一的,没有其他处理器可以接触这个数据,没有多个处理器并发访问的问题,但是会有内核抢占的问题:

  1. 如果代码被其他处理器抢占并重新调度,这是cpu变量data会变成无效,因为它对应了错误的处理器
  2. 如果另一个进程抢占了代码,有可能在一个处理器上并发访问data数据的问题

因此,在获取当前cpu时,就已经禁止了内核抢占。

新的每个CPU接口

描述了一些为每个CPU分配内存的接口,详见P223

使用每个CPU数据的原因

使用每个CPU的好处有:

  1. 减少数据锁定,每个处理器访问每个CPU数据,不用加锁
  2. 使用每个CPU数据大大减少了缓存失效,失效发生在处理器试图使他们的缓存保持同步时,如果一个处理器操作数据时,该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须刷新或者清理自己的缓存,频繁的缓存失效会造成缓存抖动

而使用每个CPU数据唯一的要求是需要禁止内核抢占

参考资料

  • Linux内核设计与实现
  • 深入Linux内核架构
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值