上一节介绍了linux进程的虚拟内存布局,我们知道了4G的虚拟空间被划分成了内核空间和用户空间,内核空间从3G开始,被划分成两部分,开始的一段叫低端内存,后面一段叫高端内存。
低端内存用于和物理地址建立线性映射,但由于内核空间只有1G的大小,在物理内存大于1G时无法建立一一映射的关系,所以引入了高端内存。高端内存用于和物理内存建立动态映射。
本节承接前面的内容,介绍低端内存建立虚拟地址和物理地址映射的过程,也就是页表项的创建。前面的章节已经介绍过
有时候以文字的形式很难将代码讲清楚,贴大段的代码在微信上很难看。后面考虑以视频讲解的方式替代。
start_kernel是内核启动进入C语言阶段的入口函数。
start_kernel->setup_arch->paging_init->
prepare_page_table
map_lowmem
paging_init里依次调用的两个函数是今天讨论的重点。
prepare_page_table完成了建立页表的准备工作:清空页目录项。
map_lowmem为低端内存创建页表项,建立物理地址和虚拟地址的映射关系。
static inline void prepare_page_table(void)
{
unsigned long addr;
phys_addr_t end;
/*
* Clear out all the mappings below the kernel image.
MODULES_VADDR是动态模块映射区起始地址,对于arm而言其值是PAGE_OFFSET下面16M的地方
*/
for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
/*上面两端总的意思就是将用户空间的虚拟地址的页目录项清0,PAGE_OFFSET是用户空间和内核空间的分界,为何要分两段处理,可能是怕MODULES_VADDR 定义在PAGE_OFFSET上面*/
/*
* Find the end of the first block of lowmem.
第一个内存块的低端内存部分要跳过
*/
end = memblock.memory.regions[0].base + memblock.memory.regions[0].size;
if (end >= arm_lowmem_limit)
end = arm_lowmem_limit;
/*
* Clear out all the kernel space mappings, except for the first
* memory bank, up to the vmalloc region.
*/
for (addr = __phys_to_virt(end);addr < VMALLOC_START; addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
}
该函数把虚拟地址0到VMALLOC_START为止对应的页目录项清零,但是会跳过第一个内存块占用的内存(映射成虚拟地址后是从3G开始,如果第一个内存块的地址超过了低端内存的最大限制,超过部分的页目录项同样清0)。
Linux前期使用memblock机制来管理和分配内存,从设备树解析出内存块后,将内存块存放在memblock.memory.regions数组中。之所以要跳过第一个内存块,是因为第一个内存块目前正在使用,比如内核镜像就是放在这里,而第一个内存块的页表项在内核启动文件head.S里就已经建立了,自然不能清除它。
pmd_off_k(addr)函数就是根据虚拟地址addr找到对应的页目录项地址,由于arm采用二级页表机制,所以pmd=pud=pgd,所以pmd_off_k实际就是取得页目录项pgd的地址。
由addr可以很简单的计算对应的页目录项的地址,因为页目录的基地址存放在mm-pgd中,只要计算addr是对应第几项页目录项即可。
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
pmd_clear函数很简单,就是把页目录项清0,同时执行TLB操作将其从缓存中删除。
#define pmd_clear(pmdp)\
do {\
pmdp[0] = __pmd(0);\
pmdp[1] = __pmd(0);\
clean_pmd_entry(pmdp);\
} while (0)
注意到这里一次操作将相邻两个页目录项清0,这和前面介绍arm的两级页表机制时说到的一个页目录项占8字节相应验,其中一个页目录项对应的一套页表项用于linux软件,另外一套用于arm硬件。一个页目录项映射的空间是2M,PMD_SIZE=2M。
map_lowmem将低端内存分割成三段进行映射,每段的区别仅仅是权限不同(代码段可读可写可执行,数据段可读可写),最后都是通过调用函数create_mapping函数进行映射。在调用map_lowmem之前,linux采用了一种早期管理内存的方式:memblock机制。关于该机制后面会专门讲解。在map_lowmem函数里从memblock管理的内存中取出低端内存部分,建立页表项。map_lowmem简化后的逻辑如下:
for_each_memblock(memory, reg) {
phys_addr_t start = reg->base;
phys_addr_t end = start + reg->size;
/*只映射低端内存,所以超过低端内存部分截断*/
if (end > arm_lowmem_limit)
end = arm_lowmem_limit;
if (start >= end)
break;
struct map_desc map;
map.pfn = __phys_to_pfn(start);/*转为物理页帧号*/
map.virtual = __phys_to_virt(start);/*线性映射为虚拟地址*/
map.length = end - start;
map.type = MT_MEMORY_RWX;
create_mapping(&map);
}
linux中使用map_desc数据结构完整地描述一片物理内存和虚拟地址的映射关系,定义于arch\arm\include\asm\mach\map.h:
struct map_desc {
unsigned long virtual; //虚拟地址起始地址
unsigned long pfn; //起始页框号
unsigned long length;
unsigned int type;//mem_types中的序号
};
Type成员可以理解为这段内存映射后的类型。
具体type定义如下:
Io.h中
/*
* Architecture ioremap implementation.
*/
#define MT_DEVICE0
#define MT_DEVICE_NONSHARED1
#define MT_DEVICE_CACHED2
#define MT_DEVICE_WC3
Map.h中,/* types 0-3 are defined in asm/io.h */
enum {
MT_UNCACHED = 4,
MT_CACHECLEAN,
MT_MINICLEAN,
MT_LOW_VECTORS,
MT_HIGH_VECTORS,
MT_MEMORY_RWX,
MT_MEMORY_RW,
MT_ROM,
MT_MEMORY_RWX_NONCACHED,
MT_MEMORY_RW_DTCM,
MT_MEMORY_RWX_ITCM,
MT_MEMORY_RW_SO,
MT_MEMORY_DMA_READY,
};
一段内存映射后我访问时,怎么知道它是什么类型的内存呢?或者说这些信息存放在哪里?实际上,一个页表项32位,并没有全部用于描述一个页的地址。因为一个页大小4K,这就决定了每一个页的起始地址都是4K对齐的,就是说我只要用20位就可以描述一个页的地址了。这样子,页表项剩余的12位就可以用来表示一些额外的信息。比如,该页是脏页,该页的权限是只读的,还是可读可写,可执行。
上面提及的内存类型types其实就是通过置位页表项或者页目录项的一些标志位实现。
事先定义了一个mem_types数组,用于表示每种类型的内存的页表项等要填什么内容,在建立页表时可以直接拿来用。
arch\arm\mm\mm.h:
struct mem_type {
pteval_t prot_pte;------------PTE属性
pteval_t prot_pte_s2;---------定义CONFIG_ARM_LPAE才有效
pmdval_t prot_l1;-------------PMD属性
pmdval_t prot_sect;-----------Section类型映射
unsigned int domain;----------定义ARM中不同的域
};
arch\arm\mm\mmu.c:
static struct mem_type mem_types[] = {
...
[MT_MEMORY_RWX] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,----------------------注意这里都是L_PTE_*类型,需要在写入MMU对应PTE时进行转换。
.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,
},
...
}
/*
* 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)
{
......
__create_mapping(&init_mm, md, early_alloc, false);
}
前面做了一些地址检查,然后调用_create_mapping。
继续分析__create_mapping的实现。
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];//根据内存类型找到对应的struct mem_type
.....
addr = md->virtual & PAGE_MASK;//页对齐
phys = __pfn_to_phys(md->pfn);//由页号得到物理地址
length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
/*prot_l1是页目录项属性,一般不会等于0,暂时不管*/
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);//根据虚拟地址和页目录项基地址得到虚拟地址对应的页目录项的地址,mm中存放着pgd的基地址,可见每个进程包括内核都有维护着自己的一套页表。
end = addr + length;
//逐级进行映射
do {
//计算下一级页目录项所对应的地址(也就是当前地址加2M向上对齐),如果该地址超过end那就直接返回end,后面的while循环会判断改返回值是否等于end,如果等于end说明不用下一级页目录项就可以满足这次的映射长度了。
unsigned long next = pgd_addr_end(addr, end);
alloc_init_pud(pgd, addr, next, phys, type, alloc, ng);
phys += next - addr;
addr = next;
} while (pgd++, addr != end);
}
pgd_addr_end定义如下:
/*
* When walking page tables, get the address of the next boundary,
* or the end address of the range if that comes earlier. Although no
* vma end wraps to 0, rounded up __boundary may wrap to 0 throughout.
*/
#define pgd_addr_end(addr, end)\
({unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;\
(__boundary - 1 < (end) - 1)? __boundary: (end);\
})
PGDIR_SIZE是一个页目录项就是一级页表所能表示的地址大小。
页目录项(一级页表)是在进程启动时就全部创建好的,比如内核的。但页目录项之后的每一级页表项是用到的时候再创建,这是为了节省内存空间所考虑的,这在前面的文章也分析过。(有没有发现我写的文章都是前后呼应,相互印证的)
所以接下来的任务便是逐级创建页表项。本级页表项会创建它所包含的下一级页表项。
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);
}
alloc_init_pud传入参数的addr和end是pgd就是pud的上一级页表所映射的地址区间。
通过前面的介绍可以知道,arm只使用二级页表,但是linux提供的接口是通用的面向4级页表的。
两级页表没有pud和pmd,
#define pud_addr_end(addr, end)(end)
继续看pmd这一级页表的创建
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);
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) {
__map_init_section(pmd, addr, next, phys, type, ng);
} else {//没有启用段映射的情况下我们走的是这个分支
alloc_init_pte(pmd, addr, next,
__phys_to_pfn(phys), type, alloc, ng);
}
phys += next - addr;
} while (pmd++, addr = next, addr != end);
}
arm同样没有pmd这级页表,所以有:
#define pmd_addr_end(addr, end)(end)
终于到了最后一级页表的创建:
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);
}
代码很简单,就是创建1024个页表项。Pte_t实质上是一个无符号32位整数,也就是二级页表项。
由前面的代码可知,linux下1级页目录也就是PGD占据了11位,1个PGD页目录项映射了2M的空间,总共有2048个PGD页目录项。二级页表项占据9位,所以一个页目录项包含512个二级页表项。
看下arm_pte_alloc怎么分配:
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)) {
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);
}
#define PTRS_PER_PTE 512
#define PTE_HWTABLE_PTRS (PTRS_PER_PTE)
#define PTE_HWTABLE_OFF (PTE_HWTABLE_PTRS * sizeof(pte_t))
#define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizeof(u32))
pmd_none判断pmd存放的内容也就是页目录项是否为空,空则为pte页表项分配空间,alloc了1024个pte_t,实际上由上面的分析可知只要512个二级页表项就足够了,这里为何分配了1024个呢?
原因在于arm硬件定义的页表项和linux软件定义的页表项不一样,所以就各分配了512个页表项分别给arm硬件mmu和linux使用。在空间排布上两者是连续的。这里又和前面的描述相互印证。
如果pmd有内容,说明页表已经建立过了,直接返回页表项即可。
static inline pte_t *pmd_page_vaddr(pmd_t pmd)
{
return __va(pmd_val(pmd) & PHYS_MASK & (s32)PAGE_MASK);
}
#define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pte_offset_kernel(pmd,addr) (pmd_page_vaddr(*(pmd)) + pte_index(addr))
set_pte_ext函数最终由汇编代码实现,这里不深究。
前面遗留了个早期内存分配函数没有分析。
static void __init *early_alloc(unsigned long sz)
{
void *ptr = memblock_alloc(sz, sz);
if (!ptr)
panic("%s: Failed to allocate %lu bytes align=0x%lx\n",
__func__, sz, sz);
return ptr;
}
其实也能猜到,早期的内存分配肯定是从memblock分配。memblock_alloc函数的分析见memblock一节。
总结下_create_mapping的具体流程。已知虚拟地址virtual,要映射的物理地址的起始页号pfn,和要映射的长度len。函数的处理流程如下:
1、 虚拟地址addr先向上页对齐
2、 由起始页号可以算出要映射的起始物理地址
3、 将要映射的长度len向上页对齐
4、 由mm结构体得到页目录基地址,由addr得到页目录号,相加得到addr所在的页目录地址pgd
5、 从pgd开始依次取出页目录项,建立页表,直到取用的页目录项数*2的21次方足以满足所需的长度len为止。针对一个页目录项,又是如何建立页表的呢:
继续依次取出一个页目录项中的下一级的目录项,对于2级页表来说,下一级目录项就是页表项pte了,依次取出每一个页表项:
pte_t *pte = arm_pte_alloc(…)分配一个页表项
set_pte_ext设置pte属性