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

FreeRTOS学习(2)——FreeRTOS的任务调度

文章目录

  • 前言
  • 任务
    • 任务与函数相比
    • 任务与进程线程相比
      • 任务与进程类比
      • 任务与线程类比
      • 总结
  • 什么是调度
  • 通用操作系统的调度机制(了解)
  • 进程的三种状态
    • 三状态模式的经典表述
    • 三状态模型的现代意义
    • 总结
  • FreeRTOS的调度机制
    • FreeRTOS提供的延时函数
    • 任务的优先级调度
    • 任务的抢占式调度
    • 时间片轮转机制
  • 总结:FreeRTOS的任务调度机制

前言

本文的内容标题叫做《FreeRTOS的任务调度》,是纯粹的理论课程,但是重点中的重点。

FreeRTOS 的任务调度,是整个 RTOS 学习中最核心、最需要理解的一部分。

如果这部分内容不求甚解,那么到后续学习FreeRTOS的使用,就会彻底变成“API Caller”,而且将完全无法调试FreeRTOS

本文的内容,将围绕两个关键字展开:

  1. 调度
  2. 任务

这两个中文名词非常简单,大家都知道它们啥意思。

但是在FreeRTOS中,它们是核心的专业术语,下面我们将逐一解释它们。

在 FreeRTOS 中,任务是调度机制的核心。

换句话说,FreeRTOS 正是以任务为基本单位来实现调度的。

因此,在讨论调度机制之前,首先必须弄清楚什么是任务。

任务

什么是任务呢?

在 FreeRTOS 中,“任务”是一个非常核心的概念。如果这一点理解错了,后面所有内容都会跟着歪。

我们先给一个标准、规范、不带情绪的定义

在 FreeRTOS 中,任务(Task)是操作系统进行调度的基本单位。

FreeRTOS的内核调度器不会直接调度函数,也不会调度中断,而是调度任务。

也就是说,FreeRTOS 的一切“并发运行”“切换执行”,本质上都是围绕任务展开的。

这是标准的定义讲法。

下面分两个角度,具体来讲述一下,什么叫做任务。

任务与函数相比

在没有 FreeRTOS 的情况下,进行裸机开发时,如果系统中存在下列需求:

  1. LED 闪烁
  2. 串口接收
  3. 传感器采样
  4. ……

这些“事情”,通常都会被我们封装成一个个函数。

然后这些函数:

  1. 要么放在while(1)循环中轮流执行
  2. 要么靠一些手动设定的标志位,在某些时机执行
  3. 要么放到中断服务函数中,在特定事件发生时执行。

总之不管采用哪种方式,在裸机开发的前提下,有一点是共通的:

这些“事情”之间的执行顺序、执行时机、是否需要等待,全部需要程序员自己来管理。

  1. 谁先执行
  2. 谁后执行
  3. 谁该多跑一会
  4. 谁该让一让

这些原本和“业务无关”的问题,在裸机开发时,全都落到了程序员头上。


而现在引入了 FreeRTOS 之后,情况发生了变化。

在前面我们已经反复强调过一点:

FreeRTOS 的核心价值,就是帮助我们管理多件“事情”的执行关系。

也就是说,程序员不再需要亲自安排这些“事情”什么时候执行、执行多久、什么时候暂停。

那么问题就来了:

既然这些“事情”本质上就是一个函数。

现在把这些“事情”交给 FreeRTOS 管理,是不是就等价于:把一个个函数,交给 FreeRTOS 管理?

我们自然就会产生一个疑惑:一个任务,是不是就是一个函数呢?

FreeRTOS 的任务调度,是不是就是在安排函数的先后执行?


从表面现象上看,这种理解并不是错误的。

因为,从代码上看,一个 FreeRTOS 的任务,就是一个函数,就是一段要执行的代码。

只不过这个函数稍微有点特殊:

  1. 函数内部需要加一个死循环。
  2. 函数不能使用return,不允许有返回值,任务的函数不允许自然结束。

所以,把任务看成函数,FreeRTOS调度任务的执行顺序,其实就是决定函数的调用顺序。

表现上这么理解是没问题的。

但如果仅仅把任务理解成“函数”,那这个理解就太停留在表面了。

可以说:任务≈函数,任务实际上不完全等于函数。

任务实际上要比函数拥有更多的特质,为了讲清楚这些特质,我们不妨来谈一谈通用操作系统中的进程和线程。

任务与进程线程相比

通用操作系统中,存在进程与线程的概念。

如果大家学过《操作系统》这门课程,肯定对进程和线程都不陌生。

如果你没学过,也没有关系,我们这里简单谈一谈线程与进程,将它们和任务做一下类比。

任务与进程类比

在通用操作系统中,什么是进程呢?

这个概念我们并不陌生,因为早在 C 语言阶段,我们就已经接触过它。

所谓进程:

可执行程序被加载到内存中,运行中的、受操作系统调度执行的程序就是一个进程。

在通用操作系统中,进程是操作系统进行资源分配与隔离的基本单元。

那什么叫“资源分配与隔离的基本单元”?

意思是:

  1. 操作系统在分配资源时,并不是随便给“一段代码”或者“一个函数”分配资源。
  2. 而是以进程为最小载体,统一管理和分配资源。
  3. 而且不同的进程之间是隔离的,它们的资源是各自独立拥有的。

那具体分配管理哪些资源呢?

操作系统为进程分配资源,最主要的、最核心的资源就是:内存资源。

进程在执行过程中必然要占用内存资源,而这些内存资源正是由操作系统以进程为单位进行分配和管理的。

比如我们之前C阶段学过的虚拟内存空间,我们称之为**“进程的”**虚拟内存空间:

通用操作系统为每一个进程都分配独立的虚拟内存空间,这个过程实际上就是以进程为基本单元,分配内存资源,而且相互独立隔离。


进程隔离,是一个非常有意思、也非常有工程价值的设计。

我们先抛一个问题给大家思考:

一个程序,一定只对应一个进程吗?

当然不是。

一个程序,完全可以由多个进程组成。一个软件,本身就可以被设计成多进程架构。

那为什么一款软件需要设计成多进程呢?

先说缺点。

进程是操作系统分配资源的基本单位,它的创建、销毁、切换成本都比较高。

因此,多进程的软件通常会表现出这些特点:

  1. 进程数量多
  2. 内存占用大
  3. 整体显得比较“臃肿”
  4. 在性能上,通常也表现平庸

所以从纯性能角度看,多进程并不是一个“讨喜”的选择。

那它的优点是什么呢?

一句话概括就是:隔离

进程之间的内存空间是完全独立、完全隔离的,这意味着:

  1. 某一个进程崩溃
  2. 不会直接影响到其他进程
  3. 不会出现“牵一发而动全身”的情况

也就是说:

即便软件内部有一个模块进程出问题,整个软件也不至于直接崩掉。

除此之外,由于进程之间互相隔离、互不干扰,多进程架构在安全性方面也具有天然优势。

如果你设计一款软件时,核心目标是:稳定性和安全性,而不是极限性能。

那么,多进程架构就是一个非常值得考虑的方案。

一个最典型、也最容易理解的案例,就是:浏览器。

以 Windows 平台上的主流浏览器为例:

  1. 每一个标签页,往往对应一个独立的进程
  2. 插件、渲染模块,也可能运行在独立进程中

这直接导致了一个结果:

  1. 浏览器整体显得比较臃肿
  2. 占用内存较多
  3. 运行体验谈不上“轻快迅速”

但换来的好处是什么呢?

  1. 某个网页卡死,只影响当前标签页
  2. 某个页面崩溃,不会拖垮整个浏览器
  3. 即便访问恶意网页,破坏范围也被限制在单个进程内

而这,正是浏览器最看重的东西。

毕竟:

一个天天崩溃的浏览器,肯定没人用;

一个不安全的浏览器,更是不可接受。

这就是通用操作系统中的进程,大家简单了解一番即可。


接下来,我们把视角重新拉回到 FreeRTOS。

FreeRTOS 是一种面向MCU的实时操作系统,它并不提供通用操作系统中那样复杂的进程机制。

但操作系统的本质是一个中间层,向下管理硬件资源,向上为程序运行提供运行环境,FreeRTOS 也不例外。

也就是说,FreeRTOS 同样需要管理单片机有限的内存资源(C8T6的 20KB RAM),那么它进行资源管理的基本单元是什么呢?

没错。

在 FreeRTOS 中,管理资源的基本单元是任务(Task)。

当我们在 FreeRTOS 中创建一个任务时,操作系统会为该任务分配一段专属的内存区域,用于保存该任务的数据与信息。

这片专属的内存区域,内存资源,用以支撑任务的独立运行。

关于任务的内存区域具体如何划分、各自都存什么内容、都有哪些作用。

这些东西并非三言两语可以讲清楚,我们将在后续章节中详细展开。

在这里,我们只需要先记住一个结论:

在 FreeRTOS 中,任务不仅仅是代码的执行载体(函数),也是操作系统进行资源管理的基本单元。

任务与线程类比

在线程这个概念出现之前,通用操作系统中,进程是资源管理的基本单元。

这里的资源,除了内存资源之外,还有一个同样重要、甚至更加核心的资源,那就是CPU 资源

操作系统可以决定:哪一个进程能够上 CPU,能够在某一个时刻获得 CPU 的执行权。

但进程相比较而言还是太“重量级”了。进程在创建、销毁以及切换时,都需要付出较大的系统开销。

如果仅仅为了在一个程序内部实现多条执行路径,使用多进程的方式,在追求性能时,显然并不是一个理想的选择。

如果使用多进程的程序来实现多条执行路径,在追求性能时,必然不是一个好的选择。

于是,线程便应运而生了。

从某种意义上来说,线程可以看作是一个轻量级的进程执行单元,它具有以下几个典型特点:

  1. 线程是操作系统调度和管理 CPU 资源的基本单元。
  2. 线程依托于进程存在,且一个进程至少有一个线程。
  3. 线程本身并不独立拥有内存资源,内存资源分配的基本单元仍然是进程。
  4. 同一个进程内部可以同时存在多个线程,这就是多线程。
  5. 一个线程本质上就是一条独立的执行路径,在多线程环境下:
    1. 进程的代码段、数据段、堆等内存区域,通常由线程共享。
    2. 每一个线程都拥有各自独立的栈。
  6. 线程之间共享数据,线程之间的相互影响远大于进程之间,其安全性和隔离性不如进程。
  7. 当然由于线程间的数据共享,再加上其轻量化的设计,线程的使用性能要远好于进程。

CPU 的资源分配,决定谁上CPU,由操作系统调度器统一管理。

调度器的职责,就是在多个线程之间进行调度,决定在某一个时刻,哪一个线程可以获得 CPU 执行权。

通过对多个线程进行快速切换,让它们轮流占用 CPU,只要切换的速度足够快,那么对于人来说,就几乎无法察觉时间上的差异。

这就是所谓的并发执行


在多线程系统中,CPU 在同一时刻只能执行一个线程的指令。

当系统需要从A线程切换到另一个线程B执行时,就会面临一个问题:

A线程下一次再执行时,怎么知道自己执行到哪里了呢?

总不能A线程再次执行时,从头再来吧?

为了解决这个问题,操作系统在进行线程切换时,会把当前线程的执行状态,或者说执行瞬间的快照完整保存下来。

这套用于描述线程“当前执行状态”的完整信息,在操作系统术语中,被称之为**“上下文”**。

所以,这里需要注意的是:

线程在发生切换(上下文切换)时,会完整保存当前的执行状态(上下文状态)。

当该线程再次获得 CPU 执行权时:

系统会恢复它之前保存的执行状态(上下文),使线程能够从上一次被切换的位置继续执行,而不是从头重新开始执行。

也就是说,线程的切换,实际上就是线程上下文的保存与恢复。


也正是由于线程具备轻量、数据共享、并发执行等特点,多线程程序往往能够获得极高的执行效率。

因此,在后端服务器、游戏服务器等对性能要求极高的场景中,通常都会采用多线程的设计方式,以追求系统的整体性能。


那么,现在我们把视角从通用操作系统,切换到 FreeRTOS。

FreeRTOS 同样需要管理 CPU 资源,同样需要决定:

在某一个时刻,哪一段代码可以上 CPU 执行。

那么,在 FreeRTOS 中,管理 CPU 资源的基本单元是什么呢?

那么FreeRTOS管理CPU资源的基本单元是什么呢?

没错,还是任务。

任务,是 FreeRTOS 调度 CPU 的基本单元。。

在 FreeRTOS 中,同样存在一个调度器,用来统一管理 CPU 的执行权,而调度器所直接管理的对象,正是任务。

也就是说,在 FreeRTOS 中:

  1. 谁能上 CPU
  2. 谁要暂时让出 CPU
  3. 谁需要等待
  4. 谁可以继续执行

全部都是围绕“任务”来进行的。

从行为的角度来看,FreeRTOS 中的任务,与通用操作系统中的线程是高度相似的。

具体表现在:

  1. 任务是一条独立的执行路径,所以任务也有自身的独立栈区。
  2. 任务可以被调度、被切换
  3. 同一时刻,CPU 上只会执行一个任务
  4. 多个任务通过快速切换,实现“并发执行”的效果
  5. 任务切换与线程切换在机制上是完全一致的,本质上都涉及执行上下文的保存与恢复:
    1. 在发生切换时,保存当前的上下文状态;
    2. 再次获得 CPU 执行权时,恢复上下文状态,从上一次被切换的位置继续执行。

这一点,与线程在通用操作系统中的运行方式几乎一致。

所以,你会发现:任务和线程非常类似,它具有线程很多特点。

总结

总结:如何正确理解 FreeRTOS 中的任务

在通用操作系统中:

  1. 进程主要负责资源管理
  2. 线程主要负责 CPU 执行与调度

而在 FreeRTOS 中,并不存在进程与线程的概念,只有任务(Task)。

我们类比进程和线程,可以给任务下一个相对完整、专业的术语定义:

在 FreeRTOS 中,任务是操作系统进行资源管理和 CPU 调度的基本单元。

操作系统会为每一个任务分配其运行所需的资源,同时由调度器统一决定每一个任务是否能够获得 CPU 的执行权。

以上描述,属于相对标准、偏术语化的表述。

如果在面试中被问到“FreeRTOS 中的任务是什么”,用这样的回答是完全没有问题的。

但如果从更通俗的角度来理解任务,可以这么理解:

任务,可以看作是一个拥有独立运行资源的线程。

为什么这么说呢?

一方面,从资源管理的角度看:

  1. FreeRTOS 会以任务为单位对系统资源进行管理
  2. 每一个任务确实都有自身独立的一块内存区域,用于存储自身信息与数据。
  3. 这是线程所没有的特点,这一点任务更类似进程。

但另一方面,从任务的实际使用上来说:

  1. 任务由FreeRTOS内核调度器统一调度执行
  2. 任务之间可以并发执行
  3. 任务之间可以进行上下文切换

FreeRTOS中的任务,都与通用操作系统中的线程行为高度一致。

从资源分配上来说,任务没有真正意义上独立隔离的内存区域,本质上所有任务还是共享同一个SRAM空间使用。

所以,FreeRTOS 中的任务更接近于 线程(Thread),而不是 进程(Process)。

比如说:

一块生姜切成丝,这一盘子生姜丝,怎么看都像土豆丝,但实际吃起来却满口姜味,那它是姜丝还是土豆丝呢?

可以说:

在 MCU 上运行的是一个程序(类似一个进程),FreeRTOS 在这个程序内部创建多个任务(类似线程)来实现并发执行。


当然,即便讲到这里,我们对任务的理解也只是一个起点。

随着后续对任务的深入学习,我们会不断加深对这一核心概念的认识。

学习 FreeRTOS,本身就是一个逐步构建理解的过程,不必急于一步到位。

什么是调度

在前面的内容中,我们已经对 FreeRTOS 中的任务有了一个粗略、初步的认识

接下来,我们开始进入另一个核心的话题——FreeRTOS 的调度机制。

首先,我们要搞清楚一个问题:什么叫做“调度”?

我们已经学习过任务,那么对于 FreeRTOS 来说,调度的含义就非常简单:

调度就是决定,某一个时刻,哪一个任务来上CPU,哪一个任务获得CPU的执行权。

CPU 很强,但它有一个致命缺点:同一时刻,只能干一件事。

无论是 STM32 这样的单片机,还是 PC、服务器,本质上都是如此。

一个 CPU 核心,在某一个“瞬间”,只能执行一段代码。

如果是通用计算机系统中的多核心CPU,它可以真正实现“并行执行”。

但嵌入式开发的MCU,显然没有这种条件,只有一个CPU核心。

假如我们要面临下面这些需求:

  1. LED 要闪
  2. 串口要收
  3. 传感器要采
  4. 通信要跑
  5. 报警要响应

这些功能同时存在,而且看起来都很着急,都希望立刻获得 CPU 去执行。

于是就产生了一个重要的问题:

谁先跑?谁后跑?谁等一等?谁必须马上插队?

解决这些问题的一整套规则,决定某个时刻谁在CPU上,这就是操作系统的调度机制。

在操作系统汇总,调度通常由专门的调度器完成,由调度器根据既定规则,“选”中某个执行实体上CPU,获取执行权。

通用操作系统的调度机制(了解)

通用操作系统中的调度机制,大致是什么样的呢?

以 Windows、Linux 这类通用操作系统为例,调度器每天面对的对象,往往非常庞杂:

  1. 几十个正在运行的进程
  2. 上百个线程
  3. 各种后台服务
  4. 驱动程序
  5. 系统守护进程

调度对象数量多、类型复杂,这是通用操作系统最显著的特点之一。

管理这么多对象,就像管理一个国家,就不能再在意一个两个“进程”的得失了,而是整体体验。

通用操作系统的调度目标,通常可以概括为三点:

  1. 尽量让用户感觉系统运行得很流畅
  2. 尽量提高 CPU 等系统资源的整体利用率
  3. 尽量让多个程序“看起来在同时运行”

关键字是:整体体验。

为了实现整体体验良好这个目标,通用操作系统采用了非常复杂且灵活多变的调度策略。

常见的调度策略/实现有:

  1. 时间片轮转
  2. 优先级调度
  3. 抢占式调度
  4. 先来先服务
  5. 多级反馈队列

这些策略的具体细节并不重要,也不要求大家记住。

在这里,我们只需要记住一个核心结论:

通用操作系统的调度目标是,整体体验优先,而不是绝对保证实时。

也就是说,在通用操作系统中:

  1. 某个程序晚10ms、20ms才获得 CPU,通常没什么大问题
  2. 因为用户感觉不出来,系统就认为这是完全可以接受的
  3. 甚至说,就算偶尔用户感觉出明显卡顿,也属于可容忍的范畴。

对于通用操作系统的调度器而言,它关注的是整体公平性和交互体验,而不是对某一个程序给出严格的时间保证。

但这样的调度机制策略,显然是不适合嵌入式系统的。

在一般嵌入式系统中,资源紧张,调度对象少,通常只有少数几个,但对实时性要求很高。

此时调度策略就不是关注整体体验了,而是:

  1. 必须重视个体
  2. 必须关注个体能否在确定时间内完成特点任务
  3. 整个系统高度重视实时性与确定性。

为了实现这些目标,在嵌入式系统中如果需要引入操作系统,我们通常会选择RTOS,也就是实时操作系统。

而将实时性与确定性作为调度的首要目标,这正是通用操作系统与实时操作系统的本质区别。

下面,我们就来重点看看:FreeRTOS 的调度机制是如何设计的。

进程的三种状态

为了讲清楚FreeRTOS的任务调度机制,我们首先要补充一个非常经典、非常基础的概念:进程的三种状态

在408课程《操作系统》这门课程中,这是一个经典的、必学的概念。

三状态模式的经典表述

在传统操作系统教科书中,是这么表述三状态模式的。

一个进程在生命周期中,一定会处于三种状态之一:

  1. 运行态(Running)
  2. 就绪态(Ready)
  3. 阻塞态(Blocked)

我们分别用一句人话来解释它们。

运行态,表示:

该进程当前正在占用 CPU,正在执行代码。

需要强调的是:在单核 CPU 上,同一时刻,只能有一个进程处于运行态,这是 CPU 的物理限制决定的。

就绪态,表示:

进程已经具备了运行的一切条件,但暂时还没有获得 CPU。

通俗一点说就是:代码没问题,数据准备好了,不缺资源,就差 CPU 没轮到我。

所以,就绪态的进程:随时可以运行

只要调度器把 CPU 分配给它,就能立刻进入运行态。

阻塞态,表示:

进程暂时无法继续执行,需要等待某个条件满足。

比如:

  1. 等待 IO 完成。
  2. 等待某个事件。
  3. 等待某个资源。
  4. 等待时间到期。

在阻塞态下,进程即使想运行,也没有资格参与 CPU 的竞争。

关于进程三种状态的结论:

  1. 在这三种状态中,只有“就绪态”的进程,才有资格参与调度,去竞争 CPU。
    1. 运行态:已经在 CPU 上了
    2. 阻塞态:没资格上 CPU
    3. 就绪态:处在“候选人”池中。
  2. 进程在这三种状态之间,会不断切换:
    1. 运行 → 阻塞(主动等待下CPU)
    2. 阻塞 → 就绪(条件满足,随时可以上CPU)
    3. 就绪 → 运行(被调度器选中,上CPU)
    4. 运行 → 就绪(被抢占,“被迫”下CPU)
  3. 调度器的核心职责,是在所有就绪态任务中,根据规则选出当前应当运行的任务,让它上CPU。

总之:

阻塞态没有资格竞争CPU,运行态已经占着 CPU,真正参与博弈的只有就绪态。

调度器所做的全部工作,就是在这些“候选人”中选出当前应该上 CPU 的那一个。

三状态模型的现代意义

当我们理解了运行态、就绪态、阻塞态这三个状态之后,很容易产生以下疑问:

现代操作系统,早就不是只有进程这种单一概念的早期操作系统了,还能够用三状态模型来描述吗?

如此复杂的现代操作系统,三状态模型是不是有点过时了?

事实上,三状态模型确实是上世纪60年代,批处理操作系统时期提出来的概念模型。

那么它对现代操作系统还有指导意义吗?

这个问题的答案,其实和下面这个问题的答案是一致的:

现代通用计算机系统,还能够用冯诺依曼体系来解释吗?

这两个问题的答案,显然都是肯定的。

现代通用计算机,从微观构成上,早就不符合冯诺依曼体系结构了,要更复杂的多得多。

但从设计哲学而言,冯诺依曼体系结构仍然是指导理论,基石理论,并没有过时。

三状态模型也是如此。

它诞生于批处理操作系统时代,但它所描述的并不是“当年的操作系统实现方式”,而是:

在多个执行实体共享 CPU 的前提下,CPU资源竞争所必然形成的三种基本逻辑状态。

只要满足两个条件:

  1. CPU 是有限资源;
  2. 存在多个执行实体同时竞争它;

那么每一个执行实体,在任意时刻,都只可能处于三种状态之一:

  1. 正在占用 CPU;
  2. 具备运行条件,但还没有获得 CPU;
  3. 暂时不具备获取CPU运行的资格。

无论操作系统如何进化,这三种状态逻辑位置始终存在。

现代操作系统,尤其是通用操作系统要比早期操作系统复杂的多,很多情况确实发生了改变:

  1. 现代操作系统提出了线程的概念,竞争CPU的实体从进程变成了线程。
  2. 线程的实际状态也早就不止3种,而是多达5~9种。

但根本上、本质上,所有复杂状态都可以被压缩回三类:

  1. 是否正在运行;
  2. 是否具备运行资格;
  3. 是否暂时失去运行资格。

这三类,本质上仍然对应:运行态、就绪态、阻塞态。

因此,三状态模型并没有被推翻,它只是被细化、被扩展、被工程化。

就像冯诺依曼体系并没有被推翻,只是微观硬件技术明显进步罢了。

理解这一点非常重要,因为后续我们讨论任何调度机制时,无论规则多么复杂,本质上都是在回答一个问题:

在所有“具备运行资格”的执行实体中,谁应该获得 CPU?

这就是调度的核心问题。

至于实体到底是进程还是线程,亦或者别的,都只是一个命名。

包括状态,就算再多复杂的状态设计,也不会脱离三状态的本质范畴。

而这个问题,正是建立在三状态模型之上的。

因此,三状态模型的价值,不在于它有多少状态,而在于它揭示了调度问题的逻辑骨架。

调度机制的实现可以变化,调度的策略可以复杂化,但指导哲学,骨架是不会改变的。

回到FreeRTOS这个简易、轻量化实时操作系统当中。

FreeRTOS没有进程,也没有线程,CPU资源竞争的实体是任务。

而实际上任务也不止三种状态,但这并不重要,并不影响其根本逻辑。

我们只需要记住:

FreeRTOS中,所有任务从根本逻辑上来说,实际上仍然具有三种核心状态:

运行态(Running)、就绪态(Ready)、阻塞态(Blocked)。

FreeRTOS的调度器,本质上也是在所有“就绪态”的任务中,挑选一个,让它进入运行态。

在操作系统教材中,这张状态图被称为“进程的三种状态”。

总结

实际上,我们完全可以给“三状态模型、进程的三种基本状态”换一个名字,换一个更贴切的名字:

CPU 资源调度问题的逻辑结构模型。

这张图描述的并不是某一具体操作系统的调度实现方式,而是CPU资源竞争所必然形成的三种逻辑状态。

而具体到实际的操作系统实现:

竞争CPU资源的实体名称可以改变,状态的名字也可以改变,状态可以更多样化,状态切换的结构可以更复杂化…

但不管怎么样,本质逻辑是不会改变的。

当然,如果你参加 408,还是要按教科书标准写:

“进程的三种状态”。

FreeRTOS的调度机制

在FreeRTOS中,调度的机制是什么样的呢?

FreeRTOS中,如何决定这一刻,哪一个任务能来上“CPU”呢?

简单直接的说,FreeRTOS的任务调度机制是:

FreeRTOS 是一个以“优先级”为核心的抢占式实时调度系统。

这句话里有三个关键词:

  1. 优先级
  2. 抢占式
  3. 实时

后面的内容,我们就一条一条把它拆开。

FreeRTOS提供的延时函数

为了更好的演示FreeRTOS的任务调度机制,借着讲完了任务的三种状态。我们需要提前了解一下FreeRTOS的延时函数。

在以往我们使用的延时操作诸如:

Delay_ms(1000);// 或者for(i=0;i<xxx;i++);

这些延时的本质都是:CPU空转等待时间过去。

说人话就是:把CPU霸占着,然后睡觉,把时间睡过去。

在裸机开发时,这是无所谓的,因为反正也没有“别人”,CPU都是我的,我想睡觉就睡。

但到了 FreeRTOS,情况完全变了。此时系统里:

  1. 不止一个任务
  2. CPU 是“公共资源”
  3. 由调度器统一分配

如果你在某个任务里写:

Delay_ms(1000);

等价于什么?

这个任务霸占 CPU 1 秒钟,调度器完全失效,你不干活,其他任务也不能上CPU干活。

为了解决这个问题,FreeRTOS 为我们提供了一个现成的延时实现函数:

voidvTaskDelay(constTickType_t xTicksToDelay);

注意:该函数只能用于任务内部,其余位置禁止使用它。

实际上,相当多FreeRTOS提供的函数,都只能在任务环境当中使用,主程序或中断中能够使用的函数比较少。

该延时函数的延时操作,和任务的状态是有关系的,具体来说是这样做的:

  1. 当前任务从“运行态”切换为“阻塞态”。
  2. 当前任务进入阻塞态后会让出 CPU,如果系统中存在其他就绪任务,则调度器会选择一个就绪任务进入执行态。
  3. 从不严格、不规范的理解角度来说:该函数会让任务在阻塞状态下,等待 xTicksToDelay 个系统节拍后再重新进入就绪态。
  4. 1个系统节拍究竟多长时间,是可以设置的。

大多数情况下,我们都会在FreeRTOS的配置文件中写下面这条配置:

#defineconfigTICK_RATE_HZ((TickType_t)1000)

这条配置设定1秒钟产生1000个Tick(系统节拍),所以一个系统节拍就是1ms。

所以vTaskDelay函数在这种默认设置下,不规范的理解,可以认为该函数是一个“以毫秒”为单位的延时函数。

关于这个延时函数,尤其是它的具体规范含义,我们后面会再详谈,今天我们只需要了解它的一些基本特点就足够了。

任务的优先级调度

在前面我们已经反复强调过一句话:

调度器,只在“就绪态”的任务中做选择。

那么问题就变得非常具体了:

在FreeRTOS当中,如果同时存在多个就绪态任务,调度器凭什么决定,这一刻,谁能上 CPU?

答案只有一个:任务的优先级

在 FreeRTOS 中,创建任务时,都需要为任务指定一个优先级数值。

需要注意的是,这里的任务优先级,和之前学过的中断优先级不同:

任务优先级数值越大,优先级越高。

优先级越高的任务,越紧急,在诸多进入就绪态的任务中,会被调度器优先调度执行。

通常情况下,FreeRTOS的任务在创建时,可选优先级为0~4,数值越大代表优先级越高。

站在调度器的视角,它是这样根据优先级来选择任务的:

  1. 如果某一时刻有多个任务同时处于就绪态,那么调度器会选择优先级最高的任务进入运行态,占用 CPU。
  2. 在没有更高优先级任务变为就绪态之前,当前优先级最高的就绪任务将持续获得 CPU 执行权,优先级较低的就绪任务不会被调度执行。
  3. 处于阻塞态的任务不参与 CPU 的竞争,即便它的优先级非常高,也必须等到阻塞条件解除,重新进入就绪态之后,才有资格参与调度。

FreeRTOS的优先级调度机制是任务调度的基本策略,核心策略,后续的其它策略都是在优先级之上发展而来的。

比如,我们来试想一种情况:

现在一个优先级为1的任务,执行一个死循环,持续占据CPU。

如果调度机制只看优先级,那么问题就来了:

这个任务既不阻塞,也不主动让出 CPU,那它是不是可以一直霸占 CPU?

其他任务是不是就永远没有机会执行了?

答案取决于一个非常关键的机制:是否开启抢占式调度。

任务的抢占式调度

是否开启抢占式调度,FreeRTOS要求必须明确在配置文件中进行设置:

#defineconfigUSE_PREEMPTION1

如果不明确配置这个宏,FreeRTOS会编译报错。

上述宏定义为1表示开启抢占式调度,若为0则表示关闭抢占式调度。

一般情况下,我们使用官网下载模板中的FreeRTOS配置文件,抢占式调度是默认开启的。

那什么是抢占式调度呢?

在 FreeRTOS 中,如果开启了抢占式调度,那么调度规则会发生一个根本性的变化:

正在运行的任务,并不一定能一直运行下去。

只要在系统运行过程中,出现了一个优先级更高的任务进入就绪态。

调度器就会在合适的时机,强制让当前任务让出 CPU,并立即切换到这个更高优先级的任务上执行。

被抢占的任务,会重新回到就绪态,继续等待竞争CPU。

也就是说:

CPU 的控制权,不完全掌握在“当前任务”手里,而是随时可能被更高优先级的任务抢走。

这就是“抢占”这两个字的真正含义。

回到刚才那个例子:

即便优先级为 1 的任务在死循环中不停地运行,

只要系统中出现了一个优先级高于 1 的任务,并且该任务进入了就绪态。

调度器会尽快打断当前任务的执行,让高优先级任务获得 CPU。

于是,一个非常重要的结论就出现了:

在抢占式调度机制下,高优先级任务永远不会被低优先级任务“饿死”。

这正是 FreeRTOS 能够满足实时性要求的核心原因之一。

当然,需要注意的是:

抢占并不是“随时、无条件”发生的,它的发生是存在条件的,它的任务切换是需要时机的。

关于这些具体的细节,我们后面会再单独展开讲。

在这一节中,只需要牢牢记住一句话就够了:

抢占式调度,保证了:一旦更高优先级任务准备好运行,就一定能尽快获得 CPU。

现在关于优先级,我们已经了解到:

  1. 高优先级的任务,在就绪态时,会优先进入执行态。
  2. 如果开启抢占式调度,那么高优先级的任务,在就绪态时,可以抢占低优先级任务的执行。

那么问题来了:

如果两个任务优先级是一样的,也同时进入了就绪状态,那么会发生什么情况呢?

时间片轮转机制

如果两个任务的优先级是一样的,那么事情就和前面讲的情况不一样了。

因为此时,调度器已经无法再通过优先级高低来做选择。

那么问题就变成了:

如果两个(或多个)任务优先级完全相同,而且它们又同时处于就绪态,调度器该怎么办?

FreeRTOS 给出的解决方式是:时间片轮转(Time Slicing)。

在开启时间片轮转的情况下,处于同一优先级的多个就绪态任务,会轮流获得 CPU 的执行权。

具体来说,调度行为表现为:

同一优先级的任务,被组织成一个就绪队列,调度器从队列头部选出一个任务运行。

该任务运行一个时间片之后,就会被切换到队列尾部

下一个同优先级任务获得 CPU

这样一来,每个同优先级任务都可以“轮流跑一会儿”,不会出现某一个任务长期独占 CPU,也不会出现同优先级任务被饿死的情况。

那么一个时间片,到底多长时间呢?

1个时间片的长度,其实就是1个系统节拍的长度。

在FreeRTOS配置文件中,必须定义下面的一个宏:

#defineconfigTICK_RATE_HZ((TickType_t)1000)

这个宏的意思是:1秒钟内,系统产生1000个系统节拍。

那么1个系统节拍就是1ms,如果开启时间片轮转,那么1个时间片也就是1ms。

于是,在时间片轮转机制下,多个优先级相同的就绪任务,它们轮流上CPU,每一个任务执行1个时间片(1ms)

当然,如果人肉眼来看,通常很难分辨这1ms的时间差距,人会觉得多个任务是“同时”执行的。

实际上它们是并发执行,而不是并行执行。

关于时间片轮转,最后还是要强调几点:

第一,与抢占式调度需要手动配置开启不同的是,时间片轮转机制在FreeRTOS中是默认开启的。

如果你不喜欢这种默认开启,喜欢追求确定性。

可以手动在FreeRTOS配置文件中,加下面的一条配置:

// 手动配置,确定开启时间片轮转机制#defineconfigUSE_TIME_SLICING1

这样就可以用配置的方式,手动配置开启时间片轮转。

当然,如果你不希望采取时间片轮转机机制,只需要将这个宏手动配置为0。

假如关闭时间片轮转(虽然实践中几乎不会这么做),如果存在多个同优先级的就绪任务,那么:

  1. FreeRTOS 不会在它们之间自动切换。
  2. 哪个任务先运行,就会一直执行
  3. 直到它主动进入阻塞状态或被更高优先级任务抢占(前提是开启抢占式调度)。

第二,时间片轮转,只发生在“同优先级任务之间”。

如果优先级都不相同,时间片轮转机制是不适用的。

总结:FreeRTOS的任务调度机制

FreeRTOS 任务调度机制总结:

  1. 调度器只在就绪态任务中进行选择,阻塞态任务不参与调度。
  2. 优先级是调度的核心依据:就绪态任务中,优先级最高的任务一定先获得 CPU。
  3. 开启抢占式调度时:更高优先级任务一旦就绪,会在调度点打断低优先级任务。
  4. 关闭抢占式调度时:当前任务不阻塞、不让出 CPU,就不会被切换。
  5. 时间片轮转只作用于同优先级任务:
    1. 开启时间片轮转:同优先级任务按 时间片(系统节拍) 轮流执行。
    2. 关闭时间片轮转:同优先级任务不会自动切换。

或者,再精简一些:

就绪状态下的任务们,先比优先级,优先级不同,优先级高的先跑,优先级相同,按时间片轮转、轮流上CPU。

http://www.gsyq.cn/news/1403893.html

相关文章:

  • 从核心到系统:CPU、MPU、MCU、SoC的演进之路与选型指南
  • IR-UWB WBAN中VMIMO与LDPC联合迭代解码器的设计与性能优化
  • 2026年4月万柏林区技术好的汽车改装门店推荐,汽车脚垫/汽车香薰/汽车玻璃膜/汽车方向盘套,汽车改装店铺找哪家 - 品牌推荐师
  • 智慧场馆实战:基于边缘计算与计算机视觉的人群智能解决方案
  • 无人机海洋数据中继:DTN协议如何克服间歇连接挑战
  • 微电网下垂控制稳定性提升:P-PID-PD拓扑与粒子群优化实践
  • 对比官方价Taotoken活动价在模型调用上的成本优势
  • HoRain云--Claude Code 输出样式
  • 相控阵雷达通信一体化:基于压缩感知的稀疏信道估计技术
  • 开发团队如何利用Taotoken CLI统一管理智能体项目的模型配置
  • 告别绝对路径依赖:5种XPath相对路径定位实战精讲
  • 廊坊黄金回收哪家好 2026.5.27权威榜单避坑指南 - 资讯纵览
  • 眼纹多用什么眼油拯救?CA眼油周期修护3周左右表情纹慢慢淡化 - 全网最美
  • 如何永久保存微信聊天记录?WeChatMsg年度报告生成终极指南
  • 告别光秃秃的地形:用Unity Terrain系统打造风格化场景的完整工作流(附资源管理技巧)
  • MTKClient:解锁联发科设备的全能工具箱,让设备调试更简单高效
  • 思源宋体TTF终极指南:如何用7种字重打造专业级中文排版体验
  • 体验 Taotoken 旗舰模型更新与稳定低延迟的推理服务
  • 5分钟极速指南:从零开始配置Arduino ESP32开发环境
  • 全源码提供-专业可靠的医疗健康预约小程序
  • 智能传感器网络设计:从边缘计算到5G通信的协同架构实战
  • 终极指南:使用OpCore Simplify快速构建OpenCore EFI的完整解决方案
  • Zenodo数据下载革命:zenodo_get工具如何让科研数据获取效率提升10倍
  • 沙海筑能,智塑展台 ——2026 迪拜能源展设计搭建优选 - 资讯焦点
  • Claude突然限流、Gemini拒绝金融问答、Qwen3中文微调失效?——ChatGPT替代方案紧急预警(附72小时迁移应急预案)
  • 2026昆山PLC培训机构排行:核心维度与标杆名录解析 - 互联网科技品牌测评
  • SMPL-X参数化人体建模:从运动捕捉到3D动画的全栈技术解析
  • 2026青岛纹眉怎么选?多门店从业者,详解纹绣世家高人气原因 - 小艾信息发布
  • 对比直接使用官方API通过聚合平台管理用量与账单的差异
  • bilili:2025年最完整的B站视频下载解决方案,一键保存高清视频与弹幕