在之前的scull例子中,我们都没有考虑并发问题。但是,对并发的管理是操作系统编程中核心的问题之一。早期的linux内核不支持对称多处理(SMP),因此,导致并发执行的唯一原因是对硬件中断的服务。但是随着多核的出现,并发问题将越来越多的出现在我们的程序中,本章就来学习如何处理并发问题。
一.并发及其管理
竞态通常是作为对资源的共享访问结果而产生的。
在设计自己的驱动程序时,第一个要记住的规则是:只要可能,就应该避免资源的共享。 若没有并发访问,就不会有竞态。这种思想的最明显的应用是避免使用全局变量。
但是,资源的共享是不可避免的 ,如硬件资源本质上就是共享、指针传递等等。
资源共享的硬性规则:
(1)在单个执行线程之外共享硬件或软件资源的任何时候,因为另外一个线程可能产生对该资源的不一致观察,因此必须显示地管理对该资源的访问。--访问管理的常见技术成为“锁”或者“互斥”:确保一次只有一个执行线程可操作共享资源。
(2)当内核代码创建了一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在(并正确工作)。对象尚不能正确工作时,不能将其对内核可用。
二.几种并发管理方式
根据不同情况,共有以下几种方式来处理并发访问:信号量,读/写信号量,completion,自旋锁,读/写自旋锁,原子变量,位操作,seqlock,RCU。
下边就这几种方法分别学习他们的出现原因和适用范围.
1.信号量和互斥体
一个信号量(semaphore: 旗语,信号灯)本质上是一个整数值,它和一对函数联合使用,这一对函数通常称为P和V。希望进入临届区的进程将在相关信号量上调用P;如果信号量的值大于零,则该值会减小一,而进程可以继续。相反,如果信号量的值为零(或更小),进程必须等待知道其他人释放该信号。对信号量的解锁通过调用V完成;该函数增 加信号量的值,并在必要时唤醒等待的进程。
当信号量用于互斥时(即避免多个进程同是在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有事也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。Linux内核中几乎所有的信号量均用于互斥。
在linux中,要使用信号量,内核源代码必须包括
直接创建信号量:
void sema_init(struct semaphore *sem,int val);
声明和初始化互斥信号量:
DECLARE_MUTEX(name); //name=1;
DECLARE_MUTEX_LOCKED(name); //name=0;
运行时初始化互斥体:
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
在linux世界中,p和v操作分别被称作:down和up
//down操作较少信号量的值,并在必要时一直等待
void down(struct semaphore *sem);
//down的可中断版本,这是最常用的函数。
int down_interruptible(struct semaphore *sem);
//down的不可休眠版本,信号量不可获取就返回非0值
int down_trylock(struct semaphore *sem);
//Linux中的V操作
void up(struct semaphore *sem);
在scull中使用信号量,也就是在scull_dev中加入信号量,明确指定要保护的资源,并确保每一个对这些资源的访问使用正确的锁定。
//结构定义
struct scull_dev{
struct scull_qset data; /*指向第一个量子集的指针*/
int quantum; /*当前的量子大小*/
int qset; /*当前的数组大小*/
unsigned long size; /*保存在其中的数据总量*/
unsigned int access_key; /*由scullid和scullpriv使用 */
struct semaphore sem; /*互斥信号量 */
struct cdev cdev; /* 字符设备结构*/
};
//scull在装载时通过下面的循环初始化
for(i=0; i
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
init_MUTEX(scull_devices[i].sem);
scull_setup_cdev(&scull_devices[i],i);
}
//在scull_write中包含以下代码,检查进程是否拥有信号量
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
//最后释放信号量
out:
up(&&dev->sem);
return retval;
2.读/写信号量
信号量对所有的调用者执行互斥,而不管每个线程到底想做什么。但是许多任务可以划分成两种不同的工作类型:一些任务只需要读取受保护的数据结构,而其它的必须做出修改。允许多个并发的读取有利于大大提高性能,因为只读任务可以并行完成它们的工作,而不需要等待其它读取这退出临界区。
在linux中
//初始化rw_sem对象
void init_rwsem(struct rw_semaphore *sem);
//只读访问,可用接口如下:
void down_read(struct rw_semaphore *sem);
//获取访问时,返回非0(内核其它函数一般在成功时返回0)
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
//针对写入者的接口
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
//当写锁需要快速转换成读,之后长时间都是只读访问,可以在结束修改后调用该函数
void downgrade_write(struct rw_semaphore *sem);
3.completion
内核编程常见的一种模式是:在当前线程之外初始化某个活动,然后等待该活的结束。在这种情况下,我们可以使用信号量来同步这两个任务:
struct semaphore sem;
init_MUTEX_LOCKED(&sem);
start_external_task(&sem);
down(&sem);
//external task完成后
up(&sem);
上述实现(信号量)并不是适用这种情况的最好工具。如果存在针对该信号量的严重竞争,性能将受到影响。受到某些竟态的影响时,信号量可能在调用up的进程完成其相关任务前消失。
在2.4.7内核版本中出现了completion接口。completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。
//创建completion
DECLARE_COMPLETION(my_completion);
//动态初始化和创建completion
struct completion my_completion;
init_completion(&my_completion);
//等待completion
void wait_for_completion(struct completion *c);
//触发completion事件,唤醒等待的线程
void complete(struct completion *c);
void complete_all(struct completion *c);
//一个completion通常是一个单次设备;也就是说,它只会被使用一次然后丢弃,如果///重复使用,且使用complete_all唤醒,则必须在重复使用前快速初始化
INIT_COMPLETION(struct completion c);
使用 completion 的实例为: complete.c
DECLARE_COMPLETION(comp);
ssize_t complete_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
printk(KERN_ALERT "process %i (%s) going to sleep/n",
current->pid, current->comm);
wait_for_completion(&comp);
printk(KERN_ALERT "awoken %i (%s)/n", current->pid, current->comm);
return 0; /* EOF */
}
ssize_t complete_write (struct file *filp, const char __user *buf, size_t count,
loff_t *pos)
{
printk(KERN_ALERT "process %i (%s) awakening the readers.../n",
current->pid, current->comm);
complete(&comp);
return count; /* succeed, to avoid retrial */
}
实验:
root@xhy-desktop:/home/xhy/MyCode/work-ldd/ch5# insmod complete.ko
root@xhy-desktop:/home/xhy/MyCode/work-ldd/ch5# cat /proc/devices
Character devices:
1 mem
。。。
250 complete
root@xhy-desktop:/home/xhy/MyCode/work-ldd/ch5# mknod /dev/complete c 250 0
root@xhy-desktop:/home/xhy/MyCode/work-ldd/ch5# cat /dev/complete
此时进程睡眠
在另外一个终端向/dev/complete写入任意内容唤醒睡眠
root@xhy-desktop:/home/xhy# echo " ">/dev/complete
4.自旋锁
信号量是实现互斥的有力工具,但并不是唯一的,事实上,大多数的互斥操作是通过自旋锁实现的。和信号量不同,自旋锁可在不能休眠的代码中使用,比如中断处理例程。
//自旋锁初始化
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;//编译时
void spin_lock_init(spinlock_t *lock);//运行时
//获得锁
void spin_lock(spinlock_t *lock);
//获得自旋锁之前禁止中断(只在本地处理器上),先前的状态保存在flags上
void spin_lock_irqsave(spinlock_t *lock,unsigned long flags);
//如果确保没有其它代码禁止本地中断,可以用该函数
void spin_lock_irq(spinlock_t *lock);
//禁止软中断
void spin_lock_bh(spinlock_t *lock);
//释放锁
void spin_unlock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock,unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);
//非阻塞的自旋锁操作
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);
5.读/写自旋锁
关于读写自旋锁的引入与读写信号量的引入原因是类似的,这种锁允许任意数量的读者同时进入临界区,但是写者必须互斥访问。
//初始化
rwlock_t my_rwlock = RW_LOCK_UNLOCKED;
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);
//读者自旋锁
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);
void read_unlock(rwlock_t * lock);
void read_unlock(rwlock_t * lock,unsigned long flags);
void read_unlock_irq(rwlock_t * lock);
void read_unlock_bh(rwlock_t * lock);
//写者自旋锁
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock,unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
void write_unlock(rwlock_t *lock);
void write_unlock_irqsave(rwlock_t *lock,unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
6.原子变量
当共享资源是一个简单的整数时,可以考虑使用原子整数类型
#include
atomic_t v = ATOMIC_INIT(value);
void atomic_set(atomic_t *v, int i);
int atomic_read(atomic_t *v);
void atomic_add(int i, atomic_t *v);
void atomic_sub(int i, atomic_t *v);
void atomic_inc(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);
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);
7.位操作
当需要以原子形式来操作单个的位时,需要使用内核中提供的位操作函数
#include
void set_bit(nr, void *addr);
void clear_bit(nr, void *addr);
void change_bit(nr, void *addr);
//该函数是唯一一个不必以原子方式实现的位操作函数,它仅仅返回指定的当前值
test_bit(nr, void *addr);
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
8.seqlock
当要保护的资源很小,很简单,会频繁被访问而且写入访问很少发生且必须快速时,就可以使用seqlock。
//初始化
seqlock_t lock1 = SEQLOCK_UNLOCKED;
seqlock_t lock2;
seqlock_init(&lock2);
读取访问通过获得一个整数顺序值而进入临界区。在退出时,该顺序值会和当前值比较;如果不相等,必须重试读取访问。所以读取者代码如下编写:
unsigned int seq;
do{
seq = read_seqbegin(&the_lock);
/* 完成需要做的工作 */
}while(read_seqretry(&the_lock,seq));
如果在中断处理例程中使用seqlock,则应该使用IRQ安全的版本:
unsigned int read_seqbegin_irqsave(seqlock_t *lock,unsigned long flags);
int read_seqretry_irqrestore(seqlock_t *lock,unsigned int seq,
unsigned long flags);
写入者要进入由seqlock保护的临界区时获得一个互斥锁:
void write_seqlock(seqlock_t *lock);
void write_sequnlock(seqlock_t *lock);
因为自旋锁用来控制写入访问,因此自旋锁的常见变种都可以使用:
void write_seqlock_irqsave(seqlock_t *lock,unsigned long flags);
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock);
void write_sequnlock_irqsave(seqlock_t *lock,unsigned long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);
9.读取-复制-更新(RCU)
读取-复制-更新也是一种高级的互斥机制,在正确的条件下,也可获得高的性能。RCU针对经常发生读取而很少写入的情形做了优化。 被保护的资源应该通过指针访问,而对这些资源的引用必须由原子代码拥有。在需要修改该数据结构时,写入线程首先复制,然后修改副本,之后用新的版本替代相关指针,这也是算法的由来。当内核确信老的版本没有其它引用时,就可以释放老的版本。
在读取端,代码使用受RCU保护的数据结构时,必须将引用数据结构的代码包括在rcu_read_lock和rcu_read_unlock调用之间。这样RCU代码可能如下表示:
struct my_stuff *stuff;
rcu_read_lock();
stuff = find_the_stuff(args...);
do_something_with(stuff);
rcu_read_unlock();
// 修改受RCU保护的数据结构的代码必须通过分配一个struct rcu_head数据结构来获 取清除用的回调函数,但并不需要初始化这个结构。通常,这个结构内嵌在由RCU保护的大资源中。在修改完资源之后,应该做如下调用:
void call_rcu(struct rcu_head *head,void (*func)(void *arg),void *arg);