今天周六,因为老婆还要早起备课,我也就跟着早早起来了,九点半就起床了,闲来无事就看了一会代码,懒得写代码看代码总可以了吧,主要看了两块,都是关于内存的,一个是内核高端临时映射,一个是伙伴系统的per_cpu_pages中的冷热页面队列,一个一个说。
关于高端映射的原理我就不多说了,内核代码很清晰,基本就是在虚拟内存的高端为每个cpu都预留了一片区域:
enum fixed_addresses {
...
#ifdef CONFIG_HIGHMEM
FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */
FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1, //为每一个cpu都留一片区域
...
__end_of_fixed_addresses
};
enum km_type {
KM_BOUNCE_READ,
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_TYPE_NR //类型的数量
};
static void __init kmap_init(void)
{
unsigned long kmap_vstart;
kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN);
kmap_pte = kmap_get_fixmap_pte(kmap_vstart);
kmap_prot = PAGE_KERNEL;
}
void *kmap_atomic(struct page *page, enum km_type type)
{
enum fixed_addresses idx;
unsigned long vaddr;
inc_preempt_count();
if (!PageHighMem(page))
return page_address(page);
idx = type + KM_TYPE_NR*smp_processor_id(); //找到属于本cpu的那一片临时映射区域
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
set_pte(kmap_pte-idx, mk_pte(page, kmap_prot));
__flush_tlb_one(vaddr);
return (void*) vaddr;
}
上面的过程简单而又清晰,在内核代码中很多地方用到了KM_USER0/1的地方,说不能在中断上下文调用,这到底是为什么?而且在上面的代码可以看出来,映射的时候,根本不管这个地址是否已经有了映射,而是直接映射进去,那么可想而知,如果原来有映射的话,那么数据肯定就被破坏了,其实也不是破坏,而是访问者的页面被默默更换了,这可能会酿成事故,我们一般要求虚拟地址是页面独占的,也就是说一个虚拟地址必须对应仅一个页面,除了这个临时映射,别的映射都可以保证这一点,对于用户映射,如果不是因为缺页,那是不会映射新页面的,因此问题解决,对于内核一一映射,初始化时就映射好了,以后也不会改变,对于vmalloc动态映射,vm_struct链表可以保证一个地址如果已经被映射就不再会有别的映射,对于kmap映射的永久映射,last_pkmap_nr也会保证这一点,但是对于高端临时映射却没有任何保证,这该如何是好,这难道是内核的不周吗?如果这样想的话就错了,前面几种映射是有保证,但是这种保证的代价就是没有话你就必须等,对于不允许等待的操作或者快速操作,有必要提供一个特殊通道给它们用,就好比大型超市结账的地方都有快速区,你如果就买一瓶纯净水,那么就不必排队了,内核也是这样。快速通道就是专门为一些快速操作提供的,但是内核是不允许出错的,为了防止意外,比如不能保证没有两个快速操作同时需要映射,那么就要进行排队,而排队就意味着等待,这好像又回到了前面的那些映射方式,这似乎是一个怪圈,如果按照这种线性的思路考虑的话,就不可能找到一种截然不同的又不用等待又不会出错的解决方案,那么内核可以讲排队的纵向过程铺开成横向的多个窗口,也就有了上面的临时映射的设计方式,km_type中的就是这些窗口,用户当然可以随便使用,但是出了问题内核概不负责,因此把规则交给用户自觉遵守而不是靠内核来扶正,这就是最好的解决方案,这个方案不用等待,来了一个用户页面的映射,那么由于一个cpu一个时间只能有一个用户进程陷入内核,那么为每个cpu都提供一个km_type窗口的话所有问题就解决了,临时映射用户页面的时候,内核执行绪十分了解自己的行为,它将该页面映射到自己cpu的KM_USER0,或者KM_USER1,然后就可以安心的操作了,因为内核固定了只有操作用户页面的才可以映射到这两个窗口,这个进程之所以安心是因为它相信别的执行绪会遵守这个规定的,另外对它本身的限制就是它的操作必须快速而且不许睡眠,当然内核也不能抢占,这是靠递增抢占标志做到的。km_type窗口中的每一个都有自己的用途,用户只要按照规定使用就没有问题,这个特殊的快速通道确实起到了快速操作的作用。
关于伙伴系统的per_cpu_pages中的冷热页面队列前面我就研究过一段时间,如果是进程的页面释放,那么就释放到热队列中,如果是设备的页面,那么就释放到冷队列中,因为设备使用的仅仅是页面,比如DMA,它是不经过cpu的,因此页面数据就不会被cpu的cache缓存,其实很多硬件外设没有MMU,因此要求为硬件外设分配内存的时候必须为它指定物理地址,并且地址必须连续,把它想成实模式cpu就可以了。外设不被cpu缓存,那么外设的页面置入冷队列,这样在释放页面的时候,伙伴系统的热队列页面数量肯定比全部数量少,这样当有页面分配需要的时候,如果是cpu需要页面,那么直接从热队列分配,此时很可能数据还在cpu缓存中,效率会大大提高,即使你不用老页面的数据而是使用一个零页面,那么在memset(addr,0,PAGE_SIZE)的时候也会提高效率,因为cpu操作的是虚拟地址,总线上才是物理地址。页面总是顺序排列的,不同的是它的虚拟地址可能每次都不同,然而cpu最终访问的页面,此时已经过了mmu,cpu是以物理地址操作cache的而不是虚拟地址,因此缓存热页面才有意义。