一、基本介绍
1、基本概念
Linux 内核同步是指控制多个进程按照一定的规则或顺序访问某些系统资源的机制,下面是几个关键概念
1、临界区和竞争条件
a.临界区:访问和操作共享数据的代码段
b.竞争条件:多个执行线程在一个临界区同时执行
2、死锁:每个线程都在互相等待,但它们永远不会释放占用的资源
a.自死锁:一个执行线程试图去获取一个自己已经持有的锁,它不得不等待锁释放,但因为它忙于等待这个锁,所以自己永远也不会有机会释放释放锁
b.ABBA死锁:每个线程都持有一把其他进程需要得到的锁
2、避免死锁
使用以下设计规则避免死锁
a.按顺序加锁
b.防止发生饥饿
c.不要重复请求一个锁
4.设计力求简单,越复杂加锁机制,越可能造成死锁
二、内核同步方法
1、原子操作
1、两个原子操作绝对不会并发的访问一个变量
2、Linux提供了两组原子操作接口,一组针对整数进行操作,一组对单独的位进行操作
3、针对整数的原子操作只能对atomic_t类型的数据进行处理,便于移植和保证该类型数据不会传递给任何非原子函数
typedef struct {volatile int counter;
} atomic_ttypedef struct {volatile long counter;
} atomic64_t
下面列举部分api, 全部api可搜索官方文档
(1)整数操作api(32位)
API | Desc |
ATOMIC_INIT(int i) | 声明一个atomic_t变量,并初始化为i |
int atomic_read(atomic_t *v) | 读取atomic_t变量,转化成int类型,原子操作 |
void atomic_set(atomic_t *v, int i) | 设置v值为i,原子操作 |
void atomic_add(int i, atomic_t *v) | v=v+i,原子操作 |
(2)位操作api
API | Desc |
void set_bit(int nr, void *addr) | 设置addr所指对象的第nr位,原子操作 |
void clear_bit(int nr, void *addr) | 清空addr所指对象的第nr位,原子操作 |
void test_and_set_bit(int nr, void *addr) | 设置addr所指对象的第nr位,并返回原先的值,原子操作 |
void test_bit(int nr, void *addr) | 返回addr所指对象的第nr位,原子操作 |
2、自旋锁
多个线程访问临界区,可以用自旋锁进行保护,自旋锁最多只能被一个可执行线程持有,其他线程需等待锁的释放。持有自旋锁的时间尽可能小于两次上下文切换耗时
自旋锁:其他线程自旋等待,消耗CPU时间
信号量:其他线程睡眠,锁释放后唤醒线程,两次上下文切换开销
(1)用法
可以使用在中断处理程序中,但是在获取锁之前要禁用本地中断,否则可能导致死锁
注:中断处理程序中不能使用信号量, 因为中断上下文是禁止睡眠的,中断通常发生频繁,考虑到系统性能不会给其分配task_struct,也就是说中断上下文不是一个进程,在睡眠后,无法调度唤醒
(2)api
API | Desc |
spin_lock() | 获取指定自旋锁 |
spin_lock_irq() | 禁止本地中断,并获取指定的自旋锁 |
spin_lock_bh() | 禁用所有下半部的执行,并获取指定的自旋锁 |
spin_unlock() | 释放指定的自旋锁 |
spin_unlock_irq() | 释放指定的自旋锁,并激活本地中断 |
spin_unlock_bh() | 释放指定的自旋锁,并激活所有下半部的执行 |
- 由于下半部可以抢占进程上下文的代码,故在下半部和进程上下文共享数据时,应该使用spin_lock_bh()禁止下半部,获取自旋锁对临界区的保护
- 由于中断处理程序可以抢占下半部,故如果二者共享数据时,需要使用spin_lock_irqsave()或spin_lock_irq()禁止本地中断,获取自旋锁对临界区的保护
- 由于同类tasklet不可能同时运行,故不需要加锁
- 由于不同类tasklet可以在不同处理器上运行,如果共享数据,需要获取普通自旋锁对临界区进行保护
- 由于同类型的软中断可以在不同处理器上运行,所以不同软中断共享数据时,需要获取锁的保护
3、读写自旋锁
读自旋锁:一个或多个任务并发持有读自旋锁,写锁等待所有读者释放锁
写自旋锁: 写自旋锁最多被一个任务持有,且此时不存在并发的读操作
API | Desc |
rwlock_init() | 动态初始化指定的rwlock_t |
read_lock() | 获取指定的读自旋锁 |
read_unlock() | 释放指定的读自旋锁 |
write_lock() | 获取指定的写自旋锁 |
write_unlock() | 释放指定的写自旋锁 |
4、信号量
信号量是内核中允许睡眠的锁,如果一个任务试图获取已被占用的信号量,信号量会将其推入一个等待队列,让其睡眠,当信号量释放时,任务会被唤醒。相比于自旋锁,可以提高CPU的使用率,但是引入了两次上下文切换和维护等待队列的开销,适用于锁被长时间持有的情况
互斥信号量:临界区只允许一个任务持有,比较常用
计数信号量:临界区允许至多有count个任务持有,当count=1就是互斥信号量
(1)创建信号量
// 静态声明信号量
// count:临界区允许访问的最多线程数量
struct semphore name;
sema_init(&name,count);// 互斥信号量的定义和初始化
static DECLARE_MUTEX(name);//初始化一个动态创建的计数信号量
//sem是指针
sema_init(sem,count);
//初始化一个动态创建的互斥信号量
init_MUTEX(sem);
(2)api
- P()和down():通过信号量计数减1来请求获取一个信号量,如果结果大于等于0,则获取信号量锁,进入临界区。反之,线程进入等待队列,进入睡眠状态
- V()和up():通过信号量计数加1来释放一个信号量,唤醒等待队列中的线程
api | Desc |
sema_init(struct semaphore *, int) | 通过指定的信号量计数初始化信号量 |
init_MUTEX(struct semaphore *) | 通过信号量计数为1,初始化信号量 |
down_interruptible(struct semaphore *) | 尝试获取信号量,如果信号量被占用,则进入可中断睡眠状态 |
down(struct semaphore *) | 尝试获取信号量,如果信号量被占用,则进入不可中断睡眠状态 |
down_trylock(struct semaphore *) | 尝试获取信号量,如果信号量被占用,则返回非0值 |
up(struct semaphore *) | 释放信号量,如果等待队列不为空,则唤醒等待队列中的线程 |
5、互斥体
- 任何时刻只有一个进程可以持有mutex
- 需要在同一个上下文加解锁,不能递归加解锁
- 不能在中断或者下半部使用mutex,可以睡眠
- 相比信号量,优先使用mutex
api | Desc |
DEFINE_MUTEX(struct mutex); | 静态地定义mutex |
init_MUTEX(struct semaphore *) | 动态初始化mutex |
mutex_lock(struct mutex *); | 为指定的mutex上锁,如果锁不可用则睡眠 |
mutex_unlock(struct mutex *); | 为指定的mutex解锁 |
mutex_trylock(struct mutex *); | 尝试获取指定的mutex,如果成功则返回1,反之返回0。 |
mutex_is_lock(struct mutex *); | 如果锁被占用,则返回1,反之返回0。 |
5、完成量
内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量可以做到同步,可以唤醒正在等待的任务
api | Desc |
DECLARE_COMPLETION(mr_comp); | 静态地定义并初始化完成变量 |
init_completion(struct completion *); | 初始化指定的动态创建的完成变量 |
wait_for_completion(struct completion *); | 等待接收指定的完成变量信号 |
complete(struct completion *); | 发信号唤醒任何等待任务 |
6、顺序锁
顺序锁对临界区进行读取数据时不加锁,只有写进程加锁。在写者少,读者多场景比较适用
当有数据写入时,序列值会增加,读取前后会检查序列值的一致性,如不一致说明读时候有数据写入,需要重新读取。
api | Desc |
DEFINE_SEQLOCK(const seqlock_t); | 静态地定义并初始化seq锁 |
write_seqlock(const seqlock_t *); | 获取写锁 |
write_sequnlock(const seqlock_t *); | 释放写锁 |
read_seqbegin(const seqlock_t *); | 读取获取读锁之前的值,返回sequence的值。 |
read_seqretry(const seqlock_t *,unsigned ); | 将seq锁中sequence的前后值进行对比。如果前后值不相等,则返回值为1,读者需要重新进行读操作;反之,返回值为0,则读者成功完成了读操作。 |
7、屏障
编译器为了提高效率,可能存在将读和写操作进行重新排序。屏障是一组确保代码顺序执行的指令,保证跨越屏障的读写操作不会发生重新排序
api | Desc |
rmb() | 阻止跨越屏障的载入动作发生重排序 |
wmb() | 阻止跨越屏障的存储动作发生重排序 |
mb() | 阻止跨越屏障的载入和存储动作发生重排序 |
barrier() | 阻止编译器跨屏障对载入或存储操作进行优化 |
【参考博客】
[1] Linux 内核设计与实现
[2] Linux内核 | 内核同步 - 世至其美