在学习 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
公布到 del.icio.us
提交到 Slashdot!


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

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

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

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



參考资料

学习


获得产品和技术


讨论



关于作者











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) |

Linux 同步方法剖析--内核原子,自旋锁和相互排斥锁的更多相关文章

  1. UNIX网络编程卷1 server程序设计范式7 预先创建线程,以相互排斥锁上锁方式保护accept

    本文为senlie原创.转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.预先创建一个线程池.并让每一个线程各自调用 accept 2.用相互排斥锁代替让每一个线 ...

  2. RAII手法封装相互排斥锁

    CriticalSectionWrapper是一个接口类 class CriticalSectionWrapper { public: // Factory method, constructor d ...

  3. Linux互斥和同步应用程序(一):posix线程和线程之间的相互排斥

    [版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet 或 .../gentleliu,文章仅供学习交流.请勿用于商业用途] 有了进程的概念,为何还要使用线程呢? 首先,回 ...

  4. Linux程序设计学习笔记----多线程编程线程同步机制之相互排斥量(锁)与读写锁

    相互排斥锁通信机制 基本原理 相互排斥锁以排他方式防止共享数据被并发訪问,相互排斥锁是一个二元变量,状态为开(0)和关(1),将某个共享资源与某个相互排斥锁逻辑上绑定之后,对该资源的訪问操作例如以下: ...

  5. Linux同步与相互排斥应用(零):基础概念

    [版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet 或 .../gentleliu,文章仅供学习交流,请勿用于商业用途]         当操作系统进入多道批处理系统时 ...

  6. Linux多线程同步之相互排斥量和条件变量

    1. 什么是相互排斥量 相互排斥量从本质上说是一把锁,在訪问共享资源前对相互排斥量进行加锁,在訪问完毕后释放相互排斥量上的锁. 对相互排斥量进行加锁以后,不论什么其它试图再次对相互排斥量加锁的线程将会 ...

  7. LINUX内核笔记:自旋锁

    目录 自旋锁作用与基本使用方法? 在SMP和UP上的不同表现? 自旋锁与上下文 使用spin_lock()后为什么不能睡眠? 强调:锁什么? 参考   1.自旋锁作用与基本使用方法? 与其他锁一样,自 ...

  8. Linux 2.6内核中新的锁机制--RCU

    转自:http://www.ibm.com/developerworks/cn/linux/l-rcu/ 一. 引言 众所周知,为了保护共享数据,需要一些同步机制,如自旋锁(spinlock),读写锁 ...

  9. Linux内核原子(1) - spinlock的实现

    spinlock的数据结构spinlock_t定义在头文件linux/spinlock_types.h里面: typedef struct { raw_spinlock_t raw_lock; #if ...

随机推荐

  1. 关联容器——map、set

    map类型通常被称为关联数组,与正常数组类似,不同之处在于其下标不必是整数.我们通过一个关键字而不是位置来查找值(键值对). 与之相对,set就是关键字的简单集合.当只是想知道一个值是否存在时,set ...

  2. 基于HTML5 SVG和CSS3炫酷蹦床式图片切换特效

    今天给大家分享一款效果非常炫酷的HTML5 SVG和CSS3蹦床式图片切换特效插件.该图片切换插件在进行图片切换时,整个屏幕就像一张大蹦床一样,将图片弹射出去,切换到另一张图片,效果非常有创意.效果图 ...

  3. 纯css3实现的幽灵按钮导航

    之前为大家介绍了好几款导航菜单,今天再给大家带来一款css3实现的幽灵按钮式的导航菜单.导航界面非常好看.右侧是一个css3实现的动画消息图标.效果图如下: 在线预览   源码下载 实现代码: htm ...

  4. JavaScrip——简单练习(抓错误信息,for循环,日期)

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  5. hdu2896 病毒侵袭 AC自动机入门题 N(N <= 500)个长度不大于200的模式串(保证所有的模式串都不相同), M(M <= 1000)个长度不大于10000的待匹配串,问待匹配串中有哪几个模式串,

    /** 题目:hdu2896 病毒侵袭 链接:http://acm.hdu.edu.cn/showproblem.php?pid=2896 题意:N(N <= 500)个长度不大于200的模式串 ...

  6. 使用Ant编译Hadoop工程报错

    在win7用Ant编译hadoop工程的时候,遇到了一个报错,如下: org.eclipse.core.runtime.CoreException: D:\workspace\hadoop-1.1.2 ...

  7. POJ 1523 SPF 割点与桥的推断算法-Tarjan

    题目链接: POJ1523 题意: 问一个连通的网络中有多少个关节点,这些关节点分别能把网络分成几部分 题解: Tarjan 算法模板题 顺序遍历整个图,能够得到一棵生成树: 树边:可理解为在DFS过 ...

  8. expect模块的使用,主要没装包折腾一晚上

    第一步首先下载expect 模块,yum list |grep expect ,安装下面的模块. expect.x86_64                               5.44.1. ...

  9. 关于BETA、RC、ALPHA、Release、GA等版本号的意义

    关于BETA.RC.ALPHA.Release.GA等版本号的意义 转载 2016年06月19日 00:04:00 2049 0 1 . 最近由于工作需要经常要去SVN上拉开源项目的源码,对项目 ...

  10. iOS边练边学--定时任务和HUD

    九宫格计算思路 利用控件的索引index计算出控件所在的行号和列号 利用列号计算控件的x值 利用行号计算控件的y值 HUD 其他说法:指示器.遮盖.蒙板 半透明HUD的做法 背景色设置为半透明颜色 定 ...