本文转载:http://blog.chinaunix.net/uid-20779306-id-1845697.html
所谓同步问题,也就是说采用锁机制或者相应的方法来避免竞态的发生。理论上已经有所了解,这篇笔记记录的是内核同步的方法。
一,原子操作
原子操作可以保证指令以原子的方式执行,原子操作是不能够被分割的指令。 原子操作分为原子整数操作和原子位操作。
1,原子整数操作:
原子整数操作操作的数据必须是atomic_t类型的。如果想把atomic_t转换成int类型,可以用函数atomic_read()来实现。
原子整数操作最常见的用途就是实现计数器。
原子整数操作列表( <asm/atomic.h> ):
2,原子位操作:
原子位操作函数是对普通的内存地址进行操作。所操作的数据的数据类型没有特殊的限制。
原子位操作的列表( <asm/bitops.h> ):
内核提供了两个函数来从指定的地址开始搜索第一个被设置(或未被设置)的位。
int find_first_bit(unsigned long *addr, unsigned int size);
int find_first_zero_bit(unsigned long *addr, unsigned int size);
二,自旋锁
发生争用时,自旋锁使得请求它的线程自旋(特别浪费处理器时间),直到获得锁为止。所以自旋锁不应该长时间被持有。这点和信号量不同,信号量当申请锁发生争用时,线程睡眠,内核转而执行其他的程序,直到锁被释放,唤醒等待的线程。
自旋锁的设计目标:在短期内进行轻量级加锁。
自旋锁相关的操作在<asm/spinlock.h>中定义。
基本使用形式如下:
|
注意:自旋锁是不可递归的。否则将出现死锁。
自旋锁可以使用在中断处理程序中(此情况下不可用信号量,因为可能导致睡眠)。此时,一定要在获得锁之前,首先禁止本地中断。否则可能被另一个中断处理程序抢占,然后申请同一个锁,这样会发生死锁。
内核提供了禁止中断的同时请求锁的接口,使用起来很方便:
|
spin_lock_irqsave():保存中断的当前状态,并禁止本地中断,然后再去申请指定的锁。
注意,我们应该对数据加锁,而不是代码加锁。
其他针对自旋锁的操作:
三,读-写自旋锁
当我们对某个数据结构的操作有读/写两种操作时,可以考虑用读/写锁这样的机制。linux提供了专门的读-写自旋锁。这种自旋锁为读和写分别提供了不同的锁。一个或多个读任务可以并发的持有读者锁。相反,用于写的锁最多只能被一个写任务持有。而且此时不能有并发的读操作。
读-写自旋锁的使用方法如下:
初始化:
rwlock_t mr_rwlock = RW_LOCK_UNLOCKED;
然后在读代码中:
|
在写代码中:
|
通常情况下,读锁和写锁应该分别放在不同的代码段中。注意,如果放一起,则要注意避免死锁。比如说不能把读锁升级为一个写锁。
|
读-写自旋锁的方法列表:
|
更常见的情况是:信号量作为一个大数据结构的一部分被动态创建。
动态:
|
此处的命名有些不规范。
2,使用信号量。
记得以前学os课程的时候有两个操作叫p()操作和v()操作。这里对应的是down()和up()操作。
通常情况下,使用down_interruptible()更为普遍,因为:
down_interruptible()获得指定的信号量,失败后进程以TASK_INTERRUPTIBLE状态进入睡眠,可以被信号唤醒。
down()获得指定的信号量,失败后会以TASK_UNINTERRUPTIBLE状态进入睡眠,此时不可以被信号唤醒。
要释放指定的信号量,可以用up()。
大体框架如下:
|
五,读写信号量
读者-写者机制使用是有条件的,只有可以自然地界定出读/写时才有价值。
所有读-写信号量都是互斥信号量。所有读-写锁的睡眠都不会被信号打断,所以它只是一个版本的down()操作。
静态创建:
static DECLARE_RWSEM(name);
动态创建:
init_rwsem(struct rw_semaphore *sem);
大体框架如下:
|
读-写信号量相比读-写自旋锁多了一种特有的操作:downgrade_writer(),这个函数可以动态的将获取的写锁转换成读锁。
六,完成变量
如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定的事件,利用完成变量可以做这个活。是使两个任务得以同步的简单方法。
完成变量由结构体completion表示,定义在<linux/completion.h>中。
创建及初始化:
静态:DECLARE_COMPLETION(mr_comp);
动态:init_completion();
在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。特定事件发生后,产生时间的任务调用complete()来发送信号唤醒正在等待的任务。
|
七,顺序和屏障
当处理多处理器之间或硬件设备之间的同步问题时,有时需要在程序代码中以指定的顺序发出读内存或者写内存的指令。举个最简单的例子:
|
因为编译器和处理器看不出a和b之间有什么关系,也就是说认为a和b之间是独立的。这时编译器会按这种顺序进行编译,但是处理器可能会为了某种优化而进行重新动态排序。这样在a赋值为1之前,b就可能赋值为2了。
通常这种重新排序的发生是因为现代处理器为了优化其传送管道,打乱了分派和提交指令的顺序。
注意:x86处理器不会这样,但是别的处理器有的会这么做。
用来确保顺序的指令称为屏障。
rmb()提供了一个读内存屏障。它确保跨越rmb()的载入动作不会发生重新排序。
wmb()提供了一个写内存屏蔽。它确保跨越wmb()的存储不会发生重新排序。
mb()提供了读屏蔽也提供了写屏蔽。
如:
|
这样就能确保a的赋值先于b的赋值了。