虚拟地址空间:用户空间和内核空间 物理内存管理:伙伴系统以及slab分配器

Posted 贺二公子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚拟地址空间:用户空间和内核空间 物理内存管理:伙伴系统以及slab分配器相关的知识,希望对你有一定的参考价值。

原文地址:https://blog.csdn.net/HUAERBUSHI521/article/details/118599134


文章目录

一.虚拟地址空间

直接使用物理内存面临的问题:

  1. 内存缺乏访问控制,安全性不足
  2. 各进程同时访问物理内存,可能会互相产生影响,没有独立性
  3. 物理内存极小,而并发进程所需又大,容易导致内存不足
  4. 进程所需空间不一,容易导致内存碎片化问题

基于以上几种原因,Linux通过mm_struct结构体描述了一个虚拟的,连续的,独立的地址空间.也就是所说的虚拟地址空间.

程序运行时,在建立了虚拟地址空间后,并没有分配实际的物理内存,而是当进程需要实际访问内存资源的时候就会由内核的请求分页机制产生缺页中断,这时才会建立虚拟地址和物理地址的映射,调入物理内存页.通过这种方式,就能够保证我们的物理内存只有在实际使用时才进行分配,避免了内存浪费的问题.

二.虚拟地址空间分布


在linux中,虚拟地址空间的内部又被划分为用户空间内核空间.

2.1 内核态与用户态的理解

  • 操作系统:是管理计算机硬件与软件资源的终端机及程序
  • 内核态:本质是一种特殊的软件程序,控制计算机的硬件资源,比如:协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行.
  • 用户态:提供应用程序运行的空间,为了使应用程序能够访问的内核管理的资源
  • 系统调用:是操作系统最小的功能单位,是用户态和内核态交互的基本接口.
  • 库函数:实际是对系统调用进行封装,提供简单的基本接口给用户.屏蔽了复杂的底层实现细节,增强了程序的灵活性. 库函数根据不同的标准有不同的标准:glibc库,posix库
  • 内核态和用户态的区别本质是:权限不同
  • 从用户态到内核态切换的三种方式:系统调用,异常,外设中断

2.2 用户空间

  • :又叫堆栈,用于存放非静态局部变量,函数参数,返回值等,栈是向下增长的.每当一个函数被调用时,就会将参数压入进程调用栈中,调用结束后返回值也会被放回栈中。同时,每调用一次函数就会创建一个新的栈,所以在递归较深时容易导致栈溢出。栈内存的申请和释放由编译器自动完成,并且栈容量由系统预先定义。
  • 内存映射段: 是高效的I/O映射方式,用于装载一个共享的动态内存库.用户可使用系统接口创建共享内存,做进程间通信.
  • :用于存放程序运行时动态内存分配,堆内存由用户申请分配和释放,堆是向上增长的
  • BSS段:用来存放程序中未初始化的全局变量和静态变量
  • 数据段:用来存放程序中已初始化全局变量与静态变量
  • 代码段代码段用来存放程序执行代码,也可能包含一些只读的常量。这块区域的大小在程序运行时就已经确定,并且为了防止代码和常量遭到修改,代码段被设置为只读

2.3 内核空间

内核空间即进程陷入内核态后才能访问的空间.虽然每个进程都具有自己独立的虚拟地址空间,但是这些虚拟地址空间中的内核空间(前896M),其实关联的都是同一块物理内存.

通过这种方法,保证了进程在切换至内核态后能够快速的访问内核空间.

内核空间虚拟地址X 对于的物理内存地址:X-0xc0000000

内核空间虚拟地址分布:

内核空间主要分为直接映射区高端内存映射区.

  • 直接映射区
    • 从内核空间起始位置开始,从低地址往高地址增长,最大为896M的区域即为直接映射区。
    • 直接映射区的896M的虚拟地址与物理地址的前896M进行直接映射.因此虚拟地址和分配的物理地址都是连续的。
    • 相互转换: 偏移量PAGE_OFFSET:0xC0000000 物理地址 = 虚拟地址-PAGE_OFFSET
  • 高端内存映射区
    • 内核空间大小只有1G,但是物理内存可不止1G.内核空间利用直接映射区将896M的内存直接映射到物理内存中,那么剩下的物理内存寻址工作,就交给了高端内存映射区.
    • 将剩下的128M的空间划分为三个高端内存的映射区,从上往下分别是:
      • 固定内存映射区: 该区域的每个地址项都服务于特定的用途
      • 永久内存映射区:该区域可以访问高端内存。使用alloc_page(_GFP_HIGHMEM)分配高端内存页,或者使用kmap将分配的高端内存映射到该区域
      • 动态内存映射区:该区域的特点是虚拟地址连续,但是其对应的物理地址并不一定连续。该区域使用内核函数vmalloc进行分配,分配的虚拟地址的物理页可能会处于低端内存,也可能处于高端内存

三.虚拟地址空间的映射

3.1 物理内存分页

在Linux系统中,通过分段分页的机制,将物理内存划分为4k大小的内存页,并且将页作为物理内存分配与回收的基本单位.

内核会为每一个物理页帧创建一个struct page结构体,其中包含的重要信息有:

  • flags:描述page的状态和其它信息
  • index: 在映射的虚拟空间(vma_area)内的偏移
  • lru: 链表头,用于各种链表上维护该页,以便于按页将不同类别分组

3.2 管理区页框分配器

Linux内核通过一个管理区页框分配器管理着物理内存上所有的页框,在管理分配器里的核心系统就是伙伴系统和每CPU高速缓存. 在linux系统中,管理区页框分配器管理着所有物理内存,无论是内核还是用户进程,需要将一些内存占为己有时,都需要请求管理区页框分配器,这时才会给你分配物理内存页框.

3.3 vm_area_structs[区域结构链表]

一个具体的区域结构vm_area_struct包含的重要字段:

  • vm_start: 指向这个区域的起始处
  • vm_end: 指向这个区域的结束处
  • vm_port: 描述这个区域内包含的所有页面的读写许可权限
  • vm_falgs: 描述这个区域内的页面是否是与其它进程共享的,还是这个进程私有的
  • vm_next: 指向链表中的下一个区域结构

    每一个 vm_area_struct 结构体描述了一片特定的虚拟地址空间.

四.物理内存的管理

4.1 内存碎片问题

在Linux中,通过分段和分页的机制,将物理内存划分为4K大小的内存页,并且将页作为物理内存分配与回收的基本单位.通过分页机制可以灵活的对内存进行管理.

  1. 如果用户申请小块内存,可以直接分配一页给用户,就可以必秒因为频繁的申请,释放小块内存而发起的系统调用带来的消耗
  2. 如果用户申请了大块内存,可以将多个页框组合成一大块内存后再进行分配,非常灵活.

但是这种直接的内存分配存在着大量的问题,非常容易导致内存碎片的问题.下面就分别介绍两种内存碎片:内部碎片和外部碎片.

外部碎片:

当我们需要分配大块内存的时,操作系统会将连续的页框组合起来形成大块内存,再将其分配给用户.但是频繁的申请和释放内存页,就会带来内存外碎片的问题.

当需要分配大块内存的时候,要用好几页组合起来才够,而系统分配物理内存页的时候会尽量分配连续的内存页面,频繁的分配与回收物理页导致大量的小块内存夹杂在已分配页面中间,形成内存外碎片.

内部碎片:

由于页是物理内存分配的基本单位,因此即使我们需要的内存很小,Linux也会至少给我们分配4K的内存页.

倘若我们需求的只有几个字节,那么该内存中有大量的空间未被使用,造成了内存浪费的问题.而我们频繁进行小块内存的申请,这种浪费现象就会愈发严重,这也就是内存内碎片的问题.

4.2 伙伴系统(buddy system)

要想解决内存外碎片的问题,无非就两种方法

  1. 内存外碎片问题的本质就是空间不连续,所以可以将非连续的空闲页框映射到连续的虚拟地址空间
  2. 记录现存的空闲连续页框块的情况,尽量避免为了满足小块内存的请求而分割大的空闲块。

Linux选择了第二种方法来解决这个问题,即引入伙伴系统算法,来解决内存外碎片的问题。

伙伴系统就是把相同大小的连续空闲页框块用链表串起来,这样页框之间看起来就像是手拉手的伙伴,这也是其名字的由来.

伙伴系统将所有的空闲页框分组为11块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块,即2的0~10次方,最大可以申请1024个连续页框,对应4MB(1024*4K)大小的连续内存.每个页框块的第一个页框的物理地址应该是该块大小的整数倍.

因为任何正整数都可以由 2^n 的和组成,所以我们总能通过拆分与合并,来找到合适大小的内存块分配出去,减少了外部碎片产生 。

4.3 slab分配器

伙伴系统很好的解决了内存外碎片的问题,但是它还是以页作为内存分配和释放的基本单位.而我们在实际的应用中则是以字节为单位.例如我们申请2个字节的空间,但是其还是会向我们分配一页,也就是4096字节的内存,因此还是会存在内存碎片的问题.

为了解决这个问题,slab分配器就应运而生了。其以字节为基本单位,专门用于对小块内存进行分配。slab分配器并未脱离伙伴系统,而是对伙伴系统的补充,它将伙伴系统分配的大内存进一步细化为小内存分配。

对于内核对象,生命周期通常是这样的:分配内存->初始化->释放内存。而内核中如文件描述符、pcb等小对象又非常多,如果按照伙伴系统按页分配和释放内存,不仅存在大量的空间浪费,还会因为频繁对小对象进行分配-初始化-释放这些操作而导致性能的消耗。

所以为了解决这个问题,对于内核中这些需要重复使用的小型数据对象,slab通过一个缓存池来缓存这些常用的已初始化的对象。

当我们需要申请这些小对象时,就会直接从缓存池中的slab列表中分配一个出去。而当我们需要释放时,我们不会将其返回给伙伴系统进行释放,而是将其重新保存在缓存池的slab列表中。通过这种方法,不仅避免了内存内碎片的问题,还大大的提高了内存分配的性能。

下面就由大到小,来画出底层的数据结构

kmem_cache是一个cache_chain的链表,描述了一个高速缓存,这个缓存可以看做是同类型对象的一种储备,每个高速缓存包含了一个slab的列表,这通常是一段连续的内存块,并包含3种类型的slabs链表:

  • slabs_full(完全分配的slab)
  • slabs_partial(部分分配的slab)
  • slabs_empty(空slab,或者没有对象被分配)。

slab是slab分配器的最小单位,在具体实现上一个slab由一个或者多个连续的物理页组成(通常只有一页)。单个slab可以在slab链表中进行移动,例如一个未满的slab节点,其原本在slabs_partial链表中,如果它由于分配对象而变满,就需要从原先的slabs_partial中删除,插入到完全分配的链表slabs_full中

内核中slab分配对象的全过程:

  1. 根据对象的类型找到cache_chain中对应的高速缓存kmem_cache
  2. 如果slabs_partial链表中还有未分配的空间,则为其分配对象。如果分配对象之后空间已满,则移动slab节点到slabs_full链表
  3. 如果slabs_partial链表没有未分配的空间,则去查看slabs_empty链表
  4. 如果slabs_empty链表还有未分配的空间,则为其分配对象,同时移动slab节点进入slabs_partial链表中
  5. 如果slabs_empty链表也没有未分配的空间,则说明此时空间不足,就会请求伙伴系统分页,并创建新的空闲slab节点放入slabs_empty链表中,回到步骤4。

从上面可以看出,slab分配器的本质其实就是通过将内存按使用对象不同再划分成不同大小的空间,即对内核对象的缓存操作.

slab分配器的优点:

  1. slab内存管理基于内核小对象,不用每次都分配一页内存,充分利用空间,避免内部碎片
  2. slab对内核中频繁创建和释放的小对象做缓存,重复利用一些相同的对象,减少内存分配次数.

以上是关于虚拟地址空间:用户空间和内核空间 物理内存管理:伙伴系统以及slab分配器的主要内容,如果未能解决你的问题,请参考以下文章

Linux - 用户态内存映射 和 内核态内存映射

Linux 内核 内存管理内存映射原理 ① ( 物理地址空间 | 外围设备寄存器 | 外围设备寄存器的物理地址 映射到 虚拟地址空间 )

linux内核内存虚拟地址映射物理地址

Linux驱动虚拟地址和物理地址的映射

Linux驱动虚拟地址和物理地址的映射

linux内核物理内存空间分布