kmap_atomic的细节以及改进

kmap_atomic用于高端内存映射,用于紧急的,短时间的映射,它没有使用任何锁,完全靠一个数学公式来避免混乱,它空间有限且虚拟地址固定,这意味着它映射的内存不能长期被占用而不被unmap,kmap_atomic在效率上要比kmap提升不少,然而它和kmap却不是用于同一场合的。不管怎么说,它的设计是很完美的。
     kernel可以在多个cpu上同时运行不同的task,然而它们共同使用一个内存地址空间,也就是说,地址空间对于多个cpu看到的是同一个,kmap_atomic使用的是地址空间顶部的一小段地址空间,内核逻辑将这一小段地址空间分成了若干个节,每一节的大小是一个page的大小,可以用来映射一个page,根据公用地址空间的原理,所有的cpu共同使用这些节,因此如何能保证N个cpu调用kmap_atomic不会将page映射到一个地址呢?这就是这个数学公式所起的作用:
idx = type + KM_TYPE_NR*smp_processor_id();
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
其中KM_TYPE_NR代表type的最大值加1:
enum km_type {
    KM_BOUNCE_READ,
    KM_SKB_SUNRPC_DATA,
    KM_SKB_DATA_SOFTIRQ,
    KM_USER0,
    KM_USER1,
...
    KM_TYPE_NR
};
调用kmap_atomic的时候,有一个参数就是上述的枚举类型km_type,这样不同的cpu得到的vaddr就不可能一样了,原因是这样的:type + KM_TYPE_NR*smp_processor_id()可以写成z=x+N*y(x<N),只要y不同,z就一定不会相同,因为z=x+N*y<N+N*y=(N+1)*y,设现有y1<y2(二者最少相差1),则若z1=z2,由于y1*N-y2*N>N,必有x1-x2>N,而x1和x2在0-N的范围内,因此这是不可能的,所以只要cpu不同,它们是不可能映射到同一虚拟地址的,也就是不会导致冲突的发生,最终通过__fix_to_virt(FIX_KMAP_BEGIN + idx)得到映射后的虚拟地址,该__fix_to_virt宏基本是一个常量转换,根据idx找到虚拟地址空间最高处的那个属于本次映射的小段。
     再看kmap_atomic这个api的原形:void *kmap_atomic(struct page *page, enum km_type type),有个参数是type,也就是说具体映射到哪一段是由调用者来决定的,可是这真的有必要吗?调用者无非需要的是一个虚拟地址而已,它不管一个page具体映射到哪个虚拟地址,就像kmap做的那样,原则上说,kmap_atomic提供的仅仅是底层的一个实现机制,一个接口,它完全可以用不同的方式实现,调用者实在没有必要牵扯进这个底层的细节问题,因此km_type是没有必要的,故而2.6.37内核中果断地去除了这个km_type,现如今2.6.37内核的kmap_atomic的实现如下:
void *kmap_atomic_prot(struct page *page, pgprot_t prot)
{
    unsigned long vaddr;
    int idx, type;
    pagefault_disable(); //原子映射是基于每cpu的,因此在当前cpu上禁用抢占,直到unmap的时候才开启,这样就不会导致原子映射的重入了,毕竟如果禁用抢占的话,调用者进程在开启抢占之前别的进程是不可能在内核空间运行,除非该进程在unmap之前睡眠,如果真的那样,别的进程就很可能在同一cpu重入kmap_atomic_prot了,然后就可能映射到同一虚拟地址(在当前2.6.37版本内部分解决了这个问题,见下面的push/pop),因此有人说原子映射期间进程不允许睡眠
    if (!PageHighMem(page)) //非高端页面直接返回一一映射地址
        return page_address(page);
    type = kmap_atomic_idx_push();  //递增一个每cpu变量,返回递增后的结果
    idx = type + KM_TYPE_NR*smp_processor_id();
    vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
    set_pte(kmap_pte-idx, mk_pte(page, prot)); //设置页表
    return (void *)vaddr;
}
static inline int kmap_atomic_idx_push(void)
{
    int idx = __get_cpu_var(__kmap_atomic_idx)++;
#ifdef CONFIG_DEBUG_HIGHMEM
    WARN_ON_ONCE(in_irq() && !irqs_disabled());
    BUG_ON(idx > KM_TYPE_NR);  //这个BUG_ON提醒__kmap_atomic_idx不能超过KM_TYPE_NR,原因同老版本的一样
#endif
    return idx;
}
可见新版本的原子映射中没有了km_type参数,只有一个page参数,完全靠一个stack实现了类似km_type的机制,在unmap的时候会调用__get_cpu_var(__kmap_atomic_idx)--将这个变量递减掉。看了这个新版本的实现之后,我们会发现,既然调用kmap_atomic_prot的时候禁用了抢占,如果进程不主动睡眠的话,在单一的cpu上__kmap_atomic_idx一般是不会大于1的,那么push中的BUG_ON当然就是杞人忧天了(除非使用一个原子映射期间又进行了另一个原子映射),然而如果原子映射的调用者睡眠了的话,谁也再来一个映射也不会和前面的重合,因为__kmap_atomic_idx此时递增了,然后这个进程也睡眠了,唤醒了原来的那个原子映射的调用者,该进程unmap了它的那个原子映射,很不巧,它释放的是第二个进程映射的页面...因此还是不要在使用原子映射之间睡眠。
     那么新版本的原子映射有没有什么缺点呢?kmap_atomic_idx_push和kmap_atomic_idx_pop都是函数,增加了几次函数调用并没有什么(它们都是inline的),最重要的是增加了几个变量的访问和操作--__kmap_atomic_idx

你可能感兴趣的:(struct,api,user)