深入淺出記憶體管理--頁表的建立
頁表的建立
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成員來做的。