驱动编程核心理论之并发控制
并发,竟态的理解:也就是Linux的东西在时间和空间被同时使用导致的问题
Linux系统提供的解决方案:
中继屏蔽
原子操作
自旋锁
信号量
1,中断屏蔽
首先这个依赖于CPU,CPU一般提供屏蔽中断的打开中断的功能,中断屏蔽使得中断和进程之间的并发
不再发生,同时进程调试也依赖于中断,内核抢占的进程间并发也就可以避免
使用方法
local_irq_disable()//屏蔽中断
...
critical section()//临界区
...
local_irq_enable()//打开中断
这种方法只能解决单个CPU的问题
注意 : 长时间中断屏蔽很危险,比较适合和自旋锁联合使用
local_irq_save(flags) 禁止中断同时保存目前CPU的中断信息位
local_irq_restore(flags) 打开中断同时恢复中断信息位
底半部操作
local_bh_disable()
local_bh_enable()
2,原子操作
原子操作是指在执行过程中不会被的代码路径所中断的操作。待理解
分为两类,针对位的操作和整型变量的操作
针对整型变量的原子操作的相关函数
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
操作并测试函数
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(atomic_t *v);
//在进行自增自减和减操作后,测试是否为0,如果是则返回true,否则返回为false
操作关返回函数,操作后返回新的值
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(int i, atomic_t *v);
int atomic_dec_return(int i, atomic_t *v);
针对位的原子操作
void set_bit(nr, void *addr);
//设置位操作,设置addr地址的第nr位,设置后该位立即写为1
void clear_bit(nr,void *addr);
//清除为操作,同上清除该位,值变为0
void change_bit(nr,void *addr);
//改变位操作,nr位反置
test_bit(nr, void *addr);
//测试位操作,返回addr地址的第nr位
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
以上三个操作是先进行测试后再进行相应的操作
例程:使用原子变量使设备只能被一个进程打开
xxx代表设备名
static atomic_t xxx_available = ATOMIC_INIT(1);
static int xxx_open(struct inode *inode, struct file *flip)//open操作函数
{
...
if(!atomic_dec_and_test(&xxx_available))
{
atomic_inc(&xxx_available);
return - EBUSY;//设备已被打开
}
...
return 0;//原子操作成功,解决竞争问题
static int xxx_release(struct inode *inode, struct file *flip)
{
atomic_inc(&xxx_available);
return 0;//减少1,释放设备
}
}
3,自旋锁
这是一种对临界资源进行互斥访问的典型手段
是要和原子操作进行配合使用的
理解方法,将其看成一个变量,该变量把一个临界区标记成“我正在运行,请稍等一会”或者标记
为“我当前不运行,可以被使用”
相关函数
定义自旋锁
spinlock_t lock;
初始化自旋锁
spin_lock_init(&lock);//动态的初始化
获得自旋锁
spin_lock(lock);//如果能够立即获得锁就马上返回,否则就自旋保持在那里,直到锁被释放
spin_trylock(&lock);//尝试能否获得,如果能返回真,否则返回假,并不会在那时保持
释放自旋锁
spin_lock(&lock)//与spin_lock或spin_trylock配合使用
一般使用流程
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
...//临界区
spin_unlock(&lock);
注意:
1,自旋锁主要针对SMP或单CPU但内核可抢占的情况,不是这两种情况的退化成空操作
2,临界区的操作还可以受到底半部的和中断的影响,还需要用到自旋锁的衍生,以下会给出几个函数
3,自旋锁实际是忙等待,只有在锁占有极短的时间才能用,否则会降低系统的性能
4,自旋锁可能会导致系统死锁,特别是在递规使用自旋锁,还有就是如果获得锁之后再阻塞也会死锁
典型结构
int xxx_count = 0;
static int xxx_open(struct inode *inode, struct file *flip)
{
...//定义锁这里应该
spinlock(&xxx_lock);//??这个函数?
if (xxx_count)
{
spin_unlock(&xxx_lock);
return - EBUSY;
}
xxx_count++;
spin_unlock(&xxx_lock);
...
return 0;
}
static int xxx_release(struct inode *inode, struct file *flip)
{
....
spinlock(&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 flag);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
3,读写锁
void read_unlock(rwlock_t *lock);
void read_unlock_irqsave(rwlock_t *lock, unsinged long flag);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
在对共享资源进行读取时,先调用读锁定,读操作完成后再进行解锁
4,写锁定
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flag);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
void write_lock_trylock(rwlock_t *lock);
5,写解锁
void write_unlock(rwlock_t *lock);
void write_unlock_irqsave(rwlock_t *lock, unsigned long flag);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
同读锁一样,也是先上锁再解锁
应用框架
rwlock_t lock;
rwlock_init(&lock);
read_lock(&lock);
....//临界资源
read_unlock(&lock);
write_lock_irqsave(&lock,flags);
...//临界资源
write_unlock_irqretore(&lock,flags);
衍生锁--顺序锁
顺序锁是对读写锁的一种优化,读写锁可能会引起阻塞,有顺序锁了就不会
说明:使用顺序锁,写时,被保护的资源依然可以被读,而不必等写完,同样,读时,被保护的资源
仍可被写
但是写与写之间仍然是互斥的
可能情况,读时,由于写入的数据使资源发生了变化,读要重新执行,但概率较小
另外,使用顺序锁时被保护的共享资源不能含有指针
1,获得顺序锁
void write_seqlock(seqlock_t *sl);
int write_tryseqlock(seqlock_t *sl);
write_seqlock_irqsave(lock, flags);
write_seqlock_bh(lock);
write_seqlock_irq(lock);
2,释放顺序锁
void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock, flags);
write_sequnlock_irq(lock);
write_sequnlock_bh(lock);
3,读开始
unsinged read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags);
读执行单元在对被顺序锁保护的共享资源进行访问时要调用该函数,返回sl的顺序号
4,重读
int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock,iv,flags);
读执行单元在访问完被顺序锁sl保护的共享资源后要调用这个函数检查是否同时有写操作,如果有
要重读。
框架
do {
seqnum = read_seqbegin(&seqlock_a);
...
} while (read_seqretry(&seqlock_a, seqnum));
衍生锁--RCU(读-拷贝-更新)//有待于进一步的学习,包括链表
RCU也是对数据保护的一种方式,其保护的数据,读写不需要的任何锁就可以访问它,不会产生竟态
死锁
过程:写之前先对保护的数据复制一个副本,修改的只是副本,然后通过回调重新指向被修改的数据
优点:是高性能的读写锁,充许多个读写执行单元访问被保护的数据
不足:多个写执行单元的同步开销很大,可能延时数据结构的释放
函数:
读锁定:
rcu_read_lock()
rcu_read_lock_bh()
读解锁:
rcu_read_unlock()
rcu_reac_unlock_bh()
使用模型:
rcu_read_lock()
...//读写临界区
rcu_read_unlock()
其实质是使能和禁止内核抢占
同步RCU
synchronize_rcu()
该函数由写执行单元调用,同时阻塞写执行单元
挂接回调
void fastcall call_rcu(struct rcu_head *head,void (*func)(struct rcu_head *rcu);
由RCU写执行单元调用,但不是使写阻塞。可以在中断上下或软中断中使用。
4,信号量
同锁一样,也是保护临界区的一种方法,只有得到信号量的进程才能访问临界区的代码
但与自旋锁不同的是当得不到信号量时,会进入休眠,而不会原地打转
函数:
定义信号量
struct semaphore sem;
初始化信号量
void sema_init(struct semaphore *sem,int val);//初始化为val
初始化互斥信号量
void init_MUTEX(struct semaphore *sem);//初始化为1
相当于sema_init(struct semaphore *sem, 1);
void init_MUTEX_LOCKED (struct semaphore *sem);//初始化为0
相当于sema_init(struct semaphore *sema, 0);
快速定义:
DECLEAR_MUTEX(name);//初始化为1
DECLEAR_NUTEX_LOCKED(name);//初始化为0
获取信号量
void down(struct semaphore *sem);//会导致睡眠,不适合在中断上下文中使用
int down_interruptible(struct semphore *sem);//使用该函数后进程进入睡眠状态可以被信号打断
信号也会导致该函数返回,返回值为非0
int down_trylock(struct semphore *sem);//该函数会尝试信号量sem,如果能立刻获得,获得并
返回0,否则返回非0,不会导致调用者睡眠,可以在中断上下文中使用
if (down_interuptible(&sem)
{
return - ERESTARTSYS;
}
释放信号量
void up(struct semaphore * sem);//释放信号量,唤醒等待者
信号量的一使用框架
DECLEAR_MUTEX(mount_sem)
down(&mount_sem)
...
critical section//临界区
...
up(&mount_sem);
例程:信号量实现一个设备只能被一个进程打开
static DECLEAR_MUTEX(xxx_lock);
static int xxx_open(struct inode *inode, struct file *flip)
{
...
if(down_trylock(&xxx_lock)
return - EBUSY;
...
return 0;
}
static int xxx_release(struct inode *inode, struct file *flip)
{
up(&xxx_lock);
return 0;
}
信号量用于同步
但是更好的方法是用完成量,completion
与completion有关的函数
定义完成量
struct completion my_completion;
初始化completion
init_completion(&my_completion);
同时完成定义和初始化
DECLEAR_COMPLETION(my_completion);
等待完成量
void wait_for_completion(struct completion *c);
唤醒完成量
void completion(stuct completion *c)//只唤醒一个等待的执行单元
void completion_all(struct completion *c);//释放所有等待同一完成量的执行单元
读写信号量
类似于自旋锁与读写自旋锁关系一样,读写信号量可能引起进程阻塞地,它可充许多个读执行单元
同时访问共享资源,而最多只能有一个写执行单元。读写信号量是比信号量的粒度更大
函数:
定义和初始化读写信号量
struct rw_semaphore my_rw;
void init_rwsem(struct rw_semaphore *sem);
读信号量获取:
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semphore *sem);
读信号量释放
void up_read(struct rw_semaphore *sem);
写信号量获取
void down_write(stuct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
写信号量释放
void up_write(struct rw_semaphore *sem);
使用框架:
rw_semaphore rw_sem;
init_rwsem(&rw_sem);
down_read(&rw_sem);
...//临界区
up_read(&rw_sem);
down_write(&rw_sem);
...//临界区
up_write(&rw_sem);
互斥体
定义初始化
struct mutex my_mutex;
mutex_init(&my_mutex);
获取互斥体
void fastcall mutex_lock(struct mutex *lock);
int fastcall mutex_lock_interuptible(stuct mutex *lock);
int fastcall mutex_trylock(stuct mutex *lock);
和信号量相比,互斥会引起睡眠,并不被信号打断
释放互斥体
void fastcall mutex_unlock(struct mutex *lock);
互斥体使用和信号量用于互斥场合完全一样
小结:中继屏蔽很小单独使用,常和自旋锁一起配合
原子操作只能针对整型和位操作
自旋锁和信号量应用最为广泛
自旋锁会导致死循环,锁定期间不能有阻塞,要求临界区小
信号量则可以适用于临界区较大的情况
阅读(1002) | 评论(0) | 转发(0) |