Linux高端内存映射(上)

高端内存概述

在32位的系统上,内核占有从第3GB~第4GB的线性地址空间,共1GB大小,内核将其中的前896MB与物理内存的0~896MB进行直接映射,即线性映射,将剩余的128M线性地址空间作为访问高于896M的内存的一个窗口。引入高端内存映射这样一个概念的主要原因就是我们所安装的内存大于1G时,内核的1G线性地址空间无法建立一个完全的直接映射来触及整个物理内存空间,而对于80x86开启PAE的情况下,允许的最大物理内存可达到64G,因此内核将自己的最后128M的线性地址空间腾出来,用以完成对高端内存的暂时性映射。而在64位的系统上就不存在这样的问题了,因为可用的线性地址空间远大于可安装的内存。下图描述了内核1GB线性地址空间是如何划分的

其中可以用来完成上述映射目的的区域为vmalloc area,Persistent kernel mappings区域和固定映射线性地址空间中的FIX_KMAP区域,这三个区域对应的映射机制分别为非连续内存分配,永久内核映射和临时内核映射。

永久内核映射

在内核初始化页表管理机制时,专门用pkmap_page_table这个变量保存了PKMAP_BASE对应的页表项的地址,由pkmap_page_table来维护永久内核映射区的页表项的映射,页表项总数为LAST_PKMAP个,具体可以看前面关于页表机制初始化的博文。这里的永久并不是指调用kmap()建立的映射关系会一直持续下去无法解除,而是指在调用kunmap()解除映射之间这种映射会一直存在,这是相对于临时内核映射机制而言的。

内核用一个pkmap_count数组来记录pkmap_page_table中每一个页表项的使用状态,其实就是为每个页表项分配一个计数器来记录相应的页表是否已经被用来映射。计数值分为以下三种情况:

计数值为0:对应的页表项没有映射高端内存,即为空闲可用的

计数值为1: 对应的页表项没有映射高端内存,但是不可用,因为上次映射后对应的TLB项还未被淸刷

计数值为n(n>1):对应的页表项已经映射了一个高端内存页框,并且有n-1个内核成分正在利用这种映射关系

下面结合代码进行具体的分析,先通过alloc_page(__GFP_HIGHMEM)分配到了一个属于高端内存区域的page结构,然后调用kmap(struct page*page)来建立与永久内核映射区的映射,需要注意一点的是,当永久内核映射区没有空闲的页表项可供映射时,请求映射的进程会被阻塞,因此永久内核映射请求不能发生在中断和可延迟函数中。

void *kmap(struct page *page)
{
	might_sleep();
	if (!PageHighMem(page))/*页框属于低端内存*/
		return page_address(page);/*返回页框的虚拟地址*/
	return kmap_high(page);
}

如果页框是属于高端内存的话,则调用kmap_high()来建立映射

void *kmap_high(struct page *page)
{
	unsigned long vaddr;

	/*
	 * For highmem pages, we can't trust "virtual" until
	 * after we have the lock.
	 */
	lock_kmap();/*获取自旋锁防止多处理器系统上的并发访问*/
	
	/*试图获取页面的虚拟地址,因为之前可能已经有进程为该页框建立了到永久内核映射区的映射*/
	vaddr = (unsigned long)page_address(page);
	
	/*虚拟地址不存在则调用map_new_virtual()为该页框分配一个虚拟地址,完成映射*/
	if (!vaddr)
		vaddr = map_new_virtual(page);
	pkmap_count[PKMAP_NR(vaddr)]++;/*相应的页表项的计数值加1*/
	BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
	unlock_kmap();
	return (void*) vaddr;
}


如果该页框之前没被映射到永久内核映射区,则要通过map_new_virtual()函数在永久内核映射区对应的页表中找到一个空闲的表项来映射这个页框,简单的说就是为这个页框分配一个线性地址。

static inline unsigned long map_new_virtual(struct page *page)
{
	unsigned long vaddr;
	int count;

start:
	/*LAST_PKMAP为永久映射区可以映射的页框数,在禁用PAE的情况下为512,开启PAE的情况下为1024,
	也就是说内核通过kmap,一次最多能映射2M/4M的高端内存*/
	count = LAST_PKMAP;
	/* Find an empty entry */
	for (;;) {
		/*last_pkmap_nr记录了上次遍历pkmap_count数组找到一个空闲页表项后的位置,首先从
		last_pkmap_nr出开始遍历,如果未能在pkmap_count中找到计数值为0的页表项,则last_pkmap_nr
		和LAST_PKMAP_MASK相与后又回到0,进行第二轮遍历*/
		last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;

		/*last_pkmap_nr变为了0,也就是说第一次遍历中未能找到计数值为0的页表项*/
		if (!last_pkmap_nr) {
			flush_all_zero_pkmaps();
			count = LAST_PKMAP;
		}
		if (!pkmap_count[last_pkmap_nr])/*找到一个计数值为0的页表项,即空闲可用的页表项*/
			break;	/* Found a usable entry */
		if (--count)
			continue;

		/*
		 * Sleep for somebody else to unmap their entries
		 */
		 /*在pkmap_count数组中,找不到计数值为0或1的页表项,即所有页表项都被内核映射了,
		    则声明一个等待队列,并将当前要求映射高端内存的进程添加到等待队列中然后
		    阻塞该进程,等待其他的进程释放了KMAP区的某个页框的映射*/
		{
			DECLARE_WAITQUEUE(wait, current);

			__set_current_state(TASK_UNINTERRUPTIBLE);
			add_wait_queue(&pkmap_map_wait, &wait);
			unlock_kmap();
			schedule();
			remove_wait_queue(&pkmap_map_wait, &wait);
			lock_kmap();

			/* Somebody else might have mapped it while we slept */
			/*在睡眠的时候,可能有其他的进程映射了该页面,所以先试图获取页面的虚拟地址,成功的话直接返回*/    			                  if(page_address(page))
			    return (unsigned long)page_address(page);

			/* Re-start */
			goto start;/*唤醒后重新执行遍历操作*/
		}
	}
	/*寻找到了一个未被映射的页表项,获取该页表项对应的线性地址并赋给vaddr*/
	vaddr = PKMAP_ADDR(last_pkmap_nr);
	/*将pkmap_page_table中对应的pte设为申请映射的页框的pte,完成永久内核映射区中的页表项到物理页框的映射*/
	set_pte_at(&init_mm, vaddr,
		   &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));

	pkmap_count[last_pkmap_nr] = 1;
	/*设置页面的虚拟地址,将该页面添加到page_address_htable散列表中*/
	set_page_address(page, (void *)vaddr);

	return vaddr;
}


当第一趟遍历pkmap_count数组找不到计数值为0的页表项时,就要调用flush_all_zero_pkmaps(),将计数值为1的页表项置为0,撤销已经不用了的映射,并且刷新TLB

static void flush_all_zero_pkmaps(void)
{
	int i;
	int need_flush = 0;

	flush_cache_kmaps();

	for (i = 0; i < LAST_PKMAP; i++) {
		struct page *page;

		/*
		 * zero means we don't have anything to do,
		 * >1 means that it is still in use. Only
		 * a count of 1 means that it is free but
		 * needs to be unmapped
		 */
		 /*将计数值为1的页面的计数值设为0,*/
		if (pkmap_count[i] != 1)
			continue;
		pkmap_count[i] = 0;

		/* sanity check */
		BUG_ON(pte_none(pkmap_page_table[i]));

		/*
		 * Don't need an atomic fetch-and-clear op here;
		 * no-one has the page mapped, and cannot get at
		 * its virtual address (and hence PTE) without first
		 * getting the kmap_lock (which is held here).
		 * So no dangers, even with speculative execution.
		 */
		 /*撤销之前的映射关系,并将page从page_address_htable散列表中删除*/
		page = pte_page(pkmap_page_table[i]);
		pte_clear(&init_mm, (unsigned long)page_address(page),
			  &pkmap_page_table[i]);

		set_page_address(page, NULL);
		need_flush = 1;
	}
	if (need_flush)/*刷新TLB*/
		flush_tlb_kernel_range(PKMAP_ADDR(0), PKMAP_ADDR(LAST_PKMAP));
}

在map_new_virtual()中,当找到一个空闲页表后,还要调用set_page_address()将page与该页表项对应的线性地址进行关联,这里并不是简单的page结构中的virtual给赋上相应的值,而是将该映射的page添加到page_address_htable散列表中,该散列表维护着所有被映射到永久内核映射区的页框,散列表中的每一项记录了页框的page结构地址和映射页框的线性地址。

void set_page_address(struct page *page, void *virtual)
{
	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);
		/*从page_address_pool中得到一个空闲的page_address_map*/
		pam = list_entry(page_address_pool.next,
				struct page_address_map, list);
		list_del(&pam->list);/*将该节点从page_address_pool中删除*/
		spin_unlock_irqrestore(&pool_lock, flags);

		/*将页框的page结构地址和虚拟地址保存到page_address_map中,可以看到并没有直接设定page的虚拟地址*/
		pam->page = page;
		pam->virtual = virtual;

		spin_lock_irqsave(&pas->lock, flags);
		list_add_tail(&pam->list, &pas->lh);/*将pam添入散列表*/
		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;
}



弄清楚了kmap()为页框建立永久内核映射,那么释放映射的话就容易理解了,当我们需要释放页框的映射时,调用kunmap()函数

void kunmap(struct page *page)
{
	if (in_interrupt())
		BUG();
	if (!PageHighMem(page))/*页框处于低端内存则直接返回*/
		return;
	kunmap_high(page);
}
void kunmap_high(struct page *page)
{
	unsigned long vaddr;
	unsigned long nr;
	unsigned long flags;
	int need_wakeup;

	lock_kmap_any(flags);
	vaddr = (unsigned long)page_address(page);
	BUG_ON(!vaddr);
	nr = PKMAP_NR(vaddr);

	/*
	 * A count must never go down to zero
	 * without a TLB flush!
	 */
	need_wakeup = 0;
	switch (--pkmap_count[nr]) {/*该页面对应的页表项计数值减1*/
	case 0:
		BUG();
	case 1:
		/*
		 * Avoid an unnecessary wake_up() function call.
		 * The common case is pkmap_count[] == 1, but
		 * no waiters.
		 * The tasks queued in the wait-queue are guarded
		 * by both the lock in the wait-queue-head and by
		 * the kmap_lock.  As the kmap_lock is held here,
		 * no need for the wait-queue-head's lock.  Simply
		 * test if the queue is empty.
		 */
		/*确定pkmap_map_wait等待队列是否为空*/
		need_wakeup = waitqueue_active(&pkmap_map_wait);
	}
	unlock_kmap_any(flags);

	/* do wake-up, if needed, race-free outside of the spin lock */
	if (need_wakeup)/*等待队列不为空则唤醒其中的进程*/
		wake_up(&pkmap_map_wait);
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值