操作系统内存空间管理:物理虚拟与映射

20 内存管理:规划进程内存空间布局

进程间为什么需要内存的隔离?怎么隔离的?
假设使用的是物理内存,那同是计算器的进程,使用相同的物理地址,如果打开了三个,三个程序分别数据10\100\1000,那么此时物理地址就不知道保存哪个数据了。

隔离方法:
进程不直接操作物理地址,操作系统给进程分配虚拟地址。所有进程看到的这个地址都是一样的,里面的内存都是从 0 开始编号。
同时操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
程序要访问虚拟地址的时候,由内核的数据结构进行转换,转换成不同的物理地址解决冲突问题。

操作系统的内存管理,主要分为三个方面。
第一,虚拟内存空间的管理,每个进程看到的是独立的、互不干扰的虚拟地址空间;
第二,物理内存的管理,物理内存地址只有内存管理模块能够使用;
第三,内存映射,需要将虚拟内存和物理内存映射、关联起来。

对于内存的访问,用户态的进程使用虚拟地址;内核态的也基本都是使用虚拟地址;
虚拟地址到物理地址的映射表,这个是内存管理模块的一部分。

虚拟内存的分配:
一部分用来放内核的东西,称为内核空间,一部分用来放进程的东西,称为用户空间。且进程无法直接访问内核空间。
用户空间在下,在低地址;内核空间在上,在高地址。
看看一个进程的虚拟地址都有什么?
进程的用户空间:
从最低位开始,Text Segment、Data Segment 和 BSS Segment。
Text Segment 是存放二进制可执行代码的位置,Data Segment 存放静态常量,BSS Segment 存放未初始化的静态变量。
堆(Heap)段。堆是往高地址增长的,是用来动态分配内存的区域,malloc 就是在这里面分配的。
Memory Mapping Segment:这块地址可以用来把文件映射进内存用的,如果二进制的执行文件依赖于某个动态链接库,就是在这个区域里面将 so 文件映射到了内存中。
栈(Stack)地址段:主线程的函数调用的函数栈就是用这里的。

内核空间:
内核里面,无论是从哪个进程进来的,看到的都是同一个内核空间,看到的都是同一个进程列表,但内核栈是各用各的。

查看进程内存空间的布局的命令:
pmap $PID 
cat /proc/$PID/maps

cat /proc/1741/maps

21 内存管理下:虚拟地址与物理地址的映射

一共有两种方式,分段机制 和 页机制
分段是x86历史原因,分页为了更高效的使用更大物理内存。分段机制用于段地址到线性地址的转换,分页机制用于线性地址到物理地址的转换。

分段机制
分段机制下的虚拟地址由两部分组成,段选择子和段内偏移量。
段选择子里面最重要的是段号,用作段表的索引。
段表里面保存的是这个段的基地址、段的界限和特权等级等。
虚拟地址中的段内偏移量应该位于0和段界限之间。如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。
如图:

实际linux中分段用于做权限审核,例如用户态 DPL 是 3,内核态 DPL 是 0。当用户态试图访问内核态的时候,会因为权限不足而报错。 Linux 倾向于另外一种从虚拟地址到物理地址的转换方式,称为分页(Paging)

对于物理内存,操作系统把它分成一块一块大小相同的页,这样更方便管理,例如有的内存页面长时间不用了,可以暂时写到硬盘上,称为换出。一旦需要的时候,再加载进来,叫做换入。这样可以扩大可用物理内存的大小,提高物理内存的利用率。

分页的原理如下:
有个页表,保存每个页的起始地址,再加上在页内的偏移量,组成线性地址,就能对于内存中的每个位置进行访问了。
页面的大小一般为 4KB。

虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。
如下图:


32 位环境下,虚拟地址空间共 4GB。如果分成 4KB 一个页,那就是 1M 个页。每个页表项需要 4 个字节来存储,那么整个 4GB 空间的映射就需要 4MB 的内存来存储映射表。如果每个进程都有自己的映射表,100 个进程就需要 400MB 的内存。对于内核来讲,有点大了 。

将页表再分页,4G 的空间需要 4M 的页表来存储映射。我们把这 4M 分成 1K(1024)个 4K,每个 4K 又能放在一页里面,这样 1K 个 4K 就是 1K 个页,这 1K 个页也需要一个表进行管理,我们称为页目录表,这个页目录表里面有 1K 项,每项 4 个字节,页目录表大小也是 4K。

页目录有 1K 项,用 10 位就可以表示访问页目录的哪一项。这一项其实对应的是一整页的页表项,也即 4K 的页表项。每个页表项也是 4 个字节,因而一整页的页表项是 1K 个。再用 10 位就可以表示访问页表项的哪一项,页表项中的一项对应的就是一个页,是存放数据的页,这个页的大小是 4K,用 12 位可以定位这个页内的任何一个位置。这样加起来正好 32 位

只给这个进程分配了一个数据页。如果只使用页表,也需要完整的 1M 个页表项共 4M 的内存,但是如果使用了页目录,页目录需要 1K 个全部分配,占用内存 4K,但是里面只有一项使用了。到了页表项,只需要分配能够管理那个数据页的页表项页就可以了,也就是说,最多 4K,这样内存就节省多了。


总结:
内存管理系统精细化为下面三件事情:
第一,虚拟内存空间的管理,将虚拟内存分成大小相等的页;
第二,物理内存的管理,将物理内存分成大小相等的页;
第三,内存映射,将虚拟内存页和物理内存页映射起来,并且在内存紧张的时候可以换出到硬盘中。

如图所示:

22 进程的虚拟内存空间管理

用户态和内核态的划分:整个虚拟内存空间要一分为二,一部分是用户态地址空间,一部分是内核态地址空间。
分界线:task_size
struct mm_struct 结构来管理内存,其中有一个成员变量stask_size;
32 位系统,最大能够寻址 2^32=4G,其中用户态虚拟地址空间是 3G,内核态是 1G。
64 位系统,内核空间和用户空间均是 128T,且间隔着很大的空隙

用户态虚拟空间的布局
用户态虚拟空间里面有几类数据,例如代码、全局变量、堆、栈、内存映射区等。在 struct mm_struct 里面都有表示。
如:
unsigned long start_code, end_code, start_data, end_data;
start_code 和 end_code 表示可执行代码的开始和结束位置,start_data 和 end_data 表示已初始化数据的开始位置和结束位置。

用户态的区域的位置如下图:


struct mm_struct 中对区域属性的描述:
struct vm_area_struct *mmap; // 单链表,用于将这些区域串起来。
struct rb_root mm_rb; // 红黑树,可以快速查找一个内存区域,并在需要改变的时候,能够快速修改。

虚拟内存区域可以映射到物理内存,也可以映射到文件;
映射是在 load_elf_binary 里面实现的。没错,就是它。加载内核的是它,启动第一个用户态进程 init 的是它,fork 完了以后,调用 exec 运行一个二进制程序的也是它。

最终形成如下图的内存映射图:


内核态的虚拟空间布局
内核态的虚拟空间和某一个进程没有关系,所有进程通过系统调用进入到内核之后,看到的虚拟地址空间都是一样的。

特别说明:内核态不是直接操作物理内存,只有内存管理部分可能直接操作物理内存,绝大部分依旧是操作虚拟内存。

32位内核布局:
如图:

直接映射区:是连续的,和物理内存是非常简单的映射关系,占896M
物理内存的开始的 896M 的空间,会被直接映射到 3G 至 3G+896M 的虚拟地址。
关于896M的使用:
在系统启动的时候,物理内存的前 1M 已经被占用了,从 1M 开始加载内核代码段,然后就是内核的全局变量、BSS 等

剩下的虚拟内存地址分成下面这几个部分:
在 896M 到 VMALLOC_START 之间有 8M 的空间。

VMALLOC_START 到 VMALLOC_END 之间称为内核动态映射空间,也即内核想像用户态进程一样 malloc 申请内存,在内核里面可以使用 vmalloc。假设物理内存里面,896M 到 1.5G 之间已经被用户态进程占用了,并且映射关系放在了进程的页表中,内核 vmalloc 的时候,只能从分配物理内存 1.5G 开始,就需要使用这一段的虚拟地址进行映射,映射关系放在专门给内核自己用的页表里面。

PKMAP_BASE 到 FIXADDR_START 的空间称为持久内核映射。使用 alloc_pages() 函数的时候,在物理内存的高端内存得到 struct page 结构,可以调用 kmap 将其映射到这个区域。

FIXADDR_START 到 FIXADDR_TOP(0xFFFF F000) 的空间,称为固定映射区域,主要用于满足特殊需求。

在最后一个区域可以通过 kmap_atomic 实现临时内核映射。假设用户态的进程要映射一个文件到内存中,先要映射用户态进程空间的一段虚拟地址到物理内存,然后将文件内容写入这个物理内存供用户态进程访问。给用户态进程分配物理内存页可以通过 alloc_pages(),分配完毕后,按说将用户态进程虚拟地址和物理内存的映射关系放在用户态进程的页表中,就完事大吉了。这个时候,用户态进程可以通过用户态的虚拟地址,也即 0 至 3G 的部分,经过页表映射后访问物理内存,并不需要内核态的虚拟地址里面也划出一块来,映射到这个物理内存页。但是如果要把文件内容写入物理内存,这件事情要内核来干了,这就只好通过 kmap_atomic 做一个临时映射,写入物理内存完毕后,再 kunmap_atomic 来解映射即可。


23 物理内存管理

平坦内存模型:
物理地址是连续的,页也是连续的,每个页大小也是一样的。每个页有一个结构 struct page 表示,这个结构也是放在一个数组里面,这样根据页号,很容易通过下标找到相应的 struct page 结构。

cpu访问内存的两种模式:
SMP模式
对称多处理器,多个cpu在总线的一侧,多个内存条组成一大片内存在总线的另一侧;所有的cpu访问内存都要过总线;缺点,就是总线会成为瓶颈,因为数据都要走它。

NUMA模式
非一致内存访问。
内存不是一整块。每个 CPU 都有自己的本地内存,CPU 访问本地内存不用过总线,因而速度要快很多,每个 CPU和内存在一起,称为一个 NUMA 节点。但是,在本地内存不足的情况下,每个 CPU 都可以去另外的 NUMA 节点申请内存,这个时候访问延时就会比较长。

SMA和NUMA示意图:


说明:
在SMP模式下,因为内存是连续的一块,所以可以从0开始对物理页编号,且是连续的。
在NUMA模式下,内存被分成多个节点,每个节点再被分成页,随着物理内存不再连续页号也不再连续,不过页号是全局唯一的。

NUMA节点
当前的主流场景是NUMA方式。NUMA节点的表示:
结构 typedef struct pglist_data pg_data_t,定义如下:
typedef struct pglist_data {
  struct zone node_zones[MAX_NR_ZONES];
  struct zonelist node_zonelists[MAX_ZONELISTS];
  int nr_zones;
  struct page *node_mem_map;
  unsigned long node_start_pfn;
  unsigned long node_present_pages; /* total number of physical pages */
  unsigned long node_spanned_pages; /* total size of physical page range, including holes */
  int node_id;
......
} pg_data_t;
其成员变量如下:
每一个节点都有自己的 ID:node_id;
node_mem_map 就是这个节点的 struct page 数组,用于描述这个节点里面的所有的页;
node_start_pfn 是这个节点的起始页号;
node_spanned_pages 是这个节点中包含不连续的物理内存地址的页面数;
node_present_pages 是真正可用的物理页面的数目。

如,64M 物理内存隔着一个 4M 的空洞,然后是另外的 64M 物理内存。这样换算成页面就是,16K 个页面隔着 1K 个页面,然后是另外 16K 个页面。这种情况下,node_spanned_pages 就是 33K 个页面,node_present_pages 就是 32K 个页面。

然后pglist_data中有很多跟zone相关的属性,具体如下:
每一个节点分成一个个区域 zone,放在数组 node_zones 里面。
nr_zones 表示当前节点的区域的数量。node_zonelists 是备用节点和它的内存区域的情况。
zone的类型(针对物理内存)如下:
ZONE_DMA 是指可用于作 DMA(Direct Memory Access,直接内存存取)的内存。DMA 是这样一种机制:要把外设的数据读入内存或把内存的数据传送到外设,原来都要通过 CPU 控制完成,但是这会占用 CPU,影响 CPU 处理其他事情,所以有了 DMA 模式。CPU 只需向 DMA 控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,这样就可以解放 CPU。

对于 64 位系统,有两个 DMA 区域。除了上面说的 ZONE_DMA,还有 ZONE_DMA32。

ZONE_NORMAL 是直接映射区,就是上面讲的,从物理内存到虚拟内存的内核区域,通过加上一个常量直接映射。

ZONE_HIGHMEM 是高端内存区,就是上面讲的,对于 32 位系统来说超过 896M 的地方,对于 64 位没必要有的一段区域。

ZONE_MOVABLE 是可移动区域,通过将物理内存划分为可移动分配区域和不可移动分配区域来避免内存碎片。


区域

内存分成了节点,把节点分成了区域。
 zone 的定义如下:

struct zone {
......
  struct pglist_data  *zone_pgdat;
  struct per_cpu_pageset __percpu *pageset;


  unsigned long    zone_start_pfn;

  unsigned long    managed_pages;
  unsigned long    spanned_pages;
  unsigned long    present_pages;


  const char    *name;
......
  /* free areas of different sizes */
  struct free_area  free_area[MAX_ORDER];


  /* zone flags, see below */
  unsigned long    flags;


  /* Primarily protects free_area */
  spinlock_t    lock;
......
} ____cacheline_internodealigned_in_

说明:
zone_start_pfn 表示属于这个 zone 的第一个页。
spanned_pages = zone_end_pfn - zone_start_pfn,也即 spanned_pages 指的是不管中间有没有物理内存空洞,反正就是最后的页号减去起始的页号。
present_pages = spanned_pages - absent_pages(pages in holes),也即 present_pages 是这个 zone 在物理内存中真实存在的所有 page 数目。
per_cpu_pageset 用于区分冷热页。如果一个页被加载到 CPU 高速缓存里面,这就是一个热页(Hot Page),CPU 读起来速度会快很多,如果没有就是冷页(Cold Page)。由于每个 CPU 都有自己的高速缓存,因而 per_cpu_pageset 也是每个 CPU 一个。


是组成物理内存的基本单位。
页的结构比较复杂,有很多union结构组成。union 结构是在 C 语言中被用于同一块内存根据情况保存不同类型数据的一种方式。这里之所以用了 union,是因为一个物理页面使用模式有多种。

第一种模式,要用就用一整页。这一整页的内存,或者直接和虚拟地址空间建立映射关系,我们把这种称为匿名页(Anonymous Page)。或者用于关联一个文件,然后再和虚拟地址空间建立映射关系,这样的文件,我们称为内存映射文件(Memory-mapped File)。

第二种模式,仅需分配小块内存。有时候,我们不需要一下子分配这么多的内存,例如分配一个 task_struct 结构,只需要分配小块的内存,去存储这个进程描述结构的对象。为了满足对这种小内存块的需要,Linux 系统采用了一种被称为 slab allocator(后来还有slub allocator和slob)的技术,用于分配称为 slab 的一小块内存。它的基本原理是从内存管理模块申请一整块页,然后划分成多个小块的存储池,用复杂的队列来维护这些小块的状态(状态包括:被分配了 / 被放回池子 / 应该被回收)。

页的分配
首先是具体页的管理:
所有的空闲页会被分组为11个页快链表,其中页块大小分别是 1、2、4、8、16、32、64、128、256、512 和 1024 个,即第 i 个页块链表中,页块中页的数目为 2^i。所以最大可以申请1024个连续页,对应4MB的连续内存;最小申请1个页,赌赢4kb内存。

如图:


然后具体页是怎么分配的呢?
采用的是伙伴系统。
当向内核请求分配 (2^(i-1),2^i]数目的页块时,按照 2^i 页块请求处理。
当分配的页块中有多余的页时,伙伴系统会根据多余的页块大小插入到对应的空闲页块链表中。
如果对应的页块链表中没有空闲页块,那我们就在更大的页块链表中去找。
例如,要请求一个 128 个页的页块时,先检查 128 个页的页块链表是否有空闲块。如果没有,则查 256 个页的页块链表;如果有空闲块的话,则将 256 个页的页块分成两份,一份使用,一份插入 128 个页的页块链表中。如果还是没有,就查 512 个页的页块链表;

总结:
有多个 CPU,那就有多个节点。每个节点用 struct pglist_data 表示,放在一个数组里面。
每个节点分为多个区域,每个区域用 struct zone 表示,也放在一个数组里面。
每个区域分为多个页。为了方便分配,空闲页放在 struct free_area 里面,使用伙伴系统进行管理和分配,每一页用 struct page 表示。

如图所示。

24 小对象下的物理内存分配

小内存的分配
创建进程的时候,会调用dup_task_struct,它想要试图复制一个 task_struct 对象,需要先调用 alloc_task_struct_node,分配一个task_struct对象。

dup_task_struct调用了kmem_cache_alloc_node 函数,task_struct_cachep在task_struct 的缓存区域 分配了一块内存。

task_struct_cachep是在系统初始化的时候被kmem_cache_create函数创建的。
有了task_struct_cachep这个缓存区,每次创建task_struct的时候,我们不用到内存里面去分配,先在缓存里面看看有没有直接可用的。
当一个进程结束,task_struct 也不用直接被销毁,而是放回到缓存中。

看下 缓存区 struct kmem_cache的结构

struct kmem_cache {
  struct kmem_cache_cpu __percpu *cpu_slab;
  /* Used for retriving partial slabs etc */
  unsigned long flags;
  unsigned long min_partial;
  int size;    /* The size of an object including meta data */
  int object_size;  /* The size of an object without meta data */
  int offset;    /* Free pointer offset. */
#ifdef CONFIG_SLUB_CPU_PARTIAL
  int cpu_partial;  /* Number of per cpu partial objects to keep around */
#endif
  struct kmem_cache_order_objects oo;
  /* Allocation and freeing of slabs */
  struct kmem_cache_order_objects max;
  struct kmem_cache_order_objects min;
  gfp_t allocflags;  /* gfp flags to use on each alloc */
  int refcount;    /* Refcount for slab cache destroy */
  void (*ctor)(void *);
......
  const char *name;  /* Name (only for display!) */
  struct list_head list;  /* List of slab caches */
......
  struct kmem_cache_node *node[MAX_NUMNODES];
};
有个变量 struct list_head list,所有的缓存(task_struct\mm_struct\fs_struct需要的)最后都会放在一个链表里面,也就是 LIST_HEAD(slab_caches)。

对于缓存来讲,其实就是分配了连续几页的大内存块,然后根据缓存对象的大小,切成小内存块。对应的示意图如下:


那这些缓存对象哪些被分配了、哪些在空着,什么情况下整个大内存块都被分配完了,需要向伙伴系统申请几个页形成新的大内存块?这些信息该由谁来维护呢?

最重要的两个成员变量出场的时候了。kmem_cache_cpu 和 kmem_cache_node,它们都是每个 NUMA 节点上有一个,我们只需要看一个节点里面的情况。


在分配缓存块的时候,要分两种路径,fast path 和 slow path,也就是快速通道和普通通道。其中 kmem_cache_cpu 就是快速通道,kmem_cache_node 是普通通道。每次分配的时候,要先从 kmem_cache_cpu 进行分配。如果 kmem_cache_cpu 里面没有空闲的块,那就到 kmem_cache_node 中进行分配;如果还是没有空闲的块,才去伙伴系统分配新的页。

页面换出
每个进程都有自己的虚拟地址空间,虚拟地址空间都非常大,物理内存不可能有这么多的空间放得下。所以,一般情况下,页面只有在被使用的时候,才会放在物理内存中。如果过了一段时间不被使用,即便用户进程并没有释放它,物理内存管理也有责任做一定的干预。例如,将这些物理内存中的页面换出到硬盘上去;将空出的物理内存,交给活跃的进程去使用。

页面换出的触发条件
1 分配内存的时候,发现没有地方了,就试图回收一下。
2 作为内存管理系统应该主动去做的,而不能等真的出了事儿再做,这就是内核线程 kswapd。这个内核线程,在系统初始化的时候就被创建。这样它会进入一个无限循环,直到系统停止。在这个循环中,如果内存使用没有那么紧张,那它就可以放心睡大觉;如果内存紧张了,就需要去检查一下内存,看看是否需要换出一些内存页。

物理内存来讲,从下层到上层的关系及分配模式如下:
物理内存分 NUMA 节点,分别进行管理;
每个 NUMA 节点分成多个内存区域;
每个内存区域分成多个物理页面;
伙伴系统将多个连续的页面作为一个大的内存块分配给上层;
kswapd 负责物理页面的换入换出;
Slub Allocator 将从伙伴系统申请的大内存块切成小块,分配给其他系统。


25 用户态内存的映射
mmap 的原理
内存映射:不仅仅是物理内存和虚拟内存之间的映射,还包括将文件中的内容映射到虚拟内存空间。此时访问内存空间就能够访问到文件里面的数据。

虚拟内存 物理内存 文件之间的映射如下图:

虚拟内存分配:
- 申请小块内存用 brk; 申请大块内存或文件映射用 mmap
- mmap 映射文件, 由 fd 得到 struct file
    - 调用 ...->do_mmap
        - 调用 get_unmapped_area 找到一个可以进行映射的 vm_area_struct
        - 调用 mmap_region 进行映射
    - get_unmapped_area
        - 匿名映射: 找到前一个 vm_area_struct
        - 文件映射: 调用 file 中 file_operations 文件的相关操作, 最终也会调用到 get_unmapped_area
    - mmap_region
        - 通过 vm_area_struct 判断, 能否基于现有的块扩展(调用 vma_merge)
        - 若不能, 调用 kmem_cache_alloc 在 slub 中得到一个 vm_area_struct 并进行设置
        - 若是文件映射: 则调用 file_operations 的 mmap 将 vm_area_struct 的内存操作设置为文件系统对应操作(读写内存就是读写文件系统)
        - 通过 vma_link 将 vm_area_struct 插入红黑树
        - 若是文件映射, 调用 __vma_link_file 建立文件到内存的反映射
说明:上面还都是操作虚拟内存。

建立虚拟地址和物理地址的关联,并分配物理地址
- 内存管理不直接分配物理内存, 物理内存在使用时才分配
- 用户态缺页异常(即访问虚拟内存的某个地址,但并没有对应的物理页), 触发缺页中断, 调用 do_page_default
- __do_page_fault 判断中断是否发生在内核
    - 若发生在内核, 调用 vmalloc_fault, 使用内核页表映射到物理页
    - 若不是, 找到对应 vm_area_struct 调用 handle_mm_fault
    - 得到多级页表地址 pgd 等
    - pgd 存在 task_struct.mm_struct.pgd 中
    - 全局页目录项 pgd 在创建进程 task_struct 时创建并初始化, 会调用 pgd_ctor 拷贝内核页表到进程的页表
- 进程被调度运行时, 通过 switch_mm_irqs_off->load_new_mm_cr3 切换内存上下文
- cr3 是 cpu 寄存器, 存储进程 pgd 的物理地址(load_new_mm_cr3 加载时通过直接内存映射进行转换)
- cpu 访问进程虚拟内存时, 从 cr3 得到 pgd 页表, 最后得到进程访问的物理地址
- 进程地址转换发生在用户态, 缺页时才进入内核态(调用__handle_mm_fault)
- __handle_mm_fault 调用 pud_alloc, pmd_alloc, handle_pte_fault 分配页表项
    - 若不存在 pte
        - 匿名页: 调用 do_anonymous_page 分配物理页 ①
        - 文件映射: 调用 do_fault ②
    - 若存在 pte, 调用 do_swap_page 换入内存 ③
    - ① 为匿名页分配内存
        - 调用 pte_alloc 分配 pte 页表项
        - 调用 ...->__alloc_pages_nodemask 分配物理页
        - mk_pte 页表项指向物理页; set_pte_at 插入页表项
    - ② 为文件映射分配内存 __do_fault
        - 以 ext4 为例, 调用 ext4_file_fault->filemap_fault
        - 文件映射一般有物理页作为缓存 find_get_page 找缓存页
        - 若有缓存页, 调用函数预读数据到内存
        - 若无缓存页, 调用 page_cache_read 分配一个, 加入 lru 队列, 调用 readpage 读数据: 调用 kmap_atomic 将物理内存映射到内核临时映射空间, 由内核读取文件, 再调用 kunmap_atomic 解映射
    - ③ do_swap_page
        - 先检查对应 swap 有没有缓存页
        - 没有, 读入 swap 文件(也是调用 readpage)
        - 调用 mk_pte; set_pet_at; swap_free(清理 swap)
- 避免每次都需要经过页表(存再内存中)访问内存
    - TLB 缓存部分页表项的副本

swap概念:
当linux系统的物理内存不够用的时候,就需要将物理内存中的不常用的那部分存储到磁盘中,以供当前运行的程序使用。当存到磁盘的那部分内容被用到时,又会将内存中不常用的内容存到磁盘,而磁盘中原本将要被用到的内容读入内存。swap就是指用于内存交换的那部分磁盘。
free -m可以查看

26 内核态内存映射
- 涉及三块内容:
    - 内存映射函数 vmalloc, kmap_atomic
    - 内核态页表存放位置和工作流程
    - 内核态缺页异常处理
- 内核态页表, 系统初始化时就创建
    - swapper_pg_dir 指向内核顶级页目录 pgd
        - xxx_ident/kernel/fixmap_pgt 分别是直接映射/内核代码/固定映射的 xxx 级页表目录
    - 创建内核态页表
        - swapper_pg_dir 指向 init_top_pgt, 是 ELF 文件的全局变量, 因此在内存管理初始化之前就存在
        - init_top_pgt 先初始化了三项
            - 第一项指向 level3_ident_pgt (内核代码段的某个虚拟地址) 减去 __START_KERNEL_MAP (虚拟地址空间内核代码起始虚拟地址) 得到实际物理地址
            - 第二项__PAGE_OFFSET_BASE 是虚拟地址空间里面内核的起始地址,也是指向 level3_ident_pgt
            - 第三项指向 level3_kernel_pgt 内核代码区
    - 初始化各页表项, 指向下一集目录
        - 页表覆盖范围较小, 内核代码 512MB, 直接映射区 1GB
        - 内核态也定义 mm_struct 指向 swapper_pg_dir
        - 初始化内核态页表, start_kernel→ setup_arch
            - load_cr3(swapper_pg_dir) 并刷新 TLB
            - 调用 init_mem_mapping→kernel_physical_mapping_init, 用 __va 将物理地址映射到虚拟地址, 再创建映射页表项
            - CPU 在保护模式下访问虚拟地址都必须通过 cr3, 系统只能照做
            - 在 load_cr3 之前, 通过 early_top_pgt 完成映射
个表项与下一级目录之间的关系如下图:


- vmalloc 和 kmap_atomic
    - 内核的虚拟地址空间 vmalloc 区域用于映射
    - kmap_atomic 临时映射
        - 32 位, 调用 set_pte 通过内核页表临时映射
        - 64 位, 调用 page_address→lowmem_page_address 进行映射
- 内核态缺页异常
    - kmap_atomic 直接创建页表进行映射
    - vmalloc 只分配内核虚拟地址, 访问时触发缺页中断, 调用 do_page_fault→vmalloc_fault 用于关联内核页表项
- kmem_cache 和 kmalloc 用于保存内核数据结构, 不会被换出; 而内核 vmalloc 会被换出

内核态总结图:


内存管理体系总结:
物理内存根据 NUMA 架构分节点。每个节点里面再分区域。每个区域里面再分页。
物理页面通过伙伴系统进行分配。分配的物理页面要变成虚拟地址让上层可以访问,kswapd 可以根据物理页面的使用情况对页面进行换入换出。

对于内存的分配需求,可能来自内核态,也可能来自用户态。

对于内核态,kmalloc 在分配大内存的时候,以及 vmalloc 分配不连续物理页的时候,直接使用伙伴系统,分配后转换为虚拟地址,访问的时候需要通过内核页表进行映射。
对于 kmem_cache 以及 kmalloc 分配小内存,则使用 slub 分配器,将伙伴系统分配出来的大块内存切成一小块一小块进行分配。kmem_cache 和 kmalloc 的部分不会被换出,因为用这两个函数分配的内存多用于保持内核关键的数据结构。内核态中 vmalloc 分配的部分会被换出,因而当访问的时候,发现不在,就会调用 do_page_fault。

对于用户态的内存分配,或者直接调用 mmap 系统调用分配,或者调用 malloc。调用 malloc 的时候,如果分配小的内存,就用 sys_brk 系统调用;如果分配大的内存,还是用 sys_mmap 系统调用。正常情况下,用户态的内存都是可以换出的,因而一旦发现内存中不存在,就会调用 do_page_fault。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值