【RT-Thread】国产操作系统内核学习

  • Post author:
  • Post category:其他




RT-Thread国产操作系统内核学习



1.初识RT-Thread



1.1 代码目录介绍

在这里插入图片描述



1.2 动态内存堆的使用


1.2.1 动态内存堆基础

以stm32f103为例,startup_stm32f103xe.s文件中对堆栈大小都有所定义:

在这里插入图片描述

在board.c头文件中定义了STM32板初始化时的堆空间地址:

在这里插入图片描述

HEAP_BEGIN和HEAP_END在board.h中定义如下:有关堆空间起始地址、堆栈大小及结束地址:

在这里插入图片描述

Image$$RW_IRAM1$$ZI$$Limit是一个链接器导出的符号,代表ZI段的结束,也就是程序执行区的RAM结束后的地址,反过来也就是我们执行区的RAM未使用的区域的起始地址

(Total RW+heap size = MCU total RAM size)



1.2.2 动态内存相关API
void *rt_malloc(rt_size_t size)
动态申请内存块
void *rt_realloc(void *rmem, rt_size_t newsize)
在已分配内存块的基础上重新分配内存块的大小(缩小的情况下,后面的数据被自动截断)
void *rt_calloc(rt_size_t count, rt_size_t size)
从内存堆中分配连续内存地址的多个内存块


1.2.3 内存查看技巧

①点开魔术棒,在Target中可以查询当前芯片的RAM和ROM大小(但具体如何测算并不知道)

在这里插入图片描述

②双击项目文件夹可打开map文件:

在这里插入图片描述

map文件底端可查看当前芯片内存大小,如下:

在这里插入图片描述



1.2.4 动态内存堆使用注意点

①内存复位

每次申请到新的内存块之后,建议对所申请到的内存块进行清零操作,如:

p = rt_malloc(10);//申请动态内存
p = RT_NULL;//清零操作
rt = rt_memset(p,0,10);//使用申请到的动态内存堆

②内存泄漏

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

在使用动态内存时需要注意:rt-malloc需要和rt_free配套使用。



1.2.5 示例代码

①动态申请内存线程代码编写

/* 线程入口 */
void thread1_entry(void *parameter)
{
	int i;
	char *ptr = RT_NULL; /* 内存块的指针 */

	for (i = 0; ; i++)
	{
    	/* 每次分配 (1 << i) 大小字节数的内存空间 */
    	ptr = rt_malloc(1 << i);

    	/* 如果分配成功 */
    	if (ptr != RT_NULL)
    	{
        	rt_kprintf("get memory :%d byte\n", (1 << i));
        	/* 释放内存块 */
        	rt_free(ptr);
        	rt_kprintf("free memory :%d byte\n", (1 << i));
        	ptr = RT_NULL;
    	}
    	else
    	{
        	rt_kprintf("try to get %d byte memory failed!\n", (1 << i));
        	return;
    	}
	}
}

②创建线程

int dynmem_sample(void)
{
	rt_thread_t tid;

	/* 创建线程1 */
	tid = rt_thread_create("thread1",
                       thread1_entry, RT_NULL,
                       THREAD_STACK_SIZE,
                       THREAD_PRIORITY,
                       THREAD_TIMESLICE);
	if (tid != RT_NULL)
    	rt_thread_startup(tid);
	return 0;
}

③导出到msh 命令列表中

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


1.3 线程的创建


1.3.1 线程组成

RT-Thread中,线程由三部分组成,线程代码(入口函数)、线程控制块、线程堆栈:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述



1.3.2 线程创建

在这里插入图片描述

init创建的线程为静态线程,create创建的为动态线程。二者都使用片内RAM的话,几乎是没有区别的,如果是外扩RAM速度会慢于片内RAM。

在这里插入图片描述

调用此函数后创建的线程会被加入到线程的就绪队列,执行调度。



1.3.3 示例代码

①线程代码(入口参数)

/* 线程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);
	}
}

/* 线程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运行结束后也将自动被系统删除
	(线程控制块和线程栈依然在idle线程中释放) */
}

②线程控制块

static rt_thread_t tid1 = RT_NULL;

static char thread2_stack[1024];
static struct rt_thread thread2;

③线程堆栈

/* 删除线程示例的初始化 */
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;
}

④相关头文件

#include <rtthread.h>

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

⑤其他代码(没懂什么意思)

ALIGN(RT_ALIGN_SIZE)
#define RT_ALIGN_SIZE 4

thread_sample中定义了两个线程:thread1和thread2,thread1的优先级为25,thread2的优先级定位25-1,thread2优先级更高,因两个线程定义在同一个函数内,执行完rt_thread_startup函数后,两个线程都将进入就绪状态,因为thread2线程优先级更高,将在thread2执行完后再执行thread1线程。



1.4 线程实例(跑马灯)


1.4.1 线程状态

线程共有5种状态:初始状态、就绪状态、运行状态、挂起状态、关闭状态,线程之间切换关系如下:

在这里插入图片描述



1.4.2 滴答时钟

每个操作系统中都存在一个“系统心跳”时钟,是操作系统中最小的时钟单位。这个时钟负责系统和时间相关的一些操作。作为操作系统运行的时间尺度,心跳时钟是由硬件定时器的定时中断产生。

系统的心跳时钟我们也常称之为系统滴答或时钟节拍,系统滴答的频率需要我们根据cpu的处理能力来决定。

时钟节拍使得内核可以将线程延时若干个整数时钟节拍,以及线程等待事件发生时,提供等待超时的依据。

频率越快,内核函数介入系统运行的几率就越大,内核占用的处理器时间就越长,系统的负荷就变大;频率越小,时间处理精度又不够;

我们在stm32平台上一般设置系统滴答频率为100HZ,即每个滴答的时间是10ms



1.4.3 GPIO驱动架构操作IO
//IO初始化
void rt_pin_mode(rt_base_pin, rt_base_t mode)
									PIN_MODE_OUTPUT
									PIN_MODE_INPUT
									PIN_MODE_INPUT_PULLUP
									PIN_MODE_INPUT_PULLDOWN
									PIN_MODE_OUTPUT_OD
//IO写入
void rt_pin_write(rt_base_pin, rt_base_t value)
									PIN_HIGH
									PIN_LOW
//IO读出
int rt_pin_read(rt_base_t pin)

Drivers目录下的drv_gpio.c文件中定义了pin脚:

在这里插入图片描述

如图,如需要使用PF4引脚,将引脚设置为14即可。



1.4.4 示例代码
//创建线程
void led_test(void)
{
	/* 创建线程,名称是led,入口是led_entry */
	tid = rt_thread_create("led",
							led_entry, RT_NULL,
							512,
							10, 10);
	/* 如果获得线程控制块,启动这个线程 */
	if(tid1 != RT_NULL)
		rt_thread_startup(tid1);
}

//编写入口函数
static void led_entry(void *param)
{
	rt_pin_mode(14, PIN_MODE_OUTPUT);
	//若此处出现红线报错,则需要引入头文件:“<rtdevice.h>”
	
	while(1)
	{
		rt_pin_write(14, PIN_LOW);
		rt_thread_delay(50);//rt_thread_mdelay(500);rt_rtthread_sleep(50);
		rt_pin_write(14, PIN_HIGH);
		rt_thread_delay(50);
	}
}

其中,delay函数和sleep函数是以系统时间节拍为单位的,mdelay函数是以ms为单位的


注:线程可以通过导出到msh命令行的方式,在msh命令行输入进行调用,或者直接将函数名复制到main函数中即可自动调用。



1.4.5 线程栈大小分配技巧

先将线程栈大小设置一个固定值(比如2048),在线程运行时通过查看线程栈的使用情况,了解线程栈使用的实际情况,根据情况设置合理的栈大小:

在这里插入图片描述

通过查看max used一列来调整分配的栈空间大小,一般将线程栈最大使用量设置为70%。



1.5 线程时间片轮转调度(timeslice_sample.c)


1.5.1 优先级和时间片

优先级和时间片是线程的两个重要参数,分别描述了线程竞争处理器资源的能力和持有处理器时间长短的能力。

RT-Thread最大支持256个优先级(数值越小优先级越高,0是最高优先级,最低优先级给空闲线程)

用户可以通过rt_config.h中的RT_THREAD_PRIORITY_MAX来修改最大支持的优先级;

针对STM32默认设置最大支持32个优先级;

具体应用中,线程总数不受限制,能创建的线程总数只和具体硬件平台的内存有关。

在这里插入图片描述



1.5.2 线程调度规则
优先级抢占调度:(实时性要求较高的任务)
	当有任务的优先级高于当前任务优先级并且处于就绪态后,就会发生任务调度。
时间片轮询调度:
	当操作系统中存在相同优先级线程时,会按照设置的时间片大小进行轮流调度。


1.6 空闲线程及两个常用的钩子函数(idle_hook_sample.c/scheduler_hook.c)

空闲线程是一个比较特殊的系统线程,它具备最低的优先级。当系统中无其他就绪线程可运行时,调度器将调度到空闲线程。

空闲线程还负责一些系统资源回收的以及将一些处于关闭态的线程从线程调度列表中移出的动作。

空闲线程在形式上是一个无线循环结构,且永远不被挂起。

在RT-Thread实时操作系统中,空闲线程向用户提供了钩子函数,空闲线程钩子函数可以让系统在空闲的时候执行一些非紧急事务,例如系统运行指示灯闪烁,CPU使用率统计等等。

c

设置钩子函数

rt_err_t rt_thread_idle_sethook(void(*hook)(void))

删除钩子函数

rt_err_t rt_thread_idle_delhook(void(*hook)(void)

注意事项:

空闲线程是一个线程状态永远为就绪态的线程,所以钩子函数中执行的相关代码必须保证空闲线程在任何时候都不会被挂起,例如rt_thread_dalay()、rt_sem_take()等可能会导致线程挂起的阻塞类函数都不能在钩子函数中使用。

空闲线程可以设置多个钩子函数。(最多4个)

系统的上下文切换是系统运行过程中最普遍的时间,有时用户可能会想知道在某一个时刻发生了什么样的线程切换,RT-Thread向用户提供了一个系统调度钩子函数,这个钩子函数在系统进行任务切换时,通过这个钩子函数,我们可以了解到系统任务调度时的一些信息

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


钩子函数的作用就是用于了解系统任务调度时的一些信息。



1.7 临界区保护(interrupt_sample.c)

临界资源是指一次仅允许一个线程访问的共享资源。可以是一个具体的硬件设备,也可以是一个变量、一个缓冲区。(多个线程之间必须互斥)

每个线程中访问(操作)临街资源的那段代码称为临界区(Critical Section),每次只准许一个线程进入临界区

如:定义了一个全局变量、两个优先级相同的线程,两个线程都对该全局变量有赋值操作,线程在时间片轮询调度的时候进行了切换,导致提前对该全局变量进行了值改变,这违背了代码设计初衷。



1.7.1 关闭系统调度保护临界区(禁止调度、关闭中断)

①禁止调度方法

禁止调度,即时把调度器锁住,不让其进行线程切换。这样就能保证当前运行的任务不被换出,直到调度器解锁,所以禁止调度室常用的临界区保护方法。

void thread_entry(void* parameter)
{
	while(1)
	{
		/*调度器上锁,上锁后将不再切换到其他线程,仅响应中断*/
		rt_enter_critical();
		/*以下进入临界区*/
		……
		/*rt_exit_critical();*/
	}
}

②关闭中断

因为所有线程的调度都是建立在中断的基础上的,所以,当我们关闭中断后,系统将不能再进行调度,线程自身也自然不会被其他线程抢占了。

void thread_entry(void* parameter)
{
	rt_base_t level;
	while(1)
	{
		/*关闭中断*/
		level = rt_hw_interrupt_disable();
		/*以下是临界区*/
		……
		/*关闭中断*/
		rt_hw_interrupt_enable(level);
	}
}


1.7.2 互斥特性保护临界区(信号量、互斥量)


信号量的使用(semaphore_sample.c)

①IPC机制

在嵌入式系统中运行的代码主要包括线程和ISR,在它们的运行过程中,它们的运行步骤有时需要

同步

(按照预定的先后次序运行),他们访问的资源有时需要

互斥

(一个时刻只允许一个线程访问资源),它们之间有时也要彼此

交换数据

。这些需求,有的是因为应用需求,有的是多线程编程模型带来的需求。

操作系统必须提供相应的机制来完成这些功能,我们把这些机制统称为进(线)程间通信(Internal Process Communication IPC),RT-Thread中的IPC机制包括信号量、互斥量、事件、邮箱、消息队列

②信号量工作机制

信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放她,从而达到同步或互斥的目的。

每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应信号量对象的实例数目(资源数目),假如信号量值N,则表示共有N个信号量实例(资源)可以被使用,当信号量实例数目为零时,再请求该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。(相当于车位满了,要等有空车位才能继续,否则就排队等)

③信号量控制块

在RT-Thread中,信号量控制块是操作系统用于管理信号量的一个数据结构

struct rt_semaphore
{
	struct rt_ipc_object parent;/**<inherit from ipc_object>*/
	rt_uint16_t value;/**<value of semaphore> */
};

定义静态信号量:struct rt_semaphore static_sem

定义动态信号量:rt_sem_t dynamic_sem

④信号量的操作

在这里插入图片描述
针对静态信号量使用初始化与脱离相关API,针对动态信号量使用创建与删除相关API

其中sem为定义的静态信号量,name为名称,value为初始值,flag分为两种:RT_IPC_FLAG_FIFO及RT_IPC_FLAG_PRIO,前者表示信号量对应的线程排队时按照先进先出的方式进行排队,后者表示按照优先级进行排队。

其中获取信号量的time单位为系统的滴答时钟,当time为负数时(一般使用RT_WAITING_FOREVER=-1),表示一直等待。

其中rt_sem_trytake函数没有time函数,其实是0,该函数是一个不等待的take函数,获取不到则返回一个”-RT_ETimeout”

此外,rt_sem_take函数会导致线程挂起,只能在线程中调取,不能在中断(ISR)中调取。


总的来说,先新建一个信号量,进行初始化(选定初始值,以及flag等待方式),在一个线程中释放信号量,另一个线程中通过take函数进行调取,如果能获取到则执行相关内容,如果获取不到则等待。



生产者消费者问题



版权声明:本文为m0_53488817原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。