Linux 内存管理之 highmem
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. highmem 实现简析
2.1 什么是 highmem ?
当系统中的物理内存大小接近或超过最大可访问【内核虚拟地址空间】时,就需要使用到 highmem,此时内核无法一次性将所有物理内存映射到虚拟地址空间,这意味着内存要临时映射需要访问的部分物理内存。我们把没有被内核永久映射的那一部分物理内存,称为 highmem 。highmem 的分界线,在不同的硬件架构下各不相同。
那前面提到的最大可访问的虚拟地址空间由谁决定?答案是由硬件架构 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_table 在 paging_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 数据,可以在原子上下文调用(譬如中断上下文),本文它们也不再展开。

浙公网安备 33010602011771号