原帖: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等函数。