进程的页表和页目录存储在内核空间还是用户空间?低端内存还是高端内存

版权声明:本文为博主原创文章,欢迎转载(文中有强调禁止转载的例外),但请说明出处。如用于培训等商业用途请先与博主联系并支付部分授权费 https://blog.csdn.net/NewThinker_wei/article/details/42089707

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


猜你喜欢

转载自blog.csdn.net/NewThinker_wei/article/details/42089707