[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

相关文章

JAVA保证HashMap线程安全的几种方式

《JAVA保证HashMap线程安全的几种方式》HashMap是线程不安全的,这意味着如果多个线程并发地访问和修改同一个HashMap实例,可能会导致数据不一致和其他线程安全问题,本文主要介绍了JAV... 目录1. 使用 Collections.synchronizedMap2. 使用 Concurren

Linux命令之firewalld的用法

《Linux命令之firewalld的用法》:本文主要介绍Linux命令之firewalld的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux命令之firewalld1、程序包2、启动firewalld3、配置文件4、firewalld规则定义的九大

Linux之计划任务和调度命令at/cron详解

《Linux之计划任务和调度命令at/cron详解》:本文主要介绍Linux之计划任务和调度命令at/cron的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux计划任务和调度命令at/cron一、计划任务二、命令{at}介绍三、命令语法及功能 :at

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

Linux内核参数配置与验证详细指南

《Linux内核参数配置与验证详细指南》在Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要,本文主要来聊聊如何配置与验证这些Linux内核参数,希望对大家有一定的帮助... 目录1. 引言2. 内核参数的作用3. 如何设置内核参数3.1 临时设置(重启失效)3.2 永久设置(重启仍生效

kali linux 无法登录root的问题及解决方法

《kalilinux无法登录root的问题及解决方法》:本文主要介绍kalilinux无法登录root的问题及解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,... 目录kali linux 无法登录root1、问题描述1.1、本地登录root1.2、ssh远程登录root2、

Linux ls命令操作详解

《Linuxls命令操作详解》通过ls命令,我们可以查看指定目录下的文件和子目录,并结合不同的选项获取详细的文件信息,如权限、大小、修改时间等,:本文主要介绍Linuxls命令详解,需要的朋友可... 目录1. 命令简介2. 命令的基本语法和用法2.1 语法格式2.2 使用示例2.2.1 列出当前目录下的文

golang获取当前时间、时间戳和时间字符串及它们之间的相互转换方法

《golang获取当前时间、时间戳和时间字符串及它们之间的相互转换方法》:本文主要介绍golang获取当前时间、时间戳和时间字符串及它们之间的相互转换,本文通过实例代码给大家介绍的非常详细,感兴趣... 目录1、获取当前时间2、获取当前时间戳3、获取当前时间的字符串格式4、它们之间的相互转化上篇文章给大家介

Linux中的计划任务(crontab)使用方式

《Linux中的计划任务(crontab)使用方式》:本文主要介绍Linux中的计划任务(crontab)使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、前言1、linux的起源与发展2、什么是计划任务(crontab)二、crontab基础1、cro

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将