在学习 Linux® 的过程中,您或许接触过并发(concurrency)、临界段(critical section)和锁定,可是怎样在内核中使用这些概念呢?本文讨论了 2.6 版内核中可用的锁定机制,包含原子运算符(atomic operator)、自旋锁(spinlock)、读/写锁(reader/writer lock)和内核信号量(kernel semaphore)。

本文还探讨了每种机制最适合应用到哪些地方。以构建安全高效的内核代码。

本文讨论了 Linux 内核中可用的大量同步或锁定机制。这些机制为 2.6.23 版内核的很多可用方法提供了应用程序接口(API)。可是在深入学习 API 之前。首先须要明确将要解决的问题。

developerWorks 上 Tim 的很多其它 剖析... 系列

并发和锁定

当存在并发特性时,必须使用同步方法。

当在同一时间段出现两个或很多其它进程而且这些进程彼此交互(比如。共享同样的资源)时,就存在并发 现象。

在单处理器(uniprocessor。UP)主机上可能发生并发。在这样的主机中多个线程共享同一个 CPU 而且抢占(preemption)创建竞态条件。抢占 通过暂时中断一个线程以运行还有一个线程的方式来实现 CPU 共享。

竞态条件 发生在两个或很多其它线程操纵一个共享数据项时,其结果取决于运行的时间。在多处理器(MP)计算机中也存在并发,当中每一个处理器中共享同样数据的线程同一时候运行。

注意在 MP 情况下存在真正的并行(parallelism)。由于线程是同一时候运行的。

而在 UP 情形中。并行是通过抢占创建的。两种模式中实现并发都较为困难。

Linux 内核在两种模式中都支持并发。内核本身是动态的,并且有很多创建竞态条件的方法。Linux 内核也支持多处理(multiprocessing),称为对称多处理(SMP)。

能够在本文后面的 參考资料 部分学到很多其它关于 SMP 的知识。

临界段概念是为解决竞态条件问题而产生的。

一个临界段 是一段不同意多路訪问的受保护的代码。

这段代码能够操纵共享数据或共享服务(比如硬件外围设备)。

临界段操作时坚持相互排斥锁(mutual exclusion)原则(当一个线程处于临界段中时,其它全部线程都不能进入临界段)。

临界段中须要解决的一个问题是死锁条件。考虑两个独立的临界段,各自保护不同的资源。每一个资源拥有一个锁,在本例中称为 A 和 B。如果有两个线程须要訪问这些资源,线程 X 获取了锁 A,线程 Y 获取了锁 B。

当这些锁都被持有时。每一个线程都试图占有其它线程当前持有的锁(线程 X 想要锁 B。线程 Y 想要锁 A)。

这时候线程就被死锁了。由于它们都持有一个锁并且还想要其它锁。一个简单的解决方式就是总是按同样次序获取锁。从而使当中一个线程得以完毕。还须要其它解决方式检測这样的情形。表 1 定义了此处用到的一些重要的并发术语。


表 1. 并发中的重要定义
术语 定义
竞态条件 两个或很多其它线程同一时候操作资源时将会导致不一致的结果。
临界段 用于协调对共享资源的訪问的代码段。
相互排斥锁 确保对共享资源进行排他訪问的软件特性。
死锁 由两个或很多其它进程和资源锁导致的一种特殊情形。将会减少进程的工作效率。





回页首


Linux 同步方法

假设您了解了一些基本理论并且明确了须要解决的问题。接下来将学习 Linux 支持并发和相互排斥锁的各种方法。在曾经,相互排斥锁是通过禁用中断来提供的,可是这样的形式的锁定效率比較低(如今在内核中仍然存在这样的使用方法)。

这样的方法也不能进行扩展,并且不能保证其它处理器上的相互排斥锁。

在下面关于锁定机制的讨论中,我们首先看一下原子运算符,它能够保护简单变量(计数器和位掩码(bitmask))。

然后介绍简单的自旋锁和读/写锁,它们构成了一个 SMP 架构的忙等待锁(busy-wait lock)覆盖。最后。我们讨论构建在原子 API 上的内核相互排斥锁。





回页首


原子操作

Linux 中最简单的同步方法就是原子操作。原子 意味着临界段被包括在 API 函数中。不须要额外的锁定,由于 API 函数已经包括了锁定。由于 C 不能实现原子操作,因此 Linux 依靠底层架构来提供这项功能。各种底层架构存在非常大差异,因此原子函数的实现方法也各不同样。一些方法全然通过汇编语言来实现,而还有一些方法依靠 c 语言而且使用 local_irq_savelocal_irq_restore 禁用中断。

旧的锁定方法
在内核中实现锁定的一种不太好的方法是通过禁用本地 CPU 的硬中断。这些函数均可用而且仍得到使用(有时用于原子运算符)。但我们并不推荐使用。local_irq_save 例程禁用中断,而 local_irq_restore 恢复曾经启用过的中断。这些例程都是可重入的(reentrant)。也就是说它们能够在其它例程上下文中被调用。

当须要保护的数据很easy时。比如一个计数器,原子运算符是种理想的方法。虽然原理简单,原子 API 提供了很多针对不同情形的运算符。以下是一个使用此 API 的演示样例。

要声明一个原子变量(atomic variable),首先声明一个 atomic_t 类型的变量。

这个结构包括了单个 int 元素。接下来,需确保您的原子变量使用 ATOMIC_INIT 符号常量进行了初始化。 在清单 1 的情形中,原子计数器被设置为 0。也能够使用 atomic_set function 在执行时对原子变量进行初始化。


清单 1. 创建和初始化原子变量
                
atomic_t my_counter ATOMIC_INIT(0);

... or ...

atomic_set( &my_counter, 0 );

原子 API 支持一个涵盖很多用例的富函数集。能够使用 atomic_read 读取原子变量中的内容,也能够使用 atomic_add 为一个变量加入指定值。最经常使用的操作是使用 atomic_inc 使变量递增。

也可用减号运算符,它的作用与相加和递增操作相反。

清单 2. 演示了这些函数。


清单 2. 简单的算术原子函数
                
val = atomic_read( &my_counter );

atomic_add( 1, &my_counter );

atomic_inc( &my_counter );

atomic_sub( 1, &my_counter );

atomic_dec( &my_counter );

该 API 也支持更多经常使用用例,包含 operate-and-test 例程。这些例程同意对原子变量进行操纵和測试(作为一个原子操作来运行)。一个叫做 atomic_add_negative 的特殊函数被加入到原子变量中。然后当结果值为负数时返回真(true)。这被内核中一些依赖于架构的信号量函数使用。

很多函数都不返回变量的值。但两个函数除外。

它们会返回结果值( atomic_add_returnatomic_sub_return),如清单 3所看到的。


清单 3. Operate-and-test 原子函数
                
if (atomic_sub_and_test( 1, &my_counter )) {
  // my_counter is zero
}

if (atomic_dec_and_test( &my_counter )) {
  // my_counter is zero
}

if (atomic_inc_and_test( &my_counter )) {
  // my_counter is zero
}

if (atomic_add_negative( 1, &my_counter )) {
  // my_counter is less than zero
}

val = atomic_add_return( 1, &my_counter ));

val = atomic_sub_return( 1, &my_counter ));

假设您的架构支持 64 位长类型(BITS_PER_LONG 是 64 的),那么能够使用 long_t atomic 操作。能够在 linux/include/asm-generic/atomic.h 中查看可用的长操作(long operation)。

原子 API 还支持位掩码(bitmask)操作。跟前面提到的算术操作不一样,它仅仅包括设置和清除操作。很多驱动程序使用这些原子操作,特别是 SCSI。位掩码原子操作的使用跟算术操作存在细微的区别。由于当中仅仅有两个可用的操作(设置掩码和清除掩码)。使用这些操作前,须要提供一个值和将要进行操作的位掩码,如清单 4 所看到的。


清单 4. 位掩码原子函数
                
unsigned long my_bitmask;

atomic_clear_mask( 0, &my_bitmask );

atomic_set_mask( (1<<24), &my_bitmask );


原子 API 原型
原子操作依赖于架构。能够在 ./linux/include/asm-<arch>/atomic.h 中找到。

自旋锁

自旋锁是使用忙等待锁来确保相互排斥锁的一种特殊方法。假设锁可用。则获取锁,运行相互排斥锁动作,然后释放锁。

假设锁不可用,线程将忙等待该锁。直到其可用为止。忙等待看起来效率低下,但它实际上比将线程休眠然后当锁可用时将其唤醒要快得多。

自旋锁仅仅在 SMP 系统中才实用。可是由于您的代码终于将会在 SMP 系统上执行。将它们加入到 UP 系统是个明智的做法。

自旋锁有两种可用的形式:全然锁(full lock)和读写锁。 首先看一下全然锁。

首先通过一个简单的声明创建一个新的自旋锁。这能够通过调用 spin_lock_init 进行初始化。清单 5 中显示的每一个变量都会实现同样的结果。


清单 5. 创建和初始化自旋锁
                
spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;

... or ...

DEFINE_SPINLOCK( my_spinlock );

... or ...

spin_lock_init( &my_spinlock );

定义了自旋锁之后,就能够使用大量的锁定变量了。

每一个变量用于不同的上下文。

清单 6 中显示了 spin_lockspin_unlock 变量。这是一个最简单的变量,它不会运行中断禁用,可是包括所有的内存壁垒(memory barrier)。

这个变量假定中断处理程序和该锁之间没有交互。


清单 6. 自旋锁 lock 和 unlock 函数
                
                spin_lock( &my_spinlock );

// critical section

spin_unlock( &my_spinlock );

接下来是 irqsaveirqrestore 对,如清单 7 所看到的。

spin_lock_irqsave 函数须要自旋锁,而且在本地处理器(在 SMP 情形中)上禁用中断。spin_unlock_irqrestore 函数释放自旋锁,而且(通过 flags 參数)恢复中断。


清单 7. 自旋锁变量,当中禁用了本地 CPU 中断
                
                spin_lock_irqsave( &my_spinlock, flags );

// critical section

spin_unlock_irqrestore( &my_spinlock, flags );

spin_lock_irqsave/spin_unlock_irqrestore 的一个不太安全的变体是 spin_lock_irq/spin_unlock_irq。 我建议不要使用此变体。由于它会如果中断状态。

最后,假设内核线程通过 bottom half 方式共享数据,那么能够使用自旋锁的还有一个变体。

bottom half 方法能够将设备驱动程序中的工作延迟到中断处理后执行。这样的自旋锁禁用了本地 CPU 上的软中断。这能够阻止 softirq、tasklet 和 bottom half 在本地 CPU 上执行。这个变体如清单 8 所看到的。


清单 8. 自旋锁函数实现 bottom-half 交互
                
                spin_lock_bh( &my_spinlock );

// critical section

spin_unlock_bh( &my_spinlock );





回页首


读/写锁

在很多情形下,对数据的訪问是由大量的读和少量的写操作来完毕的(读取数据比写入数据更常见)。读/写锁的创建就是为了支持这样的模型。这个模型有趣的地方在于同意多个线程同一时候訪问同样数据。但同一时刻仅仅同意一个线程写入数据。

假设运行写操作的线程持有此锁,则临界段不能由其它线程读取。假设一个运行读操作的线程持有此锁,那么多个读线程都能够进入临界段。

清单 9 演示了这个模型。


清单 9. 读/写自旋锁函数
                
rwlock_t my_rwlock;

rwlock_init( &my_rwlock );

write_lock( &my_rwlock );

// critical section -- can read and write

write_unlock( &my_rwlock );


read_lock( &my_rwlock );

// critical section -- can read only

read_unlock( &my_rwlock );

依据对锁的需求,还针对 bottom half 和中断请求(IRQ)对读/写自旋锁进行了改动。显然,假设您使用的是原版的读/写锁,那么依照标准自旋锁的使用方法使用这个自旋锁。而不区分读线程和写线程。





回页首


内核相互排斥锁

在内核中能够使用相互排斥锁来实现信号量行为。内核相互排斥锁是在原子 API 之上实现的。但这对于内核用户是不可见的。相互排斥锁非常easy。可是有一些规则必须牢记。同一时间仅仅能有一个任务持有相互排斥锁。而且仅仅有这个任务能够对相互排斥锁进行解锁。相互排斥锁不能进行递归锁定或解锁。而且相互排斥锁可能不能用于交互上下文。可是相互排斥锁比当前的内核信号量选项更快,而且更加紧凑,因此假设它们满足您的需求。那么它们将是您明智的选择。

能够通过 DEFINE_MUTEX 宏使用一个操作创建和初始化相互排斥锁。这将创建一个新的相互排斥锁并初始化其结构。能够在 ./linux/include/linux/mutex.h 中查看该实现。

                DEFINE_MUTEX( my_mutex );

相互排斥锁 API 提供了 5 个函数:当中 3 个用于锁定。一个用于解锁。还有一个用于測试相互排斥锁。首先看一下锁定函数。

在须要马上锁定以及希望在相互排斥锁不可用时掌握控制的情形下。能够使用第一个函数 mutex_trylock。该函数如清单 10 所看到的。


清单 10. 尝试使用 mutex_trylock 获得相互排斥锁
                
ret = mutex_trylock( &my_mutex );
if (ret != 0) {
  // Got the lock!
} else {
  // Did not get the lock
}

假设想等待这个锁。能够调用 mutex_lock。这个调用在相互排斥锁可用时返回,否则,在相互排斥锁锁可用之前它将休眠。

不管在哪种情形中。当控制被返回时,调用者将持有相互排斥锁。最后,当调用者休眠时使用 mutex_lock_interruptible

在这样的情况下,该函数可能返回 -EINTR

清单 11 中显示了这两种调用。


清单 11. 锁定一个可能处于休眠状态的相互排斥锁
                
                mutex_lock( &my_mutex );

// Lock is now held by the caller.

if (mutex_lock_interruptible( &my_mutex ) != 0)  {

  // Interrupted by a signal, no mutex held

}

当一个相互排斥锁被锁定后,它必须被解锁。这是由 mutex_unlock 函数来完毕的。这个函数不能从中断上下文调用。

最后。能够通过调用 mutex_is_locked 检查相互排斥锁的状态。

这个调用实际上编译成一个内联函数。假设相互排斥锁被持有(锁定),那么就会返回 1;否则。返回 0。清单 12 演示了这些函数。


清单 12. 用 mutex_is_locked 測试相互排斥锁锁
                
                mutex_unlock( &my_mutex );

if (mutex_is_locked( &my_mutex ) == 0) {

  // Mutex is unlocked

}

相互排斥锁 API 存在着自身的局限性,由于它是基于原子 API 的。

可是其效率比較高,假设能满足你的须要,还是能够使用的。





回页首


大内核锁(Big kernel lock)

最后看一下大内核锁(BLK)。它在内核中的用途越来越小,可是仍然有一些保留下来的使用方法。BKL 使多处理器 Linux 成为可能。可是细粒度(finer-grained)锁正在慢慢代替 BKL。BKL 通过 lock_kernelunlock_kernel 函数提供。要获得很多其它信息。请查看 ./linux/lib/kernel_lock.c。





回页首


结束语

分享这篇文章……

digg 将本文提交到 Digg
del.icio.us 公布到 del.icio.us
Slashdot 提交到 Slashdot!

Linux 性能非凡,其锁定方法也一样。

原子锁不仅提供了一种锁定机制。同一时候也提供了算术或 bitwise 操作。

自旋锁提供了一种锁定机制(主要应用于 SMP),并且读/写自旋锁同意多个读线程且仅有一个写线程获得给定的锁。

最后。相互排斥锁是一种新的锁定机制,提供了一种构建在原子之上的简单 API。无论你须要什么,Linux 都会提供一种锁定方案保护您的数据。



參考资料

学习

获得产品和技术
  • Linux Kernel Archives 上获取最新的 Linux 源码。Linux 源码本身就是关于内核操作最直接的来源。文档子文件夹中也有大量文档(虽然当中部分内容比較陈旧)。

  • 订购 SEK for Linux。这套光盘(2张 DVD)包括了最新的 IBM Linux 试用软件,这些软件来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere®。



  • 使用可从 developerWorks 直接下载的 IBM 试用软件,构建下一个 Linux 开发项目。




讨论


关于作者

M. Tim Jones

M. Tim Jones 是一名嵌入式软件project师,他是 GNU/Linux Application ProgrammingAI Application Programming 以及 BSD Sockets Programming from a Multilanguage Perspective 等书的作者。

他的project背景很广泛。从同步宇宙飞船的内核开发到嵌入式架构设计,再到网络协议的开发。Tim 是位于科罗拉多州 Longmont 的 Emulex Corp. 的一名顾问project师。

<script>window._bd_share_config={"common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"0","bdSize":"16"},"share":{}};with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];</script>
阅读(62) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~
评论热议
posted on 2017-08-08 18:09  lxjshuju  阅读(300)  评论(0编辑  收藏  举报