一、UMA/NUMA内存结构
共享存储型多处理机有两种模型:
均匀存储器存取(Uniform-Memory-Access,简称UMA)模型
非均匀存储器存取(Nonuniform-Memory-Access,简称NUMA)模型
比较典型NUMA服务器:SUN15K、IBMp690等
二、mm_struct结构体
Linux内核内存管理子系统架构如下图所示,分为用户空间、内核空间和硬件层3个 层面
用户空间:malloc/free-- >ptmalloc(glibc)/jemalloc(FreeBSD)/tcmalloc(Google)
内核空间:sys_brk、sys_mmap、sys_munmap等等
1、用户空间
应用程序使用malloc()申请内存,使用free()释放内存,malloc()/free()是glibc库 的内存分配器ptmalloc提供的接口,ptmalloc使用系统调用brk/mmap向内核以页 为单位申请内存,然后划分成小内存块分配给用户应用程序。用户空间的内存分配 器,除glibc库的ptmalloc,google的tcmalloc/FreeBSD的jemalloc。
2、内核空间
内核空间的基本功能:虚拟内存管理负责从进程的虚拟地址空间分配虚拟页, sys_brk用来扩大或收缩堆,sys_mmap用来在内存映射区域分配虚拟页, sys_munmap用来释放虚拟页。
页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。内核空间提供把页划分成小内存块分配的块分配器,提供分配内存的接口kmalloc()和释放内存接口 kfree()。块分配器:SLAB/SLUB/SLOB。
内核空间的扩展功能:不连续页分配器提供了分配内存的接口vmalloc和释放内存接口vfree,在内存碎片化时,申请连续物理页的成功率很低,可申请不连续的物理页,映射到连续的虚拟页,即虚拟地址连续页物理地址不连续。
连续内存分配器(contiguous memory allocator,CMA)用来给驱动程序预留 一段连续的内存,当驱动程序不用的时候,可以给进程使用;当驱动程序需要使用的时候,把进程占用的内存通过回收或迁移的方式让出来,给驱动程序使用。
3、硬件层面
处理器包含一个称为内存管理单元(Memory Management Unit,MMU)的部件,负责把虚拟地址转换成物理地址。内存管理单元包含一个称为页表缓存 (Translation Lookaside Buffer,TLB)的部件,保存最近使用的页表映射,避免每次把虚拟地址转换物理地址都需要查询内存中的页表。
4、虚拟地址空间布局
a、虚拟地址空间划分
以ARM64处理器为例:虚拟地址 的最大宽度是48位。内核虚拟地址在64位地址空 间顶部,高16位全是1,范围是[0xFFFF 0000 0000 0000,0xFFFF FFFF FFFF FFFF] 。用户虚拟地址在64位地址 空间的底部,高16位全是0,范围是[0x0000 0000 0000 0000,0x0000 FFFF FFFF FFFF]。
在编译ARM64架构的Linux内核时,可以选择虚拟地址宽度:
在ARM64架构linux内核中,内核虚拟地址用户虚拟地址宽度相同。所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。
b、用户虚拟地址空间布局
进程的用户虚拟地址空间的起始地址是0,长度是TASK_SIZE,由每种处理器架构定义自己的宏TASK_SIZE。ARM64架构定义宏TASK_SIZE如下所示:
Linux内核使用内存描述符mm_struct描述进程的用户虚拟地址空间,主要核心成员如下:
// 内存描述符结构体类型(今后要用的主要成员)
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs 虚拟内存区链表*/
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; /* 内存映射区域起始地 base of mmap area */
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; /* 用户虚拟地址空间的长度 size of task vm space */
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;
};
c、进程的进程描述和内存描述符关系如下图所示:
进程描述符的成员:
struct mm_struct *mm; // 进程的mm指向一个内存描述符,内核线程没有用户虚拟地址空间,所以mm是空指针。
struct mm_struct *active_mm; // 进程的active_mm和mm总是指向一个内存描述符,内核线程的active_mm在没有运行时是空指针,在运行时指向上一个进程借用的内存描述符
c、内核地址空间布局
ARM64处理器架构的内核地址空间布局如下:
三、TLB工作原理
处理器的内存管理单元(Memory Management Unit,MMU)负责把虚拟地址转换成物理地址,为了改进虚拟地址到物理地址的转换速度,避免每次转换都需要查找内存中的页表,处理器厂商在内存管理单元里面增加一个称为TLB(Translation Lookaside Buffer,TLB)的高速缓存,TLB直接为转换后备缓冲区,意译为页表缓存。
1、TLB表项格式
不同处理器架构的TLB表项的格式不同,ARM64处理器的每条TLB表项不仅包含虚 拟地址和物理地址,也包含属性:内存类型、缓存策略、访问权限、地址空间标识符(ASID)和虚拟机标识符(VMID)。
2、TLB管理
如果内核修改了可能缓存在TLB里面的页表项,那么内核必须负责使旧的TLB表项失效,内核定义每种处理器架构必须实现的函数如下:
static inline void flush_tlb_all(void) // 使所有的TLB表项失效
{
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();
}
3、ARM64架构TLB失效指令:TLB<type><level>[IS]{,<xt>}
type:
level(指定异常级别):
IS表示内存共享(inner Shareable),多个核共享。如果不使用字段IS,表示非共享,只被一个核使用。在SMP系统中,如果指令TLBI不携带字段IS,仅仅使当前核的TLB表项失效;如果指令TLBI携带字段IS,表示使所有核的TLB表项失效。
选项Xt是X0-X31中的任何一个寄存器。
flush_tlb_all用来使所有核的所有TLB失效,内核代码如下:
static inline void flush_tlb_all(void) // 使所有的TLB表项失效
{
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是指令同步屏障,这条指令冲刷处理器流水线,重新读取屏障指令后面的所有指令。
4、地址空间标识符
为了减少在进程切换时清空页表缓存的需要,ARM64处理器的页表缓存使用非全局位区分内核和进程的页表项,使用地址空间标识符(Address Space Identifier,ASID)区分不同进程的页表项。
5、虚拟机标识符
虚拟机里面运行的客户OS的虚拟地址转换成物理地址分为两个阶段:
每个虚拟机有独立的ASID空间,页表缓存使用虚拟标识符区别不同虚拟机转换表项,可以避免每次虚拟机切换都要清空页表缓存,只需要在虚拟机标识符回绕时把处理器的页表缓存清空。
四、页表
层次化的页表用于支持对大地址空间的快速、高效的管理。页表用于建立用户进程的虚拟地址空间和系统物理内存(内存、页帧)之间的关联。页表用来把虚拟页映射到物理 页,并且存放页的保护位,即访问权限。Linux内核把页表分为4级:
PGD、PUD、PMD、PT。
4.11以后版本把页表扩展到五级,在页全局目录和页上层目录之间增加了页四级目录(Page 4th Directory,P4D)
选择四级页表:页全局目录、页上层目录、页中间目录、直接页表;
选择三级页表:页全局目录、页中间目录、直接页表;
选择二级页表:页全局目录、直接页表;
处理器架构怎么选择多少级?在内核配置宏CONFIG_PGTABLE_LEVELS配置页表级数,