LINUX内核设计思想之内核同步方法

9.1 原子操作

原子操作是不可分的微粒操作,它可以保证执行过程不被打断.LINUX提供了两组原子操作接口--一组针对整数操作,另一组针对单独的位操作.

9.1.1 原子整数操作

定义于相关体系结构的<asm/atomic.h>文件中.

针对整数的原子操作只能对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_dec(&v); //v = v - 1 = 6 

Atomic_read(&v); //atomic_t类型转换为int型 

 

Int atomic_dec_and_test(atomic_t *v)

将给定的原子变量减1,如果结果为0,返回真;否则返回假.

 

9.1.2 原子位操作

原子位操作与体系相关,定义于特定体系结构的<asm/bitops.h>.示意代码如上:

Unsigned long word = 0;

Set_bit(0,&word); //设置第0

Clear_bit(0,&word); //清除第0

Change_bit(0,&word); //翻转第0位的值

Test_and_set_bit(0,&word); //原子地设置第0位并返回设置前的值

 

9.1.3 非原子位操作

非原子位操作与原子位操作完全相同,区别在于:

一、非原子位不保证原子性;

二、其名字前多两个下划线.例如,原子位操作是test_bit(),非原子位操作是__test_bit().

应用场景:如果不需要原子操性操作(比如说,如果你已经用锁保护了自己的数据),非原子位操作会比原子操作执行得快一些.

内核提供了两个例程来从指定的地址搜索第一个被设置(或未被设置)的位.

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

//查找地址addr第一个被设置的位的位号;

Int fined_first_zero_bit(unsigned long *addr,unsigned int size);

//查找地址addr第一个被置0的位的位号

如果搜索范围是一个字,可以用__ffs()__ffz()分别替代上面两个函数.

 

9.2 自旋锁

上述的原子操作只能完成简单的变量的同步,没办法完成比较大的数据结构的同步--比如,要同步线程.LINUX内核中最常见的就是自旋锁.自旋锁最多只能被一个可执行线程持有.一个线程企图获取一个被占有的自旋锁,那么该线程会一直忙等,直到锁可被获取.因此,自旋锁是不应该被长时间占用的,否则很影响系统性能.这是锁的一种表现--忙等待.还有另外一种表现为切换其他进程执行,等锁可获取的时候再唤醒等待此锁的进程.自旋锁的实现源码在体系相关结构的<asm/spinlock.h>文件中,接口在<linux/spinlock.h>.

自旋锁使用的注意事项:

ISR中使用自旋锁,一定要在获取自旋锁之前,禁止本地中断.否则,ISR打断持有锁的内核代码,有可能试图争用这个已经被占用的锁,那这个锁也就永远没办法抢到.LINUX内核已经帮我们做好这个工作了并暴露出相关的API.如下使用自旋锁即可:

Spinlock_t mr_lock=SPIN_LOCK_UNLOCKED;

Unsigned long flags;

 

Spin_lock_irqsave(&mr_lock,flags);

/*临界区代码*/

Spin_unlock_irqsave(&mr_lock,flags);

阅读材料:

配置CONFIG_DEBUG_SPINLOCK为使用自旋锁代码加入了许多调度检测手段.

自旋锁的操作:

Spin_lock_init():动态创建自旋锁;

Spin_try_lock():试图获取某特定的自旋锁,如果锁被占用,立马返回非0,否则返回0;

Spin_is_locked():判断自旋锁当前是否已经被占用,如果锁被占用,立马返回非0,否则返回0.

当然最重要的也是最常用的就是上面的示意代码.

 

9.2.1 -写自旋锁

这算是LINUX对自锁旋的一种特殊情况下自旋锁的优化策略,这种特殊情况是:只要没有写操作,多个并发的读操作是安全的.当然有写操作或者多个写操作或者写操作的时候有读的动作,都是不完全的.-写自旋锁的使用示意代码如下:

Rwlock_t mr_rwlock = RW_LOCK_UNLOCKED;

在读的分支:

Read_lock(&mr_rwlock);

/*临界区(只读)*/

Read_unlock(&mr_rwlock);

在写的分支:

Write_lock(&mr_rwlock);

/*临界区(读定)*/

Write_unlock(&mr_rwlock);

通常情况下,读锁和写锁位于完全分割的代码分支中(只能二选一执行,不能顺序执行),如果写成类下面的代码将会带来死锁:

Read_lock(&mr_rwlock);

Write_lock(&mr_rwlock);

在读-写自旋锁里面,读者可以成功地抢到锁,而写者抢不到锁时只能忙等待.

9.4 信号量

上面说了,锁主要有两种表现形式--忙等待和切换给别的进程.自旋锁属于忙等待那种,而信号量则是属于获取不到执行条件锁进入休眠让出CPU那种表现形式.如果有一个任务试图获取一个被占用的信号量时,信号量会将其推进一个等待队列让其睡眠.当信号量持有者释放了信号量再唤醒其执行.信号量和自旋锁的区别最大也就是在这一点上.但是这也并不意味着信号量比自旋锁要强大或取代.因为切换进程上下文的系统开销是很大的.因此,针对此特点,有以下结论:

.适用于锁会被进程占用时间比较长的地方;

.只能在进程上下文中才可以获取信号量锁,因为中断上下文是不能进行调度的;

.试图使用信号量锁的进程里面不能持有自旋锁,因为自旋锁是不允许睡眠的.

因此,要使用信号量还是自旋锁,需要根据锁占用的时间长短来判断.因此,信号量和自旋锁的区别主要如下:

1).信号量允许睡眠,而自旋锁不允许;

2).信号量允许内核抢占,而自旋锁不允许;

3).信号量允许任意数量的锁持有者.

9.4.1 创建和初始化信号量

信号量与体系结构相关,具体实现定义在文件<asm/semaphore.h>.struct semaphore类型用来表示信号量.

静态声明信号量:

Static DECLARE_SEMAPHORE_GENERIC(name,count)

Name是信号变量的名字,count是信号量的使用者数量.静态创建更为普通的互斥信号量如下:

Static DECLARE_MUTEX(name);

动态声明信号量:

Sema_init(sem,count);

Sem是指针,count是信号量的作用数量.初始化一个动态创建的互斥信号量如下:

Init_MUTEX(sem);

9.4.2 使用信号量

函数down_interruptible()试图取得其期望的信号量.如果获取失败,将以TASK_INTERRUPTIBLE状态进入睡眠.

函数down_trylock()函数试图获取指定的信号量,信号量已被占用的情况下返回非0;否则返回0,而且让你成功获取信号量锁.

释放信号量调用up()函数.

示意代码如下:

Static DECLARE_MUTEX(mr_sem);

If(down_interruptible(&mr_sem))

{

/*信号被接收,信号量还没有获取到*/

}

/*临界区...*/

/*释放给定的信号量*/

Up(&mr_sem);

 

 

9.5 -写信号量

所有读-写信号量都是互斥的.只要没有写者,并发持有读锁的读者数不限.相反,只有唯一的写者(在没有读者时)可以获取写锁.

结构体表征:

Struct rw_semaphore表示读-写锁.位于<linux/rwsem.h>.

静态创建:

Static DECLARE_RWSEM(name);

动态创建:

Init_rwsem(struct rw_semaphore *sem);

使用示意代码如下:

Static DECLARE_RWSEM(mr_rwsem);

/*试图获取信号量用于读*/

Down_read(&mr_rwsem);

/*临界区(只读)...*/

/*释放信号量*/

Up_read(&mr_rwsem);

 

/*试图获取信号量用于写*/

Down_write(&mr_rwsem);

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

/*释放信号量*/

Up_write(&mr_rwsem);

 

其他相关函数:

Down_read_trylock()down_write_trylock().这两个方法需要一个指向读-写信号量的指针作为参数.如果成功获取信号量锁,返回非0;如果信号量锁已经被占用,返回0.[:]返回值与普通信号量的情形相反.

Downgrade_write():动态将获取的写锁转换为读锁.

 

9.6

小结:自旋锁和信号量的选用:

低开锁加锁: 自旋锁

短期锁定: 自旋锁

长期锁定: 信号量

中断上下文加锁: 自旋锁

持有锁者需要睡眠: 信号量

 

9.7 完成变量

如果在内核中一个任务需要发出信号通知另一任务发生了某个特定事件,利用完成变量是使两个任务得以同步的简单方法.

结构体:

Struct completion表示一个完成变量,定义于<linux/completion.h>.

静态声明并初始化:

DECLARE_COMPLETION(mr_comp);

动态声明并初始化:

Init_completion()动态创建

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

Wait_for_completion()

产生事件的任务调用以下函数来发出信号唤醒正在等待的任务.

Complete()

上述三个函数都需要一个struct completion指针.

 

9.8 seq

Seq锁的实现主要依赖于一个序列计数器.当有疑义的数据被写入时,会得到一个锁,并且序列值会增加.在读取数据之前和之后,序列号都被读取.如果两次序列号值一样,说明读操作过程没有被写操作打断过.这种优化比较有利于写者.因此它允许了一定程度上的读写动作同时进行,并且保证了没有其他写者,写锁总能成功.

定义:

Seqlock_t mr_seq_lock = SEQLOCK_UNLOCKED;

写锁:

Write_seqlock(&mr_seq_lock);

/*写锁被获取...*/

Write_sequnlock(&mr_seq_lock);

读锁:

Unsigned long seg;

Do

{

Seq = read_seqbegin(&mr_seq_lock);

/*读这里的数据...*/

}while(read_seqretry(&mr_seq_lock,seq));

do{}while()可知,如果读的过程发生了数据变异(通过序列器两次值比较)进行循环操作,直到数据正确为止.

 

9.9 禁止抢占

示意代码:

Preempt_disable();

/*抢占被禁止*/

Preempt_enable();

Preempt_disable()增加抢占计数数值,从而禁止内核抢占;

preempt_enable()减少抢占计数,当该值降为0时检查执行被挂起的需要调度的任务.

 

9.10 顺序和屏障

编译器和处理器为了提高效率,可能对读和写操作重新进行了排序,例如:

在某些处理器上,以下代码:

A = 1;

B = 2;

有可能在A中存放新值之前就在B中存放新值.

但是,我们在操作内存或者和硬件交互时,常常需要确保一个给定的顺序.所有可能重新排序和写的处理器提供了机器指令来确保顺序要求,同样也可以提示编译器不要对给定点周围的指令序列进行重新排序.这些确保顺序的指令叫做"屏障".

内核中实现屏障的函数有:

Rmb():提供""内存屏障,确保跨越rmb()的载入动作不会发生重排序.就是说,rmb()之前的载入操作不会被重新排在rmb()之后去;

Wmb()方法提供了""内存屏障,功能和rmb()函数类似.区别仅仅是针对存储而非载入--确保跨越屏障的存储不发生排序;

Mb()函数既提供了读屏障也提供了写屏障.相当于上述rmb()函数和wmb()函数的功能和.

Read_barrier_depends()rmb()的变种,可以理解成rmb()一种优化.该屏障确保屏障前的读操作在屏障后的读操作之前完成,即那些相互依赖的读操作.

例如下面的代码:

线程1 线程2

a = 3; -

Mb(); -

b = 4; c = b;

- rmb();

- d = a;

如果不使用内存屏障,c可能接受了b的新值,d接收了a原来的值.c可能等于4(我们期望的),d可能等于1(不是我们期望的).

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值