20150518 Linux设备驱动中的并发控制
2015-05-18 Lover雪儿
总结一下并发控制的相关知识:
本文参考:华清远见《Linux 设备驱动开发详解》—第7章 Linux 设备驱动中的并发控制,更多详细内容请看原书
一、并发与竞态
并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race conditions)。
在 Linux 内核中,主要的竞态发生于如下几种情况:
①对称多处理器(SMP)的多个 CPU
SMP 是一种紧耦合、共享存储的系统模型,如下图所示,它的特点是多个 CPU 使用共同的系统总线,因此可访问共同的外设和储存器。
②单 CPU 内进程与抢占它的进程
Linux 2.6 内核支持抢占调度,一个进程在内核执行的时候可能被另一高优先级进程打断,进程与抢占它的进程访问共享资源的情况类似于 SMP 的多个 CPU。
③中断(硬中断、软中断、Tasklet、底半部)与进程之间
中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,则竞态也会发生。此外,中断也有可能被新的更高优先级的中断打断,因此,多个中断之间本身也可能引起并发而导致竞态
二、解决竞态的方法
解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。
访问共享资源的代码区域称为临界区(critical sections),临界区需要以某种互斥机制加以保护。中断屏蔽、原子操作、自旋锁和信号量等是 Linux 设备驱动中可采用的互斥途径,以下开始逐一讲解:
①中断屏蔽
在单 CPU 范围内避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。
CPU 一般都具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞态条件的发生。中断屏蔽将使得中断与进程之间的并发不再发生,从而避免了内核中的竞争了.
中断屏蔽的使用方法为: local_irq_disable() //屏蔽中断 ... critical section //临界区 ... local_irq_enable() //开中断 |
由于 Linux 系统的异步 I/O、进程调度等很多重要操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失甚至系统崩溃。这就要求在屏蔽了中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。
local_irq_disable()和 local_irq_enable()都只能禁止和使能本 CPU 内的中断,因此,并不能解决 SMP 多 CPU 引发的竞态。因此,单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法,它适宜与自旋锁联合使用。
与 local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作以外,还保存目前 CPU 的中断位信息,local_irq_restore(flags)进行的是local_irq_save(flags)相反的操作。
②原子操作
原子操作指的是在执行过程中不会被别的代码路径所中断的操作。
1.设置原子变量的值 void atomic_set(atomic_t *v, int i); //设置原子变量的值为 i atomic_t v = ATOMIC_INIT(0); //定义原子变量 v 并初始化为 0 //-------------------------------------------------------------------------- 2.获取原子变量的值 atomic_read(atomic_t *v); //返回原子变量的值 //-------------------------------------------------------------------------- 3.原子变量加/减 void atomic_add(int i, atomic_t *v); //原子变量增加 i void atomic_sub(int i, atomic_t *v); //原子变量减少 I //-------------------------------------------------------------------------- 4.原子变量自增/自减 void atomic_inc(atomic_t *v); //原子变量增加 1 void atomic_dec(atomic_t *v); //原子变量减少 1 //-------------------------------------------------------------------------- 5.操作并测试 int atomic_inc_and_test(atomic_t *v); int atomic_dec_and_test(atomic_t *v); int atomic_sub_and_test(int i, atomic_t *v); 上述操作对原子变量执行自增、自减和减操作后(注意没有加)测试其是否为 0,为 0 则返回 true,否则返回 false。 //-------------------------------------------------------------------------- 6.操作并返回 int atomic_add_return(int i, atomic_t *v); int atomic_sub_return(int i, atomic_t *v); int atomic_inc_return(atomic_t *v); int atomic_dec_return(atomic_t *v); 上述操作对原子变量进行加/减和自增/自减操作,并返回新的值。 |
③位原子操作
1.设置位 void set_bit(nr, void *addr); 上述操作设置 addr 地址的第 nr 位,所谓设置位即将位写为 1。 //-------------------------------------------------------------------------- 2.清除位 void clear_bit(nr, void *addr); 上述操作清除 addr 地址的第 nr 位,所谓清除位即将位写为 0。 //-------------------------------------------------------------------------- 3.改变位 void change_bit(nr, void *addr); 上述操作对 addr 地址的第 nr 位进行反置。 --------------------------------------------------------------------------- 4.测试位 test_bit(nr, void *addr); 上述操作返回 addr 地址的第 nr 位。 //-------------------------------------------------------------------------- 5.测试并操作位 int test_and_set_bit(nr, void *addr); int test_and_clear_bit(nr, void *addr); int test_and_change_bit(nr, void *addr); 上述 test_and_xxx_bit ( nr, void *addr) 操作等同于执行 test_bit (nr, void *addr) 后再执行 xxx_bit(nr, void *addr) 。 |
例.使用原子变量使设备只能被一个进程打开.
1 static atomic_t xxx_available = ATOMIC_INIT(1); /*定义原子变量*/ 2 static int xxx_open(struct inode *inode, struct file *filp){ 3 ... 4 if (!atomic_dec_and_test(&xxx_available)){ 5 atomic_inc(&xxx_available); 6 return - EBUSY; /*已经打开*/ 7 } 8 ... 9 return 0; /* 成功 */ 10 } 11 static int xxx_release(struct inode *inode, struct file *filp){ 12 atomic_inc(&xxx_available); /* 释放设备 */ 13 return 0; 14 }
④自旋锁
自旋锁(spin lock)是一种对临界资源进行互斥手访问的典型手段.
为了获得一个自旋锁,在某 CPU 上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。
如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗地说就是“在原地打转”。当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作向其调用者报告锁已释放。
1.定义自旋锁 spinlock_t spin; //-------------------------------------------------------------------------- 2.初始化自旋锁 spin_lock_init(lock) 该宏用于动态初始化自旋锁 lock //-------------------------------------------------------------------------- 3.获得自旋锁 spin_lock(lock) 该宏用于获得自旋锁 lock,如果能够立即获得锁,它就马上返回,否则,它将自 旋在那里,直到该自旋锁的保持者释放; spin_trylock(lock) 该宏尝试获得自旋锁 lock,如果能立即获得锁,它获得锁并返回真,否则立即返回假,实际上不再“在原地打转”; //-------------------------------------------------------------------------- 4.释放自旋锁 spin_unlock(lock) 该宏释放自旋锁一般这样被使用,如下所示: //定义一个自旋锁 spinlock_t lock; spin_lock_init(&lock); spin_lock (&lock) ; //获取自旋锁,保护临界区 ...//临界区 spin_unlock (&lock) ; //解锁自旋锁 lock,它与 spin_trylock 或 spin_lock 配对使用。 //-------------------------------------------------------------------------- 自旋锁一般这样被使用,如下所示: //定义一个自旋锁 spinlock_t lock; spin_lock_init(&lock); spin_lock (&lock) ; //获取自旋锁,保护临界区 ...//临界区 spin_unlock (&lock) ; //解锁 |
自旋锁主要针对 SMP 或单 CPU 但内核可抢占的情况,对于单 CPU 和内核不支持抢占的系统,自旋锁退化为空操作.
尽管用了自旋锁可以保证临界区不受别的 CPU 和本 CPU 内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。为了防止这种影响,就需要用到自旋锁的衍生。spin_lock()/spin_unlock()是自旋锁机制的基础,它们和关中断 local_irq_ disable()/开中断 local_irq_enable()、
关底半部local_bh_disable()/开底半部 local_bh_enable()、
关中断并保存状态字local_irq_save()/开中断并恢复状态 local_irq_restore()
结合就形成了整套自旋锁机制,关系如下所示:
spin_lock_irq() = spin_lock() + local_irq_disable() spin_unlock_irq() = spin_unlock() + local_irq_enable() spin_lock_irqsave() = spin_unlock() + local_irq_save() spin_unlock_irqrestore() = spin_unlock() + local_irq_restore() spin_lock_bh() = spin_lock() + local_bh_disable() spin_unlock_bh() = spin_unlock() + local_bh_enable() |
驱动工程师应谨慎使用自旋锁,而且在使用中还要特别注意 自旋锁实际上是忙等锁, 自旋锁可能导致系统死锁 这两个问题.
int xxx_count = 0;/*定义文件打开次数计数*/ static int xxx_open(struct inode *inode, struct file *filp){ ... spinlock(&xxx_lock); if (xxx_count)/*已经打开*/ { spin_unlock(&xxx_lock); return - EBUSY; } xxx_count++;/*增加使用计数*/ spin_unlock(&xxx_lock); ... return 0; /* 成功 */ } static int xxx_release(struct inode *inode, struct file *filp){ ... spinlock(&xxx_lock); xxx_count--; /*减少使用计数*/ spin_unlock(&xxx_lock); return 0; }
⑤读写自旋锁
自旋锁不关心锁定的临界区究竟进行怎样的操作,不管是读还是写,它都一视同仁。即便多个执行单元同时读取临界资源也会被锁住。实际上,对共享资源并发访问时,多个执行单元同时读取它是不会有问题的,自旋锁的衍生锁读写自旋锁(rwlock)可允许读的并发。
1.定义和初始化读写自旋锁 rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* 静态初始化 */ rwlock_t my_rwlock; rwlock_init(&my_rwlock); /* 动态初始化 */ //-------------------------------------------------------------------------- 2.读锁定 void read_lock(rwlock_t *lock); void read_lock_irqsave(rwlock_t *lock, unsigned long flags); void read_lock_irq(rwlock_t *lock); void read_lock_bh(rwlock_t *lock); //-------------------------------------------------------------------------- 3.读解锁 void read_unlock(rwlock_t *lock); void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void read_unlock_irq(rwlock_t *lock); void read_unlock_bh(rwlock_t *lock); 在对共享资源进行读取之前,应该先调用读锁定函数,完成之后应调用读解锁函数。 read_lock_irqsave()、read_lock_irq()和 read_lock_bh()分别是 read_lock()分别与local_irq_save()、local_irq_disable()和local_bh_disable()的组合 ,读解锁函数read_unlock_irqrestore()、read_unlock_ irq()、read_unlock_bh()的情况与此类似。 //-------------------------------------------------------------------------- 4.写锁定 void write_lock(rwlock_t *lock); void write_lock_irqsave(rwlock_t *lock, unsigned long flags); void write_lock_irq(rwlock_t *lock); void write_lock_bh(rwlock_t *lock); int write_trylock(rwlock_t *lock); //-------------------------------------------------------------------------- 5.写解锁 void write_unlock(rwlock_t *lock); void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void write_unlock_irq(rwlock_t *lock); void write_unlock_bh(rwlock_t *lock); write_lock_irqsave()、write_lock_irq()、write_lock_bh()分别是 write_lock() 与local_irq_save()、local_irq_disable()和local_bh_disable()的组合,写解锁函数 write_unlock_irqrestore()、write_unlock_irq()、write_unlock_bh()的情况与此类似 |
在对共享资源进行读取之前,应该先调用写锁定函数,完成之后应调用写解锁函数。和 spin_trylock()一样, write_trylock()也只是尝试获取读写自旋锁,不管成功失败,都会立即返回。
读写自旋锁一般这样被使用,如下所示: rwlock_t lock; //定义 rwlock rwlock_init(&lock); //初始化 rwlock //读时获取锁 read_lock(&lock); ... //临界资源 read_unlock(&lock); //写时获取锁 write_lock_irqsave(&lock, flags); ... //临界资源 write_unlock_irqrestore(&lock, flags); |
⑥顺序锁
顺序锁(seqlock)是对读写锁的一种优化,若使用顺序锁,读执行单元绝不会被写执行单元阻塞,也就是说,读执行单元可以在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。
但是,写执行单元与写执行单元之间仍然是互斥的,即如果有写执行单元在进行写操作,其他写执行单元必须自旋在那里,直到写执行单元释放了顺序锁。
如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新读取数据,以便确保得到的数据是完整的。这种锁在读写同时进行的概率比较小时,性能是非常好的,而且它允许读写同时进行,因而更大地提高了并发性。
顺序锁有一个限制,它必须要求被保护的共享资源不含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,将导致 Oops。
写操作
1.获得顺序锁 void write_seqlock(seqlock_t *sl); int write_tryseqlock(seqlock_t *sl); write_seqlock_irqsave(lock, flags) write_seqlock_irq(lock) write_seqlock_bh(lock) 其中: write_seqlock_irqsave() = loal_irq_save() + write_seqlock() write_seqlock_irq() = local_irq_disable() + write_seqlock() write_seqlock_bh() = local_bh_disable() + write_seqlock() 2.释放顺序锁 void write_sequnlock(seqlock_t *sl); write_sequnlock_irqrestore(lock, flags) write_sequnlock_irq(lock) write_sequnlock_bh(lock) 其中: write_sequnlock_irqrestore() = write_sequnlock() + local_irq_restore() write_sequnlock_irq() = write_sequnlock() + local_irq_enable() write_sequnlock_bh() = write_sequnlock() + local_bh_enable() 写执行单元使用顺序锁的模式如下: write_seqlock(&seqlock_a); ...//写操作代码块 write_sequnlock(&seqlock_a); 因此,对写执行单元而言,它的使用与 spinlock 相同。 |
读操作
1.读开始 unsigned read_seqbegin(const seqlock_t *sl); read_seqbegin_irqsave(lock, flags) 读执行单元在对被顺序锁 s1 保护的共享资源进行访问前需要调用该函数, 该函数 仅返回顺序锁 s1 的当前顺序号。其中: read_seqbegin_irqsave() = local_irq_save() + read_seqbegin() 2.重读 int read_seqretry(const seqlock_t *sl, unsigned iv); read_seqretry_irqrestore(lock, iv, flags) 读执行单元在访问完被顺序锁 s1 保护的共享资源后需要调用该函数来检查, 在读 访问期间是否有写操作。如果有写操作,读执行单元就需要重新进行读操作。其中: read_seqretry_irqrestore() = read_seqretry() + local_irq_restore() 读执行单元使用顺序锁的模式如下: do { seqnum = read_seqbegin(&seqlock_a); //读操作代码块 ... } while (read_seqretry(&seqlock_a, seqnum)); |
⑦读-拷贝-更新
RCU(Read-Copy Update,读-拷贝-更新),它是基于其原理命名的。
对于被 RCU 保护的共享数据结构,读执行单元不需要获得任何锁就可以访问它,不使用原子指令,而且在除 Alpha 的所有架构上也不需要内存栅(Memory Barrier),因此不会导致锁竞争、内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。
使用 RCU 的写执行单元在访问它前需首先复制一个副本,然后对副本进行修改,最后使用一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的 CPU 都退出对共享数据的操作的时候。读执行单元没有任何同步开销,而写执行单元的同步开销则取决于使用的写执行单元间的同步机制。
但是,RCU 不能替代读写锁,因为如果写比较多时,对读执行单元的性能提高不能弥补写执行单元导致的损失。因为使用 RCU 时,写执行单元之间的同步开销会比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其他写执行单元的修改操作。
1.读锁定 rcu_read_lock() rcu_read_lock_bh() 2.读解锁 rcu_read_unlock() rcu_read_unlock_bh() 使用 RCU 进行读的模式如下: rcu_read_lock() ...//读临界区 rcu_read_unlock() 其中 rcu_read_lock()和 rcu_read_unlock()实质只是禁止和使能内核的抢占调度, 如下所示: #define rcu_read_lock() preempt_disable() #define rcu_read_unlock() preempt_enable() 其变种 rcu_read_lock_bh()、rcu_read_unlock_bh()则定义为: #define rcu_read_lock_bh() local_bh_disable() #define rcu_read_unlock_bh() local_bh_enable() 3.同步 RCU synchronize_rcu() 该函数由 RCU 写执行单元调用,它将阻塞写执行单元,直到所有的读执行单元已经完成读执行单元临界区,写执行单元才可以继续下一步操作。如果有多个 RCU写执行单元调用该函数,它们将在一个 grace period (即所有的读执行单元已经完成对临界区的访问)之后全部被唤醒。synchronize_rcu()保证所有 CPU 都处理完正在运行的读执行单元临界区。 synchronize_kernel() 内核代码使用该函数来等待所有 CPU 处于可抢占状态,目前功能等同于synchronize_rcu(),但现在已经不建议使用,而是使用 synchronize_sched(),该函数用于等待所有 CPU 都处在可抢占状态,它能保证正在运行的中断处理函数处理完毕,但不能保证正在运行的软中断处理完毕。 4.挂接回调 void fastcall call_rcu(struct rcu_head *head,void (*func)(struct rcu_head *rcu)); 函数 call_rcu()也由 RCU 写执行单元调用,它不会使写执行单元阻塞,因而可以在中断上下文或软中断中使用。该函数将把函数 func 挂接到 RCU 回调函数链上,然后立即返回。函数 synchronize_rcu()的实现实际上使用了 call_rcu()函数。 void fastcall call_rcu_bh(struct rcu_head *head,void (*func)(struct rcu_head *rcu)); call_ruc_bh()函数的功能几乎与 call_rcu()完全相同,唯一差别就是它把软中断的完成也当做经历一个 quiescent state(静默状态),因此如果写执行单元使用了该函数,在进程上下文的读执行单元必须使用 rcu_read_lock_bh()。 |
每个 CPU 维护两个数据结构 rcu_data 和 rcu_bh_data,它们用于保存回调函数,函数 call_rcu()把回调函数注册到 rcu_data,而 call_rcu_bh()则把回调函数注册到rcu_bh_data,在每一个数据结构上,回调函数被组成一个链表,先注册的排在前头,后注册的排在末尾。
使用 RCU 时,读执行单元必须提供一个信号给写执行单元以便写执行单元能够确定数据可以被安全地释放或修改的时机。有一个专门的垃圾收集器来探测读执行单元的信号,一旦所有的读执行单元都已经发送信号告知它们都不在使用被 RCU 保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。
RCU 还增加了链表操作函数的 RCU 版本
static inline void list_add_rcu(struct list_head *new, struct list_head *head); 该函数把链表元素 new 插入到 RCU 保护的链表 head 的开头,内存栅保证了在引用这个新插入的链表元素之前,新链表元素的链接指针的修改对所有读执行单元是可见的。 static inline void list_add_tail_rcu(struct list_head *new, struct list_head *head); 该函数类似于 list_add_rcu(),它将把新的链表元素 new 添加到被 RCU 保护的链表的末尾。 static inline void list_del_rcu(struct list_head *entry); 该函数从 RCU 保护的链表中删除指定的链表元素 entry。 static inline void list_replace_rcu(struct list_head *old, struct list_head *new); 该函数是 RCU 新添加的函数,并不存在非 RCU 版本。它使用新的链表元素 new取代旧的链表元素 old,内存栅保证在引用新的链表元素之前,它对链接指针的修正对所有读行单元是可见的。 list_for_each_rcu(pos, head) 该宏用于遍历由 RCU 保护的链表 head,只要在读执行单元临界区使用该函数,它就可以安全地和其他_rcu 链表操作函数并发运行如 list_add_rcu()。 list_for_each_safe_rcu(pos, n, head) 该宏类似于 list_for_each_rcu,不同之处在于它允许安全地删除当前链表元素 pos。 list_for_each_entry_rcu(pos, head, member) 该宏类似于 list_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构链表,当前链表元素 pos 为一个包含 struct list_head 结构的特定的数据结构。 static inline void hlist_del_rcu(struct hlist_node *n) 它从由 RCU 保护的哈希链表中移走链表元素 n。 static inline void hlist_add_head_rcu(struct hlist_node *n, struct hlist_head *h); 该函数用于把链表元素 n 插入到被 RCU 保护的哈希链表的开头,但同时允许读执行单元对该哈希链表的遍历。内存栅确保在引用新链表元素之前, 它对指针的修改对所有读执行单元可见。 hlist_for_each_rcu(pos, head) 该宏用于遍历由 RCU 保护的哈希链表 head,只要在读端临界区使用该函数,它就可以安全地和其他_rcu 哈希链表操作函数(如 hlist_add_rcu)并发运行。 hlist_for_each_entry_rcu(tpos, pos, head, member) 类似于 hlist_for_each_rcu(),不同之处在于它用于遍历指定类型的数据结构哈希链表,当前链表元素 pos 为一个包含 struct list_head 结构的特定的数据结构。 |
目前,RCU 的使用在内核中已经非常普遍,内核中大量原先使用读写锁的代码被RCU 替换,下面的表单左右两列平行地分别给出使用读写锁和 RCU 实现链表元素读、删除、添加和修改的代码。
⑧信号量的使用
信号量(semaphore)是用于保护临界区的一种常用方法,它的使用方式和自旋锁类似。与自旋锁相同,只有得到信号量的进程才能执行临界区代码。但是,与自旋锁不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
1.定义信号量 下列代码定义名称为 sem 的信号量。 struct semaphore sem; 2.初始化信号量 void sema_init (struct semaphore *sem, int val); 该函数初始化信号量,并设置信号量 sem 的值为 val。尽管信号量可以被初始化为大于 1 的值从而成为一个计数信号量,但是它通常不被这样使用。 void init_MUTEX(struct semaphore *sem); 该函数用于初始化一个用于互斥的信号量,它把信号量 sem 的值设置为 1,等同于 sema_init (struct semaphore *sem, 1)。 void init_MUTEX_LOCKED (struct semaphore *sem); 该函数也用于初始化一个信号量,但它把信号量 sem 的值设置为 0,等同于 sema_init (struct semaphore *sem, 0)。 此外,下面两个宏是定义并初始化信号量的“快捷方式”。 DECLARE_MUTEX(name) DECLARE_MUTEX_LOCKED(name) 前者定义一个名为 name 的信号量并初始化为 1,后者定义一个名为 name 的信号量并初始化为 0。 3.获得信号量 void down(struct semaphore * sem); 该函数用于获得信号量 sem,它会导致睡眠,因此不能在中断上下文使用。 int down_interruptible(struct semaphore * sem); 该函数功能与 down()类似,不同之处为,因为 down()而进入睡眠状态的进程不能被信号打断,而因为 down_interruptible()而进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非 0。 int down_trylock(struct semaphore * sem); 该函数尝试获得信号量 sem,如果能够立刻获得,它就获得该信号量并返回 0,否则,返回非 0 值。它不会导致调用者睡眠,可以在中断上下文使用。在使用 down_interruptible()获取信号量时,对返回值一般会进行检查,如果非 0,通常立即返回-ERESTARTSYS,如: if (down_interruptible(&sem)){ return - ERESTARTSYS; } 4.释放信号量 void up(struct semaphore * sem); 该函数释放信号量 sem,唤醒等待者。 信号量一般这样被使用,如下所示:
//定义信号量 DECLARE_MUTEX(mount_sem); down(&mount_sem);//获取信号量,保护临界区 ... critical section //临界区 ... up(&mount_sem);//释放信号量 |
⑨完成量用于同步
Linux 系统提供了一种比信号量更好的同步机制,即完成量:它用于一个执行单元等待另一个执行单元执行完某事.
1.定义完成量 下列代码定义名为 my_completion 的完成量。 struct completion my_completion; 2.初始化 completion 下列代码初始化 my_completion 这个完成量。 init_completion(&my_completion); 对 my_completion 的定义和初始化可以通过如下快捷方式实现。 DECLARE_COMPLETION(my_completion); 3.等待完成量 下列函数用于等待一个 completion 被唤醒。 void wait_for_completion(struct completion *c); 4.唤醒完成量 下面两个函数用于唤醒完成量。 void complete(struct completion *c); void complete_all(struct completion *c); 前者只唤醒一个等待的执行单元,后者释放所有等待同一完成量的执行单元。 |
⑩信号量vs自旋锁
信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发生进程上下文切换,当前进程进入睡眠状态,CPU 将运行其他进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间较长时,用信号量才是较好的选择。
当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它节省上下文切换的时间。但是 CPU 得不到自旋锁会在那里空转直到其他执行单元解锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统的效率。
使用的三项原则: 1.当锁不能被获取时,使用信号量的开销是进程上下文切换时间 Tsw,使用自旋锁的开销是等待获取自旋锁 (由临界区执行时间决定) Tcs, 若 Tcs 比较小,应使用自旋锁,若 Tcs 很大,应使用信号量。 2.信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。 3.信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁。当然,如果一定要使用信号量,则只能通过 down_trylock()方式进行,不能获取就立即返回以避免阻塞。 |
⑾读写信号量
读写信号量可能引起进程阻塞,但它可允许 N 个读执行单元同时访问共享资源,而最多只能有一个写执行单元。因此,读写信号量是一种相对放宽条件的粒度稍大于信号量的互斥机制。
1.定义和初始化读写信号量 struct rw_semaphore my_rws; /*定义读写信号量*/ void init_rwsem(struct rw_semaphore *sem); /*初始化读写信号量*/ 2.读信号量获取 void down_read(struct rw_semaphore *sem); int down_read_trylock(struct rw_semaphore *sem); 3.读信号量释放 void up_read(struct rw_semaphore *sem); 4.写信号量获取 void down_write(struct rw_semaphore *sem); int down_write_trylock(struct rw_semaphore *sem); 5.写信号量释放 void up_write(struct rw_semaphore *sem);
读写信号量一般这样被使用,如下所示: rw_semaphore rw_sem; //定义读写信号量 init_rwsem(&rw_sem); //初始化读写信号量 //读时获取信号量 down_read(&rw_sem); ... //临界资源 up_read(&rw_sem); //写时获取信号量 down_write(&rw_sem); ... //临界资源 up_write(&rw_sem); |
⑿互斥体
尽管信号量已经可以实现互斥的功能,而且包含 DECLARE_MUTEX() 、init_MUTEX ()等定义信号量的宏或函数, 从名字上看就体现出了互斥体的概念, 但是mutex 在 Linux 内核中还是真实地存在的。
struct mutex my_mutex; mutex_init(&my_mutex); 下面的两个函数用于获取互斥体。 void fastcall mutex_lock(struct mutex *lock); 引起的睡眠不能被信号打断 int fastcall mutex_lock_interruptible(struct mutex *lock); 引起的睡眠能被信号打断 int fastcall mutex_trylock(struct mutex *lock); 尝试获得 mutex,获取不到 mutex 时不会引起进程睡眠 下列函数用于释放互斥体。 void fastcall mutex_unlock(struct mutex *lock);
使用方法: struct mutex my_mutex; //定义 mutex mutex_init(&my_mutex); //初始化 mutex mutex_lock(&my_mutex); //获取 mutex ...//临界资源 mutex_unlock(&my_mutex); //释放 mutex |
附驱动程序示例:
1 #include <linux/module.h> 2 #include <linux/types.h> 3 #include <linux/fs.h> 4 #include <linux/kernel.h> 5 #include <linux/device.h> 6 #include <linux/errno.h> 7 #include <linux/mm.h> 8 #include <linux/sched.h> 9 #include <linux/init.h> 10 #include <linux/cdev.h> 11 #include <asm/io.h> 12 #include <asm/system.h> 13 #include <asm/uaccess.h> 14 #include <linux/sem.h> 15 16 #define GLOBALMEM_SIZE 0x1000 /*全局内存大小:4KB*/ 17 #define MEM_CLEAR 0x1 /*清零全局内存*/ 18 #define GLOBALMEM_MAJOR 0 /*预设的 globalmem 的主设备号*/ 19 20 21 static int globalmem_major = GLOBALMEM_MAJOR; 22 /*globalmem 设备结构体*/ 23 struct globalmem_dev 24 { 25 struct cdev cdev; /*cdev 结构体*/ 26 unsigned char mem[GLOBALMEM_SIZE]; /*全局内存*/ 27 struct semaphore sem; /* 1. 并发控制用的信号量 */ 28 }; 29 30 struct globalmem_dev *globalmem_devp; /*设备结构体实例*/ 31 32 //自动添加设备节点 33 static struct class *cls = NULL; 34 35 36 /*文件打开函数*/ 37 static int globalmem_open(struct inode *inode, struct file *filp){ 38 /*将设备结构体指针赋值给文件私有数据指针*/ 39 filp->private_data = globalmem_devp; 40 return 0; 41 } 42 //释放函数 43 static int globalmem_release(struct inode *inode, struct file *filp){ 44 return 0; 45 } 46 //文件读函数 47 static ssize_t globalmem_read(struct file *filp, char __user *buf,size_t count,loff_t *ppos) 48 { 49 unsigned long p = *ppos; 50 int ret = 0; 51 struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/ 52 /*分析和获取有效的读长度*/ 53 if (p >= GLOBALMEM_SIZE) //要读的偏移位置越界 54 return count ? - ENXIO: 0; 55 if (count > GLOBALMEM_SIZE - p)//要读的字节数太大 56 count = GLOBALMEM_SIZE - p; 57 58 if(down_interruptible(&dev->sem)) //3.获取信号量 59 { 60 return -ERESTARTSYS; 61 } 62 63 /*内核空间→用户空间*/ 64 if (copy_to_user(buf, (void*)(dev->mem + p), count)) 65 { 66 ret = - EFAULT; 67 }else{ 68 *ppos += count; 69 ret = count; 70 printk(KERN_INFO "read %d bytes(s) from %ld\n", count, p); 71 } 72 73 up(&dev->sem); //4.释放信号量 74 75 return ret; 76 } 77 //文件写函数 78 static ssize_t globalmem_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos) 79 { 80 unsigned long p = *ppos; 81 int ret = 0; 82 struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/ 83 /*分析和获取有效的写长度*/ 84 if (p >= GLOBALMEM_SIZE) //要写的偏移位置越界 85 return count ? - ENXIO: 0; 86 if (count > GLOBALMEM_SIZE - p) //要写的字节数太多 87 count = GLOBALMEM_SIZE - p; 88 89 if(down_interruptible(&dev->sem)) //4.获取信号量 90 { 91 return -ERESTARTSYS; 92 } 93 94 /*用户空间→内核空间*/ 95 if (copy_from_user(dev->mem + p, buf, count)){ 96 ret = - EFAULT; 97 }else{ 98 *ppos += count; 99 ret = count; 100 printk(KERN_INFO "written %d bytes(s) from %ld\n", count, p); 101 } 102 up(&dev->sem); //释放信号量 103 return ret; 104 } 105 //文件定位函数 106 static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) 107 { 108 loff_t ret; 109 switch (orig){ 110 case 0: /*从文件开头开始偏移*/ 111 if (offset < 0){ 112 ret = - EINVAL; 113 break; 114 } 115 if ((unsigned int)offset > GLOBALMEM_SIZE) //偏移越界 116 { 117 ret = - EINVAL; 118 break; 119 } 120 filp->f_pos = (unsigned int)offset; 121 ret = filp->f_pos; 122 break; 123 124 case 1: /*从当前位置开始偏移*/ 125 if ((filp->f_pos + offset) > GLOBALMEM_SIZE) //偏移越界 126 { 127 ret = - EINVAL; 128 break; 129 } 130 if ((filp->f_pos + offset) < 0) 131 { 132 ret = - EINVAL; 133 break; 134 } 135 filp->f_pos += offset; 136 ret = filp->f_pos; 137 break; 138 default: ret = - EINVAL; break; 139 } 140 return ret; 141 } 142 // 143 static int globalmem_ioctl(struct inode *inodep, struct file *filp,unsigned int cmd, unsigned long arg) 144 { 145 struct globalmem_dev *dev = filp->private_data; 146 switch (cmd){ 147 case MEM_CLEAR: //清除全局内存 148 149 if(down_interruptible(&dev->sem)) //3.获取信号量 150 { 151 return -ERESTARTSYS; 152 } 153 memset(dev->mem, 0, GLOBALMEM_SIZE); 154 up(&dev->sem); //释放信号量 155 printk(KERN_INFO "globalmem is set to zero\n"); 156 break; 157 default: 158 return - EINVAL; //其他不支持的命令 159 break; 160 } 161 return 0; 162 } 163 //定义file_operatetiont 164 static const struct file_operations globalmem_fops = 165 { 166 .owner = THIS_MODULE, 167 .llseek = globalmem_llseek, 168 .read = globalmem_read, 169 .write = globalmem_write, 170 .ioctl = globalmem_ioctl, 171 .open = globalmem_open, 172 .release = globalmem_release, 173 }; 174 175 /*globalmem 设备驱动模块加载函数*/ 176 static int globalmem_init(void){ 177 int result,err; 178 dev_t devno = MKDEV(globalmem_major, 0); 179 /* 申请字符设备驱动区域*/ 180 if (globalmem_major){ 181 result = register_chrdev_region(devno, 1, "globalmem"); 182 }else{ 183 /* 动态获得主设备号 */ 184 result = alloc_chrdev_region(&devno, 0, 1, "globalmem"); 185 globalmem_major = MAJOR(devno); 186 } 187 if (result < 0) 188 return result; 189 printk("request major %d,minor %d\n",globalmem_major,MINOR(devno)); 190 191 /*动态申请设备结构体的内存*/ 192 globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL); 193 if(!globalmem_devp){ /* 申请失败 */ 194 result = -ENOMEM; 195 goto fail_malloc; 196 } 197 memset(globalmem_devp, 0, sizeof(struct globalmem_dev)); 198 199 /*初始化设备结构体*/ 200 cdev_init(&globalmem_devp->cdev, &globalmem_fops); 201 globalmem_devp->cdev.owner = THIS_MODULE; 202 globalmem_devp->cdev.ops = &globalmem_fops; 203 /* 注册cdev */ 204 err = cdev_add(&globalmem_devp->cdev, devno, 1); 205 if (err) 206 printk(KERN_NOTICE "Error %d adding globalmem", err); 207 208 //自动创建设备节点 209 cls = class_create(THIS_MODULE,"globalmem"); 210 device_create(cls, NULL, devno,NULL,"globalmem"); 211 212 init_MUTEX(&globalmem_devp->sem); //2.初始化信号量 213 214 return 0; 215 fail_malloc: unregister_chrdev_region(devno,1); 216 return result; 217 } 218 /*globalmem 设备驱动模块卸载函数*/ 219 static void globalmem_exit(void) 220 { 221 dev_t devno = MKDEV(globalmem_major, 0); 222 223 device_destroy(cls,devno); 224 class_destroy(cls); 225 cdev_del(&globalmem_devp->cdev); /*删除 cdev 结构*/ 226 kfree(globalmem_devp); /* 释放设备结构体内存 */ 227 unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);/*注销设备*/ 228 } 229 230 module_init(globalmem_init); 231 module_exit(globalmem_exit); 232 MODULE_LICENSE("GPL");
测试实例:
root@EasyARM-iMX257 /mnt/nfs/module/48_mutex_globalmem# echo "hello,LoverXueEr" > /dev/globalmem written 17 bytes(s) from 0 root@EasyARM-iMX257 /mnt/nfs/module/48_mutex_globalmem# cat /dev/globalmem hello,LoverXueEr |