Linux在启动过程中,要首先进行内存的初始化,那么就一定要首先创建页表。我们知道每个进程都拥有各自的进程空间,而每个进程空间又分为内核空间和用户空间。
以arm32为例,每个进程有4G的虚拟空间,其中0-3G属于用户地址空间,3G-4G属于内核地址空间,内核地址空间是所有进程共享的,因此内核地址空间的页表也是所有进程共享的。
Linux内核中用户进程内存页表的管理是通过一个结构体mm_struct来描述的:
struct mm_struct {
......
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
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
*/
......
};
这个结构体中的pgd成员就是代表着PGD页表的存放位置,通过前面文章的介绍,我们知道PGD页表项中存放的是下一级页表的基地址,这样通过它我们就可以进一步找到PUD/PMD/PTE后面的页表了。
进程页表是存放在各自进程的task_struct中的,我们先来看下task_struct:
include/linux/sched.h:
struct task_struct {
......
struct mm_struct *mm, *active_mm;
......
};
这个mm成员变量中就是存放的该进程对应的mm_struct结构体数据,通过它我们就可以知道对应进程的页表了。
mm | active_mm |
---|---|
用户进程地址空间 | 活跃的用户进程地址空间 |
active_mm成员是专门为内核进程引入的,内核进程是不需要访问用户地址空间的,也就是说mm成员是被设置为NULL的,那么为了让内核进程与普通用户进程具有统一的上下文切换方式,当内核进程进行上下文切换时,让内核进程的active_mm指向刚被调度出去的进程的active_mm,之所以引入这个机制,是为了节省context switch带来的系统开销,当我们发现要进程切换的是一个内核进程(线程)时,由于我们不需要访问用户地址,那么只需要借用上一个进程的active mm配置即可,这样一来,调度器就可以节省switch_mm的开销了,由此可以很大提高系统性能。
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct pin_cookie cookie)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);
if (!mm) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
} else
switch_mm_irqs_off(oldmm, mm, next);
通过上面的函数可见,对于mm为空的情况,直接把active_mm 设置为prev->active_mm,这就是设置的内核线程的地址空间。而对于用户进程,active_mm就被设置为等于mm,这一步是在fork的时候做的:
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm;
int retval;
tsk->min_flt = tsk->maj_flt = 0;
tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif
tsk->mm = NULL;
tsk->active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm;
if (!oldmm)
return 0;
/* initialize the new vmacache entries */
vmacache_flush(tsk);
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
fork执行的时候是会调用copy_mm函数的,此函数通过oldmm来判断当前执行fork的是内核进程还是用户进程,如果是oldmm为空,代表着要创建的是一个内核进程,此时我们直接返回,如果是一个用户进程,那么最后会设置 tsk->mm = mm; 并且 tsk->active_mm = mm; 。task_struct中的mm成员主要是记录用户地址空间,其中记录的pgd是会最终配置到MMU中的TTBR0寄存器中的。
在Linux系统中所有进程的内核页表是共享的同一套,内核页表是存放在swapper_pg_dir,这一套是我们静态定义的页表:
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
.user_ns = &init_user_ns,
INIT_MM_CONTEXT(init_mm)
};
swapper_pg_dir 仅包含内核(全局)映射,而用户空间页表仅包含用户(非全局)映射。CPU在访问一个虚拟内存时,由虚拟地址可以确定到底要访问用户地址还是内核地址,然后选择对应的TTBRx,找到对应的pgd基地址,而swapper_pg_dir 作为共享的内核地址空间,它的地址被写入TTBR1 中,且从不写入 TTBR0。
我们知道了要存放的pgd地址,那么在初始化时,还需要在对应的pgd项中配置上对应的PGD页表项内容才能使能MMU,为了获取内核地址空间的pgd offset,内核中定义了如下宏:
/* to find an entry in a page-table-directory */
#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)
如下的函数是用来创建内核地址空间映射页表的,它会通过上面的宏定义获取对应的地址,然后在地址上写入要映射的下一级页表的基地址。
/*
* 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) {
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->type == MT_ROM) &&
md->virtual >= PAGE_OFFSET && md->virtual < FIXADDR_START &&
(md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) {
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);
}
由此以来,我们可以一步一步完成内核的页表配置初始化。另外需要特别注意的是,这个init_mm结构体是会被设置到init_task中的avtive_mm上的,init_task是给swapper进程静态定义的task结构体,此进程是系统中的第一个进程,所以为了以后的进程调度,active_mm的功能是正常的,我们必须要给第一个进程赋值。
#define INIT_TASK(tsk) \
{ \
INIT_TASK_TI(tsk) \
.state = 0, \
.stack = init_stack, \
.usage = ATOMIC_INIT(2), \
.flags = PF_KTHREAD, \
.prio = MAX_PRIO-20, \
.static_prio = MAX_PRIO-20, \
.normal_prio = MAX_PRIO-20, \
.policy = SCHED_NORMAL, \
.cpus_allowed = CPU_MASK_ALL, \
.nr_cpus_allowed= NR_CPUS, \
.mm = NULL, \
.active_mm = &init_mm, \
.restart_block = { \
.fn = do_no_restart_syscall, \
}, \
......
内核地址空间使用的TTBR1作为页表基地址,而用户地址空间是TTBR0作为页表基地址,这样我们只需要配置内核页表后设置到TTBR1寄存器,后面再各个进程切换时,不对TTBR1做切换,即可共享这段内存配置,而用户空间地址,我们在进程切换是需要进行切换,这个切换是通过task_struct中的mm_struct成员来做的。