一:引子
我们在前面分析过,在linux 内存管理中,内核使用3G —>4G 的 地址空间,总共1G 的大小。而且有一部份用来做非连续空间的物理映射(vmalloc ). 除掉这部份空间之外,只留下896M 大 小供内核映射到物理地址。通常,我们把物理地址超过896M 的区域称为高端内存。内核怎样去管理高 端内存呢?今天就来分析这个问题。
内核有三种方式管理高端内存。第一种是非连续映射。这我们在前面的vmalloc 中 已经分析过了,在vmalloc 中请求页面的时候,请求的是高端内存,然后映射到VMALLOC_START 与VMALLOC_END 之 间。这一过程不再赘述。第二种方式是永久内存映射。最后一种方式叫临时内核映射。
接下来,详细的分析一下第二种和第三种方式。对于第一种方式,我们在之前已经分析过了。
借鉴网上的一个图,来说明一下这三种方式的大概映射过程。
二:永久内存映射
永久内存映射在内核的接口为: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()
会引起睡眠,所以它不能用于中断处理。但每一种映射方式都
有自己的优点和缺点,这需要我们在写代码的时候仔细考虑了。
本文详细解析了Linux内核管理高端内存的三种方式:非连续映射、永久内存映射及临时内核映射,重点介绍了后两种映射的具体实现。
6616

被折叠的 条评论
为什么被折叠?



