第四章 操作系统系统内核应用
本章将对操作系统内核的方面的应用进行介绍,通过若干个实验,来介绍内核资源的使用,方便读者理解并且应用。
4.1 线程
4.1.1 线程介绍
线程,即任务的载体。一般被设计成 while(1) 的循环模式,但在循环中一定要有让出 CPU 使用权的动作。如果是可以执行完毕的线程,则系统会自动将执行完毕的线程进行删除 / 脱离。
4.1.2 线程创建
主要创建入口函数,实现具体功能、分配内存大小、优先级和时间片段。 创建函数主要用到动态创建和静态创建。如果该线程一直都是要运行的MCU里面,建议采用静态创建。如果该线程只是在运行的过程中在特定情况触发,可以采用动态创建。
线程动态创建
创建一个闪烁程序
# 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)
{
while(1) // 灯闪烁程序
{
LED_Ctr(led0,led_on);
rt_thread_mdelay(500);
LED_Ctr(led0,led_off);
rt_thread_mdelay(500);
}
}
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);
}
静态创建
创建一个闪烁程序
# include <rtthread.h>
# define THREAD_PRIORITY 25
# define THREAD_STACK_SIZE 512
# define THREAD_TIMESLICE 5
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线 程 2 入 口 */
static void thread2_entry(void *param)
{
while(1) // 灯闪烁程序
{
LED_Ctr(led0,led_on);
rt_thread_mdelay(500);
LED_Ctr(led0,led_off);
rt_thread_mdelay(500);
}
/* 线 程 2 运 行 结 束 后 也 将 自 动 被 系 统 脱 离 */
}
int thread_sample(void)
{
/* 初 始 化 线 程 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;
}
4.1.3 线程获取和线程睡眠
获取线程
rt_thread_t rt_thread_self(void);
线程休眠
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);
4.1.4 线程挂起和恢复
不建议使用下面的函数来挂起线程。可以通过delay来延迟挂起资源。否则一定要用,需要手动切换线程。rt_schedule() 函数
rt_thread_suspend (rt_thread_t thread);
rt_err_t rt_thread_resume (rt_thread_t thread);
4.1.5 线程控制
当需要堆一个线程进行其他控制时,可以调用如下函数接口:
rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);
参数 thread 为线程句柄;参数 cmd 为控制指令;arg 为控制指令参数。
返回 RT_EOK,表示执行成功。返回 -RT_ERROR,表示执行失败。
指示控制命令 cmd 当前支持的命令如下:
RT_THREAD_CTRL_CHANGE_PRIORITY,动态更改线程优先级。
RT_THREAD_CTRL_STARTUP,开始运行一个线程。
RT_THREAD_CTRL_CLOSE,关闭一个线程。
可以运行线程2开始关闭线程1,运行5次后关闭线程2;彼此一次关闭:
/* 线 程 2 入 口 */
static void thread2_entry(void *param)
{
u32 cnt=0;
rt_thread_control(tid1,RT_THREAD_CTRL_CLOSE,RT_NULL);
while(1) // 灯闪烁程序
{
LED_Ctr(led0,led_on);
rt_thread_mdelay(500);
LED_Ctr(led0,led_off);
rt_thread_mdelay(500);
if(cnt<5)
{
cnt++;
}
else
{
rt_thread_control(&thread2,RT_THREAD_CTRL_CLOSE,RT_NULL);
rt_thread_control(tid1,RT_THREAD_CTRL_STARTUP,RT_NULL);
cnt=0;
}
}
}
/* 线 程 1 的 入 口 函 数 */
static void thread1_entry(void *parameter)
{
u32 cnt=0;
rt_thread_control(&thread2,RT_THREAD_CTRL_CLOSE,RT_NULL);
while(1) // 灯闪烁程序
{
LED_Ctr(led1,led_on);
rt_thread_mdelay(500);
LED_Ctr(led1,led_off);
rt_thread_mdelay(500);
if(cnt<5)
cnt++;
else
{
rt_thread_control(tid1,RT_THREAD_CTRL_CLOSE,RT_NULL);
rt_thread_control(&thread2,RT_THREAD_CTRL_STARTUP,RT_NULL);
cnt=0;
}
}
}
4.1.6 线程总结
线程运行的过程中,同一时间内只允许一个线程在处理器中运行,从运行的过程上划分,线程有多种
不同的运行状态,如初始状态、挂起状态、就绪状态等。在RT-Thread 中,线程包含五种状态,操作系统
会自动根据它运行的情况来动态调整它的状态。RT-Thread 中线程的五种状态,如下表所示:
1. 初始状态
当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状 态在RT-Thread
中的宏定义为RT_THREAD_INIT
2. 就绪状态
在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操
作系统会马上寻找最高优先级的就绪态线程运行。此状态在RT-Thread 中的宏定义为 RT_THREAD_READY
3 运行状态
线程当前正在运行。在单核系统中,只有rt_thread_self() 函数返回的线程处于运行状态;
在多核系统中,可能就不止这一个线程处于运行状态。此状态在RT-Thread 中的宏定义为 RT_THREAD_RUNNING
4 挂起状态
也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起 状态下,线程不参与调度。此状态在RT-Thread中的宏定义为RT_THREAD_SUSPEND
5关闭状态
当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在RT-Thread 中的宏定义为RT_THREAD_CLOSE
优先级
RT-Thread 线程的优先级是表示线程被调度的优先程度。每个线程都具有优先级,线程越重要,赋予的优先级就应越高,- 线程被调度的可能才会越大。
RT-Thread 最大支持256 个线程优先级(0~255),数值越小的优先级越高,0 为最高优先级。在一些资源比较紧张的系统中,可以根据实际情况选择只支持8 个或32 个优先级的系统配置;对于ARM Cortex-M系列,普遍采用32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比
当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。
时间片
每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick),详见第五章。假设有2 个优先级相同的就绪态线程A 与B,A 线程的时间片设置为10,B 线程的时间片设置为5,那么当系统中不存在比A 优先级高的就绪态线程时,系统会在A、B 线程间来回切换执行,并且每次对A 线程执行10 个节拍的时长,对B 线程执行5 个节拍的时长,如下图。
4.2 线程同步
4.2.1 信号量
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。
信号量工作示意图如下图所示,每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。
信号量申明
#include <rtthread.h>
#define THREAD_PRIORITY 6
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
信号量申明
struct rt_semaphore sem_lock;
信号量线程获取
rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
信号量释放
rt_sem_release(&sem_lock);
信号量静态和动态创建
rt_sem_init(&sem_KeyOn, "key_on", 1, RT_IPC_FLAG_FIFO);
信号量释放
rt_sem_detach(&sem_lock);
这里我们创建两个线程,线程1扫描按键,发送信号量,线程2 获取信号量,点亮灯。
#include "led.h"
#include "rtthread.h"
#include "sem_key.h"
#include "key.h"
// 按键获取
Board_Key *pKey;
// 申明信号量
struct rt_semaphore sem_KeyOn;
#define THREAD_PRIORITY 25
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
static rt_thread_t tid1 = RT_NULL;
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线 程 2 入 口 */
static void thread2_entry(void *param)
{
while(1) // 点亮
{
rt_sem_take(&sem_KeyOn,RT_WAITING_FOREVER); // 获取信号量
LED_Ctr(led0,led_on);
}
}
/* 线 程 1 的 入 口 函 数 */
static void thread1_entry(void *parameter)
{
Board_Key *pk=parameter;
while(1) // 灯闪烁程序
{
Key_Scan();
if(pk->key==key0 && pk->sta==press)
rt_sem_release(&sem_KeyOn); // 释放信号量
rt_thread_mdelay(10);
}
}
int Sem_Sample(void)
{
/* 按键初始化 */
Key_Init(&pKey);
/* 静态创建信号量 */
rt_sem_init(&sem_KeyOn, "keyOn", 0, RT_IPC_FLAG_FIFO);
/* 动态创建信号量 */
/* 初 始 化 线 程 2, 名 称 是 thread2, 入 口 是 thread2_entry */
rt_thread_init(&thread2,"thread2",thread2_entry,RT_NULL,&thread2_stack[0],
sizeof(thread2_stack),THREAD_PRIORITY , THREAD_TIMESLICE);
rt_thread_startup(&thread2);
/* 创 建 线 程 1, 名 称 是 thread1, 入 口 是 thread1_entry*/
tid1 = rt_thread_create("thread1",thread1_entry, pKey,THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);
/* 如 果 获 得 线 程 控 制 块, 启 动 这 个 线 程 */
if (tid1 != RT_NULL)
rt_thread_startup(tid1);
return 0;
}
也可以动态创建信号量
/* 动态创建信号量 */
sem_KeyOff=rt_sem_create("keyOff",0,RT_IPC_FLAG_FIFO);
rt_sem_t sem_KeyOff;
创建线程,执行灯灭
ALIGN(RT_ALIGN_SIZE)
static char thread3_stack[1024];
static struct rt_thread thread3;
/* 线 程 3 入 口 */
static void thread3_entry(void *param)
{
while(1) // 点亮
{
rt_sem_take(sem_KeyOff,RT_WAITING_FOREVER); // 获取信号量
LED_Ctr(led0,led_off); // 关闭灯
}
}
/* 线 程 1 的 入 口 函 数 */
static void thread1_entry(void *parameter)
{
while(1) // 灯闪烁程序
{
Board_Key *pk=parameter;
Key_Scan();
if(pk->key==key0 && pk->sta==press)
rt_sem_release(&sem_KeyOn); // 释放信号量
pk++;
if(pk->key==key1 && pk->sta==press)
rt_sem_release(sem_KeyOff); // 释放信号量
rt_thread_mdelay(10);
}
}
信号量总结
信号量是一种非常灵活的同步方式,可以运用在多种场合中。形成锁、同步、资源计数等关系,也能方便的用于线程与线程、中断与线程间的同步中。
4.2.2 互斥量
互斥量又叫相互排斥的信号量,是一种特殊的二值信号量。互斥量类似于只有一个车位的停车场:当有一辆车进入的时候,将停车场大门锁住,其他车辆在外面等候。当里面的车出来时,将停车场大门打开,下一辆车才可以进入。
互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。在初始化的时候,互斥量永远都处于开锁的状态,而被线程持有的时候则立刻转为闭锁的状态。互斥量更适合于:
(1)线程多次持有互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题。
(2)可能会由于多线程同步而造成优先级翻转的情况。
动态创建
rt_mutex_t rt_mutex_create (const char* name, rt_uint8_t flag);
rt_err_t rt_mutex_delete (rt_mutex_t mutex);
静态创建
rt_err_t rt_mutex_init (rt_mutex_t mutex, const char* name, rt_uint8_t flag);
rt_err_t rt_mutex_detach (rt_mutex_t mutex);
获取和释放
rt_err_t rt_mutex_take (rt_mutex_t mutex, rt_int32_t time);
rt_err_t rt_mutex_release(rt_mutex_t mutex);
由于系统默认不开启互斥量和事件,所以,需要开启互斥量和事件。
首先找到 rtconfig.h 文件,打开这个文件。
然后取消屏蔽 标黄的部分。
下面是参考代码:一个线程让蜂鸣器响5次,一个线程让蜂鸣器响3次。
#include "rtthread.h"
#include "mutex_beep.h"
#include "beep.h"
rt_mutex_t mutex_beep; // 互斥量
#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5
ALIGN(RT_ALIGN_SIZE)
static char thread3_stack[512];
static struct rt_thread thread3;
/* 线 程 3 入 口 */
static void thread3_entry(void *param)
{
u32 cnt=0;
while(1) // 点亮
{
cnt++;
if(cnt>5)
{
for(uint32_t i=0;i<5;i++)
{
rt_mutex_take(mutex_beep,RT_WAITING_FOREVER); // 等待互斥信号
BEEP_Ctr(beep_on);
rt_thread_mdelay(100);
BEEP_Ctr(beep_off);
rt_thread_mdelay(100);
rt_mutex_release(mutex_beep); // 释放互斥信号
}
cnt=0;
}
else
rt_thread_mdelay(500);
}
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[512];
static struct rt_thread thread2;
/* 线 程 2 入 口 */
static void thread2_entry(void *param)
{
u32 cnt=0;
while(1) // 点亮
{
cnt++;
if(cnt>3)
{
for(uint32_t i=0;i<3;i++)
{
rt_mutex_take(mutex_beep,RT_WAITING_FOREVER); // 等待互斥信号
BEEP_Ctr(beep_on);
rt_thread_mdelay(100);
BEEP_Ctr(beep_off);
rt_thread_mdelay(100);
rt_mutex_release(mutex_beep); // 释放互斥信号
}
cnt=0;
}
else
{
rt_thread_mdelay(500);
}
}
}
int Mutex_Sample(void)
{
// 创建互斥量
mutex_beep=rt_mutex_create ("mutex_beep", RT_IPC_FLAG_FIFO);
/* 静态线程3 */
rt_thread_init(&thread3,"thread3",thread3_entry,RT_NULL,&thread3_stack[0],sizeof(thread3_stack),THREAD_PRIORITY , THREAD_TIMESLICE);
rt_thread_startup(&thread3);
/* 初 始 化 线 程 2, 名 称 是 thread2, 入 口 是 thread2_entry */
rt_thread_init(&thread2,"thread2",thread2_entry,RT_NULL,&thread2_stack[0],sizeof(thread2_stack),THREAD_PRIORITY , THREAD_TIMESLICE);
rt_thread_startup(&thread2);
return 0;
}
4.2.3 事件
事件集也是线程间同步的机制之一,一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间同步。下面以坐公交为例说明事件,在公交站等公交时可能有以下几种情况:
fiP1 坐公交去某地,只有一种公交可以到达目的地,等到此公交即可出发。
fiP1 坐公交去某地,有 3 种公交都可以到达目的地,等到其中任意一辆即可出发。
fiP1 约另一人 P2 一起去某地,则 P1 必须要等到 “同伴 P2 到达公交站” 与 “公交到达公交站” 两个条件都满足后,才能出发。这里,可以将 P1 去某地视为线程,将 “公交到达公交站”、“同伴 P2 到达公交站” 视为事件的发生,情况 fi 是特定事件唤醒线程;情况 fi 是任意单个事件唤醒线程;情况 fi 是多个事件同时发生才唤醒线程。
事件集主要用于线程间的同步,与信号量不同,它的特点是可以实现一对多,多对多的同步。即一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件。这种多个事件的集合可以用一个 32 位无符号整型变量来表示,变量的每一位代表一个事件,线程通过 “逻辑与” 或 “逻辑或” 将一个或多个事件关联起来,形成事件组合。事件的 “逻辑或” 也称为是独立型同步,指的是线程与任何事件之一发生同步;事件 “逻辑与”也称为是关联型同步,指的是线程与若干事件都发生同步.
事件定义
# define EVENT_FLAG3 (1 << 3)
# define EVENT_FLAG5 (1 << 5)
等待收到事件
/* 第 一 次 接 收 事 件, 事 件 3 或 事 件 5 任 意 一 个 可 以 触 发 线 程 1, 接 收 完 后 清 除 事 件 标 志*/
rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,RT_WAITING_FOREVER, &e) == RT_EOK)
/* 第 二 次 接 收 事 件, 事 件 3 和 事 件 5 均 发 生 时 才 可 以 触 发 线 程 1, 接 收 完 后 清 除 事 件 标志 */
rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,RT_WAITING_FOREVER, &e) == RT_EOK)
发送事件
rt_event_send(&event, EVENT_FLAG3);
rt_event_send(&event, EVENT_FLAG5);
创建事件
rt_event_init(&event, "event", RT_IPC_FLAG_FIFO);