1 内核同步介绍
Linux内核同步是为了避免并发和防止竞争条件。其中,
并发:
两个或多个任务在同一个时间段同时发生,但交替执行,一个任务执行一个间隔单位后,换另一个任务执行。
竞争条件:
两个执行线程处于同一个临界区(访问和操作共享数据的代码段)中同时执行。
Linux内核仅支持单一处理器的时候,只有在中断发生或内核代码明确地请求重新调度、执行另一个任务时,数据才可能被并发访问。
Linux内核支持多处理器的时候,内核代码可以同时运行在两个或更多的处理器上,因而,如果不加保护,运行在两个不同处理器上的内核代码完全可能在同一时刻里并发访问共享数据。
Linux内核发展成抢占式内核时,调度程序可以在任何时刻抢占正在运行的内核代码,重新调度其他的进程执行。
并发产生的原因:
原因 | 描述 |
中断 | 任何时刻异步发生,随时打断当前正在执行的代码 |
软中断和tasklet | 任何时刻唤醒软中断和tasklet,随时打断当前正在执行的代码 |
内核抢占 | 内核具有抢占性,使得内核中的任务可能会被另一个任务抢占 |
睡眠、 与用户空间的同步 | 内核执行过程中可能会睡眠,这就会唤醒调度程序从而导致调度一个新的用户进程 |
对称多处理 | 两个或多个处理器可以同时执行代码 |
2 内核为什么需要同步
以一个简单的共享资源为例:一个全局整型变量和一个简单的临界区,其中的操作是将整型变量的值加1.
全局变量 i 1>读取内存上i的值到一个寄存器 2>将寄存器中的值加1 临界区 i++; 3>写回1的新值到内存 |
两个线程同时进入这个临界区,i的初值为7。预期为线程1将i变为8,线程2将i变为9
线程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(9) |
避免该竞争条件的方法:原子地读变量、增加变量、再写回变量
线程1 线程2 增加i(7->8) — — 增加i(8->9) 或者是相反 线程1 线程2 — 增加i(7->8) 增加i(8->9) — |
3 加锁保护
锁提供的机制是:临界区用锁保护,线程试图访问临界区时,需先持有对应的锁。当线程1持有锁时,如果线程2试图持有这把锁,则线程2将一直等待,直到线程1释放这把锁。
找到真正需要共享的数据和相应的临界区是最为关键的,且要给数据而不是代码加锁。
中断安全代码:在中断处理程序中能避免并发访问的安全代码。
SMP安全代码:在对称多处理器(SMP)的机器中能避免并发访问的安全代码。
抢占安全代码:在内核抢占时能避免并发访问的安全代码。
在编写内核代码时,要清楚的问题:
-
这个数据是不是全局的?除了当前线程外,其他线程能不能访问它?
-
这个数据会不会在进程上下文和中断上下文中共享?它是不是要在
两个不同的中断处理程序中共享?
-
进程在访问数据时可不可能被抢占?被调度的新程序会不会访问同一数据?
-
当前进程是不是会睡眠(阻塞)在某些资源上,如果是,它会让共享数据
处于何种状态?
-
怎样防止数据失控?
-
如果这个函数又在另一个处理器上被调度将会发生什么呢?
-
如何确保代码远离并发威胁呢?
4 内核同步方法
4.1 原子操作
原子操作可以保证指令以原子的方式执行,包括原子整数操作和原子位操作。
<1>原子整数操作
原子整数为atomic_t类型的数据,主要用于实现计数器。
atomic_t v;//定义原子整数
atomic_t u = ATOMIC_INIT(0);//定义并初始化
原子整数的操作 | 描述 |
int atomic_read(atomic_t *v) | 原子读 |
void atomic_set(atomic_t *v,int j) | 原子设置 |
void atomic_add(int j,atomic_t *v) | 原子加j |
void atomic_sub(int j,atomic_t *v) | 原子减j |
void atomic_inc(atomic_t *v) | 原子加1 |
void atomic_dec(atomic_t *v) | 原子减1 |
int atomic_dec_and_test(atomic_t *v) | 原子减1,结果为0则返回真 |
… | … |
<2>原子位操作
位操作函数是对普通的内存地址进行操作,参数为一个指针和一个位号。
原子位的操作 | 描述 |
void set_bit(int nr, void *addr) | 原子地设置addr所指对象的第nr位 |
void clear(int nr, void *addr) | 原子地清空addr所指对象的第nr位 |
void change_bit(int nr, void *addr) | 原子地翻转addr所指对象的第nr位 |
int test_and_set_bit(int nr, void *addr) | 原子地设置addr所指对象的第nr位,并返回原先的值 |
int test_and_clear_bit(int nr, void *addr) | 原子地清空addr所指对象的第nr位,并返回原先的值 |
int test_and_change_bit(int nr, void *addr) | 原子地翻转addr所指对象的第nr位,并返回原先的值 |
int test_bit(int nr, void *addr) | 原子地返回addr所指对象的第nr位 |
4.2 自旋锁
如果一个执行线程试图获得一个已经被持有的自旋锁,那么该线程就会一直进行忙循环—旋转—等待锁重新可用。
如果锁未被争用,请求锁的执行线程便能立刻得到它,继续执行。在任意时间,自旋锁都可以防止多于一个的执行线程同时进入临界区。
自旋锁主要是为了防止多处理器并发而引入的,大量应用于中断处理部分。
单处理器环境时,自旋锁仅仅被当做一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,在编译的时自旋锁就会被完全剔除出内核。
特点:
一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间),故而自旋锁不应该被长时间持有。主要用在短期间(锁的持有时间等价于系统的调度等待时间)内进行轻量级加锁。加锁和解锁分别可以禁止和允许内核抢占。持有自旋锁的线程不允许睡眠。自旋锁不可递归。
使用:
<1>静态初始化:
DEFINE_SPINLOCK(mr_lock);其中#defineDEFINE_SPINLOCK(x) spinlock_t x = {1}
<2>动态初始化:
spinlock_t mr_lock;
spin_lock_init(&mr_lock);
<3>自旋锁使用:在非中断处理程序中使用才会安全
spin_lock(&mr_lock);/*禁止抢占*/ /*临界区*/ spin_unlock(&mr_lock); |
<4>自旋锁使用:在中断处理程序中使用
在中断处理程序中,不能使用信号量,因为它会导致睡眠(中断处理程序不允许睡眠是因为其必须尽可能快地运行,以免丢失设备的中断请求),故而只能使用自旋锁。
在中断处理程序中使用自旋锁时,必须在获取锁之前,首先禁止本地中断(在当前处理器上的中断请求),否则,中断处理程序会打断正持有锁的内核代码,可能导致死锁的发生。
DEFINE_SPINLOCK(irq_lock); unsigned long flags;//用于保存中断当前状态:激活或禁止 /*保存中断当前状态、禁止本地中断、加锁*/ spin_lock_irqsave(&irq_lock, flags);/*禁止抢占,禁止本地中断*/ /*临界区*/ spin_unlock_irqrestore(&irq_lock, flags); /*解锁、让中断恢复到加锁之前的状态*/ |
<5>自旋锁方法
4.3 读-写自旋锁
锁的用途可以明确地分为读取和写入两个场景时,可以用读-写自旋锁。但这种锁机制照顾读比照顾些要多。
特点:
读者锁一个或多个读任务可以并发地持有;——共享
写者锁只能被一个写任务持有,且此时不能有并发的读或写操作。——排斥
当读锁被持有时,写操作为了互斥访问只能等待,但读者却可以继续成功占用锁。(照顾读)
自旋等待的写者在所有读者释放锁之前是无法获得锁的。
使用:
<1>静态初始化
DEFINE_RWLOCK(mr_rwlock);
<2>动态初始化
rwlock_t mr_rwlock;
rwlock_init(&mr_rwlock);
<3>读写自旋锁的使用
read_lock(&mr_rwlock); /*临界区(只读)*/ read_unlock(&mr_lock);
write_lock(&mr_rwlock); /*临界区(读写)*/ write_unlock(&mr_rwlock); |
<4>读写自旋锁方法
4.4 信号量
信号量是一种睡眠锁。如果一个任务试图获得一个已被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这是处理器能重获自由,从而去执行其他代码。当持有的信号量被释放后,处于等待队列的那个任务将被唤醒,并获得该信号量。
信号量同时允许的持有者的数量可以在声明信号量时指定.
当数量大于1时,称为计数信号量;当数量为1时,称为二值信号量(或互斥信号量).
使用:
信号量结构体
struct semaphore { atomic_t count; int sleepers; wait_queue_head_t wait; }; |
<1>静态初始化
静态初始化信号量:
struct semaphore name;
sem_init(&name, count);
静态初始化互斥信号量:
DECLARE_MUTEX(name);
//#define DECLARE_MUTEX(name)__DECLARE_SEMAPHORE_GENERIC(name,1)
<2>动态初始化
动态初始化信号量:sema_init(sem, count);//sem是信号量指针
动态初始化互斥信号量:init_MUTEX(sem);//sem是互斥信号量指针
<3>信号量的使用
static DECLARE_MUTEX(mr_sem); down_interruptible(&mr_sem));//信号量不会禁止内核抢占 /*临界区*/ up(&mr_sem); |
<4>信号量方法
4.5 读-写信号量
读-写信号量与信号量的关系如同读-写自旋锁与自旋锁的关系。
只要没有写者,并发持有读锁的读者数不限。在没有读者时只有唯一的写者可以获得写锁。所有的读-写信号量都是互斥信号量。所有的读-写信号量的睡眠都不会被信号打断。
使用:
#include <linux/rwsem.h> struct rw_semaphore { long count; #define RWSEM_UNLOCKED_VALUE 0x00000000 #define RWSEM_ACTIVE_BIAS 0x00000001 #define RWSEM_ACTIVE_MASK 0x0000ffff #define RWSEM_WAITING_BIAS (-0x00010000) #define RWSEM_ACTIVE_READ_BIAS RWSEM_ACTIVE_BIAS #define RWSEM_ACTIVE_WRITE_BIAS (RWSEM_WAITING_BIAS + RWSEM_ACTIVE_BIAS) spinlock_t wait_lock; struct list_head wait_list; }; |
<1>静态初始化
static DECLARE_RWSEM(name);
<2>动态初始化
init_rwsem(struct rw_semaphore *sem);
<3>读-写信号量的使用
Static DECLARE_RWSEM(mr_rwsem); /*试图获取信号量用于读*/ down_read(&mr_rwsem); /*临界区(只读)*/ /*释放信号量*/ up_read(&mr_rwsem); /*试图获取信号量用于写*/ down_write(&mr_rwsem); /*临界区(读和写)*/ /*释放信号量*/ up_write(&mr_rwsem); |
4.6 互斥量
信号量用途更通用,没多少使用限制。这点使得信号量适合用于那些较复杂的、未明情况下的互斥访问。引入更简单的睡眠锁——互斥体(mutex)
互斥体使用情景:
1>任何时刻只有一个任务可以持有mutex
2>在同一进程上下文中上锁和解锁
3>递归上锁和解锁是不允许的
4>当持有一个mutex时,进程不可以退出
5>mutex不能在中断或者下半部中使用
相比信号量优先使用mutex。如果发现不能满足其约束条件,且没有别的选择时在考虑选择信号量。
使用:
互斥量结构体
struct mutex { /* 1: unlocked, 0: locked, negative: locked, possible waiters */ atomic_t count; spinlock_t wait_lock; struct list_head wait_list; #ifdef CONFIG_DEBUG_MUTEXES struct thread_info *owner; const char *name; void *magic; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif }; |
<1>静态初始化
DEFINE_MUTEX(name);
<2>动态初始化
mutex_init(&mutex);
<3>互斥量的使用
mutex_lock(&mutex);
/*临界区*/
mutex_unlock(&mutex);
<4>互斥量的方法
4.7 完成变量
如果一个任务要执行一些工作时,另一个任务就会在完成变量上等待。当这个任务完成工作后,会使用完成变量去唤醒在等待的任务。
使用:
struct completion { unsigned int done; wait_queue_head_t wait; }; |
<1>静态初始化
DECLARE_COMPLETION(mr_comp);
<2>动态初始化
init_completion(&mr_comp);
<3>完成变量的使用
wait_for_completion(struct completion *);//等待完成变量
complete(struct completion *);//完成变量已完成
小结:
1信号量与互斥量:
相比信号量要优先使用互斥量,除非不能满足互斥量的约束时,才考虑选择信号量。
2自旋锁与互斥量: