一些避免竞争条件的实例

人们总是期望内核开发者确定和解决由内核控制路径的交错执行所引起的同步问题。但是,避免竞争条件是一项艰巨的任务,因为这需要对内核的各个成分如何相互作用有一个清楚的理解。为了直观地认识内核内部到底是什么样子,需要提及前面博文中所定义同步技术的几种典型应用场景。

1 引用计数器


引用计数器广泛地用在内核中以避免由于资源的并发分配和释放而产生的竞争条件。引用计数器(reference counter)只不过是一个atomic_t计数器,与特定的资源,如内存页、模块或文件相关。当内核控制路径开始使用资源时就原子地减少计数器的值,当内核控制路径使用完资源时就原子地增加计数器。当引用计数器变为0时,说明该资源未被使用,如果必要,就释放该资源。

2 大内核锁


在早期的Linux内核版本中,大内核锁(big kernel block,也叫全局内核锁或BKL)被广泛使用。在2.0版本中,这个锁是相对粗粒度的自旋锁,确保每次只有一个进程能运行在内核态。2.2和2.4内核具有极大的灵活性,不再依赖一个单独的自旋锁,而是由许多不同的自旋锁保护大量的内核数据结构。在Linux 2.6版本的内核中,用大内核锁来裸护旧的代码(绝大多数主要是与VFS和几个文件系统相关的函数)。

从内核版本2.6.11开始,用一个叫做kernel_sem的信号量来实现大内核锁(在较早的2.6版本中,大内核锁是通过自旋锁来实现的)。但是,大内核锁比简单的信号量要复杂一些。

每个进程描述符task_struct都含有lock_depth字段,其就是一个整形变量,这个字段允许同一个进程几次获取大内核锁。因此,对大内核锁两次连续的请求不挂起处理器(相对于普通自旋锁)。如果进程未获的过锁,则这个字段的值为-1;否则,这个字段的值加1,表示已经请求了多少次锁。lock_depth字段对中断处理程序、异常处理程序及可延迟函数获取大内核锁都是至关重要的。如果没有这个字段,那么,在当前进程已经拥有大内核锁的情况下,任何试图获得这个锁的异步函数都可能产生死锁。

lock_kernel ()和unlock_kernel()内核函数用来获得和释放大内核锁。前一个函数等价于:
    depth = current->lock_depth + 1;
    if (depth == 0)
        down(&kernel_sem);
    current->lock_depth = depth;

而后者等价于
    if (--current->lock_depth < 0)
        up(&kernel_sem);

注意,lock_kernel()和unlock_kernel()函数的if语句不需要原子地执行,因为lock_depth不是全局变量——这是每个CPU在自己当前进程描述符中访问的一个字段。在if语句确本地中断也不会引起竞争条件。即使新内核控制路径调用了lock_kernel(),它在终止前也必须释放大内核锁。

足以令人吃惊的是,允许一个持有大内核锁的进程调用schedule(),从而放弃CPU!不过,schedule()函数检查被替换进程的lock_depth字段,如果它的值是0或者正数,就自动释放kernel_sem信号量(参见“schedule()函数”博文)。因此,不会有显式调用schedule()的进程在进程切换前后都保持大内核锁。但是,当schedule()函数再次选择这个进程来执行的时候,将为该进程重新获得大内核锁。

然而,如果一个持有大内核锁的进程被另一个进程抢占,情况就有所不同了。一直到内核版本2.6.10还没有出现这种情况,因为获取自旋锁时会自动禁用内核抢占。但是,现在大内核锁的实现是基于信号量的,而且不会由于获得它而自动禁用内核抢占。实际上,在被大内核锁保护的临界区内允许内核抢占是改变大内核锁实现的主要原因。其次,这对于系统的响应时间会产生有益的影响。

当一个持有大内核锁的进程被抢占时,schedule()一定不能释放信号量,因为在临界区内执行代码的进程没有主动触发进程切换。所以,如果释放大内核锁,那么另外一个进程就可能获得它,并破坏由被抢占的进程所访问的数据结构。

为了避免被抢占的进程失去大内核锁,preempt_schedule_irq()临时把进程的lock_depth字段设置为-1。观察这个字段的值,schedule()假定被替换的进程不拥有kernel_sem信号量,也就不释放它。结果,被抢占的进程就一直拥有kernel_seen信号量。一旦这个进程再次被调度程序选中,preempt_schedule_irq()函数就恢复lock_depth字段原来的值,并让进程在被大内核锁保护的临界区中继续执行。

3 内存描述符读/写信号量


mm_struct类型的每个内存描述符在mmap_sem字段中都包含了自己的信号量。由于几个轻量级进程之间可以共享一个内存描述符,因此,信号量保护这个描述符以避免可能产生的竞争条件。

例如,让我们假设内核必须为某个进程创建或扩展一个内存区。为了做到这一点,内核调用do_mmap()函数分配一个新的vm_area_struct数据结构。在分配的过程中,如果没有可用的空闲内存,而共享同一内存描述符的另外一个进程可能在运行,那么当前进程可能被挂起。如果没有信号量,那么需要访问内存描述符的第二个进程的任何操作(例如,由于写时复制而产生的缺页)都可能会导致严重的数据崩溃。

这种信号量是作为读/写信号量来实现的,因为一些内核函数,如缺页异常处理程序只需要扫描内存描述符。

4 slab高速缓存链表的信号量


slab高速缓存描述符链表是通过cache_chain_sem信号量保护的,这个信号量允许互斥地访问和修改该链表。

当kmem_cache_create()在链表中增加一个新元素,而kmem_cache_shrink()和kmem_cache_reap()顺序地扫描整个链表时,可能产生竞争条件。然而,在处理中断时,这些函数从不被调用,在访问链表时它们也从不阻塞。由于内核是支持抢占的,因此这种信号量在多处理器系统和单处理器系统中都会起作用。

5 索引节点的信号量


我们以后将会提到,Linux把磁盘文件的信息存放在一种叫做索引节点(inode)的内存对象中。相应的数据结构也包括有自己的信号量,存放在i_sem字段中。

在文件系统的处理过程中会出现很多竞争条件。实际上,磁盘上的每个文件都是所有用户共有的一种资源,因为所有进程都(可能)会存取文件的内容、修改文件名或文件位置、删除或复制文件等等。例如,让我们假设一个进程在显示某个目录所包含的文件。由于每个磁盘操作都可能会阻塞,因此即使在单处理器系统中,当第一个进程正在执行显示操作的过程中,其他进程也可能存取同一目录并修改它的内容。或者,两个不同的进程可能同时修改同一目录。所有这些竞争条件都可以通过用索引节点信号量保护目录文件来避免。

只要一个程序使用了两个或多个信号量,就存在死锁的可能,因为两个不同的控制路径可能互相死等着释放信号量。一般来说,Linux在信号量请求上很少会发生死锁问题,因为每个内核控制路径通常一次只需要获得一个信号量。然而,在有些情况下,内核必须获得两个或更多的信号量锁。索引节点信号量倾向于这种情况,例如,在rename()系统调用的服务例程中就会发生这种情况。在这种情况下,操作涉及两个不同的索引节点,因此,必须采用两个信号量。为了避免这样的死锁,信号量的请求按预先确定的地址顺序进行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值