Linux 内存管理之 highmem

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. highmem 实现简析

2.1 什么是 highmem ?

当系统中的物理内存大小接近或超过最大可访问【内核虚拟地址空间】时,就需要使用到 highmem,此时内核无法一次性将所有物理内存映射到虚拟地址空间,这意味着内存要临时映射需要访问的部分物理内存。我们把没有被内核永久映射的那一部分物理内存,称为 highmemhighmem 的分界线,在不同的硬件架构下各不相同。

那前面提到的最大可访问的虚拟地址空间由谁决定?答案是由硬件架构 MMU 使用的虚拟地址位数来决定,如硬件架构的 MMU 使用 32 位虚拟地址,则最大可访问的虚拟地址空间2 ^ 32 = 4GiB

2.2 使用 highmem

2.1 小节提到,内核通过临时映射的方式访问 highmem,内核导出接口 kmap_high() 建立 highmem 页面的临时映射,导出接口 kunmap_high() 取消 kmap_high() 建立的临时映射。其它的 highmem 接口都是这两个接口的变种。

/* include/linux/highmem.h */

#ifdef CONFIG_HIGHMEM
#include <asm/highmem.h> /* `highmem` 和硬件架构相关,硬件架构负责实现具体的逻辑 */

...

#else /* CONFIG_HIGHMEM */

...

#endif /* CONFIG_HIGHMEM */

ARM32 架构的实现为例:

/* arch/arm/include/asm/highmem.h */

...

/* highmem 接口核心实现 */
extern void *kmap_high(struct page *page);
extern void kunmap_high(struct page *page);

/*
 * The following functions are already defined by <linux/highmem.h>
 * when CONFIG_HIGHMEM is not set.
 */
/* 接口 kmap_high(), kunmap_high() 的变种接口 */
#ifdef CONFIG_HIGHMEM
extern void *kmap(struct page *page);
extern void kunmap(struct page *page);
extern void *kmap_atomic(struct page *page);
extern void __kunmap_atomic(void *kvaddr);
extern void *kmap_atomic_pfn(unsigned long pfn);
#endif

先看 kmap_high(), kunmap_high() 的实现:

/* mm/highmem.c */

/**
 * kmap_high - map a highmem page into memory
 * @page: &struct page to map
 *
 * Returns the page's virtual memory address.
 *
 * We cannot call this from interrupts, as it may block.
 */
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);
	if (!vaddr) /* 新分配的 highmem 物理内存 @page 初始没有映射 虚拟地址 */
		vaddr = map_new_virtual(page);  /* 将 物理内存 @page 映射到 虚拟地址空间 */
	pkmap_count[PKMAP_NR(vaddr)]++;
	BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
	unlock_kmap();
	return (void*) vaddr;
}

static inline unsigned long map_new_virtual(struct page *page)
{
	unsigned long vaddr;
	int count;
	unsigned int last_pkmap_nr;
	unsigned int color = get_pkmap_color(page);

	start:
	count = get_pkmap_entries_count(color);
	/* Find an empty entry */
	for (;;) {
		last_pkmap_nr = get_next_pkmap_nr(color);
		if (no_more_pkmaps(last_pkmap_nr, color)) {
			flush_all_zero_pkmaps();
			count = get_pkmap_entries_count(color);
		}
		if (!pkmap_count[last_pkmap_nr])
			break;	/* Found a usable entry */
		if (--count)
			continue;

		/*
		 * Sleep for somebody else to unmap their entries
		 */
		{
			DECLARE_WAITQUEUE(wait, current);
			wait_queue_head_t *pkmap_map_wait =
				get_pkmap_wait_queue_head(color);

			__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;
		}
	}
	/* 计算 PKMAP 页面的虚拟地址 */
	vaddr = PKMAP_ADDR(last_pkmap_nr);
	/* 配置 PKMAP 页面的 PTE 表项: 将 highmem 物理页面 @page 映射到虚拟地址 @vaddr */
	set_pte_at(&init_mm, vaddr,
		   &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));

	pkmap_count[last_pkmap_nr] = 1;
	/*
	 * 记录 highmem 物理页面 @page 的虚拟地址:
	 * . 如果没定义 WANT_PAGE_VIRTUAL,记录到哈希表 page_address_maps[]
	 * . 如果定义了 WANT_PAGE_VIRTUAL,记录到页面的 page->virtual 成员变量
	 * 然后可以通过 page_address() 快速获取 highmem 物理页面 @page 的虚拟地址。
	 */
	set_page_address(page, (void *)vaddr);

	return vaddr;
}

可以看到,highmem 使用 PKMAP 虚拟地址区间 PTE 页表 pkmap_page_table 进行映射。PTE 页表 pkmap_page_tablepaging_init() 接口中分配创建:

start_kernel()
	setup_arch()
		paging_init()
			kmap_init()

/*
 * 为 PKMAP 和 FIXMAP 区间分配 PTE 页表,并将分配的 PTE 页表物理地址 设置到对应的 PMD 表项 。
 */
static void __init kmap_init(void)
{
#ifdef CONFIG_HIGHMEM
	/*
	 * 为 PKMAP_BASE 虚拟地址空间 分配 PTE 页表,并把 新分配 PTE 页表 的 物理地址 
	 * 填充到 PKMAP_BASE 虚拟地址 对应的 PMD 页表项 pmd_off_k(PKMAP_BASE).
	 *
	 * 记录 虚拟地址 PKMAP_BASE 的 PTE 页表项指针 (虚拟地址) 到 pkmap_page_table.
	 */
	pkmap_page_table = early_pte_alloc(pmd_off_k(PKMAP_BASE),
		PKMAP_BASE, _PAGE_KERNEL_TABLE);
#endif

	/* 为 FIXMAP 分配 PTE 页表 */
	early_pte_alloc(pmd_off_k(FIXADDR_START), FIXADDR_START,
			_PAGE_KERNEL_TABLE);
}

最后,set_page_address() 记录 highmem 物理页面 page 的虚拟地址。在定义了 WANT_PAGE_VIRTUAL 宏的情形下,记录到 struct page 页面对象:

/*
 * include/linux/mm_types.h
 */
struct page {
	...
#if defined(WANT_PAGE_VIRTUAL)
	void *virtual;			/* Kernel virtual address (NULL if
					   not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
	...
};

/*
 * include/linux/mm.h
 */
#if defined(WANT_PAGE_VIRTUAL)
static inline void *page_address(const struct page *page)
{
	return page->virtual;
}
static inline void set_page_address(struct page *page, void *address)
{
	page->virtual = address;
}
#define page_address_init()  do { } while(0)
#endif

否则,记录到独立的哈希表 page_address_maps

/* mm/highmem.c */

/**
 * 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)
{
	unsigned long flags;
	struct page_address_slot *pas;
	struct page_address_map *pam;

	BUG_ON(!PageHighMem(page));

	pas = page_slot(page);
	if (virtual) {		/* Add */
		pam = &page_address_maps[PKMAP_NR((unsigned long)virtual)];
		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);
				goto done;
			}
		}
		spin_unlock_irqrestore(&pas->lock, flags);
	}
done:
	return;
}

相应的 page_address()page_address_init() 的实现也围绕哈希表 page_address_maps 展开:

/* 返回 @page 在 哈希表 中 对应的 哈希链 */
static struct page_address_slot *page_slot(const struct page *page)
{
	return &page_address_htable[hash_ptr(page, 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.
 */
/* 获取 @page 映射的虚拟地址 */
void *page_address(const struct page *page)
{
	unsigned long flags;
	void *ret;
	struct page_address_slot *pas;

	if (!PageHighMem(page))
		return lowmem_page_address(page);

	pas = page_slot(page); /* 返回 @page 在 哈希表 中 对应的 哈希链 */
	ret = NULL;
	spin_lock_irqsave(&pas->lock, flags);
	if (!list_empty(&pas->lh)) { /* 遍历哈希链 @pas, 找到 @page, 然后提取其 虚拟地址 */
		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; /* 返回 @page 映射的虚拟地址 */
}

EXPORT_SYMBOL(page_address);

void __init page_address_init(void)
{
	int i;

	for (i = 0; i < ARRAY_SIZE(page_address_htable); i++) {
		INIT_LIST_HEAD(&page_address_htable[i].lh);
		spin_lock_init(&page_address_htable[i].lock);
	}
}

kunmap_high()kmap_high() 的逆实现,本文不做展开。而 kmap(), kunmap(), kmap_atomic(), __kunmap_atomic(), kmap_atomic_pfn() 都是对 kunmap_high()kmap_high() 的封装,唯一特殊的是带 atomic 的接口,通过禁用抢占page fault,并引入每 CPU__kmap_atomic_idx 数据,可以在原子上下文调用(譬如中断上下文),本文它们也不再展开。

3. 参考资料

[1] High Memory Handling

posted @ 2025-07-12 15:40  JiMoKuangXiangQu  阅读(11)  评论(0)    收藏  举报