Linux深入理解内存管理13(基于Linux6.6)---页表映射介绍
一、概述
1、内存映射的基本概念
- 物理内存:我们通常所说的内存容量,指的是物理内存,只有内核才可以直接访问物理内存,进程并不可以。
- 虚拟内存:Linux内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以很方便地访问内存,更确切地说是访问虚拟内存。虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同字长的处理器,地址空间的范围也不同。
由于每个进程都有一个独立的虚拟地址空间,且所有进程的虚拟内存加起来要比实际的物理内存大得多,因此并不是所有的虚拟内存都会分配物理内存。只有那些实际使用的虚拟内存才会分配物理内存,并通过内存映射来管理。
2、页表的作用与结构
- 作用:页表是内存管理的一种数据结构,用于记录虚拟地址与物理地址的映射关系。当进程访问虚拟内存时,内存管理单元(MMU)会查阅页表,将虚拟地址转换为物理地址,从而实现对物理内存的访问。
- 结构:页表由一系列页表项(PTE)组成,每个页表项都保存了一个虚拟页面对应的物理页面信息。在32位系统中,页表项通常占用4个字节;在64位系统中,页表项则占用8个字节。
3、页表映射的工作原理
-
分页机制:分页机制的核心思想是解除线性地址和物理地址的一一对应关系,使线性地址连续,而物理地址不连续。这样,连续的线性地址可以与任意物理内存地址相关联,即从虚拟页面到物理页面的映射。这个翻译过程由内存管理单元(MMU)完成。
-
寻址过程:
- 当进程访问一个虚拟地址时,MMU会首先查阅页表,找到该虚拟地址对应的物理地址。
- 如果页表中存在该虚拟地址的映射关系,则MMU将其转换为物理地址,并访问相应的物理内存。
- 如果页表中不存在该虚拟地址的映射关系(即发生缺页中断),则操作系统会为该虚拟地址分配一个物理页面,并更新页表,然后重新执行该访问操作。
4、Linux中的自映射技术
Linux对页目录和页表使用自映射技术。自映射是指将页目录和页表本身也映射到虚拟内存中的一部分空间。这样,操作系统可以通过访问虚拟地址来访问页目录和页表,从而简化了对它们的访问和管理。自映射技术不仅简化了页目录和页表的访问,还节省了内存空间的使用。
5、多级页表与大页
- 多级页表:为了解决页表项过多的问题,Linux采用了多级页表结构。多级页表将页表分为多个层次,每个层次都包含一定数量的页表项。这样,当需要访问某个虚拟地址时,只需要查阅相应层次的页表项即可找到对应的物理地址。多级页表结构有效地减少了页表项的数量和内存占用。
- 大页:大页是一种较大的内存页面,通常是2MB或更大。由于页变大了,需要的页表项也就少了,因此大页可以减少页表的大小和内存占用。此外,大页还可以提高CPU中TLB(Translation Lookaside Buffer)的命中率,减少遍历页表的次数,从而提高内存访问性能。然而,大页也需要预先分配,并且可能受到系统内存碎片化的影响。
二、映射模型
Linux内核中一般采用的是3级映射模型,第一层是页面目录(PDG),第二层是中间目录(PMD),页表(PTE),其三级映射的框图如下:
采用按段来映射,这时候采用的是一级页表,内存中有一个映射段,表中有4096个表项,每个表项大小为4Byte,所以这个映射表的大小为16KB,而且其位置必须是16KB边界对齐,每个段表项可以寻址1MB的大小的地址空间。当CPU访问内存时,32位的虚拟地址的高12位(bit[31:20])用作访问段映射表的索引,从表中找到对应的表项,每个表项提供一个12Bit的物理短地址,以及相应的标志位,如可读,可写等标志位。将这个12bit的物理地址和虚拟地址的低20bit拼凑在一起,就得到32bit的物理地址。但是在ARM32系统中只用到了两层映射,所以软件上就会跳过PMD表,其映射框图如下图:
32位的虚拟地址的高12位(bit[31:20])作为访问一级页表的索引值,通过TTBRx找到PGD页表项的基地址,然后加上索引值,就可以找到二级页表的基地址。以虚拟地址的次8位(bit[19:12])作为二级页表的索引值,得到相应的页表项,从这个页表项中找到20位的物理页面地址,最后将这个20位的物理页面地址和虚拟地址的低12Bit拼在一起,最终就得到了32位物理地址。整个过程由MMU硬件完成,软件不需要接入。
三、实现过程
3.1、数据结构
从ARM Linux内核建立具体内存区间的页面映射过程来看页表的映射是如何实现的。在map_lowmem()使用create_mapping()创建页表映射,这个函数的参数结构是struct map_desc,下面来研究它的相关结构体,有助于理解内核是如何处理页表映射的。
arch/arm/include/asm/mach/map.h
struct map_desc {
unsigned long virtual;
unsigned long pfn;
unsigned long length;
unsigned int type;
};
而对于内存区间的属性type指向类型位struct mem_type的mem_types数组。
arch/arm/mm/mm.h
struct mem_type {
pteval_t prot_pte;
pteval_t prot_pte_s2;
pmdval_t prot_l1;
pmdval_t prot_sect;
unsigned int domain;
};
对于domain成员用于ARM中定义的不同的域,ARM linux只是用了3个
#define DOMAIN_KERNEL 2
#define DOMAIN_USER 1
#define DOMAIN_IO 0
DOMAIN_KERNEL属于系统空间,DOMAIN_IO用于I/O地址域,实际也属于系统空间,DOMAIN_USER则属于用户空间。下面重点关注对于二级映射中的一级页表和二级页表,对于ARMV7中,下面是first-level descriptor详细说明:
prot_pl1成员用于一级页表项的控制位和标志位,具体的定义如下:
#define PMD_TYPE_MASK (_AT(pmdval_t, 3) << 0)
#define PMD_TYPE_FAULT (_AT(pmdval_t, 0) << 0)
#define PMD_TYPE_TABLE (_AT(pmdval_t, 1) << 0)
#define PMD_TYPE_SECT (_AT(pmdval_t, 2) << 0)
#define PMD_PXNTABLE (_AT(pmdval_t, 1) << 2) /* v7 */
#define PMD_BIT4 (_AT(pmdval_t, 1) << 4)
#define PMD_DOMAIN(x) (_AT(pmdval_t, (x)) << 5)
#define PMD_PROTECTION (_AT(pmdval_t, 1) << 9) /* v5 */
下面是second-level descriptor的详细说明:
prot_pte成员用于页面表项的控制位和标志位,其具体的定义如下:
/*
* + Level 2 descriptor (PTE)
* - common
*/
#define PTE_TYPE_MASK (_AT(pteval_t, 3) << 0)
#define PTE_TYPE_FAULT (_AT(pteval_t, 0) << 0)
#define PTE_TYPE_LARGE (_AT(pteval_t, 1) << 0)
#define PTE_TYPE_SMALL (_AT(pteval_t, 2) << 0)
#define PTE_TYPE_EXT (_AT(pteval_t, 3) << 0) /* v5 */
#define PTE_BUFFERABLE (_AT(pteval_t, 1) << 2)
#define PTE_CACHEABLE (_AT(pteval_t, 1) << 3)
对于系统中定义了一个全局的mem_type[]数组来描述所有的内存区间类型,定义如下:
arch/arm/mm/mmu.c
static struct mem_type mem_types[] __ro_after_init = {
[MT_DEVICE] = { /* Strongly ordered / ARMv6 shared device */
.prot_pte = PROT_PTE_DEVICE | L_PTE_MT_DEV_SHARED |
L_PTE_SHARED,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PROT_SECT_DEVICE | PMD_SECT_S,
.domain = DOMAIN_IO,
},
[MT_DEVICE_NONSHARED] = { /* ARMv6 non-shared device */
.prot_pte = PROT_PTE_DEVICE | L_PTE_MT_DEV_NONSHARED,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PROT_SECT_DEVICE,
.domain = DOMAIN_IO,
},
[MT_DEVICE_CACHED] = { /* ioremap_cache */
.prot_pte = PROT_PTE_DEVICE | L_PTE_MT_DEV_CACHED,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PROT_SECT_DEVICE | PMD_SECT_WB,
.domain = DOMAIN_IO,
},
[MT_DEVICE_WC] = { /* ioremap_wc */
.prot_pte = PROT_PTE_DEVICE | L_PTE_MT_DEV_WC,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PROT_SECT_DEVICE,
.domain = DOMAIN_IO,
},
[MT_UNCACHED] = {
.prot_pte = PROT_PTE_DEVICE,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_XN,
.domain = DOMAIN_IO,
},
[MT_CACHECLEAN] = {
.prot_sect = PMD_TYPE_SECT | PMD_SECT_XN,
.domain = DOMAIN_KERNEL,
},
#ifndef CONFIG_ARM_LPAE
[MT_MINICLEAN] = {
.prot_sect = PMD_TYPE_SECT | PMD_SECT_XN | PMD_SECT_MINICACHE,
.domain = DOMAIN_KERNEL,
},
#endif
[MT_LOW_VECTORS] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_RDONLY,
.prot_l1 = PMD_TYPE_TABLE,
.domain = DOMAIN_VECTORS,
},
[MT_HIGH_VECTORS] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_USER | L_PTE_RDONLY,
.prot_l1 = PMD_TYPE_TABLE,
.domain = DOMAIN_VECTORS,
},
[MT_MEMORY_RWX] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RW] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_XN,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RO] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_XN | L_PTE_RDONLY,
.prot_l1 = PMD_TYPE_TABLE,
#ifdef CONFIG_ARM_LPAE
.prot_sect = PMD_TYPE_SECT | L_PMD_SECT_RDONLY | PMD_SECT_AP2,
#else
.prot_sect = PMD_TYPE_SECT,
#endif
.domain = DOMAIN_KERNEL,
},
[MT_ROM] = {
.prot_sect = PMD_TYPE_SECT,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RWX_NONCACHED] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_MT_BUFFERABLE,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RW_DTCM] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_XN,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_XN,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RWX_ITCM] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,
.prot_l1 = PMD_TYPE_TABLE,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RW_SO] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_MT_UNCACHED | L_PTE_XN,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_S |
PMD_SECT_UNCACHED | PMD_SECT_XN,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_DMA_READY] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_XN,
.prot_l1 = PMD_TYPE_TABLE,
.domain = DOMAIN_KERNEL,
},
};
3.2、map_lowmem
create_mapping的参数是struct map_desc类型,用于描述一个虚拟地址区域线性映射到物理区域。基于这块区域创建PGD/PTE,下面就进入map_lowmem。
arch/arm/mm/mmu.c
static void __init map_lowmem(void)
{
phys_addr_t start, end;
u64 i;
/* Map all the lowmem memory banks. */
for_each_mem_range(i, &start, &end) {
struct map_desc map;
pr_debug("map lowmem start: 0x%08llx, end: 0x%08llx\n",
(long long)start, (long long)end);
if (end > arm_lowmem_limit)
end = arm_lowmem_limit;
if (start >= end)
break;
/*
* If our kernel image is in the VMALLOC area we need to remove
* the kernel physical memory from lowmem since the kernel will
* be mapped separately.
*
* The kernel will typically be at the very start of lowmem,
* but any placement relative to memory ranges is possible.
*
* If the memblock contains the kernel, we have to chisel out
* the kernel memory from it and map each part separately. We
* get 6 different theoretical cases:
*
* +--------+ +--------+
* +-- start --+ +--------+ | Kernel | | Kernel |
* | | | Kernel | | case 2 | | case 5 |
* | | | case 1 | +--------+ | | +--------+
* | Memory | +--------+ | | | Kernel |
* | range | +--------+ | | | case 6 |
* | | | Kernel | +--------+ | | +--------+
* | | | case 3 | | Kernel | | |
* +-- end ----+ +--------+ | case 4 | | |
* +--------+ +--------+
*/
/* Case 5: kernel covers range, don't map anything, should be rare */
if ((start > kernel_sec_start) && (end < kernel_sec_end))
break;
/* Cases where the kernel is starting inside the range */
if ((kernel_sec_start >= start) && (kernel_sec_start <= end)) {
/* Case 6: kernel is embedded in the range, we need two mappings */
if ((start < kernel_sec_start) && (end > kernel_sec_end)) {
/* Map memory below the kernel */
map.pfn = __phys_to_pfn(start);
map.virtual = __phys_to_virt(start);
map.length = kernel_sec_start - start;
map.type = MT_MEMORY_RW;
create_mapping(&map);
/* Map memory above the kernel */
map.pfn = __phys_to_pfn(kernel_sec_end);
map.virtual = __phys_to_virt(kernel_sec_end);
map.length = end - kernel_sec_end;
map.type = MT_MEMORY_RW;
create_mapping(&map);
break;
}
/* Case 1: kernel and range start at the same address, should be common */
if (kernel_sec_start == start)
start = kernel_sec_end;
/* Case 3: kernel and range end at the same address, should be rare */
if (kernel_sec_end == end)
end = kernel_sec_start;
} else if ((kernel_sec_start < start) && (kernel_sec_end > start) && (kernel_sec_end < end)) {
/* Case 2: kernel ends inside range, starts below it */
start = kernel_sec_end;
} else if ((kernel_sec_start > start) && (kernel_sec_start < end) && (kernel_sec_end > end)) {
/* Case 4: kernel starts inside range, ends above it */
end = kernel_sec_start;
}
map.pfn = __phys_to_pfn(start);
map.virtual = __phys_to_virt(start);
map.length = end - start;
map.type = MT_MEMORY_RW;
create_mapping(&map);
}
}
- 如果memblock region的起始地址包含了kernel _stext到init_end区间,则需要调用三次create mapping()建立映射。
- 如果memblock region只包含kernel _stext到init_end区间的一部分,则需要调用三次create mapping()建立映射。
- 如果memblock region不包含kernel _stext到init_end区间,则只需要调用一次create mapping()建立映射。
kernel的text段起始物理地址和init段结束的物理地址区间需要单独映射,而对于IMX6系列,kernel_x_start=80200000,kernel_x_end=81000000,而memory中定义了内存的地址空间为80000000~a0000000,采用的是直接映射的方式。
- 0x8000 0000 ~ 0x8020 0000空间第一次使用create_mapping()建立映射,对于kernel的代码段,使用MT_MEMORY_RW属性,对应的物理页面是0x80000,虚拟地址为0xc000 0000~0xc020 0000。
- 0x8020 0000 ~ 0x8100 0000空间第二次使用create_mapping()建立映射,使用MT_MEMORY_RWX属性,对应的物理页面是0x80200,虚拟地址为0xc020 0000 ~ 0xc100 0000。
- 0x8100 0000 ~ 0xa000 0000空间第三次使用create_mapping()建立映射,使用MT_MEMORY_RW属性,对应的物理页面是0x81000,虚拟地址为0xc100 0000 ~ 0xe000 0000。
3.3、create_mapping
通过定义了3个内存区间,然后调用create_mapping时,以此数据结构指针为调用参数,那么我们来看看create_mapping。
arch/arm/mm/mmu.c
/*
* Create the page directory entries and any necessary
* page tables for the mapping specified by `md'. We
* are able to cope here with varying sizes and address
* offsets, and we take full advantage of sections and
* supersections.
*/
static void __init create_mapping(struct map_desc *md)
{
if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {---解析1
pr_warn("BUG: not creating mapping for 0x%08llx at 0x%08lx in user region\n",
(long long)__pfn_to_phys((u64)md->pfn), md->virtual);
return;
}
if (md->type == MT_DEVICE &&
md->virtual >= PAGE_OFFSET && md->virtual < FIXADDR_START &&
(md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) {---解析2
pr_warn("BUG: mapping for 0x%08llx at 0x%08lx out of vmalloc space\n",
(long long)__pfn_to_phys((u64)md->pfn), md->virtual);
}
__create_mapping(&init_mm, md, early_alloc, false);---解析3
}
- 首先判断虚拟地址是否合法,判断虚拟地址在用户区域,并且不是中断向量表(中断向量表可以在虚拟地址0开始的地方)。
- 判断映射类型是否合法 ,内存类型为IO和ROM类型的不允许映射在低端内存或高于VMALLOC_END区域,只能映射在vmalloc区域。
- 参数检查后,调用__create_mapping进行实际的映射。
arch/arm/mm/mmu.c
static void __init __create_mapping(struct mm_struct *mm, struct map_desc *md,
void *(*alloc)(unsigned long sz),
bool ng)
{
unsigned long addr, length, end;
phys_addr_t phys;
const struct mem_type *type;
pgd_t *pgd;
type = &mem_types[md->type];---解析1
#ifndef CONFIG_ARM_LPAE
/*
* Catch 36-bit addresses
*/
if (md->pfn >= 0x100000) {
create_36bit_mapping(mm, md, type, ng);
return;
}
#endif
addr = md->virtual & PAGE_MASK;
phys = __pfn_to_phys(md->pfn);
length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {
pr_warn("BUG: map for 0x%08llx at 0x%08lx can not be mapped using pages, ignoring.\n",
(long long)__pfn_to_phys(md->pfn), addr);
return;
}
pgd = pgd_offset(mm, addr);---解析2
end = addr + length;
do {---解析3
unsigned long next = pgd_addr_end(addr, end);
alloc_init_p4d(pgd, addr, next, phys, type, alloc, ng);---解析4
phys += next - addr;
addr = next;
} while (pgd++, addr != end);
}
创建地址映射需要首先明确地址空间,不同的进程有不同的地址空间,而我们这里对内核虚拟地址空间而创建地址映射,因此传递的参数是init_mm。其处理流程为:
- 根据type找到对应的struct mem_type,然后虚拟地址采用4K地址对其方式,通过物理页面找到对应物理地址,然后进行参数合法性检查。
- 根据addr找到对应虚拟地址对应的pgd地址。
- (addr,length)这个虚拟地址范围可能需要占用多个PGD entry,因此采用一个循环,不断的调用alloc_init_pud函数来完成(addr,length)这个虚拟地址范围的映射。 pgd_addr_end(addr, end); 获取addr后下一个2M的虚拟起始地址,保证不超过end,如果超过end,则返end。
- 一是填充pgd entry,二是创建后续的pud translation table(如果需要的话)并进行下游Translation table的建立,对于ARM32 ,该PGD表项不存在,所以只会执行一次,接下来创建下一级页表。对于4级页表的处理器,这个会创建PGD的表项。
3.4、init_mm数据结构
首先来看看,pdg_offset,入参是mm和addr,获去所属的页面目录项PGD,内核的页表存放在swapper_pg_dir 地址中,可以通过init_mm数据结构来获得。
mm/init-mm.c
/*
* For dynamically allocated mm_structs, there is a dynamically sized cpumask
* at the end of the structure, the size of which depends on the maximum CPU
* number the system can see. That way we allocate only as much memory for
* mm_cpumask() as needed for the hundreds, or thousands of processes that
* a system typically runs.
*
* Since there is only one init_mm in the entire system, keep it simple
* and size this cpu_bitmask to NR_CPUS.
*/
struct mm_struct init_mm = {
.mm_mt = MTREE_INIT_EXT(mm_mt, MM_MT_FLAGS, init_mm.mmap_lock),
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.write_protect_seq = SEQCNT_ZERO(init_mm.write_protect_seq),
MMAP_LOCK_INITIALIZER(init_mm)
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.arg_lock = __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
.user_ns = &init_user_ns,
.cpu_bitmap = CPU_BITS_NONE,
#ifdef CONFIG_IOMMU_SVA
.pasid = INVALID_IOASID,
#endif
INIT_MM_CONTEXT(init_mm)
};
这个 swapper_pg_dir 是 pgd 的入口,前面章节已经介绍过,其值是0xc000 4000,内核的pgd的起始地址在0xc000 4000, 所以(init_mm)->pgd+(add >> 21) = (pgd_t) 0xc000 4000 + (addr >> 21)。如果addr的值是0xc000 0000,那么右移21位值为0x600,最后就为0xc000 4000 + 0x600 * 4 = 0xc000 7000,总之pdg_offset_k()可以从init_mm数据结构所指定的页面目录中找到地址addr所属的页面目录项指针pgd。首先通过init_mm结构得到页表的基地址,然后通过addr右移21得到pgd的索引值,最后在一级页表中找到对应的页表项pgd。
在计算得到虚拟地址的结束地点end = addr + length,这里是为了取得addr开始,PGDIR_SIZE为步长,end为结束标志位来进行while循环,所以对于while循环,此处按照2MB步长,遍历[virtual, virtual+length)空间创建PDG页表和PTE。
#define pgd_addr_end(addr, end) \
({ unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK; \
(__boundary - 1 < (end) - 1)? __boundary: (end); \
})
3.5、alloc_init_pud
接下来,看看alloc_init_pud的处理流程:
arch/arm/mm/mmu.c
static void __init alloc_init_pud(pgd_t *pgd, unsigned long addr,
unsigned long end, phys_addr_t phys,
const struct mem_type *type,
void *(*alloc)(unsigned long sz), bool ng)
{
pud_t *pud = pud_offset(pgd, addr);
unsigned long next;
do {
next = pud_addr_end(addr, end);
alloc_init_pmd(pud, addr, next, phys, type, alloc, ng);
phys += next - addr;
} while (pud++, addr = next, addr != end);
}
- 根据pgd中找到对应的PUD项,然后计算PUD的结束地址,给PUD建立PMD,然后循环,其处理流程与PGD类似,由对于ARM32,PUD也不存在。
由于是2级映射,这里的pud=pgd,接着调用alloc_init_pmd。
arch/arm/mm/mmu.c
static void __init alloc_init_pmd(pud_t *pud, unsigned long addr,
unsigned long end, phys_addr_t phys,
const struct mem_type *type,
void *(*alloc)(unsigned long sz), bool ng)
{
pmd_t *pmd = pmd_offset(pud, addr);---解析1
unsigned long next;
do {
/*
* With LPAE, we must loop over to map
* all the pmds for the given range.
*/
next = pmd_addr_end(addr, end);
/*
* Try a section mapping - addr, next and phys must all be
* aligned to a section boundary.
*/
if (type->prot_sect &&
((addr | next | phys) & ~SECTION_MASK) == 0) {---解析2
__map_init_section(pmd, addr, next, phys, type, ng);
} else {
alloc_init_pte(pmd, addr, next,
__phys_to_pfn(phys), type, alloc, ng);---解析3
}
phys += next - addr;
} while (pmd++, addr = next, addr != end);
}
- 通过pud拿到对应的二级页表的PMD
- 如果当前的物理地址,虚拟地址以及下一个将要映射的起始地址是按照2MB对齐,同时prot_sect代表主页表是以段映射方式,则可以按照段映射方式,不需要按照二级映射的方式
- 对应的是二级页表的初始化,pte表的初始化
回到开头,在map_lowmem创建了3段映射,都是采用段映射的方式,其映射方式如下,其的段映射地址空间,起始地址为0xc000 7000,大小为0x800。
物理地址范围 | 段表地址范围 | 虚拟地址范围 |
0x80000000-0x80200000 | 0xc0007000 | 0xc0000000-0xc0200000 |
0x80200000-0x81000000 | 0xc0007008-0xc0007038 | 0xc0200000-0xc1000000 |
0x81000000-0xa0000000 | 0xc0007040-xc00077f8 | 0xc1000000-0xe0000000 |
3.6、arm_pte_alloc
下面来看看arm-linux采用的是两级页表的映射,跳过了PUD和PMD,所以就直接到alloc_init_pte创建PTE表,其处理为:
arch/arm/mm/mmu.c
static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
unsigned long end, unsigned long pfn,
const struct mem_type *type,
void *(*alloc)(unsigned long sz),
bool ng)
{
pte_t *pte = arm_pte_alloc(pmd, addr, type->prot_l1, alloc);
do {
set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)),
ng ? PTE_EXT_NG : 0);
pfn++;
} while (pte++, addr += PAGE_SIZE, addr != end);
}
- pmd参数传递L1页表地址
- addr和end分别指明被映射到虚拟地址的起止地址
- pfn是将被映射的物理地址的页框
- type参数指明映射的类型
arm_pte_alloc函数使用port_l1作为参数,创建PGD页表目录,返回addr对应的pte地址,后面的跟之前的PMD的原理一样,遍历(addr,end)区间内存,以PAGE_SIZE为步长。
arch/arm/mm/mmu.c
static pte_t * __init arm_pte_alloc(pmd_t *pmd, unsigned long addr,
unsigned long prot,
void *(*alloc)(unsigned long sz))
{
if (pmd_none(*pmd)) {---解析1
pte_t *pte = alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);
__pmd_populate(pmd, __pa(pte), prot);
}
BUG_ON(pmd_bad(*pmd));
return pte_offset_kernel(pmd, addr);---解析2
}
判断pmd所指向的L2页表,不存在就直接通过alloc函数分配,PTE_HWTABLE_OFF(5124=2KB)+PTE_HWTABLE_SIZE(5124=2KB)总计4KB的一个物理页来存储2个linux pet 页表+2个hw pte页表。然而最开始使用 va[31:20] 一共 12 bits 来表征 1 级表项的 index_1,va[19:12] 8 bits 表征 2级表项 index_2,也就是说,1 级表项一共有 2 的 12 次幂这么多个 entry,也就是 4096 个,2 级表项有 2 的 8 次幂个 entry,也就是 256 个。这个特性是 ARM 的 MMU 硬件特性。而为什么我们alloc分配的时候,分配了2个512呢?
看看 Linux 的 pgtable-2level.h 的部分代码注释
* This leads to the page tables having the following layout:
*
* pgd pte
* | |
* +--------+
* | | +------------+ +0
* +- - - - + | Linux pt 0 |
* | | +------------+ +1024
* +--------+ +0 | Linux pt 1 |
* | |-----> +------------+ +2048
* +- - - - + +4 | h/w pt 0 |
* | |-----> +------------+ +3072
* +--------+ +8 | h/w pt 1 |
* | | +------------+ +4096
*
* See L_PTE_xxx below for definitions of bits in the "Linux pt", and
* PTE_xxx for definitions of bits appearing in the "h/w pt".
*
* PMD_xxx definitions refer to bits in the first level page table.
*
针对二级页表呢,分配了 512 + 512 个,其实真正的 ARM MMU 的二级是 256 个,他们的对应关系如上面的简要的图所示,pgd 对应到了 h/w pt 0 和 h/w pt 1,他们都是 256 的(每个 pte 是 4 个 Bytes,所以图中看到是 1K 的 Step),另外的两个是 Linux OS 对页面的一些描述信息,同他们放到一起,正好组成了 4K ,即一个页面。分配好内存后,那么就使用__pmd_populate(),生成pmd页表目录,并刷入RAM。
根据所属的页目录项的地址和address,返回相应的 PTE 表项
从 arm_pte_alloc 函数返回到 alloc_init_pte 后,继续调用 set_pte_ext,这个和结构体系相关,在 ARMv7-A架构的处理器,它的实现是在汇编函数中,其中入参如下
r0 | pted | pointer to level 2 translation table entry |
r1 | pte | PTE value to store |
r2 | ext | value for extended pte bits |
ENTRY(cpu_v7_set_pte_ext)
#ifdef CONFIG_MMU
str r1, [r0] @ linux version ----------将r1的值存入r0地址的内存中
bic r3, r1, #0x000003f0 ----------清除r1的bit[9:4],存入r3
bic r3, r3, #PTE_TYPE_MASK ----------PTE_TYPE_MASK为0x03,记清除低2位
orr r3, r3, r2 ----------r3与r2或,存入r3
orr r3, r3, #PTE_EXT_AP0 | 2 ----------这里将bit1和bit4置位,所以是Small page。
tst r1, #1 << 4 ----------判断r1的bit4是否为0
orrne r3, r3, #PTE_EXT_TEX(1) ----------设置TEX为1
eor r1, r1, #L_PTE_DIRTY
tst r1, #L_PTE_RDONLY | L_PTE_DIRTY
orrne r3, r3, #PTE_EXT_APX ----------设置AP[2]
tst r1, #L_PTE_USER
orrne r3, r3, #PTE_EXT_AP1 ----------设置AP[1]
tst r1, #L_PTE_XN
orrne r3, r3, #PTE_EXT_XN ----------设置XN位
tst r1, #L_PTE_YOUNG
tstne r1, #L_PTE_VALID
eorne r1, r1, #L_PTE_NONE
tstne r1, #L_PTE_NONE
moveq r3, #0
ARM( str r3, [r0, #2048]! -) ---------并没有写入r0,而是写入r0+2048Bytes的偏移。
THUMB( add r0, r0, #2048 )
THUMB( str r3, [r0] )
ALT_SMP(W(nop))
ALT_UP (mcr p15, 0, r0, c7, c10, 1) @ flush_pte
#endif
bx lr
ENDPROC(cpu_v7_set_pte_ext)
要理解是如何设置PTE表项,就需要参照B3.3.1 Translation table entry formants中关于Second-level descriptors的描述:
四、总结
主要是针对的是map_lowmem进行了分析,该函数主要的是完成低端内存的映射的过程,也就是lowmem : 0xc0000000 - 0xe0000000 ( 512 MB),对于这个区域,对于不同的芯片,低端内存的空间是可以配置的。针对该函数对于内存空间通过create_mapping进行了映射,对内核整个映射的过程进行了初步的梳理,对于arm32,内核是支持4级页表映射,如图下所示: