内核同步之Seq锁和屏障

5. 完成变量

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

完成变量由结构completion表示,在<linux/completion.h>中。静态初始化:

DECLARE_COPLETION(my_comp);

运行时动态初始化:

int_completion(my_comp);

在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。当特定事件发生后,产生事件的任务调用completion()来发送信号唤醒正在等待的任务。使用完成变量的例子可以参考kernel/sched.c和/kernel/fork.c文件。

 

6. Seq锁

Seq锁是在2.6内核中才引入的一种新型锁。实现这种锁主要依靠一个序列计数器。当有疑义的数据被写入时,会得到一个锁,并且序列值会增加,在读取数据之前和之后,序列号都被读取。如果读取的序列号值相同,说明在读操作进行的过程中没有被写操作打断过。如果读取的值是偶数,则表明写操作没有发生(因为锁的初始值是0,所以写锁会使值成奇数,释放的时候变成偶数)。接口在<linux/seqlock.h>中定义。

示例如下:

seqlock_t my_seq=SEQLOCK_UNLOCKED;

write_seqlock(&my_seq);

/*写入数据*/

write_sequnlock(&my_seq);

这和普通的自旋锁类似,但是不同的是在读时:

unsigned long seq;

do{

seq=read_seqbegin(&my_seq);

/*读数据*/

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

读者通过获取一个整数顺序值而进入临界区。在退出时,该顺序值会和当前值比较,如果不等,就必须重试读取访问。

 

Seq锁对写者更有利。只要没有其它写者,写锁总是能够被成功获取。挂起的写者会不断地使读者循环,直到不再有任何写者持有锁为止。Seq锁通常不能用于保护包含指针的数据结构,因为在写者修改数据结构指针的同时,读者可能会追随一个无效的指针。

 

7. RCU(读取-复制-更新)

读取-复制-更新(read-copy-update, RCU)也是一种高级的互斥机制,在正确的条件下,也卡获得高的性能。它是针对经常发生读取而很少写入的情形做了优化。被保护的资源应该通过指针访问,而对这些资源的引用必须仅有原子代码拥有。在需要修改数据结构时,写入线程先复制,然后修改副本,之后用新的版本替代相关指针。当确信老的版本没有其他引用时,就释放老的版本。

使用实例有网络路由表和内核中的Starmode射频IP驱动程序。使用RCU的代码应包含<linux/rcupdate.h>。

在读取端,代码使用RCU保护的数据结构时,必须引用数据结构的代码包括在rcu_read_lock和rcu_read_unlock调用之间:

struct data_buff * my_buff;

rcu_read_lock();

do_something_with(my_buff);

rcu_read_unlock();

函数rcu_read_lock调用非常快,它会禁止内核抢占,但不会等待任何东西。用来检验读取"锁"的代码必须是原子的。在调用rcu_read_unlock之后就不该对手保护的数据结构有任何引用。

修改受RCU保护的数据结构的代码必须通过分配一个struct rcu_head数据结构来获取清除用的回调函数。修改完资源之后,应该做如下调用:

void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg);

在可以安全释放该资源时,给定的func会被调用,func通常就是调用kfree。

 

8. 禁止抢占

我们知道,一个自旋锁被持有,内核便不能进行抢占。但是,某些情况下并不需要自旋锁,但是仍然需要关闭内核抢占。比如说,每个处理器上的数据。如果数据对于每个处理器是唯一的,那么,这样的数据可能就不需要使用锁来保护,因为数据只能被一个处理器访问。如果自旋锁没有被持有,内核又是抢占的,那么一个新调度的任务就可能访问一个变量。

为了解决这个问题,可以通过preempt_disable()来禁止内核抢占。这是一个可以嵌套调用的函数,可以调用任意次。每次调用都必须有一个相应的preempt_enable()调用。当最后一次preempt_enable()被调用后,内核抢占才重新启用。例如:

preempt_disable();

/*访问当前处理器的唯一数据*/

preempt_enable();

抢占计数存放着被持有锁的数量和preempt_disable()函数调用的次数,如果计数是0,那么内核就进行抢占,如果为1或更大的值,那么内核就不会进行抢占。

 

9. 顺序和屏障

当处理器和硬件交互时,时常需要确保一个给定的读操作发生在其他读或写操作之前。在多处理器上,可能需要按写数据的顺序读数据。但是编译器和处理器为了提高效率,可能对读和写重新排序。但是,处理器提供了机器指令来确保顺序,指示编译器不要对给定点周围的指令序列进行重新排序。这些确保顺序的指令称为屏障(barrier)。

如果前后的指令没有直接的数据依赖关系,就会被处理器重新排序,但是像下面的例子,处理器和编译器就不会对指令重新排序:

a = 1;

b = a;

函数rmb()提供了一个"读"内存屏障,它确保跨越rmb()的载入动作不会发生重排序。也就是说,在rmb()之前的载入操作不会被重新排在该调用之后。

 

函数wmb()提供了一个"写"内存屏障,与rmb()区别是它仅仅针对存储而非载入。

函数mb()既提供了读屏障也提供了写屏障。

看看下面的例子,其中a的初始值是1,b的初始值是2:

线程1                       线程2

a = 3;                        -

mb();                        _

b = 4;                        c = b;

-                            rmb();

-                            d = a;

如果不使用内存屏障,在某些处理器上,c可能接受了b的新值(4),而d接受了a原来的值(1)。

宏smp_rmb()、smp_wmb()、smp_mb()、smp_read_barrier_depends()提供了一个有用的优化。在SMP内核中它们被定义成常用的内存屏障,而在单处理器内核中,它们被定义成编译器的屏障。方法barrier()可以防止编译器垮屏障对载入或存储操作进行优化。编译器不会重新组织存储或载入操作而防止改变C代码的效果和现有数据的依赖关系。前面讨论的内存屏障可以完成编译器屏障的功能,但是编译器屏障要比内存屏障轻量得多(它实际上是轻快的)。

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值