我们前面提到linux有两种方法激活调度器:核心调度器和

周期调度器

  • 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU
  • 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要

因而内核提供了两个调度器主调度器周期性调度器,分别实现如上工作, 两者合在一起就组成了核心调度器(core scheduler), 也叫通用调度器(generic scheduler).

他们都根据进程的优先级分配CPU时间, 因此这个过程就叫做优先调度, 我们将在本节主要讲解核心调度器的设计和优先调度的实现方式.

而我们的周期性调度器以固定的频率激活负责当前进程调度类的周期性调度方法, 以保证系统的并发性

1 前景回顾

首先还是让我们简单回顾一下子之前的的内容

1.1 进程调度

内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来.

调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为两个不同的部分, 其中一个涉及调度策略, 另外一个涉及上下文切换.

内核必须提供一种方法, 在各个进程之间尽可能公平地共享CPU时间, 而同时又要考虑不同的任务优先级.

调度器的一般原理是, 按所需分配的计算能力, 向系统中每个进程提供最大的公正性, 或者从另外一个角度上说, 他试图确保没有进程被亏待.

1.2 进程的分类

linux把进程区分为实时进程和非实时进程, 其中非实时进程进一步划分为交互式进程和批处理进程

根据进程的不同分类Linux采用不同的调度策略.

对于实时进程,采用FIFO, Round Robin或者Earliest Deadline First (EDF)最早截止期限优先调度算法|的调度策略.

对于普通进程则采用CFS完全公平调度器进行调度

1.3 linux调度器的演变

table th:nth-of-type(1){
width: 20%;
}

字段 版本
O(n)的始调度算法 linux-0.11~2.4
O(1)调度器 linux-2.5
CFS调度器 linux-2.6~至今

1.4 Linux的调度器组成

2个调度器

可以用两种方法来激活调度

  • 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU
  • 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要

因此当前linux的调度程序由两个调度器组成:主调度器,周期性调度器(两者又统称为通用调度器(generic scheduler)或核心调度器(core scheduler))

并且每个调度器包括两个内容:调度框架(其实质就是两个函数框架)及调度器类

6种调度策略

linux内核目前实现了6中调度策略(即调度算法), 用于对不同类型的进程进行调度, 或者支持某些特殊的功能

  • SCHED_NORMAL和SCHED_BATCH调度普通的非实时进程
  • SCHED_FIFO和SCHED_RR和SCHED_DEADLINE则采用不同的调度策略调度实时进程
  • SCHED_IDLE则在系统空闲时调用idle进程.

5个调度器类

而依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程.

其所属进程的优先级顺序为

stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class

3个调度实体

调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度.

这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组.

linux中针对当前可调度的实时和非实时进程, 定义了类型为seched_entity的3个调度实体

  • sched_dl_entity 采用EDF算法调度的实时调度实体

    sched_rt_entity
  • 采用Roound-Robin或者FIFO算法调度的实时调度实体 rt_sched_class
  • sched_entity 采用CFS算法调度的普通非实时进程的调度实体

2 周期性调度器

周期性调度器在scheduler_tick中实现. 如果系统正在活动中, 内核会按照频率HZ自动调用该函数. 如果没有近曾在等待调度, 那么在计算机电力供应不足的情况下, 内核将关闭该调度器以减少能耗. 这对于我们的嵌入式设备或者手机终端设备的电源管理是很重要的.

2.1 周期性调度器主流程

scheduler_tick函数定义在kernel/sched/core.c, L2910中, 它有两个主要任务

  1. 更新相关统计量

管理内核中的与整个系统和各个进程的调度相关的统计量. 其间执行的主要操作是对各种计数器+1

  1. 激活负责当前进程调度类的周期性调度方法

检查进程执行的时间是否超过了它对应的ideal_runtime,如果超过了,则告诉系统,需要启动主调度器(schedule)进行进程切换。(注意thread_info:preempt_count、thread_info:flags (TIF_NEED_RESCHED))

/*
* This function gets called by the timer code, with HZ frequency.
* We call it with interrupts disabled.
*/ void scheduler_tick(void)
{
/* 1. 获取当前cpu上的全局就绪队列rq和当前运行的进程curr */ /* 1.1 在于SMP的情况下,获得当前CPU的ID。如果不是SMP,那么就返回0 */
int cpu = smp_processor_id(); /* 1.2 获取cpu的全局就绪队列rq, 每个CPU都有一个就绪队列rq */
struct rq *rq = cpu_rq(cpu); /* 1.3 获取就绪队列上正在运行的进程curr */
struct task_struct *curr = rq->curr; sched_clock_tick(); /* 2 更新rq上的统计信息, 并执行进程对应调度类的周期性的调度 */ /* 加锁 */
raw_spin_lock(&rq->lock); /* 2.1 更新rq的当前时间戳.即使rq->clock变为当前时间戳 */
update_rq_clock(rq); /* 2.2 执行当前运行进程所在调度类的task_tick函数进行周期性调度 */
curr->sched_class->task_tick(rq, curr, 0); /* 2.3 更新rq的负载信息, 即就绪队列的cpu_load[]数据
* 本质是讲数组中先前存储的负荷值向后移动一个位置,
* 将当前负荷记入数组的第一个位置 */
update_cpu_load_active(rq); /* 2.4 更新cpu的active count活动计数
* 主要是更新全局cpu就绪队列的calc_load_update*/
calc_global_load_tick(rq); /* 解锁 */
raw_spin_unlock(&rq->lock); /* 与perf计数事件相关 */
perf_event_task_tick(); #ifdef CONFIG_SMP /* 当前CPU是否空闲 */
rq->idle_balance = idle_cpu(cpu); /* 如果到是时候进行周期性负载平衡则触发SCHED_SOFTIRQ */
trigger_load_balance(rq); #endif rq_last_tick_reset(rq);
}

2.2 更新统计量

函数 描述 定义
update_rq_clock 处理就绪队列时钟的更新, 本质上就是增加struct rq当前实例的时钟时间戳 sched/core.c, L98
update_cpu_load_active 负责更新就绪队列的cpu_load数组, 其本质上相当于将数组中先前存储的负荷值向后移动一个位置, 将当前就绪队列的符合记入数组的第一个位置. 另外该函数还引入一些取平均值的技巧, 以确保符合数组的内容不会呈现太多的不联系跳读. kernel/sched/fair.c, L4641
calc_global_load_tick 跟新cpu的活动计数, 主要是更新全局cpu就绪队列的calc_load_update kernel/sched/loadavg.c, L382

2.3 激活进程所属调度类的周期性调度器

由于调度器的模块化结构, 主体工程其实很简单, 在更新统计信息的同时, 内核将真正的调度工作委托给了特定的调度类方法

内核先找到了就绪队列上当前运行的进程curr, 然后调用curr所属调度类sched_class的周期性调度方法task_tick

curr->sched_class->task_tick(rq, curr, 0);

task_tick的实现方法取决于底层的调度器类, 例如完全公平调度器会在该方法中检测是否进程已经运行了太长的时间, 以避免过长的延迟, 注意此处的做法与之前就的基于时间片的调度方法有本质区别, 旧的方法我们称之为到期的时间片, 而完全公平调度器CFS中则不存在所谓的时间片概念.

目前我们的内核中的3个调度器类struct sched_entity, struct sched_rt_entity, 和struct sched_dl_entity dl, 我们针对当前内核中实现的调度器类分别列出其周期性调度函数task_tick

调度器类 task_tick操作 task_tick函数定义
stop_sched_class - kernel/sched/stop_task.c, line 77, task_tick_stop
dl_sched_class - kernel/sched/deadline.c, line 1192, task_tick_dl
rt_sched_class - /kernel/sched/rt.c, line 2227, task_tick_rt
fail_sched_class - kernel/sched/fair.c, line 8116, task_tick_fail
idle_sched_class - kernel/sched/idle_task.c, line 53, task_tick_idle
idle_sched_class - kernel/sched/idle_task.c, line 53, task_tick_idle

如果当前进程是完全公平队列中的进程, 则首先根据当前就绪队列中的进程数算出一个延迟时间间隔,大概每个进程分配2ms时间,然后按照该进程在队列中的总权重中占得比例,算出它该执行的时间X,如果该进程执行物理时间超过了X,则激发延迟调度;如果没有超过X,但是红黑树就绪队列中下一个进程优先级更高,即curr->vruntime-leftmost->vruntime > X,也将延迟调度

延迟调度的真正调度过程在:schedule中实现,会按照调度类顺序和优先级挑选出一个最高优先级的进程执行

  • 如果当前进程是实时调度类中的进程:则如果该进程是SCHED_RR,则递减时间片[为HZ/10],到期,插入到队列尾部,并激发延迟调度,如果是SCHED_FIFO,则什么也不做,直到该进程执行完成

如果当前进程希望被重新调度, 那么调度类方法会在task_struct中设置TIF_NEED_RESCHED标志, 以表示该请求, 而内核将会在接下来的适当实际完成此请求.

3 周期性调度器的激活

3.1 定时器周期性的激活调度器

定时器是Linux提供的一种定时服务的机制. 它在某个特定的时间唤醒某个进程,来做一些工作.

在低分辨率定时器的每次时钟中断完成全局统计量更新后, 每个cpu在软中断中执行一下操作

  • 更新该cpu上当前进程内核态、用户态使用时间xtime_update
  • 调用该cpu上的定时器函数
  • 启动周期性定时器(scheduler_tick)完成该cpu上任务的周期性调度工作;

在支持动态定时器的系统中,可以关闭该调度器,从而进入深度睡眠过程;scheduler_tick查看当前进程是否运行太长时间,如果是,将进程的TIF_NEED_RESCHED置位,然后再中断返回时,调用schedule,进行进程切换操作

//  http://lxr.free-electrons.com/source/arch/arm/kernel/time.c?v=4.6#L74
/*
* Kernel system timer support.
*/
void timer_tick(void)
{
profile_tick(CPU_PROFILING);
xtime_update(1);
#ifndef CONFIG_SMP
update_process_times(user_mode(get_irq_regs()));
#endif
} // http://lxr.free-electrons.com/source/kernel/time/timer.c?v=4.6#L1409
/*
* Called from the timer interrupt handler to charge one tick to the current
* process. user_tick is 1 if the tick is user time, 0 for system.
*/
void update_process_times(int user_tick)
{
struct task_struct *p = current; /* Note: this timer irq context must be accounted for as well. */
account_process_tick(p, user_tick);
run_local_timers();
rcu_check_callbacks(user_tick);
#ifdef CONFIG_IRQ_WORK
if (in_irq())
irq_work_tick();
#endif
scheduler_tick();
run_posix_cpu_timers(p);
}

早期实现

Linux初始化时, init_IRQ()函数设定8253的定时周期为10ms(一个tick值). 同样,在初始化时, time_init()用setup_irq()设置时间中断向量irq0, 中断服务程序为timer_interrupt.

在2.4版内核及较早的版本当中, 定时器的中断处理采用底半机制, 底半处理函数的注册在start_kernel()函数中调用sechd_init(), 在这个函数中又调用init_bh(TIMER_BH, timer_bh)注册了定时器的底半处理函数. 然后系统才调用time_init( )来注册定时器的中断向量和中断处理函数.

在中断处理函数timer_interrupt()中,主要是调用do_timer()函数完成工作。do_timer()函数的主要功能就是调用mark_bh()产生软中断,随后处理器会在合适的时候调用定时器底半处理函数timer_bh()。在timer_bh()中, 实现了更新定时器的功能. 2.4.23版的do_timer()函数代码如下(经过简略):

void do_timer(struct pt_regs *regs)
{
(*(unsigned long *)&jiffies)++;
update_process_times(user_mode(regs));
mark_bh(TIMER_BH);
}

而在内核2.6版本以后,定时器中断处理采用了软中断机制而不是底半机制。时钟中断处理函数仍然为timer_interrup()-> do_timer_interrupt()-> do_timer_interrupt_hook()-> do_timer()。不过do_timer()函数的实现有所不同

void do_timer(struct pt_regs *regs)
{
jiffies_64++;
update_process_times(user_mode(regs));
update_times();
}

更详细的实现linux-2.6

Linux中断处理之时钟中断(一)

(原创)linux内核进程调度以及定时器实现机制

进程管理与调度5 – 进程调度、进程切换原理详解

Linux核心调度器之周期性调度器scheduler_tick--Linux进程的管理与调度(十八)的更多相关文章

  1. Linux进程调度器的设计--Linux进程的管理与调度(十七)

    1 前景回顾 1.1 进程调度 内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来. 调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为 ...

  2. Linux进程调度器概述--Linux进程的管理与调度(十五)

    调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为两个不同的部分, 其中一个涉及调度策略, 另外一个涉及上下文切换. 1 背景知识 1.1 什么是调度器 ...

  3. Linux CFS调度器之负荷权重load_weight--Linux进程的管理与调度(二十五)

    1. 负荷权重 1.1 负荷权重结构struct load_weight 负荷权重用struct load_weight数据结构来表示, 保存着进程权重值weight.其定义在/include/lin ...

  4. Linux进程上下文切换过程context_switch详解--Linux进程的管理与调度(二十一)

    1 前景回顾 1.1 Linux的调度器组成 2个调度器 可以用两种方法来激活调度 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测 ...

  5. Linux进程:管理和调度

    一:进程管理 进程.轻量级进程和线程 通常定义:进程是程序执行时的一个实例. 这个很像类和实例对象的关系.从内核来看:进程的目的就是担当分配系统资源(CPU,内存等)的实体. 当进程创建时,它几乎和父 ...

  6. Linux唤醒抢占----Linux进程的管理与调度(二十三)

    1. 唤醒抢占 当在try_to_wake_up/wake_up_process和wake_up_new_task中唤醒进程时, 内核使用全局check_preempt_curr看看是否进程可以抢占当 ...

  7. Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四)

    Linux进程的退出 linux下进程退出的方式 正常退出 从main函数返回return 调用exit 调用_exit 异常退出 调用abort 由信号终止 _exit, exit和_Exit的区别 ...

  8. Linux进程核心调度器之主调度器schedule--Linux进程的管理与调度(十九)

    主调度器 在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule, 从系统调用返回后, 内核也会检查当前进程是否设置了重调度标志TLF_N ...

  9. Linux CFS调度器之task_tick_fair处理周期性调度器--Linux进程的管理与调度(二十九)

    1. CFS如何处理周期性调度器 周期性调度器的工作由scheduler_tick函数完成(定义在kernel/sched/core.c, line 2910), 在scheduler_tick中周期 ...

随机推荐

  1. SharePoint如何配置Ipad跳转等问题

    如何配置Ipad跳转 Apple iPad 设备上不支持 SharePoint 标准视图.用户可以改用移动视图在 iPad 设备上查看 SharePoint 内容.默认情况下,iPad 用户被重定向到 ...

  2. Socket进程通信机制及应用

    Socket通常称为“套接字”,用于描述IP地址和端口,是一个通信链的句柄.应用程序通过套接字向网络发出请求或者应答网络请求.Socket即不是一个程序,也不是一个协议,其只是操作系统提供的通信层的一 ...

  3. 1.let命令总结

    1.let用法类似于var,但是let只在所在代码块有效 { let a = 10; var b = 1; } a // ReferenceError: a is not defined. b // ...

  4. 浅谈Web服务器和应用服务器的区别

    1Web服务器和应用服务器简介 通俗的讲,Web服务器传送页面使浏览器可以浏览,然而应用程序服务器提供的是客户端应用程序可以调用(call)的方法(methods).确切一点,你可以说:Web服务器专 ...

  5. 翻译:last_value()函数(已提交到MariaDB官方手册)

    本文为mariadb官方手册:LAST_VALUE()的译文. 原文:https://mariadb.com/kb/en/last_value/我提交到MariaDB官方手册的译文:https://m ...

  6. Hystrix降级逻辑中如何获取触发的异常

    通过之前Spring Cloud系列教程中的<Spring Cloud构建微服务架构:服务容错保护(Hystrix服务降级)>一文,我们已经知道如何通过Hystrix来保护自己的服务不被外 ...

  7. java开发各层对象含义

    综述 java的几种对象(PO,VO,DAO,BO,POJO)解释: 一.PO:persistant object 持久对象,可以看成是与数据库中的表相映射的java对象.最简单的PO就是对应数据库中 ...

  8. 【转载】C#检测客户端输入的内容是否含有危险字符串

    用户在客户端提交的内容有时候并不可信,如果客户端提交的内容中含有危险字符串信息,则很有可能造成应用程序安全性问题,如SQL注入风险等.因此在接收客户端提交过来的数据后,我们首先需要判断数据中是否含有危 ...

  9. 【转载】Sqlserver阻止保存要求重新创建表的更改

    在Sqlserver创建完表table后,后续维护过程中有时候需要往表格中新增字段,在表设计窗体中新增字段后保存,有时候会直接抛出错误信息,提示“不允许保存更改,您所做的更改要求删除并重新创建以下表” ...

  10. 应用TortoiseGit为github账号添加SSH keys,解决pull总是提示输入密码的问题

    每次同步或者上传代码到githun上的代码库时,需要每次都输入用户名和密码,这时我们设置一下SSH key就可以省去这些麻烦了.若果使用TortoiseGit作为github本地管理工具,Tortoi ...