Linux,同步方法剖析-Linux,同步方法剖析内核原子自旋锁和互斥锁x_第1页
Linux,同步方法剖析-Linux,同步方法剖析内核原子自旋锁和互斥锁x_第2页
Linux,同步方法剖析-Linux,同步方法剖析内核原子自旋锁和互斥锁x_第3页
Linux,同步方法剖析-Linux,同步方法剖析内核原子自旋锁和互斥锁x_第4页
免费预览已结束,剩余11页可下载查看

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、linux,同步方法剖析-linux,同步方法剖析内核原子,自旋锁和互斥锁x linux 同步方法剖析-linux 同步方法剖析内核原子,自旋锁和互斥锁 linux 同步方法剖析 -linux 同步方法剖析内核原子,自旋锁和互斥锁 在学习 linux 的过程中,您也许接触过并发(concurrency)、临界段(critical section)和锁定,但是如何在内核中使用这些概念呢?本文讨论了 2.6 版内核中可用的锁定机制,包括原子运算符(atomic operator)、自旋锁(spinlock)、读/写锁(reader/writer lock)和内核信号量(kernel semapho

2、re)。 本文还探讨了每种机制最适合应用到哪些地方,以构建安全高效的内核代码。 本文讨论了 linux 内核中可用的大量同步或锁定机制。这些机制为 2.6.23 版内核的许多可用方法提供了应用程序接口(api)。但是在深入学习 api 之前,首先需要明白将要解决的问题。 并发和锁定 当存在并发特性时,必须使用同步方法。当在同一时间段出现两个或更多进程并且这些进程彼此交互(例如,共享相同的资源)时,就存在 并发 现象。 在单处理器(uniprocessor,up)主机上可能发生并发,在这种主机中多个线程共享同一个 cpu 并且抢占(preemption)创建竞态条件。 抢占 通过临时中断一个线程

3、以执行另一个线程的方式来实现 cpu 共享。 竞态条件 发生在两个或更多线程操纵一个共享数据项时,其结果取决于执行的时间。在多处理器(mp)计算机中也存在并发,其中每个处理器中共享相同数据的线程同时执行。注意在 mp 情况下存在真正的并行(parallelism),因为线程是同时执行的。而在 up 情形中,并行是通过抢占创建的。两种模式中实现并发都较为困难。 linux 内核在两种模式中都支持并发。内核本身是动态的,而且有许多创建竞态条件的方法。linux 内核也支持多处理(multiprocessing),称为对称多处理(smp)。可以在本文后面的 参考资料 部分学到更多关于 smp 的知识

4、。 临界段概念是为解决竞态条件问题而产生的。一个 临界段 是一段不允许多路访问的受保护的代码。这段代码可以操纵共享数据或共享服务(例如硬件外围设备)。临界段操作时坚持互斥锁(mutual exclusion)原则(当一个线程处于临界段中时,其他所有线程都不能进入临界段)。 临界段中需要解决的一个问题是死锁条件。考虑两个独立的临界段,各自保护不同的。每个资源拥有一个锁,在本例中称为 a 和 b。假设有两个线程需要访问这些资源,线程 x 获取了锁 a,线程 y 获取了锁 b。当这些锁都被持有时,每个线程都试图占有其他线程当前持有的锁(线程 x 想要锁 b,线程 y 想要锁 a)。这时候线程就被死锁

5、了,因为它们都持有一个锁而且还想要其他锁。一个简单的解决方案就是总是按相同次序获取锁,从而使其中一个线程得以完成。还需要其他解决方案检测这种情形。表 1 定义了此处用到的一些重要的并发术语。 表 1. 并发中的重要定义 术语 定义 竞态条件 两个或更多线程同时操作资源时将会导致不一致的结果。 临界段 用于协调对共享资源的访问的代码段。 互斥锁 确保对共享资源进行排他访问的软件特性。 死锁 由两个或更多进程和资源锁导致的一种特殊情形,将会降低进程的工作效率。 回页首 linux 同步方法 如果您了解了一些基本理论并且明白了需要解决的问题,接下来将学习 linux 支持并发和互斥锁的各种方法。在以

6、前,互斥锁是通过禁用中断来提供的,但是这种形式的锁定效率比较低(现在在内核中仍然存在这种用法)。这种方法也不能进行扩展,而且不能保证其他处理器上的互斥锁。 在以下关于锁定机制的讨论中,我们首先看一下原子运算符,它可以保护简单变量(计数器和位掩码(bitmask)。然后介绍简单的自旋锁和读/写锁,它们构成了一个 smp 架构的忙等待锁(busy-wait lock)覆盖。最后,我们讨论构建在原子 api 上的内核互斥锁。 回页首 原子操作 linux 中最简单的同步方法就是原子操作。 原子 意味着临界段被包含在 api 函数中。不需要额外的锁定,因为 api 函数已经包含了锁定。由于 c 不能实

7、现原子操作,因此 linux 依靠底层架构来提供这项功能。各种底层架构存在很大差异,因此原子函数的实现方法也各不相同。一些方法完全通过汇编语言来实现,而另一些方法依靠 c 语言并且使用 local_irq_save 和 local_irq_restore 禁用中断。 当需要保护的数据非常简单时,例如一个计数器,原子运算符是种理想的方法。尽管原理简单,原子 api 提供了许多针对不同情形的运算符。下面是一个使用此 api 的示例。 要声明一个原子变量(atomic variable),首先声明一个 atomic_t 类型的变量。这个结构包含了单个 int 元素。接下来,需确保您的原子变量使用 a

8、tomic_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 使变量递增。也可用减号运算符,它的作用与相加和

9、递增操作相反。清单 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 的特殊函数被添加到原子变量中,然后当结果值

10、为负数时返回真(true)。这被内核中一些依赖于架构的信号量函数使用。 许多函数都不返回变量的值,但两个函数除外。它们会返回结果值( atomic_add_return 和 atomic_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_counte

11、r ) / 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

12、operation)。 原子 api 还支持位掩码(bitmask)操作。跟前面提到的算术操作不一样,它只包含设置和清除操作。许多驱动程序使用这些原子操作,特别是 scsi。位掩码原子操作的使用跟算术操作存在细微的差别,因为其中只有两个可用的操作(设置掩码和清除掩码)。使用这些操作前,需要提供一个值和将要进行操作的位掩码,如清单 4 所示。 清单 4. 位掩码原子函数 unsigned long my_bitmask; atomic_clear_mask( 0, my_bitmask ); atomic_set_mask( (124), my_bitmask ); 自旋锁 自旋锁是使用忙等待锁

13、来确保互斥锁的一种特殊方法。如果锁可用,则获取锁,执行互斥锁动作,然后释放锁。如果锁不可用,线程将忙等待该锁,直到其可用为止。忙等待看起来效率低下,但它实际上比将线程休眠然后当锁可用时将其唤醒要快得多。 自旋锁只在 smp 系统中才有用,但是因为您的代码最终将会在 smp 系统上运行,将它们添加到 up 系统是个明智的做法。 自旋锁有两种可用的形式:完全锁(full lock)和读写锁。 首先看一下完全锁。 首先通过一个简单的声明创建一个新的自旋锁。这可以通过调用 spin_lock_init 进行初始化。清单 5 中显示的每个变量都会实现相同的结果。 清单 5. 创建和初始化自旋锁 spin

14、lock_t my_spinlock = spin_lock_unlocked; . or . define_spinlock( my_spinlock ); . or . spin_lock_init( my_spinlock ); 定义了自旋锁之后,就可以使用大量的锁定变量了。每个变量用于不同的上下文。 清单 6 中显示了 spin_lock 和 spin_unlock 变量。这是一个最简单的变量,它不会执行中断禁用,但是包含全部的内存壁垒(memory barrier)。这个变量假定中断处理程序和该锁之间没有交互。 清单 6. 自旋锁 lock 和 unlock 函数 spin_lock

15、( my_spinlock ); / critical section spin_unlock( my_spinlock ); 接下来是 irqsave 和 irqrestore 对,如清单 7 所示。 spin_lock_irqsave 函数需要自旋锁,并且在本地处理器(在 smp 情形中)上禁用中断。spin_unlock_irqrestore 函数释放自旋锁,并且(通过 flags 参数)恢复中断。 清单 7. 自旋锁变量,其中禁用了本地 cpu 中断 spin_lock_irqsave( my_spinlock, flags ); / critical section spin_unl

16、ock_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

17、上运行。这个变体如清单 8 所示。 清单 8. 自旋锁函数实现 bottom-half 交互 spin_lock_bh( my_spinlock ); / critical section spin_unlock_bh( my_spinlock ); 回页首 读/ 写锁 在许多情形下,对数据的访问是由大量的读和少量的写操作来完成的(读取数据比写入数据更常见)。读/写锁的创建就是为了支持这种模型。这个模型有趣的地方在于允许多个线程同时访问相同数据,但同一时刻只允许一个线程写入数据。如果执行写操作的线程持有此锁,则临界段不能由其他线程读取。如果一个执行读操作的线程持有此锁,那么多个读线程都可以进入

18、临界段。清单 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)对读/写自旋锁进

19、行了修改。显然,如果您使用的是原版的读/写锁,那么按照标准自旋锁的用法使用这个自旋锁,而不区分读线程和写线程。 回页首 内核互斥锁 在内核中可以使用互斥锁来实现信号量行为。内核互斥锁是在原子 api 之上实现的,但这对于内核用户是不可见的。互斥锁很简单,但是有一些规则必须.。同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁 不能进行递归锁定或解锁,并且互斥锁可能不能用于交互上下文。但是互斥锁比当前的内核信号量选项更快,并且更加紧凑,因此如果它们满足您的需求,那么它们将是您明智的选择。 可以通过 define_mutex 宏使用一个操作创建和初始化互斥锁。这将创建一

20、个新的互斥锁并初始化其结构。可以在 ./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!

21、 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 当一个互斥锁被锁定后,

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论