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



 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1271144

相关文章
|
17天前
|
安全 Java API
Java多线程编程:使用Atomic类实现原子操作
【4月更文挑战第6天】Java的`java.util.concurrent.atomic`包提供了一系列原子类,如`AtomicInteger`和`AtomicLong`,利用CPU原子指令保证无锁情况下变量更新的原子性,从而实现线程安全。这些类在高并发场景下能避免线程阻塞,提高性能。`AtomicInteger`和`AtomicLong`支持原子地增加、减少和设置值,而`AtomicReference`则适用于原子更新引用对象。尽管原子类具有非阻塞、线程安全和易用等优点,但它们仅保证单个变量的原子性,复杂操作可能仍需传统同步机制。了解其工作原理和局限性,有助于提升并发应用性能。
|
6月前
|
缓存 调度
Atomic 类
Atomic 类
37 0
|
1月前
|
存储 安全 算法
【C++ 包装器类 std::atomic 】全面入门指南:深入理解并掌握C++ std::atomic 原子操作 的实用技巧与应用
【C++ 包装器类 std::atomic 】全面入门指南:深入理解并掌握C++ std::atomic 原子操作 的实用技巧与应用
30 1
|
1月前
|
C语言 C++ 容器
C++智能指针 shared_ptr 精简 教程
C++智能指针 shared_ptr 精简 教程
17 0
|
1月前
|
安全 C++ 开发者
【C++多线程同步】C++多线程同步和互斥的关键:std::mutex和相关类的全面使用教程与深度解析
【C++多线程同步】C++多线程同步和互斥的关键:std::mutex和相关类的全面使用教程与深度解析
18 0
|
3月前
|
Go
动态并发控制:sync.WaitGroup的灵活运用
动态并发控制:sync.WaitGroup的灵活运用
27 0
动态并发控制:sync.WaitGroup的灵活运用
|
Linux
Linux系统编程-(pthread)线程的使用案例(分离属性、清理函数等)
这篇文章介绍Linux下线程的创建与基本使用案例,主要是案例代码为主;相关的函数详细介绍在上篇文章里已经介绍过了。
190 0
|
Java 程序员
5分钟搞清楚Synchronized和Lock的概念与区别
5分钟搞清楚Synchronized和Lock的概念与区别