[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

相关文章

linux生产者,消费者问题

pthread_cond_wait() :用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。 pthread_cond_wait() 必须与pthread_mutex 配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread

Linux 安装、配置Tomcat 的HTTPS

Linux 安装 、配置Tomcat的HTTPS 安装Tomcat 这里选择的是 tomcat 10.X ,需要Java 11及更高版本 Binary Distributions ->Core->选择 tar.gz包 下载、上传到内网服务器 /opt 目录tar -xzf 解压将解压的根目录改名为 tomat-10 并移动到 /opt 下, 形成个人习惯的路径 /opt/tomcat-10

RedHat运维-Linux文本操作基础-AWK进阶

你不用整理,跟着敲一遍,有个印象,然后把它保存到本地,以后要用再去看,如果有了新东西,你自个再添加。这是我参考牛客上的shell编程专项题,只不过换成了问答的方式而已。不用背,就算是我自己亲自敲,我现在好多也记不住。 1. 输出nowcoder.txt文件第5行的内容 2. 输出nowcoder.txt文件第6行的内容 3. 输出nowcoder.txt文件第7行的内容 4. 输出nowcode

【Linux进阶】UNIX体系结构分解——操作系统,内核,shell

1.什么是操作系统? 从严格意义上说,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核(kerel),因为它相对较小,而且位于环境的核心。  从广义上说,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特生。这里所说的其他软件包括系统实用程序(system utility)、应用程序、shell以及公用函数库等

C++必修:模版的入门到实践

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:C++学习 贝蒂的主页:Betty’s blog 1. 泛型编程 首先让我们来思考一个问题,如何实现一个交换函数? void swap(int& x, int& y){int tmp = x;x = y;y = tmp;} 相信大家很快就能写出上面这段代码,但是如果要求这个交换函数支持字符型

零基础STM32单片机编程入门(一)初识STM32单片机

文章目录 一.概要二.单片机型号命名规则三.STM32F103系统架构四.STM32F103C8T6单片机启动流程五.STM32F103C8T6单片机主要外设资源六.编程过程中芯片数据手册的作用1.单片机外设资源情况2.STM32单片机内部框图3.STM32单片机管脚图4.STM32单片机每个管脚可配功能5.单片机功耗数据6.FALSH编程时间,擦写次数7.I/O高低电平电压表格8.外设接口

Windows/macOS/Linux 安装 Redis 和 Redis Desktop Manager 可视化工具

本文所有安装都在macOS High Sierra 10.13.4进行,Windows安装相对容易些,Linux安装与macOS类似,文中会做区分讲解 1. Redis安装 1.下载Redis https://redis.io/download 把下载的源码更名为redis-4.0.9-source,我喜欢跟maven、Tomcat放在一起,就放到/Users/zhan/Documents

ps基础入门

1.基础      1.1新建文件      1.2创建指定形状      1.4移动工具          1.41移动画布中的任意元素          1.42移动画布          1.43修改画布大小          1.44修改图像大小      1.5框选工具      1.6矩形工具      1.7图层          1.71图层颜色修改          1

C++入门01

1、.h和.cpp 源文件 (.cpp)源文件是C++程序的实际实现代码文件,其中包含了具体的函数和类的定义、实现以及其他相关的代码。主要特点如下:实现代码: 源文件中包含了函数、类的具体实现代码,用于实现程序的功能。编译单元: 源文件通常是一个编译单元,即单独编译的基本单位。每个源文件都会经过编译器的处理,生成对应的目标文件。包含头文件: 源文件可以通过#include指令引入头文件,以使

Linux系统稳定性的奥秘:探究其背后的机制与哲学

在计算机操作系统的世界里,Linux以其卓越的稳定性和可靠性著称,成为服务器、嵌入式系统乃至个人电脑用户的首选。那么,是什么造就了Linux如此之高的稳定性呢?本文将深入解析Linux系统稳定性的几个关键因素,揭示其背后的技术哲学与实践。 1. 开源协作的力量Linux是一个开源项目,意味着任何人都可以查看、修改和贡献其源代码。这种开放性吸引了全球成千上万的开发者参与到内核的维护与优化中,形成了