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

本文主要是介绍[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

 

这篇关于[linux][调度] 内核抢占入门 —— 高优先级线程被唤醒时会立即抢占当前线程吗 ?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/838777

相关文章

Spring Boot + MyBatis Plus 高效开发实战从入门到进阶优化(推荐)

《SpringBoot+MyBatisPlus高效开发实战从入门到进阶优化(推荐)》本文将详细介绍SpringBoot+MyBatisPlus的完整开发流程,并深入剖析分页查询、批量操作、动... 目录Spring Boot + MyBATis Plus 高效开发实战:从入门到进阶优化1. MyBatis

Linux卸载自带jdk并安装新jdk版本的图文教程

《Linux卸载自带jdk并安装新jdk版本的图文教程》在Linux系统中,有时需要卸载预装的OpenJDK并安装特定版本的JDK,例如JDK1.8,所以本文给大家详细介绍了Linux卸载自带jdk并... 目录Ⅰ、卸载自带jdkⅡ、安装新版jdkⅠ、卸载自带jdk1、输入命令查看旧jdkrpm -qa

Linux samba共享慢的原因及解决方案

《Linuxsamba共享慢的原因及解决方案》:本文主要介绍Linuxsamba共享慢的原因及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux samba共享慢原因及解决问题表现原因解决办法总结Linandroidux samba共享慢原因及解决

Spring Boot3虚拟线程的使用步骤详解

《SpringBoot3虚拟线程的使用步骤详解》虚拟线程是Java19中引入的一个新特性,旨在通过简化线程管理来提升应用程序的并发性能,:本文主要介绍SpringBoot3虚拟线程的使用步骤,... 目录问题根源分析解决方案验证验证实验实验1:未启用keep-alive实验2:启用keep-alive扩展建

新特性抢先看! Ubuntu 25.04 Beta 发布:Linux 6.14 内核

《新特性抢先看!Ubuntu25.04Beta发布:Linux6.14内核》Canonical公司近日发布了Ubuntu25.04Beta版,这一版本被赋予了一个活泼的代号——“Plu... Canonical 昨日(3 月 27 日)放出了 Beta 版 Ubuntu 25.04 系统镜像,代号“Pluc

mysql如何查看当前连接数

《mysql如何查看当前连接数》:本文主要介绍mysql如何查看当前连接数问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录mysql查看当前连接数查看mysql数据库允许最大连接数总结mysql查看当前连接数查看当前连接数SHOW STATUS LIKE

Linux安装MySQL的教程

《Linux安装MySQL的教程》:本文主要介绍Linux安装MySQL的教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux安装mysql1.Mysql官网2.我的存放路径3.解压mysql文件到当前目录4.重命名一下5.创建mysql用户组和用户并修

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

Linux上设置Ollama服务配置(常用环境变量)

《Linux上设置Ollama服务配置(常用环境变量)》本文主要介绍了Linux上设置Ollama服务配置(常用环境变量),Ollama提供了多种环境变量供配置,如调试模式、模型目录等,下面就来介绍一... 目录在 linux 上设置环境变量配置 OllamPOgxSRJfa手动安装安装特定版本查看日志在

Linux系统之主机网络配置方式

《Linux系统之主机网络配置方式》:本文主要介绍Linux系统之主机网络配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、查看主机的网络参数1、查看主机名2、查看IP地址3、查看网关4、查看DNS二、配置网卡1、修改网卡配置文件2、nmcli工具【通用