临界资源:是指同一个时段内只允许唯一一个访问者操作的资源。比如打印机、IO模块等,但Linux是多任务的,其内核对资源的管理是抢占式的。多个进程同时运行即所谓的并发,而如果多个进程都同时访问同一个资源就会产生竞态。由于驱动模块的特殊性,它不可避免会存在被多个进程同时“打开、读写、关闭”的情况。设想一下,如果某个驱动的逻辑是open的时候分配一块缓存用于read/write,close的时候又释放缓存,就会存在A进程刚打开的设备节点,B进程就关闭,缓存分配了又释放,最终在读写时导致程序崩溃。
所以,本章主要学习Linux驱动模块有哪些手段可以处理并发时的竞态问题。
原子操作 原子操作就是保证对数据修改的完整性,也就是说a = a + 1
这么简单的表达式也难以避免被编译为多个指令周期,也许在任务A中刚读完表达式右值,又被任务B更新了a
的寄存器,结果一个简单的自加1的操作都可能出现很多诡异的结果。
因此,为了确保i++
就是自加1的操作,内核封装了很多API以实现变量的原子操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <asm/atomic.h> # 引入原子操作API atomic_t v = ATOMIC_INIT(0 );int atomic_read (atomic_t * v) ;void atomic_set (atomic_t * v, int i) ;void atomic_add (int i, atomic_t * v) ; void atomic_sub (int i, atomic_t * v) ; int atomic_and (int i, atomic_t * v) ; int atomic_or (int i, atomic_t * v) ; int atomic_xor (int i, atomic_t * v) ; int atomic_andnot (int i, atomic_t * v) ; int atomic_fetch_add (int i, atomic_t * v) ;int atomic_fetch_sub (int i, atomic_t * v) ;int atomic_fetch_and (int i, atomic_t * v) ;int atomic_fetch_or (int i, atomic_t * v) ;int atomic_fetch_xor (int i, atomic_t * v) ;int atomic_fetch_andnot (int i, atomic_t * v) ;void atomic_add_return (int i, atomic_t * v) ; void atomic_sub_return (int i, atomic_t * v) ; void atomic_int (atomic_t * v) ; void atomic_dec (atomic_t * v) ; 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_negative (int i, atomic_t * v) ;
自旋锁 自旋锁是一种对临界资源互斥访问的手段,也就是说在访问资源之前上个锁,访问完成后解锁,如果一个进程在访问资源是发现“锁住”了,就会原地打转——而非进入睡眠!直到锁被解开。就好比一辆车遇到红灯后停了下来但没熄火,发动机一直在空转,直到绿灯。但自旋锁有个很大的弊端——“如果红绿灯刚好坏了,发动机会永远空转下去”。
先来看看自旋锁的简单用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <linux/spinlock.h> spinlock_t lock;spin_lock_init(&lock); spin_lock(&lock); spin_trylock(&lock); spin_unlock(&lock);
上边是最简单的使用方式,但自旋锁还会受到内核中断、底半部(BH)的影响,所以衍生出了更多的“锁定”和“解锁”API。就好比驾驶员在等红灯时跑去尿尿,恰好此时绿灯亮起,该怎么办?答:禁止驾驶员尿尿😄。
这些函数要视情况具体使用:
1 2 3 4 5 6 7 8 9 void spin_lock_irq (spinlock_t * lock) ; void spin_unlock_irq (spinlock_t * lock) ; void spin_lock_irqsave (spinlock_t * lock, unsigned long flags) ;void spin_unlock_irqrestore (spinlock_t * lock, unsigned long flags) ;void spin_lock_bh (spinlock_t * lock) ; void spin_unlock_bh (spinlock_t * lock) ;
开发驱动时应谨慎使用自旋锁,要直到它“空转”的意思是不放弃CPU,所以在其自旋时会对CPU资源造成浪费,如果不小心锁死了,那就悲催了。
综上,自旋锁只是在访问临界资源前后加了一层排他性的锁 ,至于锁内的资源操作它完全不关心,然而共享资源在并发访问时往往是这样的需求:可以被同时读,但不允许同时写 。也是基于此,内核提供了更多的API来满足这些场景。
读写自旋锁
读写自旋锁会区别读和写的资源,满足并发读取,单一写入的要求,但底层也是“自旋”的机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 rwlock_t lock;rwlock_init(&lock); read_lock(&lock); read_unlock(&lock); write_lock(&lock); write_unlock(&lock);
顺序锁
顺序锁是读写锁的优化版,因为读写锁的读和写操作是互斥的,所以使用顺序锁后,当资源正在写入时,依然可以被读取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 unsigned int read_seqbegin (const seqlock_t * sl) ;unsigned int read_seqretry (const seqlock_t *sl, unsigned int start) ;void write_seqlock (seqlock_t *sl) ;void write_sequnlock (seqlock_t *sl) ;#include <linux/seqlock.h> seqlock_t lock;seqlock_init(&lock); unsigned int start = 0 ;do { start = read_seqbegin(&lock); } while (read_seqretry(&lock, start)); write_seqlock(&lock); write_sequnlock(&lock);
RCU : Read-Copy-Update
读——复制——更新的意思是:把要写的部分先读取被拷贝一个副本,然后把内容写入副本,等到何时的时机一把更新到源。
1 2 3 4 5 6 7 8 #include <linux/rcupdate.h> void rcu_read_lock (void ) ;void rcu_read_unlock (void ) ;void synchronize_rcu (void ) ;void call_rcu (struct callback_head *head, rcu_callback_t func) ;
互斥体 互斥的机制在多线程中是很常见的,Linux内核的互斥体metux
本质是由自旋锁实现的。但与自旋锁不同的是,互斥体会进入默认睡眠,放弃CPU抢占。
1 2 3 4 5 6 7 8 9 10 11 #include <linux/mutex.h> struct mutex mutex ;mutex_init(&mutex); void mutex_lock (struct mutex *lock) ; int mutex_lock_interruptible (struct mutex *lock) ; int mutex_trylock (struct mutex *lock) ; void mutex_unlock (struct mutex *lock) ;
completion completion就是指一个执行单元等待另一个执行单元的完成信号,有点多线程同步的意思。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <linux/completion.h> struct completion completion ;void init_completion (struct completion *x) ;void reinit_completion (struct completion *x) ;void wait_for_completion (struct completion *) ;void complete (struct completion *) ;void complete_all (struct completion *) ;