一:引子
我们在前面分析过,在linux 内存管理中,内核使用3G —>4G 的 地址空间,总共1G 的大小。而且有一部份用来做非连续空间的物理映射(vmalloc ). 除掉这部份空间之外,只留下896M 大 小供内核映射到物理地址。通常,我们把物理地址超过896M 的区域称为高端内存。内核怎样去管理高 端内存呢?今天就来分析这个问题。
内核有三种方式管理高端内存。第一种是非连续映射。这我们在前面的vmalloc 中 已经分析过了,在vmalloc 中请求页面的时候,请求的是高端内存,然后映射到VMALLOC_START 与VMALLOC_END 之 间。这一过程不再赘述。第二种方式是永久内存映射。最后一种方式叫临时内核映射。
接下来,详细的分析一下第二种和第三种方式。对于第一种方式,我们在之前已经分析过了。
借鉴网上的一个图,来说明一下这三种方式的大概映射过程。
![](http://blogimg.chinaunix.net/blog/upfile2/080227152653.jpg)
二:永久内存映射
永久内存映射在内核的接口为:kmap()/kunmap(). 在 详细分析代码之前,有必须弄懂几个全局变量的含义:
PKMAP_BASE :永久映射空间的起始地址。永久映射空间为4M 。所以它最 多能映射4M /4K=1024 个 页面。
pkmap_page_table :永久映射空间对应的页目录。我们来看一下它的初始化:
pkmap_page_table = pte_offset_kernel(pmd_offset(pgd_offset_k
(PKMAP_BASE), PKMAP_BASE), PKMAP_BASE);
实际上它就是PKMAP_BASE 所 在的PTE
LAST_PKMAP :永久映射空间所能映射的页面数。在没有开启PAE 的 情况下被定义为1024
highmem_start_page :高端内存的起始页面
pkmap_count[PKMAP]: 每一项用来对应映射区域的引用计数。关于引用计数,有以下几种情 况:
为0 时: 说明映射区域可用。为1 时:映射区域不可用,因为自从它最后一次使用以来。TLB 还没有将它刷新
为N 时, 有N-1 个对象正在使用这个页面
last_pkmap_nr :在建立永久映射的时候,最后使用的序号
代码如下:
void *kmap(struct page *page)
{
// 可能引起睡眠。在永久映射区没有空闲地址的时候
might_sleep();
// 如果不是高端页面。那它在直接映射空间已经映射好了,直接计算即 可
if (page < highmem_start_page)
return page_address(page);
// 如果是高端页面。即在永久映射区为其分配地址
return kmap_high(page);
}
转到kmap_high():
void fastcall *kmap_high(struct page *page)
{
unsigned long vaddr;
spin_lock(&kmap_lock);
// 取页面地址
vaddr = (unsigned long)page_address(page);
// 如果页面还没有映射到线性地址,为它建立好映射
if (!vaddr)
vaddr = map_new_virtual(page);
// 有一个引用了,计数加1
pkmap_count[PKMAP_NR(vaddr)]++;
// 如果计数小于2 , 这种情况是无效的。
if (pkmap_count[PKMAP_NR(vaddr)] < 2)
BUG();
spin_unlock(&kmap_lock);
return (void*) vaddr;
}
map_new_virtual ()用于将一个page 映 射到永久映射区域。它的实现如下:
static inline unsigned long map_new_virtual(struct page *page)
{
unsigned long vaddr;
int count;
start:
count = LAST_PKMAP;
for (;;) {
// 从last_pkmap_nr 开 始搜索。大于LAST_PKMAP 时,又将它从0 开 始
// 其中LAST_PKMAP_MASK 被 定义为:(LAST_PKMAP-1)
last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
// 如果last_pkmap_nr 等 于0 ,也就是从头开始了
if (!last_pkmap_nr) {
// 扫描所有计数为1 的项,将它置为零。如果还有映射到页面。断开它的映射关系
flush_all_zero_pkmaps();
count = LAST_PKMAP;
}
// 如果计数为0 , 可用,就用它了,跳出循环
if (!pkmap_count[last_pkmap_nr])
break; /* Found a usable entry */
if (--count)
continue;
// 遍历了整个区都无可用区间,睡眠
{
DECLARE_WAITQUEUE(wait, current);
__set_current_state(TASK_UNINTERRUPTIBLE);
add_wait_queue(&pkmap_map_wait, &wait);
spin_unlock(&kmap_lock);
schedule();
remove_wait_queue(&pkmap_map_wait, &wait);
spin_lock(&kmap_lock);
/* Somebody else might have mapped it while we slept */
// 可能在睡眠的时候,其它进程已经映射好了,
if (page_address(page))
return (unsigned long)page_address(page);
// 重新开始
goto start;
}
}
// #define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
// 将序号转化为线性地址
vaddr = PKMAP_ADDR(last_pkmap_nr);
// 将线性地址映射到page
set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
// 将其引用计数置1
pkmap_count[last_pkmap_nr] = 1;
// 更新page 的 线性地址
set_page_address(page, (void *)vaddr);
return vaddr;
}
Kunmap() 的实现如下:
void kunmap(struct page *page)
{
// 不能在中断中
if (in_interrupt())
BUG();
// 如果不是高端页面,直接返回
if (page < highmem_start_page)
return;
// 清除掉映射关系
kunmap_high(page);
}
转入kunmap_high():
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();
// 将地址转换为序号
// #define PKMAP_NR(virt) ((virt-PKMAP_BASE) >> PAGE_SHIFT)
nr = PKMAP_NR(vaddr);
need_wakeup = 0;
// 计算引用计数
switch (--pkmap_count[nr]) {
case 0:
BUG();
case 1:
// 如果只有一个引用了,说明这页面是空闲的。看看是否有进程在等待
// 因为TLB 刷 新之后,会将其减1
need_wakeup = waitqueue_active(&pkmap_map_wait);
}
spin_unlock(&kmap_lock);
// 唤醒等待的进程
if (need_wakeup)
wake_up(&pkmap_map_wait);
}
三:临时内存映射
临时内存映射在内核中的接口为:kmap_atomic()/kunmap_atomic() 。 它映射的地址是从FIXADDR_START 到FIXADDR_TOP 的 区域。其中,每个cpu 都在里面占用了一段空间。
在内核中,enum fixed_addresses 表示各 种临时映射所占的序号。结构如下:
enum fixed_addresses {
FIX_HOLE,
FIX_VSYSCALL,
#ifdef CONFIG_X86_LOCAL_APIC
FIX_APIC_BASE, /* local (CPU) APIC) -- required for SMP or not */
#else
FIX_VSTACK_HOLE_1,
#endif
#ifdef CONFIG_X86_IO_APIC
FIX_IO_APIC_BASE_0,
FIX_IO_APIC_BASE_END = FIX_IO_APIC_BASE_0 + MAX_IO_APICS-1,
#endif
#ifdef CONFIG_X86_VISWS_APIC
FIX_CO_CPU, /* Cobalt timer */
FIX_CO_APIC, /* Cobalt APIC Redirection Table */
FIX_LI_PCIA, /* Lithium PCI Bridge A */
FIX_LI_PCIB, /* Lithium PCI Bridge B */
#endif
FIX_IDT,
FIX_GDT_1,
FIX_GDT_0,
FIX_TSS_3,
FIX_TSS_2,
FIX_TSS_1,
FIX_TSS_0,
FIX_ENTRY_TRAMPOLINE_1,
FIX_ENTRY_TRAMPOLINE_0,
#ifdef CONFIG_X86_CYCLONE_TIMER
FIX_CYCLONE_TIMER, /*cyclone timer register*/
FIX_VSTACK_HOLE_2,
#endif
FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */
FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
#ifdef CONFIG_ACPI_BOOT
FIX_ACPI_BEGIN,
FIX_ACPI_END = FIX_ACPI_BEGIN + FIX_ACPI_PAGES - 1,
#endif
#ifdef CONFIG_PCI_MMCONFIG
FIX_PCIE_MCFG,
#endif
__end_of_permanent_fixed_addresses,
/* temporary boot-time mappings, used before ioremap() is functional */
#define NR_FIX_BTMAPS 16
FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
FIX_BTMAP_BEGIN = FIX_BTMAP_END + NR_FIX_BTMAPS - 1,
FIX_WP_TEST,
__end_of_fixed_addresses
}
每一段序号都有自己的用途,例如APIC 用,IDT 用。FIX_KMAP_BEGIN 与FIX_KMAP_END 是分配给模块或者做做临时用途使用的。内核这样分配是为了保证同一个区不能有两上映射 关系。我们在后面可以看到,如果一个区已经映射到了一个物理页面。如果再在这个区上建立映射关系,就会把它以前的映射覆盖掉。所以,内核应该根据具体的用 途选择特定的序号,以免产生不可预料的错误。同时使用完临时映射之后应该立即释放当前的映射,这也是个良好的习惯.
FIX_KMAP_END 的大小被定义成:FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1 。 也就是FIX_KMAP_BEGIN 到FIX_KMAP_END 的 大小是KM_TYPE_NR*NR_CPUS.
KM_TYPE_NR 的定义如下:
enum km_type {
/*
* IMPORTANT: don't move these 3 entries, be wary when adding entries,
* the 4G/4G virtual stack must be THREAD_SIZE aligned on each cpu.
*/
KM_BOUNCE_READ,
KM_VSTACK_BASE,
KM_VSTACK_TOP = KM_VSTACK_BASE + STACK_PAGE_COUNT-1,
KM_LDT_PAGE15,
KM_LDT_PAGE0 = KM_LDT_PAGE15 + 16-1,
KM_USER_COPY,
KM_VSTACK_HOLE,
KM_SKB_SUNRPC_DATA,
KM_SKB_DATA_SOFTIRQ,
KM_USER0,
KM_USER1,
KM_BIO_SRC_IRQ,
KM_BIO_DST_IRQ,
KM_PTE0,
KM_PTE1,
KM_IRQ0,
KM_IRQ1,
KM_SOFTIRQ0,
KM_SOFTIRQ1,
KM_CRASHDUMP,
KM_UNUSED,
KM_TYPE_NR
}
在smp 系统中,每个CPU 都 有这样的一段映射区域
kmap_pte :FIX_KMAP_BEGIN 项所对应的页表项. 它的初始化如下:
#define kmap_get_fixmap_pte(vaddr) /
pte_offset_kernel(pmd_offset(pgd_offset_k(vaddr), (vaddr)), (vaddr))
void __init kmap_init(void)
{
kmap_pte = kmap_get_fixmap_pte(__fix_to_virt(FIX_KMAP_BEGIN));
}
#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
了解上述关系之后,可以看具体的代码了:
void *kmap_atomic(struct page *page, enum km_type type)
{
enum fixed_addresses idx;
unsigned long vaddr;
// 如果页面不是高端内存
inc_preempt_count();
if (page < highmem_start_page)
return page_address(page);
// 在smp 中 所对应的序号
idx = type + KM_TYPE_NR*smp_processor_id();
// 在映射断中求取序号所在的虚拟地址
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
#ifdef CONFIG_DEBUG_HIGHMEM
if (!pte_none(*(kmap_pte-idx)))
BUG();
#endif
// 根据页面属性建立不同的页面项. 并根据FIX_KMAP_BEGIN 的页表项,求出序 号所在的页表项
if (PageReserved(page))
set_pte(kmap_pte-idx, mk_pte(page, kmap_prot_nocache));
else
set_pte(kmap_pte-idx, mk_pte(page, kmap_prot));
// 在TLB 中 刷新这个地址
__flush_tlb_one(vaddr);
return (void*) vaddr;
}
我们在这个过程看中,并没有去判断一个区域有没有被映射。但这样也有一个好处,就是不会造成睡眠,因为它总有一个区 域可供其映射。与永久内核映射相比,速度显得稍微要快一点。
临时内核映射的断开接口为:kunmap_atomic ()
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);
#endif
dec_preempt_count();
preempt_check_resched();
}
我们在此看到,它并末对页面做特殊处理。
四总结:
其实,不管是那样的方式,原理都是一样的,都是在固定映射区外选定 一个地址,然后再修改PTE 项,使其指向相应的page 。 特别值得我们注意的是,因为kmap() 会引起睡眠,所以它不能用于中断处理。但每一种映射方式都 有自己的优点和缺点,这需要我们在写代码的时候仔细考虑了。