前言
📫作者简介:小明java问道之路,专注于研究计算机底层/Java/Liunx 内核,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计📫
🏆 InfoQ 签约博主、CSDN 专家博主/Java 领域优质创作者/CSDN 内容合伙人、阿里云专家/签约博主、华为云专家、51CTO 专家/TOP 红人 🏆
🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~
本文导读
内核抢占就是允许正在内核中执行的任务抢占另一个正在内核中执行的任务。本文详解 Linux 内核抢占原理以及内核源码实现过程。
一、Linux 内核抢占原理
1、内核抢占原理
何为内核抢占呢?就是允许正在内核中执行的任务抢占另一个正在内核中执行的任务。
在 Linux 内核 2.6 版本之前,陷入内核执行的任务无法被抢占,即使有比它优先级高的任务也无被抢占,只能设置重调度标记位,在退出内核空间时才能抢占它。
这样做的结果就是极大地降低了的性能,于是在 Linux 内核 2.6 版本开始支持内核抢占。这就显著地提高了性能,并且大幅提升响应能力。当内核处理中断和异常或者打开内核强占时,将会响应内核强占,此时会调用调度器 preempt schedule 方法完成强占动作。下面看看内核抢占的相关操作。
2、Linux 内核抢占源码解析
可以看到,一个任务是否可以被抢占是通过一个 preempt_count 变量来控制的,并且在 scheduler 调度器中提供了 preempt schedule()抢占调度操作。
// 在每个任务的thread_info里维护了一个preemptcount,当它等于0时是可以被抢占的
#define preempt_count()(currentthread info()->preemptcount)
// 增加 preempt_count
#define inc_preempt_count()
do {
preempt_count()++;
} while (0)
// 减少preempt_count
#define dec_preempt_count()
do {
preempt_count()--;
} while (0)
// 如果启用了内核抢占机制,那么以下宏定义生效
#ifdef CONFIG PREEMPT
extern void preempt_schedule(void);
// 设置当前任务禁止被抢占
#define preempt_disable()
do {
inc_preempt_count();
barrier();
} while (0)
// 设置当前任务可以被抢占
#define preempt enable_no resched()
do {
barrier();
dec_preempt_count();
} while (0)
// 抢占调度
#define preempt_check_resched()
do{
if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
preempt_schedule();
} while (0)
// 设置当前任务可以被抢占且马上尝试抢占调度
#define preempt_enable()
do {
preempt_enable_no_resched();
preempt_check_resched();
} while (0)
// 如果没有配置内核抢占,这些操作默认为空操作
#else
#define preempt_disable() do { } while (0)
#define preempt_enable_no_resched() do{}while(0)
#define preempt_enable() do {} while (0)
#define preempt_check_resched() do { } while (0)
#endif
#endif
复制代码
二、Linux 内核自旋锁原理
1、Linux 内核自旋锁源码解析
想一想,在 SMP 中的操作和内核任务抢占的操作是不是一样的?
多个任务可以并发或者并行执行,所以内核在实现多任务并发安全处理时都是用的同一套操作。是时候展示暴露出去的可供调用的自旋锁了。
//如果配置SMP和内核抢占机制,那么以下宏定义生效,在我们描述完该内容后,再看看这两个函数做了什么
#if defined(CONFIG SMP)&& defined(CONFIG_PREEMPT)
void_preempt_spin_lock(spinlock_t*lock);
void_preept_write_lock(rwlock_t *lock);
#define spin_lock(lock)
do {
preempt_disable();
if(unlikely(!_raw_spin_trylock(lock)))
__preempt_spin_lock(lock);
} while (0)
#define write_lock(lock)
do {
preempt_disable();
if(unlikely(!raw_write_trylock(lock)))
preempt_write_lock(lock);
} while (0)
#else // 如果没有定义,那么自旋锁不包含_preemp_spin_lock和_preempt_write_lock
#define spin_lock(lock)
do {
// 可以看到,调用禁止抢占函数和原始自旋锁_raw_spin_lock
// 意味着上了自旋锁不允许被抢占。想相为什么。如果任务A持有自旋锁
// 任务B抢占了A,B要获取自旋锁,但自旋锁被A持有,那么B只能一直自旋
// 这显然造成了死锁。怎么强占的呢?读者可以考虑中断机制和 TIF_NEED_RESCHED标志位
preempt_disable();
raw_spin_lock(lock);
} while(0)
#define write_lock(lock)
do {
preempt_disable();
_raw_write_lock(lock);
} while(0)
#endif
#define read_lock(lock)
do {
preempt_disable();
_raw_read_lock(lock);
} while(0)
// 同理开启了强占功能,那么在释放锁时需要打开强占
#define spin_unlock(lock)
do {
raw_spin_unlock(lock);
preempt_enable();
} while (0)
#define write_unlock(lock)
do {
_raw_write_unlock(lock);
preempt_enable();
} while(0)
#define read_unlock(lock)
do {
raw read unlock(lock);
preempt_enable();
} while(0)
// 关闭抢占,获取自旋锁时保存EFLAGS,也就是说上锁前后需要保证EFLAGS不变
#define spin_lock_irqsave(lock,flags)
do {
local_irq_save(flags);
preempt_disable();
_raw_spin_lock(lock);
} while (0)
// 不保存 EFLAGS 状态,而是直接关闭中断、关闭抢占,因此这个宏定义绝对安全
#define spin_lock_irq(lock)
do {
local_irq_disable();
preempt_disable();
raw_spin_lock(lock);
} while (0)
复制代码
可以看到最后都是调用我们之前讲过的自旋锁代码,包括读写自旋锁,但是这里的锁类型被分为 3 类,即 spin_lock(关闭抢占,但是不关闭中断的自旋锁)、spin_lock_irqsave(关闭抢占并保存 EFLAGS,但是不关闭中断的自旋锁)、spin_lock_irq(关闭抢占、关闭中断的自旋锁)。
关于这 3 类锁类型的操作,说明如下。
1、如果用 spin_lock 函数,请不要在可中断的上下文中调用,因为一旦中断后,中断处理程序就令使用这个自旋锁,进而发生死锁。
2、如果用 spin_lock_irqsave 函数,也请不要在可中断的上下文中调用,因为一旦中断后,中断处理程序就会使用这个自旋锁,进而发生死锁,这个函数和 spin_lock 相比仅仅多了保存和还原 EFLAGS 操作。
3、对于 spin lock_irq(lock) 函数,可任意使用,它是绝对安全的,因为不会有任务来抢占,也不
会有中断发生。
这里我们来收尾上面的判断宏#if defined(CONFIG SMP)&&defined(CONFIGPREEMPT)
,如果使用了 SMP 对称多处理器的架构且打开了任务抢占,那么多出了 void_preempt_spin_lock(spinlock_t*lock);
void_preempt_write lock(rwlock_t*lock);
两个函数。其对应实现如下。
2、抢占自旋锁
#define spin_lock(lock)
do {
preempt_disable(); // 先关抢占
// 如果尝试获取锁失败,即上锁失败,则调用preemptspin_lock函数。想想这是在本地CPU上,
// 关闭了抢占,肯定不会有其他任务来抢占。然而,是哪个任务在争用锁呢?显然就是其他CPU上的任务
if(unlikely(!_raw_spin_trylock(lock)))
__preempt_spin_lock(lock);
} while (0)
#define write_lock(lock)
do {
preempt disable(); // 先关抢占
//尝试获取写锁,如果上锁失败,则调用_preempt_write_lock函数
if(unlikely(!_raw_write_trylock(lock)))
__preempt_write_lock(lock);
} while (0)
复制代码
3、抢占写锁
void__preempt_spin_lock(spinlock_t *lock) {
// 当两次以上调用preempt_disable()后,直接调用_raw_spin_lock等待自旋锁,因为每调用
// 一次_raw_spin_lock,都会将count加1,也就是发生了禁止抢占嵌套。
// 例如,任务A先通过其他方式关闭了抢占,然后又获取了这个自旋锁,那么这里的count大于1就不难理解了
if (preempt_count()>1) {
_raw_spin_lock(lock);
return;
}
// 否则打开抢占,然后一直调用spin_is_locked判断自旋锁是否解锁,如果解锁了,那么关闭中断再次尝试// 获取锁
do {
preempt_enable();
while(spin_is_locked(lock))
// 这里相当于一个空操作
// #define cpu relax() repnop(),
// 即asmvolatile_("rep;nop": :"memory");。这用于优化 CPU的自旋操作
cpu_relax();
preempt_disable();
} while (!_raw_spin_trylock(lock));
}
复制代码
总结
代码相当简单,但是我们要领会其中的技巧,为何这样设计?这是由于开启的 SMP 多处理器将会同时执行任务,这时虽然关闭了抢占,但这个自旋锁可能被其他任务在其他 CPU 中执行,这里的执行可不一定占用 CPU,有可能时间片用完,也有可能正在执行耗时操作,一时无法释放获取的自旋锁,而这时又禁止了抢占,这样岂不是会让本地 CPU 无法运行其他任务而导致系统变得卡顿?这时就直接开启抢占,允许其他任务可以抢占这个任务,并且一直判断自旋锁是否解锁,如果解锁了,那么再次抢占回来尝试上锁。这时理解之前的判断 preempt_count()>1 就较为容易,这里已经禁止抢占两次,即使再解锁一次也无作用,因为其他任务已不可能强占。
评论