Linux内核学习----内存管理

本文深入探讨Linux内核的内存管理,包括页和区的概念,页的结构和管理,如何获取和释放页,以及kmalloc和vmalloc两种内存分配函数的使用。此外,还介绍了slab缓存层的作用和设计,以及内核栈的静态分配策略。

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


一般的内存分配分为内核空间和用户空间,二者之间有很大的不同,内核中不支持简单便捷的内存分配(内核一般不能睡眠),内核中处理内存分配错误相对困难。

内核把物理页作为内存管理的基本单元。即MMU(内存管理单元)通常以页为单位进行处理,管理系统中的页表。从虚拟内存的角度页就是最小单位。
处理器的最小可寻址单位通常为字节。
不同的体系结构中支持的页大小是不同的,其中主要的是32位体系和64位体系,前者支持4kb的页,后者支持8kb的页。

内核中使用page表示系统中的每个物理页
struct page{
	unsigned long  				flags;
	atomic_t					_count;
	atomic_t 					_mapcount;
	unsigned long				provate;
	struct address_space  		*mapping;
	pgoff_t						index;
	struct list_head  			lru;
	void 						*virtual;
}

结构体中字段的含义:

  1. flag用来存放页的状态,状态有(是否是脏的,是否被锁定再内存中),flag的每一位单独表示一种状态,至少可以同时表示出32种不同的状态;
  2. _count存放页的引用计数----即这一页被引用了多少次,-1表示当前内核没有引用这一页即再新的分配中可用,其中内核是调用page_count()进行检查唯一的参数是page结构体,该函数返回值为0就表示页空闲,正整数表示再使用;
  3. mapping:表示当前页作为页缓存使用(mapping域指向和这个页关联的address——space对象);
  4. private:表示当前页作为私有数据使用(由private指向);
  5. 作为进程页表中的映射;
  6. virtual域是页的虚拟地址(即页在虚拟内存中的地址),有些内存(高端内存)并不永久的映射到内核地址空间,这是这个值为null,使用时必须动态映射;
  7. 由于page描述的是物理页相关,因此page中的数据由于交换等原因,数据可能并不再和同一个jpage结构相关联。主要是因为page是为了描述物理内存本身而不是其中的数据。

内核通过这个结构管理所有的页,内核需要知道这个页的状态和拥有者。
拥有者:用户空间进程、动态分配的内核数据、静态内核代码、页高速缓存
系统中所有的page结构体所占的大小:每一个占40字节,如果是系统的物理页大小是8kb,物理内存是4gb,所有的占的空间为20mb

为什么会存在区?
主要是因为硬件的限制,使得内核在对待页的时候不能够一视同仁的,有些页在特定的物理地址上,在使用上会有一些限制;区就是为了内核方便管理把页分为不同的区,区里面的页是具有相似特性的。
什么是区:
一种逻辑上的划分,没有物理意义,因为系统把页划分为区,因此形成了不同的内存池
Linux必须要处理的硬件存在缺陷引起的内存寻址问题:

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

Linux主要使用的区:

  • ZONE_DMA------这个区中的页用来执行DMA操作
  • ZONE_DMA32—和ZONE_DMA类似不同之处在于这些页只能被32位设备使用
  • ZONE_NORMAL----这个区包含的是可以正常映射的页
  • ZONE_HIGHEM-----这个区包含高端内存,即其中的页并不能永久地映射到内核地址空间,比如32位机器只能映射到4g,这已经不能满足当前的硬件设备了。
    所有地区地使用和划分都是和体系结构相关地
    在x86-32上的区分布
    x86-32上的区分布
    在获取内存时必须从所属的区取吗?
    不是的,比如一般的内存分配正常实在ZONRE_NORMAL上分配,有时也可以在ZONE_DMA上分配,只是不能同时在两个区分配。
    在64位体系中就没有了ZONE_HIGMEM区,因为64位可以映射到所有的物理内存

获得页

使用那些内核提供的接口可以在内核内分配和释放内存?
在内核中所有的内存分配的单位是页,核心函数是:

struct page * alloc_pages(gfp_t  gfp_mask, unsigned int order)
分配2^oder(1<<order)个连续的物理页,并返回一个指针

使用void  * page_address(struct page *page) 将给定的页转换成它的逻辑地址
unsigned long  __get_free_pages(gfp_t  gfp_mask, unsigned int order)和alloc_pages功能一样只是返回值不一样, __get_free_pages返回的是第一个页逻辑地址
下面的两个函数是获取一页
struct page * alloc_page(gfp_t  gfp_mask)
unsigned long  __get_free_page(gfp_t  gfp_mask)

获取填充为0的页

使用下面的函数可以获取到页的内容全为0
unsigned long get_zeroed_page(unsigned int gfp_mask)和上面的__get_free_pages的工作方式是一样的只是会将分配的页填充为0。
主要用于给用户空间分配内存,类似于进行了初始化,防止将一些’随机的’的敏感数据被传递到用户空间

释放页

不使用时通过下面的函数进行释放:

void  __free_pages(struct page *page, unsigned int oder)
void  free_pages(unsigned long addr, unsigned int oder)
void  free_page(unsigned long addr)

在内核中释放内存(页)是一个技术活,必须谨慎,如果传递了错误的页或地址和order值,可能会导致系统崩溃(灾难)。因为内核时完全的相信自己的(和人差不多)。
和用户空间不同的是如果进行非法操作,内核会很高兴的将自己挂起来,停止运行。

内核中内存分配函数

kmalloc和vmalloc区别:

  • kmalloc分配的物理地址是连续的
  • vmalloc分配的虚拟地址是连续的,物理地址无须连续

kmalloc函数

kmalloc函数void *kamalloc(size_t size, gfp_t flags)返回一个指向内存块的指针,至少为size的大小。
kmalloc函数再使用的时候需要检查返回值是否为NULL
kmalloc函数的标志:
gfp_mask标志:
1. 行为修饰符:用于指定分配器的行为
2. 区修饰符:表示内存区应当从何处分配
3. 类型标志:指定所需的行为和区描述符以完成特殊型处理
区修饰符:
__GFP_DMA: 从ZONE_DMA分配
__GFP_DMA32: 只在ZONE_DMA32分配
__GFP_HIGHMEM: 从ZONE_HIGHMEM或ZONE_NORMAL分配
kfree()函数,用于释放kmalloc分配处理的内存块,和用户空间的内存分配一样,内存的分配和释放需要配对使用,调用kfree(NULL)是安全的。
void kfree(const void *ptr)

vmalloc函数

vmalloc用于分配内存void * vmalloc( unsigned long size)
由于vmalloc分配的内存的物理地址是不连续的,因此在分配内存时vmalloc会将物理上不连续的页转换为虚拟地址空间上连续的页。
因此vmalloc的性能低于kmalloc并且大多数情况下都使用的是kmalloc,vmalloc主要用在大块内存的分配上。
vfree用于释放vmalloc所分配的内存
void vfree( const void *addr)

slab层

分配和释放数据结构是所有内核中最普遍的操作之一,一般在业务中为了避免内存的频繁分配和回收,一般会使用内存池等方法。
在内核层面因为涉及全局控制,内核不能控制每一个空闲链表让他们伸缩。因此在Linux中提供了slab层,用于扮演通用的缓存层。
slab分配器的设计原则:

 - 频繁使用的数据结构也会频繁分配和释放,应当缓存它们
 - 频繁的分配和释放会导致内存碎片
 - 回收的对象可以立即投入下一次分配,对于频繁的使用内存,空闲列表的方式可以提高性能
 - 如果分配器知道对象大小,页大小和总的高速缓存大小,它可以变得更聪明
 - 如果让部分缓存专属于单个处理器,那么分配和释放就可以在不见smp锁的情况下进行
 - 如果分配器是与NUMA相关的,它就可以从相同的内存节点为请求者分配
 - 对存放的对象进行着色(标记),以防止多个对象映射到相同的高速缓存行。

slab层的设计

首slab层将不同对象划分为所谓高速缓存组,每个组下面都存放不同类型的对象。
slab由一个或多个物理上连续的页组成,每个slab都包含一些对象成员(对象:被缓存的数据结构)
slab的三种状态:满,部分满,空
内核需要分配新的对象时步骤:
先从部分满的slab中进行分配------>如果没有部分满的slab就从空的里面分配------>如果没有空的就创建一个新的slab
高速缓存和slab与对象的关系
在这里插入图片描述
每一个高速缓存使用kmem_cache结构表示其中包含三个链表:slabs_full、slabs_partial和slabs_empty
slab的描述符,struct slab

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

slab层页的分配和释放的时机:
分配:当没有部分满和空的时候会分配
释放:当可用内存变得紧缺时,系统试图释放出更多内存以供使用;或者当前高速缓存显式地被撤销时

在栈上的静态分配

在内核空间中栈的,小而且固定。内核会给每一个进程分配一个固定大小的小栈,这样可以减少内存的消耗,而且内核页无须负担太重的栈管理任务。
每个进程的内核栈大小即依赖于体系结构也与编译时的选项有关,通常为8k(32位)和16k(64为)

单页内核栈

在2.6系列内核的早期,引入了一个选项设置单页内核栈。设置后每个进程的内核栈只有1页
引入的原因:

 1. 可以让每个进程减少内存消耗
 2. 随着机器运行时间的增加,寻找两个未分配的连续的页变的越来越困难,主要是由于内存碎片的增加

每一个进程的调用链必须放在自己的内核栈中,在中断处理程序中也是这样,这样就有了一个问题,中断程序也需要吧严格的约束条件加到内核栈中,在我们使用这有一页的内核栈时,中断处理程序就不放在栈中。即会产生冲突
解决方式:引入了中断栈,中断处理程序不用再和被中断进程共享一个内核栈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值