【Linux内核】进程调度

【Linux内核】进程调度

作者:爱写代码的刚子

时间:2024.5.22

前言:本篇博客将会介绍linux中的进程调度策略

调度策略

传统将进程分为两类:

  • “I/O受限”:频繁使用I/O设备,等待I/O操作的完成(如数据库服务器)
  • “CPU受限”:需要大量CPU时间的数值计算应用程序(图像绘制程序)

另一种分类:

  • 交互式进程:经常与用户进行交互,花较多的时间等待键盘和鼠标操作。进程必须很快被唤醒。
  • 批处理进程:经常在后台运行
  • 实时进程:有很强的调度需要。不会被低优先级的进程阻塞,有较短的响应时间

调度算法可以确认实时程序的身份

Linux2.6调度程序实现了基于进程过去行为的启发式算法来区分交互式进程还是批处理进程(调度程序有偏爱交互式进程的倾向)

一些改变调度优先级的系统调用:

在这里插入图片描述

进程的抢占

Linux进程是抢占试的,如何进行抢占?

一个进程进入TASK_RUNNING状态,内核会判断它的动态优先级是否大于当前进程的优先级。如果是的话当前进程的执行被中断,调用调度程序选择另一个程序来运行。或者如果一个进程的时间片到期也可以被抢占。

此时这个进程的thread_info结构中的TIF_NEED_RESCHED标志(表明是否需要重新执行一次调度)被设置,以便时钟中断处理程序终止时调度程序被调用(这意味着当前线程在等待调度器重新安排它的执行。可能的原因包括当前线程的时间片已经用完,或者有更高优先级的任务需要立即执行。)

Linux2.6的内核是抢占式的,进程无论处于内核态还是用户态都可能被抢占

一个时间片必须持续多长时间?

时间片不能太长也不能太短

如果时间片太短:由进程切换引起的系统开销就会变高。

如果时间片太长:进程看起来就不再是并发执行,太长的时间片会降低系统的响应能力。

调度算法

早期Linux版本的调度算法:每次进程切换时,内核扫描可运行程序的链表,计算进程的优先级,然后选择“最佳”进程来运行。

**缺点:**选择"最佳"进程所要消耗的时间与可运行的进程数量相关,如果进程太多消耗太大。

Linux2.6的调度算法

对于O(n)调度器会在所有进程的时间片用完后,才会重新计算任务的优先级。而O(1)调度器则是在每个进程时间片用完后,就重新计算优先级。对于O(1)调度器为每个CPU维护了两个队列

  • active队列:存放的是时间片尚未用完的任务
  • expired队列:存放的是时间片已经耗尽的任务

调度程序总能成功地找到要执行的进程,总是至少有一个可运行的进程(swapper进程,PID = 0,只有在CPU不能执行其他进程时才执行,每个多处理器系统的CPU都有它自己的swapper进程)

swapper进程

所有进程的祖先叫做进程0 ,idle 进程或因为历史的原因叫做swapper 进程。它是在 linux 的初始化阶段从无到有的创建的一个内核线程。这个祖先进程使用静态分配的数据结构。
在多处理器系统中,每个CPU都有一个进程0,主要打开机器电源,计算机的BIOS就启动一个CPU,同时禁用其他CPU。运行的CPU 上的swapper进程初初始化内核数据结构,然后激活其他的并且使用copy_process()函数创建另外的swapper进程,把0 传递给新创建的swapper进程作为他们进程的PID

三种调度策略:

SCHED_FIFO:先进先出的实时进程

SCHED_RR:时间片轮转的实时进程

SCHED_NORMAL:普通的分时进程

调度算法根据进程是普通进程还是实时进程有很大的不同。

普通进程的调度

每个进程都有他自己的静态优先级,调度程序使用静态优先级来估价系统中这个进程与其他普通进程之间的调度程度

普通进程的优先级:100(最高优先级)~139(最低优先级)值越低优先级越高

新进程总是继承父进程的静态优先级。用户可以通过一些系统调用(nice() 、 setpriority())可以改变拥有进程的静态优先级

基本时间片

静态优先级与时间片的关系:

在这里插入图片描述

静态优先级越高(其值越小),基本时间片越长

普通进程优先级的典型值

在这里插入图片描述

动态优先级和平均睡眠时间

  • 静态优先级:优先级固定,简单且可预测,但灵活性差,可能导致资源利用不均和进程饥饿。
  • 动态优先级:优先级可变,灵活且高效,能适应系统负载和进程行为的变化,但实现复杂,调度决策不如静态优先级可预测。

动态优先级更适合需要高响应性和高资源利用率的系统,而静态优先级适合需求相对简单且调度决策需要高度可预测的系统。

动态优先级是调度程序在选择新进程来运行时所依据的一个数值。这个优先级随着进程的执行情况和系统状态的变化而变化

动态优先级 = max(100,min(静态优先级 - bonus + 5,139))

bonus

范围:0~10,值小于5表示降低动态优先级表示惩罚,大于5表示增加动态优先级表示奖赏

bonus的值依赖于进程过去的情况,与进程的平均睡眠时间相关。

平均睡眠时间是进程在睡眠状态所消耗的平均纳秒数(注意不是对过去时间求平均操作,例如:在TASK_INTERRUPTIBLE和TASK_UNINTERRUPTABLE状态计算出的平均睡眠时间是不同的,进程在运行过程中平均睡眠时间递减,最后永远不大于1s)

在这里插入图片描述

平均睡眠时间被调度器用来确认一个给定进程是交互式进程还是批处理进程。如果一个进程满足下面公式就被称作交互式进程:

  • 动态优先级 <= 3*静态优先级/4 + 28

相当于下面公式(动态优先级 = 静态优先级 - bonus + 5):

  • bonus - 5 >= 静态优先级 / 4 - 28

表达式:静态优先级 / 4 - 28被称为交互式的δ,交互式δ典型值在“普通进程优先级的典型值”图中有列出

根据公式我们可以得出:高优先级比低优先级更容易成为交互式进程。具有最低优先级(139)的进程决不会成为交互进程,因为bonus始终小于11。缺省静态优先级(120)的进程需要平均睡眠时间超过700ms才能成为交互式进程。

活动和过期队列

即使具有较高静态优先级的普通进程获取了较大的时间片,也不能让静态优先级较低的进程无法运行。为了避免进程饥饿,当一个进程用完它的时间片时,它应该被还没有用完时间片的较低静态优先级的进程取代,为了实现这种机制,调度程序维持两个不相交的可运行进程的集合。

活动进程:这些进程还没有用完它们的时间片,因此允许他们运行

过期进程:可运行进程已经用完了它们的时间片,并因此被禁止运行,直到所有活动进程都过期。

其实总体方案要复杂,由于调度程序偏爱交互式进程。用完其时间片的活动批处理进程总是变成过期进程,用完其时间片的交互式进程通常仍是活动进程:调度程序重填其时间片并将它留在活动进程的集合中。

但是如果最老的过期进程已经等待了很长时间,或者过期进程比交互式进程的静态优先级高,调度程序就把用完时间片的交互式进程移到过期进程集合中。结果,活动进程集合最终会变为空,过期进程将有机会运行。

实时进程的调度

一篇写的好的博客

实时进程的优先级:1(最高优先级)~99(最低优先级)的值。

实时进程运行的过程中,禁止低优先级进程的执行。与普通进程相反,实时进程总是被当作活动进程。

通过调用(sched_setparam()和sched_setscheduler())改变进程的优先级

如果几个可运行的实时进程具有相同的最高优先级,则调度程序选择第一个出现在与本地CPU的运行队列相应链表中的进程

实时进程被取代的情况:

  • 实时进程被另一个有更高实时优先级的实时进程抢占。
  • 实时进程执行了阻塞操作并进入睡眠(处于TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE状态)。
  • 进程停止(处于TASK_STOPPED或TASK_TRACED状态)或被杀死(处于EXIT_ZOMBIE或EXIT_DEAD状态)。
  • 进程通过调用系统调用sched_yield(),自愿放弃CPU。
  • 进程是基于时间片轮转的实时进程(SCHED_RR),而且用完了自己的时间片。

调度程序所使用的数据结构

进程链表链接所有的进程描述符,而运行队列链表链接所有的可运行程序的进程描述符,但swapper进程除外。

数据结构runqueue(linux2.6最重要的数据结构)

系统中的每个CPU都有它自己的运行队列,所有的runqueue结构存放在runqueues每CPU变量中。宏this_rq()产生本地CPU运行队列的地址,而宏cpu_rq(n)产生索引为n的CPU的运行队列的地址。

runqueue结构的字段

在这里插入图片描述

在这里插入图片描述

系统中的每个可运行进程只属于一个运行队列。只要在同一个运行队列中,它就只可能在拥有该运行队列的CPU上执行。但是可运行进程会从一个运行队列迁移到另一个运行队列。

prio_array_t [2] arrays

arrays是一个数组,包含两个prio_array_t结构,每个prio_array_t都表示一个可运行进程的集合,并包括140个双向链表表头(每个链表对应一个可能的进程优先级)、一个优先级位图和一个集合中所包含的进程数量的计数器。

在这里插入图片描述

在这里插入图片描述

active和expired字端分别指向arrays中的其中一个prio_array_t数据结构(一个为活动进程的可运行进程的集合,一个过期进程的可运行进程集合)

在这里插入图片描述

进程描述符

在这里插入图片描述

与调度程序相关的进程描述符字端

在这里插入图片描述

在这里插入图片描述

当新进程被创建的时候,由copy_process()调用的函数sched_fork()用下述方法设置current进程(父进程)和p进程(子进程)的time_slice字段:

p->time_slice = (current->time_slice + 1)>>1;
current->time_slice >>=1;

(从上面算法可以看出)父进程剩余的节拍数被分为两份,一份给父进程,另一份给子进程。(这样做可以避免获得无限CPU的时间:父进程创建一个相同的代码的子进程,然后杀死自己,适当调节创建的速度,子进程可以总是在父进程过期之前获得新的时间片。但是内核并不奖赏创建,所以这样没用)所以,一个进程不能通过创建多个后代来霸占资源(除非它有给自己实时策略的特权)。

特殊情况:

如果父进程只有一个时间片,根据上面的算法current->time_slice会被置为0,p->time_slice会变为1,这种情况下,copy_process()把current->time_slice重新置为1,然后调用scheduler_tick()递减该字段

copy_process()

也初始化子进程描述符中与进程调度相关的几个字段:

p->first_time_slice = 1;//如果进程肯定不会用完其时间片,就把该标志设为1
p->timestamp = sched_clock();//进程最近插入运行队列的时间,或涉及本进程的最近一次进程切换的时间

p->first_time_slice = 1;

因为子进程没有用完它的时间片(如果一个进程在它的第一个时间片内终止或执行新的程序,就把子进程的剩余时间奖励给父进程),所以first_time_slice被置为1.

p->timestamp = sched_clock();

sched_clock()产生的时间戳初始化p->timestamp,实际上,函数 sched_clock()返回被转化成纳秒的64位寄存器TSC

调度程序所使用的函数

其中几个重要的函数:

scheduler_tick()

维持当前最新的time_slice计数器

try_to_wake_up()

唤醒睡眠进程

recalc_task_prio()

更新进程的动态优先级

schedule()

选择要被执行的新进程

load_balance()

维持多处理器系统中运行队列的平衡

TIF_NEED_RESCHED的位置与定义

在这里插入图片描述

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱写代码的刚子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值