1. 实时操作系统的基本含义
1.1 无操作系统与实时操作系统开发
无操作系统 NOS
无操作系统(No Operating System, NOS)的嵌入式系统中,常使用所谓前后台工作模式。在系统复位后,首先进行堆栈、中断向量、 系统时钟、内存变量、部分硬件模块等初始化工作,然后进入“无限循环”,在这个无限循环中, CPU 一般根据一些全局变量的值决定执行各种功能程序(线程),这是第一条运行路线。若发生中断,将响应中断,执行中断服务程序(Interrupt Service Routines,ISR),这是第二条运行路线,执行完 ISR 后,返回中断处继续执行。从操作系统的调度功能角度理解, NOS 中的主程序,可以被简单地理解为一个 RTOS 内核,这个内核负责系统初始化和调度其它线程。
实时操作系统 RTOS
在基于 RTOS 的编程模式下,有两条线路,一条是线程线,编程时把一个较大工程分解成几个较小工程(被称之为线程或任务),有个调度者,负责这些线程的执行,另一条线路是中断线,与 NOS 情况一致, 若发生中断,将响应中断,执行中断服务程序 ISR, 然后中断处继续执行。可以进一步理解, RTOS 是一个标准内核,包括芯片初始化、设备驱动及数据结构的格式化,应用层程序员可以不直接对硬件设备和资源进行操作,而是通过标准调用方法实现对硬件的操作,所有的线程由 RTOS 内核负责调度。 也可以这样理解, RTOS是一段嵌入在目标代码中的程序,系统复位后首先执行它,用户的其他应用程序(线程)都建立在 RTOS 之上。不仅如此, RTOS 将 CPU 时间、中断、 I/O、定时器等资源都包装起来,留给用户一个标准的应用程序编程接口(Application Programming Interface, API),并根据各个线程的优先级,合理地在不同线程之间分配 CPU 时间。 RTOS 的基本功能可以简单地概括为: RTOS 为每个线程建立一个可执行的环境,方便线程间的传递消息,在中断服务程序 ISR 与线程之间传递事件,区分线程执行的优先级,管理内存,维护时钟及中断系统,并协调多个线程对同一个 I/O 设备的调用。 简而言之就是: 线程管理与调度、 线程间的通信与同步、存储管理、时间管理、中断处理等。
1.2 实时操作系统与非实时操作系统
我们知道, 操作系统(Operating System, OS)是一套用于管理计算机硬件与软件资源的程序,是计算机的系统软件。 通常我们使用的个人计算机(Personal Computer, PC) 系统,在硬件上一般由主机、显示屏、鼠标、打印机等组成。 操作系统提供设备驱动管理、进程管理、存储管理、文件系统、安全机制、网络通讯及使用者界面等功能,这类操作系统如 Windows、Mac OS、 Linux 等。
而嵌入式操作系统(Embedded Operation System, EOS)是一种工作在嵌入式微型计算机上的系统软件。一般情况下,它固化到微控制器、 应用处理器内的非易失存储体中, 它具有一般操作系统最基本的功能,负责嵌入式系统的软、硬件资源的分配、线程调度、同步机制、中断处理等功能。
嵌入式操作系统有实时与非实时之分。一般情况下, 应用处理器使用的嵌入式操作系统EOS 对实时性要求不高,主要关心功能,这类操作系统只要有 Android、 iOS、嵌入式 Linux等。而以微控制器为核心的嵌入式系统,如工业控制设备、军事设备、航空航天设备、嵌入式人工智能与物联网终端等, 大多对实时性要求较高,期望能够在较短的确定时间内完成特定的系统功能或中断响应,应用于这类系统中的操作系统就是实时操作系统。
与一般运行于 PC 机或服务器上的通用操作系统相比, RTOS 的突出特点是“实时性”,一般的通用操作系统(如 Window、 Linux 等)大都从“分时操作系统” 发展而来。在单中央处理器(Central Processing Unit, CPU)条件下,分时操作系统的主要运行方式是:对于多个线程, CPU 的运行时间被分为多个时间段,并且将这些时间段平均分配给每个线程,轮流让每个线程运行一段时间,或说每个线程独占 CPU 一段时间,如此循环,直至完成所有线程。这种操作系统注重所有线程的平均响应时间而较少关心单个线程的响应时间,对于单个线程来说,注重每次执行的平均响应时间而不关心某次特定执行的响应时间。而 RTOS 系统中,要求能“立即” 响应外部事件的请求, 这里的“立即” 含义是相对于一般操作系统而言,在更短的时间内响应外部事件。与通用操作系统不同, RTOS 注重的不是系统的平均表现,而是要求每个实时线程在最坏情况下都要满足其实时性要求,也就是说, RTOS 注重的是个体表现,更准确地讲是个体最坏情况表现。
2. RTOS 中的基本概念
2.1 内核类基本概念
在 RTOS 基础上编程,芯片启动过程先运行的一段程序代码,开辟好用户线程的运行环境,准备好对线程进行调度,这段程序代码就是 RTOS 的内核。 RTOS 一般由内核与扩展部分组成, 通常内核的最主要功能是线程调度,扩展部分的最主要功能是提供应用程序编程接口 API。
1. 调度
多线程系统中, RTOS 内核(Kernel)负责管理线程,或者说为每个线程分配 CPU 时间,并且负责线程间的通信。
**调度(Scheduling)**就是决定轮到哪个线程该运行了,它是内核最重要职责。每个线程根据其重要程度的不同,被赋予一定的优先级。不同的调度算法(Scheduling algorithm)对RTOS 的性能有较大影响, 基于优先级的调度算法(Scheduling algorithm based on priority)是 RTOS 常用的调度算法,核心思想是,总是让处于就绪态的、优先级最高的线程先运行。然而何时高优先级线程掌握 CPU 的使用权,由使用的内核类型确定,基于优先级的内核有不可抢占型和可抢占型两种类型。
2. 不可抢占型内核与可抢占型内核
不可抢占型内核(Non-Preemptive Kernel),要求每个线程主动放弃 CPU 的使用权, 不可抢占型调度算法也称为合作型多线程,各个线程彼此合作共享一个 CPU。但异步事件还是由中断服务来处理,中断服务可使高优先级的线程由挂起态变为就绪态,但中断服务以后,使用权还是回到原来被中断了的那个线程,直到该线程主动放弃 CPU 的使用权,新的高优先级的线程才能获得 CPU 的使用权。
当系统响应时间很重要时,须使用可抢占型内核(Preemptive Kernel)。在可抢占型内核中,一个正在运行的线程可以被打断,而让另一个优先级更高、且变为就绪态的线程运行。如果是中断服务子程序使高优先级的线程进入就绪态,中断完成时,被中断的线程被挂起,优先级高的线程开始运行。
3. 时钟节拍(时间嘀嗒)
时钟节拍(clock tick),有时中文也直接译为时钟嘀嗒, 它是特定的周期性中断, 通过定时器产生周期性的中断,以便内核判断是否有更高优先级的线程已进入就绪状态。
4. 实时性相关概念及 RTOS 实时性指标
**硬实时(Hard Real-Time)**要求在规定的时间内必须完成操作,是在设计操作系统时保证的,通常将具有优先级驱动的、时间确定性的、可抢占调度的 RTOS 系统称为硬实时系统。**软实时(Soft real-time)**则没有那么严格,只要按照线程的优先级, 尽可能快地完成操作即可。
RTOS 追求的是调度的实时性、响应时间的可确定性、系统高度的可靠性,评价一个RTOS 一般可以从线程调度、内存开销、系统响应时间、中断延迟等几个方面来衡量。
5. 代码临界段
代码临界段也称为临界区, 是指处理时不可分割的代码, 一旦这部分代码开始执行,则不允许任何中断打扰。为确保临界段代码的执行,在进入临界段之前要关中断,且临界段代码执行完后应立即开中断。
2.2 线程类基本概念
1. 线程的基本含义
线程是 RTOS 中最重要概念之一。在 RTOS 下,把一个复杂的嵌入式应用工程按一定规则分解成一个个功能清晰的小工程,然后设定各个小工程的运行规则,交给 RTOS 管理,这就是基于 RTOS 编程的基本思想。这一个个小工程被称之为“线程(Thread)”, RTOS 管理这些线程,被称之为“调度(Scheduling)”。
要给 RTOS 中的线程下一个准确而完整的定义并不十分容易,可以从不同角度理解线程。 从线程调度角度理解,可以认为, RTOS 中的线程是一个功能清晰的小程序,是 RTOS调度的基本单元; 从 RTOS 的软件设计角度来理解,就是在软件设计时,需要根据具体应用,划分出独立的、相互作用的程序集合,这样的程序集合就被称之为线程,每个线程都被赋予一定的优先级; 从 CPU 角度理解,在单 CPU 下,某一时刻 CPU 只会处理(执行)一个线程,或说只有一个线程占用 CPU。当线程运行时,它会认为自己是以独占 CPU 的方式在运行,线程执行时的运行环境称为上下文,具体来说就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等。 RTOS 内核的关键功能就是以合理的方式为系统中的每个线程分配时间(即调度),使之得以运行。
实际上,根据特定的 RTOS,线程可能被称之为任务(Task), 也可能使用其他名词,含义有可能稍有差异,但本质不变, 也不必花过多精力,追究其精确语义,掌握线程设计方法、理解调度过程、提高编程鲁棒性、理解底层驱动原理、提高程序规范性、可移植性与可复用性、提高嵌入式系统的实际开发能力等才是学习 RTOS 的关键。要真正理解与应用线程进行基于 RTOS 的嵌入式软件开发,需要从线程的状态、结构、优先级、调度、同步等角度来认识,将在后续部分中详细阐述。
2. 线程管理的功能特点
线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是 系统线程 和 用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除,如下图所示,每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。
RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权。当一个运行着的线程使一个比它优先级高的线程满足运行条件,当前线程的 CPU 使用权就被剥夺了,或者说被让出了,高优先级的线程立刻得到了 CPU 的使用权。如果是中断服务程序使一个高优先级的线程满足运行条件,中断完成时,被中断的线程挂起,优先级高的线程开始运行。当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。
3. 线程的上下文及线程切换
线程的上下文(Context),即 CPU 内寄存器。当多线程内核决定运行另外的线程时,它保存正在运行线程的当前上下文,这些内容保存在随机存储器(Random Access Memory,RAM) 中的线程当前状况保存区(Task’s Context Storage Area), 也就是线程自己的堆栈之中。入栈工作完成以后,就把下一个将要运行线程的当前状况从其线程栈中重新装入 CPU的寄存器,开始下一个线程的运行,这一过程叫做线程切换或上下文切换。
4. 死锁
死锁指两个或两个以上的线程无限期地互相等待对方释放其所占资源。死锁产生的必要条件有四个,即资源的互斥访问、资源的不可抢占、资源的请求保持以及线程的循环等待。死锁解决问题的方法是破坏产生死锁的任一必要条件,例如规定所有资源仅在线程运行时才分配,其他任意状态都不可分配,破坏其资源请求保持特性。
5. 线程间通信
线程间的通信是指线程间的信息交换,其作用是实现同步及数据传输。同步是指根据线程间的合作关系,协调不同线程间的执行顺序。线程间通信的方式主要有事件、消息队列、信号量、 互斥量等。有关线程间通信及下述的优先级反转、优先级继承、资源、共享资源与互斥等概念将在第后续章节中详细阐述。
6. 线程优先级、优先级驱动、优先级反转、优先级继承
在一个多线程系统中,每个线程都有一个优先级(Priority)。
**优先级驱动(Priority-Driven):**在一个多线程系统中,正在运行的线程总是优先级最高的线程。在任何给定的时间内,总是把 CPU 分配给优先级最高的线程。
**优先级反转(Priority- Inversion):**当一个线程等待比它优先级低的线程释放资源而被阻塞时,这种现象被称为优先级反转,这是一个需要在编程时必须注意的问题。优先级继承技术可以解决优先级反转问题, 目前市场上大多数商用操作系统都使用优先级继承技术。
**优先级继承(Priority-Inheritance):**优先级继承是用来解决优先级反转问题的技术。当优先级反转发生时,较低优先级线程的优先级暂时提高,以匹配较高优先级线程的优先级。这样,就可以使较低优先级线程尽快地执行并且释放较高优先级线程所需要的资源。
7. 资源、共享资源与互斥
**资源(Resources):**任何为线程所占用的实体均可称为资源。资源可以是输入/输出设备,例如打印机、键盘及显示器;资源也可以是一个变量、结构或数组等。
**共享资源(Shared Resources):**可以被一个以上线程使用的资源叫做共享资源。为了防止数据被破坏,每个线程在与共享资源打交道时,必须独占资源,即互斥。
**互斥(Mutual Exclusion):**互斥是用于控制多线程对共享数据进行顺序访问的同步机制。
在多线程应用中,当两个或更多的线程同时访问同一数据区时,就会造成访问冲突, 互斥能使它们依次访问共享数据而不引起冲突。
3. 线程
3.1 线程的三要素:线程函数、线程堆栈、线程描述符
从线程的存储结构上看,线程由三个部分组成: 线程函数、线程堆栈、线程描述符,这就是线程的三要素。线程函数就是线程要完成具体功能的程序; 每个线程拥有自己独立的线程堆栈空间,用于保存线程在调度时的上下文信息及线程内部使用的局部变量; 线程描述符(控制块)是关联了线程属性的程序控制块,记录线程的各个属性; 下面做进一步阐述。
1.线程函数
一个线程,对应一段函数代码,完成一定功能,可被称之为线程函数。从代码上看,线程函数与一般函数并无区别,被编译链接生成机器码之后,一般存储在 Flash 区。但是从线程自身角度来看,它认为 CPU 就是属于它自己的,并不知道还有其他线程存在。 线程函数也不是用来被其他函数直接调用的,而是由 RTOS 内核调度运行。要使线程函数能够被 RTOS内核调度运行,必须将线程函数进行“登记”,要给线程设定优先级、设置线程堆栈大小、给线程编号等等, 不然有几个线程都要运行起来, RTOS 内核如何知道哪个该先运行呢?由于任何时刻只能有一个线程在运行(处于激活态),当 RTOS 内核使一个线程运行时,之前的运行线程就会退出激活态。 CPU 被处于激活态的线程所独占,从这个角度看,线程函数与无操作系统(NOS)中的“main” 函数性质相近,一般被设计为“永久循环”,认为线程一直在执行,永远独占处理器。
2.线程堆栈
线程堆栈是独立于线程函数之外的 RAM,按照“先进后出” 策略组织的一段连续存储空间,是 RTOS 中线程概念的重要组成部分。在 RTOS 中被创建的每个线程都有自己私有的堆栈空间,在线程的运行过程中,堆栈用于保存线程程序运行过程中的局部变量、线程调用普通函数时会为线程保存返回地址等参数变量、保存线程的上下文等等。
虽然前面已经简要描述过“线程的上下文” 的概念,这里还要多说几句,以便对线程堆栈用于保存线程的上下文作用的充分认识。 在多线程系统中,每个线程都认为 CPU 寄存器是自己的,一个线程正在运行时,当 RTOS 内核决定不让当前线程运行,而转去运行别的线程,就要把 CPU 的当前状态保存在属于该线程的线程堆栈中,当 RTOS 内核再次决定让其运行时,就从该线程的线程堆栈中恢复原来的 CPU 状态,就像未被暂停过一样。
在系统资源充裕的情况下,可分配尽量多的堆栈空间,可以是 K 数量级的(例如常用1024 字节),但若是系统资源受限,就得精打细算了,具体的数值要根据线程的执行内容才能确定。对线程堆栈的组织及使用由系统维护,对于用户而言,只要在创建线程时指定其大小即可。
3.线程描述符
线程被创建时, 系统会为每个线程创建一个唯一的线程描述符(Task Descriptor, TD),它相当于线程在 RTOS 中的一个“身份证”, RTOS 就是通过这些“身份证”来管理线程和查询线程信息的。这个概念在不同操作系统名称不同,但含义相同, 在 RT-Thread 中被称为线程控制块(Thread Control Block, TCB), 在 μC/OS 中被称作线程控制块(Task Control Block,TCB),在 Linux 中被称为进程控制块(Process Control Block, PCB)。线程函数只有配备了相应的线程描述符才能被 RTOS 调度, 未被配备线程描述符的驻留在 Flash 区的线程函数代码就只是通常意义上的函数, 是不会被 RTOS 内核调度的。
多个线程的线程描述符被组成链表,存储于 RAM 中。每个线程描述符中含有指向前一个 TD 的指针、指向后一个 TD 的指针、线程状态、线程优先级、线程堆栈指针、线程函数指针(指向线程函数)等字段, RTOS 内核通过它来执行线程。
在 RTOS 中,一般情况下使用列表来维护线程描述符。例如, 在 RT-Thread 中阻塞列表用于存放因等待某个信号而终止运行的线程, 延时列表用于存放通过延时函数或等待某个信号指定的时间而终止运行的线程, 就绪列表则按优先级的高低存放准备要运行的线程。在RTOS 内核调度线程时,可以通过就绪列表的头节点查找链表,获取就绪列表上所有线程描述符的信息。
3.2 线程的四种状态:终止态、阻塞态、就绪态和激活态
RTOS 中的线程一般有四种状态,分别为: 终止态、阻塞态、就绪态和激活态。在任一
时刻,线程被创建后所处的状态一定是四种状态之一。
1.线程状态的基本含义
① 终止态(Terminated, Inactive):线程已经完成,或被删除,不再需要使用 CPU。
② 阻塞态(Blocked):又可称为“挂起态”。线程未准备好,不能被激活,因为该线程需要等待一段时间或某些情况发生;当等待时间到或等待的情况发生时,该线程才变为就绪态, 处于阻塞态的线程描述符存放于等待列表或延时列表中。
③ 就绪态(Ready):线程已经准备好可以被激活,但未进入激活态,因为其优先级等于或低于当前的激活线程,一旦获取 CPU 的使用权就可以进入激活态, 处于就绪态的线程描述符存放于就绪列表中。
④ 激活态(Active, Running):又称“运行态”,该线程在运行中,线程拥有 CPU 使用权。
如果一个激活态的线程变为阻塞态,则 RTOS 将执行切换操作,从就绪列表中选择优先级最高的线程进入激活态, 如果有多个具有相同优先级的线程处于就绪态,则就绪列表中的首个线程先被激活。也就是说,每个就绪列表中相同优先级的线程是按执行先进先出(First in First out, FIFO)的策略进行调度的。
在一些操作系统中,还把线程分为“中断态和休眠态”,对于被中断的线程 RTOS 把它归为就绪态;休眠态是指该线程的相关资源虽然仍驻留在内存中,但并不被 RTOS 所调度的状态,其实它就是一种终止的状态。
2.线程状态之间的转换
RTOS 线程的四种状态是动态转换的,有的情况是系统调度自动完成,有的情况是用户调用某个系统函数完成,有的情况是等待某个条件满足后完成。RT-Thread线程的四种状态转换关系如图所示。
线程通过调用函数 rt_thread_create/init() 进入到初始状态(RT_THREAD_INIT);初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态(RT_THREAD_READY);就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING);当处于运行状态的线程调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态(RT_THREAD_CLOSE);而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。
3.3 线程的基本形式: 单次执行、 周期执行、 资源驱动
线程函数一般分为两个部分:初始化部分和线程体部分。初始化部分实现对变量的定义、初始化以及设备的打开等等, 线程体部分负责完成该线程的基本功能。线程一般结构如下:
void task ( uint_32 initial_data )
{
//初始化部分
//线程体部分
}
线程的基本形式主要有单次执行线程、周期执行线程以及事件驱动线程三种, 下面介绍其结构特点。
1. 单次执行线程
单次执行线程是指线程在创建完之后只会被执行一次,执行完成后就会被销毁或阻塞的
线程,线程函数结构如下:
void task ( uint_32 initial_data )
{
//初始化部分
//线程体部分
//线程函数销毁或阻塞
}
单次执行线程由三部分组成:线程函数初始化、线程函数执行以及线程函数销毁。初始
化部分包括对变量的定义和赋值,打开需要使用的设备等等;第二部分线程函数的执行是该
线程的基本功能实现;第三部分线程函数的销毁或阻塞,即调用线程销毁或者阻塞函数将自
己从线程列表中删除。 销毁与阻塞的区别在于销毁除了停止线程的运行,还将回收该线程所
占用的所有资源,如堆栈空间等; 而阻塞只是将线程描述符中的状态设置为阻塞而已。 例如,
定时复位重启线程就是一个典型的单次执行线程。
2. 周期执行线程
周期执行线程是指需要按照一定周期执行的线程, 线程函数结构如下:
void task ( uint_32 initial_data )
{
//初始化部分
……
//线程体部分
while(1)
{
//循环体部分
}
}
初始化部分同上面一样实现包括对变量的定义和赋值,打开需要使用的设备等等, 与单次执行线程不一样的地方在于线程函数的执行是放在永久循环体中执行的,由于该线程需要按照一定周期执行,所以执行完该线程之后可能需要调用延时函数 wait 将自己放入延时列表中,等到延时的时间到了之后重新进入就绪态。该过程需要永久执行,所以线程函数执行和延时函数需要放在永久循环中。举例来说,在系统中,我们需要得到被监测水域的酸碱度和各种离子的浓度, 但并不需要时时刻刻都在检测数据,因为这些物理量的变化比较缓慢,所以使用传感器采集数据时只需要每隔半个小时采集一次数据,之后调用 wait 函数延时半个小时,此时的物理量采集线程就是典型的周期执行的线程。
3. 资源驱动线程
除了上面介绍的两种线程类型之外,还有一种线程形式,那就是资源驱动线程,这里的资源主要指信号量、 事件等线程通信与同步中的方法。这种类型的线程比较特殊,它是操作系统特有的线程类型,因为只有在操作系统下才导致资源的共享使用问题,同时也引出了操作系统中另一个主要的问题,那就是线程同步与通信。该线程与周期驱动线程的不同在于它的执行时间不是确定的,只有在它所要等待的资源可用时, 它才会转入就绪态,否则就会被加入到等待该资源的等待列表中。 资源驱动线程函数结构如下:
void task ( uint_32 initial_data )
{
//初始化部分
……
while(1)
{
//调用等待资源函数
//线