linux内核学习——内核同步

时间:2023-01-24 20:32:00

一、内核同步基础概念

共享数据:能够被多个代码段访问的数据,一般指全局变量和静态变量

临界区:访问和操作共享数据的代码段

竞争条件:如果两个执行线程可能处于同一个临界区中同时执行,就被称为竞争条件发生了

同步:避免并发和防止竞争条件成为同步


内核中可能造成并发执行的原因:

  • 中断 —— 中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码。
  • 内核抢占 —— 因为内核具有抢占性,所以内核中的任务可能会被另一任务抢占。
  • 对称多处理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/