级别: 中级

M. Tim Jones (), 顾问工程师, Emulex

2007 年 11 月 19 日在

学习 Linux® 的过程中,您也许接触过并发(concurrency)、临界段(critical

section)和锁定,但是如何在内核中使用这些概念呢?本文讨论了 2.6 版内核中可用的锁定机制,包括原子运算符(atomic

operator)、自旋锁(spinlock)、读/写锁(reader/writer lock)和内核信号量(kernel

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

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

当存在并发特性时,必须使用同步方法。当在同一时间段出现两个或更多进程并且这些进程彼此交互(例如,共享相同的资源)时,就存在并发 现象。

在单处理器(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 定义了此处用到的一些重要的并发术语。

术语定义

竞态条件两个或更多线程同时操作资源时将会导致不一致的结果。

临界段用于协调对共享资源的访问的代码段。

互斥锁确保对共享资源进行排他访问的软件特性。

死锁由两个或更多进程和资源锁导致的一种特殊情形,将会降低进程的工作效率。

果您了解了一些基本理论并且明白了需要解决的问题,接下来将学习 Linux

支持并发和互斥锁的各种方法。在以前,互斥锁是通过禁用中断来提供的,但是这种形式的锁定效率比较低(现在在内核中仍然存在这种用法)。这种方法也不能进

行扩展,而且不能保证其他处理器上的互斥锁。

在以下关于锁定机制的讨论中,我们首先看一下原子运算符,它可以

保护简单变量(计数器和位掩码(bitmask))。然后介绍简单的自旋锁和读/写锁,它们构成了一个 SMP 架构的忙等待锁(busy-wait

lock)覆盖。最后,我们讨论构建在原子 API 上的内核互斥锁。

Linux 中最简单的同步方法就是原子操作。原子

意味着临界段被包含在 API 函数中。不需要额外的锁定,因为 API 函数已经包含了锁定。由于 C 不能实现原子操作,因此 Linux

依靠底层架构来提供这项功能。各种底层架构存在很大差异,因此原子函数的实现方法也各不相同。一些方法完全通过汇编语言来实现,而另一些方法依靠 c

语言并且使用 local_irq_save 和

local_irq_restore 禁用中断。

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

当需要保护的数据非常简单时,例如一个计数器,原子运算符是种理想的方法。尽管原理简单,原子 API 提供了许多针对不同情形的运算符。下面是一个使用此 API 的示例。

要声明一个原子变量(atomic variable),首先声明一个

atomic_t 类型的变量。这个结构包含了单个

int 元素。接下来,需确保您的原子变量使用 ATOMIC_INIT 符号常量进行了初始化。

在清单 1 的情形中,原子计数器被设置为 0。也可以使用

atomic_set function 在运行时对原子变量进行初始化。

atomic_t my_counter ATOMIC_INIT(0);

... or ...

atomic_set( &my_counter, 0 );

原子 API 支持一个涵盖许多用例的富函数集。可以使用

atomic_read 读取原子变量中的内容,也可以使用 atomic_add 为一个变量添加指定值。最常用的操作是使用

atomic_inc 使变量递增。也可用减号运算符,它的作用与相加和递增操作相反。清单 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_return 和

atomic_sub_return),如清单 3所示。

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 所示。

unsigned long my_bitmask;

atomic_clear_mask( 0, &my_bitmask );

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

原子操作依赖于架构,可以在

./linux/include/asm-/atomic.h 中找到。

自旋锁是使用忙等待锁来确保互斥锁的一种特殊方法。如果锁可用,则获取锁,执行互斥锁动作,然后释放锁。如果锁不可用,线程将忙等待该锁,直到其可用为止。忙等待看起来效率低下,但它实际上比将线程休眠然后当锁可用时将其唤醒要快得多。

自旋锁只在 SMP 系统中才有用,但是因为您的代码最终将会在 SMP 系统上运行,将它们添加到 UP 系统是个明智的做法。

自旋锁有两种可用的形式:完全锁(full lock)和读写锁。

首先看一下完全锁。

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

spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;

... or ...

DEFINE_SPINLOCK( my_spinlock );

... or ...

spin_lock_init( &my_spinlock );

定义了自旋锁之后,就可以使用大量的锁定变量了。每个变量用于不同的上下文。

清单 6 中显示了 spin_lock 和

spin_unlock 变量。这是一个最简单的变量,它不会执行中断禁用,但是包含全部的内存壁垒(memory barrier)。这个变量假定中断处理程序和该锁之间没有交互。

spin_lock( &my_spinlock );

// critical section

spin_unlock( &my_spinlock );

接下来是 irqsave 和

irqrestore 对,如清单 7 所示。spin_lock_irqsave

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

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 所示。

spin_lock_bh( &my_spinlock );

// critical section

spin_unlock_bh( &my_spinlock );

许多情形下,对数据的访问是由大量的读和少量的写操作来完成的(读取数据比写入数据更常见)。读/写锁的创建就是为了支持这种模型。这个模型有趣的地方在

于允许多个线程同时访问相同数据,但同一时刻只允许一个线程写入数据。如果执行写操作的线程持有此锁,则临界段不能由其他线程读取。如果一个执行读操作的

线程持有此锁,那么多个读线程都可以进入临界段。清单 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

之上实现的,但这对于内核用户是不可见的。互斥锁很简单,但是有一些规则必须牢记。同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行

解锁。互斥锁不能进行递归锁定或解锁,并且互斥锁可能不能用于交互上下文。但是互斥锁比当前的内核信号量选项更快,并且更加紧凑,因此如果它们满足您的需

求,那么它们将是您明智的选择。

可以通过

DEFINE_MUTEX 宏使用一个操作创建和初始化互斥锁。这将创建一个新的互斥锁并初始化其结构。可以在

./linux/include/linux/mutex.h 中查看该实现。

DEFINE_MUTEX( my_mutex );

互斥锁 API 提供了 5 个函数:其中 3 个用于锁定,一个用于解锁,另一个用于测试互斥锁。首先看一下锁定函数。在需要立即锁定以及希望在互斥锁不可用时掌握控制的情形下,可以使用第一个函数 mutex_trylock。该函数如清单 10 所示。

ret = mutex_trylock( &my_mutex );

if (ret != 0) {

// Got the lock!

} else {

// Did not get the lock

}

如果想等待这个锁,可以调用

mutex_lock。这个调用在互斥锁可用时返回,否则,在互斥锁锁可用之前它将休眠。无论在哪种情形中,当控制被返回时,调用者将持有互斥锁。最后,当调用者休眠时使用 mutex_lock_interruptible。在这种情况下,该函数可能返回

-EINTR。清单 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 演示了这些函数。

mutex_unlock( &my_mutex );

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

// Mutex is unlocked

}

互斥锁 API 存在着自身的局限性,因为它是基于原子 API 的。但是其效率比较高,如果能满足你的需要,还是可以使用的。

最后看一下大内核锁(BLK)。它在内核中的用途越来越小,但是仍然有一些保留下来的用法。BKL 使多处理器 Linux 成为可能,但是细粒度(finer-grained)锁正在慢慢取代 BKL。BKL 通过

lock_kernel 和

unlock_kernel 函数提供。要获得更多信息,请查看 ./linux/lib/kernel_lock.c。

Linux

性能非凡,其锁定方法也一样。原子锁不仅提供了一种锁定机制,同时也提供了算术或 bitwise 操作。自旋锁提供了一种锁定机制(主要应用于

SMP),而且读/写自旋锁允许多个读线程且仅有一个写线程获得给定的锁。最后,互斥锁是一种新的锁定机制,提供了一种构建在原子之上的简单

API。不管你需要什么,Linux 都会提供一种锁定方案保护您的数据。

学习

您可以参阅本文在 developerWorks 全球站点上的 英文原文。

“Linux 和对称多处理”(developerWorks,2007 年 3 月)探讨了使用 SMP 进行多处理和开发 Linux 应用程序的思想。引入 SMP 之后,锁定机制就变得更加重要了。

Rusty Russell 的

讨论了关于 Linux 内核锁定的一个早期的话题。

在 LWN.net 上查阅

“”,了解为什么大内核锁在 Linux 内核中仍然占有一席之地。

在 developerWorks 上查看所有的

Linux 技巧 和

Linux 教程。

获得产品和技术

上获取最新的 Linux 源代码。Linux 源代码本身就是关于内核操作最直接的来源。文档子目录中也有大量文档(尽管其中部分内容比较陈旧)。

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

使用可从 developerWorks 直接下载的

IBM 试用软件,构建下一个 Linux 开发项目。

讨论

M. Tim Jones 是一名嵌入式软件工程师,他是 GNU/Linux Application Programming、AI Application Programming 以及 BSD Sockets Programming from a Multilanguage Perspective 等书的作者。他的工程背景非常广泛,从同步宇宙飞船的内核开发到嵌入式架构设计,再到网络协议的开发。Tim 是位于科罗拉多州 Longmont 的 Emulex Corp. 的一名顾问工程师。

linux 同步方法剖析,Linux 同步方法剖析相关推荐

  1. 剖析 Linux hypervisor

    KVM 和 Lguest 简介 Linux® 的最重要创新之一就是转变为hypervisor(或运行其他操作系统的操作系统).现在涌现许多使用 Linux 作为内核的 hypervisor 解决方案. ...

  2. 剖析Linux系统启动过程

    http://www.5dmail.net/html/2004-11-29/20041129102711.htm 内容提要 本文以RedHat9.0和i386平台为例,剖析了从用户打开电源直到屏幕出现 ...

  3. 深入剖析 linux GCC 4.4 的 STL string

    转自: 深入剖析 linux GCC 4.4 的 STL string 本文通过研究STL源码来剖析C++中标准模板块库std::string运行机理,重点研究了其中的引用计数和Copy-On-Wri ...

  4. Linux块层技术全面剖析-v0.1

    Linux块层技术全面剖析-v0.1 perftrace@gmail.com 1     前言 网络上很多文章对块层的描述散乱在各个站点,而一些经典书籍由于更新不及时难免更不上最新的代码,例如关于块层 ...

  5. 深度剖析 Linux 的 3 种“拷贝”命令

    作者 | 奇伢       责编 | 欧阳姝黎 概述 Linux 下有 3 种"拷贝",分别是 ln,cp,mv,这 3 个命令貌似都能 copy 出一个新的文件出来. 细心的小伙 ...

  6. linux 内核 课程,Linux内核分析课程-全面剖析Linux内核技术 揭开Linux内核的面纱 Linux内核学习视频教 ......

    课程名称 Linux内核分析课程-全面剖析Linux内核技术 揭开Linux内核的面纱 Linux内核学习视频 课程目录 (1)\1, 计算机是如何工作的?:目录中文件数:0个 (2)\2, 操作系统 ...

  7. 深度剖析Linux内核地址映射机制

    深度剖析Linux内核地址映射机制 1.虚拟空间数据结构   2.进程虚拟空间  3.内存映射 视频讲解如下,点击观看: Linux内核开发系列第7讲--深度剖析Linux内核地址映射机制 C/C++ ...

  8. 剖析Linux进程forkvforkclone原理机制丨进程管理指令及监控工具

    剖析Linux进程fork&vfork&clone原理机制丨进程管理指令及监控工具 视频讲解如下,点击观看: 剖析Linux进程fork&vfork&clone原理机制 ...

  9. Linux系统之时间同步方法

    Linux系统之时间同步方法 一.使用NTP服务时间同步 1.安装ntp 2.启动ntp服务 3.查看ntp服务 4.修改ntp.conf文件 5.重启服务 6.检查同步状态 二.使用ntpdate同 ...

最新文章

  1. jquery如何调用后台的方法
  2. SCWS 添加自定义词典
  3. git删除远程已经没有的分支
  4. Codeforces Round #421 B
  5. retinanet 部署_再谈RetinaNet
  6. 【Flink】Flink如何传递JVM参数给JobManager和TaskManager
  7. 开源数据屏蔽 数据加密_数据屏蔽或更改行为信息
  8. myeclipse中删除tomcat 的server后,重新添加进来的方法
  9. Keil MDK5硬件仿真之基本介绍
  10. 电商一定是一元化结构
  11. 【CVPR-2019】基于深度学习优化光照的暗光图像增强
  12. promise跟ajax区别,Promise和AJAX有什么区别?
  13. 沉降观测原始数据编译
  14. 多御浏览器安卓版有哪些地方值得下载使用?
  15. 又拍云php表单,又拍云php版Token授权防盗链整理 - YangJunwei
  16. [debug][RDP开源项目]rdpwrapper中出现not listening的解决方案
  17. 2021最新阿里云部署k8s集群(篇1 购买服务器)
  18. 元好问《摸鱼儿-雁邱词》赏析
  19. python科学计数法的显示与转换
  20. Python实现文字转语音

热门文章

  1. iMeta期刊推特官方帐号@iMetaJournal上线
  2. AEM:众里寻他千百度,用了SIP不迷路-识别污水处理单元中砷氧化细菌的多样性及代谢潜力...
  3. QIIME 2用户文档. 21图形界面q2studio(2019.7)
  4. Nature综述:真菌的多样性:真菌的高通量测序及鉴定
  5. android switcher控件,Android ViewSwitcher 的功能与用法
  6. R语言使用DALEX包的model_performance函数对caret包生成的多个算法模型进行残差分布分析并使用箱图进行残差分布的可视化
  7. pandas使用groupby函数计算dataframe数据中每个分组的N个数值的滚动计数个数(rolling count)、例如,计算某公司的多个店铺每N天(5天)的滚动销售额计数个数
  8. R语言ggplot2可视化使用不连续的y轴、中断的Y轴来可视化数值分布差异很大的数据实战:把数据轴分为两个区间或者多个区间来匹配不同区间数据的可视化(因为有的数据可能10附近,有的数值可能1W附近)
  9. 机器学习数据预处理之缺失值:插值法填充+ lagrange插值+拉格朗日插值
  10. python计算特征的统计值并文本输出