RT-Thread学习记录-1.线程部分

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 内核的关键功能就是以合理的方式为系统中的每个线程分配时间(即调度),使之得以运行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a13UraCG-1620392946912)(04Task_switching.png)]

​ 实际上,根据特定的 RTOS,线程可能被称之为任务(Task), 也可能使用其他名词,含义有可能稍有差异,但本质不变, 也不必花过多精力,追究其精确语义,掌握线程设计方法、理解调度过程、提高编程鲁棒性、理解底层驱动原理、提高程序规范性、可移植性与可复用性、提高嵌入式系统的实际开发能力等才是学习 RTOS 的关键。要真正理解与应用线程进行基于 RTOS 的嵌入式软件开发,需要从线程的状态、结构、优先级、调度、同步等角度来认识,将在后续部分中详细阐述。

2. 线程管理的功能特点

​ 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是 系统线程 和 用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除,如下图所示,每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nH1f2IWA-1620392946914)(04Object_container.png)]

​ 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线程的四种状态转换关系如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9hiCgPxC-1620392946915)(04thread_sta.png)]

​ 线程通过调用函数 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) 
    { 
        //调用等待资源函数 
        //线程体部分 
    } 
}

​ 初始化部分和线程体部分与之前两个类型的线程类似,主要区别就是在线程体执行之前会调用等待资源函数, 以等待资源实现线程体部分的功能。 仍以刚才的系统为例,数据处理是在物理量采集完成后才能进行的操作,所以在系统中使用一个信号量用于两个线程之间的同步,当物理量采集线程完成时就会释放这个信号量,而数据处理线程一直在等待这个信号量,当等待到这个信号量时,就可以进行下一步的操作。系统中的数据处理线程就是一个典型的资源驱动线程。
​ 以上就是三种线程基本形式的介绍,其中的周期执行线程和资源驱动线程从本质上来讲可以归结为一种,也就是资源驱动线程。因为时间也是操作系统的一种资源,只不过时间是一种特殊的资源,特殊在该资源是整个操作系统的实现基础,系统中大部分函数都是基于时间这一资源的,所以在分类中将周期执行线程单独作为一类。

3.4 线程调度

1. 时间嘀嗒与延时函数

​ 了解时间嘀嗒是理解调度的基础, RTOS 延时函数暂停当前线程的执行,可执行其他线程,它不同于 NOS 下的机器周期空跑延时。

时间嘀嗒

​ 时钟嘀嗒(Time tick),是 RTOS 中时间的最小度量单位,是线程调度的基本时间单元。主要用于系统计时、 线程调度等。 也就是说,要进行线程切换,至少等一个时间嘀嗒。 时钟嘀嗒由硬件定时器产生,一般以毫秒(ms)为单位。在 RT-Thread 中,由于 Arm Cortex-M内核中含有 SysTick 定时器,为了操作系统在芯片之间移植方便,时钟嘀嗒由对 SysTick 定时器编程产生。

延时函数

​ 在有操作系统的情况下, 线程一般不采用原地空跑(空循环) 的方式进行延时(该方式线程仍然占用 CPU 的使用权),而往往会使用到延时函数(该方式线程会让出 CPU 使用权),通过使用延时列表管理延时线程,从而实现对线程的延时。在 RTOS 下使用延时函数,内核把暂时不需执行的线程,插入到延时列表中,让出 CPU 的使用权,并对线程进行调度。
在 RT-Thread 中, 提供了一个延时函数 rt_thread_delay (重定义的函数接口为 thread_wait),为了直观与通用, 可以在 includes.h 文件中将 thread_wait 再次宏定义为 delay_ms, 如 delay_ms(30) 代表延时 30 个嘀嗒。 执行该函数时,将当前线程的定时器按其延时参数指示的时间插入到延时列表的相应位置,该列表中的线程的定时器按照延时时长从小到大排序,每一个线程控制块 TCB 都记录了自身需要的等待唤醒时间(该时间=线程本身的延时时间-所有前驱节点的等待时间)。在延时期间,该线程已经放弃 CPU 的控制权,内核调度正常进行,可以执行别的就绪线程。当延时时间到达时, 线程进入就绪列表,等待 RT-Thread 调度运行。
这里简要理解一下 delay_ms 函数,第 9 章对其详细解析。进入 delay_ms 函数内部,其主要执行流程是: ① 获取对内核数据区的访问; ② 获取当前线程描述符结构体指针; ③ 根据延时的时间,将当前线程插入到延时列表的相应位置; ④ 放弃 CPU 控制权,由 RTOS 内核进行线程调度。
使用 delay_ms 函数时,注意以下两点:
第一, delay_ms 只能用在对时间精度要求不高或者时间间隔较长的场合。 delay_ms 函数的延时时长参数 millisec 以时间嘀嗒为单位,在 RT-Thread 中 1 个时间嘀嗒等于 1 毫秒,这样对延时时长参数就可以理解为是以毫秒为单位,此时实际延时时间与希望延时时间相等。但如果 1 个时间嘀嗒大于 1 毫秒时,而对希望延时的时间精度有较高要求时(如延时时间不是时间嘀嗒的整数倍),由于内核是在每个时间嘀嗒到来时(即产生 SysTick 中断)才会去检查延时列表,此时的实际延时时间与希望延时时间可能会有误差,最坏的情况下的误差接近一个时间嘀嗒。所以, delay_ms 只能用在对时间精度要求不高的场合,或者时间间隔较长的场合。
第二, 延时小于 1 个时间嘀嗒,不使用 delay_ms 函数。若需延时的时间小于 1 个时间嘀嗒,则不建议使用 delay_ms 函数,而是根据具体的延时时间,决定采用变量循环空跑(NOP指令)、插入汇编语言或探索其他更合理的方式来解决。

2. 调度策略

调度是 RTOS 中最重要概念之一,正是因为 RTOS 中有了个调度者,多线程才变得可能。 线程调度策略直接影响到应用系统的实时性。

调度基础知识

调度是内核的主要职责之一,它决定将哪一个线程投入运行、何时投入运行以及运行多久,协调线程对系统资源的合理使用。对系统资源非常匮乏的嵌入式系统来说,线程调度策略尤为重要,它直接影响到系统的实时性能。
调度是一种指挥方式,有策略问题。调度策略不同,线程被投入运行时刻也不同。常用的调度策略主要有:优先级抢占调度与时间片轮转调度等,下面介绍这两种调度策略的基本
内涵。除两种调度策略之外,还有一种被称之为显示调度的方式,就是用命令直接让其运行,
在 RTOS 中很少用到。

①优先级抢占调度

​ 优先级抢占调度总是让就绪列表中优先级最高的线程先运行,对于优先级相同的线程,则采用先进先出(First In First Out, FIFO)的策略。
​ 所谓优先级(Priority)是指计算机操作系统在处理多个线程(或中断)时,决定各个线程(或中断)接受系统资源的优先等级的参数,操作系统会根据各个线程(或中断) 优先级的高低,来决定处理各个线程(或中断) 的先后次序。在 ARM Cortex-M 处理器中,中断(异常)的优先级一般在 MCU 设计阶段就确定了,优先级编号越小表示中断(异常)的优先级越高,而且高优先级可以抢占低优先级的中断(异常)。 例如, 在 RT-Thread 中,共有 32 种优先级,数值分别为 1~32,优先级数值越小,表示优先级越高。但线程的优先级数值不宜过大,否则将会影响线程管理列表所占的资源和管理的时效性。
​ 基于优先级先进先出的调度策略在运行时可分为以下三种情况:
​ 第一种情况, 设线程 B 的优先级高于线程 A,当线程 A 正在运行时,线程 B 准备就绪(发生情景:第一种情景是线程 A 创建了线程 B;第二种情景是线程 B 的延时到期;第三种情景是 用户显式的调度线程 B;第四种情景是线程 B 已获得等待的线程信号、事件、信号量或互斥量等),则调度系统在下一个时间嘀嗒中断发生的时候,会将 CPU 的使用权从线程 A 处抢夺,将其转入就绪态(即线程 A 被放入到就绪列表中),并分配 CPU 使用权给线程 B。
​ 第二种情况, 当线程 A 被阻塞后主动放弃 CPU 使用权,此时,调度系统将在当前就绪的线程中寻找优先级最高的线程,将 CPU 的使用权分配给它。
​ 第三种情况, 当存在同一优先级的多个线程都处于就绪态时,较早进入就绪态的线程优先获得系统分配的一段固定时间片供其运行。
当发生以下任意一种情况时,当前线程会停止运行, 并进入 CPU 调度:
​ 第一种情况, 由于调用了阻塞功能函数(如等待线程信号、事件、 信号量或互斥量等),激活态(运行态)线程主动放弃 CPU 使用权,会同时被放到等待列表和阻塞列表中。
​ 第二种情况, 产生了一个比激活态(运行态)线程所能屏蔽的中断优先级更高的中断。
​ 第三种情况, 更高优先级的线程已经处于就绪状态。
​ 在协调同一优先级下的多个就绪线程时,一般 RTOS 可能会加入时间片轮询的调度机制,以此协调多个同优先级线程共享 CPU 的问题。

②时间片轮询调度

​ 时间片轮转(Round Ronbin, RR) 调度策略,也总是让就绪列表中优先级最高的线程先运行,但是,对于优先级相同的线程, 使用时间片轮转方式,即相同优先级的线程相同时间片方式分享 CPU 时间。实际上,当采用 RR 调度时,不同优先级的线程是按照 FIFO 策略排列的;相同优先级的线程会才会采用时间片轮询来调用。

RT-Thread 中使用的调度策略

​ 不同的操作系统采取的线程调度策略有所区别, 如 μC/OS 总是运行处于就绪状态且优先级最高的线程; FreeRTOS 支持三种调度方式:优先级抢占式调度、时间片调度和合作式调度,实际应用主要是抢占式调度和时间片调度,合作式调度用得很少; MQXLite 采用优先级抢占调度、时间片轮转调度和显式调度。
​ 在 RT-Thread 中, 采用基于优先级先进先出(FIFO) 和时间片轮转(RR) 的综合调度策略, 该调度策略为:总是将 CPU 的使用权分配给当前就绪的、优先级最高的且是较先进入就绪态的线程,同一优先级的线程采用时间片轮转的调度算法, 其中时间片轮转策略是可选的, 是作为 FIFO 调度方式的补充,可以协调同一优先级多个就绪线程共享 CPU 的问题,改善多个同优先级就绪线程的调度问题。
​ 在 RT-Thread 中, 每个轮询线程有最长时间限制(时间片),在此时间片内该线程可以被激活。时间片由每个线程在创建时自主设置,例如线程创建时定义了其时间片为 10,也就意味着该线程在时间片轮转调度过程中每次被调度所占用的时间为 10 个时间嘀嗒,若每个时间嘀嗒为 1ms,时间片就是 10 ms。同时,在线程执行的时间片期间并不禁止抢占,这就意味着 CPU 控制权可能被其他优先级高的线程抢占。
​ 在 RT-Thread 中, 如果设置所有线程的时间片大小为 0,则不会进行时间片轮询调度,若未出现优先级抢占或者线程阻塞的情况,正在运行的线程不会主动放弃对 CPU 的使用权。反之,当线程运行到规定时间片之后,会产生一次调度判断,若此时有同优先级的线程处于就绪态,则让出 CPU 使用权,否则继续运行。
​ 在 RT-Thread 中, 调度策略是通过可挂起系统调用PendSV(Pendable Supervisor, PendSV)中断和定时器 SysTick 中断来实现的。

RT-Thread 中固有线程

​ RT-Thread 中固有线程有: 自启动线程和空闲线程, 其中空闲线程的优先级为 31,自启动线程的优先级为 10。

1. 自启动线程

​ 在内核启动之前,需要创建一个自启动线程,以便内核启动后执行它, 并由它来创建其他用户线程。当自启动线程被创建时,其状态为就绪态,会自动被放入到就绪列表中。在 RT-Thread 中自启动线程的优先级为 10,由于在启动过程中,最后由自启动线程来创建其他用户线程,因此它的优先级级必须要高于或等于其他用户线程的优先级,这样才能保证其他用户线程被正常创建并运行。否则,若自启动线程的优先级低于它所创建的用户线程的优先级,则一旦创建一个线程后, 自启动线程会被抢占,无法继续创建其他线程。

2. 空闲线程

​ 为了确保在内核无用户线程可执行的时候, CPU 能继续保持运行状态,那么就必须安排一个空闲线程,该线程不完成任何实际工作,其状态永远为就绪态,始终在就绪列表中。在 RT-Thread 中,空闲线程是在内核启动的过程被创建的, 其优先级为 31,是所有线程中最低的。 另外,空闲线程在 RT-Thread 也有着它的特殊用途:
​ 若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。
​ 空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。

3. 线程的功能列表

​ 前面已经介绍了线程(线程)有终止态、阻塞态、就绪态和激活态四种状态,RTOS 会根据线程的不同状态使用不同的功能列表进行线程管理与调度。

1. 就绪列表

​ RTOS 中要运行的线程大多先放入就绪列表,即就绪列表中的线程是即将运行的线程,随时准备被调度运行。至于何时被允许运行,由内核调度策略决定。就绪列表中的线程,按照优先级高低顺序及先进先出排列。当内核调度器确认哪个线程运行,则将该线程状态标志由就绪态改为激活态,线程会从就绪列表取出被执行。

2. 延时列表

​ 延时列表是按线程的延时时间长短的顺序排列,线程进入延时列表后,存储的延时时间与调用延时函数实参不同,存储的延时时间=(延时函数实参-所有前面线程存储时间之和)。当线程调用了延时函数,则该线程就会被放入到延时列表中,其状态由激活态转化为阻塞态。当延时时间到时,该线程状态由阻塞态转化为就绪态,线程将被从延时列表移出并放入到就绪列表中,线程状态被设置为就绪态,等待调度执行。

3. 阻塞列表

​ 当线程进行永久等待(time=-1)状态或因等待事件位、消息、信号量、互斥量时,其状态由激活态转化为阻塞态,线程就会被放到阻塞列表中。当等待的条件满足时,该线程状态由阻塞态转化为就绪态,线程会从阻塞列表中移出,被放入到就绪列表中,由 RTOS 进行调度执行。
​ 为了方便对线程进行分类管理,在 RTOS 中会根据线程等待的事件位、消息、信号量、互斥量等条件,将线程放入对应的阻塞列表。根据线程等待的条件不同,阻塞列表又可分为事件阻塞列表、消息阻塞列表、信号量阻塞列表、互斥量阻塞列表。

4. RT-Thread线程先关实操

4.1 线程的管理方式

下图描述了线程的相关操作,包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。可以使用 rt_thread_create() 创建一个动态线程,使用 rt_thread_init() 初始化一个静态线程,动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vBDyWBnu-1620392946918)(04thread_ops.png)]

创建和删除线程

一个线程要成为可执行的对象,就必须由操作系统的内核来为它创建一个线程。可以通过如下的接口创建一个动态线程:

rt_thread_t rt_thread_create(const char* name,
                            void (*entry)(void* parameter),
                            void* parameter,
                            rt_uint32_t stack_size,
                            rt_uint8_t priority,
                            rt_uint32_t tick);

调用这个函数时,系统会从动态堆内存中分配一个线程句柄以及按照参数中指定的栈大小从动态堆内存中分配相应的空间。分配出来的栈空间是按照 rtconfig.h 中配置的 RT_ALIGN_SIZE 方式对齐。线程创建 rt_thread_create() 的参数和返回值见下表:

参数描述
name线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉
entry线程入口函数
parameter线程入口函数参数
stack_size线程栈大小,单位是字节
priority线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0~255,数值越小优先级越高,0 代表最高优先级
tick线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
返回——
thread线程创建成功,返回线程句柄
RT_NULL线程创建失败

对于一些使用 rt_thread_create() 创建出来的线程,当不需要使用,或者运行出错时,我们可以使用下面的函数接口来从系统中把线程完全删除掉:

rt_err_t rt_thread_delete(rt_thread_t thread);

调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。实际上,用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态,然后放入到 rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。线程删除 rt_thread_delete( ) 接口的参数和返回值见下表:

参数描述
thread要删除的线程句柄
返回——
RT_EOK删除线程成功
-RT_ERROR删除线程失败

Note

注:rt_thread_create() 和 rt_thread_delete() 函数仅在使能了系统动态堆时才有效(即 RT_USING_HEAP 宏定义已经定义了)。

初始化和脱离线程

线程的初始化可以使用下面的函数接口完成,来初始化静态线程对象:

rt_err_t rt_thread_init(struct rt_thread* thread,
                        const char* name,
                        void (*entry)(void* parameter), void* parameter,
                        void* stack_start, rt_uint32_t stack_size,
                        rt_uint8_t priority, rt_uint32_t tick);

静态线程的线程句柄(或者说线程控制块指针)、线程栈由用户提供。静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。需要注意的是,用户提供的栈首地址需做系统对齐(例如 ARM 上需要做 4 字节对齐)。线程初始化接口 rt_thread_init() 的参数和返回值见下表:

参数描述
thread线程句柄。线程句柄由用户提供出来,并指向对应的线程控制块内存地址
name线程的名称;线程名称的最大长度由 rtconfig.h 中定义的 RT_NAME_MAX 宏指定,多余部分会被自动截掉
entry线程入口函数
parameter线程入口函数参数
stack_start线程栈起始地址
stack_size线程栈大小,单位是字节。在大多数系统中需要做栈空间地址对齐(例如 ARM 体系结构中需要向 4 字节地址对齐)
priority线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0 ~ 255,数值越小优先级越高,0 代表最高优先级
tick线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
返回——
RT_EOK线程创建成功
-RT_ERROR线程创建失败

对于用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。线程脱离函数如下:

rt_err_t rt_thread_detach (rt_thread_t thread);

线程脱离接口 rt_thread_detach() 的参数和返回值见下表:

参数描述
thread线程句柄,它应该是由 rt_thread_init 进行初始化的线程句柄。
返回——
RT_EOK线程脱离成功
-RT_ERROR线程脱离失败

这个函数接口是和 rt_thread_delete() 函数相对应的, rt_thread_delete() 函数操作的对象是 rt_thread_create() 创建的句柄,而 rt_thread_detach() 函数操作的对象是使用 rt_thread_init() 函数初始化的线程控制块。同样,线程本身不应调用这个接口脱离线程本身。

启动线程

创建(初始化)的线程状态处于初始状态,并未进入就绪线程的调度队列,我们可以在线程初始化 / 创建成功后调用下面的函数接口让该线程进入就绪态:

rt_err_t rt_thread_startup(rt_thread_t thread);

当调用这个函数时,将把线程的状态更改为就绪状态,并放到相应优先级队列中等待调度。如果新启动的线程优先级比当前线程优先级高,将立刻切换到这个线程。线程启动接口 rt_thread_startup() 的参数和返回值见下表:

参数描述
thread线程句柄
返回——
RT_EOK线程启动成功
-RT_ERROR线程起动失败

获得当前线程

在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄:

rt_thread_t rt_thread_self(void);

该接口的返回值见下表:

返回描述
thread当前运行的线程句柄
RT_NULL失败,调度器还未启动

使线程让出处理器资源

当前线程的时间片用完或者该线程主动要求让出处理器资源时,它将不再占有处理器,调度器会选择相同优先级的下一个线程执行。线程调用这个接口后,这个线程仍然在就绪队列中。线程让出处理器使用下面的函数接口:

rt_err_t rt_thread_yield(void);

调用该函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。

rt_thread_yield() 函数和 rt_schedule() 函数比较相像,但在有相同优先级的其他就绪态线程存在时,系统的行为却完全不一样。执行 rt_thread_yield() 函数后,当前线程被换出,相同优先级的下一个就绪线程将被执行。而执行 rt_schedule() 函数后,当前线程并不一定被换出,即使被换出,也不会被放到就绪线程链表的尾部,而是在系统中选取就绪的优先级最高的线程执行(如果系统中没有比当前线程优先级更高的线程存在,那么执行完 rt_schedule() 函数后,系统将继续执行当前线程)。

使线程睡眠

在实际应用中,我们有时需要让运行的当前线程延迟一段时间,在指定的时间到达后重新运行,这就叫做 “线程睡眠”。线程睡眠可使用以下三个函数接口:

rt_err_t rt_thread_sleep(rt_tick_t tick);
rt_err_t rt_thread_delay(rt_tick_t tick);
rt_err_t rt_thread_mdelay(rt_int32_t ms);

这三个函数接口的作用相同,调用它们可以使当前线程挂起一段指定的时间,当这个时间过后,线程会被唤醒并再次进入就绪状态。这个函数接受一个参数,该参数指定了线程的休眠时间。线程睡眠接口 rt_thread_sleep/delay/mdelay() 的参数和返回值见下表:

参数描述
tick/ms线程睡眠的时间: sleep/delay 的传入参数 tick 以 1 个 OS Tick 为单位 ; mdelay 的传入参数 ms 以 1ms 为单位;
返回——
RT_EOK操作成功

挂起和恢复线程

当线程调用 rt_thread_delay() 时,线程将主动挂起;当调用 rt_sem_take(),rt_mb_recv() 等函数时,资源不可使用也将导致线程挂起。处于挂起状态的线程,如果其等待的资源超时(超过其设定的等待时间),那么该线程将不再等待这些资源,并返回到就绪状态;或者,当其他线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态。

线程挂起使用下面的函数接口:

rt_err_t rt_thread_suspend (rt_thread_t thread);

线程挂起接口 rt_thread_suspend() 的参数和返回值见下表:

参数描述
thread线程句柄
返回——
RT_EOK线程挂起成功
-RT_ERROR线程挂起失败,因为该线程的状态并不是就绪状态

Note

注:只能使用本函数来挂起线程自己,不可以在线程A中尝试挂起线程B,而且在挂起线程自己后,需要立刻调用 rt_schedule() 函数进行手动的线程上下文切换。用户只需要了解该接口的作用,不推荐使用该接口。该接口可视为内核内部接口。

恢复线程就是让挂起的线程重新进入就绪状态,并将线程放入系统的就绪队列中;如果被恢复线程在所有就绪态线程中,位于最高优先级链表的第一位,那么系统将进行线程上下文的切换。线程恢复使用下面的函数接口:

rt_err_t rt_thread_resume (rt_thread_t thread);

线程恢复接口 rt_thread_resume() 的参数和返回值见下表:

参数描述
thread线程句柄
返回——
RT_EOK线程恢复成功
-RT_ERROR线程恢复失败,因为该个线程的状态并不是 RT_THREAD_SUSPEND 状态

控制线程

当需要对线程进行一些其他控制时,例如动态更改线程的优先级,可以调用如下函数接口:

rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);

线程控制接口 rt_thread_control() 的参数和返回值见下表:

函数参数描述
thread线程句柄
cmd指示控制命令
arg控制参数
返回——
RT_EOK控制执行正确
-RT_ERROR失败

指示控制命令 cmd 当前支持的命令包括:

•RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级;

•RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup() 函数调用;

•RT_THREAD_CTRL_CLOSE:关闭一个线程,等同于 rt_thread_delete() 或 rt_thread_detach() 函数调用。

设置和删除空闲钩子

空闲钩子函数是空闲线程的钩子函数,如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。设置 / 删除空闲钩子的接口如下:

rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));

设置空闲钩子函数 rt_thread_idle_sethook() 的输入参数和返回值如下表所示:

函数参数描述
hook设置的钩子函数
返回——
RT_EOK设置成功
-RT_EFULL设置失败

删除空闲钩子函数 rt_thread_idle_delhook() 的输入参数和返回值如下表所示:

函数参数描述
hook删除的钩子函数
返回——
RT_EOK删除成功
-RT_ENOSYS删除失败

Note

注:空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如 rt_thread_delay(),rt_sem_take() 等可能会导致线程挂起的函数都不能使用。

设置调度器钩子

在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:

void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));

设置调度器钩子函数的输入参数如下表所示:

函数参数描述
hook表示用户定义的钩子函数指针

钩子函数 hook() 的声明如下:

void hook(struct rt_thread* from, struct rt_thread* to);

调度器钩子函数 hook() 的输入参数如下表所示:

函数参数描述
from表示系统所要切换出的线程控制块指针
to表示系统所要切换到的线程控制块指针

Note

注:请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统 API,更不应该导致当前运行的上下文挂起)。

4.2 线程应用示例

下面给出在 Keil 模拟器环境下的应用示例。

创建线程示例

这个例子创建一个动态线程初始化一个静态线程,一个线程在运行完毕后自动被系统删除,另一个线程一直打印计数,如下代码:

#include <rtthread.h>

#define THREAD_PRIORITY         25
#define THREAD_STACK_SIZE       512
#define THREAD_TIMESLICE        5

static rt_thread_t tid1 = RT_NULL;

/* 线程 1 的入口函数 */
static void thread1_entry(void *parameter)
{
    rt_uint32_t count = 0;

    while (1)
    {
        /* 线程 1 采用低优先级运行,一直打印计数值 */
        rt_kprintf("thread1 count: %d\n", count ++);
        rt_thread_mdelay(500);
    }
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 2 入口 */
static void thread2_entry(void *param)
{
    rt_uint32_t count = 0;

    /* 线程 2 拥有较高的优先级,以抢占线程 1 而获得执行 */
    for (count = 0; count < 10 ; count++)
    {
        /* 线程 2 打印计数值 */
        rt_kprintf("thread2 count: %d\n", count);
    }
    rt_kprintf("thread2 exit\n");
    /* 线程 2 运行结束后也将自动被系统脱离 */
}

/* 线程示例 */
int thread_sample(void)
{
    /* 创建线程 1,名称是 thread1,入口是 thread1_entry*/
    tid1 = rt_thread_create("thread1",
                            thread1_entry, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);

    /* 如果获得线程控制块,启动这个线程 */
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);

    /* 初始化线程 2,名称是 thread2,入口是 thread2_entry */
    rt_thread_init(&thread2,
                   "thread2",
                   thread2_entry,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack),
                   THREAD_PRIORITY - 1, THREAD_TIMESLICE);
    rt_thread_startup(&thread2);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(thread_sample, thread sample);

仿真运行结果如下:

\ | /
- RT -     Thread Operating System
 / | \     3.1.0 build Aug 24 2018
 2006 - 2018 Copyright by rt-thread team
msh >thread_sample
msh >thread2 count: 0
thread2 count: 1
thread2 count: 2
thread2 count: 3
thread2 count: 4
thread2 count: 5
thread2 count: 6
thread2 count: 7
thread2 count: 8
thread2 count: 9
thread2 exit
thread1 count: 0
thread1 count: 1
thread1 count: 2
thread1 count: 3

线程 2 计数到一定值会执行完毕,线程 2 被系统自动删除,计数停止。线程 1 一直打印计数。

Note

注:关于删除线程:大多数线程是循环执行的,无需删除;而能运行完毕的线程,RT-Thread 在线程运行完毕后,自动删除线程,在 rt_thread_exit() 里完成删除动作。用户只需要了解该接口的作用,不推荐使用该接口(可以由其他线程调用此接口或在定时器超时函数中调用此接口删除一个线程,但是这种使用非常少)。

线程时间片轮转调度示例

这个例子创建两个线程,在执行时会一直打印计数,如下代码:

#include <rtthread.h>

#define THREAD_STACK_SIZE   1024
#define THREAD_PRIORITY     20
#define THREAD_TIMESLICE    10

/* 线程入口 */
static void thread_entry(void* parameter)
{
    rt_uint32_t value;
    rt_uint32_t count = 0;

    value = (rt_uint32_t)parameter;
    while (1)
    {
        if(0 == (count % 5))
        {
            rt_kprintf("thread %d is running ,thread %d count = %d\n", value , value , count);

            if(count> 200)
                return;
        }
         count++;
     }
}

int timeslice_sample(void)
{
    rt_thread_t tid = RT_NULL;
    /* 创建线程 1 */
    tid = rt_thread_create("thread1",
                            thread_entry, (void*)1,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid != RT_NULL)
        rt_thread_startup(tid);


    /* 创建线程 2 */
    tid = rt_thread_create("thread2",
                            thread_entry, (void*)2,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE-5);
    if (tid != RT_NULL)
        rt_thread_startup(tid);
    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(timeslice_sample, timeslice sample);

仿真运行结果如下:

 \ | /
- RT -     Thread Operating System
 / | \     3.1.0 build Aug 27 2018
 2006 - 2018 Copyright by rt-thread team
msh >timeslice_sample
msh >thread 1 is running ,thread 1 count = 0
thread 1 is running ,thread 1 count = 5
thread 1 is running ,thread 1 count = 10
thread 1 is running ,thread 1 count = 15
…
thread 1 is running ,thread 1 count = 125
thread 1 is rthread 2 is running ,thread 2 count = 0
thread 2 is running ,thread 2 count = 5
thread 2 is running ,thread 2 count = 10
thread 2 is running ,thread 2 count = 15
thread 2 is running ,thread 2 count = 20
thread 2 is running ,thread 2 count = 25
thread 2 is running ,thread 2 count = 30
thread 2 is running ,thread 2 count = 35
thread 2 is running ,thread 2 count = 40
thread 2 is running ,thread 2 count = 45
thread 2 is running ,thread 2 count = 50
thread 2 is running ,thread 2 count = 55
thread 2 is running ,thread 2 count = 60
thread 2 is running ,thread 2 cunning ,thread 2 count = 65
thread 1 is running ,thread 1 count = 135
…
thread 2 is running ,thread 2 count = 205

由运行的计数结果可以看出,线程 2 的运行时间是线程 1 的一半。

线程调度器钩子示例

在线程进行调度切换时,会执行调度,我们可以设置一个调度器钩子,这样可以在线程切换时,做一些额外的事情,这个例子是在调度器钩子函数中打印线程间的切换信息,如下代码:

#include <rtthread.h>

#define THREAD_STACK_SIZE   1024
#define THREAD_PRIORITY     20
#define THREAD_TIMESLICE    10

/* 针对每个线程的计数器 */
volatile rt_uint32_t count[2];

/* 线程 1、2 共用一个入口,但入口参数不同 */
static void thread_entry(void* parameter)
{
    rt_uint32_t value;

    value = (rt_uint32_t)parameter;
    while (1)
    {
        rt_kprintf("thread %d is running\n", value);
        rt_thread_mdelay(1000); // 延时一段时间
    }
}

static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;

static void hook_of_scheduler(struct rt_thread* from, struct rt_thread* to)
{
    rt_kprintf("from: %s -->  to: %s \n", from->name , to->name);
}

int scheduler_hook(void)
{
    /* 设置调度器钩子 */
    rt_scheduler_sethook(hook_of_scheduler);

    /* 创建线程 1 */
    tid1 = rt_thread_create("thread1",
                            thread_entry, (void*)1,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);

    /* 创建线程 2 */
    tid2 = rt_thread_create("thread2",
                            thread_entry, (void*)2,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY,THREAD_TIMESLICE - 5);
    if (tid2 != RT_NULL)
        rt_thread_startup(tid2);
    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(scheduler_hook, scheduler_hook sample);

仿真运行结果如下:

 \ | /
- RT -     Thread Operating System
 / | \     3.1.0 build Aug 27 2018
 2006 - 2018 Copyright by rt-thread team
msh > scheduler_hook
msh >from: tshell -->  to: thread1
thread 1 is running
from: thread1 -->  to: thread2
thread 2 is running
from: thread2 -->  to: tidle
from: tidle -->  to: thread1
thread 1 is running
from: thread1 -->  to: tidle
from: tidle -->  to: thread2
thread 2 is running
from: thread2 -->  to: tidle
…

由仿真的结果可以看出,对线程进行切换时,设置的调度器钩子函数是在正常工作的,一直在打印线程切换的信息,包含切换到空闲线程。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值