要想一个系统不崩溃,性能还得好,同步技术是非常关键的。但是,完全避免竞态条件几乎是难于上青天。因为它要求对内核各个功能模块之间的交互得有一个清晰深刻的理解。下面我们看一下Linux内核中一些具体保护数据访问的示例,加深对其理解,甚至可以在自己的内核设计上借鉴一下。
1 引用计数器
引用计数器是内核中保护某个资源或者模块的一种有效手段,比如分配内存,使用某个内核模块,或者打开某个文件的时候。它是一个atomic_t类型的原子变量。当内核中某个程序访问该资源的时候,计数器加1,当内核程序释放资源,计数器减1。当计数器的值为0时,它就可以被释放了。
2 大内核锁
关于这部分请参阅网友universus写的这篇文章-大内核锁将何去何从。我觉得写得还是非常详细的。
3 内存描述符读写信号量
每个内存描述符都可以使用数据结构mm_struct进行表达,它有一个成员mmap_sem,专门用来保护该描述符避免竞态条件的发生。因为每个内存描述符可以被几个轻量级进程共享。这是用户态多线程共享内存的硬件基础。
假设内核需要为某个进程创建或扩展一段内存区域。为此,调用do_mmap()函数,分配一个新的类型为vm_area_struct虚拟内存给进程。在这个过程中,如果已经没有内存可用,且每段内存都有一个信号量保护,所以,当前进程挂起,其它进程还可以正常访问他们的共享内存继续运行。但是,如果没有信号量保护,当前进程申请内存就会成功(其实可能占用了其它进程的内存)。而与当前进程共享内存的进程就会请求访问内存描述符(比如,写时复制(Copy on Write)导致的页错误),从而导致严重的数据损坏。
此处一般使用的是读/写信号量,因为大部分的内核函数,比如页错误异常处理程序只需要查看内存描述符,不会修改它。这样可以提高系统的并发性能。
4 Slab Cache列表信号量
slab是一种Linux内核内存分配算法,slab分配算法采用cache存储内核对象。这些对象的描述符使用一个列表进行管理。这个列表使用一个称为cache_chain_sem的信号量进行保护,从而对列表进行独占访问。
因为往这个列表中插入新对象的同时,kmem_cache_shrink()和kmem_cache_reap()会扫描这个列表,这就带来了竞态条件的发生。当然了,中断不会调用这些函数,所以不需要信号量。所以,主要是在支持内核抢占的多核和单核系统中起作用。所以选择信号量而不是自旋锁。