内核同步的实现
原子操作
内核提供了两组原子操作的接口,一组对整数进行操作,一组对单独的位进行操作。
1.原子的整数操作
整数的原子操作只对atomiac_t
类型的数据进行处理,好处是让原子函数只接收atomic_t
类型的数据,也保证了该类型的数据不传递给其他函数。另一个好处是使得原子操作最终接收到的是内存地址。
//atomic_t 定义在 <linux/types.h>:
typedef struct {
volatile int counter;
} atomic_t;
//所有的原子整数操作都声明在 <asm/atomic.h>
ATOMIC_INIT(int i) //初始化
atomic_t u = ATOMIC_INIT(0); /*初始化并赋值 */
//这两个可以实现计数器
void atomic_inc(atomic_t *v) //Atomically add one to v.
void atomic_dec(atomic_t *v) //Atomically subtract one from v.
int atomic_read(atomic_t *v) //Atomically read the integer value of v. 可以用来转换成 int
void atomic_set(atomic_t *v, int i) //Atomically set v equal to i.
//执行一个操作然后返回结果
int atomic_dec_and_test(atomic_t *v) //减1 如果是0 返回TRUE
原子操作通常是内联函数,通常通过内嵌汇编指令实现。如果函数本身就是原子的,往往被定义为一个宏。读操作本身就是一个原子操作,所以atomic_read()
就是一个宏。
64位的原子变量 atomic_t
和atomic64_t
由于移植性原因,atomic_t
的大小无法在体系机构间改变,要使用64的原子变量的话就需要使用atomic64_t
。和atomic_t
一样,atomic64_t
也有一样的原子操作函数。
2.原子位操作
- 位操作函数是对普通的内存地址进行操作的,参数是一个指针和一个位号。
- 位操作是对普通的指针进行的,所以没有特定的数据类型。只要指针指向了你想要的数据,就可以进行操作。
原子位操作函数
void set_bit(int nr, void *addr) #Atomically set the nr-th bit starting from addr.
set_bit(0, &word); # 设置 word 的第0位
void clear_bit(int nr, void *addr) #Atomically clear the nr-th bit starting from addr.
void change_bit(int nr, void *addr) #Atomically flip the value of the nr-th bit starting from addr.
int test_and_set_bit(int nr, void *addr) #Atomically set the nr-th bit starting from addr and return the previous value.
int test_and_clear_bit(int nr, void *addr) #Atomically clear the nr-th bit starting from addr and return the previous value.
int test_and_change_bit(int nr, void *addr) #Atomically flip the nr-th bit starting from addr and return the previous value.
int test_bit(int nr, void *addr)# Atomically return the value of the nrth bit starting from addr.
自旋锁(spin lock)
并不是所有的临界区只处理变量的加减,有的临界区需要处理复杂的数据结构,且跨越多个函数。例如将一个数据结构的数据拿出并转换格式和解析放到另外一个数据结构中。这时候就需要锁来提供保护。自旋锁是内核中最常见的,自旋锁最多被一个运行线程持有,等待的线程会一直处于自旋的状态(loops— spins—waiting)。
信号量:自旋的状态一直消耗 CPU,适合短时间的锁。另一种形式就是让后来的线程不循环等待,去执行其他代码。这样会带来开销,并进行2次上下文切换。
自旋锁的实现
自旋锁的实现与体系结构相关,通过汇编实现。linux 的自旋锁是不可递归的。
//体系结构相关代码 <asm/spinlock.h>
//可用接口代码 <linux/spinlock.h>
// 自旋锁的基本形式
DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* critical region ... */
spin_unlock(&mr_lock);
中断处理程序可以使用自旋锁(也只可以使用自旋锁),当时在获取锁之前,一定要禁止本地中断(当前 CPU 的中断请求),否则,中断会打断已经获取锁的内核代码,造成死锁。在不同 CPU 上不需要。
关于自旋锁和下半部,下半部会抢占进程上下文,需要在加锁的同时禁止下半部,同样中断也会抢占下半部,下半部获取锁后也需要禁止中断。tasklet 和 软中断不会抢占另外的 tasklet 和 软中断。不需要禁用下半部。
//禁止中断同时获取锁的 接口,会保存中断的状态,没有**的中断不会被**
spin_lock_irqsave(&mr_lock, flags);
/* critical region ... */
spin_unlock_irqrestore(&mr_lock, flags);
//明确中断是**的状态下,用以下代码,解锁后直接**。
spin_lock_irq(&mr_lock);
/* critical section ... */
spin_unlock_irq(&mr_lock);
内核配置选项 CONFIG_DEBUG_SPINLOCK 和 CONFIG_DEBUG_LOCK_ALLOC 帮助调试锁操作
自旋锁的函数
函数 | 描述 |
---|---|
spin_lock() | Acquires given lock |
spin_lock_irq() | Disables local interrupts and acquires given lock |
spin_lock_irqsave() | Saves current state of local interrupts, disables local inter- rupts, and acquires given lock |
spin_unlock() | Releases given lock |
spin_unlock_irq() | Releases given lock and enables local interrupts |
spin_unlock_irqrestore() | Releases given lock and restores local interrupts to given pre- vious state |
spin_lock_init() | Dynamically initializes given spinlock_t 获得一个spinlock_t类型的指针 |
spin_trylock() | Tries to acquire given lock; if unavailable, returns nonzero |
spin_is_locked() | Returns nonzero if the given lock is currently acquired, other- wise it returns zero |
读写自旋锁
一个链表读操作是可以并发的,写操作只能一个进程执行,并且在写的时候不允许读,这种就可以用读写自旋锁。
但是有可能多个读者一直占用锁,造成写者等待过久。
//初始化
DEFINE_RWLOCK(mr_rwlock);
//Then, in the reader code path:
read_lock(&mr_rwlock);
/* critical section (read only) ... */
read_unlock(&mr_rwlock);
//Finally, in the writer code path:
write_lock(&mr_rwlock);
/* critical section (read and write) ... */
write_unlock(&mr_lock);
//下面代码会带来死锁,因为写锁会一直等待读锁释放。
read_lock(&mr_rwlock); write_lock(&mr_rwlock);
如果你在中断处理程序使用 read_lock()
而不是read_lock_irqsave()
那需要额外禁止写操作中断write_lock_irqsave()
,否则会造成死锁。
信号量
Linux 中的信号量是一种睡眠锁,当任务获取一个被占用的信号量时,回进入等待队列进行睡眠,CPU 去执行其他任务。
- 在占有信号量的同时不能占有自旋锁。等待信号量的时候回去睡眠。
- 信号量不会禁止内核抢占。
计数信号量和二值信号量
二值信号量:同时最多一个任务持有信号量锁。常用,也叫互斥信号量。
计数信号量:允许同一时刻多个任务持有信号量,在声明的时候指定数量,用的不多。
信号量的获取与释放
两个原子操作 P()/down() 和 V()/up() ,down()操作后,计数减1,大于等于0,获取锁,小于0进入队列并睡眠。临界区操作完成后,up()释放信号量计数加1.
读写信号量 几乎和读写自旋锁一样,提供了一个down_read_trylock()
转换读写锁。
信号量的实现
//信号量的实现与体系结构相关 代码在<asm/semaphore.h>
创建和初始化:
// count 是计数
struct semaphore name;
sema_init(&name, count);
//创建互斥信号量
static DECLARE_MUTEX(name);
//动态创建
sema_init(sem, count);
init_MUTEX(sem);
信号量的使用:
/* define and declare a semaphore, named mr_sem, with a count of one */
static DECLARE_MUTEX(mr_sem);
/* attempt to acquire the semaphore ... */
if (down_interruptible(&mr_sem)) {
/*
down_interruptible(&mr_sem) 获取信号量,如果不可用,是进程进入睡眠TASK_INTERRUPTIBLE
signal received, semaphore not acquired ... */
}
/* critical region ... */
/* release the given semaphore */
up(&mr_sem);
互斥体(mutex)
更简单的睡眠锁,指的是任何可以睡眠的强制互斥锁(计数只能是1),比如计数为1的信号量。
mutex 对应数据结构是 mutex,操作接口简单,实现高效,使用限制强。
函数 | 描述 |
---|---|
DEFINE_MUTEX(name); | 静态定义 |
mutex_init(&mutex); | 动态定义 |
mutex_lock(struct mutex *) | Locks the given mutex; sleeps if the lock is unavailable |
mutex_unlock(struct mutex *) | Unlocks the given mutex |
mutex_trylock(struct mutex *) | Tries to acquire the given mutex; returns one if suc- cessful and the lock is acquired and zero otherwise |
mutex_is_locked (struct mutex *) | Returns one if the lock is locked and zero otherwise |
- mutex
不能在中断和下半部使用
- 持有mutex
的进程不能退出
- 首选mutex
,如果不能满足其约束条件,在使用信号量。
锁的选择
需求 | 选择的加锁方式 |
---|---|
Low overhead locking | Spin lock |
Short lock hold time | Spin lock |
Long lock hold time | Mutex |
Need to lock from interrupt context | Spin lock |
Need to sleep while holding lock | Mutex |
其他锁
完成变量(Completion Variables)
Completion Variables 一个任务在等待另一个任务完成,这个任务完成工作后唤醒等待的任务。vfork()就是使用这个来唤醒父进程。
顺序锁(seq)
一种新型锁,依靠序列计数器,写操作会+1 ,读数据前后会读取这个数,值相同代表没有写入。
- jiffies_64 使用的是 seq 锁
u64 get_jiffies_64(void)
{
unsigned long seq; u64 ret;
do {
seq = read_seqbegin(&xtime_lock);
ret = jiffies_64;
} while (read_seqretry(&xtime_lock, seq));
return ret;
}
// 更新定时器的写锁
write_seqlock(&xtime_lock);
jiffies_64 += 1;
write_sequnlock(&xtime_lock);
禁止抢占
单CPU 可能不需要自旋锁,这时候会遇到伪并发,抢占进程操作同一个数据的情况。
preempt_disable(); //禁止内核抢占,抢占计数+1
/* preemption is disabled ... */
preempt_enable();
顺序和屏障
程序代码编译后,在 cpu 中执行的顺序可能会被 CPU 重新排序,可以用屏障的方法来固定顺序。
函数 | 描述 |
---|---|
rmb() | Prevents loads from being reordered across the barrier |
read_barrier_depends() | Prevents data-dependent loads from being re- ordered across the barrier |
wmb() | Prevents stores from being reordered across the barrier |
mb() | Prevents load or stores from being reordered across the barrier |
barrier() | Prevents the compiler from optimizing stores or loads across the barrier |