深入浅出内存管理--页表的创建

页表的创建

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成员来做的。

你可能感兴趣的:(内核笔记,深入浅出内存管理)