系列文章目录
RT-THREAD 内核快速入门(一)线程
RT-THREAD 内核快速入门(二)定时器
RT-THREAD 内核快速入门(三) 信号量,互斥量,事件
文章目录
前言
这是这是快速入门系列的第四篇,也就是编程篇里面最后的倒数第二篇,之后还有一个内存管理篇和中断篇篇,内核快速入门系列就介绍完毕了。
这篇是线程间的通信,也就是线程间的数据传递与前篇的同步稍微不一样。同步一般不涉及线程间的数据传递,而通信是设计传递的数据的,这也是在前篇的同步篇 ,里面的信号量的延申,理解好信号量的实现方式,将会很快理解通信方式。由于是有前篇的基础,这里会比较快的迅速介绍,仅对工作方式做简单的介绍。如果还未理解前面的同步篇,建议先看看。这篇重点会放在线程通信与线程同步,以及它们之间的联系与注意点。
如果是不了解系列的框架,可以看到这个链接的前言先去了解
系列框架在前言
一、邮箱
1.工作方式
邮箱的工作方式与信号量是挺类似的,区别是发送信号量一般不会考虑到信号量溢出。而邮箱是会满的,邮箱可以存放多少邮件取决于设定的内存池(邮筒的大小)大小。
理解:
可以用邮递员与收件员来解释邮箱的工作方式。
发邮件:
邮递员会根据邮箱设定的方式,邮递员在邮箱满的时候会根据是否选择超时来递送邮件。如果在一定时间内,邮箱还是满的(没收到邮箱有空位的通知),就会得到提示(返回RT_EFULL值)。邮件的存放顺序和现实中是一样的,越早到的邮件越放到后面,也就是最先到的邮件先放在其他下面,得到优先领取。邮递员还可以发送紧急邮件,把邮件放在所有邮件(已发送为取的邮件)的上面。
取邮件:
如果有多个收件员(线程)则会根据一定的方式排队(通常使用优先级进行排队)来取邮件,每排一次队取一次邮件。
2.特点
邮箱与同步通信这些不同的就是,邮箱有一个内存池的机制,决定邮箱能够存放多少邮件,每个邮件4个字节。4个字节也就是32为MCU指针的大小(固定的),刚好容纳一个指针的内容,传入指针就得到需要访问内容的地址,就可以通过邮件访问内容地址操作内存。其他的,例如线程取邮件的排队方式,发送邮件激活线程和发送信号量激活的方式一样(不同的就是邮箱会满)。
3.例程
这里通过发送指针的方式,发送结构体指针(指针大小与类型无关,操作系统以及MCU的位数和编译器位数有关),这里32位,刚好4个字节
/*
* 程序清单:邮箱例程
*
* 创建两个动态邮箱,一个发送一个接收
* 这里通过发送指针的方式,发送结构体指针(指针大小与类型无关,操作系统以及MCU的位数和编译器位数有关),这里32位,刚好4个字节
*
*/
#include <rtthread.h>
#define THREAD_PRIORITY 10
#define THREAD_TIMESLICE 5
/* 邮箱控制块 */
static struct rt_mailbox mb;
/* 用于放邮件的内存池 */
static char mb_pool[128];
static int mb_data=0;
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
//邮箱件定义结构体
struct msg
{
rt_uint8_t *data_ptr;
rt_uint32_t data_size;
};
/* 线程1入口 */
static void thread1_entry(void *parameter)
{
struct msg* msg_ptr;
int *a;
while (1)
{
if (rt_mb_recv(&mb, (rt_uint32_t*)&msg_ptr,RT_WAITING_FOREVER) == RT_EOK)
{
rt_kprintf("thread1: sent a mail from mailbox, the adree:%d,const:%d,size:%d\n", msg_ptr->data_ptr,*msg_ptr->data_ptr,msg_ptr->data_size);
/* 在接收线程处理完毕后,需要释放相应的内存块 */
rt_free(msg_ptr);//释放的内存
}
if (rt_mb_recv(&mb, (rt_uint32_t*)&a,RT_WAITING_FOREVER) == RT_EOK)
{
rt_kprintf("thread1: sent a mail from mailbox, the adree:%d,const:%d\n", &a,a);
}
}
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程2入口 */
static void thread2_entry(void *parameter)
{
struct msg* msg_ptr;
int a=0;
while (1)
{
mb_data++;
msg_ptr = (struct msg*)rt_malloc(sizeof(struct msg));
msg_ptr->data_ptr = (rt_uint8_t *)&mb_data; /* 指向相应的数据块地址 */
msg_ptr->data_size =sizeof(mb_data); /* 数据块的长度 */
rt_kprintf("thread2: sent a mail from mailbox, the adree:%d,const:%d,size:%d\n", msg_ptr->data_ptr,*msg_ptr->data_ptr,msg_ptr->data_size);
rt_kprintf("thread2: sent a mail from mailbox, the adree:%d,const:%d\n",&a,a);
/* 发送这个消息指针给 mb 邮箱 */
rt_mb_send(&mb, (rt_uint32_t)msg_ptr);
/* 发送这个消息指针给 mb 邮箱 */
rt_mb_send(&mb, (rt_uint32_t)&a);
rt_thread_mdelay(500);
}
}
int mailbox_sample(void)
{
rt_err_t result;
/* 初始化一个mailbox */
result = rt_mb_init(&mb,
"mbt", /* 名称是mbt */
&mb_pool[0], /* 邮箱用到的内存池是mb_pool */
sizeof(mb_pool) / 4, /* 邮箱中的邮件数目,因为一封邮件占4字节 */
RT_IPC_FLAG_FIFO); /* 采用FIFO方式进行线程等待 */
if (result != RT_EOK)
{
rt_kprintf("init mailbox failed.\n");
return -1;
}
rt_thread_init(&thread1,
"thread1",
thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread1);
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;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(mailbox_sample, mailbox sample);
运行结果:
例程分析:
运行方式与信号量差不多,区别是,发送邮件是将内存里面的地址给发送出去。得到地址,就可以通过邮箱的对象里面的指针访问了,邮箱收取邮件是通过直接复制的方式,将邮件的内容复制到数据块中的。
注意点:
- 如果使用了malloc的方式申请了内存,为了防止内存泄露,记得释放申请的内存。使用局部变量的方式,是不够稳定的,虽然也可以,局部变量的内存是随机分配的,容易被覆盖,一旦被覆盖,内容就就丢失了。发送邮件指针/内容,建议使用malloc。
- 在中断里面不能使用malloc,因为容易导致内存泄漏,如果想要发送内容,又想要在中断里面使用类似邮箱这样的功能可以使用队列。
二、消息队列
1.工作方式
队列就是加强版的邮箱,可以容纳不定长字节的内容。相当于邮件的内容是不定长的,不只是限于4个字节(虽然使用指针的方式也可以实现相同的事情)。
2.联系
区别:
区别大概有两点,其他的使用方式和邮件是一样的,就不做讨论了。
- 消息队列的内容是不定长的。邮件是定长的,4字节,消息队列也可也设置为4字节,作为邮箱来使用,发送时发送的消息发送指针就可以了。
- 消息队列发送的是内容。是直接对发送内容进行复制的,而
邮箱是发送的是本身的内容,并不对内容进行复制,内容地址一旦改变就容易丢失(特别是发送地址的情况)。
3.例程
发送4字节内容的消息队列,方便与邮件做对比。
/*
* 程序清单:消息队列例程
* 一个发送消息,一个接收消息,发送内容
*/
#include <rtthread.h>
/* 消息队列控制块 */
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)
{
int *recv;
while (1)
{
/* 从消息队列中接收消息到 msg_ptr 中 */
if (rt_mq_recv(&mq, &recv, sizeof(recv), RT_WAITING_FOREVER) == RT_EOK)
{
/* 成功接收到消息,进行相应的数据处理 */
rt_kprintf("thread: recv msg from msg queue,the constest :%d the adree:%d size =%d\n", &recv,recv,4);
}
}
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)
{
static int cnt;
while (1)
{
cnt++;
rt_kprintf("thread: sent msg from msg queue,the constest :%d the adree:%d size =%d\n", &cnt,cnt,4);
/* 发送这个消息指针给 mq 消息队列 */
rt_mq_send (&mq, &cnt, sizeof(cnt));
rt_thread_mdelay(500);
}
}
/* 消息队列示例的初始化 */
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), 24, 5);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(msgq_sample, msgq sample);
运行结果:
简单分析:
发送的地址和接受地址是不一样的,也就是取出消息的时候是从消息池中取出。而在发送的时候,将信息复制到发送的消息池当中。通过对比,而邮件是直接发送原来邮件的内容(地址不变),消息队列确实是复制原来的内容的方式。这样就可以解决在邮箱不能在中断里面发送邮件容易出现内存泄漏的问题(中断里面不能使用malloc,除非局部变量用static限制,局部变量不限定变量容易被修改)
三、信号
工作方式
信号就是软中断,可以给线程安装一个信号,其他线程或者中断就可以通过发送信号,来激活线程使其相应信号(软中断)。如果当前线程正在运行,线程本身发送中断信号,便会开辟新的栈区取响应软中断。
联系
软中断与中断十分类似,都是不知道信号什么时候来,来了就执行中断。但是这个软中断只能打断比设定优先级低的线程,系统默认优先级为10级,线程比这个安装中断优先级高的线程就不能被打断。如果本身线程安装软中断优先级比软中断的优先级要高,就只能等到本身安装信号的线程挂起,软中断才能得到执行。可以理解为软中断也是一个线程,优先级为10级的线程,这个软中断和线程又有点不一样,软中断的变量并不会保存到栈里面,也就是类似中断的执行方式。
使用软中断需要注意的是:安装了信号的线程栈需要加大。
例程
/*
* 程序清单:信号例程
*
* 这个例子会创建一个线程,线程安装信号,然后给这个线程发送信号。
*
*/
#include <rtthread.h>
#define THREAD_PRIORITY 24
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
static rt_thread_t tid1 = RT_NULL;
/* 线程1的信号处理函数 */
void thread1_signal_handler(int sig)
{
rt_kprintf("thread1_signal_handler %d\n", sig);
rt_kprintf("thread1_signal_handler thread1 tid2->current_priority %d\n", tid1->current_priority);
}
/* 线程1的入口函数 */
static void thread1_entry(void *parameter)
{
int cnt = 0;
/* 安装信号 */
rt_signal_install(SIGUSR1, thread1_signal_handler);
rt_signal_unmask(SIGUSR1);
while (1)
{
/* 线程1采用低优先级运行,一直打印计数值 */
rt_kprintf("thread1 count : %d\n", cnt);
rt_kprintf("thread1 tid1->current_priority %d\n", tid1->current_priority);
cnt++;
rt_thread_mdelay(100);
}
}
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程2入口 */
static void thread2_entry(void *param)
{
static rt_uint32_t count = 0;
/* 发送信号 SIGUSR1 给线程1 */
rt_thread_kill(tid1, SIGUSR1);
while(1)
{
count++;
if(count<=5)
{
rt_kprintf("thread2 is ruing count is: %d\n", count);
}
if(count > 5 ) break;
}
rt_kprintf("thread2 exit\n");
/* 线程2运行结束后也将自动被系统删除
(线程控制块和线程栈依然在idle线程中释放) */
}
/* 信号示例的初始化 */
int signal_sample(void)
{
/* 创建线程1 */
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),
9, 10);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(signal_sample, signal sample);
运行结果:
五、联系
学习完基本的线程通信与同步,就可以构建它们之间的联系,找出共性与特性,方便理解和运用。
线程间通信的联系
最明显的联系就是邮件与消息队列之间的联系了。
相同点:
宏观上的工作方式是一样的,也就意味着,消息队列就是不定长的邮件。
不同点:
消息队列存放消息是复制内容的方式,而邮箱是直接发送相应的内容,并不会进行复制。使用消息队列不用考虑内容泄漏,但是相应的,消息队列开销比邮箱大,效率不如邮箱高。
其次就是软中断与中断的关系了。
相同点:
作用相同,都是中断。用来应对不确定性事件。
不同点:
软中断是特殊的线程,按照线程的实现方式进行调度。中断就是按照不同芯片的中断方式进行实现的,不参与调度,但会有中断优先级的这些分类来对中断进行管理。
线程间通信与同步的联系
又是这一副图,可见真的很重要,一定要记住和好好理解的。线程的5个状态,要时刻记在脑中。
相同点:
都可以用来进行同步。只要忽略掉通信的内容,只是用来接受信号,完全可以用作线程间同步,不过比较浪费资源,也不能实现互斥量这种防止优先级翻转的问题。比如消费者生产者问题,生产者直接生产产品,然后将产品通过消息队列发送产品出去,解决了临界区访问的问题,不再需要锁。他们们可以公用的方式原因是它们之间激活线程与线程的实现挂起的都是差不多的,线程先排队的方式也是设置的两种方式,因此可以替代(看上面的图,线程的五种方式)。某种情况下,定时器也可以用线程替代,可以通过高优先级线程挂起时间来进行一定的定时。
不同点:
通信,包括通信的内容,做到通信与同步一体的功能。其实用信号量也是可以实现这样的功能的,设置全局结构体作为通信数据块即可,当操作完这块数据块,就可以发送信号来同步线程。通信实现这样的功能就不需要额外构造全局结构,直接发送相应的数据块或者指针就可以了,避免全局变量过多带来的负面影响。
总结
主要的RTOS编程的方式已经介绍完,剩下的就是内存管理和中断管理的一点点编程方式。内存管理就是malloc和free分配堆的编程思路,中断就是中断锁(临界保护的一种方式)和中断的上下半的处理方式,都很好理解,下一篇将会介绍这两种种编程方式以及编程注意事项,内核快速入门篇就结束了。