避免竞争条件的实例

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


引用计数器

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

大内核锁

在早期的Linux内核版本中,大内核锁被广泛使用。从2.6.11开始,用一个叫做kernel_sem的信号量来实现大内核锁。但是,大内核锁比简单的信号量要复杂一些。

每个进程描述符都含有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()函数再次选择这个进程来执行的时候,将为该进程重新获得大内核锁。

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

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

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

内存描述符读/写信号量

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

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

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

Slab高速缓存链表的信号量

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

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

索引节点的信号量

Linux把磁盘文件的信息存放在一种叫索引节点的内存对象中。相应的数据结构也包括有自己的信号量,存放在i_sem字段中。

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

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


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值