【玩转RT-Thread】线程管理(详细原理)(上)

简介: 【玩转RT-Thread】线程管理(详细原理)(上)

一、序言

在日常生活中,我们通常会将一个大的问题拆分细化,拆开成若干个小问题,通过逐个解决小问题,大问题也就解决了。

同样的在RT-Thread多线程操作系统中,开发人员基于这种分而治之的思想,将一个复杂的应用问题抽象成若干个小的、可调度的、可序列化的程序单元。当合理地划分任务并正确地执行时,这种设计能够让系统满足实时系统的性能及时间的要求。

下面看一个例子:我们的任务是读取传感器上的数据,并将相关数据显示出来。通过拆分结构,我们可以发现主要有两个任务:

1.读取数据

2.显示数据

简单来说,就是一个子任务不间断地读取传感器数据,并将数据写到共享内存中,另外一个子任务周期性的从共享内存中读取数据,并将传感器数据输出到显示屏上。

在RT-Thread 中,与上述子任务对应的程序实体就是线程,线程是实现任务的载体

它是RT-Thread中最基本的调度单位,它描述了一个任务执行的运行环境,也描述了这个任务所处的优先等级,重要的任务可设置相对较高的优先级,非重要的任务可以设置较低的优先级,不同的任务还可以设置相同的优先级,轮流运行。

上下文:当线程运行时,它会认为自己是以独占CPU 的方式在运行,线程执行时的运行环境称为上下文,具体来说就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等。

二、线程管理的功能特点

RT-Thread 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程用户线程。系统线程是由RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除。

如图所示,每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。

  • RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到CPU 的使用权。
  • 当一个运行着的线程使一个比它优先级高的线程满足运行条件,当前线程的CPU 使用权就被剥夺了,或者说被让出了,高优先级的线程立刻得到了CPU 的使用权。
    如果是中断服务程序使一个高优先级的线程满足运行条件,中断完成时,被中断的线程挂起,优先级高的线程开始运行。
  • 当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程上下文(详细内容可参考【操作系统】进程上下文和线程上下文)信息恢复。

三、线程的工作机制

1.线程控制块

在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; /* 用户数据*/
};
  • 其中init_priority 是线程创建时指定的线程优先级,在线程运行过程当中是不会被改变的(除非用户 执行线程控制函数进行手动调整线程优先级)。
  • cleanup 会在线程退出时,被空闲线程回调一次以执行用户设置的清理现场等工作。
  • 最后的一个成员user_data 可由用户挂接一些数据信息到线程控制块中,以提供类似线程私有数据的实现。

2.线程的重要属性

(1) 线程栈

  • RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。
  • 线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。
  • 对于线程第一次运行,可以以手工的方式构造这个上下文来设置一些初始的环境:入口函数(PC 寄存器)、入口参数(R0 寄存器)、返回位置(LR 寄存器)、当前机器运行状态(CPSR 寄存器)。
  • 线程栈的增长方向是芯片构架密切相关的,RT-Thread 3.1.0 以前的版本,均只支持栈由高地址向低地址增长的方式,对于ARM Cortex-M 架构,线程栈可构造如下图所示。

(2) 线程状态

线程运行的过程中,同一时间内只允许一个线程在处理器中运行,从运行的过程上划分,线程有多种不同的运行状态,如初始状态、挂起状态、就绪状态等。

在RT-Thread 中,线程包含五种状态,操作系统会自动根据它运行的情况来动态调整它的状态。如下表所示:

状态 描述
初始态 当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在RT-Thread 中的宏定义为RT_THREAD_INIT
就绪态 在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在RT-Thread 中的宏定义为RT_THREAD_READY
运行态 线程当前正在运行。在单核系统中,只有rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在RT-Thread 中的宏定义为RT_THREAD_RUNNING
挂起态 也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在RT-Thread 中的宏定义为RT_THREAD_SUSPEND
关闭态 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在RT-Thread 中的宏定义为RT_THREAD_CLOSE

(3) 线程优先级

  • RT-Thread 线程的优先级是表示线程被调度的优先程度。每个线程都具有优先级,线程越重要,赋予的优先级就应越高,线程被调度的可能才会越大。
  • RT-Thread 最大支持256 个线程优先级(0~255),数值越小的优先级越高,0 为最高优先级。在一些资源比较紧张的系统中,可以根据实际情况选择只支持8 个或32 个优先级的系统配置;对于ARM Cortex-M系列,普遍采用32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。

(4) 时间片

每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick)。

假设有2 个优先级相同的就绪态线程A 与B,A 线程的时间片设置为10,B 线程的时间片设置为5,那么当系统中不存在比A 优先级高的就绪态线程时,系统会在A、B 线程间来回切换执行,并且每次对A 线程执行10 个节拍的时长,对B 线程执行5 个节拍的时长,如下图。

(5) 线程的入口函数

线程控制块中的entry是线程的入口函数,它是线程实现预期功能的函数。

线程的入口函数由用户设计实现,一般有以下两种代码形式:

1.无限循环模式

在实时系统中,线程通常是被动式的:这个是由实时系统的特性所决定的,实时系统通常总是等待外

界事件的发生,而后进行相应的服务:

void thread_entry(void* paramenter)
{
while (1)
  {
    /* 等待事件的发生*/
    /* 对事件进行服务、进行处理*/
  }
}

作为一个实时系统,一个优先级明确的实时系统,如果一个线程中的程序陷入了死循环操作,那么比它优先级低的线程都将不能够得到执行。

所以在实时操作系统中必须注意的一点就是:线程中不能陷入死循环操作,必须要有让出CPU使用权的动作,如循环中调用延时函数或者主动挂起。用户设计这种无线循环的线程的目的,就是为了让这个线程一直被系统循环调度运行,永不删除。

2.顺序执行或有限次循环模式

如简单的顺序语句、do whlie() 或for() 循环等,此类线程不会循环或不会永久循环,可谓是“一次性”线程,一定会被执行完毕。在执行完毕后,线程将被系统自动删除。

static void thread_entry(void* parameter)
{
  /* 处理事务#1 */
  /* 处理事务#2 */
  /* 处理事务#3 */
}

(6) 常见的线程错误码

#define RT_EOK 0 /* 无错误*/
#define RT_ERROR 1 /* 普通错误*/
#define RT_ETIMEOUT 2 /* 超时错误*/
#define RT_EFULL 3 /* 资源已满*/
#define RT_EEMPTY 4 /* 无资源*/
#define RT_ENOMEM 5 /* 无内存*/
#define RT_ENOSYS 6 /* 系统不支持*/
#define RT_EBUSY 7 /* 系统忙*/
#define RT_EIO 8 /* IO 错误*/
#define RT_EINTR 9 /* 中断系统调用*/
#define RT_EINVAL 10 /* 非法参数*/

3.线程状态切换

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() 函数,将状态更改为关闭状态。

!!! note “注意事项” RT-Thread 中,实际上线程并不存在运行状态,就绪状态和运行状态是等同的。

目录
相关文章
|
6天前
|
存储 设计模式 并行计算
CopyOnWriteArrayList:深入理解Java中的线程安全List原理和应用
CopyOnWriteArrayList:深入理解Java中的线程安全List原理和应用
|
6天前
|
缓存 监控 Java
深入Elasticsearch:线程池的原理与应用
深入Elasticsearch:线程池的原理与应用
|
6天前
|
Java
线程池ThreadPoolExcutor源码剖析---工作原理
线程池ThreadPoolExcutor源码剖析---工作原理
|
2月前
|
监控 Java 调度
Java并发编程:线程池的原理与实践
【5月更文挑战第30天】 在现代软件开发中,尤其是Java应用中,并发编程是一个不可忽视的领域。线程池作为提升应用性能和资源利用率的关键技术之一,其正确使用和优化对系统稳定性和效率至关重要。本文将深入探讨线程池的核心原理、常见类型以及在实际开发中的使用案例,旨在帮助开发者更好地理解和运用线程池技术,构建高性能的Java应用程序。
|
2月前
|
安全 Java 编译器
Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)
Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)
|
2月前
|
安全 Java 开发者
谈谈Java线程同步原理
【5月更文挑战第24天】Java 线程同步的原理主要基于两个核心概念:互斥(Mutual Exclusion)和可见性(Visibility)。
20 3
|
19天前
|
缓存 算法 Java
深入解析线程上下文切换的原理与优化策略
深入解析线程上下文切换的原理与优化策略
31 0
|
2月前
|
消息中间件 安全 Ubuntu
【操作系统原理】—— 线程同步
【操作系统原理】—— 线程同步
26 1
|
2月前
|
监控 Java 开发者
深入理解Java并发编程:线程池的工作原理与实践
【5月更文挑战第29天】 在现代Java应用开发中,高效地管理并发任务是至关重要的。本文将深入探讨Java线程池的核心机制,揭示其背后的设计哲学和运作模式。通过分析线程池的优势、工作过程及关键参数,结合实例演示如何合理配置和优化线程池以提高应用程序的性能和响应能力。
|
2月前
|
Java 开发者
Java并发编程:理解线程池的工作原理与实践应用
【5月更文挑战第29天】在Java并发编程中,线程池是一种管理线程资源的重要工具。通过深入探讨线程池的工作原理和实践应用,本文将帮助开发者更好地理解和使用线程池,提高系统性能和稳定性。