第十章 内核同步方法
一、原子操作
1、概述
针对整数的原子操作只能对atomic_t类型的数据进行处理。引入了一个特殊数据类型,而没有直接使用C语言的int类型,主要是出于两个原因:首先,让原子函数只接收atomic t类型的操作数,可以确保原子操作只与这种特殊类型数据一起使用。同时,这也保证了该类型的数据不会被传递给任何非原子函数。
typedef struct{
volatile int counter;
}atomic_t;
2、原子整型的使用
//定义一个atomic_t类型的数据方法很平常,
//你还可以在定义时给它设定初值
atomic_t v; /* 定义v */
atomic_t u = ATOMIC_INIT(0);/*定义u并把它初始化为0*/
//操作也都非常简单:
atomic_set ( &v,4); /*v = 4 (原子地)*/
atomic_add (2 , &ev); /*v = v+2 - 6(原子地)*/
atomic_inc (&v); /* v - v + 1 =7{原子地)*/
//如果需要将atomic_t转换成int型,可以使用atomic_read()来完成:
printk ( "%d\n" ,atomic_read(&v)) ; /*会打印"7"★ /
3、原子位操作
unsigned long word = 0;
set_bit (0 , &word) ;/*第0位被设置(原子地)*/
set_bit(1 , &word) ;/*第1位被设置(原子地)*/
printk ( "%u1\n" , word) ;/*打印3*/
clear_bit(1 ,&word) ;/*清空第1位*/
change_bit (0 , &word) ;/*翻转第О位的值,这里它被清空*/
/*原子地设置第0位并且返回设置前的值(0)*/
if(test_and_set_bit (0, &word){
/*永远不为真*/
}
/*下面的语句是合法的;你可以把原子位指令与一般的c语句混在一起*/
word n 7 ;
二、自旋锁
1、概述
在Linux中最常见的就是自旋锁,自旋锁可以最多被一个可执行线程持有,如果发生争用,那么尝试获取锁的线程会一直进行忙循环(在原地旋转,等待锁重新可用,特别浪费处理器时间)。所以自旋锁不应该被长时间持有。事实上,这点正是使用自旋锁的初衷:在短期间内进行轻量级加锁。持有自旋锁的时间最好少于完成两次上下文切换的耗时,即持有自旋锁的时间尽可能短。
2、自旋锁使用
DEFINE_SPINLOCK (mr_1ock);
spin_lock ( &mr_lock);
/*临界区...*/
spin_unlock ( &mr_lock);
注意事项:
- 自旋锁不可递归
- 中断处理程序中获取自旋锁前,要禁止本地中断
3、自旋锁API
三、读写自旋锁
1、概述
有时,锁的用途可以明确的分为读取和写入,只要其他程序没有写操作,多个并发的读操作是安全的,在前面探讨过得任务链表(存进程的)就是这种情况,通过读写锁去获取保护。
2、读写自旋锁的使用
/*读/写自旋锁的使用方法类似于普通自旋锁,它们通过下面的方法初始化:*/
DEFINE_RWLOCK(mr_rwlock);
//然后,在读者的代码分支中使用如下函数:
read_lock ( &mr_rwlock) ;
/*临界区(只读)…*/
read_unlock ( &mr_rwlock) ;
//最后,在写者的代码分支中使用如下函数:
write_lock ( &mr_rwlock) ;
/*临界区〈读写)…*/
write_unlock ( &mr_rwlock) ;
//通常情况下,读锁和写锁会位于完全分割开的代码分支中,如上例所示。
3、读写锁API
四、信号量
1、概述
Linux中的信号量是一种睡眠锁,如果有一个任务试图获取一个不可用(已经被占用)的信号量时,信号量会将其推进到一个等待队列,然后让其睡眠,这时处理器重获自由去执行其他代码,当持有信号量可用(被释放)后,处于等待队列的任务将会被唤醒从而获得信号量 。
2、特点
- 适合锁会被长时间的占用,如果很短,睡眠、维护等待队列以及唤醒都要比占用时间长了
- 只能在进程上下文上才应该去获取信号量锁,而不是中断上下文
- 占用信号量的同时不能占用自旋锁。因为在等待信号量时可能会睡眠而在持有自旋锁时不允许睡眠
- 更好的利用了处理器的使用率
3、 多个信号量
信号量可以同时允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它。信号量同时允许的持有者数量可以在声明信号量时指定。这个值称为使用者数量(usage count)或简单地叫数量(count)。分为二值信号量和计数信号量。
4、信号量的使用
//semaphore类型用来表示信号量。可以通过以下方式静态地声明信号量
struct semaphore name ; //其中name是信号量
sema_init ( &name,count) ; //count是信号量的使用数量:
//创建更为普通的互斥信号量可以使用以下快捷方式,
static DECLARE_MUTEX(name) ;
//更常见的情况是,信号量作为一个大数据结构的一部分动态创建。
//此时,只有指向该动态创建的信号量的间接指针,
sema_init(sem, count) ;
//与前面类似,初始化一个动态创建的互斥信号量时使用如下函数:
init_MUTEX(sem) ;
//获取信号量,信号量减一
down(&name)
//释放信号量,信号量加一
up(&name)
5、信号量API
五、互斥体
1、概述
虽然信号量带来的睡眠的锁在某些复杂场景下会比自旋锁更加适合,更加轻量,但是简单的锁定而使用信号量并不方便,并且信号量也缺乏强制的规则来行使任何形式的自动调试,即便受限的调试也不可能。为了找到一个更简单睡眠锁,内核开发者们引入了互斥体(mutex),信号量和互斥体优先选择互斥体。
2、互斥体的使用
//静态地定义mutex,你需要做:
DEFINE_MUTEX (name);
//动态初始化mutex,你需要做:
mutex_init ( &mutex);
//对互斥锁锁定和解锁并不难:
mutex_lock ( &mutex) ;
/*临界区*/
mutex_unlock ( &mutex);
3、特性
- 任何时刻中只有一个任务可以持有mutex,即mutex的使用计数永远是1
- 给mutex上锁者必须负责给其再解锁——在同一上下文中上锁和解锁
- 递归地上锁和解锁是不允许的
- 当持有mutex时,进程不可以退出
- mutex不能在中断或者下半部中使用
- mutex 只能通过官方API管理:它只能使用上节中描述的方法初始化,不可被拷贝、手动初始化或者重复初始化。
六、锁的使用场景
了解何时使用自旋锁,何时使用互斥体(或信号量)对编写优良代码很重要,但是多数情况下,并不需要太多的考虑,因为在中断上下文中只能使用自旋锁,而在任务睡眠时只能使用互斥体。
七、禁止抢占
1、概述
由于内核是抢占性的,内核的进程在任何时刻都可能停下来以便来运行另一个被调度的进程,所以同一临界区可能会被两个进程访问,为了避免我们引入同步机制,例如一个自旋锁被持有,内核便不能进行抢占。 内核抢占和SMP(多处理器)都面临相同的并发问题,因为同步机制的引入所以两者都是安全的
2、内核抢占API
其中的计数值就是禁止抢占函数调用就+1,如果本身为0内核为可抢占状态
八、顺序与屏障
1、概述
当处理多处理器之间或硬件设备之间的同步问题时,有时需要以指定的顺序发出读内存(读入)和写内存(存储)指令。在和硬件交互时,时常需要确保一个给定的读操作发生在其他读或写操作之前。另外,在多处理器上,可能需要按写数据的顺序读数据(通常确保后来以同样的顺序进行读取)。但是编译器和处理器为了提高效率,可能对读和写重新排序,这样无疑使问题复杂化。
幸好,所有可能重新排序和写的处理器提供了机器指令来确保顺序要求。同样也可以指示编译器不要对给定点周围的指令序列进行重新排序。这些确保顺序的指令称作屏障(barriers)。