Linux内存管理第九章 -- High Memory Management(高端内存)

Linux内存管理第九章 – High Memory Management

kernel仅仅能够直接访问那些已经建立了页表项的物理内存。但是在大多数情况下,在32位机器上,用户空间和内核空间被切割为3GB/1GB,这就意味着内核最多能够直接访问的内存为896MB。但是在64位机器上,这不是一个问题,因为64位机器上有足够的虚拟地址空间。
在很多32位机器的都超过了1GB的物理内存,内核如何访问超过896MB的物理内存是个不可忽视的问题。Linux使用的解决方案是将high memory zone中的物理内存临时映射到lower page table,至于如何映射,后面回来详细讨论。
high memory和IO有一个相互关联的问题:它们必须互相访问,因为不是所有设备都能访问high memory,不是所有memory都是对CPU可用的。举个例子:如果CPU的PAE扩展功能被打开了,硬件设别能够访问的内存大小是2GB或者使用64位系统。如果设备写入内存失败时最好的情况,最坏的时刻是干扰了内核的内存。解决该问题的办法是使用bounce buffer,后续会详细讨论。
本章从简洁地描述PKMap地址空间如何被管理开始,然后再来讨论page如何映射被取消映射到虚拟高端内存区域。然后再将一下映射的原子操作性进而深入到bounce buffer。最后再来讨论下在memory很紧张的情况下如何使用emergency pools。

Managing the PKMap Address Space

PKMap地址空间是从PKMAP_BASE到FIXADDR_START,该区间位于内核页表的top位置。该预留的空间非常有限。在x86上,PKMAP_BASE是在0xFE000000,而FIXADDR_START的值依赖于在编译阶段的配置选项,但该区域通常是线性地址空间尾部的一些pages大小。这也就是说只有少于32MB的页表空间用户将high memory映射到可用的虚拟地址空间。PKMap地址空间具体位置参考下图:
temp

Mapping High Memory Pages(临时映射)

用于映射high memory的API见下表。映射一个page的主要函数是kmap().对于不想要阻塞的用户来说,可以使用kmap_noblock(),对于不想在映射过程中被中断的用户可以使用kmap_atomic()。由于kmap pool非常小因此调用kmap()的使用者应尽可能快速再调用kumap()因为相比于low memory,high memory的size越大这个映射小窗口的压力就越大。

void * kmap(struct page *page)
将一个high memory的struct page映射到low memory,返回映射后的虚拟地址
void * kmap_nonblock(struct page *page)
功能和kmap()一样,当没有可用的slot的时候,该函数不会则塞
void * kmap_atomic(struct page *page, enum km_type type)
kmap空间中有些slot是以原子使用的方式给中断使用。该函数不会被鼓励使用因为调用者不会进入睡眠和调度状态。该函数用于将一个high memory page进行原子操作映射而达到特殊的目的

函数kmap()自身非常简单。它首先通过函数might_sleep()来检查调用它的调用者不是中断处理函数因为该函数可能再次被中断。然后再检查输入参数struct *page是否小于highmem_start_page因为低于这个值得struct *page已经是可见的了,不需要再次被映射。
如果检查发现struct *page已经处于low memory中,那么久简单地返回它所对应的虚拟地址。通过这样的方式就不要调用者自己来判断每个struct *page是否已经处于low memory中,无论怎么样,该函数都是安全的。
如果检查发现是一个high page需要被映射,那么就调用kmap_high()来开始做真正的工作。kmap_high()函数首先检查page->virtual是否为NULL,不为NULL,则表明已经被映射过了。如果为NULL,再调用map_new_virtual()来映射该page。

void *kmap(struct page *page) {
	might_sleep();
	if (page < highmem_start_page)
		return page_address(page);
	return kmap_high(page);
}
void fastcall *kmap_high(struct page *page)
{
	unsigned long vaddr;
	/*
	 * For highmem pages, we can't trust "virtual" until
	 * after we have the lock.
	 *
	 * We cannot call this from interrupts, as it may block
	 */
	spin_lock(&kmap_lock);
	vaddr = (unsigned long)page_address(page);
	if (!vaddr)
		vaddr = map_new_virtual(page);
	pkmap_count[PKMAP_NR(vaddr)]++;
	if (pkmap_count[PKMAP_NR(vaddr)] < 2)
		BUG();
	spin_unlock(&kmap_lock);
	return (void*) vaddr;
}

使用map_new_virtual()函数来创建一个新的虚拟映射是一个线性扫描pkmap_count的简单case。每次扫描的起始偏移为last_pkmap_nr而不是0是为防止前后两次kmap()中重复扫描相同的区域。当last_pkmap_nr接近到最大值时,将会调用flush_all_zero_pkmaps()先将pkmap_count数组中值为1的项(即没有人使用该page),将其对应的case页表项清除。
如果扫描一轮之后仍然没有空闲的entry,当前调用进程会被添加到pkmap_map_wait等待队列直到下一个kunmap()调用释放一个空闲entry。
也就是pkmap_count[ ]数组全部被用完了,先解除映射,然后再等待kunmap()到来。
一旦一个新的映射被创建,pkmap_count数组使用数也会增加然后返回对应的low memory虚拟地址。

#define PKMAP_NR(virt)  ((virt-PKMAP_BASE) >> PAGE_SHIFT)
//PKMAP_BASE - FIXADDR_START之间的地址整个page,一个page,一个page往后映射,
//将每个page的起始虚拟地址记录到struct page->virtual中
#define PKMAP_ADDR(nr)  (PKMAP_BASE + ((nr) << PAGE_SHIFT))

Unmapping Pages

解除一个high memory page映射的函数如下表。kumap()函数主要做两个检查:第一,判断该调用是否处于中断上下文,第二,检查page是否低于highmem_start_page,如果是,该函数已经处于low memory不需要做进一步处理。一旦需要将一个page解除映射,就调用kumap()来执行解除操作。

void kunmap(struct page *page)
将一个struct page从low memory中解除映射并释放它所映射的页表entry
void kunmap_atomic(void *kvaddr, enum km_type type)
将一个page原子性解除映射,即解除过程不会被中断

而kumap_high()的原理非常简单。将 pkmap_count对应的元素值减一。如果pkmap_count某个元素的值变为1,即该映射地址没有人再使用了,则pkmap_map_wait等待队列的进程将会被唤醒来使用这个空闲的slot。该page此时并不立即从页表中解除映射因为这需要刷新TLB,这个动作被推迟到flush_all_zero_pkmaps()时执行。

void fastcall kunmap_high(struct page *page)
{
	unsigned long vaddr;
	unsigned long nr;
	int need_wakeup;

	spin_lock(&kmap_lock);
	vaddr = (unsigned long)page_address(page);
	if (!vaddr)
		BUG();
	nr = PKMAP_NR(vaddr);

	/*
	 * A count must never go down to zero
	 * without a TLB flush!
	 */
	need_wakeup = 0;
	switch (--pkmap_count[nr]) {
	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.
		 */
		need_wakeup = waitqueue_active(&pkmap_map_wait);
	}
	spin_unlock(&kmap_lock);

	/* do wake-up, if needed, race-free outside of the spin lock */
	if (need_wakeup)
		wake_up(&pkmap_map_wait);
}

这也就是说,不用每次释放一个page就是删除一个页表项,而是先把这些释放的slot攒着,到快没有slot可用的时候再集中清空页表,刷新TLB。因为只要页表由变化就需要刷新TLB,而刷新TLB代价很大,不能频繁刷新。

Mapping High Memory Pages Atomically(固定映射)

kmap_atomic()不被鼓励使用但是当有必要的时候仍然为每个CPU预留一些slots,比如当bounce buffer在中断中被驱动设别使用。一种硬件架构需要很多不同的使用原子映射high memory的需求,这些需求用km_type所列举。全部kmap_atimic()需求数量为KM_TYPE_NR。在x86上,总共有6种不同的使用atomic kmaps的情况。
下图中标红色的位置就是固定映射区:
gyfd
系统在启动阶段在FIX_KMAP_BEGIN ~ FIX_KMAP_END中为每一个处理器预留KM_TYPE_NR个atomic mapping entry。因此很明显可以看出一个调用atomic kmap的用户不会进入睡眠或者退出直到自己调用kumap_atomic(),因为该处理器中的下一个进程可能也要用相同的entry但是会fail。
下面再看看具体的示意图:
kmap
下面来看下kmap_atomic()源码:

void *kmap_atomic(struct page *page, enum km_type type)
{
	enum fixed_addresses idx;
	unsigned long vaddr;

	/* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
	inc_preempt_count();
	if (page < highmem_start_page)
		return page_address(page);

	idx = type + KM_TYPE_NR*smp_processor_id();//计算每个CPU的映射类型为type的偏移
	vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);//从FIX_TOP 减掉(FIX_KMAP_BEGIN + idx)*PAGE_SHIFT 即为该固定映射的虚拟地址
#ifdef CONFIG_DEBUG_HIGHMEM
	if (!pte_none(*(kmap_pte-idx)))
		BUG();
#endif
	set_pte(kmap_pte-idx, mk_pte(page, kmap_prot));
	__flush_tlb_one(vaddr);

	return (void*) vaddr;
}

void kunmap_atomic(void *kvaddr, enum km_type type)
{
#ifdef CONFIG_DEBUG_HIGHMEM
	unsigned long vaddr = (unsigned long) kvaddr & PAGE_MASK;
	enum fixed_addresses idx = type + KM_TYPE_NR*smp_processor_id();
	if (vaddr < FIXADDR_START) { // FIXME
		dec_preempt_count();
		preempt_check_resched();
		return;
	}
	if (vaddr != __fix_to_virt(FIX_KMAP_BEGIN+idx))
		BUG();
	/*
	 * force other mappings to Oops if they'll try to access
	 * this pte without first remap it
	 */
	pte_clear(kmap_pte-idx);//清除页表项
	__flush_tlb_one(vaddr);//每次释放都刷新TLB中的特定页表项
#endif
	dec_preempt_count();
	preempt_check_resched();
}

Bounce Buffers

有一些设备不能够访问CPU可见的全部范围的内存空间,Bounce Buffers对于这些设备来说就非常重要了。一个非常明显的例子就是当一些硬件设备地址总线的位数比CPU的地址总线位数少的时候,例如:32位的设备跑在64位架构中,或者因特尔处理器开启PAE.

Bounce Buffers的基本概念非常简单。驻留在low memory中的Bounce Buffers只要地址够低就能够满足这些特殊设备从Bounce Buffers中读数据或者向Bounce Buffers中写数据,然后再将这些数据copy到high memory中。额外的以此copy是不期望看到的,但是确实无法避免的。在low memory中分配的pages作为page buffer供DMA向这些特殊设备来回传递数据。当IO完成之后,这些数据将被kernel copy到high memory的buffer page中,因此Bounce Buffers扮演了一种桥梁的作用。Bounce Buffers有很大的开销因为至少会引起整个page的拷贝,但是相比较于将low memory换出到磁盘的消耗来说,这也算不上很大的消耗。

Disk Buffering

通过slab allocator分配的struct buffer_head来管理pages中的大小约为1KB放入blocks。buffer_head的使用者拥有注册回调函数的选项。这个回调函数被存储在buffer_head->b_end_io()然后当IO完成的时候调用该回调函数。这种机制是bounce buffers自己将数据从bounce buffers拷贝出来,该回调函数为bounce_end_io_write()。
关于buffer heads其他特性或者buffer heads如何被block layer所使用超越了当前文章的范围。其余IO layer更加相关。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章