第四章 互斥与同步
linux系统下并发的来源主要有:
. 中断处理路径
当系统正在执行当前进程时, 发生中断, 中断处理函数和被中断的进程之间形成的并发.在单处理器中,虽然
中断处理函数的执行路径与被中断的进程间不是真正严格意义上的并发, 然后中断处理函数和被中断进程间
却可能形成竞态, 软中断的执行也可以归到这类并发中.
. 调度器的可抢占性
在单处理器上, 因为可抢占性, 导致的进程与进程之间的并发.
. 多处理器的并发执行
多处理器系统上进程与进程之间是严格意义上的并发, 每个处理器都可独自调度运行一个进程, 在同一时刻
有多个进程在同时运行.
4.2 local_irq_enable与local_irq_disable
在单处理器不可抢占系统中, 使用local_irq_enable/local_irq_disable是消除异步并发源的有效方式, 驱动程
序中应该避免使用这个两个宏, 但spinlock等互斥机制中常常用到这两个宏.
local_irq_enable宏用来打开本地处理器中断, local_irq_disable用来关闭处理器中断.
其中trace_hardirqs_on()和trace_hardirqs_off()用做调试, 重点关注raw_local_irq_enable()/raw_local_
irq_disable(), 这两个宏的具体实现都信赖于处理器体系架构, 会同处理器有不同的指令来启用和关闭处理
器响应外部中断的 能力, ARM平台则使用CPSIE指令.
.................注意这里是关闭的当前处理器的中断响应能力, 非单中断号响应能力
????????????这样关掉后, 是关的普通中断, 还是将timer中断也关闭了. faster中断呢?如果timer都没有, 那么
就会抢占内核也不会有冲突了.
??????????抢占内核的抢占发生在哪些情况下, 目前所知是在中断程序返回时, 是否还有在系统调用返回时?
local_irq_enable与local_irq_disable的变体是local_irq_save与local_irq_restore宏.这两个宏相对于lo
cal_irq_enable/disable最大的不同在于, local_irq_save会在关中断前, 将处理器当前的标志位保存在一个
unsigned long flags中, 在调用local_irq_restore的时候, 将保存的flags恢复到处理器的flags寄存器中,
这样做的目的是防止在一个中断关闭的环境中, 因为调用local_irq_disable, 将之前的中断响应状态破坏掉.
在单处理器不可抢占系统中, local_irq_enable/disable及其它体对共享数据保护是简单有效的, 但使用时应
该注意, 长时间的关中断进行互斥保护, 会影响到系统的性能.
4.3 自旋锁
自旋锁的最目的是在多处理器系统中提供对共享数据的保护, 核心思想是: 设置一个在多处理器之间共享的全
局变量锁v, 当V=1时为上锁, V=0时为解锁, 如果cpu1上的代码要进入临界区, 要先读V的值, 判断是否为0,
如果非0, 表明其它cpu正在对数据进行访问, 此时cpu1进入忙等待的自旋状态,如果v=0, 表示没有其它cpu进
入临界区, 此时cpu1可以访问该资源, 它先把v置1, 然后进入临界区, 访问完毕离开临界区时将v置为0(解锁
状态)
实现的关键在于, 必须确保处理器1读取V,判断V,更新V这一操作序列是原子操作.
4.3.1 spin_lock
spinlock_t实际上就是个volatile unsigned int变量
spin_lock函数中调用raw_spin_lock是个宏, 实两天与处理器相关.
调度器的可抢占特性. 没有定义时, 是空定义.
真正的上锁操作发生在do_raw_spin_lock函数中, 在讨论函数实现细节前, 看来看看为什么要关闭系统的可
抢占性, 比如在一个系统调用时, 发生一个外部中断, 当中断函数返回时, 由于可抢占性, 将会出现另一个
调度点, 如果cpu出现一个比当前被中断进程更高优先级的进行, 那么中断进程就会被换出.
????????进入自旋锁,关闭内核抢占, 是为了保护共享的资源不会受到破坏. 确坏执行进程的执行顺序, 这个顺
序不可以被破坏吗?
函数接着调用do_raw_spin_lock开始真正的上锁骨操作
ARM处理器上专用以实现互斥访问的指令ldrex和strex来达到原子操作目的.
. "ldrex%0, [%1]" 相当于"tmp=lock->raw_lock", 即读自旋锁V的初始状态, 放在临时变量tmp中.
. "teq %0, #0" 判断v是否为0, 如果不为0, 表示此时自旋锁处于上锁状态, 代码执行"bne 1b"指令, 开始
进行忙等待: 不停地到标号1处读取自旋锁的状态, 并判断是否为0.
. "strexeq %0, %2, [%1]",指令是说, 如果V=0, 表示可以进入临界区, 那么就用常量1来更新V的值, 并把
更新操作执行结果放回变量tmp中.
. "teqeq %0, #0"用来判断上一条指令对V的更新操作结果是否为0, 如果是0表示更新成功, 如果V=1, 代码
可以进入临界区. 如果tmp!=0, 表明更新V没有成功, 代码执行"bne 1b"进入忙等待.
这里之所有要执行"teqeq %0, #0", 正是要利用ldrex和strex指令来达到原子操作的目的.
strex和ldrex加入了对共享内存互斥访问的支持, 这两条指令是在ARM V6中引入的.
4.3.2 spin_lock的变体spin_lock_irq
如果进程A持有锁, 并进入了临界区, 此时中断来了, 中断中又去获取锁, 所以会出现死锁, 为了避免这种情
况, 于是出现了spin_lock_irq和spin_lock_irqsave函数.
其中的raw_spin_lock_irq函数的实现, 相对于raw_spin_lock只是在调用preempt_diable之前又调用了local
_irq_disable, 这样确保在获得一个锁时不会发生中断, local_irq_disable只能用来关闭本地处理器中断,
但如果处理器B上的进程也去获得锁时, 会有怎么样的情况呢, 因为此时处理器A上的进程可以继续执行, 在
离开临界时会释放锁, 这样处理器B中断处理函数就可以结束此前的自旋状态.
这说明通过自旋锁进入临界区中的代码必须尽可能短的时间内执行完, 因为它执行越长, 别的处理器需要自
旋的等待时间越长, 最差的情况是进程在临界区中被换出处理器, 所以作为使用自旋锁时一条确定的规则,
任何拥有自旋锁的代码都是必须是原子的, 不能休眠.
............使用自旋锁的代码, 必须是原子的, 不能休眠.
如果知道一个自旋锁在中断处理的上下文中有可能被使用到时, 应使用spin_lock_irq函数, spin_lock只能
在确定中断上下文中不会使用到自旋锁的情形下使用.
与spin_lock_irq类似的还有一个spin_lock_irqsave宏, 它与spin_lock_irq最大的区别是, 关闭中断前会将
处理器当前的FLAGS寄存器的值保存在一个变量中, 在调用spin_unlock_irqrestore释放锁时, 会将保存的FL
AGS重新写回寄存器中.
另一个与中断处理相关的spinlock版本是spin_lock_bh函数, 该函数用来处理进程与延迟处理导致的并发中
的互斥问题, 相对于spin_lock_irq函数, 它用来关闭softirq的能力.
最后,自旋锁还设计了一组对应的非阻塞的版本, 在获得锁时, 发现处于上锁状态, 会直接返回0, 而不是自旋
, 如果成功获得锁, 将返回1.
4.3.3 单处理器上的spin_lock函数
在单处理器系统上, 内核可分为抢点和不可抢点两种:
对于第一种, 并发来源主要是外部中断等异步事件, 所以在这种系统下, 进入临界只只需要关闭处理器的中
断即可(local_irq_disable/local_irq_save), 在离开临界区时只需要打开/恢复处理器中断.
对于第二种, 并发来源除了中断与异常等异步事件外, 还包括因为可抢占性导致的进程间的并发, 所以进入临
界区时除了要关闭处理器中断, 还要关闭内核调度器的可抢占性.
..............关于关闭处理器的调度器可抢占性, 内核已经自己实现.
linux内核为了统一单处理器和多处理器的竞态处理代码, 将spin_lock函数及其它体延伸到了单处理器上, 对
单处理器而言, 如果是非抢占式系统, spin_lock/spin_unlock将等同于空操作; 对于可抢占系统, spin_lock
/spin_unlock刚分别用来关闭和打开抢占性.
而spin_lock_irq/spin_lock_irqsave等在单处理器上等同于local_irq_disable/local_irq_save, 如果是可
抢式系统, 那么需要在上述的中断控制函数后, 再加上内核可抢占性的preempt操作.
从代码移植性的角度来考虑, 即使在单处理器上只需要调用local_irq_disable来对共享资源进行保护时, 也
应该使用spin_lock_irq.
4.3.4 读者与写者自旋锁rwlock
spin_lock类的函数进入临界区时, 对临界区中的操作行为不做细分, 与spin_lock类比起来, 这种锁比较有意
思的地方在于: 它允许任意数量的读者同时进入临界区, 但写者必须进行互斥访问. 一个进程想去读的话, 必
须检查是否有进程正在写, 有的话必须自旋, 一个进程想去写的话, 必须先检查是否有进程正在读或写, 有的
话必须自旋.
相比较spinlock, rwlock在锁的定义以及irq与preempt操作方面没有任何不同, 唯一不同的是, rwlock针对读
和写都设计了各自的锁操作函数
写入者上锁操作:
读取者的上锁操作:
. 如果当前有进程正在读, 其它进程可以读, 但不能写.
当一个进程试图写, 只要有其它进程正在读或写, 它都必须自旋.
当一个进程试图读, 只有没有其它进程正在写, 它都可以获得锁.
在一个存在大量读操作,而数据的更新较少发生的系统中, 使用读写锁对共享资源进行保护, 相对普通自旋锁
, 无疑会大大提升系统性能.
4.4. 信号量(semaphore)
相对于自旋锁, 信号量的最大特点是允许调用它的线程进入睡眠状态, 这意味着试图获得某一信号量的进程
会导致它对处理器拥有权的丧失,也即出现进程的切换
4.4.1 信号量的定义与初始化
入临界区的执行路径的个数.
wait_list 用于管理所有在该信号量上睡眠的进程, 无法获得信号量的进程将进入睡眠状态.
如果驱动定义了一个信号量变量, 注意不要直接对该变量的成员进行赋值, 应该使用sema_init函数来初始化
该信号量. sema_init函数定义如下:
所以sema_init(struct semaphore *sem, int val)调用会把信号量sem的lock值设为解锁状态, count设定为
函数调用参数val, 同时初始化wait_list链表头
4.4.2 DOWN操作
信号量上的操作主要有DOWN和UP, DOWN操作有:
操作引起混乱, 进入临界区, 判断sem->count是否大于0; 如果大于0, 成功获得信号量, count就减1, 然后退
出. 如果count不大于0, 无法获得信号量, 此时调用__down_interruptible, 后者完成一个进程无法获得信号
量时的操作, 在内部调用__down_common, 调用时参数state = TASK_INTERRUPTIBLE, timeout = LONG_MAX.
__down_common首先通过对struct semaphore_waiter变量waiter的使用, 使当前进程放到信号量sem的成员变
量wait_list管理的队列中, 接着在for循环中把当前进程的状态设置为TASK_INTERRUPTIBLE, 再调用schedu
le_timeout使当前进程进入睡眠状态. 函数将停在schedule_timeout调用上, 直到再次被调度执行, 当该进
程再一次被调度执行时, schedule_timeout开始返回, 被再次调度的原因有: 如果waiter.up不为0, 说明进
程在信号量sem的wait_list队列中被该信号量的up操作所唤醒, 进程可以获得信号量, 返回0. 如果进程是被
用户空间发送的信号所中断或者超时引起的唤醒, 则返回相应的错误码, 因此对down_interruptible的调用
总是应该坚持检查其返回值, 以确定函数是已经获得了信号量还是因为操作被中断而需特别处理, 通常驱动
对返回的非0值要做的工作就是返回-ERESTARTSYS.
睡眠状态直到有别的进程释放了该信号量. 对用户空间来说, 如果应和程序阻塞在驱动中的down函数中, 将
无法通过一些强制措施来结束该进程, 如Ctrl+D. 因此, 除非必要, 否则驱动应该避免使用down函数.
无法获得信号量, 则将返回一错误码-ETIME, 在到期前进程的睡眠状态为TASK_UNINTERRUPTIBLE. 成功返回0.
4.4.3 UP操作
wait_list不为空, 说明有其它进程正睡眠在wait_list上等待信号量, 此时调用__up(sem)来唤醒进程:
__up函数首先用list_first_entry取得sem->wait_list链表上的第一个waiter节点C, 然后将其从sem->wait_
list链表中删除, waiter->up = 1, 最后调用wake_up_process来唤醒waiter C上的进程C, 这样进程C将大之
前down_interruptible调用的schedule_timeout(timeout)处本来, waiter->up = 1, down_interruptible
返回0, 进程C获得信号量, 进程D和E继续等待直到有进程释放信号量或者被用户空间中断掉.
????????此时唤醒只是将waiter->up =1, 但并没有将sem->count++, 对吗?
即使不是信号量的拥有者, 也可以调用up函数来释放一个信号量.
在linux系统中, 信号量的一个常见用途是实现互斥机制, 这种情况下信号量的count为1, 任意时刻只允许一
个进行进入临界区. 内核提供了一个宏DECLARE_MUTEX专门用于这种用途的信号量定义和初始化.
该宏定义了一个count=1的信号量变量name, 并初始化了相关成员
????????因为信号量的down_interruptible中包含spinlock_irqsave, 所以此时semaphore也是禁中断,禁抢占的
4.4.4 读取者与写入者信号量rwsem
. activity=0, 表时当前信号量上没有任何活动的读者/写者.
. activity=-1, 表明当前在信号量上有一个活动的写者.
. activity为正值n, 表明当前在该信号量上有n个活动的读者.
静态定义一个rwsem变量同时用DECLARE_RWSEM宏进行初始化.
#define DECLARE_RWSEM(name)
对一个rwsem变量动态初始化使用init_rwsem宏. rwsem的初始状态是没有任何活动的读者和写者.
用count=1的信号量实现的互斥方法不是linux下的经典用法,linux内核针对count=1的信号量重新定义了一个
数据结构struct mutex, 一般称为互斥锁. 同时内核根据使用场景的不同, 把用于信号量的DOWN和UP操作在
struct mutex上作了优化和扩展.
4.5.1 互斥锁的定义和初始化
互斥锁mutex的概念本来就来自semaphore, 如果去掉那些跟调试相关的成员, struct mutex和struct semap
hore并没有本质的不同:
4.5.2互斥锁的DOWN操作
互斥锁在mutex上的DOWN操作在linux内核中为mutex_lock函数:
速判断当前可否获得互斥锁, 如果成功获得, 则函数直接返回, 否则进入到__mutex_lock_slowpatch函数中,
这种设计是基于这样的一个事实在: 想要获得某一互斥锁的代码绝大部分都可以成功获得. 所以进入__mutex
_lock_slowpath的概率很低.
__mutex_fastpath_lock是一个平台相关的函数:
__res的当前值来更新cuont->counter, 这里说的"试图"是因为这个更新的操作未必会成功,
可能有别的进行也在操作count->counter, 为不使这种可能的竞争引起对count->counter的
值更新混乱, 所以这里用了ARM指令中用于实现互斥访问的指令ldrex和strex, ldrex, strex
保证了对count->counter的"读-更新-写"操作序列原子性.
接下来在__res |= __ex_flag执行完之后, 通过if语句判断__res是否为0, 有两种情况会导致
__res不为0: 1是在调用这个函数前count->counter=0, 表明互斥锁已经被其它进程获得, 这样
第二行处的__res=-1; 2是在第三行的更新操作不成功, 表明当前有另外一个进程也在对count->
counter进行同样的操作. 这两种情况都将导致__mutex_fastpatch_lock不能直接返回, 而是
进入fail_fn, 也就是调用__mutex_lock_slowpath.
此处的unlikely是GCC编译优化扩展的宏, 表时__res!=0为真的可能性很小, 编译器借此可以调
整一些编译后的代码顺序, 达到某种程序的优化.
如果__mutex_fastpath_lock函数不能第一时间获得互斥锁返回, 那么将进入__mutex_lock_slowpath
, 正如其名所示, 代码将进入一段艰难坎坷的旅途.
__mutex_lock_slowpath函数与信号量DOWN操作中的down函数非常相似, 不过__mutex_lock_slowpath
在把当前进程放入mutex的wait_list之前会试图多次询问mutex中的count是否为1, 也就是说在
进入wait_lsit前会多次考察别的进程是否已经 释放了这个互斥锁, 这主要基于这样一个事实,
拥有互斥锁的进程总是会在尽可能短的时间里释放, 如要别的进程已经释放了该互斥锁, 那么
当前进程将可以获得该互斥锁而没必要去睡眠.
4.5.3 互斥锁的UP操作
互斥锁的UP操作为mutex_unlock
分别用于对互斥锁的快速和慢速解锁.
, 导致代码中__orig不为0也有两种情况: 1是调用这个函数前count->counter不为0, 表明当前进程占有的互
斥锁期间有别的进程竞争该互斥锁; 2是对count->counter的更新操作不成功, 表明当前有另外一个进程也在
对count->counter进程操作, 这种情况主要是别对进程此时调用mutex_lock函数导致竞争, 因为互斥的原因,
别的进程此时不可能调用mutex_unlock, 这种情况的处理是非常重要的, 不只是关系到count->counter正确更
新的问题, 还涉及能否防止一个唤醒操作的丢失.
在没有别的进程竞争该互斥锁的情况下, __mutex_fastpath_unlcok函数要完成的工作最简单, 把count->cou
nter的值加1然后返回, 如果有竞争, 那么函数进入__mutex_unlock_slowpath, 这个函数主要用来唤醒在当
前mutex的wait_list中休眠的进行, 如同up函数一样.
4.6 顺序锁seqlock
设计思想是, 对某一共享数据读时不加锁, 写的时候加锁, 为了保证读的过程中不会因为写者的出现导致该
共享数据的更新, 需要在读者和写者之前引入一整值, 如果与之前读的值不一样, 则说明本次读操作过程中
发生了数据更新, 读操作无效. 因此要求写者在开始写的时候要更新sequence的值.
程序中静态定义一个seqlock并同时初始化, 可以使用DEFINE_SEQLOCK宏, 该宏会定义一个 seqlock_t变量,
并初始化其sequence为0, lock为0.
ck,那么需要更新sequence的值以便让其它写者知道共享数据发生了更新, 写者与写者间不需要sequence.
写者在seqlock上的解锁操作write_sequnlock:
否在写.
主要工作是释放自旋锁lock, 至于写者对sequence的更新, 主要是用来告诉我读取者有数据更新, 所以必须确
保sequence的值在写入的前后发生变化. 在此基础上sequence提供另外一个信号是写入过程有没有结束, sequ
ence的最低位为0, 表明写入过程结束, 否则表明写入过程正在进行.
static inline int write_tryseqlock(seqlock_t *sl);
写者使用write_tryseqlock来保证无法获得lock时, 不让自己进入自旋状态而直接返回0, 成功获得锁返回1.
读者在读前需要先调用read_seqbegin, 该函数返回读开始前的sequence值.
是其实际使用的地方, 写者的实际写入操作占用时间不应太长
内核还给读者提供了一个read_seqretry函数, 与read_seqbegin的返回值一起使用, 来判断本次读操作是否有
效:函数的参数start是读者在读操作前调用read_seqbegin获得的初始值, 如果本次读无效, 那么read_seqre
try返1, 否则返回0.
流程如下: 写之前write_seqlock获得锁, 更新sequence值, 成功后开始写, 写结束释放锁.
读之前先得到sequence的值start, 无需获得锁, 然后开始读, 读结束再判断start是否有更新, 如
果失败,则重新读取.
读者与写者的对应版本:
者都互斥. 因此要保护的资源很小很简单, 会很频繁被访问并且写入操作很少发生且必须快速时, 就使用seq
lock.
4.7 RCU
RCU全称是Read-Copy-Udate, 读/写--复制--更新, 在Llinux提供的所有内核互斥设施中属于一种免锁机制.同
前面讨论的锁一样,RCU的适用模型也是读者与写者共享的系统, 与前边的锁不一样的是,RCU中的读和写操作无
须考虑两者间的互斥问题. 前面的锁都要涉及内存操作, 同时还伴有内存屏障方法的引入, 这使得锁操作的系
统开锁变得很大. 在此基础上, linux内核加入了对RCU这种免锁的互斥访问机制的支持.
RCU的原理简单地说, 是将读者和写者要访问的共享数据放在一个指针P中, 读者通过P来访问其中的数据, 而
写者通过修改P来更新数据, 大量的工作集中在写者一方. 免锁的实现必定要通过双方属守一定的规则才能达
成.
4.7.1 读者的RCU临界区
对读者来说, 要访问共享数据, 首先调用rcu_read_lock和rcu_read_unlcok函数构建自己所谓的读者侧临界区
, 然后在临界区中获得指向共享数据的指针, 实际的读操作就是对该指针的引用, 一个关于读者的明确规则是
, 对指针的引用必须在临界区中完成, 离开临界区后不应该出现任何形式的对该指针的引用. 在临界区中, 关
闭内核可抢占性意味着在临界区中不会因为中断的发生而导致进行的切换, 明确规则, 临界区中的代码不能发
生睡眠.
rcu_read_lock和rcu_read_unlock实际做的工作仅仅是关闭和打开内核的可抢占性.
4.7.2 写入者的RCU操作
RCU操作中写者要写成的工作是重新分配一个被保护的共享数据区, 将老数据区的数据复制到新数据区, 然后
再根据需要修改新数据区, 最后用新数据区指针替换老的指针, 替换指针的操作是原子操作. 在写者做完这些
工作后, 后续的所有RCU读操作都将访问这个新的共享数据区, 写者用新指针替换老指针后还不能马上释放老
指针指向的数据区所占用的内存空间, 因为系统中还可能存在对老指针的引用.
主要有以下两种情况:
1. 在单处理器来看, 读者进入RCU临界区后, 刚获得共享区的指针后发生一个中断, 如果写者恰好是中断处理
函数中的行为, 那么当中断返回后, 被中断进行的RCU临界区中继续执行时, 将会继续引用老指针.
2. 在多处理器系统, 当处理器A上的一个读者进入RCU临界区并获得共享数据区中的指针后, 在还没有来得及
引用该指时, 处理器B上的写者更新了指向共享数据区的指针, 这样处理器A上的读者也将引用到老指针.
因此,写者在替换掉共享区的指针后, 老指针所指向的共享数据区所在的空间还不能马上释放. 写者需要和内
核共同协作, 在确定所有对老指针的引用都结束后才可以释放老指针指向的内存空间. 为此,写者在用新指针
替换掉老指针后需要做的是, 调用call_rcu函数向内核注册一个回调, 内核在确定所有对老指针的引用都结
束时会调用回调函数, 回调的功能主要是释放老指针指向的内存空间.
, call_rcu中的参数func就是指向该架设函数的指针. head为调用func时传递到func中的参数. 实际使用中,
会把struct rcu_head内嵌到共享数据所在的结构体中, 这样在回调函数中通过传进来的struct rcu_head指针
, 使用container_of宏获得指向旧的共享数据区指针, 然后调用kfree释放旧的数据区.
内核确保没有读者对老指针的引用是基于以下规则:
所有可能对共享数据区指针的不一致引用一定是发生在读者的RCU临界区, 因为临界区由rcu_read_lock和unl
cok来界定, 所以就单处理器而言, 在临界区中一定会不发生进程切换, 所以如果在某一CPU上发生了进程切换
, 那么所有对老指针的引用都会结束, 之后读者再进入RCU临界区都将看到新指针. 因此,内核确定没有对老指
针的引用条件是:系统中所有处理器上都至少发生了一次进程切换.
4.7.3 RCU使用的特点
RCU实质上是对读者与写者自旋锁rwlock的一种优化: RCU的读者在读数据时除了关内核可抢占性外, 与普通数
据读取操作没有任何区别, 读者也不关心当前有没有写者在对共享数据区进行操作, 而rwlock, 在读者工作时
, 必须确保没有写者在工作, 否则读取进程将进入自旋, 所以RCU可以让多个读者与写者同时工作.
相对于读者, RCU写者的开销比较大, 它需要申请新的内存空间, 正常的数据更新操作, 向内核注册回调函数
, 同时也要考虑与其他写者之间的互斥, 但与rwlock不一样的是, 写者不需要考虑与读者互斥.
可见,RCU读者性能的提升是在增加写者负担的前提下完成的. 如果在一个读者与写者共存的系统中, 按照设
计者的说法, 如果写者的操作比较在10%以上, 就应该考虑其它的互斥方法, 反之采用RCU的实现可以获得更高
的性能.
synchronize_rcu, 这个函数类似于call_rcu, 它可能会阻塞, 因为它要等所有对老指针的引用都结束时才返
回, 函数返回意味着系统中所有的老指针的引用都消失了.
4.8 原子变量与位操作
atomic_inc执行原子加函数.
atomic_t是个struct 类型, 在需要整型变量的地方不能直接用atomic_t变量, 否则会产生编译错误. 另外在
实际使用时应注意, atomic_t型变量只能保证自身操作的原子性, 对一个由多个整形变量组成的共享数据, 即
便把这些变量全部声明为原子型, 对它们的使用也都是用atomic类的函数, 也不能保证对该共享数据操作的原
子性, 此时需要用到前边提到的其它互斥方法.
与单个原子变量相对的是位操作的原子性.
4.9 等待队列
4.9.1 等待队列头wait_queue_head_t
定义等待队列:
1. 通过DECLARE_WAIT_QUEUE_HEAD宏来完成等待队列头对象的静态定义与初始化:
2. 通过init_waitqueue_head宏在程序运行期间初始化一个头节点对象
在其上的进程在被唤醒时具有排他性.
private, 等待队列上的私有数据, 实际使用中用来指向睡眠在该节点上的进各的task_struct结构
func, 当该节点上的睡眠进程需要被唤醒时执行的唤醒函数.
task_list, 用来将各独立的等待队列节点链接起来形成链表.
程序可以通过DECLARE_WAITQUEUE来定义并初始化一个等待队列节点:
等待队列常用的模式便是实现进程的睡眠等待, 当进程需要的资源无法获得时, 进程将进入睡眠, 并让出处理
器,进入睡眠状态, 意味着进程将从调度器的运行队列中移除, 此时进行被挂在某一等待队列的节点中.
为了实现进程的睡眠机制, 系统会产生一个新的等待队列节点, 然后将教程的task_struct对象放到等待队列
节点对象的private成员中.
内核中对等待队列的核心操作是等待wait与唤醒wake up.
4.10 完成接口completion
#define DECLARE_COMPLETION(work) \
struct completion work = COMPLETION_INITIALIZER(work)
如果要重新初始化一个已使用过的struct completion, 可以使用INIT_COMPLETION宏
#define INIT_COMPLETION(X) ((x).done = 0)
如果要动态初始化一个struct completion变量, 应该调用init_completion函数
wait队列中. wait_for_common内部调用了do_wait_for_common来做这件事
等待者首先检查completion中的done成员, 它表示当前在completion上的完成者数量, 如果没有完成者, 那么
等待者进入睡眠队列进行等待, 这种睡眠是不可中断的, DECLARE_WAITQUEUE定义并初始化一个等待节点wait,
代表当前进程的current变量将会记录到wait的private变量, wait中的func函数指针指向default_wake_func
tion, 当wait上的进程被唤醒时将调用该函数. 进程需要睡眠时, 调用__add_wait_queue_tail_exclusive把
wait节点加入到completion管理的队列尾部, wait->flags |= WQ_FLAG_EXCLUSIVE等待节点wait中的这个标记
将在完成者的唤醒操作中使用到.
若干时间之后, 进程因某种原因被唤醒, 表现为从schedule_timeout函数返回, 它将检查done成员和timeout
变量以决定后续的动作, timeout大于0, 表示进程没有超时, x->done=0表示completion上没有完成者, 此时
如果没有信号需要处理, 将继续睡眠.
如果进程睡眠超时, 将返回timeout值, 如果没有超时且有完成者在completion上出现, 进程将离开睡眠队列
, 在将完成者数量减1之后, 等待者结束等待状态返回.
如果考虑到进程进入睡眠队列的状态及睡眠超时时间的设定, 内核提供了api:
和标志__wait_up_common遍历当前completion所管理的等待队列的每一个节点, 此时nr_exclusive=1, flags
中令有WQ_FLAG_EXCLUSIVE标志, 意味着本次唤醒只唤醒一个等待者. func指向default_wake_function, 用来
做实际的唤醒工作.
相对于complete一次只唤醒一个等待者, complete_all用来唤醒completion队列上的所有等待者进程, 注意这
里complete_all在这里假设完成者的最大数量是(~0U)/2, 这是个很大的值, 现在系统中很少有等待者进程数
量会达到这个值, 因此在complete_all之后completeion中的done值将失去本来意义, 如果继续用该completi
on, 应调用前面提过的INIT_COMPLETION宏.
自旋锁不会进入睡眠, 最适合在不允许睡眠的上下文环境中执行. 如果获得锁失败, 会处于忙等待状态, 因此
要求获得锁的进程在尽可能短的时间内完成对共享资源的访问.
互斥锁的实现来源于信号量, 如果一个进程在进入临界区前试用调用互斥锁时, 有可能会进入休眠, 所以中断
上下文中严格禁止使用互斥锁和信号量.
自旋锁是一种基于忙等待的互斥机制, 但现实中被自旋锁保护的临界区代码往旆可以很快执行完, 这种情况下
用互斥锁, 可能引起进程切换的开销往往要比忙等待大得的, 因此不能以为进程睡眠一定会比忙等待更有利于
系统性能.
本文欢迎大家转载
原文出自: http://blog.csdn.net/dyron
4.1 并发的来源
并发, 是指可能导致对共享资源的访问出现竞争状态的若干执行路径, 不一定是指严格的时间意义上的并发执行linux系统下并发的来源主要有:
. 中断处理路径
当系统正在执行当前进程时, 发生中断, 中断处理函数和被中断的进程之间形成的并发.在单处理器中,虽然
中断处理函数的执行路径与被中断的进程间不是真正严格意义上的并发, 然后中断处理函数和被中断进程间
却可能形成竞态, 软中断的执行也可以归到这类并发中.
. 调度器的可抢占性
在单处理器上, 因为可抢占性, 导致的进程与进程之间的并发.
. 多处理器的并发执行
多处理器系统上进程与进程之间是严格意义上的并发, 每个处理器都可独自调度运行一个进程, 在同一时刻
有多个进程在同时运行.
4.2 local_irq_enable与local_irq_disable
在单处理器不可抢占系统中, 使用local_irq_enable/local_irq_disable是消除异步并发源的有效方式, 驱动程
序中应该避免使用这个两个宏, 但spinlock等互斥机制中常常用到这两个宏.
local_irq_enable宏用来打开本地处理器中断, local_irq_disable用来关闭处理器中断.
#define local_irq_enable() \
do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
#define local_irq_disable() \
do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
其中trace_hardirqs_on()和trace_hardirqs_off()用做调试, 重点关注raw_local_irq_enable()/raw_local_
irq_disable(), 这两个宏的具体实现都信赖于处理器体系架构, 会同处理器有不同的指令来启用和关闭处理
器响应外部中断的 能力, ARM平台则使用CPSIE指令.
.................注意这里是关闭的当前处理器的中断响应能力, 非单中断号响应能力
????????????这样关掉后, 是关的普通中断, 还是将timer中断也关闭了. faster中断呢?如果timer都没有, 那么
就会抢占内核也不会有冲突了.
??????????抢占内核的抢占发生在哪些情况下, 目前所知是在中断程序返回时, 是否还有在系统调用返回时?
local_irq_enable与local_irq_disable的变体是local_irq_save与local_irq_restore宏.这两个宏相对于lo
cal_irq_enable/disable最大的不同在于, local_irq_save会在关中断前, 将处理器当前的标志位保存在一个
unsigned long flags中, 在调用local_irq_restore的时候, 将保存的flags恢复到处理器的flags寄存器中,
这样做的目的是防止在一个中断关闭的环境中, 因为调用local_irq_disable, 将之前的中断响应状态破坏掉.
在单处理器不可抢占系统中, local_irq_enable/disable及其它体对共享数据保护是简单有效的, 但使用时应
该注意, 长时间的关中断进行互斥保护, 会影响到系统的性能.
4.3 自旋锁
自旋锁的最目的是在多处理器系统中提供对共享数据的保护, 核心思想是: 设置一个在多处理器之间共享的全
局变量锁v, 当V=1时为上锁, V=0时为解锁, 如果cpu1上的代码要进入临界区, 要先读V的值, 判断是否为0,
如果非0, 表明其它cpu正在对数据进行访问, 此时cpu1进入忙等待的自旋状态,如果v=0, 表示没有其它cpu进
入临界区, 此时cpu1可以访问该资源, 它先把v置1, 然后进入临界区, 访问完毕离开临界区时将v置为0(解锁
状态)
实现的关键在于, 必须确保处理器1读取V,判断V,更新V这一操作序列是原子操作.
4.3.1 spin_lock
static inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
spinlock_t实际上就是个volatile unsigned int变量
typedef struct raw_spinlock {
volatile unsigned int raw_lock;
} raw_spinlock_t;
typedef struct spinlock {
union {
struct raw_spin_lock rlock;
};
} spinlock_t;
spin_lock函数中调用raw_spin_lock是个宏, 实两天与处理器相关.
static inline void raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
do_raw_spin_lock(lock)
}
函数首先调用preempt_disable宏, 后者在定义了CONFIG_PREEMPT, 即在内核支持抢占式调度系统时, 将关闭
调度器的可抢占特性. 没有定义时, 是空定义.
真正的上锁操作发生在do_raw_spin_lock函数中, 在讨论函数实现细节前, 看来看看为什么要关闭系统的可
抢占性, 比如在一个系统调用时, 发生一个外部中断, 当中断函数返回时, 由于可抢占性, 将会出现另一个
调度点, 如果cpu出现一个比当前被中断进程更高优先级的进行, 那么中断进程就会被换出.
????????进入自旋锁,关闭内核抢占, 是为了保护共享的资源不会受到破坏. 确坏执行进程的执行顺序, 这个顺
序不可以被破坏吗?
函数接着调用do_raw_spin_lock开始真正的上锁骨操作
static inline void do_raw_spin_lock(raw_spinlock_t *lock)
{
unsigned long tmp;
__asm__ __volatile__ (
"1: ldrex %0, [%1]\n"
"teq %0, #0\n"
"strexeq %0, %2, [%1]\n"
"teqeq %0, #0\n"
"bne 1b"
:"=&r"(tmp)
:"r"(&lock->raw_lock),"r"(1)
:"cc");
smp_mb();
}
ARM处理器上专用以实现互斥访问的指令ldrex和strex来达到原子操作目的.
. "ldrex%0, [%1]" 相当于"tmp=lock->raw_lock", 即读自旋锁V的初始状态, 放在临时变量tmp中.
. "teq %0, #0" 判断v是否为0, 如果不为0, 表示此时自旋锁处于上锁状态, 代码执行"bne 1b"指令, 开始
进行忙等待: 不停地到标号1处读取自旋锁的状态, 并判断是否为0.
. "strexeq %0, %2, [%1]",指令是说, 如果V=0, 表示可以进入临界区, 那么就用常量1来更新V的值, 并把
更新操作执行结果放回变量tmp中.
. "teqeq %0, #0"用来判断上一条指令对V的更新操作结果是否为0, 如果是0表示更新成功, 如果V=1, 代码
可以进入临界区. 如果tmp!=0, 表明更新V没有成功, 代码执行"bne 1b"进入忙等待.
这里之所有要执行"teqeq %0, #0", 正是要利用ldrex和strex指令来达到原子操作的目的.
strex和ldrex加入了对共享内存互斥访问的支持, 这两条指令是在ARM V6中引入的.
4.3.2 spin_lock的变体spin_lock_irq
如果进程A持有锁, 并进入了临界区, 此时中断来了, 中断中又去获取锁, 所以会出现死锁, 为了避免这种情
况, 于是出现了spin_lock_irq和spin_lock_irqsave函数.
其中的raw_spin_lock_irq函数的实现, 相对于raw_spin_lock只是在调用preempt_diable之前又调用了local
_irq_disable, 这样确保在获得一个锁时不会发生中断, local_irq_disable只能用来关闭本地处理器中断,
但如果处理器B上的进程也去获得锁时, 会有怎么样的情况呢, 因为此时处理器A上的进程可以继续执行, 在
离开临界时会释放锁, 这样处理器B中断处理函数就可以结束此前的自旋状态.
这说明通过自旋锁进入临界区中的代码必须尽可能短的时间内执行完, 因为它执行越长, 别的处理器需要自
旋的等待时间越长, 最差的情况是进程在临界区中被换出处理器, 所以作为使用自旋锁时一条确定的规则,
任何拥有自旋锁的代码都是必须是原子的, 不能休眠.
............使用自旋锁的代码, 必须是原子的, 不能休眠.
如果知道一个自旋锁在中断处理的上下文中有可能被使用到时, 应使用spin_lock_irq函数, spin_lock只能
在确定中断上下文中不会使用到自旋锁的情形下使用.
与spin_lock_irq类似的还有一个spin_lock_irqsave宏, 它与spin_lock_irq最大的区别是, 关闭中断前会将
处理器当前的FLAGS寄存器的值保存在一个变量中, 在调用spin_unlock_irqrestore释放锁时, 会将保存的FL
AGS重新写回寄存器中.
另一个与中断处理相关的spinlock版本是spin_lock_bh函数, 该函数用来处理进程与延迟处理导致的并发中
的互斥问题, 相对于spin_lock_irq函数, 它用来关闭softirq的能力.
最后,自旋锁还设计了一组对应的非阻塞的版本, 在获得锁时, 发现处于上锁状态, 会直接返回0, 而不是自旋
, 如果成功获得锁, 将返回1.
static inline int spin_trylock(spinlock_t *lock)
static inline int spin_trylock_irq(spinlock_t *lock)
spin_trylock_irqsave(lock, flags);
int spin_trylock_bh(spinlock_t *lock);
4.3.3 单处理器上的spin_lock函数
在单处理器系统上, 内核可分为抢点和不可抢点两种:
对于第一种, 并发来源主要是外部中断等异步事件, 所以在这种系统下, 进入临界只只需要关闭处理器的中
断即可(local_irq_disable/local_irq_save), 在离开临界区时只需要打开/恢复处理器中断.
对于第二种, 并发来源除了中断与异常等异步事件外, 还包括因为可抢占性导致的进程间的并发, 所以进入临
界区时除了要关闭处理器中断, 还要关闭内核调度器的可抢占性.
..............关于关闭处理器的调度器可抢占性, 内核已经自己实现.
linux内核为了统一单处理器和多处理器的竞态处理代码, 将spin_lock函数及其它体延伸到了单处理器上, 对
单处理器而言, 如果是非抢占式系统, spin_lock/spin_unlock将等同于空操作; 对于可抢占系统, spin_lock
/spin_unlock刚分别用来关闭和打开抢占性.
而spin_lock_irq/spin_lock_irqsave等在单处理器上等同于local_irq_disable/local_irq_save, 如果是可
抢式系统, 那么需要在上述的中断控制函数后, 再加上内核可抢占性的preempt操作.
从代码移植性的角度来考虑, 即使在单处理器上只需要调用local_irq_disable来对共享资源进行保护时, 也
应该使用spin_lock_irq.
4.3.4 读者与写者自旋锁rwlock
spin_lock类的函数进入临界区时, 对临界区中的操作行为不做细分, 与spin_lock类比起来, 这种锁比较有意
思的地方在于: 它允许任意数量的读者同时进入临界区, 但写者必须进行互斥访问. 一个进程想去读的话, 必
须检查是否有进程正在写, 有的话必须自旋, 一个进程想去写的话, 必须先检查是否有进程正在读或写, 有的
话必须自旋.
相比较spinlock, rwlock在锁的定义以及irq与preempt操作方面没有任何不同, 唯一不同的是, rwlock针对读
和写都设计了各自的锁操作函数
写入者上锁操作:
static inline int do_raw_write_lock(raw_rwlock_t *rw)
{
unsigned long tmp;
__asm__ __volatile__(
"1: ldrex %0, [%1]\n"
"teq %0, #0\n"
"strexeq %0, %2, [%1]\n"
"teq %0, #0\n"
"bne 1b"
:"=&r"(tmp)
:"r"(&rw->lock),"r"(0x80000000)
:"cc");
smp_mb();
}
读取者的上锁操作:
static inline void do_raw_read_lock(raw_rwlock_t *rw)
{
unsigned long tmp, tmp2;
__asm__ __volatile__(
"1: ldrex %0,[%2]\n"
"adds %0, %0, #1\n"
"strexpl %1, %0, [%2]\n"
"rsbpls %0, %1, #0\n"
"bmi 1b"
:"=&r"(tmp),"=&r"(tmp2)
:"r"(&rw->lock)
:"cc");
smp_mb();
}
rwlock同样有多个版本. 对于读取者
void read_lock(rwlock_t *lock);
void read_lock_irq(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_unlock(rwlock_t *lock);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
对于写入者:
void write_lock(rwlock_t *lock);
void write_lock_irq(rwlock_t *lock);
void write_lock_irqsace(rwlock_t *lock, unsigned long flags);
void write_unlock(rwlock_t *lock);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_irqsave(rwlock_t *lock, unsigned long flags);
try版本:
int read_lock(rwlock_t *lock);
int write_lock(rwlock_t *lock);
. 如果当前有进程正在写, 其它进程不能读, 也不能写.
. 如果当前有进程正在读, 其它进程可以读, 但不能写.
当一个进程试图写, 只要有其它进程正在读或写, 它都必须自旋.
当一个进程试图读, 只有没有其它进程正在写, 它都可以获得锁.
在一个存在大量读操作,而数据的更新较少发生的系统中, 使用读写锁对共享资源进行保护, 相对普通自旋锁
, 无疑会大大提升系统性能.
4.4. 信号量(semaphore)
相对于自旋锁, 信号量的最大特点是允许调用它的线程进入睡眠状态, 这意味着试图获得某一信号量的进程
会导致它对处理器拥有权的丧失,也即出现进程的切换
4.4.1 信号量的定义与初始化
struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
其中, lock是自旋锁变量, 用于实现对信号量的另一个成员的原子操作, count用于表示通过该信号量允许进
入临界区的执行路径的个数.
wait_list 用于管理所有在该信号量上睡眠的进程, 无法获得信号量的进程将进入睡眠状态.
如果驱动定义了一个信号量变量, 注意不要直接对该变量的成员进行赋值, 应该使用sema_init函数来初始化
该信号量. sema_init函数定义如下:
static inline void sema_init(struct semaphore *sem, int val)
{
sttaic struct lock_class_key __key;
*sem=(struct semaphore)__SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
初始化主要通过__SEMAPHOREINITIALIZER宏完成
#define __SEMAPHORE_INITIALIZER(name, n) \
{
.lock = __SPIN_LOCK_UNLOCKED((name).lock), \
.count = n,
.wait_list = LIST_HEAD_INIT((name).wait_list),
}
所以sema_init(struct semaphore *sem, int val)调用会把信号量sem的lock值设为解锁状态, count设定为
函数调用参数val, 同时初始化wait_list链表头
4.4.2 DOWN操作
信号量上的操作主要有DOWN和UP, DOWN操作有:
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_killable(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
int down_timeout(struct semaphore *sem, long jiffies);
down_interruptible函数定义如下:
int down_interruptible(struct semaphore *sem)
{
unsigned long flags;
int result = 0;
spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
result = __down_interruptible(sem);
spin_unlock_irqrestore(&sem->lock, flags);
return result;
}
函数首先通过对spin_lock_irqsave的调用来保证对sem->count操作的原子性, 防止多进程对sem->count同时
操作引起混乱, 进入临界区, 判断sem->count是否大于0; 如果大于0, 成功获得信号量, count就减1, 然后退
出. 如果count不大于0, 无法获得信号量, 此时调用__down_interruptible, 后者完成一个进程无法获得信号
量时的操作, 在内部调用__down_common, 调用时参数state = TASK_INTERRUPTIBLE, timeout = LONG_MAX.
__down_common首先通过对struct semaphore_waiter变量waiter的使用, 使当前进程放到信号量sem的成员变
量wait_list管理的队列中, 接着在for循环中把当前进程的状态设置为TASK_INTERRUPTIBLE, 再调用schedu
le_timeout使当前进程进入睡眠状态. 函数将停在schedule_timeout调用上, 直到再次被调度执行, 当该进
程再一次被调度执行时, schedule_timeout开始返回, 被再次调度的原因有: 如果waiter.up不为0, 说明进
程在信号量sem的wait_list队列中被该信号量的up操作所唤醒, 进程可以获得信号量, 返回0. 如果进程是被
用户空间发送的信号所中断或者超时引起的唤醒, 则返回相应的错误码, 因此对down_interruptible的调用
总是应该坚持检查其返回值, 以确定函数是已经获得了信号量还是因为操作被中断而需特别处理, 通常驱动
对返回的非0值要做的工作就是返回-ERESTARTSYS.
void down(struct semaphore *sem)
与down_interruptible相比, down是不可中断的, 这意味着调用它的进程如果无法获得信号量, 将一直处于
睡眠状态直到有别的进程释放了该信号量. 对用户空间来说, 如果应和程序阻塞在驱动中的down函数中, 将
无法通过一些强制措施来结束该进程, 如Ctrl+D. 因此, 除非必要, 否则驱动应该避免使用down函数.
int down_killable(struct semaphore *sem)
睡眠的进程可以收到一些致命性的信号, 被唤醒而导致信号量的操作被中断. 极少使用.
int down_trylock(struct semphore *sem)
进程试图获得信号量, 如果无法获得则直接返回1而不进入睡眠, 返回0表明已经成功获得了信号量.
int down_timeout(struct semaphore *sem, long jiffies);
在无法获得信号量时将进入睡眠, 但处于这种睡眠状态有时间限制, 如果jiffies指时的时间到期时函数依然
无法获得信号量, 则将返回一错误码-ETIME, 在到期前进程的睡眠状态为TASK_UNINTERRUPTIBLE. 成功返回0.
4.4.3 UP操作
void up(struct semaphore *sem)
{
unsigned long flags;
spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem);
spin_unlock_irqrestore(&sem->lock, flags);
}
如果信号量sem的wait_list队列为空, 表时没有其它进程在等待该信号量, 只要把sem的count加1即可, 如果
wait_list不为空, 说明有其它进程正睡眠在wait_list上等待信号量, 此时调用__up(sem)来唤醒进程:
__up函数首先用list_first_entry取得sem->wait_list链表上的第一个waiter节点C, 然后将其从sem->wait_
list链表中删除, waiter->up = 1, 最后调用wake_up_process来唤醒waiter C上的进程C, 这样进程C将大之
前down_interruptible调用的schedule_timeout(timeout)处本来, waiter->up = 1, down_interruptible
返回0, 进程C获得信号量, 进程D和E继续等待直到有进程释放信号量或者被用户空间中断掉.
????????此时唤醒只是将waiter->up =1, 但并没有将sem->count++, 对吗?
即使不是信号量的拥有者, 也可以调用up函数来释放一个信号量.
在linux系统中, 信号量的一个常见用途是实现互斥机制, 这种情况下信号量的count为1, 任意时刻只允许一
个进行进入临界区. 内核提供了一个宏DECLARE_MUTEX专门用于这种用途的信号量定义和初始化.
#define DEFINE_SEMAPHORE(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
..........在3.0.8内核上已经没有书上说的DECLARE_MUTEX宏了, 替换的是这个DEFINE_SEMAPHORE
该宏定义了一个count=1的信号量变量name, 并初始化了相关成员
????????因为信号量的down_interruptible中包含spinlock_irqsave, 所以此时semaphore也是禁中断,禁抢占的
4.4.4 读取者与写入者信号量rwsem
struct rw_semaphore {
__s32 activity;
spinlock_t wait_lock;
struct list_head wait_list;
}
acitity的确切含义是:
. activity=0, 表时当前信号量上没有任何活动的读者/写者.
. activity=-1, 表明当前在信号量上有一个活动的写者.
. activity为正值n, 表明当前在该信号量上有n个活动的读者.
静态定义一个rwsem变量同时用DECLARE_RWSEM宏进行初始化.
#define DECLARE_RWSEM(name)
对一个rwsem变量动态初始化使用init_rwsem宏. rwsem的初始状态是没有任何活动的读者和写者.
void __init_rwsem(struct rw_semaphore *sem)
{
sem->activity = 0;
spin_lock_init(&sem->wait_lock);
INIT_LIST_HEAD(&sem->wait_list);
}
读者的DOWN操作:
void __sched down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
读者的UP操作:
void up_read(struct rw_semaphore *sem);
写入者的DOWN操作:
void __sched down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
写入者的UP操作:
void up_write(struct rw_semaphore *sem);
4.5 互斥锁mutex
用count=1的信号量实现的互斥方法不是linux下的经典用法,linux内核针对count=1的信号量重新定义了一个
数据结构struct mutex, 一般称为互斥锁. 同时内核根据使用场景的不同, 把用于信号量的DOWN和UP操作在
struct mutex上作了优化和扩展.
4.5.1 互斥锁的定义和初始化
互斥锁mutex的概念本来就来自semaphore, 如果去掉那些跟调试相关的成员, struct mutex和struct semap
hore并没有本质的不同:
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
#if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)
struct task_struct *owner;
#endif
#ifdef CONFIG_DEBUG_MUTEXES
const char *name;
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
定义一个静态的struct mutex 变量同时初始化的方法是利用内核的DEFINE_MUTEX;
#define __MUTEX_INITIALIZER(lockname) \
{
.count = ATOMIC_INIT(1),\
.wait_lock = __SPIN_LOCK_UNLOCKED(lockname.wait_lock),\
.wait_list=LIST_HEAD_INIT(lockname.wait_list)\
}
#define DEFINE_MUTEX(mutexname) \
struct mutex mutexname = __MUTEX_INITIALIZER(mutexname);
如果在程序的执行期初始化一个mutex变量, 则可以使用mutex_init宏.
void mutex_init(struct mutex *lock)
{
atomic_set(&lock->count, 1);
spin_lock_init(&lock->wait_lock);
INIT_LIST_HEAD(&lock->wait_list);
}
4.5.2互斥锁的DOWN操作
互斥锁在mutex上的DOWN操作在linux内核中为mutex_lock函数:
void __sched mutex_lock(struct mutex *lock)
{
might_sleep();
__mutex_fastpath_lock(&lock->count, __mutex_lock_slowpath);
mutex_set_owner(lock);
}
函数的设计思想体现在__mutex_fastpath_lock和__mutex_lock_slowpath上, __mutex_fastpath_lock用来快
速判断当前可否获得互斥锁, 如果成功获得, 则函数直接返回, 否则进入到__mutex_lock_slowpatch函数中,
这种设计是基于这样的一个事实在: 想要获得某一互斥锁的代码绝大部分都可以成功获得. 所以进入__mutex
_lock_slowpath的概率很低.
__mutex_fastpath_lock是一个平台相关的函数:
static inline void __mutex_fastpath_lock(atomic_t *count, void (*fail_fn)(atomic_t *))
{
int __ex_flag, __res;
__asm__ (
"ldrex %0, [%2] \n\t"
"sub %0, %0, #1 \n\t"
"strex %1, %0, [%2] "
: "=&r" (__res), "=&r" (__ex_flag)
: "r" (&(count)->counter)
: "cc","memory" );
__res |= __ex_flag;
if (unlikely(__res != 0))
fail_fn(count);
}
函数通过ldrex完成__res=count->counter, 第二行汇编完成__res=__res-1, 第三行试图用
__res的当前值来更新cuont->counter, 这里说的"试图"是因为这个更新的操作未必会成功,
可能有别的进行也在操作count->counter, 为不使这种可能的竞争引起对count->counter的
值更新混乱, 所以这里用了ARM指令中用于实现互斥访问的指令ldrex和strex, ldrex, strex
保证了对count->counter的"读-更新-写"操作序列原子性.
接下来在__res |= __ex_flag执行完之后, 通过if语句判断__res是否为0, 有两种情况会导致
__res不为0: 1是在调用这个函数前count->counter=0, 表明互斥锁已经被其它进程获得, 这样
第二行处的__res=-1; 2是在第三行的更新操作不成功, 表明当前有另外一个进程也在对count->
counter进行同样的操作. 这两种情况都将导致__mutex_fastpatch_lock不能直接返回, 而是
进入fail_fn, 也就是调用__mutex_lock_slowpath.
此处的unlikely是GCC编译优化扩展的宏, 表时__res!=0为真的可能性很小, 编译器借此可以调
整一些编译后的代码顺序, 达到某种程序的优化.
如果__mutex_fastpath_lock函数不能第一时间获得互斥锁返回, 那么将进入__mutex_lock_slowpath
, 正如其名所示, 代码将进入一段艰难坎坷的旅途.
__mutex_lock_slowpath函数与信号量DOWN操作中的down函数非常相似, 不过__mutex_lock_slowpath
在把当前进程放入mutex的wait_list之前会试图多次询问mutex中的count是否为1, 也就是说在
进入wait_lsit前会多次考察别的进程是否已经 释放了这个互斥锁, 这主要基于这样一个事实,
拥有互斥锁的进程总是会在尽可能短的时间里释放, 如要别的进程已经释放了该互斥锁, 那么
当前进程将可以获得该互斥锁而没必要去睡眠.
4.5.3 互斥锁的UP操作
互斥锁的UP操作为mutex_unlock
void __sched mutex_unlock(struct mutex *lock)
{
/*
* The unlocking fastpath is the 0->1 transition from 'locked'
* into 'unlocked' state:
*/
#ifndef CONFIG_DEBUG_MUTEXES
/*
* When debugging is enabled we must not clear the owner before time,
* the slow path will always be taken, and that clears the owner field
* after verifying that it was indeed current.
*/
mutex_clear_owner(lock);
#endif
__mutex_fastpath_unlock(&lock->count, __mutex_unlock_slowpath);
}
和mutex_lock函数一样,mutex_unlcok也有两条主线: __mutex_fastpath_unlcok和__mutex_unlock_slowpath,
分别用于对互斥锁的快速和慢速解锁.
static inline void __mutex_fastpath_unlock(atomic_t *count, void (*fail_fn)(atomic_t *))
{
int __ex_flag, __res, __orig;
__asm__ (
"ldrex %0, [%3] \n\t"
"add %1, %0, #1 \n\t"
"strex %2, %1, [%3] "
: "=&r" (__orig), "=&r" (__res), "=&r" (__ex_flag)
: "r" (&(count)->counter)
: "cc","memory" );
__orig |= __ex_flag;
if (unlikely(__orig != 0))
fail_fn(count);
}
这里除了将count->counter的值加1以外, 代码和__mutex_fastpath_lock中几乎完成一样. 在最后的if语句中
, 导致代码中__orig不为0也有两种情况: 1是调用这个函数前count->counter不为0, 表明当前进程占有的互
斥锁期间有别的进程竞争该互斥锁; 2是对count->counter的更新操作不成功, 表明当前有另外一个进程也在
对count->counter进程操作, 这种情况主要是别对进程此时调用mutex_lock函数导致竞争, 因为互斥的原因,
别的进程此时不可能调用mutex_unlock, 这种情况的处理是非常重要的, 不只是关系到count->counter正确更
新的问题, 还涉及能否防止一个唤醒操作的丢失.
在没有别的进程竞争该互斥锁的情况下, __mutex_fastpath_unlcok函数要完成的工作最简单, 把count->cou
nter的值加1然后返回, 如果有竞争, 那么函数进入__mutex_unlock_slowpath, 这个函数主要用来唤醒在当
前mutex的wait_list中休眠的进行, 如同up函数一样.
4.6 顺序锁seqlock
设计思想是, 对某一共享数据读时不加锁, 写的时候加锁, 为了保证读的过程中不会因为写者的出现导致该
共享数据的更新, 需要在读者和写者之前引入一整值, 如果与之前读的值不一样, 则说明本次读操作过程中
发生了数据更新, 读操作无效. 因此要求写者在开始写的时候要更新sequence的值.
typedef struct {
unsigned sequence;
spinlock_t lock;
} seqlock_t;
sequence用来协调读者与写者的操作, spinlock变量lock在多个写者之间做互斥作用.
程序中静态定义一个seqlock并同时初始化, 可以使用DEFINE_SEQLOCK宏, 该宏会定义一个 seqlock_t变量,
并初始化其sequence为0, lock为0.
#define DEFINE_SEQLOCK(x)\
seqlock_t x = __SEQLOCK_UNLOCKED(x)
#define __SEQLOCK_UNLOCKED(lockname) \
{0, __SPIN_LOCK_UNLOCKED(lockname)}
如果要动态初始化一个seqlock变量, 可以使用seqlock_init:
#define seqlock_init(x) \
do {
(x)->sequence = 0;
spin_lock_init(&(x)->lock);
} while (0)
写者在seqlock上的上锁操作
static inline void write_seqlock(seqlock_t *sl)
{
spin_lock(&sl->lock);
++sl->sequence;
smp_wmb();
}
写者对写之前需要先获得seqlock上的锁lock, 这说明写者之间必须保证互斥操作, 如果某一写者成功获得lo
ck,那么需要更新sequence的值以便让其它写者知道共享数据发生了更新, 写者与写者间不需要sequence.
写者在seqlock上的解锁操作write_sequnlock:
static inline void write_sequnlock(seqlock_t *sl)
{
smp_wmb();
sl->sequence++;
spin_unlock(&sl->lock);
}
????????为什么write_seqlock中是++sl->sequence, write_sequnlcok中是sl->sequence++; 加两次是为了表明是
否在写.
主要工作是释放自旋锁lock, 至于写者对sequence的更新, 主要是用来告诉我读取者有数据更新, 所以必须确
保sequence的值在写入的前后发生变化. 在此基础上sequence提供另外一个信号是写入过程有没有结束, sequ
ence的最低位为0, 表明写入过程结束, 否则表明写入过程正在进行.
static inline int write_tryseqlock(seqlock_t *sl);
写者使用write_tryseqlock来保证无法获得lock时, 不让自己进入自旋状态而直接返回0, 成功获得锁返回1.
读者在读前需要先调用read_seqbegin, 该函数返回读开始前的sequence值.
static __always__inline unsigned read_seqbegin(const seqlock_t *sl)
{
unsigned ret;
repeat:
ret = sl->sequence;
smb_rmb();
if(unlikely(ret & 1)) {
cpu_relax();
goto repeat;
}
return ret;
}
如果当前正好有写者在进行写操作, 那么该函数将循环直到写结束, 这就是sequence最低位的用途, 这里正好
是其实际使用的地方, 写者的实际写入操作占用时间不应太长
内核还给读者提供了一个read_seqretry函数, 与read_seqbegin的返回值一起使用, 来判断本次读操作是否有
效:函数的参数start是读者在读操作前调用read_seqbegin获得的初始值, 如果本次读无效, 那么read_seqre
try返1, 否则返回0.
流程如下: 写之前write_seqlock获得锁, 更新sequence值, 成功后开始写, 写结束释放锁.
读之前先得到sequence的值start, 无需获得锁, 然后开始读, 读结束再判断start是否有更新, 如
果失败,则重新读取.
读者与写者的对应版本:
write_seqlock_irq(lock)
write_seqlock_irqsave(lock, flags)
write_seqlock_bh(lock)
write_sequnlock_irq(lock)
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_bh(lock)
read_seqbegin_irqsave(lock, flags)
read_seqretry_irqrestore(lock, iv, flags)
rwlock与seqlock非常相似, 不同在于seqlock在写的时候只与其它写者互斥, 而rwlock在写的时候与读者和写
者都互斥. 因此要保护的资源很小很简单, 会很频繁被访问并且写入操作很少发生且必须快速时, 就使用seq
lock.
4.7 RCU
RCU全称是Read-Copy-Udate, 读/写--复制--更新, 在Llinux提供的所有内核互斥设施中属于一种免锁机制.同
前面讨论的锁一样,RCU的适用模型也是读者与写者共享的系统, 与前边的锁不一样的是,RCU中的读和写操作无
须考虑两者间的互斥问题. 前面的锁都要涉及内存操作, 同时还伴有内存屏障方法的引入, 这使得锁操作的系
统开锁变得很大. 在此基础上, linux内核加入了对RCU这种免锁的互斥访问机制的支持.
RCU的原理简单地说, 是将读者和写者要访问的共享数据放在一个指针P中, 读者通过P来访问其中的数据, 而
写者通过修改P来更新数据, 大量的工作集中在写者一方. 免锁的实现必定要通过双方属守一定的规则才能达
成.
4.7.1 读者的RCU临界区
对读者来说, 要访问共享数据, 首先调用rcu_read_lock和rcu_read_unlcok函数构建自己所谓的读者侧临界区
, 然后在临界区中获得指向共享数据的指针, 实际的读操作就是对该指针的引用, 一个关于读者的明确规则是
, 对指针的引用必须在临界区中完成, 离开临界区后不应该出现任何形式的对该指针的引用. 在临界区中, 关
闭内核可抢占性意味着在临界区中不会因为中断的发生而导致进行的切换, 明确规则, 临界区中的代码不能发
生睡眠.
rcu_read_lock和rcu_read_unlock实际做的工作仅仅是关闭和打开内核的可抢占性.
4.7.2 写入者的RCU操作
RCU操作中写者要写成的工作是重新分配一个被保护的共享数据区, 将老数据区的数据复制到新数据区, 然后
再根据需要修改新数据区, 最后用新数据区指针替换老的指针, 替换指针的操作是原子操作. 在写者做完这些
工作后, 后续的所有RCU读操作都将访问这个新的共享数据区, 写者用新指针替换老指针后还不能马上释放老
指针指向的数据区所占用的内存空间, 因为系统中还可能存在对老指针的引用.
主要有以下两种情况:
1. 在单处理器来看, 读者进入RCU临界区后, 刚获得共享区的指针后发生一个中断, 如果写者恰好是中断处理
函数中的行为, 那么当中断返回后, 被中断进行的RCU临界区中继续执行时, 将会继续引用老指针.
2. 在多处理器系统, 当处理器A上的一个读者进入RCU临界区并获得共享数据区中的指针后, 在还没有来得及
引用该指时, 处理器B上的写者更新了指向共享数据区的指针, 这样处理器A上的读者也将引用到老指针.
因此,写者在替换掉共享区的指针后, 老指针所指向的共享数据区所在的空间还不能马上释放. 写者需要和内
核共同协作, 在确定所有对老指针的引用都结束后才可以释放老指针指向的内存空间. 为此,写者在用新指针
替换掉老指针后需要做的是, 调用call_rcu函数向内核注册一个回调, 内核在确定所有对老指针的引用都结
束时会调用回调函数, 回调的功能主要是释放老指针指向的内存空间.
void call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu));
RCU的写者负责在替换掉老指针后调用call_rcu向内核注册一回调函数, 回调负责释放老指针指向的内存空间
, call_rcu中的参数func就是指向该架设函数的指针. head为调用func时传递到func中的参数. 实际使用中,
会把struct rcu_head内嵌到共享数据所在的结构体中, 这样在回调函数中通过传进来的struct rcu_head指针
, 使用container_of宏获得指向旧的共享数据区指针, 然后调用kfree释放旧的数据区.
内核确保没有读者对老指针的引用是基于以下规则:
所有可能对共享数据区指针的不一致引用一定是发生在读者的RCU临界区, 因为临界区由rcu_read_lock和unl
cok来界定, 所以就单处理器而言, 在临界区中一定会不发生进程切换, 所以如果在某一CPU上发生了进程切换
, 那么所有对老指针的引用都会结束, 之后读者再进入RCU临界区都将看到新指针. 因此,内核确定没有对老指
针的引用条件是:系统中所有处理器上都至少发生了一次进程切换.
4.7.3 RCU使用的特点
RCU实质上是对读者与写者自旋锁rwlock的一种优化: RCU的读者在读数据时除了关内核可抢占性外, 与普通数
据读取操作没有任何区别, 读者也不关心当前有没有写者在对共享数据区进行操作, 而rwlock, 在读者工作时
, 必须确保没有写者在工作, 否则读取进程将进入自旋, 所以RCU可以让多个读者与写者同时工作.
相对于读者, RCU写者的开销比较大, 它需要申请新的内存空间, 正常的数据更新操作, 向内核注册回调函数
, 同时也要考虑与其他写者之间的互斥, 但与rwlock不一样的是, 写者不需要考虑与读者互斥.
可见,RCU读者性能的提升是在增加写者负担的前提下完成的. 如果在一个读者与写者共存的系统中, 按照设
计者的说法, 如果写者的操作比较在10%以上, 就应该考虑其它的互斥方法, 反之采用RCU的实现可以获得更高
的性能.
synchronize_rcu, 这个函数类似于call_rcu, 它可能会阻塞, 因为它要等所有对老指针的引用都结束时才返
回, 函数返回意味着系统中所有的老指针的引用都消失了.
4.8 原子变量与位操作
typedef struct {
int counter;
} atomic_t;
Linux系统中定义了一大堆以"atomic_"打头的原子操作函数, 这些函数的实现都信赖于特定的硬件平台;
atomic_inc执行原子加函数.
atomic_t是个struct 类型, 在需要整型变量的地方不能直接用atomic_t变量, 否则会产生编译错误. 另外在
实际使用时应注意, atomic_t型变量只能保证自身操作的原子性, 对一个由多个整形变量组成的共享数据, 即
便把这些变量全部声明为原子型, 对它们的使用也都是用atomic类的函数, 也不能保证对该共享数据操作的原
子性, 此时需要用到前边提到的其它互斥方法.
与单个原子变量相对的是位操作的原子性.
4.9 等待队列
4.9.1 等待队列头wait_queue_head_t
struct __wait_queue_head {
spinlock_t lock; // 等待队列的自旋锁, 用于并发互斥
struct list_head task_list; //双向链表结构, 用来将等待队列构成链表
};
typedef struct __wait_queue_head wait_queue_head_t;
定义等待队列:
1. 通过DECLARE_WAIT_QUEUE_HEAD宏来完成等待队列头对象的静态定义与初始化:
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.task_list = { &(name).task_list, &(name).task_list } }
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
2. 通过init_waitqueue_head宏在程序运行期间初始化一个头节点对象
#define init_waitqueue_head(q) \
do { \
static struct lock_class_key __key; \
\
__init_waitqueue_head((q), &__key); \
} while (0)
void __init_waitqueue_head(wait_queue_head_t *q, struct lock_class_key *key)
{
spin_lock_init(&q->lock);
lockdep_set_class(&q->lock, key);
INIT_LIST_HEAD(&q->task_list);
}
4.9.2 等待队列的节点
typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
}
flags, 唤醒等待队列上的进程时, 该标志会影响唤醒操作的行为模式, WQ_FLAG_EXCLUSIVE, 该标志表明睡眠
在其上的进程在被唤醒时具有排他性.
private, 等待队列上的私有数据, 实际使用中用来指向睡眠在该节点上的进各的task_struct结构
func, 当该节点上的睡眠进程需要被唤醒时执行的唤醒函数.
task_list, 用来将各独立的等待队列节点链接起来形成链表.
程序可以通过DECLARE_WAITQUEUE来定义并初始化一个等待队列节点:
#define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \
.task_list = { NULL, NULL } }
#define DECLARE_WAITQUEUE(name, tsk) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
如果要在程序运行期初始化一个等待队列节点对象, 可以用init_waitqueue_entry函数:
static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
{
q->flags = 0;
q->private = p;
q->func = default_wake_function;
}
4.9.3 等待队列的应用
等待队列常用的模式便是实现进程的睡眠等待, 当进程需要的资源无法获得时, 进程将进入睡眠, 并让出处理
器,进入睡眠状态, 意味着进程将从调度器的运行队列中移除, 此时进行被挂在某一等待队列的节点中.
为了实现进程的睡眠机制, 系统会产生一个新的等待队列节点, 然后将教程的task_struct对象放到等待队列
节点对象的private成员中.
内核中对等待队列的核心操作是等待wait与唤醒wake up.
4.10 完成接口completion
struct completion {
unsigned int done; // 表示当前completion的状态
wait_queue_head_t wait; //wait是一等待队列, 用来管理当前在等待在该completion上的所有进程
};
如果要静态定义一个struct completion变量并初始化, 可以使用DECLARE_COMPLETION宏
#define DECLARE_COMPLETION(work) \
struct completion work = COMPLETION_INITIALIZER(work)
如果要重新初始化一个已使用过的struct completion, 可以使用INIT_COMPLETION宏
#define INIT_COMPLETION(X) ((x).done = 0)
如果要动态初始化一个struct completion变量, 应该调用init_completion函数
static inline void init_completion(struct completion *x)
{
x->done = 0;
init_waitqueue_head(&x->wait);
}
完成接口对执行路径间的同步可以通过等待者与完成者模型来表述, 内核定义wait_for_completion;
void __sched wait_for_completion(struct completion *x)
{
wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
}
wait_for_completion内调用wait_for_common来使当前进程以TASK_UNINTERRUPTIBLE睡眠在completion x上的
wait队列中. wait_for_common内部调用了do_wait_for_common来做这件事
等待者首先检查completion中的done成员, 它表示当前在completion上的完成者数量, 如果没有完成者, 那么
等待者进入睡眠队列进行等待, 这种睡眠是不可中断的, DECLARE_WAITQUEUE定义并初始化一个等待节点wait,
代表当前进程的current变量将会记录到wait的private变量, wait中的func函数指针指向default_wake_func
tion, 当wait上的进程被唤醒时将调用该函数. 进程需要睡眠时, 调用__add_wait_queue_tail_exclusive把
wait节点加入到completion管理的队列尾部, wait->flags |= WQ_FLAG_EXCLUSIVE等待节点wait中的这个标记
将在完成者的唤醒操作中使用到.
若干时间之后, 进程因某种原因被唤醒, 表现为从schedule_timeout函数返回, 它将检查done成员和timeout
变量以决定后续的动作, timeout大于0, 表示进程没有超时, x->done=0表示completion上没有完成者, 此时
如果没有信号需要处理, 将继续睡眠.
如果进程睡眠超时, 将返回timeout值, 如果没有超时且有完成者在completion上出现, 进程将离开睡眠队列
, 在将完成者数量减1之后, 等待者结束等待状态返回.
如果考虑到进程进入睡眠队列的状态及睡眠超时时间的设定, 内核提供了api:
int wait_for_completion_interruptible(struct completion *x);
可中断的等待状态.
int wait_for_completion_killable(struct completion *x);
可杀死的等待状态. 等待的进程可以被kill signal唤醒并中止等待状态
unsigned long wait_for_completion_timeout(struct completion *x, unsigned long timeout)
不可中断的等待状态, 但在timeout指定的时间到期后, 进程将中止等待状态
unsigned long wait_for_completion_interrutpible_timeout(struct completion *x,unsigned long
timeout);
可中断的等待状态, 但在timeout指定的时间到期后, 进程将中止等待状态.
unsigned long wait_for_completion_killable_timeout(struct completion *x, unsigned long
timeout);
可杀死的等待状态, 但在timeout指定的时间到期之后, 进程将中止等待状态.
对于完成者的行为, 内核函数为complete和complete_all, 前者只唤醒一个等待者, 后者将唤醒所有等待者.
void complete(struct completion *x)
{
unsigned long flags;
spin_lock_irqsave(&x->wait.lock, flags);
x->done++;
__wake_up_common(&x->wait, TASK_NORMAL, 1, 0, NULL);
spin_unlock_irqrestore(&x->wait.lock, flags);
}
函数先将完成者数量加1, 然后调用__wake_up_common执行唤醒等待者的操作. 3,4参数表示排他性唤醒的个数
和标志__wait_up_common遍历当前completion所管理的等待队列的每一个节点, 此时nr_exclusive=1, flags
中令有WQ_FLAG_EXCLUSIVE标志, 意味着本次唤醒只唤醒一个等待者. func指向default_wake_function, 用来
做实际的唤醒工作.
相对于complete一次只唤醒一个等待者, complete_all用来唤醒completion队列上的所有等待者进程, 注意这
里complete_all在这里假设完成者的最大数量是(~0U)/2, 这是个很大的值, 现在系统中很少有等待者进程数
量会达到这个值, 因此在complete_all之后completeion中的done值将失去本来意义, 如果继续用该completi
on, 应调用前面提过的INIT_COMPLETION宏.
本文欢迎大家转载
原文出自: http://blog.csdn.net/dyron
自旋锁不会进入睡眠, 最适合在不允许睡眠的上下文环境中执行. 如果获得锁失败, 会处于忙等待状态, 因此
要求获得锁的进程在尽可能短的时间内完成对共享资源的访问.
互斥锁的实现来源于信号量, 如果一个进程在进入临界区前试用调用互斥锁时, 有可能会进入休眠, 所以中断
上下文中严格禁止使用互斥锁和信号量.
自旋锁是一种基于忙等待的互斥机制, 但现实中被自旋锁保护的临界区代码往旆可以很快执行完, 这种情况下
用互斥锁, 可能引起进程切换的开销往往要比忙等待大得的, 因此不能以为进程睡眠一定会比忙等待更有利于
系统性能.