一、内核同步基础概念
共享数据:能够被多个代码段访问的数据,一般指全局变量和静态变量
临界区:访问和操作共享数据的代码段
竞争条件:如果两个执行线程可能处于同一个临界区中同时执行,就被称为竞争条件发生了
同步:避免并发和防止竞争条件成为同步
内核中可能造成并发执行的原因:
- 中断 —— 中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码。
- 内核抢占 —— 因为内核具有抢占性,所以内核中的任务可能会被另一任务抢占。
- 对称多处理 (SMP)—— 两个或多个处理器可以同时执行代码。
- 软中断和tasklet —— 内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码。
- 睡眠及与用户空间的同步 —— 在内核执行的进程可能会睡眠。这就会唤醒调度程序,从而导致调度一个新的用户进程执行。
同步是要给数据加锁,而不是给代码加锁。两个处理器绝对不能同时访问同一共享数据,如果有其他执行线程可以访问这些数据,那么就要给这些数据加上某种形式的锁。
死锁条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了。所有线程都在相互等待,但他们永远不会释放已经占有的资源。于是任何线程都无法继续,这边意味着死锁的发生。
二、内核同步方法
1.原子操作
原子操作可以保证指令以原子的方式执行 —— 执行过程不被打断。
原子操作是其他同步方法的基石。
内核提供了两组原子操作接口 —— 一组针对整数进行操作,另一组针对单独的位进行操作。
原子整数操作:
针对整数的原子操作只能对atomic_t类型的整数进行处理。这里没有直接使用int类型原因:
- 只接受atomic_t类型的操作数,确保原子操作至于这种特殊类型数据一起使用。
- 使用atomic_t类型确保编译器不对相应的值进行访问优化,这点是的原子操作最终接收到正确的内存地址,而不是一个别名。
- 在不同体系结构上实现原子操作的时候,使用atomic_t可以屏蔽期间的差异。
atomic_t类型定义在文件<linux/types.h>中:
typedef struct {
volatile int counter;
}
定义并操作一个atomic_t类型的数据:
atomic_t v; //定义 v
atomic_t u = ATOMIC_INIT(0); //定义 u 并把它初始化为0
atomic_set(&v, 4); // v = 4
atomic_add(2, &v); // v = v + 2 = 6
atomic_inc(&v); //v = v + 1 = 7
如果需要把atomic_t转换成int型,可以使用atomic_read()来完成:
printk("%d\n", atomic_read(&v)); // 会打印 7
原子整数操作列表 原子整数操作 描述 ATOMIC_INIT(int i) 在声明一个atomic_t变量时,将它初始化为 i int atomic_read(atomic_t *v) 原子地读取整数变量 v void atomic_set(atomic_t *v,int i) 原子地设置v 值为 i void atomic_add(int i,atomic_t *v) 原子地给v加 i void atomic_sub(int i,atomic_t *v) 原子地从v减 i void atomic_inc(atomic_t *v) 原子地给v加 1 void atomic_dec(atomic_t *v) 原子地从v减 1 void atomic_sub_and_test(int i,atomic_t *v) 原子地从v减 i,如果结果等于0,返回真;否则返回假 void atomic_add_negative(int i,atomic_t *v) 原子地给v加 i,如果结果是负数,返回真;否则返回假 void atomic_add_return(int i,atomic_t *v) 原子地给v加 i,且返回结果 void atomic_sub_return(int i,atomic_t *v) 原子地给v减 i,且返回结果 void atomic_inc_return(int i,atomic_t *v) 原子地给v加 1,且返回结果 void atomic_dec_return(int i,atomic_t *v) 原子地给v减 1,且返回结果 void atomic_dec_and_test(atomic_t *v) 原子地给v减 1,如果结果等于0,返回真;否则返回假 void atomic_add_and_test(atomic_t *v) 原子地给v加 1,如果结果等于0,返回真;否则返回假 64位原子整数操作与32位基本相同,只是在函数名中atomic后加上了64,如:long atomic64_read(atomic64_t *v)
原子位操作:
除了原子整数操作,内核也提供了一组针对位这一级数据进行的操作函数。
由于原子位操作是对普通的指针进行的操作,所以不像原子整型对应atomic_t,这里没有特殊的数据类型。
e.g.
unsigned long word = 0;
set_bit(0, &word); //第0位被设置为1(原子地)
set_bit(1, &word); //第1位被设置为1(原子地)
printk("%ul\n", word); //打印 3
clear_bit(1, &word); //清空第1位
change_bit(2, &word); //翻转第2位的值,这里它被置1,因为之前为0
原子位操作列表 原子位操作 描述 void set_bit(int nr, void *addr) 原子地设置addr所指对象的第nr位 void clear_bit(int nr, void *addr) 原子地清空addr所指对象的第nr位 void change_bit(int nr, void *addr) 原子地翻转addr所指对象的第nr位 int test_and_set_bit(int nr, void *addr) 原子地设置addr所指对象的第nr位,
并返回之前的值int test_and_clear_bit(int nr, void *addr) 原子地清空addr所指对象的第nr位,
并返回之前的值int test_and_change_bit(int nr, void *addr) 原子地翻转addr所指对象的第nr位,
并返回之前的值int test_bit(int nr, void *addr) 原子地返回addr所指对象的第nr位 内核还提供了一组与上述操作对应的非原子位操作函数,但不保证原子性,且其名字多个前缀(两个下划线),如:__test_bit()。
如果数据已经加锁保护了,使用非原子位操作函数可能会执行更快一点。
2.自旋锁
linux内核中最常见的锁是自旋锁。自旋锁最多只能被一个可执行线程持有。一个被争用的自旋锁使得请求它的线程在等待所重新可用时自旋(忙等待,特别浪费处理器时间)。所以自旋锁不应该被长时间持有,这也是使用自旋锁的初衷:在短时间内进行轻量级加锁。
加锁原则:对数据加锁二不是代码加锁
加锁和解锁要成对使用,否则只加锁不解锁会造成阻塞,从而妨碍程序运行。
自旋锁实现和体系结构密切相关。这些与体系结构相关的代码定义在<asm/spinlock.h>中,实际要用到的接口定义在<linux/spinlock.h>中。
定义自旋锁:
spinlock_t lock;
初始化自旋锁:
spin_lock_init(&lock);
在内核中,将定义和初始化自旋锁合并成一个宏:
DEFINE_SPINLOCK(lock);
自旋锁的基本使用形式:
DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* 临界区...*/
spin_unlock(&mr_lock);
自旋锁方法列表 方法 描述 spin_lock() 获得指定的自旋锁 spin_lock_irq() 禁止本地中断并获取指定的锁 spin_lock_irqsave() 先保存本地中断的当前状态,禁止本地中断并获取指定的锁 spin_unlock() 释放指定的锁 spin_unlock_irq() 释放指定的锁,并激活本地中断 spin_unlock_irqrestore() 释放指定的锁,并让本地中断恢复到以前状态 spin_lock_init() 动态初始化指定的spinlock_t spin_trylock() 试图获取指定的锁,如果为获取,则返回非0 spin_is_locked() 如果指定的锁当前正在被获取,则返回非0,否则返回0 有时,锁的用途可以明确地分为读取和写入两个场景,例如:生产者和消费者问题。为此,linux内核提供了专门的读—写自旋锁。
一个或多个读任务可以并发第持有读者锁;用于写的锁只能被一个写任务持有,且此时不能有并发的读操作。
读写锁初始化:
DEFINE_RWLOCK(mr_rwlock);
在读者的代码分支中使用如下函数:
read_lock(&mr_rwlock);
/* 临界区(只读)...... */
read_unlock(&mr_rwlock);
在写者的代码分支中使用如下函数:
write_lock(&mr_rwlock);
/* 临界区(读写)...... */
write_unlock(&mr_rwlock);注意:读锁和写锁不能混用,否则会造成死锁,如下:
read_lock(&mr_rwlock);
write_lock(&mr_rwlock);
3.信号量
linux中的信号量是一种睡眠锁。如果一个任务试图获得一个不可用(已经被占用)的信号量时,信号量会把它推进到一个等待队列,然后将其睡眠
当持有的信号量可用(被释放)后,处于等待队列中的任务将被唤醒,并获得该信号量。
信号量比自旋锁提供了更好的处理器利用率,因为没有把时间花在忙等待上,但是信号量比自旋锁有更大的开销(任务睡眠和唤醒涉及两次进程调度)。
创建和初始化信号量:
信号量具体定义在<asm/semaphore.h>(前面说过,与体系结构相关的实现都定义在asm下)中。
静态声明信号量:
struct semaphore name;
sema_init(&name, count); //name是信号量变量名,count是信号量的可使用数量
未指定count的信号量默认为1,即互斥信号量。
信号量使用down() 和 up()操作来请求获得和释放一个信号量。
down()操作通过对信号量计数减一来请求获得一个信号量。如果结果是0或者大于0,获得信号量锁,任务就可以进入临界区。如果是负数,任务会被放入等待队列,处理器执行其他任务。
up()操作用来释放信号量,它会增加信号量的计数值。如果在该信号量上的等待队列不为空,则处于队列中的等待任务在被唤醒的同时会获得该信号量。
信号量使用举例:
static DECLARE_MUTEX(mr_sem); //定义并声明一个互斥信号量,名字为mr_sem,用于信号量计数
if (down_interruptible(&mr_sem)) //试图获取信号量,如果此时没有获得信号量,则进入等待队列,直到获得信号量
{
/* 临界区...... */
}
up(&mr_sem); //释放给定的信号量
信号量方法列表 方法 描述 sema_init(struct semaphore *sem, int count) 以指定的计数值初始化动态创建信号量 init_MUTEX(struct semaphore *) 以计数值 1 初始化动态创建信号量 init_MUTEX_LOCKED(struct semaphore *) 以计数值 0 初始化动态创建信号量
(初始化为加锁状态)down(struct semaphore *) 试图获得指定的信号量,如果信号量
已被争用,则进入不可中断睡眠状态down_interruptible(struct semaphore *) 试图获得指定的信号量,如果信号量
已被争用,则进入可中断睡眠状态down_trylock(struct semaphore *) 试图获得指定的信号量,如果信号量
已被争用,则立刻返回非0值up(struct semaphore *) 释放指定的信号量,如果睡眠队列不空,
则唤醒其中一个任务
读写信号量:
static DECLARE_RESEM(mr_rwsem);
down_read(&mr_rwsem);
/* 临界区 (只读)*/
up_read(&mr_rwsem);
down_write(&mr_rwsem);
/* 临界区 (读写)*/
up_write(&mr_rwsem);
互斥体:
互斥体是一个更简单的睡眠锁,其行为和计数为1的信号量类似,单接口更简单,实现也更高效,使用限制更强。
静态定义mutex:
DEFINE_MUTEX(mutex); //mutex为互斥体的名字
动态初始化mutex:
mutex_init(&mutex);
对互斥锁锁定和解锁:
mutex_lock(&mutex);
/* 临界区 */
mutex_unlock(&mutex);
Mutex方法 方法 描述 mutex_lock(struct mutex*) 为指定的mutex上锁,如果所不可用则睡眠 mutex_unlock(struct mutex*) 为指定的mutex解锁 mutex_trylock(struct mutex*) 试图获取指定的mutex,如果成功返回1;否则返回0 mutex_is_locked(struct mutex*) 如果锁已被争用,则返回1;否则返回0
完成量:
定义在头文件linux/completion.h中:
完成量(completion)是Linux系统提供的一种比信号量更好的同步机制,是对信号量的一种补充。
它用于一个执行单元等待另一个执行单元完成某事。使用完成量等待时,调用进程是以独占睡眠方式进行等待的,不是忙等待。
请参考:
http://bdxnote.blog.163.com/blog/static/84442352012427113929430/