Linux内核分析(七)----并发与竞态
这两天家里的事好多,我们今天继续接着上一次的内容学习,上次我们完善了字符设备控制方法,并深入分析了系统调用的实质,今天我们主要来了解一下并发和竞态。
今天我们会分析到以下内容:
1. 并发和竞态简介
2. 竞态解决办法
3. 为我们的虚拟设备增加并发控制
在前几次博文我们已经实现了简单的字符设备,看似完美但我们忽视了一个很严重的问题,即并发问题,那么什么是并发,又如何解决并发呢,我们下面进行分析。
1. 并发与竞态概念
1. 何为并发:并发是指多个执行单元同时、并行被执行。
2. 何为竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量,静态变量等)的访问容易发生竞态。
3. 我们虚拟设备的缺陷:对于我们前期的虚拟设备驱动个,假设一个执行单元A对其写入300个字符‘a’,而另一个执行单元B对其写入300个字符‘b’,第三个执行单元读取所有字符。如果A、B被顺序执行那么C读出的则不会出错,但如果A、B并发执行,那结果则是我们不可料想的。
2. 竞态发生的情况
1. 对称多处理器(SMP)的多个CPU:SMP是一种紧耦合、共享存储的系统模型,它的特点是多个CPU使用共同的系统总线,因此可以访问共同的外设和存储器。
2. 单CPU内进程与抢占它的进程:2.6的内核支持抢占调度,一个进程在内核执行的时候可能被另一高优先级进程打断。
3. 中断(硬中断、软中断、tasklet、低半部)与进程之间:中断可以打断正在执行的进程,处理中断的程序和被打断的进程间也可能发生竞态。
3. 竞态的解决办法
解决竞态问题的途径是保证对共享资源的互斥访问。访问共享资源的代码区域称为临界区,临界区要互斥机制保护。Linux设备驱动中常见的互斥机制有以下方式:中断屏蔽、原子操作、自旋锁和信号量等。
l 竞态解决办法
上面我们已经分析了竞态产生的原因、发生的情况以及解决办法,下面我们对常见的解决办法一一分析。
1. 中断屏蔽
1. 基本概念:在单CPU中避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。由于linux的异步I/O、进程调度等很多内容都依靠中断,所以我们应该尽快的执行完临界区的代码,换句话就是临界区代码应该尽量少。
2. 具体操作:linux内核提供了下面具体方法
Local_irq_disable();//屏蔽中断
Local_irq_enable();//打开中断
Local_irq_save(flags);//禁止中断并保存当前cpu的中断位信息
2. 原子操作
1. 基本概念:原子操作指在执行过程中不会被别的代码中断的操作。
2. 具体操作:linux内核提供了一系列的函数来实现内核中的原子操作,这些操作分为两类,一类是整型原子操作,另一类是位原子操作,其都依赖底层CPU的原子操作实现,所以这些函数与CPU架构有密切关系。
1) 整型原子操作
a) 设置原子变量的值
atomic_t v = ATOMIC_INIT(0);//定义原子变量v并初始化为0
void atomic_set(atomic_t *v, int i);//设置原子变量值为i
b) 获取原子变量的值
atomic_read(atomic_t *v);//返回原子变量v的值
c) 原子变量加、减操作
void atomic_add(int i, atomic_t *v);//原子变量v增加i
void atomic_sub(int I, atomic_t *v);//原子变量v减少i
d) 原子变量自增、自减
void atomic_inc(atomic_t *v);//原子变量v自增1
void atomic_dec(atomic_t *v);//原子变量v自减1
e) 操作并测试
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);
/*上述三个函数对原子变量v自增、自减和减操作(没有加)后测试其是否为0,如果为0返回true,否则返回false*/
f) 操作并返回
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);
/*上述函数对原子变量v进行自增、自减、加、减操作,并返回新的值*/
2) 位原子操作
a) 设置位
void set_bit(nr,void *addr);//设置addr地址的第nr位,即向该位写入1。
b) 清除位
void clear_bit(nr,void *addr);//清除addr地址的第nr位,即向该位写入0。
c) 改变位
void change_bit(nr,void *addr);//对addr地址的第nr取反
d) 测试位
int test_bit(nr,void *addr);//返回addr地址的第nr位
e) 测试并操作位
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_bit后,再执行xxx_bit函数*/
3. 自旋锁
1. 基本概念:自旋锁是一种对临界资源进行互斥访问的手段。
2. 工作原理:为获得自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量,由于其为原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量,如果测试结果表明已经空闲,则程序获得这个自旋锁并继续执行,如果测试结果表明该锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗的说就是在“原地打转”。
3. 具体操作:linux内核中与自旋锁相关的操作主要有:
1) 定义自旋锁
spinlock_t lock;
2) 初始自旋锁
spin_lock_init(lock);
3) 获得自旋锁
spin_lock(lock);//获得自旋锁lock
spin_trylock(lock);//尝试获取lock如果不能获得锁,返回假值,不在原地打转。
4) 释放自旋锁
spin_unlock(lock);//释放自旋锁
为保证我们执行临界区代码的时候不被中断等影响我们的自旋锁又衍生了下面的内容
5) 自旋锁衍生
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_disable()
4. 使用注意事项:
1) 自旋锁实质是忙等锁,因此在占用锁时间极短的情况下,使用锁才是合理的,反之则会影响系统性能。
2) 自旋锁可能导致系统死锁。
3) 自旋锁锁定期间不能调用可能引起进程调度的函数。
4. 读写自旋锁
1. 基本概念:为解决自旋锁中不能允许多个单元并发读的操作,衍生出了读写自旋锁,其不允许写操作并发,但允许读操作并发。
2. 具体操作:linux内核中与读写自旋锁相关的操作主要有:
1) 定义和初始化读写自旋锁
rwlock_t my_rwlock = RW_LOCK_UNLOCKED;//静态初始化
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);//动态初始化
2) 读锁定
read_lock();
read_lock_irqsave();
read_lock_irq();
read_lock_bh();
3) 读解锁
read_unlock();
read_unlock_irqrestore();
read_unlock_irq();
read_unlock_bh();
4) 写锁定
write_lock();
write_lock_irqsave();
write_lock_irq();
write_lock_bh();
write_trylock();
5) 写解锁
write_unlock();
write_unlock_irqrestore();
write_unlock_irq();
write_unlock_bh();
5. 顺序锁
1. 基本概念:顺序锁是对读写锁的一种优化,如果使用顺序锁,读执行单元在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读,不必等待写执行单元的完成,写执行单元也不需等待读执行单元完成在进行写操作。
2. 注意事项:顺序锁保护的共享资源不含有指针,因为在写执行单元可能使得指针失效,但读执行单元如果此时访问该指针,将导致oops。
3. 具体操作:linux内核中与顺序锁相关的操作主要有:
1) 写执行单元获得顺序锁
write_seqlock();
write_tryseqlock();
write_seqlock_irqsave();
write_seqlock_irq();
write_seqlock_bh();
2) 写执行单元释放顺序锁
write_sequnlock();
write_sequnlock_irqrestore();
write_sequnlock_irq();
write_sequnlock_bh();
3) 读执行单元开始
read_seqbegin();
read_seqbegin_irqsave();//local_irq_save + read_seqbegin
4) 读执行单元重读
read_seqretry ();
read_seqretry_irqrestore ();
6. RCU(读—拷贝—更新)
1. 基本概念:RCU可以看做是读写锁的高性能版本,相比读写锁,RCU的优点在于即允许多个读执行单元同时访问被保护数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。
2. 注意事项:RCU不能代替读写锁。
3. 具体操作:linux内核中与RCU相关的操作主要有:
1) 读锁定
rcu_read_lock ();
rcu_read_lock_bh ();
2) 读解锁
rcu_read_unlock ();
rcu_read_unlock_bh ();
3) 同步RCU
synchronize_rcu ();//由RCU写执行单元调用
synchronize_sched();//可以保证中断处理函数处理完毕,不能保证软中断处理结束
4) 挂接回调
call_rcu ();
call_rcu_bh ();
有关RCU的操作还有很多,大家可以参考网络。
7. 信号量
1. 基本概念:信号量用于保护临界区的常用方法与自旋锁类似,但不同的是当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
2. 具体操作:linux内核中与信号量相关的操作主要有:
1) 定义信号量
Struct semaphore sem;
2) 初始化信号量
void sema_init(struct semaphore *sem, int val);//初始化sem为val,当然还有系统定义的其他宏初始化,这里不列举
3) 获得信号量
void down(struct semaphore *sem);//获得信号量sem,其会导致睡眠,并不能被信号打断
int down_interruptible(struct semaphore *sem);//进入睡眠可以被信号打断
int down_trylock(struct semaphore *sem);//不会睡眠
4) 释放信号量
void up(struct semaphore *sem);//释放信号量,唤醒等待进程
注:当信号量被初始为0时,其可以用于同步。
8. Completion用于同步
1. 基本概念:linux中的同步机制。
2. 具体操作:linux内核中与Completion相关的操作主要有:
1) 定义Completion
struct completion *my_completion;
2) 初始化Completion
void init_completion(struct completion *x);
3) 等待Completion
void wait_for_completion(struct completion *);
4) 唤醒Completion
void complete(struct completion *);//唤醒一个
void complete_all(struct completion *);//唤醒该Completion的所有执行单元
9. 读写信号量
1. 基本概念:与自旋锁和读写自旋锁的关系类似
2. 具体操作:linux内核中与读写信号量相关的操作主要有:
1) 定义和初始化读写自旋锁
struct rw_semaphore sem;
init_rwsem(&sem);
2) 读信号量获取
down_read ();
down_read_trylock();
3) 读信号量释放
up_read ();
4) 写信号量获取
down_write ();
down_write_trylock ();
5) 写信号量释放
up_write();
10. 互斥体
1. 基本概念:用来实现互斥操作
2. 具体操作:linux内核中与互斥体相关的操作主要有:
1) 定义和初始化互斥体
struct mutex lock;
mutex_init(&lock);
2) 获取互斥体
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_lock_killable(struct mutex *lock);
3) 释放互斥体
void mutex_unlock(struct mutex *lock);
上面我们介绍了linux内核中为了解决竞态所提供的方法,我们下面使用信号量为我们的虚拟设备增加并发控制。
l 为我们的虚拟设备增加并发控制
我们增加了并发控制后的代码如下,详细代码参考https://github.com/wrjvszq/myblongs
1 struct mem_dev{ 2 struct cdev cdev; 3 int mem[MEM_SIZE];//全局内存4k 4 dev_t devno; 5 struct semaphore sem;//并发控制所使用的信号量 6 }; 7 static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos){ 8 unsigned long p = *ppos; 9 unsigned int count = size; 10 int ret = 0; 11 int *pbase = filp -> private_data; 12 13 if(p >= MEM_SIZE) 14 return 0; 15 if(count > MEM_SIZE - p) 16 count = MEM_SIZE - p; 17 18 if(down_interruptible(&my_dev.sem))//获取信号量 19 return - ERESTARTSYS; 20 21 if(copy_from_user(pbase + p,buf,count)){ 22 ret = - EFAULT; 23 }else{ 24 *ppos += count; 25 ret = count; 26 } 27 28 up(&my_dev.sem);//释放信号量 29 30 return ret; 31 } 32 static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos){ 33 int * pbase = filp -> private_data;/*获取数据地址*/ 34 unsigned long p = *ppos;/*读的偏移*/ 35 unsigned int count = size;/*读数据的大小*/ 36 int ret = 0; 37 38 if(p >= MEM_SIZE)/*合法性判断*/ 39 return 0; 40 if(count > MEM_SIZE - p)/*读取大小修正*/ 41 count = MEM_SIZE - p; 42 43 if(down_interruptible(&my_dev.sem))//获取信号量 44 return - ERESTARTSYS; 45 46 if(copy_to_user(buf,pbase + p,size)){ 47 ret = - EFAULT; 48 }else{ 49 *ppos += count; 50 ret = count; 51 } 52 53 up(&my_dev.sem);//释放信号量 54 55 return ret; 56 }
至此我们今天的工作完成,快过年了家里好多事,没有太多时间,还请大家见谅,提前祝大家新年快乐。
作者:wrjvsz 来源于:http://www.cnblogs.com/wrjvszq/,转载请注明出处。
作者:wrjvszq
出处:http://http://www.cnblogs.com/wrjvszq/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
----------------------------------------------------------
感谢您耐心读完,如果对您有帮助,请右下角推荐,谢谢您的支持