简要做个笔记,以备忘。
需同步的原因是,我们并发访问了共享资源。我们将访问或操作共享资源的代码段称“临界区”,如果两个执行线程处于同一临界区中同时执行,称“竞争条件”。这里术语执行线程指任何正在执行的代码实例,如一个在内核执行的进程、一个中断处理程序或一个内核线程。
举个简单例子,i++操作。该操作可以转换为下面的机器指令序列:
1.得到当前变量i的值,并保存到一个寄存器。
2.将寄存器的值加1。
3.将i的新值写回到内存中。
当两个线程同时进入这个临界区,若i初值为7,我们期望如下操作:
线程1 线程2
获得 i(7) ——
增加 i(7->8) ——
写回 i(8) ——
—— 获得 i(8)
—— 增加 i(8->9)
—— 写回 i(9)
然而实现情况可能是:
线程1 线程2
获得 i(7) 获得 i(7)
增加 i(7->8) ——
—— 增加 i(7->8)
写回 i(8) ——
—— 写回 i(8)
上面描述中,并发访问共享资源,i值可能出现为8,导致了非预期的功能。
Linux内核中提供了多种方法和接口来解决上面的问题。
1.原子访问
即原子执行获得i,增加i和写回i 3条指令。相关接口示例如下:
#include <linux/types.h>
typedef struct {
volatile int counter;
} atomic_t;
#include <asm/atomic.h>
atomic_t v = ATOMIC_INIT(0); /* 定义v并初始化为0 */
atomic_set(&v, 4); /* v = 4(原子的) */
atomic_add(2, &v); /* v = v+2 = 6(原子的) */
atomic_inc(&v); /* v = v+1 =7(原子的) */
当然这里只演示少部分操作,更多参考man手册。
2.自旋锁
将临界区看作一个房间,将执行线程看作一个人。
自旋锁的意思就是,当执行线程A进入房间(临界区),则将其上锁。此时执行线程B来到门前,发现已经被上锁,则在门前等待址到A开锁出来。
这里一个关键信息是“等待”,这会占用大量CPU时间,因此持有自旋锁的执行线程要尽可能的短。否则会造成性能问题。
与自旋锁有关的接口定义在<linux/spinlock.h>中,其基本使用形式如下:
DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* 临界区…… */
spin_unloc(&mr_lock);
3.信号量
还是上面房间和人的比喻,信号量也是一种锁。
此时,执行线程B来到门前,发现门已被锁。这时它并不等待,而是在门口表格登记一下,找个椅子睡觉去了,这样做有个好处,CPU资源可以空闲出来,而不是浪费在等待。A出来后,发现表格上有名字,就根据名字找到B给他一拳。B醒来去到房间。
信号量的具体实现定义在<asm/semaphore.h>中,使用示例如下:
/* 声明一个信号量,名为mr_sem用于信号量计数 */
static DECLARE_MUTEM(mr_sem);
/* 尝试获取信号量 */
if (down_interruptible(&mr_sem)) {
/* 信号被接收,信号量还未获取 */
}
/* 临界区…… */
up(&mr_sem); /* 释放给定信号量 */
由于信号量在发生争用时,睡眠而非等待,因此可适用于锁被长时间持有的情况。相反若锁持有时间较短时,由于睡眠、维护等待队列以及唤醒所花费的开销可能比锁被占用的全部时间还要长,使用信号量就不适合了。
【读书笔记】《Linux内核设计与实现》内核同步介绍&内核同步方法