安徽城乡建设厅官方网站,杭州自助建站模板,软考哪个培训机构通过率高,商城设计方案Linux 设备驱动中必须要解决的一个问题是多个进程对共享的资源的并发访问#xff0c;并发的访问会导致竞态#xff0c;即使是经验丰富的驱动工程师也常常设计出包含并发问题bug 的驱动程序。 一、基础概念 1、Linux 并发相关基础概念 a -- 并发#xff08;concurrency#… Linux 设备驱动中必须要解决的一个问题是多个进程对共享的资源的并发访问并发的访问会导致竞态即使是经验丰富的驱动工程师也常常设计出包含并发问题bug 的驱动程序。 一、基础概念 1、Linux 并发相关基础概念 a -- 并发concurrency并发指的是多个执行单元同时、并发被执行而并发的执行单元对共享资源硬件资源和软件上的全局变量、静态变量等的访问则很容易导致竞态race condition b -- 竞态race condition 竞态简单的说就是两个或两个以上的进程同时访问一个资源同时引起资源的错误 c -- 临界区Critical Section每个进程中访问临界资源的那段代码称为临界区 d -- 临界资源 一次仅允许一个进程使用的资源称为临界资源多道程序系统中存在许多进程它们共享各种资源然而有很多资源一次只能供一个进程使用 在宏观上并行或者真正意义上的并行这里为什么是宏观意义的并行呢我们应该知道“时间片”这个概念微观上还是串行的所以这里称为宏观上的并行可能会导致竞争 类似两条十字交叉的道路上运行的车。当他们同一时刻要经过共同的资源交叉点的时候如果没有交通信号灯就可能出现混乱。在linux 系统中也有可能存在这种情况 2、并发产生的场合 a -- 对称多处理器SMP的多个CPU SMP 是一种共享存储的系统模型它的特点是多个CPU使用共同的系统总线因此可访问共同的外设和储存器这里可以实现真正的并行 b -- 单CPU内进程与抢占它的进程 一个进程在内核执行的时候有可能被另一个高优先级进程打断 c -- 中断和进程之间 中断可以打断正在执行的进程如果中断处理函数程序访问进程正在访问的资源则竞态也会发生 3、解决竞态问题的途径 解决竞态问题的途径最重要的是保证对共享资源的互斥访问所谓互斥访问是指一个执行单元在访问共享资源的时候其他的执行单元被禁止访问。 Linux 设备中提供了可采用的互斥途径来避免这种竞争。主要有原子操作信号量自旋锁。 那么这三种有什么相同的地方有什么区别呢适用什么不同的场合呢会带来什么边际效应要彻底弄清楚这些问题要从其所处的环境来进行细化分类处理。是UP(单CPU)还是SMP(多CPU)是抢占式内核还是非抢占式内核是在中断上下文不是进程上下文。似交通信号灯一样的措施来避免这种竞争。 先看一下三种并发机制的简单概念 原子锁原子操作不可能被其他的任务给调开一切包括中断,针对单个变量。 自旋锁使用忙等待锁来确保互斥锁的一种特别方法,针对是临界区。 信号量包括一个变量及对它进行的两个原语操作此变量就称之为信号量针对是临界区。 二、并发处理途径详解 1、中断屏蔽 在单CPU范围内避免静态的一种简单而省事的方法是在进入临界区之前屏蔽系统的中断这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占防止某些竞争条件的发生。具体而言 a -- 中断屏蔽将使得中断和进程之间的并发不再发生 b -- 由于Linux内核的进程调度等操作都依赖中断来实现内核抢占进程之间的并发也得以避免 中断屏蔽的使用方法 [cpp] view plaincopy local_irq_disable() local_irq_enable() 只能禁止和使能本地CPU的中断所以不能解决多CPU引发的竞态 local_irq_save(flags) local_irq_restore(flags) 除了能禁止和使能中断外还保存和还原目前的CPU中断位信息 local_bh_disable() local_bh_disable() 如果只是想禁止中断的底半部这是个不错的选择。 但是要注意 a -- 中断对系统正常运行很重要长时间屏蔽很危险有可能造成数据丢失乃至系统崩溃所以中断屏蔽后应尽可能快的执行完毕。 b -- 宜与自旋锁联合使用。 所以不建议使用中断屏蔽。 2、原子操作 原子操作分为原子整型操作和原子位操作就是绝不会在执行完毕前被任何其他任务和时间打断不会执行一半又去执行其他代码。原子操作需要硬件的支持因此是架构相关的其API和原子类型的定义都在include/asm/atomic.h中使用汇编语言实现。 在linux中原子变量的定义如下 typedef struct {volatile int counter;} atomic_t; 关键字volatile用来暗示GCC不要对该类型做数据优化所以对这个变量counte的访问都是基于内存的不要将其缓冲到寄存器中。存储到寄存器中可能导致内存中的数据已经改变而寄存其中的数据没有改变。 原子整型操作 1定义atomic_t变量 #define ATOMIC_INIT(i) ( (atomic_t) { (i) } ) atomic_t v ATOMIC_INIT(0); //定义原子变量v并初始化为0 2设置原子变量的值 #define atomic_set(v,i) ((v)-counter (i))
void atomic_set(atomic_t *v, int i);//设置原子变量的值为i 3获取原子变量的值 #define atomic_read(v) ((v)-counter 0)
atomic_read(atomic_t *v);//返回原子变量的值 4原子变量加/减 static __inline__ void atomic_add(int i, atomic_t * v) //原子变量增加i static __inline__ void atomic_sub(int i, atomic_t * v) //原子变量减少i 5原子变量自增/自减 #define atomic_inc(v) atomic_add(1, v) //原子变量加1
#define atomic_dec(v) atomic_sub(1, v); //原子变量减1 6操作并测试 //这些操作对原子变量执行自增,自减,减操作后测试是否为0,是返回true,否则返回false #define atomic_inc_and_test(v) (atomic_add_return(1, (v)) 0)
static inline int atomic_add_return(int i, atomic_t *v) 原子操作的优点编写简单缺点是功能太简单只能做计数操作保护的东西太少。下面看一个实例 [cpp] view plaincopy static atomic_t vATOMIC_INIT(1); static int hello_open (struct inode *inode, struct file *filep) { if(!atomic_dec_and_test(v)) { atomic_inc(v); return -EBUSY; } return 0; } static int hello_release (struct inode *inode, struct file *filep) { atomic_inc(v); return 0; } 3、自旋锁 自旋锁是专为防止多处理器并发而引入的一种锁它应用于中断处理等部分。对于单处理器来说防止中断处理中的并发可简单采用关闭中断的方式不需要自旋锁。 自旋锁最多只能被一个内核任务持有如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁那么这个任务就会一直进行忙循环——旋转——等待锁重新可用忙等待即当一个进程位于其临界区内任何试图进入其临界区的进程都必须在进入代码连续循环。要是锁未被争用请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。 1自旋锁的使用 spinlock_t spin; //定义自旋锁spin_lock_init(lock); //初始化自旋锁spin_lock(lock); //成功获得自旋锁立即返回否则自旋在那里直到该自旋锁的保持者释放 spin_trylock(lock); //成功获得自旋锁立即返回真否则返回假而不是像上一个那样在原地打转spin_unlock(lock);//释放自旋锁 下面是一个实例 [cpp] view plaincopy static spinlock_t lock; static int flag 1; static int hello_open (struct inode *inode, struct file *filep) { spin_lock(lock); if(flag !1) { spin_unlock(lock); return -EBUSY; } flag 0; spin_unlock(lock); return 0; } static int hello_release (struct inode *inode, struct file *filep) { flag 1; return 0; } 自旋锁主要针对SMP或单CPU但内核可抢占的情况对于单CPU和内核不支持的抢占的系统自旋锁退化为空操作因为自旋锁本身就需进行内核抢占。在单CPU和内核可抢占的系统中自旋锁持有期间内核的抢占将被禁止。由于内核可抢占的单CPU系统的行为实际很类似于SMP系统因此在这样的单CPU系统中使用自旋锁仍十分重要。 尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰但是得到锁的代码路径在执行临界区的时候还可能受到中断和底半部的影响。为了防止这种影响。为了防止影响就需要用到自旋锁的衍生。 2注意事项 a -- 自旋锁是一种忙等待。它是一种适合短时间锁定的轻量级的加锁机制。 b -- 自旋锁不能递归使用。自旋锁被设计成在不同线程或者函数之间同步。这是因为如果一个线程在已经持有自旋锁时其处于忙等待状态则已经没有机会释放自己持有的锁了。如果这时再调用自身则自旋锁永远没有执行的机会了即造成“死锁”。 【自旋锁导致死锁的实例】 1a进程拥有自旋锁在内核态阻塞的内核调度进程bb也要或得自旋锁b只能自旋而此时抢占已经关闭了a进程就不会调度到了b进程永远自旋。 2进程a拥有自旋锁中断来了cpu执行中断中断处理函数也要获得锁访问共享资源此时也获得不到锁只能死锁。 3内核抢占 内核抢占是上面提到的一个概念不管当前进程处于内核态还是用户态都会调度优先级高的进程运行停止当前进程当我们使用自旋锁的时候抢占是关闭的。 4自旋锁有几个重要的特性 a -- 被自旋锁保护的临界区代码执行时不能进入休眠。 b -- 被自旋锁保护的临界区代码执行时是不能被被其他中断中断。 c -- 被自旋锁保护的临界区代码执行时内核不能被抢占。 从这几个特性可以归纳出一个共性被自旋锁保护的临界区代码执行时它不能因为任何原因放弃处理器。 4、信号量 linux中提供了两种信号量一种用于内核程序中一种用于应用程序中。这里只讲属前者 信号量和自旋锁的使用方法基本一样。与自旋锁相比信号量只有当得到信号量的进程或者线程时才能够进入临界区执行临界代码。信号量和自旋锁的最大区别在于当一个进程试图去获得一个已经锁定的信号量时进程不会像自旋锁一样在远处忙等待。 信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时信号量会将其推入等待队列然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后在等待队列中的一个任务将被唤醒从而便可以获得这个信号量。 1信号量的实现 在linux中信号量的定义如下 struct semaphore {spinlock_t lock; //用来对count变量起保护作用。 unsigned int count; // 大于0资源空闲等于0资源忙但没有进程等待这个保护的资源小于0资源不可用并至少有一个进程等待资源。 struct list_head wait_list; //存放等待队列链表的地址当前等待资源的所有睡眠进程都会放在这个链表中。}; 2信号量的使用 static inline void sema_init(struct semaphore *sem, int val); //设置sem为val
#define init_MUTEX(sem) sema_init(sem, 1) //初始化一个用户互斥的信号量sem设置为1
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0) //初始化一个用户互斥的信号量sem设置为0 定义和初始化可以一步完成DECLARE_MUTEX(name); //该宏定义信号量name并初始化1DECLARE_MUTEX_LOCKED(name); //该宏定义信号量name并初始化0 当信号量用于互斥时即避免多个进程同是在一个临界区运行信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下一个信号量有时也称为一个“互斥体mutex”,它是互斥mutual exclusion的简称。Linux内核中几乎所有的信号量均用于互斥。 使用信号量内核代码必须包含asm/semaphore.h 。 3获取锁定信号量 void down(struct semaphore *sem) int down_interruptible(struct semaphore *sem) int down_killable(struct semaphore *sem) 4释放信号量 void up(struct semaphore *sem) 下面看一个实例 [cpp] view plaincopy //定义和初始化 static struct semaphore sem; sema_init(sem,1); static int hello_open (struct inode *inode, struct file *filep) { // p操作获得信号量保护临界区 if(down_interruptible(sem)) { //没有获得信号量 return -ERESTART; } return 0; } static int hello_release (struct inode *inode, struct file *filep) { //v操作释放信号量 up(sem); return 0; } 三、自旋锁与信号量的比较 信号量自旋锁1、开销成本进程上下文切换时间忙等待获得自旋锁时间2、特性a -- 导致阻塞产生睡眠 b -- 进程级的内核是代表进程来争夺资源的a -- 忙等待内核抢占关闭 b -- 主要是用于CPU同步的3、应用场合只能运行于进程上下文还可以出现中断上下文4、其他还可以出现在用户进程中只能在内核线程中使用 从以上的区别以及本身的定义可以推导出两都分别适应的场合。只考虑内核态 后记除了上述几种广泛使用的的并发控制机制外还有中断屏蔽、顺序锁(seqlock)、RCU(Read-Copy-Update)等等做个简单总结如下图