Linux进程调度与抢占
一、linux内核抢占介绍
1.抢占发生的必要条件
a.preempt_count抢占计数必须为0,不为0说明其它地方调用了禁止抢占的函数,比如spin_lock系列函数。
b.中断必须是使能的状态,因为抢占动作要依赖中断。
preempt_schedule()具体源码实现参考如下:
asmlinkage __visible void __sched notrace preempt_schedule(void)
{
/*
* If there is a non-zero preempt_count or interrupts are disabled,
* we do not want to preempt the current task. Just return..
*/
/*preempt_disable()会增加preempt_count的计数*/
if (likely(!preemptible()))
return;
preempt_schedule_common();
}
#define preemptible() (preempt_count() == 0 && !irqs_disabled())
2.spin_lock系列函数
a.spin_lock()会调用preempt_disable函数关闭抢占.
b.spin_lock_irq()会调用spin_lock()函数和local_irq_disable()函数(关闭中断)
c.spin_lock_irqsave()会调用spin_lock()函数和local_irq_save()函数(关闭中断,同时保存cpu对中断的屏蔽状态)
spin_lock():
/*include/linux/spinlock.h*/
static __always_inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock) _raw_spin_lock(lock) /*kernel/locking/spinlock.c*/
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
__raw_spin_lock(lock);
} /*include/linux/spinlock_api_smp.h*/
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable(); /*调用禁止抢占函数*/
spin_acquire(&lock->dep_map, , , _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
spin_unlock():
/*kernel/locking/spinlock.c*/
void __lockfunc _raw_spin_unlock(raw_spinlock_t *lock)
{
__raw_spin_unlock(lock);
} /*include/linux/spinlock_api_smp.h*/
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
spin_release(&lock->dep_map, , _RET_IP_);
do_raw_spin_unlock(lock);
preempt_enable();
}
preempt_enable():
/*include/linux/preempt.h*/
#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \
/*这里提供了一个抢占点__preempt_schedule(),其它高优先级的进程可直接抢占*/\
__preempt_schedule(); \
} while ()
由上可知,spin_unlock()系列函数可以直接触发内核抢占,因为它里面提供可抢占点。
3.preempt_disable()和local_irq_disable()的区别
由抢占发生的必要条件可知两个函数都可以关闭抢占。区别不在于关抢占和关中断函数上,而是在对应的开抢占和开中断的函数上,也就是
preempt_enable()函数local_irq_enable()函数。preempt_enable()会是能抢占并提供抢占点,而local_irq_enable()仅仅是开中断(是能抢占),
并没有提供抢占点。
4.抢占点可能是:时钟tick中断处理返回、中断返回、软中断结束、yield()(进程调用它放弃CPU)等等多种情况。
5.注意spin_lock系列函数关闭了抢占,但是并没有关闭调度!
6.原子上下文中不可睡眠,可以打开内核中的CONFIG_DEBUG_ATOMIC_SLEEP选项,运行时一旦检测出在原子上下文中可能睡眠就会打印栈回溯信息。
7.进程的优先级使用nice值表示。
二、进程调度
1.目前4.14.35内核中只有下列sched_class:
fair_sched_class: .next = idle_sched_class
rt_sched_class : .next = fair_sched_class
dl_sched_class : .next = rt_sched_class
idle_sched_class: .next = NULL
stop_sched_class: .next = dl_sched_class
所有的调度类构成一个单链表:
stop_sched_class --> dl_sched_class --> rt_sched_class --> fair_sched_class --> idle_sched_class --> NULL
#ifdef CONFIG_SMP
#define sched_class_highest (&stop_sched_class)
#else
#define sched_class_highest (&dl_sched_class)
#endif
#define for_each_class(class) for (class = sched_class_highest; class; class = class->next)
SCHED_NORMAL:普通的分时进程,使用的fair_sched_class调度类
SCHED_FIFO:先进先出的实时进程,使用的是rt_sched_class调度类。
当调用程序把CPU分配给进程的时候,它把该进程描述符保留在运行队列链表的当前位置。使用此调度策略的进程一旦使用CPU则一直运行。如
果没有其他可运行的更高优先级实时进程,进程就会继续使用CPU,想用多久就用多久,即使还有其他具有相同优先级的实时进程处于可运行状态。
SCHED_RR:时间片轮转的实时进程,使用的rt_sched_class调度类。
当调度程序把CPU分配给进程的时候,它把该进程的描述符放在运行队列链表的末尾。这种策略保证对所有具有相同优先级的SCHED_RR实时进程
进行公平分配CPU时间。
SCHED_BATCH:是SCHED_NORMAL的分化版本,使用的fair_shed_class调度类。
采用分时策略,根据动态优先级,分配CPU资源。在有实时进程的时候,实时进程优先调度。但针对吞吐量优化,除了不能抢占外与常规进程一
样,允许任务运行更长时间,更好使用高速缓存,适合于成批处理的工作。
SCHED_IDLE:优先级最低,在系统空闲时运行,使用的是idle_sched_class调度类,给0号进程使用。
SCHED_DEADLINE:新支持的实时进程调度策略,使用的是dl_sched_class调度类。
针对突发型计算,并且对延迟和完成时间敏感的任务使用,基于EDF(earliest deadline first)。
2.调度类struct sched class
struct sched_class {
const struct sched_class *next; void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*yield_task) (struct rq *rq);
bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt); void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags); /*
* It is the responsibility of the pick_next_task() method that will
* return the next task to call put_prev_task() on the @prev task or
* something equivalent.
*
* May return RETRY_TASK when it finds a higher prio class has runnable tasks.
*/
struct task_struct * (*pick_next_task) (struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
void (*put_prev_task) (struct rq *rq, struct task_struct *p); #ifdef CONFIG_SMP
int (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
void (*migrate_task_rq)(struct task_struct *p); void (*task_woken)(struct rq *this_rq, struct task_struct *task); void (*set_cpus_allowed)(struct task_struct *p, const struct cpumask *newmask); void (*rq_online)(struct rq *rq);
void (*rq_offline)(struct rq *rq);
#endif void (*set_curr_task) (struct rq *rq);
void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
void (*task_fork) (struct task_struct *p);
void (*task_dead) (struct task_struct *p); /*
* The switched_from() call is allowed to drop rq->lock, therefore we
* cannot assume the switched_from/switched_to pair is serliazed by
* rq->lock. They are however serialized by p->pi_lock.
*/
void (*switched_from) (struct rq *this_rq, struct task_struct *task);
void (*switched_to) (struct rq *this_rq, struct task_struct *task);
void (*prio_changed) (struct rq *this_rq, struct task_struct *task, int oldprio); unsigned int (*get_rr_interval) (struct rq *rq, struct task_struct *task); void (*update_curr) (struct rq *rq); #define TASK_SET_GROUP 0
#define TASK_MOVE_GROUP 1 #ifdef CONFIG_FAIR_GROUP_SCHED
void (*task_change_group) (struct task_struct *p, int type);
#endif
};
next: 指向下一个调度类,用于在函数pick_next_task、check_preempt_curr、set_rq_online、set_rq_offline中遍历整个调度类,根据调度
类的优先级选择调度类。
优先级为: stop_sched_class-->dl_sched_class-->rt_sched_class-->fair_sched_class-->idle_sched_class
enqueue_task: 将任务加入到调度类中
dequeue_task: 将任务从调度类中移除
yield_task/yield_to_task: 主动放弃CPU
check_preempt_curr: 检查当前进程是否可被强占
pick_next_task: 从调度类中选出下一个要运行的进程
put_prev_task: 将进程放回到调度类中
select_task_rq: 为进程选择一个合适的cpu的运行队列
migrate_task_rq: 迁移到另外的cpu运行队列
pre_schedule: 调度以前调用
post_schedule: 通知调度器完成切换
task_woken: 用于进程唤醒
set_cpus_allowed: 修改进程cpu亲和力
affinityrq_online: 启动运行队列
rq_offline:关闭运行队列
set_curr_task: 当进程改变调度类或者进程组时被调用
task_tick: 将会引起进程切换,驱动运行running强占,由time_tick调用
task_fork: 进程创建时调用,不同调度策略的进程初始化不一样
task_dead: 进程结束时调用
switched_from/switched_to:进程改变调度器时使用
prio_changed: 改变进程优先级.
3.调度的触发
调度的触发主要有两种方式:
(1)一种是本地定时中断触发调用scheduler_tick()函数,然后使用当前运行进程的调度类中的task_tick.
(2)另外一种则是主动调用schedule().
不管是哪一种最终都会调用到__schedule函数,该函数调用pick_netx_task,通过(rq->nr_running==rq->cfs.h_nr_running)判断出如果当前
运行队列中的进程都在cfs调度器中,则直接调用cfs的调度类(内核代码里面这一判断使用了likely说明大部分情况都是满足该条件的)。如
果运行队列不都在cfs中,则通过优先级stop_sched_class-->dl_sched_class-->rt_sched_class-->fair_sched_class-->idle_sched_class
遍历选出下一个需要运行的进程,然后进程任务切换。
4.发生调度的时机
处于TASK_RUNNING状态的进程才会被进程调度器选择,其他状态不会进入调度器,系统发生调度的时机如下:
a.调用cond_resched()时
b.显式调用schedule()时
c.从中断上下文返回时
当内核开启抢占时,会多出几个调度时机:
d.在系统调用中或者中断下文中调用preemt_enable()时
5.__schedule()实现
TODO:分析它
6.CFS(Completely Fair Scheduler)调度
该部分代码位于linux/kernel/sched/fair.c中,定义了const struct sched_classfair_sched_class,这个是CFS的调度类定义的对象。其中
基本包含了CFS调度的所有实现。
CFS实现三个调度策略:
SCHED_NORMAL:这个调度策略是被常规任务使用
SCHED_BATCH:这个策略不像常规的任务那样频繁的抢占,以牺牲交互性为代价下,因而允许任务运行更长的时间以更好的利用缓存,这种策略
适合批处理。
SCHED_IDLE:这是nice值甚至比19还弱,但是为了避免陷入优先级导致问题,这个问题将会死锁这个调度器,因而这不是一个真正空闲定时调
度器。
CFS调度类fair_sched_class:
enqueue_task():当任务进入runnable状态,这个回调将把这个任务的调度实体(entity)放入红黑树并且增加nr_running变量的值。
dequeue_task():当任务不再是runnable状态,这个回调将会把这个任务的调度实体从红黑树中取出,并且减少nr_running变量的值。
yield_task():除非compat_yield sysctl是打开的,这个回调函数基本上就是一个dequeue后跟一个enqueue,这那种情况下,他将任务的调度
实体放入红黑树的最右端
check_preempt_curr():这个回调函数是检查一个任务进入runnable状态是否应该抢占当前运行的任务。
pick_next_task():这个回调函数选出下一个最合适运行的任务。
set_curr_task():当任务改变他的调度类或者改变他的任务组,将调用该回调函数。
task_tick():这个回调函数大多数是被time tick调用。它可能引起进程切换,这就驱动了运行时抢占。
/*
* 一个调度实体(红黑树的一个节点),其包含一组或一个指定的进程,包含一个自己的运行队列,
* 一个父亲指针,一个指向需要调度的队列.
*/
struct sched_entity {
/* For load-balancing: */
struct load_weight load; /*权重,在数组prio_to_weight[]包含优先级转权重的数值*/
struct rb_node run_node; /*实体在红黑树对应的节点信息*/
struct list_head group_node; /*实体所在的进程组*/
unsigned int on_rq; /*实体是否处于红黑树运行队列中*/ u64 exec_start; /*开始运行时间*/
u64 sum_exec_runtime; /*总运行时间*/
/*
虚拟运行时间,在时间中断或者任务状态发生改变时会更新.
其会不停的增长,增长速度与load权重成反比,load越高,增长速度越慢,就越可能处于红黑树最左边
被调度。每次时钟中断都会修改其值,具体见calc_delta_fair()函数
*/
u64 vruntime;
/*进程在切换进cpu时的sum_exec_runtime值*/
u64 prev_sum_exec_runtime;
/*此调度实体中进程移到其他cpu组的数量*/
u64 nr_migrations; struct sched_statistics statistics; #ifdef CONFIG_FAIR_GROUP_SCHED
int depth;
/*
父亲调度实体指针,如果是进程则指向其运行队列的调度实体,如果是进程组则指向其上一个进程组的
调度实体,在set_task_rq函数中设置。
*/
struct sched_entity *parent;
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq; /*实体所处红黑树运行队列*/
/* rq "owned" by this entity/group: */
struct cfs_rq *my_q; /*实体的红黑树运行队列,如果为NULL表明其是一个进程,若非NULL表明其是调度组*/
#endif #ifdef CONFIG_SMP
/*
* Per entity load average tracking.
*
* Put into separate cache line so it does not
* collide with read-mostly values above.
*/
struct sched_avg avg ____cacheline_aligned_in_smp;
#endif
};
load
指定了权重, 决定了各个实体占队列总负荷的比重, 计算负荷权重是调度器的一项重任, 因为CFS所需的虚拟时钟的速度最终依赖于负荷, 权
重通过优先级转换而成,是vruntime计算的关键
run_node
调度实体在红黑树对应的结点信息, 使得调度实体可以在红黑树上排序
sum_exec_runtime
记录程序运行所消耗的CPU时间, 以用于完全公平调度器CFS
on_rq
调度实体是否在就绪队列上接受检查, 表明是否处于CFS红黑树运行队列中,需要明确一个观点就是,CFS运行队列里面包含有一个红黑树,但
这个红黑树并不是CFS运行队列的全部,因为红黑树仅仅是用于选择出下一个调度程序的算法。很简单的一个例子,普通程序运行时,其并不在
红黑树中,但是还是处于CFS运行队列中,其on_rq为真。只有准备退出、即将睡眠等待和转为实时进程的进程其CFS运行队列的on_rq为假。
vruntime
虚拟运行时间,调度的关键,其计算公式:一次调度间隔的虚拟运行时间 = 实际运行时间 * (NICE_0_LOAD / 权重)。可以看出跟实际运行时
间和权重有关,红黑树就是以此作为排序的标准,优先级越高的进程在运行时其vruntime增长的越慢,其可运行时间相对就长,而且也越有可
能处于红黑树的最左结点,调度器每次都选择最左边的结点为下一个调度进程。注意其值为单调递增,在每个调度器的时钟中断时当前进程的
虚拟运行时间都会累加。单纯的说就是进程们都在比谁的vruntime最小,最小的将被调度。
cfs_rq
此调度实体所处于的CFS运行队列
my_q
如果此调度实体代表的是一个进程组,那么此调度实体就包含有一个自己的CFS运行队列,其CFS运行队列中存放的是此进程组中的进程,这些
进程就不会在其他CFS运行队列的红黑树中被包含(包括顶层红黑树也不会包含他们,他们只属于这个进程组的红黑树)。
sum_exec_runtime
跟踪运行时间是由update_curr不断累积完成的。内核中许多地方都会调用该函数, 例如, 新进程加入就绪队列时, 或者周期性调度器中. 每次
调用时, 会计算当前时间和exec_start之间的差值, exec_start则更新到当前时间. 差值则被加到sum_exec_runtime.
在进程执行期间虚拟时钟上流逝的时间数量由vruntime统计。
在进程被撤销时, 其当前sum_exec_runtime值保存到prev_sum_exec_runtime, 此后, 进程抢占的时候需要用到该数据, 但是注意, 在prev_sum_exec_runtime
中保存了sum_exec_runtime的值, 而sum_exec_runtime并不会被重置, 而是持续单调增长。
每一个进程的task_struct中都嵌入了sched_entry对象,所以进程是可调度的实体,但是可调度的实体不一定是进程,也可能是进程组。
7.CFS调度总结:
Tcik中断,主要会更新调度信息,然后调整当前进程在红黑树中的位置。调整完成以后如果当前进程不再是最左边的叶子,就标记为Need_resched
标志,中断返回时就会调用scheduler()完成切换、否则当前进程继续占用CPU。从这里可以看出CFS抛弃了传统时间片概念。Tick中断只需要更新红黑树。
红黑树键值即为vruntime,该值通过调用update_curr函数进行更新。这个值为64位的变量,会一直递增,__enqueue_entity中会将vruntime作为键值将
要入队的实体插入到红黑树中。__pick_first_entity会将红黑树中最左侧即vruntime最小的实体取出。
优秀文章:
Linux 2.6 Completely Fair Scheduler 内幕: https://www.ibm.com/developerworks/cn/linux/l-completely-fair-scheduler/index.html
Linux进程调度与抢占的更多相关文章
- linux进程调度之 FIFO 和 RR 调度策略
转载 http://blog.chinaunix.net/uid-24774106-id-3379478.html linux进程调度之 FIFO 和 RR 调度策略 2012-10-19 18 ...
- Linux进程调度原理
Linux进程调度原理 Linux进程调度机制 Linux进程调度的目标 1.高效性:高效意味着在相同的时间下要完成更多的任务.调度程序会被频繁的执行,所以调度程序要尽可能的高效: 2.加强交互性能: ...
- Linux内核态抢占机制分析
http://blog.sina.com.cn/s/blog_502c8cc401012pxj.html [摘要]本文首先介绍非抢占式内核(Non-Preemptive Kernel)和可抢占式内核( ...
- Linux内核态抢占机制分析(转)
Linux内核态抢占机制分析 http://blog.sina.com.cn/s/blog_502c8cc401012pxj.html 摘 要]本文首先介绍非抢占式内核(Non-Preemptive ...
- Linux进程调度原理【转】
转自:http://www.cnblogs.com/zhaoyl/archive/2012/09/04/2671156.html Linux进程调度的目标 1.高效性:高效意味着在相同的时间下要完成更 ...
- Linux内核态抢占机制分析【转】
转自:http://blog.csdn.net/yiyeguzhou100/article/details/53097665 目录(?)[-] 1非抢占式和可抢占式内核的区别 21 用户态抢占User ...
- Linux进程调度(3):进程切换分析
3.调度函数schedule()分析 当kernel/sched.c:sched_tick()执行完,并且时钟中断返回时,就会调用kernel/sched.c:schedule()完成进程切换.我们 ...
- 深度讲解Linux内存管理和Linux进程调度-打通任督二脉
我在多年的工程生涯中发现很多工程师碰到一个共性的问题:Linux工程师很多,甚至有很多有多年工作经验,但是对一些关键概念的理解非常模糊,比如不理解CPU.内存资源等的真正分布,具体的工作机制,这使得他 ...
- [转载]Linux进程调度原理
[转载]Linux进程调度原理 Linux进程调度原理 Linux进程调度的目标 1.高效性:高效意味着在相同的时间下要完成更多的任务.调度程序会被频繁的执行,所以调度程序要尽可能的高效: 2.加强交 ...
随机推荐
- swagger2的接口文档
以前见过一个swagger2的接口文档,特别好用,好看,对接口中入参描述的很详细:适合用于项目的开发 后来自己做项目的时候,没有找到这个swagger版本 <dependency> < ...
- Windows Server 2008环境下Apache2.4+Tomcat8配置
安装步骤 1. 安装配置JDK2. 安装配置Apache3. 安装配置Tomcat4. 启动服务并测试 一.Apache安装与配置 1.Apache解压在D盘根目录下建立一个文件夹Apache Gro ...
- 几种优化方法的整理(SGD,Adagrad,Adadelta,Adam)
参考自: https://zhuanlan.zhihu.com/p/22252270 常见的优化方法有如下几种:SGD,Adagrad,Adadelta,Adam,Adamax,Nadam 1. SG ...
- java读取UTF-8的txt文件发现开头的一个字符问题
今天遇到一个奇葩问题,在读取一个TXT文件时,出现开头多了一个问号(?).如下图: 莫名奇妙的多了一个.最后通过网上资料,知道在Java中,class文件采用utf8的编码方式,JVM运行时采用utf ...
- Android Studio 使用本地gradle配置详解
由于国内墙的原因,我们的Gradle无法使用 但是我们可以通过去下载我们想要的Gradle版本 然后再AndoidStudio内去配置本地的版本 进而去实现了Gradle的配置 注意一: so我们按照 ...
- Electron入门之ipcMain,ipcRenderer
ipcMain 模块是类EventEmitter的实例.当在主进程中使用它的时候,它控制着由渲染进程(web page)发送过来的异步或同步消息.从渲染进程发送过来的消息将触发事件. [发消息] 发送 ...
- mybat使用注解的方式如@Select写sql
package com.polymer.app.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibati ...
- linux软件管理 源码包
源码包安装位置 安装在指定位置当中,一般是 /usr/local/软件名/ 注意: 需要指定安装路劲为了便于卸载,因为源码包的是没有卸载命令的,它的的卸载方式就是把对应安装文件夹删除了. 所以如果 ...
- PPT设计灵感功能
最近老师让我们做PPT,我用的是power point是office365加强版.我发现如果想平常那样直接做的话排版太死板了,不好看.我当时想:管他呢,先做出来再说.刚输完标题,放了几张图片进去右边就 ...
- python-基础数据类型,集合及深浅copy
一 数据类型定义及分类 我们人类可以很容易的分清数字与字符的区别,但是计算机并不能呀,计算机虽然很强大,但从某种角度上看又很傻,除非你明确的告诉它,1是数字,“汉”是文字,否则它是分不清1和‘汉’的区 ...