一、内存管理架构

内存管理子系统架构可以分为:用户空间、内核空间及硬件部分3个层面,具体结构如下所示:
1、用户空间:应用程序使用malloc()申请内存资源/free()释放内存资源。
2、内核空间:内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。
3、硬件:处理器包含一个内存管理单元(Memory Management Uint,MMU)的部件,负责把虚拟地址转换为物理地址。

在这里插入图片描述

二、虚拟地址空间布局架构

上面的用户空间和内核空间所指的都是虚拟地址,物理地址没有用户和内核之分。每个项目的物理地址对于进程不可见,谁也不能直接访问这个物理地址。操作系统会给进程分配一个虚拟地址。所有进程看到的这个地址都是一样的,里面的内存都是从 0 开始编号。
所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。
在程序里面,指令写入的地址是虚拟地址。例如,位置为 10M 的内存区域,操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
当程序要访问虚拟地址的时候,由内核的数据结构进行转换,转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了。
32位处理器使用32位虚拟地址,而64位处理器却不是使用64位虚拟地址。因为目前应用程序没有那么大的内存需求,所以ARM64和X86_64处理器不支持完全的64位虚拟地址,而是使用了48位。我们计算一下,如果是 32 位,有 2^32 = 4G 的内存空间都是我的,不管内存是不是真的有 4G。如果是 64 位,在 ARM64和X86_64 下面,其实只使用了 48 位,那也挺恐怖的。48 位地址长度也就是对应了 256TB 的地址空间。我都没怎么见过 256T 的硬盘,别说是内存了。
在这里插入图片描述

这么大的虚拟空间一切二,一部分用来放内核的东西,称为内核空间,一部分用来放进程的东西,称为用户空间。

2.1内核地址空间布局

在这里插入图片描述
(1)线性映射区域的范围是[PAGE_OFFSET, 2 64 −1],起始位置是 PAGE_OFFSET =(0xFFFF FFFF FFFF FFFF << (VA_BITS-1)),长度是内核虚拟地址空间的一半。称为线性映射区域的原因是虚拟地址和物理地址是线性关系:
虚拟地址 =((物理地址 − PHYS_OFFSET)+ PAGE_OFFSET),其中 PHYS_OFFSET是内存的起始物理地址。
(2)vmemmap 区域的范围是[VMEMMAP_START, PAGE_OFFSET),长度是VMEMMAP_SIZE =(线性映射区域的长度 / 页长度 * page 结构体的长度上限)。
内核使用 page 结构体描述一个物理页,内存的所有物理页对应一个 page 结构体数组。如果内存的物理地址空间不连续,存在很多空洞,称为稀疏内存。vmemmap 区域是稀疏内存的 page 结构体数组的虚拟地址空间。
(3)PCI I/O 区域的范围是[PCI_IO_START, PCI_IO_END),长度是 16MB,结束地址是PCI_IO_END = (VMEMMAP_START − 2MB)。
外围组件互联(Peripheral Component Interconnect,PCI)是一种总线标准,PCI I/O 区域是 PCI 设备的 I/O 地址空间。
(4)固定映射区域的范围是[FIXADDR_START, FIXADDR_TOP),长度是FIXADDR_SIZE,结束地址是 FIXADDR_TOP = (PCI_IO_START − 2MB)。固定地址是编译时的特殊虚拟地址,编译的时候是一个常量,在内核初始化的时候映射到物理地址。
(5) vmalloc区域的范围是[VMALLOC_START, VMALLOC_END),起始地址是VMALLOC_START,等于内核模块区域的结束地址,结束地址是 VMALLOC_END = (PAGE_OFFSET −PUD_SIZE − VMEMMAP_SIZE − 64KB),其中 PUD_SIZE 是页上级目录表项映射的地址空间的长度。
vmalloc 区域是函数 vmalloc 使用的虚拟地址空间,内核使用 vmalloc 分配虚拟地址连续但物理地址不连续的内存。内核镜像在 vmalloc 区域,起始虚拟地址是(KIMAGE_VADDR + TEXT_OFFSET) ,其中 KIMAGE_VADDR 是内核镜像的虚拟地址的基准值,等于内核模块区域的结束地址MODULES_END;TEXT_OFFSET 是内存中的内核镜像相对内存起始位置的偏移。
(6)内核模块区域的范围是[MODULES_VADDR, MODULES_END),长度是 128MB,起始地址是 MODULES_VADDR =(内核虚拟地址空间的起始地址 + KASAN 影子区域的长度)。内核模块区域是内核模块使用的虚拟地址空间。
(7)KASAN 影子区域的起始地址是内核虚拟地址空间的起始地址,长度是内核虚拟地址空间长度的 1/8。内核地址消毒剂(Kernel Address SANitizer,KASAN)是一个动态的内存错误检查工具。它为发现释放后使用和越界访问这两类缺陷提供了快速和综合的解决方案。

2.2用户地址空间布局

用户虚拟地址空间有两种布局,区别是内存映射区域的起始位置和增长方向不同。当进程调用 execve 以装载 ELF 文件的时候,函数 load_elf_binary 将会创建进程的用户虚拟地址空间。
在这里插入图片描述

用户空间其实包含以下几个区域,我们最低位开始排起:
(1)代码段,数据段,未初始化的数据段(bss)
(2)存放动态生成数据的堆,堆是往高地址增长的
(3)动态库的代码段,数据段和未初始化的数据段(bss)
(4)存放局部变量和实现函数调用的栈
(5)把文件映射到虚拟地址空间的内存映射区
(6)存放在栈底的环境变量和参数字符串

内核使用内存描述符 mm_struct 描述进程的用户虚拟地址空间。

struct mm_struct {
	struct {
		struct vm_area_struct *mmap;//虚拟内存区域链表
		struct rb_root mm_rb;//虚拟内存区域红黑树	
		u64 vmacache_seqnum; //信号量,表示vma是否在cache中/* per-thread vmacache */
#ifdef CONFIG_MMU
		unsigned long (*get_unmapped_area) (struct file *filp,//在内存映射区域找到一块没有映射的区域
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
#endif
		unsigned long mmap_base;//内存映射区域的起始地址
		unsigned long mmap_legacy_base;	/* base of mmap area in bottom-up allocations */
#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES
		/* Base adresses for compatible mmap() */
		unsigned long mmap_compat_base;
		unsigned long mmap_compat_legacy_base;
#endif
		unsigned long task_size;//用户虚拟地址空间长度
		unsigned long highest_vm_end;	/* highest vma end address */
		pgd_t * pgd;//指向页全局目录,也就是第一级页表

		/**
		 * @mm_users: The number of users including userspace.
		 *
		 * Use mmget()/mmget_not_zero()/mmput() to modify. When this
		 * drops to 0 (i.e. when the task exits and there are no other
		 * temporary reference holders), we also release a reference on
		 * @mm_count (which may then free the &struct mm_struct if
		 * @mm_count also drops to 0).
		 */
		atomic_t mm_users;//共享一个用户虚拟地址空间的进程数量,也就是线程包含的进程数量

		/**
		 * @mm_count: The number of references to &struct mm_struct
		 * (@mm_users count as 1).
		 *
		 * Use mmgrab()/mmdrop() to modify. When this drops to 0, the
		 * &struct mm_struct is freed.
		 */
		atomic_t mm_count;//内存描述符的引用计数

#ifdef CONFIG_MMU
		atomic_long_t pgtables_bytes;//PTE页表页大小
#endif
		int map_count;//虚拟内存映射数量

		spinlock_t page_table_lock;//保护页表的锁
		/* Protects page tables and some
					     * counters
					     */
		struct rw_semaphore mmap_sem;

		struct list_head mmlist; //链表指向可能进行了交换的内存
		/* List of maybe swapped mm's.	These
					  * are globally strung together off
					  * init_mm.mmlist, and are protected
					  * by mmlist_lock
					  */


		unsigned long hiwater_rss;//RSS的高水位使用情况
		unsigned long hiwater_vm;//高水位虚拟内存使用情况

		unsigned long total_vm;//进程地址空间的映射页数
		unsigned long locked_vm;//被锁住不能换出的页数
		unsigned long pinned_vm;//不能换出也不能移动的页数
		unsigned long data_vm;//存放数据的页数
		unsigned long exec_vm;//存放可执行文件的页数
		unsigned long stack_vm;//存放栈的页数
		unsigned long def_flags;

		spinlock_t arg_lock; /* protect the below fields */
		
		//可执行程序代码段的起始地址和结束地址,数据段的起始地址和结束地址
		unsigned long start_code, end_code, start_data, end_data;
		//堆的起始地址,堆的当前地址(也是结束地址),栈的起始地址(栈的结束地址在寄存器的栈顶指针中)
		unsigned long start_brk, brk, start_stack;
		//参数字符串的起始地址和结束地址,环境变量的其实地址和结束地址
		unsigned long arg_start, arg_end, env_start, env_end;

		unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

		/*
		 * Special counters, in some configurations protected by the
		 * page_table_lock, in other configurations by being atomic.
		 */
		struct mm_rss_stat rss_stat;

		struct linux_binfmt *binfmt;

		/* Architecture-specific MM context */
		mm_context_t context;//处理器架构特定的内存瓜茉莉上下文

		unsigned long flags; /* Must use atomic bitops to access */

		struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_MEMBARRIER
		atomic_t membarrier_state;
#endif
#ifdef CONFIG_AIO
		spinlock_t			ioctx_lock;
		struct kioctx_table __rcu	*ioctx_table;
#endif
#ifdef CONFIG_MEMCG
		/*
		 * "owner" points to a task that is regarded as the canonical
		 * user/owner of this mm. All of the following must be true in
		 * order for it to be changed:
		 *
		 * current == mm->owner
		 * current->mm != mm
		 * new_owner->mm == mm
		 * new_owner->alloc_lock is held
		 */
		struct task_struct __rcu *owner;
#endif
		struct user_namespace *user_ns;

		/* store ref to file /proc/<pid>/exe symlink points to */
		struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIER
		struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
		pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_NUMA_BALANCING
		/*
		 * numa_next_scan is the next time that the PTEs will be marked
		 * pte_numa. NUMA hinting faults will gather statistics and
		 * migrate pages to new nodes if necessary.
		 */
		unsigned long numa_next_scan;

		/* Restart point for scanning and setting pte_numa */
		unsigned long numa_scan_offset;

		/* numa_scan_seq prevents two threads setting pte_numa */
		int numa_scan_seq;
#endif
		/*
		 * An operation with batched TLB flushing is going on. Anything
		 * that can move process memory needs to flush the TLB when
		 * moving a PROT_NONE or PROT_NUMA mapped page.
		 */
		atomic_t tlb_flush_pending;
#ifdef CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH
		/* See flush_tlb_batched_pending() */
		bool tlb_flush_batched;
#endif
		struct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGE
		atomic_long_t hugetlb_usage;
#endif
		struct work_struct async_put_work;

#if IS_ENABLED(CONFIG_HMM)
		/* HMM needs to track a few things per mm */
		struct hmm *hmm;
#endif
	} __randomize_layout;

	/*
	 * The mm_cpumask needs to be at the end of mm_struct, because it
	 * is dynamically sized based on nr_cpu_ids.
	 */
	unsigned long cpu_bitmap[];
};

三、物理内存体系架构

目前多处理器系统有两种体系架构:

1)一致内存访问(Uniform Memory Access,UMA),所有处理器访问内存花费的时间是相同。

在这里插入图片描述

这种结构的CPU 是通过一条通用总线连接到内存。这种设计中,瓶颈马上出现了。第一个瓶颈与设备对RAM 的访问有关。早期,所有设备之间的通信都需要经过 CPU,结果严重影响了整个系统的性能。为了解决这个问题,有些设备加入了直接内存访问(DMA)的能力。DMA 允许设备在北桥的帮助下,无需 CPU 的干涉,直接读写 RAM。到了今天,所有高性能的设备都可以使用 DMA。虽然 DMA 大大降低了 CPU 的负担,却占用了北桥的带宽,与 CPU 形成了争用。所以现在很少使用了。

2)非一致内存访问(Non-Unit Memory Access,NUMA):指内存被划分成多个内存节点的多处理器系统,访问一个内存节点花费的时间取决于处理器和内存节点的距离。
在这里插入图片描述

图中的NCPU是多个物理CPU core。当一个系统中的CPU越来越多、内存越来越多的时候,内存总线就会成为一个系统的瓶颈。如果大家都还挤在同一个总线上,速度必然很慢。于是我们可以采取一种方法,把一部分CPU和一部分内存直连在一起,构成一个节点,不同节点之间CPU访问内存采用间接方式。节点内的内存访问速度就会很快,节点之间的内存访问速度虽然很慢,但是我们可以尽量减少节点之间的内存访问,这样系统总的内存访问速度就会很快。系统仍然要让所有内存能被所有处理器所访问,导致内存不再是统一的资源。处理器能以正常的速度访问本地内存(连接到该处理器的内存)。但它访问其它处理器的内存时,却需要使用处理器之间的互联通道。有多少条bus不是cpu决定的,也不是内存决定的,而是主板决定的,主板决定着某几个cpu连接在某条bus上,某些内存连接在某条bus上。
使用下面的命令可以查看内存架构:

jian@ubuntu:~$ numactl -H
available: 1 nodes (0)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 7953 MB
node 0 free: 5900 MB
node distances:
node   0 
  0:  10 

如上面所示,我们只有一个内存节点,我们这个8个cpu核心都是使用这一个内存节点,所以我们可以认为我们是UMA架构的,当然,也可以认为是只有一个节点的NUMA架构。Linux中的代码对UMA和NUMA是统一处理的,因为UMA可以看成是只有一个节点的NUMA。如果编译内核时配置了CONFIG_NUMA,内核支持NUMA架构的计算机,内核中会定义节点指针数组来表示各个node。如果编译内核时没有配置CONFIG_NUMA,则内核只支持UMA架构的计算机,内核中会定义一个内存节点。这样所有其它的代码都可以统一处理了。

无论是UMA架构还是iNUMA架构,处理器通常只实现一个物理地址空间,外围设备和物理内存使用统一的物理地址空间。arm64还把分配给外围设备的物理地址区域称为设备内存。

下面是飞腾E2000的物理地址分布图:
在这里插入图片描述
虽然上图只用到了44位,但是在arm64中,我用过的arm64开发版,他们的物理地址都是48位的,包括现在这个飞腾的E2000。

CONFIG_ARM64_PA_BITS_48=y
CONFIG_ARM64_PA_BITS=48

ARM64 架构定义了两种内存类型。

  1. 正常内存(Normal Memory):包括物理内存和只读存储器(ROM)。
  2. 设备内存(Device Memory):指分配给外围设备寄存器的物理地址区域。

3.1 正常内存

ARM64处理器对正常内存只有通过内存映射方式的方式。但是,正常内存可以设置共享属性和缓存属性。共享属性用来定义一个位置是否可以被多个核共享,分为不可共享、内部共享和外部共享。不可共享是指只被处理器的一个核使用,内部共享是指一个处理器的所有核共享或者多个处理器共享,外部共享是指处理器和其他观察者(比如图形处理单元或 DMA 控制器)共享。缓存属性用来定义访问时是否通过处理器的缓存。

3.2 设备内存

ARM64处理器对外围设备寄存器的编址方式有两种:

  1. I/O 指令方式:处理器为外围设备专门实现了一套IO读写指令,处理器通过专门的 I/O 指令来访问这一空间中的地址单元,比如,arm64专门的 I/O 指令:
#define readb(c)		({ u8  __v = readb_relaxed(c); __iormb(__v); __v; })
#define readw(c)		({ u16 __v = readw_relaxed(c); __iormb(__v); __v; })
#define readl(c)		({ u32 __v = readl_relaxed(c); __iormb(__v); __v; })
#define readq(c)		({ u64 __v = readq_relaxed(c); __iormb(__v); __v; })

#define writeb(v,c)		({ __iowmb(); writeb_relaxed((v),(c)); })
#define writew(v,c)		({ __iowmb(); writew_relaxed((v),(c)); })
#define writel(v,c)		({ __iowmb(); writel_relaxed((v),(c)); })
#define writeq(v,c)		({ __iowmb(); writeq_relaxed((v),(c)); })
  1. 内存映射方式:处理器也实现了一个单独的地址空间,称为“I/O 地址空间”或“I/O 端口空间”,处理器可以像访问一个内存单元那样访问外围设备,不需要提供专门的 I/O 指令。

ARM64 架构根据 3 种属性把设备内存分为 4 种类型。
(1)Device-nGnRnE,这种类型限制最严格。
(2)Device-nGnRE。
(3)Device-nGRE。
(4)Device-GRE,这种类型限制最少。

3 种属性分别如下。
(1)聚集属性:G 表示聚集(Gathering),nG 表示不聚集(non Gathering)。聚集属性决定对内存区域的多个访问是否可以被合并为一个总线事务。如果地址被标记为“不聚集”,那么必须按照程序里面的地址和长度访问。如果地址被标记为“聚集”,处理器可以把两个“写一个字节”的访问合并成一个“写两个字节”的访问,可以把对相同内存位置的多个访问合并,例如读相同位置两次,处理器只需要读一次,为两条指令返回相同的结果。
(2)重排序属性:R 表示重排序(Re-ordering),nR 表示不重排序(non Re-ordering)。这个属性决定对相同设备的多个访问是否可以重新排序。如果地址被标记为“不重排序”,那么对同一个块的访问总是按照程序顺序执行。
(3)早期写确认属性:E 表示早期写确认(Early Write Acknowledgement),nE 表示不执行早期写确认(non Early Write Acknowledgement)。这个属性决定是否允许处理器和从属设备之间的中间写缓冲区发送“写完成”确认。如果地址被标记为“不执行早期写确认”,那么必须由外围设备发送“写完成”确认。如果地址被标记为“早期写确认”,那么允许写缓冲区在外围设备收到数据之前发送“写完成”确认。

四、内存结构

由于现在我接触的基本都是使用UMA的结构,所以下面说的都是这种:
内存管理子系统使用节点(node),区域(zone)、页(page)三级结构描述物理内存。节点就是上面说的bus,一条bus就会有一个节点,我们都是UMA架构,所以只会有一个节点;zone是每个节点会把内存分为高端内存,低端内存,DMA区域等等的内存区域;页就是物理内存的最小单位了,也是虚拟内存映射到物理内存的最小单位。最后,在NUMA内存架构中, Linux定义了一个 pglist_data 的结构体来管理所有的内存节点.

  1. 内存节点(node)

在NUMA体系的内存节点是根据处理器和内存的距离划分的,而在具有不连续内存的NUMA系统中,表示比区域的级别更高的内存区域,根据物理地址是否连续划分,每块物理地址连续的内存是一个内存节点。内存节点结构体在linux内核include/linux/mmzone.h文件中,

/*
 * On NUMA machines, each NUMA node would have a pg_data_t to describe
 * it's memory layout. On UMA machines there is a single pglist_data which
 * describes the whole memory.
 *
 * Memory statistics and page replacement data structures are maintained on a
 * per-zone basis.
 */
struct bootmem_data;
typedef struct pglist_data {
	struct zone node_zones[MAX_NR_ZONES];//内存区域数组
	struct zonelist node_zonelists[MAX_ZONELISTS];//备用区域数组

	int nr_zones;//该节点包含的内存区域数量
#ifdef CONFIG_FLAT_NODE_MEM_MAP	/* means !SPARSEMEM */
	struct page *node_mem_map;//指向物理页描述符数组
#ifdef CONFIG_PAGE_EXTENSION
	struct page_ext *node_page_ext;//页的扩展属性
#endif
#endif
#ifndef CONFIG_NO_BOOTMEM
	struct bootmem_data *bdata;//早期内存管理器
......
} pg_data_t;

现在在我们嵌入式,我们查看config可以看到:

jian@ubuntu:~/share/e2000/phytium-linux-kernel$ cat .config |grep UMA
CONFIG_ARCH_SUPPORTS_NUMA_BALANCING=y
# CONFIG_NUMA is not set

CONFIG_NUMA is not set表示现在使用的是UMA架构,对于UMA,内核会定义唯一的一个节点。内存节点定义在linux内核mm/nobootmem.c:

#ifndef CONFIG_NUMA
struct pglist_data __refdata contig_page_data;
EXPORT_SYMBOL(contig_page_data);
#endif

对于NUMA,内核会定义内存节点指针数组,不同架构定义的不一定相同,我们以arm64为例。内存节点定义在arch/arm64/mm/numa.c

struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;
  1. 内存区域(zone)

每一个节点分成一个个区域 zone,放在数组 node_zones 里面。这个数组的大小为 MAX_NR_ZONES。我们来看区域的定义。内存节点被划分为内存区域,内存区域结构体在linux内核include/linux/mmzone.h文件中

struct zone {
	unsigned long watermark[NR_WMARK];
	unsigned long nr_reserved_highatomic;
	long lowmem_reserve[MAX_NR_ZONES];
#ifdef CONFIG_NUMA
	int node;
#endif
	struct pglist_data	*zone_pgdat;
	struct per_cpu_pageset __percpu *pageset;
#ifndef CONFIG_SPARSEMEM
	unsigned long		*pageblock_flags;
#endif /* CONFIG_SPARSEMEM */
	unsigned long		zone_start_pfn;
	unsigned long		managed_pages;
	unsigned long		spanned_pages;
	unsigned long		present_pages;
	const char		*name;
#ifdef CONFIG_MEMORY_ISOLATION
	unsigned long		nr_isolate_pageblock;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
	seqlock_t		span_seqlock;
#endif
	int initialized;
	ZONE_PADDING(_pad1_)
	struct free_area	free_area[MAX_ORDER];//内存区域数组,用于伙伴分配器进行页分配
	unsigned long		flags;//内存区域的属性,定义在下面
	spinlock_t		lock;
	ZONE_PADDING(_pad2_)
	unsigned long percpu_drift_mark;
#ifdef CONFIG_COMPACTION
	unsigned int		compact_considered;
	unsigned int		compact_defer_shift;
	int			compact_order_failed;
#endif
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
	bool			compact_blockskip_flush;
#endif
	bool			contiguous;
	ZONE_PADDING(_pad3_)
	atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];
	atomic_long_t		vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

//struct zone 的 flags 参数
enum zone_type {
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
	ZONE_DEVICE,
#endif
	__MAX_NR_ZONES
};

ZONE_DMA 是指可用于作 DMA(Direct Memory Access,直接内存存取)的内存。DMA 是这样一种机制:要把外设的数据读入内存或把内存的数据传送到外设,原来都要通过 CPU 控制完成,但是这会占用 CPU,影响 CPU 处理其他事情,所以有了 DMA 模式。CPU 只需向 DMA 控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,这样就可以解放 CPU。对于 64 位系统,有两个 DMA 区域。除了上面说的 ZONE_DMA,还有 ZONE_DMA32。在这里你大概理解 DMA 的原理就可以,不必纠结,我们后面会讲 DMA 的机制。
ZONE_NORMAL 是直接映射区,就是上一节讲的,从物理内存到虚拟内存的内核区域,通过加上一个常量直接映射。ZONE_HIGHMEM 是高端内存区,就是上一节讲的,对于 32 位系统来说超过 896M 的地方,对于 64 位没必要有的一段区域。
ZONE_MOVABLE 是可移动区域,通过将物理内存划分为可移动分配区域和不可移动分配区域来避免内存碎片。
这里你需要注意一下,我们刚才对于区域的划分,都是针对物理内存的。
我们看看我们的内存各个zone是怎么分布的:

root@kylin:~# cat /proc/zoneinfo  |grep Node
Node 0, zone    DMA32
Node 0, zone   Normal
Node 0, zone  Movable

我们可以看到我们的飞腾e2000只有一个节点,分为3个zone。
3. 内存页(page)

了解了区域 zone,接下来我们就到了组成物理内存的基本单位,页的数据结构 struct page。这是一个特别复杂的结构,里面有很多的 union,union 结构是在 C 语言中被用于同一块内存根据情况保存不同类型数据的一种方式。这里之所以用了 union,是因为一个物理页面使用模式有两种。第一种模式,仅需分配小块内存,Linux 系统采用了一种被称为 slab allocator的技术,下一节会讲。
第二种模式,要用就用一整页。这一整页的内存,或者直接和虚拟地址空间建立映射关系,我们把这种称为匿名页(Anonymous Page)。或者用于关联一个文件,然后再和虚拟地址空间建立映射关系,这样的文件,我们称为内存映射文件(Memory-mapped File)。
每个物理页对应一个page结构体,称为页描述符,内存节点的pglist_data实例的成员node_mem_map指向该内存节点包含的所有物理页的页描述符组成的数组。内存区域结构体在linux内核include/linux/mm_types.h文件中

struct page {
    /* 在lru算法中主要用到的标志flags
     * PG_active: 表示此页当前是否活跃,当放到或者准备放到活动lru链表时,被置位
     * PG_referenced: 表示此页最近是否被访问,每次页面访问都会被置位
     * PG_lru: 表示此页是处于lru链表中的
     * PG_mlocked: 表示此页被mlock()锁在内存中,禁止换出和释放
     * PG_swapbacked: 表示此页依靠swap,可能是进程的匿名页(堆、栈、数据段),匿名mmap共享内存映射,shmem共享内存映射
     */
	unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously */
	/*
	 * Five words (20/40 bytes) are available in this union.
	 * WARNING: bit 0 of the first word is used for PageTail(). That
	 * means the other users of this union MUST NOT use the bit to
	 * avoid collision and false-positive PageTail().
	 */
	union {//这个是最大的联合体,下面说明各个部分在什么情况下使用
		//该结构体用于匿名映射
		struct {	/* Page cache and anonymous pages */
			/**
			 * @lru: Pageout list, eg. active_list protected by
			 * zone_lru_lock.  Sometimes used as a generic list
			 * by the page owner.
			 */
			struct list_head lru;
			/* See page-flags.h for PAGE_MAPPING_FLAGS */
			struct address_space *mapping;
			pgoff_t index;		/* Our offset within mapping. */
			/**
			 * @private: Mapping-private opaque data.
			 * Usually used for buffer_heads if PagePrivate.
			 * Used for swp_entry_t if PageSwapCache.
			 * Indicates order in the buddy system if PageBuddy.
			 */
			unsigned long private;
		};
		//该结构体用于slab等管理
		struct {	/* slab, slob and slub */
			union {
				struct list_head slab_list;	/* uses lru */
				struct {	/* Partial pages */
					struct page *next;
#ifdef CONFIG_64BIT
					int pages;	/* Nr of pages left */
					int pobjects;	/* Approximate count */
#else
					short int pages;
					short int pobjects;
#endif
				};
			};
			struct kmem_cache *slab_cache; /* not slob */
			/* Double-word boundary */
			void *freelist;		/* first free object */
			union {
				void *s_mem;	/* slab: first object */
				unsigned long counters;		/* SLUB */
				struct {			/* SLUB */
					unsigned inuse:16;
					unsigned objects:15;
					unsigned frozen:1;
				};
			};
		};
		//该结构体用于复合页的尾页
		struct {	/* Tail pages of compound page */
			unsigned long compound_head;	/* Bit zero is set */

			/* First tail page only */
			unsigned char compound_dtor;
			unsigned char compound_order;
			atomic_t compound_mapcount;
		};
		//该结构体用于复合页的第二尾页
		struct {	/* Second tail page of compound page */
			unsigned long _compound_pad_1;	/* compound_head */
			unsigned long _compound_pad_2;
			struct list_head deferred_list;
		};
		//该结构体用于页表页
		struct {	/* Page table pages */
			unsigned long _pt_pad_1;	/* compound_head */
			pgtable_t pmd_huge_pte; /* protected by page->ptl */
			unsigned long _pt_pad_2;	/* mapping */
			union {
				struct mm_struct *pt_mm; /* x86 pgds only */
				atomic_t pt_frag_refcount; /* powerpc */
			};
#if ALLOC_SPLIT_PTLOCKS
			spinlock_t *ptl;
#else
			spinlock_t ptl;
#endif
		};
		//该结构体用于设备内存页
		struct {	/* ZONE_DEVICE pages */
			/** @pgmap: Points to the hosting device page map. */
			struct dev_pagemap *pgmap;
			unsigned long hmm_data;
			unsigned long _zd_pad_1;	/* uses mapping */
		};

		/** @rcu_head: You can use this to free a page by RCU. */
		struct rcu_head rcu_head;
	};

	union {		/* This union is 4 bytes in size. */
		/*
		 * If the page can be mapped to userspace, encodes the number
		 * of times this page is referenced by a page table.
		 */
		atomic_t _mapcount;//内存管理子系统中映射的页表项计数,用于表示页是否已经映射

		/*
		 * If the page is neither PageSlab nor mappable to userspace,
		 * the value stored here may help determine what this page
		 * is used for.  See page-flags.h for a list of page types
		 * which are currently stored here.
		 */
		unsigned int page_type;

		unsigned int active;		/* SLAB */
		int units;			/* SLOB */
	};

	/* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
	atomic_t _refcount;

#ifdef CONFIG_MEMCG
	struct mem_cgroup *mem_cgroup;
#endif

	/*
	 * On machines where all RAM is mapped into kernel address space,
	 * we can simply calculate the virtual address. On machines with
	 * highmem some memory is mapped into kernel virtual memory
	 * dynamically, so we need a place to store that address.
	 * Note that this field could be 16 bits on x86 ... ;)
	 *
	 * Architectures with slow multiplication can define
	 * WANT_PAGE_VIRTUAL in asm/page.h
	 */
#if defined(WANT_PAGE_VIRTUAL)
	void *virtual;			/* Kernel virtual address (NULL if
					   not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
	int _last_cpupid;
#endif
} _struct_page_alignment;

#define PAGE_FRAG_CACHE_MAX_SIZE	__ALIGN_MASK(32768, ~PAGE_MASK)
#define PAGE_FRAG_CACHE_MAX_ORDER	get_order(PAGE_FRAG_CACHE_MAX_SIZE)

struct page_frag_cache {
	void * va;
#if (PAGE_SIZE < PAGE_FRAG_CACHE_MAX_SIZE)
	__u16 offset;
	__u16 size;
#else
	__u32 offset;
#endif
	/* we maintain a pagecount bias, so that we dont dirty cache line
	 * containing page->_refcount every time we allocate a fragment.
	 */
	unsigned int		pagecnt_bias;
	bool pfmemalloc;
};

页帧代表系统内存的最小单位,对内存中的每个页都会创建 struct page 的一个实例。该结构使用了喝多联合体,就是为了保持该结构尽可能小,因为即使在中等程度的内存配置下,系统的内存同样会分解为大量的页。例如,系统的标准页长度为4 KiB,在主内存大小为4G时,大约共有1000000页。
其结构大概如下图所示:
在这里插入图片描述
也就是说,如果是UMA架构,我们只有一个节点,就是struct pglist_data contig_page_data;,如果是NUMA架构我们会有多个节点,组成的pglist_data 数组就是struct pglist_data *node_data[MAX_NUMNODES] ;在struct pglist_data下是由struct zone组成的。而struct zone里面的struct free_area free_area[MAX_ORDER],就是我们后面说的伙伴系统。这些都是只是记录节点的信息而已,具体的每一个页,也就是struct page还在由struct pglist_data掌控的。具体物理内存node、zone、page关系如下:

在这里插入图片描述
我们可以通过/proc/pagetypeinfo文件看到完成的内存模型,node、zone、type、page:

jian@ubuntu:~/share/note/phytium-linux-kernel-5$ sudo cat  /proc/pagetypeinfo
[sudo] password for jian: 
Page block order: 9
Pages per block:  512

Free pages count per migrate type at order       0      1      2      3      4      5      6      7      8      9     10 
Node    0, zone      DMA, type    Unmovable      0      0      0      0      0      0      0      1      0      0      0 
Node    0, zone      DMA, type      Movable      0      0      0      0      0      0      0      0      0      1      3 
Node    0, zone      DMA, type  Reclaimable      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone      DMA, type   HighAtomic      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone      DMA, type      Isolate      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone    DMA32, type    Unmovable    162    189     71     27     13      2      0      0      0      5      0 
Node    0, zone    DMA32, type      Movable     38      4      1      1      1      1      0      0      0      0    202 
Node    0, zone    DMA32, type  Reclaimable     76     29      6     10      6      4      1      0      1      0      0 
Node    0, zone    DMA32, type   HighAtomic      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone    DMA32, type      Isolate      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone   Normal, type    Unmovable     14     12     21     26     14      7      1      0      0      1      3 
Node    0, zone   Normal, type      Movable      1      1      3      2      0      1      0      1      0      0      0 
Node    0, zone   Normal, type  Reclaimable      8      5      4      8      1      0      1      0      1      0      0 
Node    0, zone   Normal, type   HighAtomic      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone   Normal, type      Isolate      0      0      0      0      0      0      0      0      0      0      0 

Number of blocks type     Unmovable      Movable  Reclaimable   HighAtomic      Isolate 
Node 0, zone      DMA            1            7            0            0            0 
Node 0, zone    DMA32          160         1340           28            0            0 
Node 0, zone   Normal          142          340           30            0            0 


五、内存模型

内存模型是其实就是从cpu的角度看,其物理内存的分布情况,在linux kernel中,使用什么的方式来管理这些物理内存。
内存管理子系统支持3种内存模型:
1)平坦内存(Flat Memory):内存的物理地址空间是连续的,没有空洞。
如果从系统中任意一个processor的角度来看,当它访问物理内存的时候,物理地址空间是一个连续的,没有空洞的地址空间,那么这种计算机系统的内存模型就是Flat memory。这种内存模型下,物理内存的管理比较简单,每一个物理页帧都会有一个page数据结构来抽象,因此系统中存在一个struct page的数组(mem_map),每一个数组条目指向一个实际的物理页帧(page frame)。在flat memory的情况下,PFN(page frame number)和mem_map数组index的关系是线性的(有一个固定偏移,如果内存对应的物理地址等于0,那么PFN就是数组index)。因此从PFN到对应的page数据结构是非常容易的,反之亦然,具体可以参考page_to_pfn和pfn_to_page的定义。此外,对于flat memory model,节点(struct pglist_data)只有一个(为了和Discontiguous Memory Model采用同样的机制)。需要强调的是struct page所占用的内存位于直接映射(directly mapped)区间,因此操作系统不需要再为其建立page table。

#define __pfn_to_page(pfn)  (mem_map + ((pfn) - ARCH_PFN_OFFSET)) 

2)不连续内存(Discontiguous Memory):内存的物理地址空间存在空洞,这种模型可以高效地处理空洞。
如果cpu在访问物理内存的时候,其地址空间有一些空洞,是不连续的,那么这种计算机系统的内存模型就是Discontiguous memory。一般而言,NUMA架构的计算机系统的memory model都是选择Discontiguous Memory,不过,这两个概念其实是不同的。NUMA强调的是memory和processor的位置关系,和内存模型其实是没有关系的,只不过,由于同一node上的memory和processor有更紧密的耦合关系(访问更快),因此需要多个node来管理。Discontiguous memory本质上是flat memory内存模型的扩展,整个物理内存的address space大部分是成片的大块内存,中间会有一些空洞,每一个成片的memory address space属于一个node(如果局限在一个node内部,其内存模型是flat memory)。因此,这种内存模型下,节点数据(struct pglist_data)有多个,宏定义NODE_DATA可以得到指定节点的struct pglist_data。而,每个节点管理的物理内存保存在struct pglist_data 数据结构的node_mem_map成员中(概念类似flat memory中的mem_map)。这时候,从PFN转换到具体的struct page会稍微复杂一点,我们首先要从PFN得到node ID,然后根据这个ID找到对于的pglist_data 数据结构,也就找到了对应的page数组,之后的方法就类似flat memory了。

#define __pfn_to_page(pfn)            \ 
({    unsigned long __pfn = (pfn);        \ 
    unsigned long __nid = arch_pfn_to_nid(__pfn);  \ 
    NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\ 
})

3)稀疏内存(Space Memory):内存的物理地址空间存在空洞,如果要支持内存热插拔,只能选择稀疏内存模型。
Memory model也是一个演进过程,刚开始的时候,使用flat memory去抽象一个连续的内存地址空间(mem_maps[]),出现NUMA之后,整个不连续的内存空间被分成若干个node,每个node上是连续的内存地址空间,也就是说,原来的单一的一个mem_maps[]变成了若干个mem_maps[]了。一切看起来已经完美了,但是memory hotplug的出现让原来完美的设计变得不完美了,因为即便是一个node中的mem_maps[]也有可能是不连续了。其实,在出现了sparse memory之后,Discontiguous memory内存模型已经不是那么重要了,按理说sparse memory最终可以替代Discontiguous memory的,这个替代过程正在进行中,4.4的内核仍然是有3中内存模型可以选择。
为什么说sparse memory最终可以替代Discontiguous memory呢?实际上在sparse memory内存模型下,连续的地址空间按照SECTION(例如1G)被分成了一段一段的,其中每一section都是hotplug的,因此sparse memory下,内存地址空间可以被切分的更细,支持更离散的Discontiguous memory。此外,在sparse memory没有出现之前,NUMA和Discontiguous memory总是剪不断,理还乱的关系:NUMA并没有规定其内存的连续性,而Discontiguous memory系统也并非一定是NUMA系统,但是这两种配置都是multi node的。有了sparse memory之后,我们终于可以把内存的连续性和NUMA的概念剥离开来:一个NUMA系统可以是flat memory,也可以是sparse memory,而一个sparse memory系统可以是NUMA,也可以是UMA的。对于经典的sparse memory模型,一个section的struct page数组所占用的内存来自directly mapped区域,页表在初始化的时候就建立好了,分配了page frame也就是分配了虚拟地址。但是,对于SPARSEMEM_VMEMMAP而言,虚拟地址一开始就分配好了,是vmemmap开始的一段连续的虚拟地址空间,每一个page都有一个对应的struct page,当然,只有虚拟地址,没有物理地址。因此,当一个section被发现后,可以立刻找到对应的struct page的虚拟地址,当然,还需要分配一个物理的page frame,然后建立页表什么的,因此,对于这种sparse memory,开销会稍微大一些(多了个建立映射的过程)。

#define __pfn_to_page(pfn)    (vmemmap + (pfn))

#define vmemmap            ((struct page *)VMEMMAP_START - \
                 SECTION_ALIGN_DOWN(memstart_addr >> PAGE_SHIFT))

六、虚拟地址和物理地址的转换

cpu读写指令和数据都需要用到内存,而我们程序操作的都是虚拟内存,大家觉得,为什么系统设计者要引入虚拟地址呢?
设想一下,如果一台计算机的内存中只运行一个程序 A,因为程序 A 的地址在链接时就可以确定,例如从内存地址 0x8000 开始,每次运行程序 A 都装入内存 0x8000 地址处开始运行,没有其它程序干扰。现在改变一下,内存中又放一道程序 B,程序 A 和程序 B 各自运行一秒钟,如此循环,直到其中之一结束。这个新场景下就会产生一些问题,当然这里我们只关心内存相关的这几个核心问题。

  1. 谁来保证程序 A 跟程序 B 没有内存地址的冲突?换句话说,就是程序 A、B 各自放在什么内存地址,这个问题是由 A、B 程序协商,还是由操作系统决定。
  2. 怎样保证程序 A 跟程序 B 不会互相读写各自的内存空间?这个问题相对简单,用保护模式就能解决。
  3. 如何解决内存容量问题?程序 A 和程序 B,在不断开发迭代中程序代码占用的空间会越来越大,导致内存装不下。
  4. 还要考虑一个扩展后的复杂情况,如果不只程序 A、B,还可能有程序 C、D、E、F、G……它们分别由不同的公司开发,而每台计算机的内存容量不同。这时候,又对我们的内存方案有怎样的影响呢?

想完美地解决以上最核心的 4 个问题,一个较好的方案是:让所有的程序都各自享有一个从 0 开始到最大地址的空间,这个地址空间是独立的,是该程序私有的,其它程序既看不到,也不能访问该地址空间,这个地址空间和其它程序无关,和具体的计算机也无关。事实上,计算机科学家们早就这么做了,这个方案就是虚拟地址。
虚拟地址
正如其名,这个地址是虚拟的,自然而然地和具体环境进行了解耦,这个环境包括系统软件环境和硬件环境。
事实上,所有的应用程序开始的部分都是这样的。这正是因为每个应用程序的虚拟地址空间都是相同且独立的。那么这个地址是由谁产生的呢?答案是链接器,其实我们开发软件经过编译步骤后,就需要链接成可执行文件才可以运行,而链接器的主要工作就是把多个代码模块组装在一起,并解决模块之间的引用,即处理程序代码间的地址引用,形成程序运行的静态内存空间视图。只不过这个地址是虚拟而统一的,而根据操作系统的不同,这个虚拟地址空间的定义也许不同,应用软件开发人员无需关心,由开发工具链给自动处理了。由于这虚拟地址是独立且统一的,所以各个公司开发的各个应用完全不用担心自己的内存空间被占用和改写。
物理地址
虽然虚拟地址解决了很多问题,但是虚拟地址只是逻辑上存在的地址,无法作用于硬件电路的,程序装进内存中想要执行,就需要和内存打交道,从内存中取得指令和数据。而内存只认一种地址,那就是物理地址。
什么是物理地址呢?物理地址在逻辑上也是一个数据,只不过这个数据会被地址译码器等电子器件变成电子信号,放在地址总线上,地址总线电子信号的各种组合就可以选择到内存的储存单元了。
但是地址总线上的信号(即物理地址),也可以选择到别的设备中的储存单元,如显卡中的显存、I/O 设备中的寄存器、网卡上的网络帧缓存器。不过如果不做特别说明,我们说的物理地址就是指选择内存单元的地址。
虚拟地址到物理地址的转换
明白了虚拟地址和物理地址之后,我们发现虚拟地址必须转换成物理地址,这样程序才能正常执行。要转换就必须要转换机构,它相当于一个函数:p=f(v),输入虚拟地址 v,输出物理地址 p。
那么要怎么实现这个函数呢?用软件方式实现太低效,用硬件实现没有灵活性,最终就用了软硬件结合的方式实现,它就是 MMU(内存管理单元)。MMU 可以接受软件给出的地址对应关系数据,进行地址转换。
MMU一个工具,我们通过mmu去读取地址关系转化表,再根据虚拟地址空间地址找到物理地址所在区域,可以看图:
在这里插入图片描述
下面我们不妨想一想地址关系转换表的实现. 如果在地址关系转换表中,这样来存放:一个虚拟地址对应一个物理地址。那么问题来了,32 位地址空间下,4GB 虚拟地址的地址关系转换表就会把整个 32 位物理地址空间用完,这显然不行。
系统设计者最后采用一个这样的方案,即把虚拟地址空间和物理地址空间都分成同等大小的块,也称为页,按照虚拟页和物理页进行转换。根据软件配置不同,这个页的大小可以设置为 4KB、2MB、4MB、1GB,这样就进入了现代内存管理模式——分页模型。于是mmu的功能就是这样的了:
在这里插入图片描述
结合图片可以看出,一个虚拟页可以对应到一个物理页,由于页大小一经配置就是固定的,所以在地址关系转换表中,只要存放虚拟页地址对应的物理页地址就行了。
MMU 页表
现在我们开始研究地址关系转换表,其实它有个更加专业的名字——页表。它描述了虚拟地址到物理地址的转换关系,也可以说是虚拟页到物理页的映射关系,所以称为页表。为了增加灵活性和节约物理内存空间(因为页表是放在物理内存中的),所以页表中并不存放虚拟地址和物理地址的对应关系,只存放物理页面的地址,MMU 以虚拟地址为索引去查表返回物理页面地址,而且页表是分级的,总体分为三个部分:一个顶级页目录,多个中级页目录,最后才是页表。.
在这里插入图片描述
上图中 CR3 就是 CPU 的一个的寄存器,MMU 就是根据这个寄存器找到页目录的。所以,每个进程都有一个页表基地址,我们每次切换进程都会把当前cpu寄存器的值入栈,这叫环境保护,等cpu再次切换回来的时候出栈,恢复cpu寄存器大值,这叫环境恢复。

七、页表

页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。 每一个进程都拥有一个自己的页表,PCB表中有指针指向页表。
CPU中有一个页表寄存器,里面存放着当前进程页表的起始地址和页表长度。将上述计算的页表号和页表长度进行对比,确认在页表范围内,然后将页表号和页表项长度相乘,得到目标页相对于页表基地址的偏移量,最后加上页表基地址偏移量就可以访问到相对应的框了,CPU拿到框的起始地址之后,再把页内偏移地址加上,访问到最终的目标地址。每个进程都有页表,页表起始地址和页表长度的信息在进程不被CPU执行的时候,存放在其栈内。

在这里插入图片描述
CPU 并不是直接访问物理内存地址,而是通过虚拟地址空间来间接的访问物理内存地址。虚拟地址空间是操作系统为每个正在执行的进程分配一个逻辑地址,比如在 32 位系统,范围 0-4G-1 。操作系统通过将虚拟地址空间和物理内存地址之间建立映射关系,让 CPU 能够间接访问物理内存地此一般情况将虚拟地址空间以 512byte-8K, 作为一个单位,称为页,并从 0 开始依次对它进行页编号。这个大小就称为页面。将物理地址按照同样大小,作为一个单位,称为框或者是块。也从。开始依次进行对每个框编号。 OS 通过维护一张表,这张表记录每一对页和框的映射关系。 系统为每个进程建立一个页表,在进程逻辑地址空间中每一页,依次在页表中有一个表项,记录该页对应的物理块号.通过直找页表就可以很容易地找到该页在内存中的位置。页表具有逻辑地址到物理地址映射作用。 大多系统页面大小为 4KB 。

ARM64处理器把页表称为转换表,最多4级。ARM64处理器支持3种页长度,4KB、16KB和64KB。 页长度和虚拟地址的宽度决定了转换表的级数。
一般情况下,linux内核把页表直接分为4级:页全局目录(PGD)、页上层目录(PUD)、页中间目录(PMD)、直接页表
(PT)。
如果选择三级(页全局目录(PGD)、页中间目录(PMD)、直接页表(PT))。
如果选择二级(页全局目录(PGD)和有接页表(PT))。

实际上,linux还存在五级页表的结构,每个进程有独立的页表,进程的mm_struct实例成员pgd指向页全局目录.前面四级页表的表项存放下一级页表的起始地址,直接页表的表项存放页帧号(PFN).

在这里插入图片描述

五级目录结构的查询页表,把虚拟地址转换成物理地址流程:
1 、根据页全局目录的起始地址和页全局目录索引得到页全局目录表项的地址,然后再从表项得到页四级目录的起始地址;
2 、根据页四级目录的起始地址和页四级目录索引得到页四级目录表项的地址,然后从表项得到页上层目录的起始地址;
3 、根据页上层目录的起始地址和页上层目录索引得到页上层目录表项的地址,然后从表项得到页中间目录的起始地址;
4 、根据页中间目录的起始地址和页中间目录索引得到页中间目录表项的地址,然后从表项得到直接页表的起始地址;
5 、根据直接页表的起始地址和直接页表索引得到页表项的地址,然后从表项得到页帧号;
6 、把页帧号和页内偏移组合形成物理地址。

因为大部分linux系统都是使用四级目录结构,我们用四级页表举个例子,一般64位系统都是使用48位虚拟地址的,页长度和转换表级数关系是这样子的:
在这里插入图片描述
每级转换表占用一页,有512项,索引是48位虚拟地址的9个位,最后的偏移地址4K大小。

八、内存映射原理分析

  • List item

创建内存映射的时候,在进程的用户虚拟地址空间中分配一个虚拟内存区域,分为两种:

  1. 文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间,数据源是存储设备上的文件。
  2. 匿名映射:没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间,没有数据源。

内存映射的原理如下:

  1. 创建内存映射的时候,在进程的用户虚拟地址空间中分配一个虚拟内存区域。
  2. Linux 内核采用延迟分配物理内存的策略,在进程第一次访问虚拟页的时候,产生缺页异常。
  3. 如果是文件映射,那么分配物理页,把文件指定区间的数据读到物理页中,然后在页表中把虚拟页映射到物理页;
  4. 如果是匿名映射,那么分配物理页,然后在页表中把虚拟页映射到物理页。

其实 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 位,也就是用前 10 位定位到页目录表中的一项。将这一项对应的页表取出来共 1k 项,再用中间 10 位定位到页表中的一项,将这一项对应的存放数据的页取出来,再用最后 12 位定位到页中的具体位置访问数据。如下图所示:
在这里插入图片描述
你可能会问,如果这样的话,映射 4GB 地址空间就需要 4MB+4KB 的内存,这样不是更大了吗? 当然如果页是满的,当时是更大了,但是,我们往往不会为一个进程分配那么多内存。
比如说,上面图中,我们假设只给这个进程分配了一个数据页。如果只使用页表,也需要完整的 1M 个页表项共 4M 的内存,但是如果使用了页目录,页目录需要 1K 个全部分配,占用内存 4K,但是里面只有一项使用了。到了页表项,只需要分配能够管理那个数据页的页表项页就可以了,也就是说,最多 4K,这样内存就节省多了。
当然对于 64 位的系统,两级肯定不够了,就变成了四级目录,分别是全局页目录项 PGD(Page Global Directory)、上层页目录项 PUD(Page Upper Directory)、中间页目录项 PMD(Page Middle Directory)和页表项 PTE(Page Table Entry)。
在这里插入图片描述

Logo

开源、云原生的融合云平台

更多推荐