Linux内核-内核同步方法

Posted by keys961 on March 26, 2019

1. 原子操作

原子操作:保证指令以原子方式执行

内核提供整数和位的原子操作。

1.1. 原子整数操作

只能为类型是atomic_t进行原子整数操作(定义在<linux/types.h>):

typedef struct {
    volatile int counter; //SPARC只能用24位,后8位为锁
} atomic_t

操作可在<asm/atomic.h>找到,这里不细述。

原子操作只保证原子性,不保证顺序性,后者需要用屏障指令实现。

64位的原子整数位atomic64_t,定义和操作基本和上面一样。

1.2. 原子位操作

定义在<asm/bitops.h>。不需要特定的数据类型,操作参数通常为一个指针和一个偏移,即可实现原子位操作。

2. 自旋锁

锁最多只能被一个线程持有,其它线程一直循环等待锁重新可用。

该锁不能长时间持有(否则浪费处理器时间),只适合短期轻量加锁,但避免了上下文切换。

接口定义在<linux/spinlock.h>中,使用只需:

DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* Critical Section */
spin_unlock(&mr_lock);

它也可被中断处理程序使用,使用时必须屏蔽本地CPU中断,内核提供了这样的接口(spin_lock_irq/irqsave(&lock, flag),区别是是否存储本地中断状态)。

自旋锁不可递归(因为不可重入)

3. 读写自旋锁

读之间共享,读写或者写之间互斥,使用方法类似:

DEFINE_RWLOCK(mr_rwlock);
read/write_lock(&mr_rwlock);
read/write_unlock(&mr_rwlock);

注意,锁升级会带来死锁。

同样,内核也提供接口,供中断处理程序使用。

4. 信号量

与自旋锁不同,信号量为0时,其它请求线程会进入阻塞(睡眠)状态,直到计数大于0时被唤醒。

信号量定义在<asm/semaphore.h>struct semaphore中。OS实验中,我也使用了这个东西(虽然用的C的信号量,但本质一样),可看这里

信号量初始为1时,即等价为互斥锁(mutex)。

5. 读写信号量

数据结构为rw_semaphore,定义在<linux/rwsem.h>中。

它的计数为1,只能有1个写入,同时满足读之间共享,读写/写之间互斥的条件。

可通过down_read/write()up_read/write()获取/释放信号量。

6. 互斥体

行为等价于计数为1的信号量,但接口更加简单。

使用:

DEFINE_MUTEX(name);
mutex_init(&mutex);
mutex_lock(&mutex);
/*Critical section*/
mutex_unlock(&mutex);

它:

  • 不可重入(因此不能递归)
  • 不能再中断/下半部中使用
  • 不可被拷贝/手动初始化/重复初始化
  • 只能在一个上下文中加锁和解锁
  • 持有时进程不可退出。

但最好优先使用它。

7. 完成变量

使2个任务同步,即一个任务等待另一个任务完成后再继续。

数据结构为struct completion,数据结构和操作定义在<linux/completion.h>

等待线程调用wait_for_completion(),另一个任务调用completion()唤醒。

8. BLK:大内核锁

一个全局的自旋锁,特性有:

  • 持有BLK时可睡眠,锁会被丢弃,唤醒后锁再次获得;
  • BLK支持递归和重入
  • BLK只能再进程上下文使用
  • 新用户不能使用BLK

它定义在<linux/smp_lock.h>,现在基本不使用。

9. 顺序锁

简称seq锁,定义在<linux/seqlock.h>,提供一种简单的机制,用于读写共享数据。

它维护一个计数器:

  • 写入时,获取锁,计数增加1,释放时,锁再加1(成为偶数)

  • 写操作还需要获取一个锁,用于写写互斥

  • 读取时,不加锁,前后读该锁的计数:

    • 之前的读需要读到偶数,若为奇数,则在进入临界区时要等待

      进入临界区后,是允许写入的,这不同于读写锁

    • 差值为0,则读时没被写操作打断

    • 差值为偶数,说明被写入,则需要重读

unsigned long seq;
do {
    seq = read_seqbegin(&mr_seq_lock);
    /*Read data*/
}while(read_seqretry(&mr_seq_lock, seq));

它适用于多个读者和少数写者,且写优先于读。

10. 禁止抢占

由于内核可抢占,任何进程可以停下来让另一个运行。而某些代码段我们不想被抢占。自旋锁可以解决,但不需要自旋锁时,也可关闭内核抢占:

preempt_disable();
/*preemption disabled*/
preempt_enable();
/*preemption enabled*/

它通过引用计数实现,因此可重入。

11. 顺序屏障

处理多处理器的同步问题是,需要按顺序发出读和写指令,但编译器和CPU会打乱顺序以提高效率(乱序执行),这时候需要内存屏障

读屏障rmb():确保跨越该屏障的读不会重排序(之前的读不会排到后面,同理后面的读不会排到前面)

写屏障wmb():确保跨越该屏障的写不会重排序(之前的写不会排到后面,同理后面的写不会排到前面)

读依赖屏障read_barrier_depends():确保跨越屏障的具有数据依赖的读取动作不会重排序

读写屏障mb():确保跨越屏障的读写操作不会重排序

内核也提高SMP的屏障。

屏障在不同体系结构上,效果会不同。