1. 程式人生 > >深入淺出記憶體管理--頁表的建立

深入淺出記憶體管理--頁表的建立

頁表的建立

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成員來做的。