linux内存原理简介

本文详细介绍了Linux内存管理的模型,包括UMA和NUMA,以及Linux内核如何管理内存,如页分配器、块分配器和内存描述符mm_struct。还阐述了地址空间布局、TLB的工作原理和页表结构。最后讨论了ARM64架构的内存管理特性,如虚拟地址空间布局和ASID在进程切换中的作用。
摘要由CSDN通过智能技术生成

这里简单介绍一下linux中内存模型和基本源码实现。

内存模型

共享存储型多处理机有两种模型:一致性内存访问(Uniform-Memory-Access,简称UMA)模型和非一致性内存访问(Nonuniform-Memory-Access,简称NUMA)模型。

UMA模型

每个cpu访问内存中的任何地址所需要的时间是相等的。此类结构也叫作对称多处理器架构(SMP),多个cpu对称工作,无主次或从属关系。各个cpu共享相同的物理内存,每个cpu访问任意内存时间相同,因此这个结构也是一致性内存访问。
扩展UMA的主要方法有增加内存、提升cpu工作频率、添加cpu、改善io性能等。由于UMA模型中架构所需的资源(io、内存等)都是共享的,这就导致扩展能力非常有限,最受限的是内存。由于每一个cpu都需要通过相同的内存总线来访问相同的内存资源,所以随着cpu数量的增加,内存访问冲突会迅速增加,最终可能造成cpu资源的浪费,使cpu性能的有效性大大降低。事实证明,SMP架构在cpu核心数为2或4时能够发挥最大性能。
在这里插入图片描述

NUMA模型

NUMA是一种分布式存储器访问,处理器可以访问不同的存储地址,大幅提升并行性。每个cpu都有本地内存,可以支持特别快速的访问,各个cpu之间通过总线相连,以支持对不同cpu的本地内存访问,但是比访问自己的本地内存慢。
NUMA模型可以解决SMP模型的扩展问题,一个物理服务器可以支持上百个cpu,比较典型NUMA服务器有SUN15K、IBMp690等。但是这个架构也有一定的局限性,访问远程内存的频率远远超过本地内存时,当cpu数量增加,系统性能无法迅速增加。为了更好地发挥性能,开发时应尽量减少不同cpu间的信息交互。
在这里插入图片描述

linux内存结构

linux内核内存管理子系统架构如下图所示,分为用户空间、内核空间和硬件层3个层面。
在这里插入图片描述

用户空间

应用程序使用malloc()申请内存,使用free()释放内存,malloc()/free()是glibc库的内存分配器ptmalloc提供的接口,ptmalloc使用系统调用brk/mmap向内核以页为单位申请内存,然后划分成小内存块分配给用户应用程序。用户空间的内存分配器,除glibc库的ptmalloc,google的tcmalloc/FreeBSD的jemalloc。

内核空间

内核空间的基本功能:虚拟内存管理负责从进程的虚拟地址空间分配虚拟页,sys_brk用来扩大或收缩堆,sys_mmap用来在内存映射区域分配虚拟页,sys_munmap用来释放虚拟页。
内核使用延迟分配物理内存策略,进程第一次访问物理页时,触发页错误异常。页错误异常处理程序向页分配器申请物理页在进程页表中会把虚拟页映射到物理页。
页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。内核空间提供把页划分成小内存块分配的块分配器,提供分配内存的接口kmalloc()和释放内存接口kfree()。块分配器:SLAB/SLUB/SLOB。
内核空间的扩展功能:不连续页分配器提供了分配内存的接口vmalloc和释放内存接口vfree,在内存碎片化时,申请连续物理页的成功率很低,可申请不连续的物理页,映射到连续的虚拟页,即虚拟地址连续页物理地址不连续。
连续内存分配器(contiguous memory allocator,CMA)用来给驱动程序预留一段连续的内存,当驱动程序不用的时候,可以给进程使用;当驱动程序需要使用的时候,把进程占用的内存通过回收或迁移的方式让出来,给驱动程序使用。
在内核初始化过程中,页分配器还没有准备好,需要使用临时引导内存分配器来分配内存。
可以通过内存控制组来控制进程占用的资源。当内存碎片化时,找不到连续的物理页,那么就进行内存碎片整理,通过迁移的方式得到连续的物理页;在内存不足时,页回收器负责回收物理页:对没有后备存储设备支持的物理页,把数据提出到交换机,然后释放物理页;对于有后备存储设备支持的物理页,把数据写回存储设备,然后释放物理页。如果页回收失败,就kill进程或通过内存耗尽杀手将内存杀死。

硬件层面

处理器包含一个称为内存管理单元(Memory Management Unit,MMU)的部件,负责把虚拟地址转换成物理地址。内存管理单元包含一个称为页表缓存(Translation Lookaside Buffer,TLB,注意是在cpu上)的部件,保存最近使用的页表映射,避免每次把虚拟地址转换物理地址都需要查询内存中的页表。

虚拟地址空间布局

虚拟地址空间划分

介绍一下64位系统,以ARM64处理器为例。目前ARM64处理器不完全支持64位虚拟地址,因为虚拟地址宽度最大48位,现在进程用不到64位空间。
在这里插入图片描述
内核虚拟地址在64位地址空间顶部,高16位全是1,范围是[0xFFFF 0000 0000 0000,0xFFFF FFFF FFFF FFFF]。用户虚拟地址在64位地址 空间的底部,高16位全是0,范围是[0x0000 00000000 0000,0x0000 FFFF FFFF FFFF]。
高16位全是1或是0的地址称为规范地址,两者之间称为不规范地址,是不允许使用的。当然也有例外,LVA,叫大虚拟内存,页长度64KB,虚拟地址宽度52位,但目前很少使用。
在编译ARM64架构的Linux内核时,可以选择虚拟地址宽度:

  1. 选择页长度4KB,默认虚拟地址宽度为39位;
  2. 选择页长度16KB,默认虚拟地址宽度为47位;
  3. 选择页长度64KB,默认虚拟地址宽度为42位;
  4. 选择48位虚拟地址。

在ARM64架构linux内核中,内核虚拟地址和用户虚拟地址宽度相同。所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址 空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。

用户虚拟地址空间布局

进程的用户虚拟地址空间的起始地址是0,长度是TASK_SIZE,由每种处理器架构定义自己的宏TASK_SIZE。ARM64架构定义宏TASK_SIZE如下所示:

  • 32位用户空间程序:TASK_SIZE的值是TASK_SIZE_32,即0x10000000,等于4GB。
  • 64位用户空间程序:TASK_SIZE的值是TASK_SIZE_64,即2的VA_BITS次方字节,VA_BITS是编译内核时选择的虚拟地址位数。看下代码
/*
 * PAGE_OFFSET - the virtual address of the start of the linear map (top
 *		 (VA_BITS - 1))
 * KIMAGE_VADDR - the virtual address of the start of the kernel image
 * VA_BITS - the maximum number of bits for virtual addresses.
 * VA_START - the first kernel virtual address.
 * TASK_SIZE - the maximum size of a user space task.
 * TASK_UNMAPPED_BASE - the lower boundary of the mmap VM area.
 */
#define VA_BITS			(CONFIG_ARM64_VA_BITS)
#define VA_START		(UL(0xffffffffffffffff) << VA_BITS)
#define PAGE_OFFSET		(UL(0xffffffffffffffff) << (VA_BITS - 1))
#define KIMAGE_VADDR		(MODULES_END)
#define MODULES_END		(MODULES_VADDR + MODULES_VSIZE)
#define MODULES_VADDR		(VA_START + KASAN_SHADOW_SIZE)
#define MODULES_VSIZE		(SZ_128M)
#define VMEMMAP_START		(PAGE_OFFSET - VMEMMAP_SIZE)
#define PCI_IO_END		(VMEMMAP_START - SZ_2M)
#define PCI_IO_START		(PCI_IO_END - PCI_IO_SIZE)
#define FIXADDR_TOP		(PCI_IO_START - SZ_2M)
#define TASK_SIZE_64		(UL(1) << VA_BITS)

#ifdef CONFIG_COMPAT
#define TASK_SIZE_32		UL(0x100000000)

Linux内核使用内存描述符mm_struct描述进程的用户虚拟地址空间,看源码

//内存描述符结构体类型
struct mm_struct {
	struct vm_area_struct *mmap;		// 虚拟内存区域链表 
	struct rb_root mm_rb;       //虚拟内存区域红黑树
	u32 vmacache_seqnum;                   /* 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;  //内存描述符的引用计数

	atomic_long_t nr_ptes;			/* PTE page table pages */
#if CONFIG_PGTABLE_LEVELS > 2
	atomic_long_t nr_pmds;			/* PMD page table pages */
#endif
	int map_count;				/* number of VMAs */

	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;	/* High-watermark of RSS usage */
	unsigned long hiwater_vm;	/* High-water virtual memory usage */

	unsigned long total_vm;		/* Total pages mapped */
	unsigned long locked_vm;	/* Pages that have PG_mlocked set */
	unsigned long pinned_vm;	/* Refcount permanently increased */
	unsigned long data_vm;		/* VM_WRITE & ~VM_SHARED & ~VM_STACK */
	unsigned long exec_vm;		/* VM_EXEC & ~VM_WRITE & ~VM_STACK */
	unsigned long stack_vm;		/* VM_STACK */
	unsigned long def_flags;
    // 代码段/数据段起始地址和结束地址
	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;

	cpumask_var_t cpu_vm_mask_var;

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

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

	struct core_state *core_state; /* coredumping support */
#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_CPUMASK_OFFSTACK
	struct cpumask cpumask_allocation;
#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
#if defined(CONFIG_NUMA_BALANCING) || defined(CONFIG_COMPACTION)
	/*
	 * 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.
	 */
	bool tlb_flush_pending;
#endif
	struct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGE
	atomic_long_t hugetlb_usage;
#endif
	struct work_struct async_put_work;
};

全贴下来了,加了些注释,还是需要自己看。

进程的进程描述和内存描述符关系

在这里插入图片描述
进程描述符有两个成员

struct mm_struct *mm; // 进程的mm指向一个内存描述符,内核线程没有用户虚1拟地址空间,所以mm是空指针。
struct mm_struct *active_mm; // 进程的active_mm和mm总是指向一个内存描述符,内核线程的active_mm在没有运行时是空指针,在运行时指向上一个进程借用的内存描述符

举个例子,有两个进程属于同一线程组,看图
在这里插入图片描述

内核地址空间布局

还是以ARM64处理器架构为例,其内核地址空间布局如下:
在这里插入图片描述

  • 线性映射区域:是内核虚拟地址空间的一半。区域中虚拟地址和物理地址是线性关系。
  • vmemmap:稀疏内存page结构体数组的地址空间。稀疏内存以后有机会再介绍。
  • PCI I/O区域:外围相关互联设备组件总线的标准,PCI设备IO地址。
  • 固定映射区域:在内核初始化时就映射到物理地址的区域。
  • vmalloc区域:内核调用vmalloc函数,分配的虚拟地址就在这个区域,分配的虚拟地址连续,但是物理地址不连续。
  • 内核镜像:在malloc区域内,其虚拟地址的基准值等于内核模块区域的结束地址。
  • KASAN影子区域:起始地址是整个内核虚拟地址空间的起始地址,长度是虚拟地址空间的八分之一。KASAN是内核消毒剂,是个动态的内存错误检测工具,为释放内存后仍访问内存或越界访问内存这两类缺陷提供快速、综合的解决方案。

TLB工作原理

处理器的内存管理单元(Memory Management Unit,MMU)负责把虚拟地址转换成物理地址,为了改进虚拟地址到物理地址的转换速度,避免每次转换都需要查找内存中的页表,处理器厂商在内存管理单元里面增加一个称为TLB(Translation Lookaside Buffer,TLB)的高速缓存,TLB直接为转换后备缓冲区,意译为页表缓存。

TLB表项格式

不同处理器架构的TLB表项的格式不同,ARM64处理器的每条TLB表项不仅包含虚拟地址和物理地址,也包含属性:内存类型、缓存策略、访问权限、地址空间标识符(ASID)和虚拟机标识符(VMID)。

TLB管理

如果内核修改了可能缓存在TLB里面的页表项,那么内核必须负责使旧的TLB表项失效,内核定义每种处理器架构必须实现的函数如下:

// 使所有TLB表项失效
static inline void flush_tlb_all(void) 
{
    dsb(ishst);
    __tlbi(vmalle1is);
    dsb(ish);
    isb();
}

// 使指定用户地址空间的所有TLB表项失效,参数mm是进程的内存描述符
static inline void flush_tlb_mm(struct mm_struct *mm) 
{
unsigned long asid = ASID(mm) << 48;
    dsb(ishst);
    __tlbi(aside1is, asid);
    dsb(ish);
}

/* 使指定用户地址空间的某个范围的TLB表项失效,参数vma是虚拟内存区域,start起始地址,end结束地址 */
static inline void flush_tlb_range(struct vm_area_struct *vma, unsigned long start, unsigned long end)
{
    __flush_tlb_range(vma, start, end, false);
}

// 使指定用户地址空间里面的指定虚拟页的TLB表项失效,
// 参数vma是虚拟内存区域,uaddr是一个虚拟页中的任意虚拟地址 
static inline void flush_tlb_page(struct vm_area_struct *vma, unsigned long uaddr)
{
    unsigned long addr = uaddr >> 12 | (ASID(vma->vm_mm) << 48);
    dsb(ishst);
    __tlbi(vale1is, addr);
    dsb(ish);
}

// 使内核的某个虚拟地址范围的TLB表项失效,参数start是起始地址,end结束地址 
static inline void flush_tlb_kernel_range(unsigned long start, unsigned long end)
{
    unsigned long addr;
    if ((end - start) > MAX_TLB_RANGE) {
        flush_tlb_all();
        return;
    }
    start >>= 12;
    end >>= 12;
    dsb(ishst);
    for (addr = start; addr < end; addr += 1 << (PAGE_SHIFT - 12))
        __tlbi(vaae1is, addr);
    dsb(ish);
    isb();
}

ARM64架构TLB失效指令

TLBI <type><level>[IS]{,<xt>}
解释一下各参数

  • type:
    ALL(所有表项)
    VMALL(当前虚拟机的阶段1的所有表项,即表项的VMID是当前虚拟机的VMID)。虚拟机里面运行的客户操作系统的虚拟地址 转换成物理地址分两个阶段:1把虚拟地址转换成中间物理地址;2把中间物理地址 转换成物理地址。
  • level(指定异常级别):
    E1:异常级别1
    E2:异常级别2
    E3:异常级别3
  • IS表示内存共享(inner Shareable),多个核共享。如果不使用字段IS,表示非共享,只被一个核使用。在SMP系统中,如果指令TLBI不携带字段IS,仅仅使当前核的TLB表项失效;如果指令TLBI携带字段IS,表示使所有核的TLB表项失效。
  • Xt:选项Xt是X0-X31中的任何一个寄存器。

flush_tlb_all用来使所有核的所有TLB失效,内核代码如下

static inline void flush_tlb_all(void) 
{
    dsb(ishst);
    __tlbi(vmalle1is);
    dsb(ish);
    isb();
}

说个小实话,这几行代码,刚才贴过……这里再贴一遍,为了解释方便。

  • dsb ishst:确保屏障前面的存储指令执行完毕,dsb是数据同步屏障,ishst中ish表示共享域是内部共享,st表示存储 ,ishst表示数据同步屏障指令对所有核的存储指令起作用。
  • tlbi vmalle1is:使用所有核上匹配当前VMID、阶段1和异常级别1的所有TLB表项失效。
  • dsb ish:确保当前的TLB失效指令执行完毕,ish表示数据同步屏障指令对所有核起作用。
  • isb:isb是指令同步屏障,这条指令冲刷处理器流水线,重新读取屏障指令后面的所有指令。

地址空间标识符

为了减少在进程切换时清空页表缓存的需要,ARM64处理器的页表缓存使用非全局位区分内核和进程的页表项,使用地址空间标识符(Address Space Identifier,ASID)区分不同进程的页表项。

虚拟机标识符

虚拟机里面运行的客户OS的虚拟地址转换成物理地址分为两个阶段:

  1. 把虚拟地址转换成中间物理地址(由客户操作系统的内核控制,和非虚拟化的转换过程相同);
  2. 把中间物理地址转换成物理地址(由虚拟机监控器控制,虚拟机监控器为每个虚拟机维护一个转换表,分配一个虚拟机标识符VMID(Virutal machineidentifier))。

每个虚拟机有独立的ASID空间,页表缓存使用虚拟标识符区别不同虚拟机转换表项,可以避免每次虚拟机切换都要清空页表缓存,只需要在虚拟机标识符回绕时把处理器的页表缓存清空。
这里介绍一下ASID。ASID(地址空间标识符)长度为8位,也可16位,在SMP系统中,ARM64架构要求ASID在处理器所有核上是唯一的,而ASID值只有256个(8位),进程数量可能超过256个,那么两个进程的ASID可能性相同。为了解决这个问题,内核引入了ASID版本号。
每个进程有一个64位的软件ASID,低8位存放硬件ASID,高56位存放ASID版本号。64位全局变量asid_generation的高56位保存全局ASID版本号。当进程被调度时,比较进程的ASID版本号和全局ASID版本号。如果版本号相同,那么直接使用上次分配的硬件ASID,否则需要给进程重新分配硬件ASID。
如果存在空闲的硬件ASID,那么选择一个分配给进程。如果没有空闲的硬件ASID,那么把全局ASID版本号加1,重新从1开始分配硬件ASID,即硬件ASID从255回绕到1。因为刚分配的硬件ASID可能和某个进程的硬件ASID相同,只是ASID版本号不同,页表缓存可能包含了这个进程的页表项,所以必须把所有处理器的页表缓存清空。

页表

层次化的页表用于支持对大地址空间的快速、高效的管理。页表用于建立用户进程的虚拟地址空间和系统物理内存(内存、页帧)之间的关联。页表用来把虚拟页映射到物理页,并且存放页的保护位,即访问权限。Linux内核把页表分为4级:PGD、PUD、PMD、PT。

  1. PGD(Page Global Directory)–>页全局目录
  2. PUD(Page Upper Directory)–>页上层目录
  3. PMD(Page Middle Directory)–>页中间目录
  4. PT(Page Table)–>直接页表

linux在4.11以后版本把页表扩展到五级,在页全局目录和页上层目录之间增加了页四级目
录(Page 4th Directory,P4D)。页表结构:

  • 选择四级页表:页全局目录、页上层目录、页中间目录、直接页表;
  • 选择三级页表:页全局目录、页中间目录、直接页表;
  • 选择二级页表:页全局目录、直接页表;

处理器架构怎么选择多少级?在内核配置宏CONFIG_PGTABLE_LEVELS配置页表级数,案例分析五级页表结构如下:
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值