Linux内核设计与实现——读书笔记(7)内核同步方法

1、原子操作

  原子操作只能保证原子性,顺序性通过屏障指令来实现。
  atomic_t类型用于32位原子操作,本质是一个被封装的int。64位类型使用atomic64_t,本质是一个被封装的long类型。

1.1、原子整型操作API函数

  定义在<asm/atomic.h>中。64位API为atomic64 开头。

            API描述
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(atomic_t *v,int i)原子地给v值加i
void atomic_sub(atomic_t *v,int i)原子地给v值减i
void atomic_inc(atomic_t *v)原子地给v值加1
void atomic_dec(atomic_t *v)原子地给v值减1
int atomic_add_return(int i,atomic_t *v)原子地给v值加i,返回结果
int atomic_sub_return(int i,atomic_t *v)原子地给v值减i,返回结果
int atomic_inc_return(int i,atomic_t *v)原子地给v值加1,返回结果
int atomic_dec_return(int i,atomic_t *v)原子地给v值减1,返回结果
int atomic_add_negative(int i,atomic_t *v)原子地给v值加i,如果结果为负数,返回真;否则返回假
int atomic_sub_and_test(int i,atomic_t *v)原子地给v值减i,如果结果等于0,返回真;否则返回假
int atomic_inc_and_test(int i,atomic_t *v)原子地给v值加1,如果结果等于0,返回真;否则返回假
int atomic_dec_and_test(int i,atomic_t *v)原子地给v值减1,如果结果等于0,返回真;否则返回假

1.2、原子位操作API

  定义在文件 <asm/bitops.h> 中。原子位操作API有两个参数,一个数据指针和一个位号。要注意的是位号没有做限制,32位机上位号范围为0~31。

            API描述
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位

  内核还提供了两个例程用来从指定的地址开始搜索第一个被设置的位。

int find_first_bit(unsigned long *addr,unsigned int size);
int find_first_zero_bit(unsigned long *addr,unsigned int size);

  第一个参数是数据指针;
  第二个参数是要搜索的总位数;
  如果搜索范围仅限于一个字,使用_ffs()和ffz(),只需传递一个要搜索的指针。

2、自旋锁

  自旋锁最多只能被一个可执行线程持有,同一个锁可以用在多个位置。线程申请不到锁时的自旋行为特别浪费处理器时间,所以,自旋锁不能长时间持有。虽说可以在申请不到锁时让请求线程睡眠来避免处理器开销,但是会存在两次明显的上下文切换,所以主要还是要让线程持有锁的时间尽可能地短
  注意:自旋锁不可递归,否则死锁。
  自旋锁可以在中断处理程序中使用,在获得锁之前要禁用本地中断(只需禁止当前CPU的中断就好)。
  内核提供了禁止中断同时请求锁的接口:

DEFINE_SPINLOCK(my_lock);
unsigned long flags;
spin_lock_irqsave(&my_lock,flags);
...
spin_unlock_irqrestore(&my_lock,flags);

2.1、自旋锁操作API

API描述
spin_lock()获取指定的自旋锁
spin_lock_irq()禁止本地中断并获取指定的锁
spin_lock_irqsave()保存本地中断状态,禁止本地中断并获取指定的锁
spin_lock_bh()禁止下半部并获取指定的锁
spin_unlock()释放指定锁
spin_unlock_irq()释放指定锁,并激活本地中断
spin_unlock_bh()释放指定锁,并激活下半部
spin_unlock_irqrestore()释放指定锁,将中断恢复为禁用前状态
spin_lock_init()动态初始化指定的spinlock_t
spin_trylock()试图获取指定锁,不自选,如果未获取返回非0
spin_is_locked()如果指定锁正在被获取,则返回非0,否则返回0

2.2、调试自旋锁

  配置选项CONFIG_DEBUG_SPINLOCK如果被激活,内核会检查是否使用了未初始化的锁,是否在没加锁的时候就执行开锁操作。
  如果全程调试锁,还应打开CONFIG_DEBUG_LOCK_ALLOC

3、读写自旋锁

  主要是:不能并发写,写时不能读,可以并发读。写时不能读,也意味着如果先持有读锁时,不能申请写锁,否则会造成死锁:

read_lock(&my_lock);		
write_lock(&my_lock);		//因为读锁还没释放,申请写锁时会自旋

  读写自旋锁使用如下方法初始化:

DEFINE_RWLOCK(my_rwlock);

  读分支锁的使用如下:

read_lock(&my_lock);
read_unlock(&my_lock);

  写分支锁的使用如下:

write_lock(&my_lock);
write_unlock(&my_lock);

  通常情况下,读锁和写锁会位于完全分隔开的代码分支中,不能把一个读锁“升级”为写锁。

  如果写和读不能清晰分开,使用一般的自旋锁就ok。

3.1、读写自旋锁的操作API

API描述
read_lock()获取指定的读锁
read_lock_irq()禁止本地中断并获取指定的读锁
read_lock_irqsave()保存本地中断状态,禁止本地中断并获取指定的读锁
read_unlock()释放指定读锁
read_unlock_irq()释放指定读锁,并激活本地中断
read_unlock_irqrestore()释放指定读锁,将中断恢复为禁用前状态
wirte_lock()获取指定的写锁
wrtie_lock_irq()禁止本地中断并获取指定的写锁
write_lock_irqsave()保存本地中断状态,禁止本地中断并获取指定的写锁
write_unlock()释放指定写锁
write_unlock_irq()释放指定写锁,并激活本地中断
write_unlock_irqrestore()释放指定写锁,将中断恢复为禁用前状态
write_trylock()试图获取指定写锁,不自旋,如果未获取返回非0
rwlock_init()初始化指定的rwlock_t

4、信号量

  信号量是一种睡眠锁。如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。当所需信号量可用后,处于等待队列的此任务会被唤醒,获得该信号量。
  一些结论:

  • 由于申请不到信号量时会进入睡眠,所以会比自旋锁提供更好的处理器利用率,但是会有更大的开销(睡眠,维护等待队列,唤醒);
  • 信号量适用于锁会长时间持有的情况;
  • 只能在进程上下文中才能获取信号量锁;
  • 占用信号量时不能占用自旋锁。

4.1、计数信号量和二值信号量

  信号量维护一个计数器(count),用于表明可持有者的数量。二值信号量和自旋锁的count值为1。二值信号量也称为互斥信号量。
  信号量支持两个原子操作(PV操作),PV来自荷兰语Proberen和Vershogen。前者叫做测试操作(字面意思是探查),后者叫做增加操作。后来系统把这两种操作叫做dowm()up()。**dowm()**操作通过对count减1来请求获取一个信号量,如果count值小于0,则任务放入等待队列;**up()**用于释放信号量,对count做加1操作。

4.2、创建和初始化信号量

  实现定义在 <asm/semaphore.h> 中。使用struct semaphore类型来表示信号量:

struct semaphore {
    atomic_t count;
    int sleepers;
    wait_queue_head_t wait;                                                                                                        
};

  通过以下方式静态地声明信号量:

struct semaphore name;
sema_init(&name,count);

  name为信号量变量名,count为信号使用数量。
  普通的互斥信号量静态创建时可以使用以下方式:

static DECLARE_MUTEX(name);

  更常见的是使用以下方式动态创建:

sema_init(sem,count);

  sem是信号量指针,count是信号量的使用者数量。
  普通的互斥信号量动态创建时可以使用以下方式:

init_MUTEX(sem);

4.3、信号量使用API

API描述
sema_init(struct semaphore *,int);以指定的计数值初始化动态创建的信号量
int_MUTEX(struct semaphore *);以计数值1初始化动态创建的信号量
init_MUTEX_LOCKED(struct semaphore *);以计数值0初始化动态创建的信号量
down_interrputible(struct semaphore *);试图获取指定的信号量,如果信号量已被争用,则进入可中断睡眠状态
down(struct semaphort *);试图获取指定的信号量,如果信号量已被争用,则进入不可中断睡眠状态
down_trylock(struct semaphore *);试图获得指定信号量,如果信号量已被争用,则立即返回非0
up(struct semaphore *);释放指定的信号量,如果睡眠队列不为空,则唤醒其中一个任务

5、读写信号量

  读写信号量由rw_semaphore 结构表示,定义在 <linux/resem.h> 中。
  静态创建读写信号量:

static DECLARE_RWSEM(name);

   动态创建的读写信号量可以通过以下函数初始化:

init_rwsem(struct rw_semaphore *sem);

  读写信号量都是互斥信号量,引用计数等于1(只对写互斥)

5.1、读写信号量API

API描述
DECLARE_RWSEM(name);静态创建读写信号量
init_rwsem(struct rw_semaphore *sem)初始化指定的读写信号量
down_read(struct rw_semaphore *sem);试图获取读信号量
down_write(struct rw_semaphore *sem);试图获取写信号量
down_read_trylock(struct rw_semaphore *sem);试图获取读信号量,如果信号量锁被争用,返回0,否则返回非0
down_write_trylock(struct rw_semaphore *sem);试图获取写信号量,如果信号量锁被争用,返回0,否则返回非0
void downgrade_write(struct rw_semaphore * sem );动态将获取的写信号量锁转换为读信号量锁
up_read(struct rw_semaphore *sem);试图释放读信号量
up_write(struct rw_semaphore *sem);试图释放写信号量

  除非代码中的读和写可以明白无误地分割开来,否则最好不使用。

6、互斥体

  以前,内核中唯一允许睡眠的锁是信号量。通常大多数时候把信号量只用在计数为1的互斥行为,但是信号量没有使用限制,用途较广,应该更适用于复杂、情况未明的访问。因此,对于简单的锁定情况使用信号量并不方便,也缺乏强制性规则来进行调试。
  因此,引入了互斥体。互斥体的定义是:任何可以睡眠的强制互斥锁
  互斥体对应的数据结构是mutex,静态定义一个mutex:

DEFINE_MUTEX(name);

  动态初始化mutex:

mutex_init(&mutex);

  锁定和解锁mutex:

mutex_lock(struct mutex *)		//上锁,不可用则睡眠
mutex_trylock(struct mutex *)	//试图上锁,成功返回1,失败返回0
mutex_unlock(struct mutex *)	//解锁
mutex_is_locked(struct mutex *)	//如果锁已上锁,返回1,否则,返回0
  • 给mutex上锁者必须负责解锁,不能在一个上下文中锁定后,在另一个上下文进行解锁;
  • 当持有一个mutex时,进程不能退出;
  • 不能在中断或者下半部中使用mutex,即使是mutex_trylock()

  打开配置选项CONFIG_DEBUG_MUTEXES 可以进行检测。

7、各种锁的使用场景(重要)

需求建议
低开销优先使用自旋锁
短期优先使用自旋锁
长期优先使用互斥体
中断上下文自旋锁
持有锁需要睡眠互斥体

8、完成变量

  内核中一个任务发出信号通知另一个任务某个特定事件,可以使用完成变量来同步。例如:当子进程执行或者退出时,vfork()系统调用使用完成变量唤醒父进程。
  完全变量由结构completion表示,定义在 <linux/completion.h> 中。
  静态创建并初始化完全变量:

DECLARE_COMPLETION(mr_COMP);

  动态创建并初始化完全变量:

init_completion()

  需要等待的任务调用以下函数来等待:

wait_for_completion(struct completion *)

  当特定事件发生后,产生事件的任务调用以下函数来唤醒正在等待的任务:

complete(struct completion *)

9、顺序锁

  顺序锁简称seq锁。实现这种锁主要依靠一个序列计数器,当有数据被写入时,会得到一个锁,并且序列值会增加。在读取数据之前和之后,序列值也会被读取。如果前后两次序列号值相同,则表示在都过程中没有被写打断;如果读取的值是一个偶数,那么就表明写操作没有发生。
  定义一个seq锁:

seqlock_t my_seqlock = DEFINE_SEQLOCK(my_seqlock);

  写锁使用方式:

write_seqlock(&my_seqlock);
...
write_sequnlock(&my_seqlock);

  读锁使用方式:

unsigned long seq;
do{
	seq = read_seqbegin(&my_seqlock);
}while(read_seqretry(&my_seqlock,seq));

  seq锁对写者更加有利,只要没有其他写者,写锁总能够被获取成功。
  seq锁使用在如下场景:

  • 读者很多,写者很少;
  • 写者虽少,但是希望优先于读者;
  • 数据简单,甚至是整形;

  seq锁最好的例子是jiffies,存储了系统启动到当前的时钟节拍数。
在这里插入图片描述

10、禁止抢占

  内核的规则规定了:如果一个自旋锁被持有,内核就不能进行抢占
  如果一个变量是处理器上独立的变量,可能就不需要加锁,这时候可以使用以下函数来禁止和激活内核抢占:

API描述
preempt_disable()增加抢占计数值,从而禁止内核抢占
preempt_enable()减少抢占计数,当preempt_count降为0时,检查和执行被挂起的需要调度的任务
preempt_enable_no_resched()激活内核抢占但不在检查任何被挂起的需调度任务
preempt_count()返回抢占计数

  这些函数实际上操作的就是thread_info 结构中的preempt_count计数器。笔记第二篇7.3小节有提到

11、顺序和屏障

  在一些处理器上,可能需要按某种顺序来读写数据,但是编译器好处理器为了提高效率,可能对读和写重新排序,这样可能会出现一些问题。不过,所有可能重新排序的处理器都提供了机器指令来确保顺序要求。这些确保顺序的指令称为屏障

11.1、屏障API

API描述
rmb()阻止跨越屏障的载入(读)动作发生重排序
read_barrier_depends()阻止跨越屏障的具有数据依赖关系的载入动作重排序
wmb()阻止跨越屏障的存储(写)动作发生重排序
mb()阻止跨越屏障的载入(读)和存储(写)动作发生重排序
smp_rmb()该函数在smp系统上映射为rmb()函数,在x86 UP系统上映射为barrier()函数
smp_read_barrier_depends()该函数在smp系统上映射为read_barrier_depends()函数,在x86 UP系统上映射为barrier()函数
smp_wmb()该函数在smp系统上映射为wmb()函数,在x86 UP系统上映射为barrier()函数
smp_mb()该函数在smp系统上映射为mb()函数,在x86 UP系统上映射为barrier()函数
barrier()阻止编译器跨屏障对载入(读)或存储(写)操作进行优化

  屏障也是使用 __asm__ __volatile__ 实现的,参考
  
  
  
  

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr_zhangsq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值