zsky · 2022年02月12日

RT-Thread快速入门-线程管理(上)

公众号【一起学嵌入式】

在 RT-Thread 中,最基本的调度单位是线程,其他 RTOS 也叫任务。如果学习过或者了解过 RTOS,任务这种叫法是最为熟知的。

本篇文章来学习一下 RT-Thread 线程方面的内容。对于初学者来说,转换一下思维,建立多任务(线程)的编程思想。

引言

对于裸机编程,整个软件系统只有一个线程(任务)在执行,实现方式是通过一个大循环完成的。应用程序是一个无限循环,循环中调用各个功能模块的函数,完成相应的操作。

RTOS 是一个多任务系统,可以把总体功能划分为多个小模块,每个小模块独立运行完成某项功能,即将任务分解。这样使得复杂项目的实现变得简单了。

关于为什么要用 RTOS,或者说 RTOS 的优点,网上资料很多,在此不再赘述。

线程基础

1. 线程调度

对于一款 RTOS 来说,最核心的部分就是线程(任务)调度器。调度器的作用是根据调度算法来决定当前需要执行的线程。

RT-Thread 的线程调度器是抢占式的,基于优先级对线程进行调度。每个线程均具有一个优先级,调度器的主要工作是,从就绪线程列表中查找最高优先级线程,然后将 CPU 的使用权分配给它。

"可抢占"意味着,如果高优先级线程就绪或者满足运行条件,RT-Thread 马上将 CPU 的控制权交给高优先级线程。

2. 线程优先级

RT-Thread 线程的优先级表示线程被调度的优先程度。每个线程都具有优先级,对于重要的线程,应该赋予其高优先级,这样才能保证线程被优先调度。

RT-Thread 最大支持 256个优先级(0~255),数值越小的线程优先级越高。0 为最高优先级。最低优先级默认分配给空闲线程,用户一般不用。

可以根据实际情况配置优先级个数,对于 ARM Cortex-M 系列,普遍采用 32 个优先级(0~31)。

3. 时间片

RT-Thread 允许多个线程具有相同的优先级,相同优先级的线程之间采用时间片轮转的方式进行调度。创建线程的时候,可以配置线程的时间片参数。时间片仅对优先级相同的就绪线程有效。

时间片的作用是约束线程单次运行的时长,其单位是系统时钟节拍(OS Tick)。

4. 线程栈

RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。

线程栈还用来存放函数中的局部变量。当一个函数调用另外一个函数时,函数中的局部变量会暂时存放在栈中。

线程栈的增长方向由芯片架构决定的:由高地址向低地址增长、由低地址向高地址增长。这两种增长方式 RT-Thread 均支持。对于 ARM Cortex-M 架构,线程栈构造如下图所示:

image-20220115002105202.png

5. 线程的入口函数

入口函数是线程实现预期功能的函数。线程的入口函数由用户设计,一般有以下两种形式:

  • 无线循环模式

这种线程会一直被系统循环调度,执行任务,不会删除。

void thread_entry(void *parameter)
{
    while(1)
    {
      /* 线程处理 */
    }
}
  • 顺序执行或有限次循环模式

这类线程不会循环或者不会永久循环,执行完毕之后,线程将被系统自动删除。

void thread_entry(void *parameter)
{
    /* 处理任务 */
    ...
}

线程状态

对于单 CPU 来说,系统运行过程中,同一时刻只有一个线程在处理器运行。在运行过程中,线程有多种不同的运行状态:

  • 初始状态,线程刚创建还未开始运行时处于的状态,此状态下,线程不参与调度。
  • 就绪状态,线程具备运行条件的状态,等待被调度器调度执行。
  • 运行状态,线程正在运行。
  • 挂起状态,也称为阻塞状态。由于资源不可用或线程主动延时一段时间,而进入的状态。线程不能执行。
  • 关闭状态。线程运行结束处于的状态。此时线程不参与调度。

下图是各个状态之间的转换图,通过此图可以对 RT-Thread 线程的运行状态有一个整体的认识。

image-20220115215823502.png

图中涉及到的系统调用函数,在后面的学习会进行详细讲解。此处进行简单的说明:

  • rt_thread_create/init() 创建或初始化一个线程,此线程处于初始状态。
  • rt_thread_startup() 函数使得初始化状态的线程进入到就绪状态。
  • rt_thread_delay(),rt_sem_take(), rt_mutex_take() 等函数使得运行状态的线程进入到挂起状态。
  • rt_thread_resume(), rt_sem_release() 等函数使得挂起状态的线程返回到就绪状态。
  • rt_thread_delete/detach() 函数将挂起状态的线程更改为关闭状态。
  • rt_thread_exit(),处于运行状态的线程,运行结束,在线程的最后部分调用此函数,将状态更改为关闭状态。

线程控制块

在 RT-Thread 中,线程控制块由结构体 struct rt_thread 表示。

线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等。

线程控制块也包含线程与线程之间连接用的链表结构,线程等待事件集合等。

详细定义如下,列出了关键结构成员,并加入了注释,了解即可:

struct rt_thread
{
  /* rt 对象 */
  char        name[RT_NAME_MAX];   /* 线程名称 */
  rt_uint8_t  type;                /* 对象类型 */
  rt_uint8_t  flags;               /* 标志位 */

  rt_list_t   list;                /* 对象列表 */
  rt_list_t   tlist;               /* 线程列表 */

  /* 栈指针与入口指针 */
  void       *sp;                /* 栈指针 */
  void       *entry;             /* 线程入口函数指针 */
  void       *parameter;         /* 参数 */
  void       *stack_addr;        /* 栈地址指针 */
  rt_uint32_t stack_size;        /* 栈大小 */

  /* 错误代码 */
  rt_err_t    error;            /* 线程错误代码 */

  rt_uint8_t  stat;             /* 线程状态 */

    ....

  /* 优先级 */
  rt_uint8_t  current_priority;   /* 当前优先级 */
  rt_uint8_t  init_priority;      /* 初始优先级 */
  rt_uint32_t number_mask;

    ......

  rt_ubase_t  init_tick;       /* 线程初始化计数值 */
  rt_ubase_t  remaining_tick;  /* 线程剩余计数值 */

  struct rt_timer thread_timer;  /* 内置线程定时器 */

  void (*cleanup)(struct rt_thread *tid); /* 线程退出清楚函数指针 */

    ...

    rt_uint32_t user_data;  /* 用户数据 */
};
typedef struct rt_thread *rt_thread_t;

创建线程

RT-Thread 提供了先管理相关的系统函数:包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程 等。

此处只讲解如何创建一个线程。

在 RT-Thread 中,要创建一个线程,并使得它能够被执行,需要两步:

  • 创建线程,此时一个新线程被创建,并处于初始状态。
  • 启动线程,此时线程由初始状态进入就绪状态,可以被调度器执行。

1. 创建线程

创建一个线程的函数 rt_thread_create() 定义如下:

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)

调用这个函数时,系统会从动态内存堆中分配一个线程句柄(线程控制块),并按照参数中指定的栈大小从动态内存堆中分配相应的空间。

函数的各个参数解释如下:

参数描述
name线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉
entry线程入口函数
parameter线程入口函数参数
stack_size线程栈大小,单位是字节
priority线程的优先级。优先级范围根据系统配置(rtconfig.h 中的RT_THREAD_PRIORITY_MAX 宏定义)
tick线程的时间片大小。单位是操作系统的时钟节拍

线程创建成功,返回线程的控制块指针,也可称为线程句柄。

创建失败,则返回 RT_NULL。

2. 线程启动

线程创建完成后,需要使其进入就绪状态,也就是启动线程。可以通过调用 rt_thread_startup() 函数来完成。其函数原型为:

rt_err_t rt_thread_startup(rt_thread_t thread)

调用此函数成功后,会将线程放到相应优先级队列中等待调度。如果新启动的线程优先级比当前优先级高,将立即切换到这个线程。

参数 thread ,线程句柄,即线程控制块的指针。

线程启动成功,返回 RT_EOK;启动失败,则返回 -RT_ERROR。

线程创建示例

此处用一个简单的例子,来演示一下 RT-Thread 线程创建。程序源码如下:

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#include <rtthread.h>

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

void thread_entry(void *parameter)
{
    rt_uint32_t count = 0;
    
    while(1)
    {
        /* 线程运行,打印计数 */
        rt_kprintf("thread run: %d\n", count ++);
        rt_thread_mdelay(500);
    }
}

int main(void)
{
    rt_thread_t tid = RT_NULL;
    
    /* 创建线程, 名称是 thread_test, 入口是 thread_entry*/
    tid = rt_thread_create("thread_test",
                            thread_entry, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
                            
    /* 线程创建成功,则启动线程 */
    if (tid != RT_NULL)
    {
        rt_thread_startup(tid);
    }

    return 0;
}

编译运行,结果如下,线程创建成功,执行过程中,在打印计数信息:

image-20220115234731346.png

OK,今天先到这,下次继续。加油~

公众号【一起学嵌入式】,一起学习,一起成长
推荐阅读
关注数
2392
内容数
31
公众号【一起学嵌入式】专注嵌入式软件技术分享,RTOS、Linux、C/C++,一起学习,一起进步。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息