目录[-]linux
在上一篇介绍了linux驱动的调试方法,这一篇介绍一下在驱动编程中会遇到的并发和竟态以及如何处理并发和竞争。android
首先什么是并发与竟态呢?并发(concurrency)指的是多个执行单元同时、并行被执行。而并发的执行单元对共享资源(硬件资源和软件上的全局、静态变量)的访问则容易致使竞态(race conditions)。可能致使并发和竟态的状况有:程序员
SMP(Symmetric Multi-Processing),对称多处理结构。SMP是一种紧耦合、共享存储的系统模型,它的特色是多个CPU使用共同的系统总线,所以可访问共同的外设和存储器。 算法
中断。中断可 打断正在执行的进程,若中断处理程序访问进程正在访问的资源,则竞态也会发生。中断也可能被新的更高优先级的中断打断,所以,多个中断之间也可能引发并发而致使竞态。chrome
内核进程的抢占。linux是可抢占的,因此一个内核进程可能被另外一个高优先级的内核进程抢占。若是两个进程共同访问共享资源,就会出现竟态。编程
以上三种状况只有SMP是真正意义上的并行,而其余都是宏观上的并行,微观上的串行。但其都会引起对临界共享区的竞争问题。而解决竞态问题的途径是保证对共享资源的互斥访问,即一个执行单元在访问共享资源的时候,其余的执行单元被禁止访问。那么linux内核中如何作到对对共享资源的互斥访问呢?在linux驱动编程中,经常使用的解决并发与竟态的手段有信号量与互斥锁,Completions 机制,自旋锁(spin lock),以及一些其余的不使用锁的实现方式。下面一一介绍。微信
信号量其实就是一个整型值,其核心是一个想进入临界区的进程将在相关信号量上调用 P; 若是信号量的值大于零, 这个值递减 1 而且进程继续. 相反,,若是信号量的值是 0 ( 或更小 ), 进程必须等待直到别人释放信号量. 解锁一个信号量经过调用 V 完成; 这个函数递增信号量的值,,而且, 若是须要, 唤醒等待的进程。而当信号量的初始值为1的时候,就变成了互斥锁。数据结构
信号量的典型使用形式:并发
//声明信号量 struct semaphore sem; //初始化信号量 void sema_init(struct semaphore *sem, int val) //经常使用下面两种形式 #define init_MUTEX(sem) sema_init(sem, 1) #define init_MUTEX_LOCKED(sem) sema_init(sem, 0) //如下是初始化信号量的快捷方式,最经常使用的 DECLARE_MUTEX(name) //初始化name的信号量为1 DECLARE_MUTEX_LOCKED(name) //初始化信号量为0 //经常使用操做 DECLARE_MUTEX(mount_sem); down(&mount_sem); //获取信号量 ... critical section //临界区 ... up(&mount_sem); //释放信号量
经常使用的down操做还有微信公众平台
// 相似down(),由于down()而进入休眠的进程不能被信号打断,而由于down_interruptible()而进入休眠的进程能被信号打断, // 信号也会致使该函数返回,此时返回值非0 int down_interruptible(struct semaphore *sem); // 尝试得到信号量sem,若当即得到,它就得到该信号量并返回0,不然,返回非0.它不会致使调用者睡眠,可在中断上下文使用 int down_trylock(struct semaphore *sem);
完成量(completion)提供了一种比信号量更好的同步机制,它用于一个执行单元等待另外一个执行单元执行完某事。
</pre></div><div><pre name="code" class="cpp">// 定义完成量 struct completion my_completion; // 初始化completion init_completion(&my_completion); // 定义和初始化快捷方式: DECLEAR_COMPLETION(my_completion); // 等待一个completion被唤醒 void wait_for_completion(struct completion *c); // 唤醒完成量 void cmplete(struct completion *c); void cmplete_all(struct completion *c);
若一个进程要访问临界资源,测试锁空闲,则进程得到这个锁并继续执行;若测试结果代表锁扔被占用,进程将在一个小的循环内重复“测试并设置”操做,进行所谓的“自旋”,等待自旋锁持有者释放这个锁。自旋锁与互斥锁相似,可是互斥锁不能用在可能睡眠的代码中,而自旋锁能够用在可睡眠的代码中,典型的应用是能够用在中断处理函数中。自旋锁的相关操做:
// 定义自旋锁 spinlock_t spin; // 初始化自旋锁 spin_lock_init(lock); // 得到自旋锁:若能当即得到锁,它得到锁并返回,不然,自旋,直到该锁持有者释放 spin_lock(lock); // 尝试得到自旋锁:若能当即得到锁,它得到并返回真,不然当即返回假,再也不自旋 spin_trylock(lock); // 释放自旋锁: 与spin_lock(lock)和spin_trylock(lock)配对使用 spin_unlock(lock); 自旋锁的使用: // 定义一个自旋锁 spinlock_t lock; spin_lock_init(&lock); spin_lock(&lock); // 获取自旋锁,保护临界区 ... // 临界区 spin_unlock(); // 解锁
自旋锁持有期间内核的抢占将被禁止。自旋锁能够保证临界区不受别的CPU和本CPU内的抢占进程打扰,可是获得锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。为防止这种影响,须要用到自旋锁的衍生:
spin_lock_irq() = spin_lock() + local_irq_disable() spin_unlock_irq() = spin_unlock() + local_irq_enable() spin_lock_irqsave() = spin_lock() + 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()
以上是linux驱动编程中常常用到的锁机制,下面讲一些内核中其余的一些实现。
有时, 你能够从新打造你的算法来彻底避免加锁的须要.。许多读者/写者状况 -- 若是只有一个写者 -- 经常可以在这个方式下工做.。若是写者当心使数据结构,由读者所见的,是一直一致的,,有可能建立一个不加锁的数据结构。在linux内核中就有一个通用的无锁的环形缓冲实现,具体内容参考<linux/kfifo.h>。
原子操做指的是在执行过程当中不会被别的代码路径所中断的操做。原子变量与位操做都是原子操做。如下是其相关操做介绍。
// 设置原子变量的值 void atomic_set(atomic_t *v, int i); // 设置原子变量的值为i atomic_t v = ATOMIC_INIT(0); // 定义原子变量v,并初始化为0 // 获取原子变量的值 atomic_read(atomic_t *v); // 返回原子变量的值 // 原子变量加/减 void atomic_add(int i, atomic_t *v); // 原子变量加i void atomic_sub(int i, atomic_t *v); // 原子变量减i // 原子变量自增/自减 void atomic_inc(atomic_t *v); // 原子变量增长1 void atomic_dec(atomic_t *v); // 原子变量减小1 // 操做并测试:对原子变量进行自增、自减和减操做后(没有加)测试其是否为0,为0则返回true,不然返回false 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); // 操做并返回: 对原子变量进行加/减和自增/自减操做,并返回新的值 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); 位原子操做: // 设置位 void set_bit(nr, void *addr); // 设置addr地址的第nr位,即将位写1 // 清除位 void clear_bit(nr, void *addr); // 清除addr地址的第nr位,即将位写0 // 改变位 void change_bit(nr, void *addr); // 对addr地址的第nr位取反 // 测试位 test_bit(nr, void *addr); // 返回addr地址的第nr位 // 测试并操做:等同于执行test_bit(nr, void *addr)后再执行xxx_bit(nr, void *addr) int test_and_set_bit(nr, void *addr); int test_and_clear_bit(nr, void *addr); int test_and_change_bit(nr, void *addr);
使用seqlock锁,读执行单元不会被写执行单元阻塞,即读执行单元能够在写执行单元对被seqlock锁保护的共享资源进行写操做时仍然能够继续读,而没必要等待写执行单元完成写操做,写执行单元也不须要等待全部读执行单元完成读操做才去进行写操做。写执行单元之间还是互斥的。若读操做期间,发生了写操做,必须从新读取数据。seqlock锁必需要求被保护的共享资源不含有指针。
// 得到顺序锁 void write_seqlock(seqlock_t *sl); int write_tryseqlock(seqlock_t *sl); write_seqlock_irqsave(lock, flags) write_seqlock_irq(lock) write_seqlock_bh() // 释放顺序锁 void write_sequnlock(seqlock_t *sl); write_sequnlock_irqrestore(lock, flags) write_sequnlock_irq(lock) write_sequnlock_bh() // 写执行单元使用顺序锁的模式以下: write_seqlock(&seqlock_a); ... // 写操做代码块 write_sequnlock(&seqlock_a); 读执行单元操做: // 读开始:返回顺序锁sl当前顺序号 unsigned read_seqbegin(const seqlock_t *sl); read_seqbegin_irqsave(lock, flags) // 重读:读执行单元在访问完被顺序锁sl保护的共享资源后须要调用该函数来检查,在读访问期间是否有写操做。如有写操做,重读 int read_seqretry(const seqlock_t *sl, unsigned iv); read_seqretry_irqrestore(lock, iv, flags) // 读执行单元使用顺序锁的模式以下: do{ seqnum = read_seqbegin(&seqlock_a); // 读操做代码块 ... }while(read_seqretry(&seqlock_a, seqnum));
读取-拷贝-更新(RCU) 是一个高级的互斥方法,在合适的时候能够取得很是高的效率。RCU能够看做读写锁的高性能版本,相比读写锁,RCU的优势在于既容许多个读执行单元同时访问被保护的数据,又容许多个读执行单元和多个写执行单元同时访问被保护的数据。可是RCU不能替代读写锁,由于若是写比较多时,对读执行单元的性能提升不能弥补写执行单元致使的损失。因为平时应用较少,因此不作多说。
以上就是linux驱动编程中涉及的并发与竞态的内容,下面作一个简单的小结。
如今的处理器基本上都是SMP类型的,并且在新的内核版本中,基本上都支持抢占式的操做,在linux中不少程序都是可重入的,要保护这些数据,就得使用不一样的锁机制。而锁机制的基本操做过程其实大同小异的,声明变量,上锁,执行临界区代码,而后再解锁。不一样点在于,能够重入的限制不一样,有的能够无限制重入,有的只容许异种操做重入,而有的是不容许重入操做的,有的能够在可睡眠代码中使用,有的不能够在可睡眠代码中使用。而在考虑不一样的锁机制的使用时,也要考虑CPU处理的效率问题,对于不一样的代码长度,不一样的代码执行时间,选择一个好的锁对CPU的良好使用有很大的影响,不然将形成浪费。
以前在linux设备驱动第三篇:写一个简单的字符设备驱动中介绍了简单的字符设备驱动,下一篇将介绍一些字符设备驱动中得高级操做。
第一时间得到博客更新提醒,以及更多技术信息分享,欢迎关注我的微信公众平台:程序员互动联盟(coder_online),扫一扫下方二维码或搜索微信号coder_online便可关注,阅读android,chrome等多种热门技术文章。