小结:
内核同步方法:
顺序和屏障
临界区:访问和操作共享数据的代码。竞争条件:两个执行线程有可能在同一个临界区中同时执行。
同步:避免并发和防止竞争条件。
锁的形式和锁的粒度各不相同,各个锁机制之前的主要区别在于:当锁被其他线程持有时,其他的行为表现。
辨识出真正需要共享的数据和相应的临界区才是真正的挑战。
如:一段内核代码操作某资源时产生系统中断,而该中断的处理函数还要访问这个资源。
中断安全代码:在中断处理程序中能避免并发访问的安全代码
SMP安全代码:在SMP中。。。。。。。。。。。。。。。
抢占安全代码:在内核抢占时。。。。。。。。。。。。。
需要保护:大多数内核数据结构需要加锁。记住:给数据而不是给代码加锁。
编写内核代码时,要问自己:
(1). 这个数据是不是全局的?除了当前线程外,其他线程能不能访问它?
(2). 这个数据会不会在进程上下文和中断上下文中共享?是不是要在两个不同的中断处理程序中共享?
(3). 进程在访问数据时可不可能被抢占?被调度的新程序会不会访问同一数据?
(4). 当前进程是不是会睡眠在某些资源上?如果是,它会让共享数据处于何种状态?
(5). 怎样防止数据失控?
(6). 如果这个函数又在另一个处理器上被调度了将会发生什么?
(7). 如何确保代码远离并发威胁呢?
条件
死锁避免:
当锁争用严重时,加锁太粗会降低可扩展性;而锁争用不明显时,加锁过细会加大系统开销,造成浪费。
原子操作:不可分割的指令。
内核提供了2组原子操作接口 - 一组对整数进行操作;另一组针对单独的位进行操作。
大多数系统结构会支持原子操作的简单算术指令,或者通过锁内存总线的方式实现。
最常见用户:实现计数器
特性:开销小
原子整数操作
atomic_t:
atomic_t u = ATOMIC_INIT(0);
atomic_set()
atomic_add()
atomic_inc()
atomic_read()
atomic_dec_and_test()
原子性:确保指令执行期间不被打断(通过原子操作等)
顺序性:确保多条指令出现,本该的顺序性依然要保持(通过屏障barrier)
64位原子操作
atomic64_t
原子位操作
set_bit()
clear_bit()
test_and_set_bit()
对应的非原子位函数,多了两个下划线
__test_bit()
等待锁时,一直循环-旋转-等待。
场景:适合短时间内轻量级锁
警告:自旋锁是不可递归的!
自旋锁和中断处理程序:
自旋锁在中断处理程序中使用时,一定要先禁止本地中断,然后获取锁,否则可能打断当前持有锁的进程,而导致其他进程不能获取锁。
内核提供禁止中断同时请求锁的接口:
DEFINE_SPINLOCK(mr_lock);
unsigned long flags;
spin_lock_irqsave(&mr_lock, flags); //保存中断当前状态,并禁止本地中断,然后获取指定的锁
…
spin_unlock_irqrestore(&mr_lock, flags); // 对指定的锁解锁,然后让中断恢复
调试自旋锁
CONFIG_DEBUG_SPINLOCK选项
其他自旋锁的方法
spin_lock_init():动态创建自旋锁
spin_try_lock():试图获得特定的自旋锁,如锁已被争用,立即返回非0,不等待自旋锁被释放
spin_is_locked():检查锁是否被占用
一个或多个任务可以并发持有读者锁,但写者锁只能有一个。
特点:照顾读。
DEFINE_RWLOCK(mr_rwlock);
read_lock(&mr_rwlock);
…
read_unlock(&mr_rwlock);
write_lock(&mr_rwlock);
write_unlock(&mr_rwlock);
信号量特征:
(1). 信号量适用于锁会被长时间持有的情况,睡眠、维护等待队列及唤醒的开销很大。
(2). 只能在进程上下文中获取信号量锁
(3). 占用信号量的同时不能占用自旋锁。
(4). 往往在需要和用户空间同步时,你的代码需要睡眠,此时使用信号量是唯一选择。
(5). 信号量不同于自旋锁,它不会禁止内核抢占,所有持信号量的代码可以被抢占。
计数信号量和二值信号量
内核使用信号量时基本用到的都是互斥信号量。
创建和初始化信号量
信号量的实现和体系结构相关。
静态:static DECLARE_MUTEX(name)
动态:sema_init(sem, count); 或 init_MUTEX(sem);
使用信号量
down_interruptible():睡眠时可唤醒。TASK_INTERRUPTIBLE
down() :睡眠时不可唤醒。TASK_UNINTERRUPTIBLE
down_trylock():试图获得指定信号量,如果被征用,不等待,直接返回非0值
up()
rw_semaphore,区分读写的信号量。所有读写信号量都是互斥信号量,所有读写锁的睡眠都不会被信号打断。
静态初始化:static DECLARE_RWSEM(name);
动态初始化:init_rwsem(sem);
down()
down_read_trylock()
down_write_trylock()
downgradge_write():动态地将写锁转换成读锁。
mutex,类似计数为1的信号量,但操作接口更简单,实现更高效,使用限制更强。
静态:DEFINE_MUTEX(name);
动态:mutex_init(&mutex);
锁定:mutex_lock(&mutex);
解锁:mutex_unlock(&mutex);
mutex相比信号量的场景更严格:
内核配置:CONFIG_DEBUG_MUTEXES
需求 | 建议加锁方式 |
---|---|
低开销加锁 | 优先使用自旋锁 |
短期锁定 | 优先使用自旋锁 |
长期加锁 | 优先使用互斥体 |
中断上下文加锁 | 使用自旋锁 |
持有锁需睡眠 | 使用互斥体 |
如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量。如子进程执行或退出时,vfork()系统调用使用完成变量唤醒父进程
用法:
静态创建并初始化:DECLARE_COMPLETION(mr_comp);
动态创建并初始化:init_completion()
在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。当特定事件发生后,产生事件的任务调用complete()来发送信号唤醒正在等待的任务。
BKL是一个全局自旋锁,特性:
(1). 持有BKL的任务仍然可以睡眠。因为当任务无法被调度时,锁会被自动丢弃;当任务被调度时,锁会被重新获得。睡眠不会造成任务死锁。
(2). BKL是一种递归锁。
(3). BKL只能在进程上下文中。
(4). BKL在持有时会禁止内核抢占。
用于读写共享数据。实现这种锁主要依靠一个序列计数器,当有疑义的数据被写入时会得到一个锁,并且序列值会增加。在读取数据之前和之后,序列号都被读取,如果序列号相同,说明没有被写操作打断过。此外,如果读数据是偶数,也表明写没发生过。
用法:
DEFINE_SEQLOCK(mr_seq_lock);
write_seqlock(&mr_seq_lock);
write_sequnlock(…)
读时区别很大:
do{
seq = read_seqbegin(&mr_seq_lock);
}while(read_seqretry(&mr_seq_lock,seq));
特点:
(1). 多个读者和少数写者时,seq锁提供轻量级访问
(2). seq锁对写者更有利。
适用场景:
(1). 数据存在很多读者,数据写者很少
(2). 写优先于读
(3). 数据很简单
如jiffies
内核抢占可以使用自旋锁作为非抢占区域的标记。当然,每个处理器上的数据不要锁保护。
禁止内核抢占:preempt_disable()和preempt_enable()
每个处理器上的数据访问问题:get_cpu()和put_cpu():在返回当前处理器号前首先关闭内核抢占。
屏障:保证顺序要求,指示编译器不要对给定点周围的指令进行重新排序。
内存屏障
rmb():提供一个读内存屏障,确保跨越rmb()的载入动作不会发生重排。
wmb():提供一个写内存屏障
mb():提供读写屏障
read_barrier_depends():rmb的变种,只针对后续读操作锁依靠的那些载入。保证屏障前的读操作在屏障后的读操作之前完成。【只针对特定的读】
对应的,宏smp_rmb()、smp_wmb()、smp_mb()、smp_read_barrier_depends()提供了 一个有用的优化。
编译器屏障
barrier():可以防止编译器跨屏障对load和store进行优化。前面的内存屏障可以实现编译器屏障的功能,但编译器屏障更轻量级,它只防止编译器可能重排指令。