当前位置: 首页 > news >正文

Linux内核设计——(二)进程调度

目录

一、进程调度简介

二、多任务

三、调度器

3.1 I/O消耗型和处理器消耗型进程

3.2 进程优先级

3.3 CFS算法

3.4 实时调度策略

3.5 SCHED_FIFO

3.6 SCHED_RR

3.7 调度器入口

四、上下文切换

4.1 睡眠和唤醒

4.2 need_resched标志

4.3 用户抢占

4.4 内核抢占


一、进程调度简介

进程调度程序(调度程序)被视为在运行态进程之间分配有限的处理器时间资源的内核子系统,负责决定将何进程投入运行,何时运行以及何长运行时间。

调度程序是多任务操作系统的基础。只有通过合理调度,系统资源才能最大限度发挥作用,多线程才会有并发执行的效果。

最大限度利用处理器时间的原则为,只要有可以执行的进程,那么就总会有进程正在执行。

但是只要系统中可运行的进程的数目比处理器的个数多,那么某一时刻则会有一些进程不能执行。这些进程在等待运行。在一组处于可运行状态的进程中选择一个来执行,是调度程序所需完成的基本工作。


二、多任务

多任务操作系统,即能并发地交互执行多个进程的操作系统。

在单处理器机器上,这会产生多个进程在同时运行的幻觉;

在多处理器机器上,这会使多个进程在不同的处理器上真正同时、并行地运行。

多任务操作系统分为两类:

  • 非抢占式多任务
  • 抢占式多任务

Linux提供抢占式的多任务模式——完全公平调度算法,简称CFS

该模式下,由调度程序来决定何时终止一个进程的运行,以供其他进程运行的机会——该行为被称之为强制挂起,即抢占

进程被抢占之前都有一个预先设置好的运行时间——分配给每个可运行进程的处理器时间段,即进程时间片

非抢占式的多任务模式下,除非进程自己主动挂起,即让步,否则会一直运行。该机制有很多缺陷,如无法预料进程独占的处理器时间,悬挂进程不做出让步等等。


三、调度器

3.1 I/O消耗型和处理器消耗型进程

进程可以被分为:

  • I/O消耗型
  • 处理器消耗型

I/O消耗型进程指进程大部分时间用来提交或等待I/O请求。

这里的I/O指任何类型的可阻塞资源,如多数用户图形界面程序,即GUI,都属于I/O密集型,还有键盘输入或是网络I/O等等。

因此,I/O消耗型进程常处于可运行状态,但运行时间通常较短,因为它们在等待更多的I/O请求时最后总会阻塞。调度策略更倾向于I/O消耗型程序,以提供更好的程序响应速度

处理器消耗型指进程大部分时间用来执行代码。除非被抢占,否则它们通常一直不断运行,毕竟因为没有太多I/O需求。但是,因为它们不属于I/O驱动类型,所以从系统响应速度考虑,调度器不应该经常让它们运行。对于这类处理器消耗型的进程,调度策略往往是尽量降低其调度频率,从而延长其运行时间

所以,调度策略通常要在两个矛盾的目标中间寻找平衡:进程响应迅速(短时间响应)和最大系统利用率(高吞吐量)。

3.2 进程优先级

Linux采用两种不同的优先级范围:

  • nice值
  • 实时优先级

nice值,即时间片的比例,实现静态优先级。的范围为-20 ~ +19,默认为0。nice值越高则进程优先级越低,nice值越低则进程优先级越高。可以在Linux终端通过Bash Shell查看:

ps -el

结果中标记为NI的列即进程对应的nice值:

实时优先级的范围为0 ~ 99,其值可配置。和FreeRTOS一样,该值越高则进程优先级越高,该值越低则进程优先级越低。可以在Linux终端通过Bash Shell查看:

ps-eo state,uid,pid,ppid,rtprio,time

结果中标记为RTPTIO的列即进程对应的实时优先级,若有进程显示“-”,则说明它不是实时进程:

3.3 CFS算法

对于I/O消耗型进程而言,我们有两个目标:

  1. 希望系统能给与它更多的处理器时间。这并非指它需要很多的处理器时间,而是因为我们希望在它需要时总能得到处理器。
  2. 希望它在被唤醒时(即触发I/O事件)能够抢占处理器消耗型进程,以便保证它很好的交互性能。

试想,有以下两个可运行的进程:一个文本编辑器(I/O消耗型)和一个视频解码程序(处理器消耗性)。

显然,对于文本编辑器,无论用户的输入速度有多快,都无法赶上处理器处理速度,而用户往往希望按键按下系统便可马上响应;对于视频解码程序,处理器往往很容易被占用,而虽然它完成的越快越好,但用户并分辨不出来也不关心它是否立即执行或者半秒钟之后才开始执行。

这样的场景下,因为文本编辑器属于交互式应用,理想情况则是调度器应当给予文本编辑器相比视频解码程序更多的处理器时间。

Linux则通过完全公平调度算法(CFS)实现上述目标。CFS是一个针对普通进程的调度器类(Linux 调度器是以模块方式提供的——为了允许不同类型的进程可以有针对性地选择调度算法。这种模块化结构即调度器类),在Linux中被称为SCHED_NORMAL,定义在kernel/sched_fair.c中。

此时不再通过给I/O消耗型进程分配给定的优先级和时间片,而是分配一个特定的处理器使用比:

试想,若文本编辑器和视频解码程序为仅有的两个运行进程,且具备同样的nice值。

显然,处理器的使用比为50%——即平分处理器时间。但由于文本编辑器将更多的时间用于等待用户输入,因此肯定不会用到处理器的50%。同时,视频解码程序无疑将能有机会用到超过50%的处理器时间以便它能更快速地完成解码任务。

此时,CFS注意到给文本编辑器的处理器使用比是50%,实则它却用得少之又少。尤其是CFS发现文本编辑器比视频解码器运行的时间短得多。

这样的场景下,为了兑现让所有进程能公平分享处理器的承诺,它会立刻抢占视频解码程序,让文本编辑器投入运行。

由此可见,CFS实现动态优先级。 

3.4 实时调度策略

Linux提供两种实时调度策略:

  • SCHED_FIFO
  • SCHED_RR

SCHED_NORMAL为普通的、非实时的调度策略。

借助调度类的框架,这些实时策略并不被完全公平调度器来管理,而是被一个特殊的实时调度器管理,具体的实现定义在文件kernel/sched_rt.c中。

Linux的实时调度算法提供了一种软实时工作方式。软实时的含义是,内核调度进程,尽力使进程在它的限定时间到来前运行,但内核不保证总能满足这些进程的要求。

两种实时算法实现的皆为静态优先级

3.5 SCHED_FIFO

SCHED_FIFO实现了一种先入先出的,不使用时间片的,简单调度算法:

处于可运行状态的SCHED_FIFO级的进程会比任何SCHED_NORMAL级的进程都先得到调度。一旦一个SCHED_FIFO级进程处于可执行状态,就会一直执行,直到它自己受阻塞或显式地释放处理器为止——它不基于时间片,可一直执行下去。

只有更高优先级的SCHED_FIFO或者SCHED_RR任务才能抢占SCHED_FIFO任务;

如果有两个或者更多的同优先级的SCHED_FIFO级进程,它们会轮流执行,但是依然只有在它们愿意让出处理器时才会退出;

只要有SCHED_FIFO级进程在执行,其他级别较低的进程就只能等待它变为不可运行态后才有机会执行。

3.6 SCHED_RR

SCHED_RR与SCHED_FIFO大体相同,只是SCHED_RR级的进程在耗尽事先分配给它的时间后就不能再继续执行了。即,SCHED_RR带有时间片的SCHED_FIFO——一种实时轮流调度算法。

当SCHED_RR任务耗尽它的时间片时,在同一优先级的其他实时进程被轮流调度。时间片只用来重新调度同一优先级的进程。

3.7 调度器入口

Linux进程调度的主要入口为函数schedule(),定义在文件kermel/sched.c中。

它正是内核其他部分用于调用进程调度器的入口:选择何个进程可以运行,何时将其投入运行。Schedule()通常都需要和一个具体的调度类相关联,即它会找到一个最高优先级的调度类——后者需要有自己的可运行队列,然后问后者何者才是下个该运行的进程。


四、上下文切换

4.1 睡眠和唤醒

睡眠/休眠(即被阻塞)的进程处于一个特殊的不可执行状态——若没有这种特殊状态,调度程序就可能选出一个本不愿意被执行的进程。

进程休眠的原因多种,皆是为了等待目标事件,如获取被占用的内核信号量,对文件read()和获取键入等等。但无论何种情况,内核的操作相同:进程将自身标记为休眠状态,放入等待队列(相关API可参考Linux驱动开发——(七)Linux阻塞和非阻塞IO),然后调用schedule()选择和执行一个其他进程

唤醒通过wake_up()进行,它会唤醒指定的等待队列上的所有进程。如当磁盘数据到来时,VFS(虚拟文件系统)就要负责对等待队列调用wake_up(),以便唤醒队列中等待这些数据的进程。

4.2 need_resched标志

上下文切换,即从一个可执行进程切换到另一个可执行进程。

内核必须知道在什么时候调用schedule()。

若仅靠用户程序代码显式地调用schedule(),它们可能就会永远地执行下去。

内核提供了一个need_resched标志以表明是否需要重新执行一次调度。

比如,当某个进程应该被抢占时,scheduler_tick()就会设置这个标志;当一个优先级高的进程进入可执行状态的时候,try_to_wake_up()也会设置这个标志,内核检查该标志确认其被设置,调用schedule()来切换到一个新的进程;在返回用户空间以及从中断返回的时候,内核也会检查need_resched标志。如果已被设置内核会在继续执行之前调用调度程序。

函数描述
set_tsk_need_resched()设置指定进程中的need_resched标志
clear_tsk_need_resched()清除指定进程中的need_resched标志
need_resched()检查need_resched标志的值,如果被设置就返回真,否则返回假

该标志对于内核来讲是一个信息,它表示有其他进程应当被运行了,要尽快调用调度程序。

每个进程都包含一个need_resched标志,这是因为访问进程描述符内的数值要比访问一个全局变量快(因为current宏速度很快并且描述符通常都在高速缓存中)。

4.3 用户抢占

内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占

在内核返回用户空间的时候,它知道自己是安全的,因为既然它可以继续去执行当前进程,那么它当然可以再去选择一个新的进程去执行。所以,内核无论是在中断处理程序返回用户空间,还是在系统调用后返回用户空间,都会检查need_resched标志。如果它被设置了,那么,内核会选择一个其他更合适的进程投入运行。

4.4 内核抢占

在不支持内核抢占的其它系统内核中,内核代码可以一直执行直到它完成为止。即,调度程序没有办法在一个内核级的任务正在执行的时候重新调度——内核中的各任务是以协作方式调度的,不具备抢占性。

而Linux支持内核抢占。只要没有持有,重新调度就是安全的,内核就可以进行抢占。

相关文章:

  • CMake实战指南一:add_custom_command
  • 手撕算法——宽度优先搜索-BFS
  • Shell脚本编程之正则表达式
  • JS DOM节点增删改查
  • Spring事务传播机制
  • 算法(动态规划)
  • elasticsearch索引数据备份与恢复
  • Python基于OpenCV和SVM实现中文车牌识别系统GUI界面
  • 【STL 之速通pair vector list stack queue set map 】
  • Linux系统学习Day04 阻塞特性,文件状态及文件夹查询
  • LeetCode 416、606题解(中等dp、回溯)
  • FPGA_DDR(一) 仿真
  • continew-admin的报错问题
  • HTTPS在信息传输时使用的混合加密机制,以及共享、公开密钥加密的介绍。
  • Java Flow 编程:异步数据流介绍
  • 学习日记-0407(Inductive Matrix Completion Using Graph Autoencoder)
  • C盘清理——快速处理
  • SOLIDWORKS 2025教育版有效的数据管理与团队协作
  • Android studio学习之路(六)--真机的调试以及多媒体照相的使用
  • NXP i.MX 平台下双平台设备驱动解析:`imx-lcdifv3` 与 `imx-drm` 的实战解剖
  • 文件大小 wordpress/seo排名查询工具
  • 武汉个人做网站联系电话/专业黑帽seo
  • 郑州网站制作公司/app推广全国代理加盟
  • 自己怎么做网站首页/被忽悠去做网销了
  • wordpress模版建站/今日足球赛事数据
  • php7.3能装wordpress/seo建设招商