原帖:http://bbs.csdn.net/topics/390956135?page=1#post-398690481
进程的页表和页目录存储在内核空间还是用户空间?
A. 如果页表、页目录都在在内核空间的低端内存中,那么:
内核通过cr3能获得全局页目录中的物理地址,由于低端内存的线性映射,内核就能据此算出页目录的虚拟地址,进而实现对页目录的读写;同理,根据页目录中的内容可以获得页表的物理地址,如果页表也在内核空间的低端内存中,那么根据线性映射的偏移也能算出页表的虚拟地址,这样就能对页表进行读写。看似行的通,但是,
如果所有进程的页表都存放在低端内存,那进程数量很多时低端内存肯定不堪重负。
B. 如果页表在用户空间(或在内核空间的非线性映射区):
即使通过页目录读取到了页表的物理地址,由于内核只能通过虚拟地址对实际的内存进行访问,所以内核还是无法对页表中的内容进行读写,因为内核不知道页表的虚拟地址(这时已经不能用物理地址减去一个偏移量来计算虚拟地址了)。
翻了下源码,3.0.8版本的x86部分,找到些线索了:页目录好像确实要放在线性映射区,但页表却不一定。相关的代码如下:
/*
* Each physical page in the system has a struct page associated with
* it to keep track of whatever it is we are using the page for at the
* moment. Note that we have no way to track which tasks are using
* a page, though if it is a pagecache page, rmap structures can tell us
* who is mapping it.
*/
struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
atomic_t _count; /* Usage count, see below. */
...
...
/*
* On machines where all RAM is mapped into kernel address space,
* we can simply calculate the virtual address. On machines with
* highmem some memory is mapped into kernel virtual memory
* dynamically, so we need a place to store that address.
* Note that this field could be 16 bits on x86 ... ;)
*
* Architectures with slow multiplication can define
* WANT_PAGE_VIRTUAL in asm/page.h
*/
#if defined(WANT_PAGE_VIRTUAL) // WANT_PAGE_VIRTUAL这个宏会使能virtual成员
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */ // 这个virtual成员也许会play an important role
#endif /* WANT_PAGE_VIRTUAL */
...
...
};
// pmd_page宏先根据pmd的地址字段算出对应的物理页号,进而获得对应的page结构体指针
#define pmd_page(pmd) pfn_to_page((pmd_val(pmd) & PTE_PFN_MASK) >> PAGE_SHIFT)
// pte_offset_map宏根据一个页目录描述符指针和一个虚拟地址返回页描述符的指针(指向其虚拟地址)。
// 其中page_address用于根据一个page指针返回该物理页的虚拟地址,后面会有关于它的介绍。
// pte_index((address))从虚拟地址address中提取页索引字段
#define pte_offset_map(dir, address) \
((pte_t *)page_address(pmd_page(*(dir))) + pte_index((address)))
// page_addres (根据一个page指针返回该物理页的虚拟地址) 的实现方式如下:
// 实现方式一: 当WANT_PAGE_VIRTUAL这个宏存在时直接用page结构的virtual成员存储虚拟地址
#if defined(WANT_PAGE_VIRTUAL)
#define page_address(page) ((page)->virtual)
#define set_page_address(page, address) \
do { \
(page)->virtual = (address); \
} while(0)
#define page_address_init() do { } while(0)
#endif
// 实现方式二: 建一个哈希表存储虚拟地址到page的映射
#if defined(HASHED_PAGE_VIRTUAL)
void *page_address(struct page *page);
void set_page_address(struct page *page, void *virtual);
void page_address_init(void);
#endif
// 实现方式三: 不使用哈希表也不使能virtual成员时,(适用于系统中所有的memory都被线性映射到低端内存的情况),
// 直接用减偏移量的方法获得虚拟地址。
static __always_inline void *lowmem_page_address(struct page *page)
{
return __va(PFN_PHYS(page_to_pfn(page)));
}
#if !defined(HASHED_PAGE_VIRTUAL) && !defined(WANT_PAGE_VIRTUAL)
#define page_address(page) lowmem_page_address(page)
#define set_page_address(page, address) do { } while(0)
#define page_address_init() do { } while(0)
#endif
// 实现方式二即哈希表实现方式的细节
#if defined(HASHED_PAGE_VIRTUAL)
#define PA_HASH_ORDER 7
/*
* Describes one page->virtual association
*/
struct page_address_map { // 记录虚拟地址到page的映射的结构体
struct page *page;
void *virtual;
struct list_head list;
};
/*
* page_address_map freelist, allocated from page_address_maps.
*/
static struct list_head page_address_pool; /* freelist */
/*
* Hash table bucket
*/
static struct page_address_slot {
struct list_head lh; /* List of page_address_maps */
spinlock_t lock; /* Protect this bucket's list */
} ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];
/**
* page_address - get the mapped virtual address of a page
* @page: &struct page to get the virtual address of
*
* Returns the page's virtual address.
*/
void *page_address(struct page *page) // 从page获取虚拟地址的方法
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
if (!PageHighMem(page))
return lowmem_page_address(page);
pas = page_slot(page);
ret = NULL;
spin_lock_irqsave(&pas->lock, flags);
if (!list_empty(&pas->lh)) {
struct page_address_map *pam;
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
ret = pam->virtual;
goto done;
}
}
}
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
/**
* set_page_address - set a page's virtual address
* @page: &struct page to set
* @virtual: virtual address to use
*/
void set_page_address(struct page *page, void *virtual) // 讲page与虚拟地址建立映射的方法
{
unsigned long flags;
struct page_address_slot *pas;
struct page_address_map *pam;
BUG_ON(!PageHighMem(page));
pas = page_slot(page);
if (virtual) { /* Add */
BUG_ON(list_empty(&page_address_pool));
spin_lock_irqsave(&pool_lock, flags);
pam = list_entry(page_address_pool.next,
struct page_address_map, list);
list_del(&pam->list);
spin_unlock_irqrestore(&pool_lock, flags);
pam->page = page;
pam->virtual = virtual;
spin_lock_irqsave(&pas->lock, flags);
list_add_tail(&pam->list, &pas->lh);
spin_unlock_irqrestore(&pas->lock, flags);
} else { /* Remove */
spin_lock_irqsave(&pas->lock, flags);
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
list_del(&pam->list);
spin_unlock_irqrestore(&pas->lock, flags);
spin_lock_irqsave(&pool_lock, flags);
list_add_tail(&pam->list, &page_address_pool);
spin_unlock_irqrestore(&pool_lock, flags);
goto done;
}
}
spin_unlock_irqrestore(&pas->lock, flags);
}
done:
return;
}
#endif
楼上的代码贴的有点乱了,上面那一堆代码中核心是这一部分,其他所有的代码都是在解释这几行中出现的宏或函数:
// pte_offset_map宏根据一个页目录描述符指针和一个虚拟地址返回页描述符的指针(指向其虚拟地址)。
// 其中page_address用于根据一个page指针返回该物理页的虚拟地址,后面会有关于它的介绍。
// pte_index((address))从虚拟地址address中提取页索引字段
#define pte_offset_map(dir, address) \
((pte_t *)page_address(pmd_page(*(dir))) + pte_index((address)))
通过上面的代码可知,当获取到一个页目录描述符dir后,可以在不依赖内存的线性映射条件下对页表的内容(页描述符)进行访问(关键在于获取其虚拟地址),如楼上所说,内核提供了三种方式来实现page到虚拟地址的映射,只有第三种方式是依赖线性映射的。
所以,页表没有必要放到内核空间的低端内存中。
但是对于页目录,似乎必须放在内核空间。
struct page *follow_page(struct vm_area_struct *vma, unsigned long address,
unsigned int flags)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *ptep, pte;
spinlock_t *ptl;
struct page *page;
struct mm_struct *mm = vma->vm_mm;
...
...
pgd = pgd_offset(mm, address); // 获得全局页目录描述符
...
...
pud = pud_offset(pgd, address); // 获得上级页目录描述符
...
...
pmd = pmd_offset(pud, address); // 获得中级页目录描述符
...
...
ptep = pte_offset_map_lock(mm, pmd, address, &ptl); // 获得页描述符
pte = *ptep;
/* pte_offset_map_lock的定义如下:主要是对pte_offset_map的调用,
这个宏前面已经解释过了
#define pte_offset_map_lock(mm, pmd, address, ptlp) \
({ \
spinlock_t *__ptl = pte_lockptr(mm, pmd); \
pte_t *__pte = pte_offset_map(pmd, address); \ //
*(ptlp) = __ptl; \
spin_lock(__ptl); \
__pte; \
})*/
...
...
}
/*
* pgd_offset() returns a (pgd_t *)
* pgd_index() is used get the offset into the pgd page's array of pgd_t's;
*/
// mm_struct的pgd字段是全局页目录的虚拟地址,不是物理地址?
#define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))
/* to find an entry in a page-table-directory. */
// pgd_page_vaddr需要调用__va,可见是依赖线性映射的,__va的定义如下:
// #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address)
{
return (pud_t *)pgd_page_vaddr(*pgd) + pud_index(address);
}
static inline unsigned long pgd_page_vaddr(pgd_t pgd)
{
return (unsigned long)__va((unsigned long)pgd_val(pgd) & PTE_PFN_MASK);
}
/* Find an entry in the second-level page table.. */
// pud_page_vaddr需要调用__va,可见是依赖线性映射的:
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address)
{
return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(address);
}
static inline unsigned long pud_page_vaddr(pud_t pud)
{
return (unsigned long)__va((unsigned long)pud_val(pud) & PTE_PFN_MASK);
}
// 全局页目录、二级页目录和三级页目录的访问(即获取其虚拟地址)都依赖地址的线性映射,因此课件
// 这三级目录表都应放在内核空间的低端内存中。只有最后一级目录即页表本身可以不在低端内存中。
关于页表可以不在低端内存中的说法,另一个证据是:(ULK3 Section 2.5 Paging in Linux中的一个截图)
如果页表保存在高端内存,内核会建立一个临时内核映射去访问它。pte_unmap用于释放由pte_offset_map建立的映射,这两个一般要成对使用,当使用完由pte_offset_map建立的地址映射后,要及时unmap。因为有的平台上pte_offset_map是靠kmap_atomic实现的。
另外,跟踪一下 pgd_alloc \ pud_alloc \ pmd_alloc \ pte_alloc_map \ pte_alloc_kernel 等这些函数是有帮助的。可以看到其内部会调用 __get_free_pages、alloc_pages等函数。