第10章 内核同步方法

第十章 内核同步方法

原子操作

原子操作保证指令以原子的方式执行,执行过程中不被打断。
内核提供了两组原子操作接口,一组针对整数进行操作,另一组针对单独的位进行操作。

原子整数操作

只针对atomic_t类型的数据进行处理。

/*  */
typedef struct{
	volatile int counter;
} atomic_t;
/* 原子整型的操作都声明在 */
/* 使用举例 */
atomic_t v;
atomic_t u=ATOMIC_INIT(0);
atomic_set(&v,4);
atomic_set(2,&v);
atomic_inc(&v);
atomic_read(&v);

原子整数操作往往是内联函数,往往是通过内嵌汇编指令来实现的。在大多数体系结构上,读取一个字本身就是一个原子操作。
原子整数操作最常见的用途就是实现计数器,如atomic_inc()和atomic_dec()。
原子整数操作也可以原子地执行一个操作并检查结果,原子减操作和检查。

64位原子操作

typedef struct
{
	volatile long counter;
}atomic64_t;

和32位功能相同。

原子位操作

#include 
void set_bit(int nr,void *addr);
void clear_bit(int nr,void *addr);

也提供了非原子位函数,有两个下划线的前缀。

自旋锁

自旋锁(spin lock):最多只能被一个可执行线程持有。会轮询检查锁的状态,浪费处理器时间,适合在短期内进行轻量级加锁。
也可以采用让请求锁的线程进行睡眠,在锁可用时将其唤醒,但是涉及到两次上下文切换(执行代码较多)。

自旋锁方法

实现和体系结构密切相关,定义在asm下spin_lock.h中。
实际使用接口定义在中,基本使用形式如下:

DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* 临界区... */
spin_unlock(&mr_lock);

自旋锁可以使用在中断处理程序中。在处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(在当前处理器上的中断请求),防止中断在该处理器上再次进行加锁,从而死锁。如果中断发生在其它处理器上,并不会出现死锁,因为不妨碍锁的释放。

DEFINE_SPINLOCK(mr_lock)
unsigned long flags;
/**
 * spin_lock_irqsave()
 * 保存锁的当前状态,并禁止本地中断,然后在去获得指定的锁
 */
spin_lock_irqsave(&mr_lock,flags);

/**
 * spin_unlock_irqrestore()
 * 对指定的锁解锁,然后让中断恢复到加锁前的状态
 */
spin_unlock_irqrestore(&mr_lock,flags);

加锁原则:
对数据加锁,不是对代码加锁。
如果确定中断在加锁前是激活的,可以无条件的在解锁时激活中断,使用spin_lock_irq()和spin_unlock_irq()会更好一些。

DEFINE_SPINLOCK(mr_lock);
spin_lock_irq(&mr_lock);
spin_unlock_irq(&mr_lock);

其它操作

spin_lock_init()动态创建自旋锁
spin_try_lock()
spin_is_locked()

自旋锁和下半部

在与下半部配合使用时,必须小新地使用锁机制。
由于下半部可以抢占进程上下文的代码,所以下半部和进程上下文共享数据时,必须对进程上下文中的数据进行保护,加锁的同时还要禁止下半部的执行。
同样,由于中断处理程序可以抢占下半部,如果二者之间共享数据,必须在获取恰当的锁的同时还要禁止中断。
同类的tasklet不能同时运行,同类tasklet的共享数据不需要保护。但是数据被两个不同tasklet共享时,就需要在访问下半部的数据中获得一个普通的自旋锁。不需要禁止下半部,同一个处理器上不会有tasklet相互抢占。
软中断数据共享时必须进行保护,但是同一处理器上的软中断不会抢占另一软中断,没必要禁止下半部。
防止同一处理器上重复加锁而死锁。

读-写自旋锁

有时,锁的用途可以明确分为写入和读取两个场景。
一个或多个读任务可以并发地持有读者锁;写锁最多只能被一个写任务持有,而且此时不能有并发的操作。
读/写锁,也被叫做共享/排斥锁,并发/排斥锁。对读者共享,对写者排斥。

/* 初始化 */
DEFINE_RWLOCK(mr_rwlock);
/* 读者 */
read_lock(&mr_rwlock);
/* 临界区(只读)... */
read_unlock(&mr_rwlock);

/* 写者 */
write_lock(&mr_rwlock);
/* 临界区(读写)... */
write_unlock(&mr_rwlock);

信号量

是一种睡眠锁。

  • 适合锁会长时间持有的情况
  • 只能在进程上下文中使用
  • 不能同时持有自旋锁

计数信号量和二值信号量

信号量允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它。
通常情况下,信号量和自旋锁一样,在一个时刻仅允许一个锁的持有者,这时的信号量被称为二值信号量或者互斥信号量,计数为1。

创建和初始化信号量

与体系结构相关,定义在中。
静态声明

/* name是信号量的变量名,count是信号量的使用数量 */
struct semaphore name;
sema_init(&name,count);

互斥信号量创建的快捷方式

static DECLARE_NUTEX(name);

动态创建

sema_init(sem,count);

动态创建的互斥信号量

init_MUTEX(sem)

使用信号量

down_interruptible()试图获取指定的信号量,如果信号量不可用,它将把调用进程设置成TASK_INTERRUPTIBLE状态,进入睡眠。
down()会让进程在TASK_UNTERRUPTIBLE状态下睡眠,但是会在等待信号量的时候不在响应信号。
down_trylock()函数,尝试以阻塞方式来获取指定的信号量。在信号量已被占用时,它立刻返回非零值,否则,它返回0,成功持有信号量锁。
up()释放指定的信号量。

static DELCARE_MUTEX(mr_sem);
if(down_interruptible(&mr_sem){
	/* 信号被接收,信号量未获取 */
}

/* 临界区 */

/* 释放给定的信号量 */
up(&mr_sem);

读-写信号量

内核中以rw_semaphore结构体表示的,定义在中。
静态创建

static DECLARE_RWSWM(name);

初始化

init_rwsem(struct rw_semaphore *sem);

所有的读写信号量都是互斥信号量,只对写者互斥,不对读者。只有一个版本的down()操作。
示例:

static DECLARE_RWSEM(mr_rwsem);
/* 试图获取信号量用于读 */
down_read(&mr_rwsem);

/* 临界区(只读) */

/* 释放信号量 */
up_read(&mr_rwsem);


down_write(&mr_rwsem);

/* 临界区(读和写) */

/* 释放信号量 */
up_write(&mr_rwsem);

互斥体

和互斥信号量类似,但操作接口更简单,实现更高效。
对应数据结构mutex


DEFINE_MUTEX(name);

mutex_init(&mutex);

mutex_lock(&mutex);

mutex_unlock(&mutex);

信号量和互斥体

选择mutex(除非不能满足约束条件)的优先级高于信号量。

自旋锁和互斥体

中断上下文中只能使用自旋锁,任务睡眠是只能使用互斥体。

完成变量

内核中一个任务需要发出信号通知另一任务发生了某个特定事件,利用完成变量(completion variable)是使两个任务同步的简单方法。代替信号量的一个简单的解决办法。
例如,当子进程执行或者退出时,vfork()系统调用使用完成变量唤醒父进程。
数据结构为completion,定义在中。

DECLARE_COMPLETION(mr_comp);

通过init_completion()动态创建并初始化完成变量
wait_for_completion()来等待特定事件
complete()来发送信号唤醒正在等待的任务
使用例子可以参考kernel/sched.c和kernel/fork.c。

BLK:大内核锁

2.0版本

顺序锁

简称seq锁,提供了一个简单机制来读写共享数据。
主要靠一个序列计数器,写入数据的时候,会得到一个锁,序列值会增加,写完成后锁释放,序号减少。在读取数据之前和之后,序列号均会被读取,如果一致,说明读操作没有被写操作打断。如果读取值为偶数,表明当前没有写操作。

/* 定义一个seq锁 */
seqlock_t mr_seq_lock = DEFINE_SEQLOCK(mr_seq_lock);
/* 写锁的方法 */
write_seqlock(&mr_seq_lock);
write_sequnlock(&mr_seq_lock);
/* 读数据 */
unsigned long seq;
do{
	seq = read_seqbegin(&mr_seq_lock);
}while(read_seqretry(&mr_seq_lock,seq);

适合场景:

  • 数据读者很多,写者很少
  • 优先于读
  • 数据很简单

jiffies就使用了seq锁:

u64 get_jiffies_64(void)
{
	unsigned long seq;
	u64 ret;
	do{
		seq = read_seqbegin(&xtime_lock);
		ret =jiffies;
	}while(read_seqretry(&xtime_lock,seq));
	return ret;
}

定时器中断会更新jiffies的值,也需要是有seq锁:

write_seqlock(&xtime_lock);
jiffies_64 += 1;
write_sequnlock(&xtime_lock);

jiffies和内核时间管理,内核源码kernel/timer.c和kernel/time/tick-common.c。

禁止抢占

由于内核是抢占性的,内核中的进程在任何时刻都可能停下来以便另一个具有更高优先级的进程运行。内核抢占代码使用自旋锁作为非抢占区域的标记。
使用preempt_disable()来禁止内核抢占,使用preempt_enable()恢复内核抢占,二者成对出现。
更简洁的方式是get_cpu()。

int cpu;
/* 禁止内核抢占,并将CPU设置为当前处理器 */
cpu=get_cpu();
/* 对每个处理器的数据进行操作... */
/* 给予内核抢占性 */
put_cpu();

顺序和屏障

当处理多处理器之间或者硬件设备之间的同步问题时,有时需要以指定的顺序发出读内存和写内存指令。
编译器和处理器为了提高效率,可能对读写进行排序,可以使用特殊的指令(barriers,屏障)避免重新排序。
rmb()
wmb()
mb()

你可能感兴趣的:(linux内核学习笔记,linux)