这个文章很长,主要是对RT-Threadd的教学视频 (视频链接,)的总结和回顾。是一个比较个人化的东西,其实比较专业的内核指导在RTT官网上也有总结(内核链接)。只是视频中有一些比较基础的官网上没提到的地方做下总结。
|----+ RT-Thread:
|------src:RT-Thread内核代码文件
|------libcpu:各类芯片/内核移植代码
|------include:RT-Thread内核头文件
|------components:RT-Thread外部组件代码
|-------+ project
|----------application:用户应用代码
|----------drivers:RT-Thread的驱动,不同平台底层驱动的具体实现
|----------kernel-sample:内核例程
|----------Libraries:STM32芯片固件库
|----------rt-thread:RT-Thread源代码
|-------+ project
|----------Application:用户应用代码
|----------Drivers:RT-Thread的底层驱动代码
|----------STM32_HAL:存放STM32固件库文件【Libraries】
|----------Kernel:存放RT-Thread内核核心代码【src】
|----------CORTEX-M3:存放CORTEX-M3移植代码【libcpu】
|----------DeviceDrives:驱动框架源码【components->d】
|----------finsh:RT-Thread命令行和finsh组件【components->f】
|----------kernel-sample:RT-Thread内核例程
int main(void)
{
return 0;
}
无法从main函数中看到RT-Thread的启动过程
$sub$$main
int $Sub$$main(void)
{
rt_hw_interrupt_disable();//关中断
rtthread_startup();//RTT启动函数
return 0;
}
关于
$Sub$$main
和main之间的说法,我之前有解释过,可以看链接。
int rtthread_startup(void)
{
rt_hw_interrupt_disable();//关中断
/* board level initalization
* NOTE: please initialize heap inside board initialization.
*/
rt_hw_board_init();//硬件平台初始化
/* show RT-Thread version */
rt_show_version();//显示版本号码
/* timer system initialization */
rt_system_timer_init();//系统时钟初始化
/* scheduler system initialization */
rt_system_scheduler_init();//系统调度器的初始化
#ifdef RT_USING_SIGNALS
/* signal system initialization */
rt_system_signal_init();//系统信号机制的初始化
#endif
/* create init_thread */
rt_application_init();//很重要,因为用户的任务在这里面创建的
/* timer thread initialization */
rt_system_timer_thread_init();
/* idle thread initialization */
rt_thread_idle_init();
/* start scheduler */
rt_system_scheduler_start();//使rtt跑起来
/* never reach here */
return 0;
}
void rt_application_init(void)
{
rt_thread_t tid;
#ifdef RT_USING_HEAP
tid = rt_thread_create("main", main_thread_entry, RT_NULL,
RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(tid != RT_NULL);
#else
rt_err_t result;
tid = &main_thread;
result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(result == RT_EOK);
/* if not define RT_USING_HEAP, using to eliminate the warning */
(void)result;
#endif
rt_thread_startup(tid);
}
int a = 0 ;
char *p1
int main()
{
int b;
char s[] = "abc";
char *p2;
char *p3 = "123456";
static int c = 0;
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
}
- 定义的局部变量都来源于栈空间,栈是由编译器自动分配和释放的,当函数结束,栈空间就释放掉;
- malloc函数申请的都来源于堆空间。
在board.c中,需要一个函数事先帮我们配置好动态内存的api。
在board.c中,也就是进行相关内存初始化的时候,会去配置动态内存。会根据实际芯片,或者是板卡上的内存去配置。
rt_system_heap_init((void *)HEAP_BEGIN,(void *)HEAP_END);
//从系统中取到一块内存设置为动态内存区,然后可以通过RTT特定的rt_malloc和rt_free,申请和释放内存
//一般在board.c中调用,在进行板级初始化的时候,配置动态内存,根据芯片或板卡的实际情况配置
函数需要提供一个起始地址HEAP_BEGIN
和结束地址HEAP_END
,中间的这段空间会被系统当作动态内存空间使用。
本节示例使用的芯片,具有64kram空间。
rt_malloc
和rt_free
,申请和释放内存。#define HEAP_BEGIN ((void *)&Image$$RW_IRAM$$Limit)
HEAP_BEGIN = (void *)&Image$$RW_IRAM$$Limit
Image$$RW_IRAM$$Limit
连接器导出的符号,代表ZI段的结束,也就是程序执行去的RAM结束后的地址,反过来就是我们执行去的RAM未使用的区域的起始地址Image$$RW_IRAM$$Limit
表示的。我们一般以片内ram的结束地址,作为结束地址。
Total RW+heap size = MCU total RAM size
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;
}
thread1_entry
:任务的入口函数/* 线程入口 */
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;
}
}
}
使用rt_malloc申请内存,如果申请内存失败,会返回RT_NULL[常量],申请的空间要少于系统空闲的空间才能成功
通过命令行的方式,导出特定的符号
p = rt_malloc(10);
if(p != RT_NULL)
{
rt_memset(p,0,10);
}
void *rt_realloc(void *tmen,rt_size_t newsize)
如果前面分配了50个字节的内存空间,后面发现不够,想加到100,或者原本分配了50,太大了,需要20就够了
void *rt_calloc(rt_size_t count, rt_size_t size)
RTT中,线程有三部分组成:线程代码(入口函数)、线程控制块,线程堆栈
void thread_entry(void *parameter)
{
whlie(1)
{
/* 等待事件的发生 */
/* 处理事件 */
}
}
while中往往要加入让出CPU使用权的API函数,如果不加,其他线程得不到执行。
void thread_entry(void *parameter)
{
/* 事务1处理 */
/* 事务2处理 */
}
线程控制块是操作系统用于管理线程的一个数据结构,他会存放线程的一些信息,例如,优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构、线程等待时间集合等。
/**
* Thread structure
*/
struct rt_thread
{
/* rt object */
char name[RT_NAME_MAX]; /**< the name of thread */
rt_uint8_t type; /**< type of object */
rt_uint8_t flags; /**< thread's flags */
#ifdef RT_USING_MODULE
void *module_id; /**< id of application module */
#endif
rt_list_t list; /**< the object list */
rt_list_t tlist; /**< the thread list */
/* stack point and entry */
void *sp; /**< stack point */
void *entry; /**< entry */
void *parameter; /**< parameter */
void *stack_addr; /**< stack address */
rt_uint32_t stack_size; /**< stack size */
/* error code */
rt_err_t error; /**< error code */
rt_uint8_t stat; /**< thread status */
/* priority */
rt_uint8_t current_priority; /**< current priority */
rt_uint8_t init_priority; /**< initialized priority */
#if RT_THREAD_PRIORITY_MAX > 32
rt_uint8_t number;
rt_uint8_t high_mask;
#endif
rt_uint32_t number_mask;
#if defined(RT_USING_EVENT)
/* thread event */
rt_uint32_t event_set;
rt_uint8_t event_info;
#endif
#if defined(RT_USING_SIGNALS)
rt_sigset_t sig_pending; /**< the pending signals */
rt_sigset_t sig_mask; /**< the mask bits of signal */
void *sig_ret; /**< the return stack pointer from signal */
rt_sighandler_t *sig_vectors; /**< vectors of signal handler */
void *si_list; /**< the signal infor list */
#endif
rt_ubase_t init_tick; /**< thread's initialized tick */
rt_ubase_t remaining_tick; /**< remaining tick */
struct rt_timer thread_timer; /**< built-in thread timer */
void (*cleanup)(struct rt_thread *tid); /**< cleanup function when thread exit */
/* light weight process if present */
#ifdef RT_USING_LWP
void *lwp;
#endif
rt_uint32_t user_data; /**< private user data beyond this thread */
};
typedef struct rt_thread *rt_thread_t;
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);
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);
rt_err_t rt_thread_startup(rt_thread_t thread);
调用此函数后创建的线程会被加入到线程的就绪队列,执行调度。
#include
#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)
/* 创建线程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);
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程2入口 */
static void thread2_entry(void *param)
{
}
/* 初始化线程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);
然后再示例中,rt_thread_delay(50),由于系统时钟是100HZ,一个滴答时间是4ms,50个滴答是0.5秒。
/**
* This is the timer interrupt service routine.
*
*/
void SysTick_Handler(void)
{
/* enter interrupt */
rt_interrupt_enter();
HAL_IncTick();
rt_tick_increase();
/* leave interrupt */
rt_interrupt_leave();
}
void rt_pin_mode(rt_base_t pin, rt_base_t mode)
{
RT_ASSERT(_hw_pin.ops != RT_NULL);
_hw_pin.ops->pin_mode(&_hw_pin.parent, pin, mode);
}
//IOmode
#define PIN_MODE_OUTPUT 0x00
#define PIN_MODE_INPUT 0x01
#define PIN_MODE_INPUT_PULLUP 0x02
#define PIN_MODE_INPUT_PULLDOWN 0x03
#define PIN_MODE_OUTPUT_OD 0x04
#define STM32F10X_PIN_NUMBERS 144
#if (STM32F10X_PIN_NUMBERS == 144)
__STM32_PIN_DEFAULT,
__STM32_PIN(1, E, 2),
__STM32_PIN(2, E, 3),
__STM32_PIN(3, E, 4),
__STM32_PIN(4, E, 5),
__STM32_PIN(5, E, 6),
__STM32_PIN_DEFAULT,
__STM32_PIN(7, C, 13),
__STM32_PIN(8, C, 14),
__STM32_PIN(9, C, 15), // 如果要使用C15管教,设置PIN为9即可
...
void rt_pin_write(rt_base_t pin, rt_base_t value)
PIN_HIGH
PIN_LOW
void rt_pin_write(rt_base_t pin, rt_base_t value)
{
RT_ASSERT(_hw_pin.ops != RT_NULL);
_hw_pin.ops->pin_write(&_hw_pin.parent, pin, value);
}
int rt_pin_read(rt_base_t pin)
{
RT_ASSERT(_hw_pin.ops != RT_NULL);
return _hw_pin.ops->pin_read(&_hw_pin.parent, pin);
}
#include
/* 线程1的入口函数 */
static void thread1_entry(void *parameter)
{
rt_pin_mode(14,PIN_MODE_OUTPUT);
while(1)
{
rt_pin_write(14,PIN_LOW);
rt_thread_delay(50);//rt_thread_mdelay(500);rt_tjread_sleep(50);
rt_pin_write(14,PIN_HIGH);
rt_thread_delay(50);
}
}
void led_test()
{
rt_thread_t tid1;
/* 创建线程1,名称是thread1,入口是thread1_entry*/
tid1 = rt_thread_create("led",
led_entry, RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);
/* 如果获得线程控制块,启动这个线程 */
if (tid1 != RT_NULL)
rt_thread_startup(tid1);
}
MSH_CMD_EXP。。。
先将线程大小设置一个固定值(比如2048),在线程运行时通过查看线程栈的使用情况,了解线程栈的实际使用情况,根据情况设置合理的栈大小。
一般将线程栈最大使用量设置为70%。
时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick),详见第五章。
假设有 2 个优先级相同的就绪态线程 A 与 B,A 线程的时间片设置为 10,B 线程的时间片设置为 5,那么当系统中不存在比 A 优先级高的就绪态线程时,系统会在 A、B 线程间来回切换执行,并且每次对 A 线程执行 10 个节拍的时长,对 B 线程执行 5 个节拍的时长,如下图。
操作系统总是让具有最高优先级的就绪任务优先运行:即当有任务的优先级高于当前任务优先级并且处于就绪态后,就一定会发生任务调度
当操作系统中存在相同优先级的线程时(优先级相同就不会抢占),操作系统会按照设置的时间片大小来轮流调度线程,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick)
在timeslice.c
/*
* 程序清单:相同优先级线程按照时间片轮番调度
*
* 这个例子中将创建两个线程,每一个线程都在打印信息
*
*/
#include
#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;
/* 创建线程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 也有着它的特殊用途:若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。
rt_err_t rt_thread_idle_sethook(void (*hook)(void))
rt_err_t rt_thread_idle_delhook(void (*hook)(void))
rt_thread_delay
和rt_sem_take
都可能会导致线程挂起的阻塞类函数都不能在钩子函数中使用RT_IDEL_HOOK_LIST_ZISE
决定可以设置几个钩子函数。系统的上下文切换是系统运行时最普遍的事件,有时用户可能会想知道在某一时刻发生了什么样的线程切换,RT-Thread向用户提供了一个系统调度钩子函数,这个钩子函数在系统进行任务切换时运行,通过这个钩子函数,我们可以了解到系统任务调度时一些信息。
rt_scheduler_sethook(void (*hook)(struct rt_thread *form, struct rt_thread *to))
即把调度器锁住, 不让线程进行切换。这样可以保证当前运行的任务不被换出,直到调度器解锁。
void thrend_entry(void* paremeter)
{
while(1)
{
/* 调度器上锁,仅响应中断 */
rt_enter_critical();
/* 进入临界区*/
...
/* 调度器解锁 */
rt_exit_critical();
}
}
因为所有的线程调度都是建立在中断的基础上的,所以,当我们关闭中断后,系统将不能在进行调度,线程自身也自然不会其他线程抢占了
void thrend_entry(void* paremeter)
{
rt_base_t level;
while(1)
{
/* 关闭中断 */
level = rt_hw_interruput_disable();
/* 以下是临界区 */
...
/* 关闭中断 */
rt_hw_interruput_enable(level);
}
}
在嵌入式系统中运行的代码主要包括线程和中断[ISR],在它们的运行过程中,它们的运行步骤有时需要同步(按照预定的先后次序运行),他们访问的资源有时候需要互斥(一个时刻只允许一个线程访问资源),它们制键又是也要彼此交换数据。这些需要,有的是因为应用需求,有的是多线程编程模型带来的需求。
操作系统必须提供相应的机制来完成这些功能(同步,互斥,数据交互),我们把这些机制统称为**进程间通信(Internal Process Communication , IPC),RT-Thread中的IPC机制包括信号量、互斥量、事件、邮箱、消息队列。
通过IPC机制,我们可以协调多个线程(包括ISR)“默契”的工作,从而共同完成一个整项工作。
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。
每个信号量对象都有一个信号量和一个线程等待队列,信号量的值对应信号量对象的实例数目(资源数目),加入信号量值N,则便是共有N个信号量实例(资源)可以被使用,当信号量实例数目为0时,再请求信号量的线程就会被挂起在信号量的等待队列上,等待可用的信号量实例。
在RT-Thread中,信号量控制块是操作系统用于管理信号量的一个数据结构。
struct rt_semaphore
{
struct rt_ipc_object parent; /*in herit from ipc_object*/
rt_uint16_t value; /*value of semaphore*/
}
struct rt_semaphore static_sem
rt_sem_t dynamic_sem
rt_err_t rt_sem_init(rt_sem_t sem, //信号量指针
const char *name, //信号量名称
rt_uint32_t value, //信号量初始值
rt_uint8_t flag) //信号量标志:RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO
多个线程等待信号量的排队方式:RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO – 先进先出/优先级
rt_err_t rt_sem_detach(rt_sem_t sem)
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag)
rt_err_t rt_sem_delete(rt_sem_t sem)
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time) //时间参数:RT_WAITING_FOREVER=-1
=0
:立即返回>0
:以系统tick时间为单位继续等待<0
:RT_WAITING_FOREVERrt_err_t rt_sem_trytake(rt_sem_t sem)
rt_err_t rt_sem_release(rt_sem_t sem)
static void rt_thread1_entry(void *parameter)
{
static rt_uint8_t count = 0;
while(1)
{
if(count <= 100)
{
count++;
}
else
return;
/* count每计数10次,就释放一次信号量 */
if(0 == (count % 10))
{
rt_kprintf("t1 release a dynamic semaphore.\n" );
rt_sem_release(dynamic_sem);
}
}
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread2_entry(void *parameter)
{
static rt_err_t result;
static rt_uint8_t number = 0;
while(1)
{
/* 永久方式等待信号量,获取到信号量,则执行number自加的操作 */
result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
rt_kprintf("t2 take a dynamic semaphore, failed.\n");
rt_sem_delete(dynamic_sem);
return;
}
else
{
number++;
rt_kprintf("t2 take a dynamic semaphore. number = %d\n" ,number);
}
}
}
生产者消费者问题是一个经典的、多线程同步问题。
有两个线程:一个生产者线程和一个消费者线程。两个线程共享一个初始为空、固定大小为n的缓存区。
解决生产者消费者问题实际上是要解决线程间互斥关系问题和同步关系问题。
由于缓冲区是临界资源,它一个时刻只允许一个生产者放入消息,或者一个消费者从中取出消息,所以这里要解决一个互斥访问的问题。
同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,所以我们还需要解决一个同步的问题。
/* 定义最大5个元素能够被产生 */
#define MAXSEM 5
/* 用于放置生产的整数数组 */
rt_uint32_t array[MAXSEM];
/* 指向生产者、消费者在array数组中的读写位置 */
static rt_uint32_t set, get;
/* 指向线程控制块的指针 */
static rt_thread_t producer_tid = RT_NULL;
static rt_thread_t consumer_tid = RT_NULL;
struct rt_semaphore sem_lock;
struct rt_semaphore sem_empty, sem_full;
/* 生产者线程入口 */
void producer_thread_entry(void *parameter)
{
int cnt = 0;
/* 运行10次 */
while (cnt < 10)
{
/* 获取一个空位 */
rt_sem_take(&sem_empty, RT_WAITING_FOREVER);
/* 修改array内容,上锁 */
rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
array[set % MAXSEM] = cnt + 1;
rt_kprintf("the producer generates a number: %d\n", array[set % MAXSEM]);
set++;
rt_sem_release(&sem_lock);
/* 发布一个满位 */
rt_sem_release(&sem_full);
cnt++;
/* 暂停一段时间 */
rt_thread_mdelay(20);
}
rt_kprintf("the producer exit!\n");
}
/* 消费者线程入口 */
void consumer_thread_entry(void *parameter)
{
rt_uint32_t sum = 0;
while (1)
{
/* 获取一个满位 */
rt_sem_take(&sem_full, RT_WAITING_FOREVER);
/* 临界区,上锁进行操作 */
rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
sum += array[get % MAXSEM];
rt_kprintf("the consumer[%d] get a number: %d\n", (get % MAXSEM), array[get % MAXSEM]);
get++;
rt_sem_release(&sem_lock);
/* 释放一个空位 */
rt_sem_release(&sem_empty);
/* 生产者生产到10个数目,停止,消费者线程相应停止 */
if (get == 10) break;
/* 暂停一小会时间 */
rt_thread_mdelay(50);
}
rt_kprintf("the consumer sum is: %d\n", sum);
rt_kprintf("the consumer exit!\n");
}
int producer_consumer(void)
{
set = 0;
get = 0;
/* 初始化3个信号量 */
rt_sem_init(&sem_lock, "lock", 1, RT_IPC_FLAG_FIFO);
rt_sem_init(&sem_empty, "empty", MAXSEM, RT_IPC_FLAG_FIFO);
rt_sem_init(&sem_full, "full", 0, RT_IPC_FLAG_FIFO);
...
}
互斥量(互斥锁)是用于线程间互斥访问的IPC对象,它是一种特殊的二值信号量,当某个线程访问系统中的共享资源时,通过引入互斥量机制,可以保证其他线程无法获得共享资源的访问权。
互斥量只有两种状态,LOCKED和UNLOCKED,分别代表加锁和开锁的两种情况。
互斥量控制块是操作系统用于管理互斥量的一个数据结构。
struct rt_mutex
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint16_t value; /**< value of mutex 只有两种状态*/
rt_uint8_t original_priority; /**< priority of last thread hold the mutex 上一次拥有这个互斥量的优先级*/
rt_uint8_t hold; /**< numbers of thread hold the mutex ,某个线程持有互斥量的次数*/
struct rt_thread *owner; /**< current owner of mutex ,当前持有线程的线程控制块*/
};
struct rt_mutex static_mutex
rt_mutex_t dynamic_mutex
//RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO
//RT_WAITING_FOREVER = -1
- 规定时间内没有得到互斥量会返回-RT_TIME_OUT
- 当一个线程已经take了一个互斥量之后,再次take不用等待,并且hold+1
不能在中断中释放。
tatic void rt_thread_entry1(void *parameter)
{
while(1)
{
/* 线程1获取到互斥量后,先后对number1、number2进行加1操作,然后释放互斥量 */
rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
number1++;
rt_thread_mdelay(10);
number2++;
rt_mutex_release(dynamic_mutex);
}
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread_entry2(void *parameter)
{
while(1)
{
/* 线程2获取到互斥量后,检查number1、number2的值是否相同,相同则表示mutex起到了锁的作用 */
rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
if(number1 != number2)
{
rt_kprintf("not protect.number1 = %d, mumber2 = %d \n",number1 ,number2);
}
else
{
rt_kprintf("mutex protect ,number1 = mumber2 is %d\n",number1);
}
number1++;
number2++;
rt_mutex_release(dynamic_mutex);
if(number1 >=50)
return;
}
}
使用信号量会导致的另一个潜在的问题就是线程优先级翻转问题。
所谓线程优先级翻转,即当一个高优先级线程试图通过某种IPC对象已被一低优先级的线程访问共享资源时,如果该IPC对象已被一低优先级的线程所持有,而这个低优先级线程在运行过程中,可能又被一些中等优先级的线程抢占,因此会造成高优先级线程被许多具有较低优先级的线程阻塞的情况。
优先级翻转会造成高优先级线程的实时性得不到保证。
在RT-Thread中,通过互斥量的优先级继承算法,可以有效的解决优先级翻转问题。
优先级翻转是指提高某个占有某种共享资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,从而得到更快的执行,然后释放贡献资源,而当这个低优先级线程释放该资源时,优先级重新回到初始设定值
继承优先级的线程避免了系统共享资源被任何中间优先级线程抢占
。。。
就是两个线程去抢一个互斥量,优先级继承是系统中互斥量的特点,不需要做特殊处理
优先级翻转现象提醒编程人员对共享资源进行互斥访问的代码段应尽量短。
信号量主要用于“一对一”的线程同步;当需要“一对多”、“多对一"、”多对多“的同步时,就需要事件集来处理了。
RT-Thread中的事件集用一个32位无符号整型变量来表示,变量中的一个位代表一个事件,[bit如果是0的话,事件没有发生,如果为1,事件发生了]。线程通过”逻辑与“或”逻辑或”与一个或多个事件建立关联形成一个事件组合。
在RT-Thread中,事件集控制块是操作系统用于管理事件的一个数据结构。
struct rt_event
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint32_t set; /**< event set */
};
typedef struct rt_event *rt_event_t;
struct rt_event static_evt
rt_event_t dynamic_evt
//RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO
RT_EVENT_FLAG_AND、RT_EVENT_FLAG_OR、RT_EVENT_FLAG_CLEAR
/* 线程1入口函数 */
static void thread1_recv_event(void *param)
{
rt_uint32_t e;
/* 第一次接收事件,事件3或事件5任意一个可以触发线程1,接收完后清除事件标志 */
if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("thread1: OR recv event 0x%x\n", e);
}
rt_kprintf("thread1: delay 1s to prepare the second event\n");
rt_thread_mdelay(1000);
/* 第二次接收事件,事件3和事件5均发生时才可以触发线程1,接收完后清除事件标志 */
if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("thread1: AND recv event 0x%x\n", e);
}
rt_kprintf("thread1 leave.\n");
}
/* 线程2入口 */
static void thread2_send_event(void *param)
{
rt_kprintf("thread2: send event3\n");
rt_event_send(&event, EVENT_FLAG3);
rt_thread_mdelay(200);
rt_kprintf("thread2: send event5\n");
rt_event_send(&event, EVENT_FLAG5);
rt_thread_mdelay(200);
rt_kprintf("thread2: send event3\n");
rt_event_send(&event, EVENT_FLAG3);
rt_kprintf("thread2 leave.\n");
}
RT-Thread操作系统的邮箱用于线程间通信,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的4个字节的内容(针对32位系统,指针的大小即为4个字节,所以一封邮件敲好能够容纳一个指针)。
线程或中断服务例程把一封4字节长度的邮件发送到邮箱中,而其他需要的线程可以从邮箱中接收这些邮件并进行处理。
在RT-Thread中,邮箱控制块是操作系统用于管理邮箱的一个数据结构。
struct rt_mailbox
{
struct rt_ipc_object parent; /**< inherit from ipc_object 从IPC对象中继承*/
rt_uint32_t *msg_pool; /**< start address of message buffer 有效消息缓冲区的指针*/
rt_uint16_t size; /**< size of message pool 容量*/
rt_uint16_t entry; /**< index of messages in msg_pool 邮箱中邮件的数目*/
rt_uint16_t in_offset; /**< input offset of the message buffer */
rt_uint16_t out_offset; /**< output offset of the message buffer */
//邮箱的进出偏移量
rt_list_t suspend_sender_thread; /**< sender thread suspended on this mailbox */
//邮箱满的时候发邮件会导致线程挂起,这个成员用来记录挂起的线程的
};
typedef struct rt_mailbox *rt_mailbox_t;
struct rt_mailbox static_mb
rt_mailbox_t dynamic_mb
send可在中断中使用,因为他不会等待,send_wait不行。
result = rt_mb_init(&mb,
"mbt", /* 名称是mbt */
&mb_pool[0], /* 邮箱用到的内存池是mb_pool */
sizeof(mb_pool) / 4, /* 邮箱中的邮件数目,因为一封邮件占4字节 */
RT_IPC_FLAG_FIFO); /* 采用FIFO方式进行线程等待 */
消息队列是RT-Thread中另一种常用的线程间通信方式,消息队列是对邮箱的扩展。
消息队列能够接收来自线程或中断服务例程中发出的不固定长度的消息,并把消息缓存在自己的内存空间中,而其他线程能够从消息队列中读取相应的消息并进行对应的处理。
- 消息队列内都有内存池,内存池会根据一定的大小划分为消息框,消息队列的消息,都保存在内存框中,内存框以链表的形式组件起来的,
- 消息队列支持紧急消息的发送,当是紧急消息的时候,会直接链接到链表头,等待线程就可以第一时间获取到
- 当消息队列满的时候,发送消息失败。
在RT-Thread中,消息队列控制块是操作系统用于管理消息队列的一个数据结构。
struct rt_messagequeue
{
struct rt_ipc_object parent; /**< inherit from ipc_object ;从IPC对象中继承*/
void *msg_pool; /**< start address of message queue,指向一段内存空间,保存消息队列的各个消息 */
rt_uint16_t msg_size; /**< message size of each message 消息队列中每一个消息框,消息的大小*/
rt_uint16_t max_msgs; /**< max number of messages 消息队列的容量/长度,消息队列所能容纳的数目*/
rt_uint16_t entry; /**< index of messages in the queue 当消息队列有消息进来,+1,有线程读取,-1*/
void *msg_queue_head; /**< list head 消息头指针,指向第一条*/
void *msg_queue_tail; /**< list tail 消息尾指针,指向最后一条*/
void *msg_queue_free; /**< pointer indicated the free node of queue ,空闲指针指向未被使用的消息框*/
};
typedef struct rt_messagequeue *rt_mq_t;
#define RT_ALIGN_SIZE 4
定义了4字节对齐,假如我们定义为1,对齐为4,定义为5,对齐成8,size必须是4的整数倍。
指针的原因:消息队列以链表的形式存在,会有一个链表的指针,在32位系统下,一个指针4字节
struct rt_messagequeue static_mq
rt_mq_t dynamic_mq
一般消息send和紧急消息urgent:
紧急消息发送后,直接放到消息队列链表头部,接收线程可以直接获取
/*
* Copyright (c) 2006-2018, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2018-08-24 yangjie the first version
*/
/*
* 程序清单:消息队列例程
*
* 这个程序会创建2个动态线程,一个线程会从消息队列中收取消息;一个线程会定时给消
* 息队列发送 普通消息和紧急消息。
*/
#include
/* 消息队列控制块 */
static struct rt_messagequeue mq;
/* 消息队列中用到的放置消息的内存池 */
static rt_uint8_t msg_pool[2048];
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
/* 线程1入口函数 */
static void thread1_entry(void *parameter)
{
char buf = 0;
rt_uint8_t cnt = 0;
while (1)
{
/* 从消息队列中接收消息 */
if (rt_mq_recv(&mq, &buf, sizeof(buf), RT_WAITING_FOREVER) == RT_EOK)
{
rt_kprintf("thread1: recv msg from msg queue, the content:%c\n", buf);
if (cnt == 19)
{
break;
}
}
/* 延时50ms */
cnt++;
rt_thread_mdelay(50);
}
rt_kprintf("thread1: detach mq \n");
rt_mq_detach(&mq);
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程2入口 */
static void thread2_entry(void *parameter)
{
int result;
char buf = 'A';
rt_uint8_t cnt = 0;
while (1)
{
if (cnt == 8)
{
/* 发送紧急消息到消息队列中 */
result = rt_mq_urgent(&mq, &buf, 1);
if (result != RT_EOK)
{
rt_kprintf("rt_mq_urgent ERR\n");
}
else
{
rt_kprintf("thread2: send urgent message - %c\n", buf);
}
}
else if (cnt >= 20)/* 发送20次消息之后退出 */
{
rt_kprintf("message queue stop send, thread2 quit\n");
break;
}
else
{
/* 发送消息到消息队列中 */
result = rt_mq_send(&mq, &buf, 1);
if (result != RT_EOK)
{
rt_kprintf("rt_mq_send ERR\n");
}
rt_kprintf("thread2: send message - %c\n", buf);
}
buf++;
cnt++;
/* 延时5ms */
rt_thread_mdelay(5);
}
}
/* 消息队列示例的初始化 */
int msgq_sample(void)
{
rt_err_t result;
/* 初始化消息队列 */
result = rt_mq_init(&mq,
"mqt",
&msg_pool[0], /* 内存池指向msg_pool */
1, /* 每个消息的大小是 1 字节 */
sizeof(msg_pool), /* 内存池的大小是msg_pool的大小 */
RT_IPC_FLAG_FIFO); /* 如果有多个线程等待,按照先来先得到的方法分配消息 */
if (result != RT_EOK)
{
rt_kprintf("init message queue failed.\n");
return -1;
}
rt_thread_init(&thread1,
"thread1",
thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack), 25, 5);
rt_thread_startup(&thread1);
rt_thread_init(&thread2,
"thread2",
thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack), 25, 5);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(msgq_sample, msgq sample);
软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上(系统滴答定时器)。软件定时器使系统能够提供不受数目限制的软件定时器服务。
RT-Thread操作系统的软件定时器,以系统节拍(OS Tick)的时间长度为定时单位,提供了基于系统节拍整数倍的定时能力,即定时数值是OS Tick的整数倍。例如一个OS Tick是10ms,那么上层软件定时器只能提供10ms,20ms,100ms等事件精度的定时服务,而不是定时为15ms、20ms、35ms等。
当软件定时器所设定的定时事件到了后,会调用用户设置的定时器timeout回调函数,用户需要定时运行的程序会在回调函数中得到处理。
根据timeout所在的上下文环境不同,定时器分为两种模式,
rtconfig.h
]来决定是否启用该模式。当启用SOFTTIMER模式后,我们可以在定时器初始化时指定定时器工作在SOFTTIMER模式。在RT-Thread中,软件定时器控制块时操作系统用于管理软件定时器的一个数据结构
struct rt_timer
{
struct rt_object parent; /**< inherit from rt_object */
rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL]; /*定时器链表节点*/
void (*timeout_func)(void *parameter); /**超时函数的函数指针 < timeout function */
void *parameter; /**超时函数的入参< timeout function's parameter */
rt_tick_t init_tick; /**指定定时器超时时间< timer timeout tick */
rt_tick_t timeout_tick; /*超时时间到达时的系统时钟节拍计数*< timeout tick */
};
typedef struct rt_timer *rt_timer_t;
struct rt_timer staric_timer
rt_timer_t dynamic_timer
flag:
- RT_TIMER_FLAG_ONE_SHOT:指定超时函数运行的次数,定时一次就停止
- RT_TIMER_FLAG_PERIODIC:周期性调用定时函数
- RT_TIMER_FLAG_HARD_TIMER:HARD_TIMER模式
- RT_TIMER_FLAG_SOFT_TIMER:SOFT_TIMER模式
不指定默认HARD_TIMER模式
无内存创建时,函数返回0则失败
创建了两个定时器,一个周期,一个单次,超时函数里面打印并退出
/* 定时器1超时函数 */
static void timeout1(void *parameter)
{
rt_kprintf("periodic timer is timeout %d\n", cnt);
/* 运行第10次,停止周期定时器 */
if (cnt++ >= 9)
{
rt_timer_stop(timer1);
rt_kprintf("periodic timer was stopped! \n");
}
}
/* 定时器2超时函数 */
static void timeout2(void *parameter)
{
rt_kprintf("one shot timer is timeout\n");
}
int timer_sample(void)
{
/* 创建定时器1 周期定时器 */
timer1 = rt_timer_create("timer1", timeout1,
RT_NULL, 10,
RT_TIMER_FLAG_PERIODIC);
/* 启动定时器1 */
if (timer1 != RT_NULL) rt_timer_start(timer1);
/* 创建定时器2 单次定时器 */
timer2 = rt_timer_create("timer2", timeout2,
RT_NULL, 30,
RT_TIMER_FLAG_ONE_SHOT);
/* 启动定时器2 */
if (timer2 != RT_NULL) rt_timer_start(timer2);
return 0;
}
为了提高内存分配的效率,并且避免内存碎片,RT-Thread提供了另外一种内存管理方法:内存池(Memory Pool)
RT-Thread的内存池支持线程挂起功能,当内存池中无空闲内存块时,申请线程会被挂起,直到内存池中有新的可用内存块,再将挂起的线程唤醒。基于这个特点内存池非常适合需要通过内存资源进行同步的场景。
内存池在创建时先从系统中获取一大块内存(静态或动态),然后分成相同大小的多个小内存块,这些小内存块通过链表连接起来(此链表也称空闲链表)。线程每次申请分配内存块的时候,系统从空闲链表中取出链表上第一个内存块,提供给申请者。
我们在一个系统中,可以创建多个内存池,它在创建的时候,是从系统中获取一大块内存,获取到的内存可以是静态的形式,比如我们先定义一块数组,将其作为内存池的内存分配资源,也可以通过动态内存堆的方式,申请一个动态的地址当作内存资源。
将一个大的内存块分成大小相同的小的内存块,小内存块在内存池中是通过链表的方式链接起来的,线程可以向内存申请小内存块,取出链表头开始的第一个空闲内存块;当线程使用完内存块以后,将内存块释放,返回给内存池中
在RT-Thread中,内存池控制块是操作系统用于管理内存池的一个数据结构。
struct rt_mempool
{
struct rt_object parent; /**< inherit from rt_object ,继承于rt_object这个对象*/
void *start_address; /**< memory pool start ,申请内存空间的内存地址*/
rt_size_t size; /**< size of memory pool ,记录大内存块的大小*/
rt_size_t block_size; /**< size of memory blocks ,记录小内存块的大小*/
rt_uint8_t *block_list; /**< memory blocks list ,小内存块列表*/
rt_size_t block_total_count; /**< numbers of memory block ,整个内存池中一共有多少小内存块*/
rt_size_t block_free_count; /**< numbers of free memory block ,当前空闲内存块的个数 */
rt_list_t suspend_thread; /**< threads pended on this resource,由于内存池支持线程挂起,所以此参数用来记录挂起在内存池上的线程列表 */
rt_size_t suspend_thread_count; /**< numbers of thread pended on this resource ,挂起在内存池上的线程数目*/
};
typedef struct rt_mempool *rt_mp_t;
mp:内存控制块的地址。
name:内存池的名称
start:内存地址,可以是一个数组,这就是大内存块。
size:大内存块的大小
block_size:小内存块大小。
ps:消息队列中,消息的大小按系统定义的对齐格式(例如4字节对齐)对齐。如果block_size输入1,则系统自动定为4.
可以计算出初始化的内存池中,有多少内存块。计算公式为:cnt=size/(系统的block_size+4)
。 4是因为内存是以链表的方式链接起来的,所以每一个内存块多一个指针的大小
实现动态内存的时候,不需要提前定义一个内存块,她所需要的内存块,会通过动态的方式,从系统动态内存堆里面分配。如果系统当前有可用的内存,会返回内存控制块的指针,如果系统中没有可用的内存,那么会返回null空指针
- mp,指明从哪个系统中去申请内存块。如果申请不到,系统挂起
//线程1申请48次,线程2释放2次,
static rt_uint8_t *ptr[50];
static rt_uint8_t mempool[4096];
static struct rt_mempool mp;
/* 线程1入口 */
static void thread1_mp_alloc(void *parameter)
{
int i;
for (i = 0 ; i < 50 ; i++)
{
if (ptr[i] == RT_NULL)
{
/* 试图申请内存块50次,当申请不到内存块时,
线程1挂起,转至线程2运行 */
ptr[i] = rt_mp_alloc(&mp, RT_WAITING_FOREVER);
if (ptr[i] != RT_NULL)
rt_kprintf("allocate No.%d\n", i);
}
}
}
/* 线程2入口,线程2的优先级比线程1低,应该线程1先获得执行。*/
static void thread2_mp_release(void *parameter)
{
int i;
rt_kprintf("thread2 try to release block\n");
for (i = 0; i < 50 ; i++)
{
/* 释放所有分配成功的内存块 */
if (ptr[i] != RT_NULL)
{
rt_kprintf("release block %d\n", i);
rt_mp_free(ptr[i]);
ptr[i] = RT_NULL;
}
}
}
rt_mp_init(&mp, "mp1", &mempool[0], sizeof(mempool), 80); //4096/(80+4)=48个内存块