• 不连续页分配器 & 每处理器内存分配器 & 页表


    主要参考了《深入linux内核》和《Linux内核深度解析》,另外简单浅析了一下相关内容

    不连续页分配器

    当设备长时间运行后,内存碎片化,很难找到连续的物理页。在这种情况下,如果需要分配长度超过一页的内存块,可以使用不连续页分配器,分配虚拟地址连续但是物理地址不连续的内存块。

    在32位系统中,不连续页分配器还有一个好处:优先从高端内存区域分配页,保留稀缺的低端内存区域。

    vmalloc的优点:
    vmalloc的大块内存(大于等于1页)分配成功率一般高于kmalloc以及高阶buddy。避免外部碎片的问题,在机器长时间运行后,外部碎片严重,可能出现无法通过buddy或者kmalloc等分配大块连续的物理内存,这个时候,vmalloc可以讲单页的物理内存组织起来,映射到连续的虚拟地址空间,这个过程对用户是透明的。对用户而言,他们得到了地址连续的内存块。

    vmalloc的缺点:
    vmalloc分配内存的效率低于kmalloc并且如果盲用vmalloc还可能产生内部碎片的问题,比如使用vmalloc只申请1byte,但是vmalloc却会分配2*PAGE_SIZE(默认:VM_NO_GUARD若是没有置位,将需要对分配PAGE_SIZE用于隔离)的连续虚拟内存以及1*PAGE_SIZE的物理内存。
    vmalloc分配内存的过程需要查找合适的虚拟内存空间,然后再分配物理内存,再建立映射关系,这些逻辑相当于在内核线性地址空间的kmalloc来说,显得较为繁琐。

    系统接口

    不连续页分配器所提供接口如下:

    • void *vmalloc(unsigned long size);
      • 分配不连续的物理页并且把物理页映射到连续的虚拟地址空间;
    • void vfree (const void * addr);
      释放vmalloc分配的物理页和虚拟地址空间;
    • void *vmap(struct type **pages,unsigned int count,unsigned long flags,pgprot_t prot);
      • 把已经分配的不连续物理而映射到连续的虚拟地址空间;
    • void vunmap(const void *addr);
      释放使用vmap分配的虚拟地址空间。

    内核还提供接口:

    • void *kvmalloc(size_t size,gfp_t flags);
      首先尝试使用kmalloc分配内存块,如果失败,那么使用vmalloc函数分配不连续的物理页;
    • void kvfree (const void * addr);
      如果内存块是是使用vmalloc分配的,那么使用vfree释放,否则使用kfree释放。

    Linux 中常用内存分配函数

    用户空间

    • malloc/calloc/realloc/free。不保证物理连续。大小限制(堆申请)。单位为字节。
      场景: calloc初始化为0,realloc改变内存大小。

    • mmap/munmap。场景:将文件利用虚拟内存技术映射到内存当中。

    • brk/sbrk。场景:虚拟内存到内存的映射。

    内核空间

    • vmalloc/vfree。虚拟连续/物理不连续。大小限制(vmalloc区)单位为页(vmalloc区域)。场景:可能睡眠,不能从中断上下文中调用,或其他不允许阻塞情况下调用。

    • slab(kmalloc/kcalloc/krealloc/kfree)。物理连续。大小限制(64b-4mb)。单位为2^order字节(Normal区域)。场景:大小有限,不如vmalloc/malloc大。

      还有一个叫做kmem_cache_create(物理连续。64-4mb。字节大小需要对齐(Normal区域)。
      场景:便于固定大小数据的频繁分配和释放,分配时从缓存池中获取地址,释放时也不一定真正释放内存,通过slab进行管理)。

    • 伙伴系统

      • _get_free_page/_get_free_pages。物理连续。4mb (1024页),单位为页(Normal区域)。场景:get_free_pages,但是限定不能使用HIGHMEM)。
      • alloc_page/alloc_pages/free_pages,物理连续。4mb,单位为页(Normal/Vmalloc都可以)。场景︰配置定义最大页面数2^11,一次能分配到的最大页面数是1024,

    内核源码数据结构

    在Linux中,struct vm_area_struct表示的虚拟地址是给进程使用的,而struct vm_struct表示的虚拟地址是给内核使用的,它们对应的物理页面都可以是不连续的。

    image-20220604005736838

    每个虚拟内存区域对应一个vmap_area实例;

    成员va_start是起始虚拟地址,成员va_end是结束虚拟地址,虚拟内存区域的范围是[va_start,va_end)。
    成员flags是标志位,如果设置了标志位VM_VM_AREA,表示成员vm指向一个vm_struct实例,即vmap_area实例关联一个vm_strut实例。
    成员rb_node是红黑树节点,用来把vmap_area实例加入根节点是vmap_area_root的红黑树中,借助红黑树可以根据虚拟地址快速找到vmap_area实例。
    成员list是链表节点,用来把vmap_area实例加入头节点是vmap_area_list的链表中,这条链表按虚拟地址从小到大排序。

    struct vmap_area {
        // 虚拟内存区域的范围
    	unsigned long va_start; // 起始虚拟地址
    	unsigned long va_end; // 结束虚拟地址
        
    	unsigned long flags; // 标志位,如果被设置为VM_VM_AREA,表示成员vm指向一个vm_struct
        
        // 红黑树节点,用来把vmap_area实例加入到根节点是vmap_area_root的红黑树当中
    	struct rb_node rb_node;
    	struct list_head list;   // 链表存储
    	struct llist_node purge_list;    /* "lazy purge" list */
    	struct vm_struct *vm;
    	struct rcu_head rcu_head;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    每个vmap_area实例关联着一个vm_struct实例;

    成员addr是起始虚拟地址,成员size是长度。
    成员flags是标志位,如果设置了标志位VM_ALLOC,表示虚拟内存区域是使用函数vmalloc分配的。
    成员pages指向page指针数组,成员nr_pages是页数。page指针数组的每个元素指向一个物理页的page实例。
    成员next指向下一个vm_struct实例,成员phys_addr是起始物理地址,这两个成员仅仅在不连续页分配器初始化以前使用。

    struct vm_struct {
    	struct vm_struct	*next; // 指向下一个vm_struct实例
    	void			*addr; // 起始虚拟地址
    	unsigned long		size; // 长度
    	unsigned long		flags; // 标志位
    	struct page		**pages; // page指针数组
    	unsigned int		nr_pages; // 页数
    	phys_addr_t		phys_addr; // 起始物理地址
    	const void		*caller;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如图3.47所示,如果虚拟内存区域是使用函数vmap分配的,vm_struct结构体的差别是:成员flags没有设置标志位VM_ALLOC,成员pages是空指针,成员nr_pages是0。

    image-20220805170050043

    技术原理

    vmalloc虚拟地址空间的范围是(VMALLOC_START,VMALLOC_END),每种处理器架构都需要定义这两个宏,
    比如:ARM64架构定义的宏如下:

    arch\arm64\include\asm\pgtable.h

    #define VMALLOC_START		(MODULES_END)
    #define VMALLOC_END		(PAGE_OFFSET - PUD_SIZE - VMEMMAP_SIZE - SZ_64K)
    
    • 1
    • 2
    • MODULES_END是内核模块区域的结束地址;
    • PAGE_OFFSET是线性映射区域的起始地址;
    • PUD_SIZE是一个页上层目录表项映射的地址空间长度;
    • VMEMMAP_SIZE是vmemmap区域的长度。

    vmalloc虚拟地址空间的起始地址=内核模块区域的结束地址

    vmalloc虚拟地址空间的结束地址=线性映射区域的起始地址 - 一个页上层目录表项映射的地址空间长度- vmemmap区域的长度-64KB

    vmalloc函数执行过程分为三步

    1、分配虚拟内存区域

    1. 分配vm_struct实例和vmap_area实例;
    2. 然后遍历已经存在的vmap_area实例,在两个相邻的虚拟内存区域之间找到一个足够大的空洞,如果找到了,把起始虚拟地址和结束虚拟地址保存在新的vmap_area实例中,然后把新的vmap_area实例加入红黑树和链表;
    3. 最后把新的vmap_area实例关联到vm_struct实例。

    2、分配物理页

    1. vm_struct实例的成员nr_pages存放页数n;
    2. 分配page指针数组,数组的大小是n,vm_struct实例的成员pages指向page指针数组;
    3. 然后连续执行n次如下操作:从页分配器分配一个物理页,把物理页对应的page实例的地址存放在page指针数组中。

    3、在内核的页表中把虚拟页映射到物理页

    • 内核的页表就是0号内核线程的页表。0号内核线程的进程描述符是全局变量init_task,成员active_mm指向全局变量init_mm,init_mm的成员pgd指
      向页全局目录swapper_pg_dir。

    备注:函数vmap and vmalloc区别在于不需要分配物理页。

    每处理器内存分配器

    在多处理器系统中,每处理器变量为每个处理器生成一个变量的副本,每个处理器访问自己的副本,从而避免了处理器之间的互斥和处理器缓存之间的同步,提高了程序的执行速度。

    编程接口

    每处理器变量分为静态和动态两种。

    静态每处理器变量

    使用宏“DEFINE_PER_CPU(type, name)”定义普通的静态每处理器变量,使用宏“DECLARE_PER_CPU(type, name)”声明普通的静态每处理器变量。

    把宏“DEFINE_PER_CPU(type, name)”展开以后是:

    __attribute__((section(".data..percpu")))  __typeof__(type)  name
    
    • 1

    可以看出,普通的静态每处理器变量存放在“.data…percpu”节(每处理器数据节)中。

    定义静态每处理器变量的其他变体如下。

    (1)使用宏“DEFINE_PER_CPU_FIRST(type, name)”定义必须在每处理器变量集合中最先出现的每处理器变量。
    (2)使用宏“DEFINE_PER_CPU_SHARED_ALIGNED(type, name)”定义和处理器缓存行对齐的每处理器变量,仅仅在SMP系统中需要和处理器缓存行对齐。
    (3)使用宏“DEFINE_PER_CPU_ALIGNED(type, name)”定义和处理器缓存行对齐的每处理器变量,不管是不是SMP系统,都需要和处理器缓存行对齐。
    (4)使用宏“DEFINE_PER_CPU_PAGE_ALIGNED(type, name)”定义和页长度对齐的每处理器变量。
    (5)使用宏“DEFINE_PER_CPU_READ_MOSTLY(type, name)”定义以读为主的每处理器变量。

    如果想要静态每处理器变量可以被其他内核模块引用,需要导出到符号表,具体如下。

    (1)如果允许任何内核模块引用,使用宏“EXPORT_PER_CPU_SYMBOL(var)”把静态每处理器变量导出到符号表。
    (2)如果只允许使用GPL许可的内核模块引用,使用宏“EXPORT_PER_CPU_SYMBOL_GPL(var)”把静态每处理器变量导出到符号表。

    动态每处理器变量

    为动态每处理器变量分配内存的函数如下。
    (1)使用函数__alloc_percpu_gfp为动态每处理器变量分配内存。

    void __percpu *__alloc_percpu_gfp(size_t size, size_t align, gfp_t gfp);
    
    • 1

    参数size是长度,参数align是对齐值,参数gfp是传给页分配器的分配标志位。
    (2)宏alloc_percpu_gfp(type, gfp)是函数__alloc_percpu_gfp的简化形式,参数size取“sizeof(type)”,参数align取“__alignof__(type)”,即数据类型type的对齐值。
    (3)函数__alloc_percpu是函数__alloc_percpu_gfp的简化形式,参数gfp取GFP_KERNEL。

    void __percpu *__alloc_percpu(size_t size, size_t align);
    
    • 1

    (4)宏alloc_percpu(type)是函数_alloc_percpu的简化形式,参数size取“sizeof(type)”,参数align取“__alignof_(type)”

    最常用的是宏alloc_percpu(type)。

    使用函数free_percpu释放动态每处理器变量的内存。

    void free_percpu(void __percpu *__pdata);
    
    • 1

    访问每处理器变量

    ptr是为每处理器变量分配内存时返回的虚拟地址

    宏“this_cpu_ptr(ptr)”用来得到当前处理器的变量副本的地址,
    宏“get_cpu_var(var)”用来得到当前处理器的变量副本的值。

    宏“this_cpu_ptr(ptr)”展开以后是:

    unsigned long __ptr;
    __ptr = (unsigned long) (ptr);
    (typeof(ptr)) (__ptr + per_cpu_offset(raw_smp_processor_id()));
    
    • 1
    • 2
    • 3

    可以看出,当前处理器的变量副本的地址等于基准地址加上当前处理器的偏移。

    宏“per_cpu_ptr(ptr, cpu)”用来得到指定处理器的变量副本的地址,
    宏“per_cpu(var, cpu)”用来得到指定处理器的变量副本的值。

    宏“get_cpu_ptr(var)”禁止内核抢占并且返回当前处理器的变量副本的地址,宏“put_cpu_ptr(var)”开启内核抢占,这两个宏成对使用,确保当前进程在内核模式下访问当前处理器的变量副本的时候不会被其他进程抢占。

    宏“get_cpu_var(var)”禁止内核抢占并且返回当前处理器的变量副本的值,宏“put_cpu_var(var)”开启内核抢占,这两个宏成对使用,确保当前进程在访问当前处理器的变量副本的时候不会被其他进程抢占。

    技术原理

    每处理器区域是按块(chunk)分配的,每个块分为多个长度相同的单元(unit),每个处理器对应一个单元。
    在NUMA系统上,把单元按内存节点分组,同一个内存节点的所有处理器对应的单元属于同一个组。

    分配块的方式有两种。
    (1)基于vmalloc区域的块分配。从vmalloc虚拟地址空间分配虚拟内存区域,然后映射到物理页。
    (2)基于内核内存的块分配。直接从页分配器分配页,使用直接映射的内核虚拟地址空间。

    基于vmalloc区域的块分配,适合多处理器系统;基于内核内存的块分配,适合单处理器系统或者处理器没有内存管理单元部件的情况,目前这种块分配方式不支持NUMA系统。

    多处理器系统默认使用基于vmalloc区域的块分配方式,单处理器系统默认使用基于内核内存的块分配方式。

    分配块的方式

    基于vmalloc区域的每处理器内存分配器的数据结构如图3.48所示,每个块对应一个pcpu_chunk实例。

    image-20220805161139568

    (1)成员data指向vm_struct指针数组,vm_struct结构体是不连续页分配器的数据结构,每个组对应一个vm_struct实例,vm_struct实例的成员addr指向组的起始地址。块以组为单位分配虚拟内存区域,一个组的虚拟地址是连续的,不同组的虚拟地址不一定是连续的。

    (2)成员populated是填充位图,记录哪些虚拟页已经映射到物理页;成员nr_populated是已填充页数,记录已经映射到物理页的虚拟页的数量。创建块时,只分配了虚拟内存区域,没有分配物理页,从块分配每处理器变量时,才分配物理页。物理页的page实例的成员index指向pcpu_chunk实例。

    (3)成员map指向分配图,分配图是一个整数数组,用来存放每个小块(block)的偏移和分配状态,成员map_alloc记录分配图的大小,成员map_used记录分配图已使用的项数。

    (4)成员free_size记录空闲字节数,成员contig_hint记录最大的连续空闲字节数。

    (5)成员base_addr是块的基准地址,一个块的每个组必须满足条件:组的起始地址 = (块的基准地址 + 组的偏移)。

    (6)成员list用来把块加入块插槽,插槽号是根据空闲字节数算出来的。基于内核内存的每处理器内存分配器的数据结构如图3.49所示,和基于vmalloc区域的每处理器内存分配器的不同如下。

    基于内核内存的每处理器内存分配器的数据结构如图3.49所示,和基于vmalloc区域的每处理器内存分配器的不同如下。

    image-20220805161457666

    (1)pcpu_chunk实例的成员data指向page结构体数组。
    (2)创建块的时候,分配了物理页,虚拟页直接映射到物理页。
    (3)不支持NUMA系统,一个块只有一个组

    一个块中偏移为offset、长度为size的区域,是由每个单元中偏移为offset、长度为size的小块(block)组成的。从一个块分配偏移为offset、长度为size的区域,就是从每个单元分配偏移为offset、长度为size的小块。为每处理器变量分配内存时,返回的虚拟地址是(chunk->base_addr +offset − delta),其中chunk->base_addr是块的基准地址,offset是单元内部的偏移,delta是(pcpu_base_addr − __per_cpu_start),__per_cpu_start是每处理器数据段的起始地址,内核把所有静态每处理器变量放在每处理器数据段,pcpu_base_addr是第一块的基准地址,每处理器内存分配器在初始化的时候把每处理器数据段复制到第一块的每个单元。

    使用宏“this_cpu_ptr(ptr)”访问每处理器变量,ptr是为每处理器变量分配内存时返回的虚拟地址。我们看看宏“this_cpu_ptr(ptr)”怎么得到当前处理器的变量副本的地址:

    this_cpu_ptr(ptr)
    = ptr + __per_cpu_offset[cpu]   /* cpu是当前处理器的编号 */
    = ptr + (delta + pcpu_unit_offsets[cpu])
    = (ptr + delta) + pcpu_unit_offsets[cpu]
    = (chunk->base_addr + offset) + pcpu_unit_offsets[cpu]
    = (chunk->base_addr + pcpu_unit_offsets[cpu]) + offset
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    pcpu_unit_offsets[cpu]是处理器对应的单元的偏移,(chunk->base_addr + pcpu_unit_offsets[cpu])是处理器对应的单元的起始地址,加上单元内部的偏移offset,就是变量副本的地址

    问:为每处理器变量分配内存时,返回的虚拟地址为什么要减去delta?

    答:因为宏“this_cpu_ptr(ptr)”在计算变量副本的地址时加上了delta,所以分配内存时返回的虚拟地址要提前减去delta。宏“this_cpu_ptr(ptr)”为
    什么要加上delta?原因是要照顾内核的静态每处理器变量。

    如图3.50所示,__per_cpu_start是每处理器数据段的起始地址,内核把所有静态每处理器变量放在每处理器数据段,pcpu_base_addr是第一块的基准地址,每处理器内存分配器在初始化时把每处理器数据段复制到第一块的每个单元。

    image-20220805162430353

    使用宏“this_cpu_ptr(ptr)”访问静态每处理器变量时,ptr是内核镜像的每处理器数据段中的变量的虚拟地址,必须加上第一块的基准地址和每处理器数据段的起始地址的差值,才能得到第一块中变量副本的地址。

    分配图

    分配图是一个整数数组,存放每个小块的偏移和分配状态,每个小块的长度是偶数,偏移是偶数,使用最低位表示小块的分配状态,如果小块被分配,那么设置最低位。

    假设系统有4个处理器,一个块分为4个单元,块的初始状态如图3.51所示,分配图使用了两项:第一项存放第一个小块的偏移0,空闲;第二项存放单元的结束标记,偏移是单元长度pcpu_unit_size,最低位被设置。

    image-20220805163151728

    分配一个长度是32字节的动态每处理器变量以后,块的状态如图3.52所示。

    每个单元中偏移为0、长度为32字节的小块被分配出去,分配图使用了三项:第一项存放第一个小块的偏移0,已分配;第二项存放第二个小块的偏移32,空闲;第三项存放单元的结束标记,偏移是单元长度pcpu_unit_size,最低位被设置。

    image-20220805162921075

    chunk组织成链表

    分配器根据空闲长度把块组织成链表,把每条链表称为块插槽,插槽的数量是pcpu_nr_slots,根据空闲长度n计算插槽号的方法如下。
    (1)如果空闲长度小于整数长度,或者最大的连续空闲字节数小于整数长度,那么插槽号是0。
    (2)如果块全部空闲,即空闲长度等于单元长度,那么取最后一个插槽号,即(pcpu_nr_slots − 1)。
    (3)其他情况:插槽号 = fls(n) − 3,并且不能小于1。fls(n)是取n被设置的最高位,例如fls(1) = 1,fls(0x80000000) = 32,相当于(log2n +1)。减3的目的是让空闲长度是1~15字节的块共享插槽1。其代码如下:

    mm/percpu.c

    #define PCPU_SLOT_BASE_SHIFT      5   /* 1~15共享相同的插槽 */
    static int __pcpu_size_to_slot(int size)
    {
         int highbit = fls(size);   /* size的单位是字节 */
         return max(highbit - PCPU_SLOT_BASE_SHIFT + 2, 1);
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    常见块

    确定块的参数
    创建一个块时,需要知道以下参数。
    (1)块分为多少个组?
    (2)每个组的偏移是多少?
    (3)每个组的长度是多少?
    (4)原子长度是多少?原子长度是对齐值,即组的长度必须是原子长度的整数倍。
    (5)单元长度是多少?
    块的各种参数是在创建第一个块的时候确定的,第一个块包含了内核的静态每处理器变量。

    创建块
    函数pcpu_create_chunk负责创建块,以基于vmalloc区域的块分配方式为
    例说明执行过程。
    (1)调用函数pcpu_alloc_chunk,分配pcpu_chunk实例并且初始化。
    (2)调用函数pcpu_get_vm_areas,负责从vmalloc虚拟地址空间分配虚拟内存区域。
    (3)块的基准地址等于(第0组的起始地址 − 第0组的偏移)。

    函数pcpu_get_vm_areas的输入参数是以下4个参数。
    (1)pcpu_group_offsets:每个组的偏移
    (2)pcpu_group_sizes:每个组的长度
    (3)pcpu_nr_groups:组的数量
    (4)pcpu_atom_size:原子长度
    需要找到一个基准值base,第n组的虚拟内存区域是[base +pcpu_group_offsets[n], base + pcpu_group_offsets[n] +pcpu_group_sizes[n]),基准值必须满足条件:基准值和原子长度对齐,并且每个组的虚拟内存区域是空闲的。

    分配内存

    把申请长度向上对齐到偶数
    根据申请长度计算出插槽号n
    遍历从插槽号n到最大插槽号pcpu_nr_slots的每个插槽 {
        遍历插槽中的每个块 {
            如果申请长度大于块的最大连续空闲字节数,那么不能从这个块分配内存。
            遍历块的分配图,如果有一个空闲小块的长度大于或等于申请长度,处理如下:
            如果小块的长度大于申请长度,先把这个小块分裂为两个小块。
            更新分配图。
            更新空闲字节数和最大连续空闲字节数
            根据空闲字节数计算新的插槽号,把块移到新的插槽中。
        }
    }
    如果分配失败,处理如下:
    如果是原子分配 {
        向全局工作队列添加1个工作项pcpu_balance_work,异步创建新的块。
    } 否则 {
        如果最后一个插槽是空的,那么创建新的块,然后重新分配内存。
    }
    如果分配成功,处理如下:
    
    如果是原子分配 {
       如果空闲的已映射到物理页的虚拟页的数量小于PCPU_EMPTY_POP_PAGES_LOW(值为2),那么向全局工作队列添加1个工作项pcpu_balance_work,异步分配物理页。
    } 否则 {
        在分配出去的区域中,对于没有映射到物理页的虚拟页,分配物理页,在内核的页表中把虚拟页映射到物理页。
    }
    把分配出去的区域清零。
    返回地址(chunk->base_addr + offset − delta),其中chunk->base_addr是块的基准地址,offset是单元内部
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    页表

    页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。 每一个进程都拥有一个自己的页表,PCB表中有指针指向页表。

    1、地址结构

    逻辑地址、物理地址、逻辑地址空间、物理地址空间?

    2、页表

    页表用来把虚拟页映射到物理页,并且存放页的保护位,即访问权限。

    • 分页逻辑地址=P页号.D页内位移

      P=线性逻辑地址/页面大小 D=线性逻辑地址-P*页面大小

    • 分页物理地址=F页帧号.D页内位移

    CPU并不是直接访问物理内存地址,而是通过虚拟地址空间来间接的访问物理内存地址。虚拟地址空间是操作系统为每个正在执行的进程分配一个逻辑地址,比如在32位系统,范围0 ~ 4G-1。操作系统通过将虚拟地址空间和物理内存地址之间建立映射关系,让CPU能够间接访问物理内存地址。

    • 一般情况将虚拟地址空间以512byte-8K,作为一个单位,称为页,并从0开始依次对它进行页编号。这个大小就称为页面。

    • 将物理地址按照同样大小,作为一个单位,称为框或者是块。也从0开始依次进行对每个框编号。

    OS通过维护一张表,这张表记录每一对页和框的映射关系。windows系统页面大小为4KB。

    image-20220605162230477

    系统为每个进程建立一个页表,在进程逻辑地址空间中每一页,依次在页表中有一个表项,记录该页对应的物理
    块号。通过查找页表就可以很容易地找到该页在内存中的位置。页表具有逻辑地址到物理地址映射作用。

    ARM64处理器页表

    Linux内核把页表直接分为4级:页全局目录(PGD)、页上层目录(PUD)、页中间目录(PMD)、直接页表(PT)。
    如果选择三级(页全局目录、页中间目录、直接页表)。如果选择二级(页全局目录和直接页表)

    4级页表的48bitVA地址空间

    内核现在最多可支持5级页表

    新的Intel芯片的MMU硬件规定可以进行5级页表管理。扩展9位达到57bit VA和52bit PA,支持丧心病狂的128PB VA和4PB PA,甚至大页都达到256G。

    内核在PGD和PUD之间,增加了一个叫P4D的层次。不过新的5级页表可以通过内核cmdline来控制内核启用4级还是5级页表,这个比原先4级页表的支持又智能了不少(OS发行版不用出两个版本了)。

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

    image-20220605162946316

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

    ARM64处理器把页表称为转换表,最多4级。ARM64处理器支持3种页长度,4KB、16KB和64KB。 页长度和虚拟地址的宽度决定了转换表的级数。

    (1)页长度是4KB:使用4级转换表,转换表和内核的页表术语的对应关系是:0级转换表对应页全局目录,1级转换表对应页上层目录,2级转换表对应页中间目录,3级转换表对应直接页表。

    48位虚拟地址被分解为如图3.54所示。

    image-20220805164852371

    每级转换表占用一页,有512项,索引是48位虚拟地址的9个位。
    (2)页长度是16KB:使用4级转换表,转换表和内核的页表术语的对应关系是:0级转换表对应页全局目录,1级转换表对应页上层目录,2级转换表对应页中间目录,3级转换表对应直接页表。
    48位虚拟地址被分解为如图3.55所示。

    image-20220805164907421

    0级转换表有2项,索引是48位虚拟地址的最高位;其他转换表占用一页,有2048项,索引是48位虚拟地址的11个位。
    (3)页长度是64KB:使用3级转换表,转换表和内核的页表术语的对应关系是:1级转换表对应页全局目录,2级转换表对应页中间目录,3级转换表
    对应直接页表。

    48位虚拟地址被分解为如图3.56所示。

    image-20220805164928733

    1级转换表有64项,索引是48位虚拟地址的最高6位;其他转换表占用一页,有8192项,索引是48位虚拟地址的13个位。

    ARM64处理器把表项称为描述符(descriptor),使用64位的长描述符格式。描述符的第0位指示描述符是不是有效的:0表示无效,1表示有效;第1
    位指定描述符类型,如下。

    (1)在第0~2级转换表中,0表示块(block)描述符,1表示表(table)描述符。块描述符存放一个内存块(即巨型页)的起始地址,表描述符存放下一级转换表的地址。
    (2)在第3级转换表中,0表示保留描述符,1表示页描述符。

  • 相关阅读:
    Halcon Variable Inspect 安装失败
    【大模型入门】LLM-AI大模型介绍
    Day48 力扣动态规划 : 647. 回文子串 |516.最长回文子序列 |动态规划总结篇
    【MyBatisPlus】快速入门
    Python寻找包的安装位置
    计算机组成原理 期末复习笔记
    从SPL看开放计算能力的意义
    06、HSMS协议介绍
    Node.js
    一些编程的基础
  • 原文地址:https://blog.csdn.net/qq_53111905/article/details/126181278