永久映射区域
在32bit系统中,内核部分的虚拟地址空间紧张,内核中不能直接线性映射所有的内存,所以有了高端内存的概念。高端内存(Highmem)中的页不能永久地映射到内核地址空间,因此通过alloc_page(__GFP_HIGHMEM)系列函数得到的内存页没有固定的虚拟地址,内核开发人员为其设计了永久映射区域来访问高端内存。而在64bit系统中虚拟地址非常大,完全可以线性映射所有的内存,已经没有高端内存的概念了,自然也就没有了固定映射的概念.
首先回顾一下内核的vmalloc区域,其一可以通过vmalloc从buddy中分配物理页帧并且在VMALLOC_START~VMALLOC_END
区间为其创建映射关系,物理页帧可以是不连续的;其二可以使用ioremap将设备寄存器映射到该区域而不需要物理内存。
我们为什么不可以将高端内存页映射到这个区域而是新设计一个专门的地址区域呢?从虚拟地址到物理地址的映射核心的一点是页表,当正确设置了页表之后通过MMU就可以访问物理内存,这是vmalloc和永久映射都必须做的。而vmalloc区域是通过vm_struct和vmap_area来管理的,对应着进程虚拟地址空间的管理结构:mm_struct和vma_area_struct.在第一次申请成功后只设置了init_mm中的页表,其他进程访问的时候会发生缺页异常,在异常处理中完成同步页表的工作。
vmalloc设计的时间比较早,但是它当初的初衷就是为了使用不连续物理内存和映射设备,本身的实现比较复杂,除了页表操作之外还维护了vmap_area来实现页表同步的工作,但是kmap()和kunmap()之间的程序使用这段时间非常短,基本上是在一个函数内完成map和unmap的工作
,所以不需要类似于ioremap这种的操作,单独划分了一小块虚拟地址区域.(这段纯属扒灰和推测)
实现原理
要永久映射一个给定的page结构到内核地址空间,可以使用:void *kmap(struct page *page)
,它不能连续映射多页只能拆分成单个页来完成映射。
它使用主内核页表中一个专门的页表,其页表地址存放在pkmap_page_table中,页表包含512项或1024项,因此内核一次最多访问2M或4M的高端内存。
pkmap_count是一个容量为LAST_PKMAP的整数数组,其中每个元素都对应一个持久映射页。
宏定义与关键变量定义:
pkmap_page_table:高端内存主内核页表中,一个用于永久内核映射的专用页表锁在的地址
LAST_PKMAP: 上述页表所含有的表项(512或者1024)
PKMAP_BASE:该页表所映射线性地址的start地址
pkmap_count:对页表项提供计数器的数组
page_address_htable:散列表,用于记录高端页框与永久内核映射的线性地址之间的关系
page_address_map:一个数据结构,包含指向页描述符的指针和分配给页框的线性地址;用于为高端内存的每个页框提供当前映射,它被包含在page_address_htable这个hansh表中
kmap
映射时会判断页帧是否属于高端内存,如果不是则内核可以通过线性映射的方式直接访问它,不需要再额外映射地址。而如果是页帧属于高端内存时,首先它会检查是否已经映射过地址了.它保存映射过的page信息到一个hash表中,根据page地址来查找虚拟地址会更加快,如果映射过地址就直接返回,否则map_new_virtual为page映射新的地址并返回.
-
map_new_virtual通过线性扫描查看是否有可用地址,它不是每次从0开始而是从last_pkmap_nr记录的位置,当每次last_pkmap_nr经历过一圈轮回后会通过flush_all_zero_pkmaps批量将所有的unmap过的页表项解除,这也是为什么kunmap中没有对页表项的操作.
-
如果当前没有可用的虚拟地址,调用者就将自己挂到等待队列中pkmap_map_wait,等待kunmap时将自身唤醒.
-
最后取到可用地址,则设置页表项并返回虚拟地址.
页帧是高端内存时则会在页表中找到空闲的地址进行映射,也就是set_pte.因为这段区间是非常有限的,很容易消耗光,当地址消耗完时它并没有直接返回错误而是将自己挂到一个等待队列中,等待其他地址释放时唤醒自己并继续进行映射,它是有肯可能引起睡眠的。
当不再需要映射时,应该解除映射void kunmap(struct page *page)
,但是里面没有直接撤销对应的页表项,因为修改页表项需要flush TLB操作,代价非常大,而是将撤销页表的操作放在了kmap->flush_all_zero_pkmaps()中,当没有可用的地址时会遍历所有的地址项逐个撤销已经不使用的页表项.
kunmap
kunmap中会检查当前页是否是高端内存,否则直接返回.如果是高端内存,首先找到page映射的虚拟地址,然后将虚拟地址对应引用计数-1,因为操作页表需要flush TLB操作,代价比较昂贵,所以此时没有更改页表项,而是在flush_all_zero_pkmaps集中清除unmap的页表项.
如果有进程正在等待空闲pkmap的地址,则唤醒等待队列中的任务.
固定映射
固定映射是在编译时就确定的地址空间,但是它对应的物理页可以是任意的.每一个固定地址中的项都是一页范围,这些地址的用途是固定的,这是该区间被设计的最主要的用途,内核的注释中:to have a constant address at compile time, but to set the physical address only in the boot process.
reflink:https://0xax.gitbooks.io/linux-insides/content/MM/linux-mm-2.html
先来看看关于固定映射的一些宏:
unsigned long __FIXADDR_TOP = 0xfffff000;
EXPORT_SYMBOL(__FIXADDR_TOP);
#define FIXADDR_TOP ((unsignedlong)__FIXADDR_TOP)
#define FIXADDR_SIZE (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_BOOT_SIZE (__end_of_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE)
固定映射地址在编译时对此类地址的处理类似于常数,内核一启动就为其分配了虚拟地址,内核会确保在上下文切换期间对应于固定映射的页表项不会从TLB刷出,所以在访问固定映射的内存时都是通过TLB高速缓存取得对应的物理地址,所以访问速度会比普通页面块。
固定映射的地址定义在fixed_addresses中,即这些虚拟地址的用途是固定的,但是后端的物理内存可以动态映射,更加类似于java类的接口.
fixmap的顶端地址是在编译时就已经确定的,之后的连续内存都是以PAGE_SIZE为单位分配给不同的用途,固定映射的地址和类别是线性映射的
#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
#define __virt_to_fix(x) ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)
临时映射
临时映射是相对于永久映射来说的,永久映射的api kmap()和kunmap()函数不能用于中断处理程序,因为它可能会睡眠,所以又发明了一个新的区间专门为中断上下文中使用高端内存而设计的:临时映射区间也叫原子映射。
临时映射可以在不能睡眠的地方,如中断处理程序中,因为获取映射时,绝对不会阻塞。
临时映射函数如下:
void *kmap_atomic(struct *page, enum kmtype type);
void kunmap_atomic(void *kvaddr, enum km_type type);
这个函数不会睡眠,因此可以用在中断上下文和其他不能重新调度的地方;它也禁止内核抢占,因为映射对每个处理器都是唯一的。
临时映射的虚拟地址位于fixmap中,在其中保留了KM_TYPE_NR*NR_CPUS*PAGE_SIZE
的地址,这样每个cpu有自己的地址,在map_unmap中间是禁止内核抢占的,因为映射对每个处理器都是唯一的,这样可以保证不会发生任务切换到不同的cpu2上访问到仍然是cpu1的地址,它是一种per-cpu地址.
临时映射应用在原子上下文,本身不能睡眠,也不能不unmap就退出,这样如果不发生多次中断嵌套,即不会重复进行kmap_atomic,每个cpu能够同时映射的地址是有限的,目前KM_TYPE_NR=21足够了.不过KM_TYPE_NR=3也就足够了,中断上半部,软中断上下文,进程上下文各有一个.
kmap_atomic为什么是原子性的?
里面的实现只是找到一个临时映射区间的idx, idx本身也是per-cpu的数据,这样每次使用时都是使用该cpu拥有的区间FIXMAP_START + FIX_KMAP_BEGIN + (idx * cpu)
,然后转换为fixmap的地址,然后就set_pte.
kunmap_atomic的过程基本上是相同的,只是将对应idx的页表项清除掉present标志位.
临时映射的原子性是通过充足的地址空间来保证的,在任意时间都有可用的地址空间,不需要等待资源释放