转自:http://blog.csdn.net/goodluckwhh/article/details/9006065

版权声明:本文为博主原创文章,未经博主允许不得转载。

 
 

一、信号量

1.信号量的概念

信号量也是一种锁,当信号量不可用时,尝试获取信号量的任务将挂起直到它拿到了信号量。由于尝试获取信号量的任务可能挂起,因而中断服务程序以及可延迟函数不能使用信号量。

对于信号量来说需要注意:

  1. 只有对信号量计数值的操作是原子的
  2. 信号量的自旋锁只用于保护信号量的等待队列
  3. 信号量是比较特殊的,其up操作不是必须由down操作的调用者发起。如果把信号量也看作是一把锁,则该锁是很特殊的,它不一定由持有锁的任务释放,任何其它任务都可以做释放动作即调用up

因此信号量的down和up是可以并发执行的。但是由于保护了信号量的计数值和等待队列,因而这种并发并不会导致up和down本身出问题。
因为down操作可能导致调用者休眠,因而不能休眠的场景是不允许调用该函数的,比如中断上下文,而up可以在任意上下文调用。如果要在中断上下文调用可以使用down_trylock,它尝试获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0。

2.信号量的数据结构和相关API

1.数据结构

信号量用数据结构semaphore表示, 它包含以下域:

  • count:受该信号狼保护的资源的计数值,如果大于0表示资源可用;否则表示资源不可用。对它的操作必须是原子的。如果存在检查并更新的操作,这两个操作的组合必须也是原子的,比如down中的比较并减1。
  • wait_list:等待该信号量保护的资源可用的任务队列的链表。
  • lock:保护等待任务链表的自旋锁。

2.初始化

init_MUTEX( ) 将信号量的count域初始化为1,表示资源当前可用
init_MUTEX_LOCKED( ) 将信号量的count域初始化为0 ,表示资源当前不可用 
DECLARE_MUTEX 完成和 init_MUTEX类似的操作,但是它还多一个静态分配一个信号量的动作
DECLARE_MUTEX_LOCKED 和init_MUTEX_LOCKED类似,但是它还多一个静态分配一个信号量的动作
当然也可以将信号量的初始者设置为其它正值。

3.获取和释放信号量

up()函数用于释放信号量,如果当前信号量的等待队列为空,即没有任务在等待该信号量被释放,则它增加信号量的count值,然后返回,否则它唤醒等待队列上的第一个任务。
down用于获取信号量,如果信号量的值大于0,则它将count的值减1,并返回;否则调用者将被添加到等待队列的尾部,并等待直到被唤醒即该任务获得资源。
void down(struct semaphore *sem)
int down_trylock(struct semaphore *sem)//尝试获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0
int down_interruptible(struct semaphore *sem)//获取信号量,但是在等待信号量的时候可以被打断,如果在等待过程中被打断,则返回-EINTR
int down_timeout(struct semaphore *sem, long jiffies) //用于获取信号量,但是最多等待jiffies长的时间,如果在指定的时间期限内没有获取信号量,则就返回-ETIME。
void up(struct semaphore *sem)

3. 读写信号量的概念

读写信号量类似于读写自旋锁,它是为读多写少的场景做了优化的信号量,不同于自旋锁的是,在无法获得信号量时,它挂起而不是自旋。
可以有多个任务并发的为读获取读写信号量,但是同一时间点只能有一个任务可以为写获得读写信号量。因此只有没有任何任务为读或写持有该信号量时,新的为写获取信号量的操作才能成功。
内核FIFO的方式存储等待队列中的任务:

  1. 如果读者或写者无法获取读写信号量,就会被添加到等待队列的尾部
  2. 当信号量被释放时,就唤醒等待队列中的第一个任务(先唤醒谁取决于实现采取的策略,代码最能说明问题,最好查阅实际的代码)
  3. 如果唤醒的第一个任务是写,则其它任务继续睡眠,如果唤醒的第一个任务是读,则会唤醒它之后的所有读任务直到碰到了一个写任务,该写任务以及在它之后的所有任务都继续睡眠

由于它还是信号量,因此其应用场景的限制和信号量相同。

4.读写信号量的数据结构

rw_semaphore数据结构用于表示读写信号量,它包含如下的域:

  • activity:0表示没有任务在读或者写,大于0表示有任务正在读,-1表示有一个任务正在写
  • wait_list:存放等待该信号量的任务的链表
  • wait_lock:用于保护等待任务链表的自旋锁

可以用init_rwsem(sem)初始化读写信号量,也可以用DECLARE_RWSEM(name)声明并初始化一个信号量
void down_read(struct rw_semaphore *sem)
int down_read_trylock(struct rw_semaphore *sem)//尝试为读获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0
void down_write(struct rw_semaphore *sem)
int down_write_trylock(struct rw_semaphore *sem)//尝试为读获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0
void up_read(struct rw_semaphore *sem)  
void up_write(struct rw_semaphore *sem)  
void downgrade_write(struct rw_semaphore *sem)//它将一个写信号量降格为读信号量,并唤醒等待队列上的读任务。因此它的调用者应该持有写信号量

二、顺序锁

1.顺序锁的概念

使用读写锁时,读锁和写锁的优先级是一样的。2.6内核引入的顺序锁和读写自旋锁相同,区别在于它给写者赋予了更高的优先级:在使用顺序锁时即便读者正在进行读操作,写者也可以进行写动作。读写锁的优点在于写者永远不会由于有读者正在进行读而等待,其缺点在于读者可能需要尝试读好多次才能读到合法的数据。

不是所有的数据类型都能用顺序锁来保护,如果要使用顺序锁,以下原则必须被遵循:

  1. 被保护的数据结构不能包含由写者保护而由读者释放的指针
  2. 读者临界区的代码不能有副作用
  3. 读者临界区应该很小,而且写者应该尽量少获取顺序锁,否则重复的读会造成一些性能损伤

2.数据结构

顺序锁使用数据结构seqlock_t表示,它包含两个域:

  • 自旋锁lock
  • 整数序列号

序列号作为顺序计数器存在。每个读者必须至少读这个值两次,一次在读数据之前,一次在读数据之后,如果两次读获取的序列号相同,则读到了一个合法的值,否则说明在读的过程中有写者更新了数据,因此读者需要重新读取。
有两种方法可以初始化顺序锁:

  1. seqlock_t lock1 = SEQLOCK_UNLOCKED;
  2. seqlock_t lock2; seqlock_init(&lock2);

这两种方法都会把顺序锁初始化未上锁状态。

1.写操作

写者必须先获取锁,再操作,然后再释放锁。
write_seqlock:用于为写获取顺序锁,它会获取顺序锁中的自旋锁,然后将顺序锁的序列号加1
write_sequnlock:用于释放顺序锁,它也会增加顺序锁的序列号,然后释放顺序锁中的自旋锁
这种设计确保了写者正在写数据并且没有完成时序列号为奇数,没有写者在修改数据时,序列号为偶数。

2.读操作

对于读者来说,它需要采取下列形式的操作序列:
    unsigned int seq;
    do {
        seq = read_seqbegin(&seqlock);
        /* ... CRITICAL REGION ... */
    } while (read_seqretry(&seqlock, seq));
read_seqbegin:返回顺序锁当前序列号的值
read_seqretry:如果指定的值和顺序锁的序列号的值不等或者顺序锁的序列号为奇数,则返回1。
需要注意的是,读者并不会关闭内核抢占,而由于写者获取了顺序锁中的自旋锁,因而它会禁止内核抢占。

三、Read-Copy Update (RCU)

RCU是另外一种被设计用来在SMP环境下保护主要操作是读操作的数据同步技术。RCU允许多个读者和多个写者同时并发操作。RCU没使用任何锁也没使用任何由多个CPU共享的计数器;相比于读写自旋锁和顺序锁,这是一个极大地优势。
RCU有极大地优势,但是也有很大的限制,它限制了它所能保护的数据结构:

  • 被保护的资源应当是动态分配的、通过指针来存取的, 并且所有对这些资源的引用必须由原子代码持有
  • 当进入由RCU保护的临界区时不能休眠

RCU操作包括读操作,写操作,以及释放旧版本的操作组成。

1.写操作

它的实现原理是:当数据结构需要改变时,写线程做一个拷贝,改变这个拷贝(这里需要一个内存屏障,以保证更新能被其它CPU看到),接着使相关的指针指向新的版本,当内核确认没有CPU还在引用旧版本时旧的版本就可以被释放.

2.读操作

当内核代码想要读取一个由RCU保护的数据结构时,它

  1. 调用rcu_read_lock(相当于preempt_disable)
  2. 进行读访问
  3. 调用rcu_read_unlock(相当于preempt_enable)

这里需要注意的是在1和3之间的代码不允许休眠。

3.释放旧的版本

在RCU中关键的是旧版本何时释放。由于其它处理器上的代码可能还有对旧的数据的引用,因而不能立即释放它。内核必须在它确保已经没有任何指向旧版本的引用时才能释放旧版本。实际上,只有当所有读者都调用了rcu_read_unlock后才能释放旧的拷贝。内核要求每个读者在开始下列动作之前调用rcu_read_unlock宏:

  • CPU进行进程切换
  • CPU开始在向用户模式转变
  • CPU开始执行idle进程

call_rcu函数由写者调用以清除旧的数据结构。该函数原型如下:
void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg);

  • head:其中head是rcu_head类型的数据结构指针,它通常嵌入在受保护的数据结构中。
  • func:在可以释放RCU数据结构时会被调用以释放旧的数据结构

call_rcu函数会在rcu_head描述符中存放回调函数的地址和它的参数,然后将描述符插入到一个每CPU回调链表中。内核会定期检查是否可以释放RCU数据结构了,如果是的话,就会由一个tasklet调用回调函数。

四、完成量(Completions)

1.完成量的概念

内核中常见的一种场景是在当前任务启动另外一个任务,然后等待该任务完成做完某个事情。考虑使用信号量来完成这个工作:
struct semaphore sem;
init_MUTEX_LOCKED(&sem);
start_external_task(&sem);
down(&sem);
当外部完成我们期望的动作时,它调用up(&sem)。
但是信号量并不是特别适合这种场景,信号量对“可用”的情况做了优化,即使用信号量时期望在大部分情况下它是可用的。然而在上述场景中很显然down必然走的是信号量不可用的分支。
completion是用来解决这种问题的一种机制。completion允许一个任务告诉另一个任务工作已经完成。

2.数据结构和相关API

内核使用数据结构completion来表示completion
void init_completion(struct completion *x)
可以使用该函数完成completion的初始化或者通过DECLARE_COMPLETION(work)声明并初始化一个completion,INIT_COMPLETION(x)宏用户初始化completion x。
void wait_for_completion(struct completion *c);
该函数在c上等待,并且不可打断。如果调用了wait_for_completion而没有任何任务调用complete,则wait_for_completion的将永远等待。
wait_for_completion_interruptible(struct completion *x)
该函数在x上等待,但是可能在等待过程中被打断,当由于被打断而返回时,返回值为-ERESTARTSYS;否则返回0
void complete(struct completion *c);
void complete_all(struct completion *c);
这两个函数可用于唤醒在c上等待的任务。区别在于complete只唤醒一个等待的任务,而complete_all唤醒所有的。
completion 机制的典型使用是在模块退出时与内核线程的终止一起. 在这个原型例子里,
void complete_and_exit(struct completion *c, long retval);
唤醒在c上等待的任务,并且以retval退出本任务。

五、关闭本地中断

关闭中断是一种设置临界区的方法。当关闭了中断时,即便是硬件中断也无法打断代码的运行,因而它可以保护同时被中断服务程序访问的数据结构。但是需要注意的是关闭本地中断并不能保护可能被多个CPU访问的数据结构。因此在SMP架构下,往往需要用关闭中断和自旋锁想结合的方式来保护被中断服务程序使用的共享资源。
local_irq_disable() 宏用来在本地CPU关闭中断
local_irq_enable() 宏用来在本地CPU打开中断, which makes use of the of the sti assembly language instruction, enables them. As stated in the 使用这两个函数的问题在于,在需要的时候我们可以简单的关闭中断,但是简单粗暴的打开中断不一定是正确的,因为在我们关闭中断时,中断可能已经是关闭的,这时如果我们简单的打开了中断就可能导致问题。
local_irq_save:关闭中断并且保存中断状态字
local_irq_restore:以指定的中断状态字恢复中断
这两个宏就很好的解决了问题,因为local_irq_restore只是将中断恢复到了我们调用local_irq_save时的状态。

六、使能和关闭可延迟函数

由于可延迟函数在不可预期的时间点被执行,因而被可延迟函数访问的数据结构也需要进行保护。
禁止可延迟函数执行的最简单的办法是关闭本地中断,因为这样中断服务程序就没办法执行了,也就没办法启动可延迟函数。
由于软中断在处于中断状态时不会执行,而tasklet是基于软中断实现的,因而只要禁止本地的软中断就可以在本地CPU上禁止可延迟函数。
local_bh_disable宏用于将本地CPU的软中断计数加1,因而就禁止了本地的软中断。
local_bh_enable宏用于将本地CPU的软中断计数减1,用于打开本地软中断
这两个函数可以都可以被重复调用,但是调用了多少次local_bh_disable,就要相应的调用多少次local_bh_enable才能打开软中断

七、选择同步技术

有很多技术都可用于避免访问共享的数据时出现竞态的手段。但是各种手段对系统性能的影响是不同的。但是作为一条原则,应该使用在该场景下能获得最大并发等级或者说并发数量的技术手段。系统的并发等级取决于:

  • 可以并发操作的I/O设备数
  • 做有效工作的CPU数

为了最大化I/O吞度量,应该使得关闭中断的时间尽可能短。

为了提高CPU效率,应该尽可能避免使用自旋锁。因为它不仅导致自旋的CPU处于忙等状态,而且会对高速缓存造成不利的影响。

取决于访问共享数据的内核任务的类型,需要采用的同步技术也会有区别:

任务 单处理器环境下使用的同步技术 多处理器环境下使用的额外的同步技术
可休眠任务(内线线程,系统调用) 信号量 不需要额外的同步技术
中断 关闭本地中断 自旋锁
可延迟函数 不需要 不需要或者使用自旋锁(取决于不同的tasklet是否会访问相同的数据结构)
可休眠任务+中断 关闭本地中断 自旋锁
可休眠任务+可延迟函数 关闭本地软中断 自旋锁
中断+可延迟函数 关闭本地中断 自旋锁
可休眠任务+中断+可延迟函数 关闭本地中断 自旋锁

linux内核同步之信号量、顺序锁、RCU、完成量、关闭中断【转】的更多相关文章

  1. Linux 内核同步之自旋锁与信号量的异同【转】

    转自:http://blog.csdn.net/liuxd3000/article/details/8567070 Linux 设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发访问会导 ...

  2. [内核同步]浅析Linux内核同步机制

    转自:http://blog.csdn.net/fzubbsc/article/details/37736683?utm_source=tuicool&utm_medium=referral ...

  3. Linux内核同步机制

    http://blog.csdn.net/bullbat/article/details/7376424 Linux内核同步控制方法有很多,信号量.锁.原子量.RCU等等,不同的实现方法应用于不同的环 ...

  4. Linux 内核同步机制

        本文将就自己对内核同步机制的一些简要理解,做出一份自己的总结文档.     Linux内部,为了提供对共享资源的互斥访问,提供了一系列的方法,下面简要的一一介绍. Technorati 标签: ...

  5. Linux内核同步

    Linux内核剖析 之 内核同步 主要内容 1.内核请求何时以交错(interleave)的方式执行以及交错程度如何. 2.内核所实现的基本同步机制. 3.通常情况下如何使用内核提供的同步机制. 内核 ...

  6. 浅析Linux内核同步机制

    非常早之前就接触过同步这个概念了,可是一直都非常模糊.没有深入地学习了解过,最近有时间了,就花时间研习了一下<linux内核标准教程>和<深入linux设备驱动程序内核机制>这 ...

  7. Linux内核同步机制--转发自蜗窝科技

    Linux内核同步机制之(一):原子操作 http://www.wowotech.net/linux_kenrel/atomic.html 一.源由 我们的程序逻辑经常遇到这样的操作序列: 1.读一个 ...

  8. Linux内核同步【转】

    本文转载自:http://blog.csdn.net/a775992553/article/details/8797710 Linux设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发访 ...

  9. Linux内核同步机制之(五):Read Write spin lock【转】

    一.为何会有rw spin lock? 在有了强大的spin lock之后,为何还会有rw spin lock呢?无他,仅仅是为了增加内核的并发,从而增加性能而已.spin lock严格的限制只有一个 ...

随机推荐

  1. 第二篇 Fiddler配置_浏览器&手机

    什么是Fiddler? 网络项目的开发和测试中,Fiddler是强大的抓包工具,它的原理是以web代理服务器的形式进行工作的 ,可以说是非常常用的手头工具了,本文就Fiddler使用和配置进行说明. ...

  2. [leetcode-648-Replace Words]

    In English, we have a concept called root, which can be followed by some other words to form another ...

  3. Week8 Teamework from Z.XML-Z.XML游戏功能说明

    我们小组的游戏终于新鲜出炉了,好开心~ 快来看看有什么功能吧. 游戏目标::=打倒最多的敌人,获得积分,放松心情,获取快乐. 游戏菜单::= 关于+设置+帮助+积分榜+开始游戏吧 (截图还在路上..) ...

  4. chromium源码阅读--图片处理

    JavaScript 图像替换 JavaScript 图像替换技术检查设备能力,然后“做正确的事”. 您可以通过 window.devicePixelRatio 确定设备像素比,获取屏幕的宽度和高度, ...

  5. js定时器实现图片轮播

    效果展示如下: setInterval(moverleft,3000);定时器设置为3秒,而且实现图片下方的小圆点序号跟图片对应,点击小圆点也能切换图片. 代码如下: <!DOCTYPE htm ...

  6. iOS版微信开发小结(微信支付,APP跳转微信公众号)

    最近公司心血来潮,一心要搞微信.废话不多说,直接上干货. 开发前准备: 1.在微信开发者平台获取开发者认证:(一年300元人民币) PS:具体流程按照微信流程指示操作即可,在这就不废话了. 2.下载微 ...

  7. JVM内存区域配置

    堆内存:新域+旧域 设置堆内存初始化大小 java -Xms128m 设置堆内存初始化大小128MB 设置堆内存最大大小 java -Xmx256m 设置堆内存最大256MB 通常将堆内存的初始化大小 ...

  8. BZOJ4651 NOI2016网格(割点)

    首先显然可以通过孤立角落里的跳蚤使其不连通,所以只要有解答案就不会大于2.同样显然的一点是当且仅当跳蚤数量<=2且连通时无解.做法其实也很显然了:特判无解,若跳蚤不连通输出0,否则看图中是否无割 ...

  9. hdu 1856 More is better (并查集)

    More is better Time Limit: 5000/1000 MS (Java/Others)    Memory Limit: 327680/102400 K (Java/Others) ...

  10. P2483 【模板】k短路([SDOI2010]魔法猪学院)

    题目背景 感谢@kczno1 @X_o_r 提供hack数据 题目描述 iPig在假期来到了传说中的魔法猪学院,开始为期两个月的魔法猪训练.经过了一周理论知识和一周基本魔法的学习之后,iPig对猪世界 ...