首页 > 代码库 > Hasen的linux设备驱动开发学习之旅--linux设备驱动中的并发与竞态

Hasen的linux设备驱动开发学习之旅--linux设备驱动中的并发与竞态

/**
 * Author:hasen
 * 参考 :《linux设备驱动开发详解》
 * 简介:android小菜鸟的linux
 * 	         设备驱动开发学习之旅
 * 主题:linux设备驱动中的并发与竞态
 * Date:2014-11-04
 */
1、并发与竞态
	并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(软件上的全
	局变量,静态变量等)的访问则很容易导致竞态(race conditions).
	
	主要的竞态发生在以下几种情况:
		(1)对称多处理(SMP)的多个CPU
		(2)单CPU内进程与抢占它的进程
		(3)中断(硬中断、软中断、Tasklet、底半部)与进程之间
	中断屏蔽、原子操作、自旋锁和信号量是Linux设备驱动中可采用的互斥途径。
2.中断屏蔽
	在单CPU范围内避免竞态的简单和省事的方法就是在进入临界区之前屏蔽系统的中断。
	中断屏蔽的使用方法是:
		local_irq_disable() /*屏蔽中断*/
		...
		critical setion /*临界区*/
		...
		local_irq_enable() /*开中断*/
	长时间屏蔽中断是很危险的。
	除了禁止系统中断,还可以保存当前CPU的中断位信息
		local_irq_save(flags)
		...
		critical setion 
		...
		local_irq_restore(flags)
	如果只是想禁止中断的底半部
		local_bh_disable()
		...
		critical setion 
		...
		local_bh_enable()
3、原子操作
	原子操作是指在执行过程中不会被别的代码路径所中断的操作。
	整型原子操作:
		(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) ;/*自增后测试返回值是否为0,为0返回true*/
			int atomic_dec_and_test(atomic_t *v) ;/*自减后测试返回值是否为0,为0返回true*/
			int atomic_sub_and_test(int i, atomic_t *v) ;/*减操作后测试返回值是否为0,
				为0返回true*/
		(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_set_bit(nr,void *addr);
			int test_clear_bit(nr,void *addr);
			int test_change_bit(nr,void *addr);
	使用原子变量实现设备只能被一个进程打开
		
		static atmoic_t xxx_available = ATOMIC_INIT(1) ;/*定义原子变量*/
		static int xxx_open(struct inode *inode,struct file *filp)
		{
			...
			if(!atomic_dec_and_test(&xxx_available)){
				atomic_inc(&xxx_avaliable) ;
				return -EBUSY ;/*已经打开*/
			}
			...
			return 0 ;/*成功*/
		}
		static int xxx_release(struct inode *inode,struct file *flip)
		{
			atomic_inc(&xxx_avaliable) ;/*释放设备*/
			return 0 ;
		}
4、自旋锁
	自旋锁(spin lock)是一种典型的对临界资源进行互斥访问的手段。为了获取一个自旋锁,在某个CPU上运行
	的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以
	在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自
	旋锁并继续执行,如果测试结果表明锁仍然被占用,程序将在一个小的循环内重复这个“测试并设置”操作,
	即进行所谓的“自旋”。当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作
	向其调用者报告锁已经释放。
		
	自旋锁的相关操作:
	(1)定义自旋锁
		spinlock_t lock ;
	(2)初始化自旋锁
		spin_lock_init(lock) ;/*该宏用于动态初始化自旋锁lock*/
	(3)获得自旋锁
		/*该宏用于获取自旋锁lock,如果能够立即获得锁,马上返回,否则,自旋直到自旋锁的保持者释放*/
		spin_lock(lock) ;
		/*该宏尝试获得自旋锁lock,如果能够立即获得锁,它获得锁并返回真,否则立即返回假,不再自旋*/
		spin_trylock(lock) ;
	(4)释放自旋锁
		/*该宏释放自旋锁lock,它与spin_trylock或者spin_lock配对使用*/
		spin_unlock(lock)
	自旋锁一般这样使用:
		spinlock_t lock ;
		spin_lock_init(&lock) ;
		spin_lock(&lock) ;/*获取自旋锁,保护临界区*/
		... /*临界区*/
		spin_lock_unlock(&lock) ; /*解锁*/
	
	使用自旋锁需要注意一下问题:
		(1)自旋锁实际上是忙等待,当临界区很大,或有共享资源占用锁时间
			很长时,使用自旋锁会降低系统的性能
		(2)自旋锁有可能导致系统死锁
		(3)自旋锁锁定期间不能调用可能引起进程调度的函数
		
	实例:使用自旋锁实现设备只能被一个进程打开
	
		int xxx_count = 0 ;/*定义文件打开的次数计数*/
		static int xxx_open(struct inode *inode ,struct file *filp)
		{
			...
			spin_lock(&xxx_lock) ;
			if(xxx_open){ /*已经打开*/
				spin_unlock(&xxx_lock) ;
				return -EBUSY ;
			}
			xxx_count ++ ;/*增加使用计数*/
			spin_unlock(&xxx_lock) ;
			...
			return 0 ; /*成功*/
		}
		static int xxx_release(struct inode *inode,struct file *filp)
		{
			...
			spin_lock(&xxx_lock) ;
			xxx_count-- ;/*减少使用计数*/
			spin_unlock(&xxx_lock) ;
			return 0 ;
		}
		
	读写自旋锁
		
	读写自旋锁在写操作方面只能有一个写进程,在读操作方面,可以同时有多个读执行单元。
	当然,读和写不能同时进行。
	
	读写自旋锁的操作如下:
		(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_irqstore(rwlock_t *lock,unsigned long flags) ;
			void read_unlock_irq(rwlock_t *lock) ;
			void read_unlock_bh(rwlock_t *lock) ;
		(4)写锁定
			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) ;
			int write_trylock(rwlock_t *lock) ;
		(5)读解锁
			void read_unlock(rwlock_t *lock) ;
			void read_unlock_irqstore(rwlock_t *lock,unsigned long flags) ;
			void read_unlock_irq(rwlock_t *lock) ;
			void read_unlock_bh(rwlock_t *lock) ;
	读写自旋锁使用如下:
		rwlock_t lock ;/*定义rwlock*/
		rwlock_init(&lock) ;/*初始化rwlock*/
		/*读时获取锁*/
		read_lock(&lock) ;
		... /*临界资源*/
		read_unlock(&lock) ;
		/*写时获取锁*/
		write_lock_irqsave(&lock,flags) ;
		/*临界资源*/
		write_unlock_irqstore(&lock,flags) ;
		
	顺序锁
	
	顺序锁(seqlock),对读写锁的一种优化,使用顺序锁时,读不会被写执行单元阻塞,也就是说,当向一个临界
	资源中写入的同时,也可以从此临界资源中读取,即实现同时读写,但是同时写不被允许。如果读执行单元在读
	操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新开始,这样保证了数据的完整性,当然这
	种可能是微乎其微。顺序锁的性能是非常好的,同时他允许读写同时进行,大大的提高了并发性。
	
	但是他有一个限制:共享资源不含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指
	针,将导致Oops( 网上搜索这个词的意思是:吃惊的感叹词。我理解为访问该指针将会导致意想不到的结果)。
	
	Linux内核中,写执行单元设计的顺序锁操作如下:
	(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() = local_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() + lock_irq_restore()
		write_sequnlock_irq(lock) = write_sequnlock() + lock_irq_enable()
		write_sequnlock_bh(lock) = write_sequnlock() + local_bh_enable()
	写执行单元的模式如下:
		write_seqlock(&seqlock_a);
		/*写操作代码块*/
		write_sequnlock(&seqlock_a) ;
	
	读开始
		unsigned read_seqbegin(const seqlock_t *sl)
		read_seqbegin_irqsave(lock,flags)
	读单元在对顺序锁sl保护的共享资源进行访问之前需要调用该函数,该函数返回顺序锁的当前顺序号。其中:
		read_seqbegin_irqsave() = local_irq_save() + read_seqbegin()
	重读
		int read_seqretry(const seqlock_t *sl,unsigned iv) ;
		read_seqretry_irqrestore(lock ,iv,flags)
	读执行单元在访问被顺序锁保护的共享资源后需要调用该函数来检查,在读访问期间是否有写操作,
	如果有写操作,读执行单元将会重新进行读操作。其中:
		read_seqretry_irqrestore() = read_seqretry() + local_irq_restore()
	读执行单元使用顺序锁的模式如下:
		do{
			seqnum = read_seqbegin(&seqlock_a) ;
			/*读操作代码块*/
			...
		}while(read_seqretry(&seqlock_a,seqnum)) ;
	
		
	读-拷贝-更新
	
	RCU详解参见网址:http://www.ibm.com/developerworks/cn/linux/l-rcu/ ,此处不再细说
	
	RCU(Read-Copy-Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。
	对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个
	副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针
	重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。
	
	因此RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使用原子指令,而
	且在除alpha的所有架构上也不需要内存栅(Memory Barrier),因此不会导致锁竞争,内存延迟以
	及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。写者的同步开销比较大,它
	需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改
	操作。读者必须提供一个信号给写者以便写者能够确定数据可以被安全地释放或修改的时机。有一个专门
	的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据
	结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。 RCU与rwlock的不同之处是:它既允
	许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以
	有多个写者并行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取
	决于使用的写者间同步机制。但RCU不能替代rwlock,因为如果写比较多时,对读者的性能提高不能弥补
	写者导致的损失。
	
	读者在访问被RCU保护的共享数据期间不能被阻塞,这是RCU机制得以实现的一个基本前提,也就说当读者
	在引用被RCU保护的共享数据期间,读者所在的CPU不能发生上下文切换,spinlock和rwlock都需要这样
	的前提。写者在访问被RCU保护的共享数据时不需要和读者竞争任何锁,只有在有多于一个写者的情况下需
	要获得某种锁以与其他写者同步。写者修改数据前首先拷贝一个被修改元素的副本,然后在副本上进行修
	改,修改完毕后它向垃圾回收器注册一个回调函数以便在适当的时机执行真正的修改操作。等待适当时机
	的这一时期称为grace period,而CPU发生了上下文切换称为经历一个quiescent state,grace period
	就是所有CPU都经历一次quiescent state所需要的等待的时间。垃圾收集器就是在grace period之后
	调用写者注册的回调函数来完成真正的数据修改或数据释放操作的。以下以链表元素删除为例详细说明这
	一过程。
	
	写者要从链表中删除元素 B,它首先遍历该链表得到指向元素 B 的指针,然后修改元素 B 的前一个元素的 
	next 指针指向元素 B 的 next 指针指向的元素C,修改元素 B 的 next 指针指向的元素 C 的 prep指
	针指向元素 B 的 prep指针指向的元素 A,在这期间可能有读者访问该链表,修改指针指向的操作是原子的,
	所以不需要同步,而元素 B 的指针并没有去修改,因为读者可能正在使用 B 元素来得到下一个或前一个元
	素。写者完成这些操作后注册一个回调函数以便在 grace period 之后删除元素 B,然后就认为已经完成
	删除操作。垃圾收集器在检测到所有的CPU不在引用该链表后,即所有的 CPU 已经经历了 quiescent state,
	grace period 已经过去后,就调用刚才写者注册的回调函数删除了元素 B。
	
	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_lock_bh()则定义为:
		#define rcu_read_lock_bh() local_bh_disable()
		#define rcu_read_unlock_bh() local_bh_enable()
		
		(3)同步RCU
			
			/*该函数由RCU写执行单元调用,它将阻塞写执行单元,直到所有的读执行单元已经完成读执行
			 * 单元临界区,写执行单元才可以继续下一步操作*/
			synchronize_rcu()
			
			/*内核代码使用该函数来等待所有CPU处于可抢占状态,建议使用synchronize_sched()*/
			synchronize_kernel()
			
			挂接回调
			
			/*函数call_rcu也由RCU写执行单元调用,它不会使写执行单元阻塞,因而可以在中断上下文
			 或者软中断使用,该函数 将func挂接到RCU回调函数链上,然后立即返回*/
			void call_rcu(struct rcu_head *head,void(*func)(struct rcu_head *rcu));
			
			/*类似于call_rcu(),唯一的差别是它把软中断的完成也当做经历一个静默状态,因此如果写执行
			 单元使用了该函数,在进程上下文的读执行单元必须使用rcu_read_lock_bh()*/
			void call_rcu_bh(struct rcu_head *head,void(*func)(struct rcu_head *rcu)) ;
			
			RCU还增加了链表操作函数的RCU版本:
			
			/*链表元素new插入到RCU保护的链表head的开头*/
			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) ;
			
			/*从RCU保护的链表中删除指定的链表元素entry*/
			static inline void list_del_rcu(struct list_head *entry) ;
			
			/*使用新的链表元素new代替旧的链表元素old,内存栅保证在引用新的链表元素之前,它对链接指针的
			 修正对所有的读执行单元都是可见的*/
			static inline void list_replace_rcu(struct list_head *old,struct list_head *new) ;
			
			/*遍历由RCU保护的链表head,只要在读执行单元临界区使用该函数,它就可以安全地和其他的_rcu
			 链表操作函数并发执行*/
			list_for_each_rcu(pos,head)
			
			/*类似于list_for_each_rcu,不同的是他允许安全地删除当前链表元素pos*/
			list_for_each_safe_rcu(pos,n,head)
			
			/*类似于list_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构链表,
			 当前链表元素pos为一包含struct list_head结构的特定的数据结构*/
			list_for_each_entry_rcu(pos,head,member)
			
			/*它从RCU保护的哈希链表中移走链表元素n*/
			static inline void hlist_del_rcu(struct hlist_node *n)
			
			/*该函数用于把链表元素n插入被RCU保护的哈希链表的开头,但同时允许读执行单元对该
			 哈希链表的遍历。内存栅确保在引用新链表元素之前,它对指针的修改对所有读执行单元可见*/
			static inline void hlist_add_head_rcu(struct hlist_node *n,struct hlist_head *h)
			
			/*该宏用于遍历由RCU保护的哈希链表head,只要在读端临界区使用该函数,它就可以安全地和
			 其他的_rcu链表操作函数并发执行*/
			hlist_for_each_rcu(pos,head)
			
			/*类似于hlist_for_each_rcu(),不同之处在于它用于遍历指定类型的数据结构链表,
			 当前链表元素pos为一包含struct list_head结构的特定的数据结构*/
			hlist_for_each_entry_rcu(tpos,pos,head,member)
		
5、信号量
	信号量(semaphone)是一种用于保护临界区的一种常用方法,它的使用方法和自旋锁类似,与自旋锁相同,
	只有得到信号量的进程才能执行临界区的代码。但是,与自旋锁不同的是,获取不到信号量时,进程不会原地
	打转而是进入休眠等待状态。
	
	Linux中与信号量相关的操作主要有:
	(1)定义信号量
		struct semaphore sem ;
	(2)初始化信号量
		
		/*初始化信号量,定义信号量sem的值为val*/
		void sema_init(struct semaphore *sem,int val) ;
		
		/*该宏用于初始化一个用于互斥的信号量,它把信号量的值设置为1*/
		#define init_MUTEX(sem) sema_init(sem,1) 
		
		/*该宏用于初始化一个用于互斥的信号量,它把信号量的值设置为0*/
		#define init_MUTEX_LOCKED(sem) sem_init(sem,0)
		
		下面两个是定义并初始化信号量的“快捷方式”
		DECLARE_MUTEX(name) /*定义名为name的信号量并初始化为1*/ 
		DECLARE_MUTEX_LOCKED(name) /*定义名为name的信号量并初始化为0*/ 
		
	(3)获取信号量
		
		/*该函数用于获取信号量sem,它会导致睡眠,因此不能在中断上下文中使用*/
		void down(struct semaphore *sem) ;
		
		/*该函数与down类似,不同之处在于,因为down()而进入睡眠状态的进程不能被信号打断,
		 但因为down_interruptible()而进入睡眠状态的进程能被信号打断,信号也会导致该 函
		 数返回,这时返回值非0*/
		void down_interruptible(struct semaphore *sem) ;
		
		/*该函数尝试获得信号量sem,如果能够立即获得,它就获得该信号量并返回0,否则返回非0值,
		 它不会导致调用者睡眠,可以在中断上下文使用*/
		int down_trylock(struct semaphore *sem) ;
		
		在使用down_interruptible()获取信号量时,对返回值一般都会进行检查,如果非0,
		返回-ERESTARTSYS,如下:
			if(down_interruptible(&dem))
				return -ERESTARTSYS ;
		
	(4)释放信号量
		void up(struct semaphore *sem) ;/*该函数释放信号量sem,唤醒等待者*/
	信号量一般这么使用:
		/*定义信号量*/
		DECLARE_MUTEX(mount_sem) ;
		down(&mount_sem) ;/*获取信号量,保护临界区*/
		...
		critical setion /*临界区*/
		...
		up(&mount_sem) ; /*释放信号量*/
		
	例子:使用信号量实现设备只能被一个进程打开
	
		static DECLARE_MUTEX(xxx_lock) ;/*定义互斥锁*/
		
		static int xxx_open(struct inode *inode,struct file *filp)
		{
			...
			if(down_trylock(&xxx_lock)) /*获得打开锁*/
				return -EBUSY ; /*设备忙*/
			...
			return 0 ;/*成功*/
		}
		static int xxx_release(struct inode *inode,struct file *filp)
		{
			up(&xxx_lock) ;/*释放打开锁*/
			return 0 ;
		}
		
	信号量初始化为0时,可以用于同步。
	
	linux提供一种更好的同步机制,即完成量(completion).
	与completion操作有关的操作有以下4种:
		(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) ;
			
	自旋锁和信号量选用的3项原则:
		(1)当锁不能被获取到时,使用信号量的开销是进程上下文切换时间Tsw,使用自旋锁的开销是等
		待获取自旋锁(由临界区执行时间决定)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 re_sem ;/*定义读写信号量*/
	init_rwsem(&re_sem) ;/*初始化读写信号量*/
	/*读时获取信号量*/
	down_read(&rw_sem) ;
	... /*临界资源*/
	up_read(&rw_sem) ;
	
	/*写时获取信号量*/
	down_wirte(&rw_sem) ;
	... /*临界资源*/
	up_write(&rw_sem) ;
	
7、互斥体
	正宗的互斥体(mutex)在linux中是存在的。
	(1)定义和初始化互斥体
		struct mutex my_mutex ;
		mutex_init(&my_mutex) ;
	(2)获取互斥体
		void inline __sched mutex_lock(struct mutex *lock) ;
		int __sched mutex_lock_interruptible(struct mutex *lock) ;
		int __sched mutex_trylock(struct mutex *lock) ;
	(3)释放互斥体
		void __sched mutex_unlock(struct mutex *lock) ;
	
	mutex的使用和信号量用于互斥的场合完全一样:
		struct mutex my_mutex ;/*定义互斥体*/
		mutex_init(&my_mutex) ;/*初始化互斥体*/
		
		mutex_lock(&my_mutex) ;/*获取mutex*/
		... /*临界资源*/
		mutex_unlock(&my_mutex) ;/*释放mutex*/

8、小结
	并发和竞态广泛存在,中断屏蔽、原子操作、自旋锁和信号量都是解决并发问题的机制。中断屏蔽很少
	被单独使用,原子操作只能针对整数进行,因此自旋锁和信号量使用的最多。
	自旋锁会导致死循环,锁定期间不允许阻塞,因此要求锁定的临界区小。信号量允许临界区阻塞,用于
	临界区大的情况。
	读写自旋锁和读写信号量分别是放宽了条件的自旋锁和信号量,它们允许多个执行单元对共享资源的并
	发读。

Hasen的linux设备驱动开发学习之旅--linux设备驱动中的并发与竞态