当前位置: 首页 > news >正文

[linux][调度] 内核抢占入门 —— 高优先级线程被唤醒时会立即抢占当前线程吗 ?

在支持抢占的内核中,如果高优先级的线程被唤醒的时候,这个时候 cpu 被其它线程占用着,并且正在运行的这个线程的优先级比刚被唤醒的这个线程优先级低。

这个时候,刚唤醒的线程,能直接抢占正在运行的线程吗 ?

不能

在内核抢占中,有两种类型的点,一个是检查点,一个是抢占点。在检查点的地方会做检查,如果需要抢占,那么会设置一个需要抢占的标志,但是在检查点的时候不做真正的抢占;真正的抢占是在抢占点,抢占点会判断检查点中设置的标志,如果需要抢占并且允许抢占的话,那么就会进行抢占调度。

1 两个标志

两个标志分别是抢占计数和重新调度标志,前者表示能不能抢占,后者表示是不是需要抢占。能不能抢占指的是当前正在运行的线程能不能被抢占;需不需要抢占说的是在运行队列中等待的队列是不是优先级更高,如果有优先级更高的线程在等待,那么说明需要抢占。只有两个标志都满足的情况下,也就是说需要抢占并且允许抢占,才会做抢占调度。

1.1 抢占计数 preempt count

抢占计数用来表示当前运行的任务能不能被抢占。抢占计数保存在 struct thread_info 里,linux 中的,一个线程除了 struct task_struct 这样一个进程控制块来维护之外,每个线程都还有一个 struct thread_info。struct thread_info 的定义和 cpu 架构有关系,并不是内核统一的,如下是在 linux 源码中搜索 struct thread_info 结构体的定义,可以看出来,每种 cpu 架构都有自己的定义。

在 struct thread_info 中有一个成员是 preempt_count,对 preempt_count  进行操作的函数是是 __preempt_count_add() 和 __preempt_count_sub()。preempt_count 大于 0,禁止抢占;等于 0 的时候,允许抢占。

include/asm-generic/preempt.h

static __always_inline void __preempt_count_add(int val)
{*preempt_count_ptr() += val;
}static __always_inline void __preempt_count_sub(int val)
{*preempt_count_ptr() -= val;
}

preempt count 分成了 4 段来使用。

bit0 ~ bit7: preempt,表示抢占计数

bit8 ~ bit15: 表示软中断计数

bit16 ~ bit23: 表示硬中断计数

bit24 ~ bit27: 表示有没有 nmi 中断

只要 preempt count 不是 0,那么就不能抢占。所以当前 cpu 在处理软中断、硬中断、nmi 中断的时候,也是不能抢占的。

对 preempt count 的使用,在加自旋锁的时候会关闭抢占,自旋锁解锁的时候会开抢占;其它在显式调用 preempt_disable() 的地方会关闭抢占,显式调用 preempt_enable() 的时候会开抢占。

内核中定义了几个宏,可以判断当前 cpu 处于什么状态。

in_irq(): cpu 正在处理硬中断

in_softirq(): cpu 正在处理软中断

in_interrupt(): 正在处理硬中断,或者软中断,或者 nmi 中断

in_nmi(): cpu 正在处理 nmi 中断

int_task: cpu 当前处在线程上下文

/** Are we doing bottom half or hardware interrupt processing?** in_irq()       - We're in (hard) IRQ context* in_softirq()   - We have BH disabled, or are processing softirqs* in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled* in_serving_softirq() - We're in softirq context* in_nmi()       - We're in NMI context* in_task()	  - We're in task context** Note: due to the BH disabled confusion: in_softirq(),in_interrupt() really*       should not be used in new code.*/
#define in_irq()		(hardirq_count())
#define in_softirq()		(softirq_count())
#define in_interrupt()		(irq_count())
#define in_serving_softirq()	(softirq_count() & SOFTIRQ_OFFSET)
#define in_nmi()		(preempt_count() & NMI_MASK)
#define in_task()		(!(preempt_count() & \(NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))

1.2 重新调度标志 need resched

重新调度标志也是保存在线程的 struct thread_info 中,保存在 flags 字段,标志为 TIF_NEED_RESCHED。如果需要重新调度,那么这个标志是设置到当前正在运行的这个线程的 struct thread_info 中的。

相关的操作函数如下,设置标志,清除标志,判断当前是不是设置了标志。

static inline void set_tsk_need_resched(struct task_struct *tsk)
{set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}static inline void clear_tsk_need_resched(struct task_struct *tsk)
{clear_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}static inline int test_tsk_need_resched(struct task_struct *tsk)
{return unlikely(test_tsk_thread_flag(tsk,TIF_NEED_RESCHED));
}

2 检查点

2.1 线程被唤醒时

当现成被唤醒的时候,这个时候需要检查刚唤醒的这个线程是不是比正在运行的这个线程优先级高,如果是的话,则设置重新调度标志;否则不设置。

唤醒线程的函数是 wake_up_process(),我们可以跟踪这个函数。

wake_up_process()

调用

try_to_wake_up()

调用

ttwu_runnable()

调用

ttwu_do_wakeup()

调用

check_preempt_curr()

在函数 check_preempt_curr() 将刚被唤醒的线程和正在运行的线程进行对比,如果两者属于同一个调度策略,那么调用本策略内的检查函数;如果前者比后者的调度策略优先级高,比如前者是 SCHED_FIFO 的调度策略,后者是 SCHED_NORMAL 的调度策略,这种情况下是需要抢占调度的,那么就会直接设置需要抢占标志。

void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{if (p->sched_class == rq->curr->sched_class)rq->curr->sched_class->check_preempt_curr(rq, p, flags);else if (p->sched_class > rq->curr->sched_class)resched_curr(rq);/** A queue event has occurred, and we're going to schedule.  In* this case, we can save a useless back to back clock update.*/if (task_on_rq_queued(rq->curr) && test_tsk_need_resched(rq->curr))rq_clock_skip_update(rq);
}

rt 调度策略的 check_preempt_curr() 对应的函数是 check_preempt_curr_rt()。

(1)如果刚唤醒的线程优先级比正在运行的线程优先级高,那么直接设置抢占标志,在下一次调度的时候便会进行抢占调度

(2)如果在多核机器上,并且两者的优先级相等,并且刚唤醒的线程是不能迁移的,正在运行的线程是可以迁移的,那么会将正在运行的线程迁移到其它核上运行,在当前核上运行刚唤醒的线程

static void check_preempt_curr_rt(struct rq *rq, struct task_struct *p, int flags)
{if (p->prio < rq->curr->prio) {resched_curr(rq);return;}#ifdef CONFIG_SMP/** If:** - the newly woken task is of equal priority to the current task* - the newly woken task is non-migratable while current is migratable* - current will be preempted on the next reschedule** we should check to see if current can readily move to a different* cpu.  If so, we will reschedule to allow the push logic to try* to move current somewhere else, making room for our non-migratable* task.*/if (p->prio == rq->curr->prio && !test_tsk_need_resched(rq->curr))check_preempt_equal_prio(rq, p);
#endif
}

SCHED_NORMAL 调度策略的检查函数是 check_preempt_wakeup()。

2.2 tick

每种调度策略都要实现一个函数 task_tick(),这个函数是定时触发。在 task_tick 函数中也会检查,当前任务是不是需要抢占。rt 调度策略的 tick 函数是 task_tick_rt(),普通调度策略的 tick 函数是 task_tick_fair()。

task_tick_rt():

static void task_tick_rt(struct rq *rq, struct task_struct *p, int queued)
{struct sched_rt_entity *rt_se = &p->rt;update_curr_rt(rq);update_rt_rq_load_avg(rq_clock_pelt(rq), rq, 1);watchdog(rq, p);/** RR tasks need a special form of timeslice management.* FIFO tasks have no timeslices.*/// 如果是 SCHED_FIFO,直接返回if (p->policy != SCHED_RR)return;// 如果 SCHED_RR 的时间片还没用完,直接返回// 时间片用完了,才会设置抢占标志if (--p->rt.time_slice)return;p->rt.time_slice = sched_rr_timeslice;/** Requeue to the end of queue if we (and all of our ancestors) are not* the only element on the queue*/for_each_sched_rt_entity(rt_se) {if (rt_se->run_list.prev != rt_se->run_list.next) {requeue_task_rt(rq, p, 0);resched_curr(rq);return;}}
}

对于 SCHED_NORMAL 普通调度策略来说,检查是不是需要抢占的实现在函数 check_preempt_tick() 中。

当前线程有一个最小的运行时间,为 0.75ms,如果当前这个线程的运行时间还不足 0.75ms,那么不会设置抢占标志。当实际运行时间大于 0.75ms 的时候,才会设置抢占标志。


static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{unsigned long ideal_runtime, delta_exec;struct sched_entity *se;s64 delta;ideal_runtime = sched_slice(cfs_rq, curr);delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;if (delta_exec > ideal_runtime) {resched_curr(rq_of(cfs_rq));/** The current task ran long enough, ensure it doesn't get* re-elected due to buddy favours.*/clear_buddies(cfs_rq, curr);return;}/** Ensure that a task that missed wakeup preemption by a* narrow margin doesn't have to wait for a full slice.* This also mitigates buddy induced latencies under load.*/if (delta_exec < sysctl_sched_min_granularity)return;se = __pick_first_entity(cfs_rq);delta = curr->vruntime - se->vruntime;if (delta < 0)return;if (delta > ideal_runtime)resched_curr(rq_of(cfs_rq));
}

3 抢占点

由第一节可以知道,在加自旋锁时,软中断中,处理硬件中断时,这些时候都是禁止了抢占的,那么当 cpu 退出这些区域的时候便会检查当前是不是需要抢占,如果需要抢占并且允许抢占的话,便会抢占。

3.1 释放自旋锁的时候

spin_unlock() 最终会调用到函数 preemt_enable(),使能抢占。在 preempt_enable() 函数中会做判断,判断两个标志,需要抢占标志和抢占计数,如果抢占计数为 0 并且需要抢占,那么便会进行抢占调度。

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{spin_release(&lock->dep_map, _RET_IP_);do_raw_spin_unlock(lock);preempt_enable();
}#define preempt_enable() \
do { \barrier(); \if (unlikely(preempt_count_dec_and_test())) \__preempt_schedule(); \
} while(0)#define preempt_count_dec_and_test() \({ preempt_count_sub(1); should_resched(0); })

3.1 打开软中断时

在 linux 内核中,当数据会被线程和软中断并发访问时,在线程中加锁时需要关闭软中断。关闭软中断和打开软中断的函数如下。在打开软中断时,会进行判断,如果需要抢占并且允许抢占,便会进行抢占调度。

static inline void local_bh_disable(void)
{__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}static inline void local_bh_enable(void)
{__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}

3.3 中断退出的时候

中断返回的时候,如果需要抢占调度,那么会调用函数 preempt_schedule_irq()。这段代码一般是使用汇编指令来实现的。如下是 arm 中的实现,下边这段代码,只有定义了 CONFIG_PREEMPTION 时,才会生效。

arch/arm/kernel/entry-armv.S

#ifdef CONFIG_PREEMPTION
svc_preempt:mov	r8, lr
1:	bl	preempt_schedule_irq		@ irq en/disable is done insideldr	r0, [tsk, #TI_FLAGS]		@ get new tasks TI_FLAGStst	r0, #_TIF_NEED_RESCHEDreteq	r8				@ go againb	1b
#endif

相关文章:

  • PCM /G711音频播放器 :Audacity
  • js 分割号查找内容
  • Unity 镜头动画
  • Rust 实战练习 - 8. 内存,ASM,外挂 【重磅】
  • HJ73 计算日期到天数转换
  • 为什么要使用注意力机制?
  • ip 协议
  • gateway 分发时若两个服务的路由地址一样,怎么指定访问想要的服务下的地址
  • Mysql-实战数据备份与恢复
  • 何时需要指定泛型:Scala编程指南
  • 如何使用python链表
  • Java基础知识总结(26)
  • linux debian运行pip报错ssl tsl module in Python is not available
  • ethers.js:sign(签名)
  • 【第十二届“泰迪杯”数据挖掘挑战赛】【2024泰迪杯】B题基于多模态特征融合的图像文本检索—解题全流程(持续更新)
  • 78. Subsets
  • Date型的使用
  • download使用浅析
  • express如何解决request entity too large问题
  • JavaScript HTML DOM
  • js中的正则表达式入门
  • k8s 面向应用开发者的基础命令
  • PHP 的 SAPI 是个什么东西
  • springboot_database项目介绍
  • Swift 中的尾递归和蹦床
  • vagrant 添加本地 box 安装 laravel homestead
  • VUE es6技巧写法(持续更新中~~~)
  • 聊聊spring cloud的LoadBalancerAutoConfiguration
  • 模仿 Go Sort 排序接口实现的自定义排序
  • 爬虫模拟登陆 SegmentFault
  • 区块链共识机制优缺点对比都是什么
  • 如何在 Tornado 中实现 Middleware
  • 微信开放平台全网发布【失败】的几点排查方法
  • 以太坊客户端Geth命令参数详解
  • 我们雇佣了一只大猴子...
  • ​​​​​​​​​​​​​​汽车网络信息安全分析方法论
  • ​LeetCode解法汇总307. 区域和检索 - 数组可修改
  • ​一、什么是射频识别?二、射频识别系统组成及工作原理三、射频识别系统分类四、RFID与物联网​
  • #图像处理
  • $con= MySQL有关填空题_2015年计算机二级考试《MySQL》提高练习题(10)
  • (22)C#传智:复习,多态虚方法抽象类接口,静态类,String与StringBuilder,集合泛型List与Dictionary,文件类,结构与类的区别
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (Matalb分类预测)GA-BP遗传算法优化BP神经网络的多维分类预测
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (待修改)PyG安装步骤
  • (翻译)Quartz官方教程——第一课:Quartz入门
  • (附源码)springboot 个人网页的网站 毕业设计031623
  • (六)激光线扫描-三维重建
  • (强烈推荐)移动端音视频从零到上手(上)
  • (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境
  • (一)python发送HTTP 请求的两种方式(get和post )
  • (一)spring cloud微服务分布式云架构 - Spring Cloud简介
  • (转)h264中avc和flv数据的解析
  • (转)jdk与jre的区别
  • (转)利用PHP的debug_backtrace函数,实现PHP文件权限管理、动态加载 【反射】...