内核同步方法

  • 1 原子操作
    • 原子整数操作
      • 原子性与顺序性的比较
    • 原子位操作
  • 2 自旋锁
      • 自旋锁是不可递归的
    • 其他针对自旋锁的操作
    • 自旋锁和下半部
  • 3 读-写自旋锁
  • 4 信号量
    • 创建和初始化信号量
    • 使用信号量
  • 5 读-写信号量
  • 6 自旋锁和信号量
  • 7 完成变量
  • 8 BKL(大内核锁)
    • Seq锁
  • 9 禁止抢占
  • 10 顺序和屏障

1 原子操作

原子操作是保证指令以原子的方式执行,执行过程不被打断。内核提供了两组原子接口,一组针对整数进程操作,另一组针对单独的位进行操作。在Linux支持的所有体系结构上都实现了这两组接口。

原子整数操作

针对整数的原子操作只能对atomic_t类型的数据进行处理。Linux支持的所有机器上的整数数据都是32位的,但是使用atomic_t的代码只能将该类型的数据当做24位来用。这个限制完全是因为在SPARC体系结构上,原子操作的实现不同于其他体系结构:32位int类型的低8位嵌入了一个锁,

因为SPARC体系结构对原子操作缺乏指令级的支持,所有只能利用该锁来避免对原子类型数据的并发访问。

原子操作的声明在<asm/atomic.h>文件中。所有的体系结构内核会提供一些相同的方法,有些体系结构会提供一些在该体系结构上使用的额外原子操作方法。

定义一个atomic_t类型的数据,还可以在定义时给它设定初值:

atomic_t v;  /* 定义v */
atomic_t u = ATOMIC_INIT(0);   /* 定义u并把u初始化为0 */

原子整数操作列表如下:

原子性与顺序性的比较

原子性确保指令执行期间不被打断,要么全部执行完,要么根本不执行。而顺序性确保即使两条或多条指令出现在独立的执行线程中,它们要执行顺序要按规定的执行。例如,给一个整数初始化为10,要么初始化成功,要么初始化失败,这就是原子性。接着又有一个操作给整数初始化为20,原子性不管是先初始化为10还是先初始化为20,这是顺序性的责任。

原子位操作

原子性操作是与体系结构相关的操作,定义在文件<asm/bitops.h>中。位操作函数是对普通的内存地址进行操作的,它的参数是一个指针和一个尾号。原子位操作的列表如下:

内核还提供了两个例程用来从指定的地址开始搜素第一个被设置(未被设置)的位

int find_first_bit(unsigned long *addr,unsigned int size);
int find_first_zero_bit(unsigned long *addr,unsigned int size);

2 自旋锁

Linux内核最常见的锁时自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被争用(已经被使用)的自旋锁,那么该线程就会一直进行忙循环,等待锁重新可用。在任何时刻,自旋锁都可以防止多余一个的执行线程同时进入临界区。

如果自旋锁已经被争用了,那么请求它的线程在等待锁重新可用时将一直自旋,所以特别浪费处理器时间,因此自旋锁不应该被长时间持有。

自旋锁的实现和体系结构密切相关,代码往往通过汇编实现。这些与体系结构相关的代码定义在文件<asm/spinlock.h>中,实际需要用到的接口定义在文件<linux/spinlock.h>中。自旋锁的基本使用形式如下:

spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;spin_lock(&mr_lock);
/* 临界区 */
spin_unlock(&mr_lock);

因为自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区内,这就为多处理器机器提供了防止并发访问所需的保护机制。注意在单处理器机器上,编译的时候并不会加入自旋锁,它仅仅被当做一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全剔除内核。

自旋锁是不可递归的

Linux内核实现的自旋锁是不可递归的,如果你请求一个你已经持有的自旋锁,那么你将会自旋,等待释放这个锁,由于自旋,释放这个锁的操作不会执行,所有会一直处于自旋忙等待中,于是你被自己锁死了。

自旋锁可以使用在中断处理程序中。在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(当前处理器上的中断请求),否则,中断处理程序就会打断正持有锁的内核代码,有可能试图去争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自旋,但是锁的持有者在这个中断处理程序执行完毕前不可能运行,会造成死锁。注意,需要关闭的只是当前处理器上的中断,如果中断发生在不同的处理器上,即使中断处理程序在同一锁上自旋,也不会妨碍锁的持有者最终释放锁。

内核提供的禁止中断同时请求锁的接口:

spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;
unsigned long flags;spin_lock_irqsave(&mr_lock,flags);
/* 临界区 */
spin_lock_irqrestore(&mr_lock,flags);

函数spin_lock_irqsave保存中断的当前状态,并禁止中断,然后再去获取指定的锁。反过来spin_lock_irqrestore对指定的锁解锁,然后让中断恢复到加锁前的状态。

配置选项CONFIG_DEBUG_SPINLOCK为使用自旋锁的代码加入了许多调试检测手段。

其他针对自旋锁的操作

spin_lock_init()用来初始化动态创建的自旋锁。spin_try_lock试图获得某个特定的自旋锁,其他自旋锁操作如下:

自旋锁和下半部

由于下半部可以抢占进程上下文中的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。同样,由于中断程序程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。

3 读-写自旋锁

Linux提供了专门的读写自旋锁,这种自旋锁为读和写分别提供了不同的锁,一个或多个读任务可以并发的持有读者锁;相反,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。有时把读写锁叫做共享排斥锁,或者并发排斥锁,因为这种锁以共享(对读者而言)和排斥(对写着而言)的形式获得使用。

读写自旋锁的使用方式类似于普通自旋锁:

rwlock_t my_rwlock = RW_LOCK_UNLOCKED;  /* 初始化 */
read_lock(&my_rwlock);
/* 临界区 只读*/
read_unlock(&my_lock);

在可以写的临界区加上如下代码:

write_lock(&my_rwlock);
/* 临界区(写) */
write_unlock(&my_rwlock);

不能同时请求读锁和写锁:

read_lock(&my_rwlock);
write_lock(&my_rwlock);

这样将会带来死锁,因为写锁会不断自旋,而读锁得不到释放。
针对读写自旋锁的操作如下:

在使用Linux读-写自旋锁时,最后要考虑的一点是这种锁照顾读比照顾写要多一点,当读锁被持有时,写操作为了互斥访问只能等待,但是,读者却可以继续成功地占用锁。而自旋等待的写者在所有读者释放锁之前是无法获得锁的。

自旋锁提供了一种快速简单的锁实现方式,如果加锁时间不长并且代码不会休眠,利用自旋锁时最佳选择。

4 信号量

Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其放到一个等待队列,然后让其睡眠。这是处理器能去执行其他代码。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量。

信号量可以同时允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它。信号量同时允许的持有者数量可以在声明信号量时指定,这个值称为使用者数量。通常情况下,信号量和自旋锁一样,在一个时刻仅允许有一个锁持有者。当数量等于1,这样的信号量被称为二值信号量或者被称为互斥信号量;初始化时也可以把数量设置为大于1的非0值,这种情况,信号量被称为计数信号量,它允许在一个时刻至多有count个锁持有者。

信号量支持两个原子操作P()和V()。前者叫做测试操作,后者叫做增加操作,后来系统把这两种操作分别叫做down()和up(),Linux也遵从这种叫法。down()通过对信号量减1来请求一个信号量,如果减1结果是0或者大于0,那么就获得信号量锁,任务就可以进入临界区,如果结果是负的,那么任务会被放入等待队列。相反,当临界区的操作完成后,up()操作用来释放信号量,如果在该信号量上的等待队列不为空,那么处于队列中等待的任务被唤醒。

创建和初始化信号量

信号量的实现是与体系结构有关的,具体实现定义在文件<asm/semaphore.h>中。struct semaphore类型表示信号量。可以通过以下方式静态声明信号量:

static DECLARE_SEMAPHORE_GENERIC(name,count);

其中name是信号量变量名,count是信号量的使用者数量。创建更为普通的互斥信号量可以使用以下方式:

static DECLARE_MUTEX(name);

我们可以使用sema_init对信号量进行动态初始化:

sema_init(sem,count);

sem是指针,count是信号量的使用者数量。初始化一个动态创建的互斥信号量时使用以下函数:

sema_MUTEX(sem)

使用信号量

函数down_interruptible()试图获取指定的信号量,如果获取失败,它将以TASK_INTERRUPTIBLE状态进入睡眠。如果进程在等待获取信号量的时候接受到了信号,那么该进程就会被唤醒,而函数down_interruptible()会返回EINTR。另外一个函数down()获取信号量失败会让进程在TASK_UNINTERRUPTIBLE状态下睡眠,我们应该避免这种情况,因为进程等待信号量的时候就不再响应信号了。

使用down_trylock()函数,可以尝试获取指定的信号量,在信号量被占用时,它立刻返回非0值,否则,返回0,并且成功获取信号量锁。

要释放指定的信号,需要调用up()函数。

针对信号量的操作如下表:

5 读-写信号量

与自旋锁一样,信号量也有区分读写访问的可能,。读写信号量在内核中是由rw_semaphore结构表示的,定义在文件<linux/rwsem.h>中。通过以下语句可以创建静态声明的读写信号量:

static DECLARE_RWSEM(name);

动态创建读写信号量可以通过下面的函数:

init_rwsem(struct rw_semaphore *sem)

所有的读写信号量都是互斥信号量(它们的引用计数等于1)。只要没有写着,并发持有读锁的读者数不限。相反,只有唯一的写者(没有读者时)可以获得写锁。所有的读写锁的睡眠都不会被信号打断,它只有一个down()操作:

6 自旋锁和信号量

在中断上下文中只能使用自旋锁,在任务睡眠时只能使用信号量。

7 完成变量

如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量是使两个任务以同步的简单方法。如果一个任务要执行一些工作时,另一任务就会在完成变量上等待,当这个任务完成后,会使用完成变量去唤醒在等待的任务。

完成变量由结构体completion表示,定义在<linux/cmpletion.h>中。可以通过以下方式创建:

DECLARE_COMPLETION(mr_comp)  /* 静态创建 */
init_completion()   /* 动态创建 */

在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。当特定事件发生后,产生事件的任务调用complete()来发送信号唤醒正在等待的任务。

8 BKL(大内核锁)

BKL(大内核锁)是一个全局自旋锁,使用它主要是为了方便实现从Linux最初的SMP过度到细粒度加锁机制。BKL有如下特性:

  • 持有BKL的任务仍然可以睡眠。因为当任务无法被调度时,所加锁会自动被丢弃,当任务被调度时,锁又会被重新获得。
  • BKL是一种递归锁,一个进程可以多次请求一个锁,并不会自旋锁那样产生死锁现象
  • BKL可以用于进程上下文中
  • BKL是有害的

在内核中不鼓励使用BKL,新代码中不再使用BKL,但是这种锁仍然在部分内核代码中得到沿用。BKL的使用方式和自旋锁类似。函数lock_hernel()请求锁,unlock_kernel()释放锁。一个执行线程可以递归的请求锁,但是,释放锁时也必须调用同样次数的unlock_kernel()操作,在最后一个解锁操作完成后,锁才会被释放。这些接口被声明在<linux/smp_lock.h>中。

Seq锁

Seq锁是在2.6内核版本中才引入的一种新型锁。实现这种锁主要依靠一个序列计数器。当有疑义的数据被写入时,会得到一个锁,并且序列值会增加。在读取数据之前和之后,序列号都被读取。如果读取的序号号值相同,说明在读操作进行的过程中没有被写操作打断过。

定义一个seq锁的形式为:

seqlock_t mr_seq_lock = SEQLOCK_UNLOCKED;


在多个读者好少数写者共享一把锁的时候,seq锁有助于提供一种非常轻量级和具有可扩展性的外观。但是seq锁对写者更有利,只要没有其他写者,写锁总是能够被成功获得,读者不会影响写锁,这个和读写自旋锁及信号量一样。另外,挂起的写者会不断地使得读操作循环,直到不再有任何写者持有锁为止。

9 禁止抢占

由于内核时抢占性的,内核中的进程在任何时候都可能停下来以便另一个更高优先级的进程运行。这意味着一个任务与被抢占的任务可能会在同一个临界区内运行,为了避免这种情况,内核抢占代码使用自旋锁作为非抢占区域的标记。如果一个自旋锁被持有,内核便不能进行抢占。因为内核抢占和SMP面对相同的并发问题,并且内核已经是SMP安全的,因此,这种简单的变化使得内核也是抢占安全的。

实际中,某些情况下并不需要自旋锁,但是仍然需要关闭内核抢占,出现最频繁的情况就是每个处理器上的数据。如果数据对每个处理器是唯一的,那么这样的数据可能就不需要使用锁来保护,因为数据只能被一个处理器访问,如果自旋锁没有被持有,内核又是抢占的,那么一个新调度的任务就可能访问同一个变量,如下所示

这样,即使这是一个单处理器,变量foo也会被多个进程以伪并发的方式访问。通常,这个变量会请求得到一个自旋锁(防止多处理器上的真并发)。但是如果这是每个处理器上独立的变量,可能就不需要锁。

为了解决这个问题,可以通过preempt_disable()禁止内核抢占。这是一个可以嵌套调用的函数,可以调用任意次。每次调用都必须有一个相应的preempt_enable()调用。当最后一次preempt_enable()被调用后,内核抢占才重新启用。

抢占计数存放着持有锁的数量和preempt_disable()的调用次数,如果计数是0,那么内核可以进行抢占,如果为1或更大的值,那么内核就不会进行抢占。函数preempt_count()返回这个值。

为了更简洁的方法解决每个处理器上的数据访问问题,可以通过get_cpu获得处理器编号。这个函数在返回当前处理器号前会首先关闭内核抢占。put_cpu()会恢复内核抢占:

10 顺序和屏障

屏障是告诉编译器不要对给定点周围的指令序列进行重新排序。

rmb()方法提供了一个读内存屏障,它确保在rmb()之前的载入操作不会被重新排在该调用之后,在rmb()之后的载入操作不会被重新排在该调用之前。
wmb()提供了一个写内存屏障,这个函数的功能和rmb()类似,区别仅仅是它是针对存储而非载入。
mb()方法即提供了读屏障也提供了写屏障。
内核和编译器屏障方法如下:

注意,对于不同体系结构,屏障的实际效果差别很大。例如,如果一个体系结构不执行打乱存储(比如intel x86芯片),那么wmb()就什么也不做。

Linux内核设计与实现---内核同步方法相关推荐

  1. Linux内核设计与实现——内核数据结构

    主要内容 链表 队列 映射 二叉树 1. 链表 单向链表.双向链表 环形链表 linux内核中的链表使用方法和一般数据结构中定义的链表是有所不同的. 传统链表: 传统双向链表.png 传统的链表有个最 ...

  2. 《Linux内核设计与实现》读书笔记 - 目录 (完结)

    读完这本书回过头才发现, 第一篇笔记居然是 2012年8月发的, 将近一年半的时间才看完这本书(汗!!!). 为了方便以后查看, 做个<Linux内核设计与实现>读书笔记 的目录: < ...

  3. 读《Linux内核设计与实现》我想到了这些书

          从题目中可以看到,这篇文章是以我读<Linux内核设计与实现>而想到的其他我读过的书,所以,这篇文章的主要支撑点是<Linux内核>.       开始读这本书已经 ...

  4. 读 Linux内核设计与实现 我想到了这些书

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴!     ...

  5. Linux内核设计与实现学习笔记目录

    **注:**这是别人的笔记,我只是把目录抄过来 <Linux内核设计与实现学习笔记> 1.<Linux内核设计与实现>读书笔记(一)-内核简介 2.<Linux内核设计与 ...

  6. 《Linux内核设计与实现》读书笔记(十九)- 可移植性

    linux内核的移植性非常好, 目前的内核也支持非常多的体系结构(有20多个). 但是刚开始时, linux也只支持 intel i386 架构, 从 v1.2版开始支持 Digital Alpha, ...

  7. 初探内核之《Linux内核设计与实现》笔记上

    内核简介  本篇简单介绍内核相关的基本概念. 主要内容: 单内核和微内核 内核版本号 1. 单内核和微内核   原理 优势 劣势 单内核 整个内核都在一个大内核地址空间上运行. 1. 简单. 2. 高 ...

  8. 《Linux内核设计与实现》读书笔记 - 目录 (完结)【转】

    转自:http://www.cnblogs.com/wang_yb/p/3514730.html 读完这本书回过头才发现, 第一篇笔记居然是 2012年8月发的, 将近一年半的时间才看完这本书(汗!! ...

  9. Linux内核设计与实现

    <Linux内核设计与实现>读书笔记 目录: <Linux内核设计与实现>读书笔记(一)-内核简介 <Linux内核设计与实现>读书笔记(二)- 内核开发的准备 & ...

最新文章

  1. 不“刷脸”回不了家?郑州业主拒绝录入人脸信息回家如做贼,物业:少数服从多数...
  2. ie中placeholder字体颜色兼容问题
  3. C# Lambda 和 匿名函数的GC总结
  4. 线性调频信号 matlab
  5. Mysql高性能笔记(一):Schema与数据类型优化
  6. 关于wordpress站点地图代码调试
  7. 【最佳实践】【Blend】Triggers、Actions 和 Behaviors
  8. 关于/etc/init.d/nfs脚本解读中的疑问解答
  9. 使用CALayer设置图像边框
  10. 使用 :after伪元素撑开 div
  11. 飞思卡尔磁力计MAG3110 快速上手指南
  12. 芋道Docker部署
  13. Linux如何安装/卸载.deb文件(保姆级教程)
  14. Java多线程--内存模型(JMM)--详解
  15. Opencv实现颜色检测
  16. 3个APP海外推广方式,不走寻常路
  17. 物联网周刊(第 6 期):开源硬件公司 Adafruit
  18. 联想服务器能够上固态硬盘吗,联想Y400能不能装固态硬盘 需要什么接口的
  19. mac电脑如何抢火车票
  20. [向量] 点积应用-两个向量夹角

热门文章

  1. 小程序在wxml页面中取整
  2. 设计撑百万并发的数据库架构
  3. CentOS Vi编辑器
  4. JSONArray.fromObject不执行且不报错问题的解决
  5. python!!!!惊了,这世上居然还有这么神奇的东西存在
  6. 1058. 选择题(20)
  7. Android WifiDisplay分析一:相关Service的启动
  8. $ npm install opencv ? 你试试?! 在windows环境下,使用node.js调用opencv攻略
  9. 【动态规划】【多重背包】[HDU 1291]悼念512汶川大地震遇难同胞――珍惜现在,感恩生活...
  10. C:打印菱形(自己的方法)