rt-thread学习

线程的状态

  • 初始态(RT_THREAD_INIT):创建线程时的状态
  • 就绪态(RT_THREAD_READY):线程已经准备执行,只等待CPU调用
  • 运行态(RT_THREAD_RUNNING):线程正在执行,此时在独占CPU
  • 挂起态/阻塞态(RT_THREAD_SUSPEND):正在等待某个时序或者中断。包含线程被挂起,线程被延时,线程在等待信号量,读写队列,等待读写事件
  • 关闭态(RT_THREAD_CLOSE):线程运行结束,等待系统回收资源

线程常用函数

线程挂起函数rt_thread_suspend()

挂起指定线程。被挂起的线程绝不会得到处理器的使用权,不管该线程具有什么优先级。

线程挂起可以由多种方法实现:线程调用rt_thread_delay()rt_thread_suspend()等函数接口可以使得线程主动挂起,放弃CPU使用权,当线程调用rt_sem_take()rt_mb_recv()等函数时,资源不可使用也会导致调用线程被动挂起。

当线程已经是挂起态的时候无法调用rt_thread_suspend()函数,已经是挂起态的线程调用rt_thread_suspend()将返回错误代码,挂起的线程想要恢复可以调用rt_thread_resume()函数

注:通常不应该使用这个函数来挂起线程本身,如果确实需要采用rt_thread_suspend函数挂起当前线程,需要在调用rt_thread_suspend()函数后立刻调用rt_schedule()函数进行手动的线程上下文切换。

rt_kprintf("挂起LED1线程!\n");
uwRet = rt_thread_suspend(led1_thread);/* 挂起LED1线程 */
if (RT_EOK == uwRet)
{
rt_kprintf("挂起LED1线程成功!\n");
} else
{
rt_kprintf("挂起LED1线程失败!失败代码:0x%lx\n",uwRet);
}

注意:由于RT-Thread中挂起线程函数不允许将已经在阻塞态的线程进行操作,所以,在挂起的时候可能会挂起失败。我们一般调用挂起函数是在 线程就绪或者运行的时候将其挂起,而不是在挂起态再将线程挂起

线程恢复函数rt_thread_resume()

线程恢复就是让挂起的线程重新进入就绪状态,恢复的线程会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。如果被恢复线程在所有就绪态线程中,位于最高优先级链表的第一位,那么系统将进行线程上下文的切换。

rt_kprintf("恢复LED1线程!\n");
uwRet = rt_thread_resume(led1_thread);/* 恢复LED1线程! */
if (RT_EOK == uwRet)
{
rt_kprintf("恢复LED1线程成功!\n");
}
else
{
rt_kprintf("恢复LED1线程失败!失败代码:0x%lx\n",uwRet);
}

线程设计要点

作为一个嵌入式开发人员,要对自己设计的嵌入式系统要了如指掌,线程的优先级信息,线程与中断的处理,线程的运行时间、逻辑、状态 等都要知道,才能设计出好的系统,所以,在设计的时候需要根据需求制定框架。在设计之初就应该考虑下面几点因素:线程运行的上下文 环境、线程的执行时间合理设计。

RT-Thread中程序运行的上下文包括:

  • 中断服务函数。
  • 普通线程。
  • 空闲线程。
  1. 中断服务函数:

中断服务函数是一种需要特别注意的上下文环境,它运行在非线程的执行环境下(一般为芯片的一种特殊运行模式(也被称作特权模式)),在 这个上下文环境中不能使用挂起当前线程的操作,不允许调用任何会阻塞运行的API函数接口。另外需要注意的是,中断服务程序最好保持精简短 小,快进快出,一般在中断服务函数中只做标记事件的发生,让对应线程去执行相关处理,因为中断服务函数的优先级高于任何优先级的线程,如 果中断处理时间过长,将会导致整个系统的线程无法正常运行。所以在设计的时候必须考虑中断的频率、中断的处理时间等重要因素,以便配合 对应中断处理线程的工作。

  1. 线程:

线程看似没有什么限制程序执行的因素,似乎所有的操作都可以执行。但是做为一个优先级明确的实时系统,如果一个线程中的程序出现了死循 环操作(此处的死循环是指没有不带阻塞机制的线程循环体),那么比这个线程优先级低的线程都将无法执行,当然也包括了空闲线程,因为死 循环的时候,线程不会主动让出CPU,低优先级的线程是不可能得到CPU的使用权的,而高优先级的线程就可以抢占CPU。这个情况在实时操作系 统中是必须注意的一点,所以在线程中不允许出现死循环。如果一个线程只有就绪态而无阻塞态,势必会影响到其他低优先级线程的执行,所以 在进行线程设计时,就应该保证线程在不活跃的时候,线程可以进入阻塞态以交出CPU使用权,这就需要我们自己明确知道什么情况下让线程进 入阻塞态,保证低优先级线程可以正常运行。在实际设计中,一般会将紧急的处理事件的线程优先级设置得高一些。

  1. 空闲线程:

空闲线程(idle线程)是RT-Thread系统中没有其他工作进行时自动进入的系统线程。用户可以通过空闲线程钩子方式,在空闲线程上钩入自己的 功能函数。通常这个空闲线程钩子能够完成一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。除了空闲线程钩子,RT-Thread系 统还把空闲线程用于一些其他的功能,比如当系统删除一个线程或一个动态线程运行结束时,会先行更改线程状态为非调度状态,然后挂入一个待 回收队列中,真正的系统资源回收工作在空闲线程完成,空闲线程是唯一不允许出现阻塞情况的线程,因为RT-Thread需要保证系统用于都有一个 可运行的线程。

对于空闲线程钩子上挂接的空闲钩子函数,它应该满足以下的条件:

  • 不会挂起空闲线程;

  • 不应该陷入死循环,需要留出部分时间用于系统处理系统资源回收。

线程的执行时间:

线程的执行时间一般是指两个方面,一是线程从开始到结束的时间,二是线程的周期。

在系统设计的时候这两个时间候我们都需要考虑,例如,对于事件A对应的服务线程Ta,系统要求的实时响应指标是10ms,而Ta的最大运行时间 是1ms,那么10ms就是线程Ta的周期了,1ms则是线程的运行时间,简单来说线程Ta在10ms内完成对事件A的响应即可。此时,系统中还存在着 以50ms为周期的另一线程Tb,它每次运行的最大时间长度是100us。在这种情况下,即使把线程Tb的优先级抬到比Ta更高的位置,对系统的实 时性指标也没什么影响,因为即使在Ta的运行过程中,Tb抢占了Ta的资源,等到Tb执行完毕,消耗的时间也只不过是100us,还是在事件A规定 的响应时间内(10ms),Ta能够安全完成对事件A的响应。但是假如系统中还存在线程Tc,其运行时间为20ms,假如将Tc的优先级设置比Ta更高, 那么在Ta运行的时候,突然间被Tc打断,等到Tc执行完毕,那Ta已经错过对事件A(10ms)的响应了,这是不允许的。所以在我们设计的时候, 必须考虑线程的时间,一般来说处理时间更短的线程优先级应设置更高一些。

消息队列

基本概念

队列可以在线程与线程间、中断和线程间传送信息,实现了线程接收来自其他线程或中断的不固定长度的消息,并根据不同的接口选择传递消息是否存放在线程自己的空间。线程能够从队列里面读取消息,当队列中的消息是空时,挂起读取线程,用户还可以指定挂起的线程时间timeout;当队列中有新消息时,挂起的读取线程被唤醒并处理新消息,消息队列是一种异步的通信方式

线程先得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。同时RT- Thread中的消息队列支持优先级,也就是说在所有等待消息的线程中优先级最高的会先获得消息。

用户在处理业务时,消息队列提供了异步处理机制,允许将一个消息放入队列,但并不立即处理它,同时队列还能起到缓冲消息作用。

RT-Thread中使用队列数据结构实现线程异步通信工作,具有如下特性:

  • 消息支持先进先出方式排队与优先级排队方式,支持异步读写工作方式。
  • 读队列支持超时机制。
  • 支持发送紧急消息,这里的紧急消息是往队列头发送消息。
  • 可以允许不同长度(不超过队列节点最大值)的任意类型消息。
  • 一个线程能够从任意一个消息队列接收和发送消息。
  • 多个线程能够从同一个消息队列接收和发送消息。
  • 当队列使用结束后,需要通过删除队列操作释放内存函数回收。

运作机制

RT-Thread操作系统的消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息 队列控制块中的msg_queue_head和msg_queue_tail;有些消息框可能是空的,它们通过msg_queue_free形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。

线程或者中断服务程序都可以给消息队列发送消息。当发送消息时,消息队列对象先从空闲消息链表上取下一个空闲消息块,把线程或者中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部。当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能成功发送消息;当空闲消息链表上无可用消息块,说明消 息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL)。

发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,从空闲消息链表上取下来的消息块不是挂到消息队列的队尾,而是挂到队首,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。

读取消息时,根据msg_queue_head找到最先入队列中的消息节点进行读取。根据消息队列控制块中的entry判断队列是否有消息读取,对全部空闲(entry为0)队列进行读消息操作会引起线程挂起。

当消息队列不再被使用时,应该删除它以释放系统资源,一旦操作完成,消息队列将被永久性的删除。

消息队列的阻塞机制

我们创建的队列,是每个线程都可以去对他进行读写操作的,但是为了保护每个线程对它进行读写操作的过程,我们必须要有阻塞机制,在某个线程对它读写操作的时候,必须保证该线程能正常完成读写操作,而不受后来的线程干扰,凡事都有先来后到嘛!

假设有一个线程A对某个队列进行读操作的时候(也就是我们所说的出队),发现它没有消息,那么此时线程A有3个选择:第一个选择,线程A扭头就走, 既然队列没有消息,那我也不等了,干其它事情去,这样子线程A不会进入阻塞态;第二个选择,线程A还是在这里等等吧,可能过一会队列就有消息,此时线程A会进入阻塞状态,在等待着消息的道来,而线程A的等待时间就由我们自己定义,比如设置1000个tick的等待,在这1000个tick到来之前线程A都是处于阻塞态 ,当阻塞的这段时间线程A等到了队列的消息,那么线程A就会从阻塞态变成就绪态,如果此时线程A比当前运行的线程优先级还高,那么,线程A就会得到消息并且运行;假如1000个tick都过去了,队列还没消息,那线程A就不等了,从阻塞态中唤醒,返回一个没等到消息的错误代码,然后继续执行线程A的其他代码;第三个选 择,线程A死等,不等到消息就不走了,这样子线程A就会进入阻塞态,直到完成读取队列的消息。

而在发送消息操作的时候,为了保护数据,当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能成功发送消息;当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL),发送消息并不带有阻塞机制的,因为发送消息的环境可能是在中断中,不允许有阻塞的情况。

消息队列的应用场景

消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及在中断服务函数中给线程发送消息(中断服务例程不能接收消息)。

消息队列代码块

struct rt_messagequeue {
struct rt_ipc_object parent; /*消息队列属于内核对象,会在自身结构体里面包含一个内核对象类型的成员,通过这个成员可以将消息队列挂到系统对象容器里面。*/
void *msg_pool; /*存放消息的消息池开始地址。*/
rt_uint16_t msg_size; /*每条消息大小,消息队列中也就是节点的大小,单位为字节*/
rt_uint16_t max_msgs; /*能够容纳的最大消息数量。*/
rt_uint16_t entry; /*队列中的消息索引,记录消息队列的消息个数。*/
void *msg_queue_head; /*链表头指针,指向即将读取数据的节点。*/
void *msg_queue_tail; /*链表尾指针,指向允许写入数据的节点*/
void *msg_queue_free; /*指向队列的空闲节点的指针*/
};
typedef struct rt_messagequeue *rt_mq_t;

消息队列创建函数rt_mq_create()

消息队列创建函数,顾名思义,就是创建一个队列,与线程一样,都是需要先创建才能使用的东西,RT-Thread肯定不知道我们需要什么样的 队列,所以,我们需要怎么样的队列我们就自己创建就行了,比如队列的长度,队列句柄,节点的大小这些信息都是我们自己定义的

/* 创建一个消息队列 */
test_mq = rt_mq_create("test_mq", /* 消息队列名字 */
40, /* 消息的最大长度 */
20, /* 消息队列的最大容量 */
RT_IPC_FLAG_FIFO);/* 队列模式 FIFO(0x00)*/
if (test_mq != RT_NULL)
rt_kprintf("消息队列创建成功!\n\n");

消息队列删除函数rt_mq_delete()

队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空,而且不能再次使用这个消息队 列了,但是需要注意的是,如果某个消息队列没有被创建,那也是无法被删除的。删除消息队列的时候会把所有由于访问此消息队列而进入阻塞态的线程都从阻塞链表中删除,mq是rt_mq_delete传入的参 数,是消息队列句

/* 定义消息队列控制块 */
static rt_mq_t test_mq = RT_NULL;
rt_err_t uwRet = RT_EOK;
uwRet = rt_mq_delete(test_mq);
if (RT_EOK == uwRet)
rt_kprintf("消息队列删除成功!\n\n");

消息队列发送消息函数rt_mq_send()

线程或者中断服务程序都可以给消息队列发送消息。当发送消息时,消息队列对象先从空闲消息链表上取下一个空闲消息块,把线程或者 中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部。当且仅当空闲消息链表上有可用的空闲消息块时, 发送者才能成功发送消息;当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL)

static void send_thread_entry(void* parameter)
{
rt_err_t uwRet = RT_EOK;
uint32_t send_data1 = 1;
uint32_t send_data2 = 2;
while (1) {/* K1 被按下 */
if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
/* 将数据写入(发送)到队列中,等待时间为 0 */
uwRet = rt_mq_send(test_mq, /* 写入(发送)队列的ID(句柄) */
&send_data1, /* 写入(发送)的数据 */
sizeof(send_data1)); /* 数据的长度 */
if (RT_EOK != uwRet) {
rt_kprintf("数据不能发送到消息队列!错误代码: %lx\n",uwRet);
}
}/* K1 被按下 */
if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
/* 将数据写入(发送)到队列中,等待时间为 0 */
uwRet = rt_mq_send(test_mq, /* 写入(发送)队列的ID(句柄) */
&send_data2, /* 写入(发送)的数据 */
sizeof(send_data2)); /* 数据的长度 */
if (RT_EOK != uwRet) {
rt_kprintf("数据不能发送到消息队列!错误代码: %lx\n",uwRet);
}
}
rt_thread_delay(20);
}
}

消息队列接收消息函数rt_mq_recv()

当消息队列中有消息时,接收线程才能接收到消息,接收消息是有阻塞机制的,用户可以自定义等待时间, RT-Thread的接收消息过程是:接收一个消息后消息队列的头链表消息被转移到了空闲消息链表中

/* 队列读取(接收),等待时间为一直等待 */
uwRet = rt_mq_recv(test_mq, /* 读取(接收)队列的ID(句柄) */
&r_queue, /* 读取(接收)的数据保存位置 */
sizeof(r_queue), /* 读取(接收)的数据的长度 */
RT_WAITING_FOREVER); /* 等待时间:一直等 */
if (RT_EOK == uwRet)
{
rt_kprintf("本次接收到的数据是:%d\n",r_queue);
} else
{
rt_kprintf("数据接收出错,错误代码: 0x%lx\n",uwRet);
}

消息队列的注意事项

  1. 使用rt_mq_recv()、rt_mq_send()、rt_mq_delete()等这些函数之前应先创建需消息队列,并根据队列句柄进行操作。

  2. 队列读取采用的是先进先出(FIFO)模式,会首先读取出首先存储在队列中的数据。当然也有例外, RT-Thread给我们提供了另一个函数,可以发送紧急消息的,那么读取的时候就会读取到紧急消息的数据。

  3. 必须要我们定义一个存储读取出来的数据的地方,并且把存储数据的起始地址传递给 rt_mq_recv()函数,否则,将发生地址非法的错误。

  4. 接收消息队列中的消息是拷贝的方式,读取消息时候定义的地址必须保证能存放下即将读取消息的大小

信号量

基本概念

信号量(Semaphore)是一种实现线程间通信的机制,实现线程之间同步或临界资源的互斥访问,常用于协助一组 相互竞争的线程来访问临界资源。在多线程系统中,各线程之间需要同步或互斥实现临界资源的保护,信号量功能 可以为用户提供这方面的支持。

优先级翻转:低优先级的任务比高优先级的任务更快的执行。

通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况:

  • 0:表示没有积累下来的release释放信号量操作,且有可能有在此信号量上阻塞的线程。
  • 正值,表示有一个或多个release释放信号量操作。

以同步为目的的信号量和以互斥为目的的信号量在使用有如下不同:

  • 用作互斥时,信号量创建后可用信号量个数应该是满的, 线程在需要使用临界资源时,先获取信号量,使其变空,这样其他线程需要使用临界资源时就会因为无法获取信 号量而进入阻塞,从而保证了临界资源的安全。但是这样子有一个缺点就是有可能产生优先级翻转,优先级翻转 的危害具体会在互斥量章节中详细讲解。

  • 用作同步时,信号量在创建后被置为空,线程1取信号量而阻塞,线程2在某种条件发生后,释放信号量,于是线 程1得以进入就绪态,如果线程1的优先级是最高的,那么就会立即切换线程,从而达到了两个线程间的同步。 同样的,在中断服务函数中释放信号量,也能达到线程与中断间的同步。

在操作系统中,我们使用信号量的目的是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。这样, 当一个线程在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再做处理, 从而使得临界资源得到有效的保护。

信号量还有计数型信号量,计数型信号量允许多个线程对其进行操作,但限制了线程的数量。比如有一个停车场,里面 只有100个车位,那么能停的车只有100辆,也相当于我们的信号量有100个,假如一开始停车场的车位还有100个,那么 每进去一辆车就要消耗一个停车位,车位的数量就要减一,对应的,我们的信号量在使用之后也需要减一,当停车场停 满了100辆车的时候,此时的停车位为0,再来的车就不能停进去了,否则将造成事故,也相当于我们的信号量为0,后 面的线程对这个停车场资源的访问也无法进行,当有车从停车场离开的时候,车位又空余出来了,那么,后面的车就能 停进去了,在我们信号量的操作也是一样的,当我们释放了这个资源,后面的线程才能对这个资源进行访问。

二值信号量应用场景

在嵌入式操作系统中二值信号量是线程间、线程与中断间同步的重要手段。为什么叫二值信号量呢?因为信号量资 源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1,把这种只有 0和 1 两种情况的信号量称之为二值信号量。

在线程系统中,我们经常会使用这个二值信号量,比如,某个线程需要等待一个标记,那么线程可以在轮询中查询这个 标记有没有被置位,这样子做,就会很消耗CPU资源,其实根本不需要在轮询中查询这个标记,只需要使用二值信号量即 可,当二值信号量没有的时候,线程进入阻塞态等待二值信号量到来即可,当得到了这个信号量(标记)之后,在进行 线程的处理即可,这样子么就不会消耗太多资源了,而且实时响应也是最快的。

再比如某个线程使用信号量在等中断的标记的发生,在这之前线程已经进入了阻塞态,在等待着中断的发生,当在中断发 生之后,释放一个信号量,也就是我们常说的标记,当它退出中断之后,操作系统进行线程的调度,如果这个线程能够 运行,系统就会把等待这个线程运行起来,这样子就大大提高了我们的效率。

二值信号量在线程与线程中同步的应用场景:假设我们有一个温湿度的传感器,假设是1s采集一次数据,那么我们让他在 液晶屏中显示数据出来,这个周期也是要1s一次的,如果液晶屏刷新的周期是100ms更新一次,那么此时的温湿度的数据 还没更新,液晶屏根本无需刷新,只需要在1s后温湿度数据更新的时候刷新即可,否则CPU就是白白做了多次的无效数据 更新,CPU的资源就被刷新数据这个线程占用了大半,造成CPU资源浪费,如果液晶屏刷新的周期是10s更新一次,那么温 湿度的数据都变化了10次,液晶屏才来更新数据,那拿这个产品有啥用,根本就是不准确的,所以,还是需要同步协调工 作,在温湿度采集完毕之后,进行液晶屏数据的刷新,这样子,才是最准确的,并且不会浪费CPU的资源。

同理,二值信号量在线程与中断同步的应用场景:我们在串口接收中,我们不知道啥时候有数据发送过来,有一个线程是 做接收这些数据处理,总不能在线程中每时每刻都在线程查询有没有数据到来,那样会浪费CPU资源,所以在这种情况下 使用二值信号量是很好的办法,当没有数据到来的时候,线程就进入阻塞态,不参与线程的调度,等到数据到来了,释放 一个二值信号量,线程就立即从阻塞态中解除,进入就绪态,然后运行的时候处理数据,这样子系统的资源就会很好的被利用起来。

二值信号量运作机制

创建二值信号量,为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数, 二值信号量的最大可用信号量个数为1。

信号量获取,从创建的信号量资源中获取一个信号量,获取成功返回正确。否则线程会等待其它线程释放该信号量,超时时 间由用户设定。当线程获取信号量失败时,线程将进入阻塞态,系统将线程挂到该信号量的阻塞列表中。

二值信号量无效的时候,假如此时有线程获取该信号量的话,那么线程将进入阻塞状态。

假如某个时间中断/线程释放了信号量,那么,由于获取无效信号量进入阻塞态的线程 将获得信号量并且恢复为就绪态

计数型信号量的运作机制

计数型信号量与二值信号量其实都是差不多的,一样用于资源保护,不过计数信号量则允许多个线程获取信号量访问 共享资源,但会限制线程的最大数目。访问的线程数达到信号量可支持的最大数目时,会阻塞其他试图获取该信号量 的线程,直到有线程释放了信号量。这就是计数型信号量的运作机制,虽然计数信号量允许多个线程访问同一 个资源,但是也有限定,比如某个资源限定只能有3个线程访问,那么第4个线程访问的时候,会因为获取不到信号 量而进入阻塞,等到有线程(比如线程1)释放掉该资源的时候,第4个线程才能获取到信号量从而进行资源的访问。

信号量函数注意

在RT-Thread中,无论是二值信号量还是计数信号量,都是由我们自己创建的,二值信号量的最大计数值为1,并且都是使用RT-Thread的同一个释放与获取函数,所以在将信号量当二值信号量使用的时候要注意:用完信号量及时释放,并且不 要调用多次信号量释放函数。

信号量创建函数rt_sem_create()

二值信号量的创建很简单,因为创建的是二值的信号量,所以该信号量的容量只有一个,其可用信号量个数要么是0, 要么是1,而计数信号量则可以由用户决定在创建的时候初始化多少个可用信号量。

在创建信号量的时候,我们只需要传入我们的信号量名称、初始化的值和阻塞唤醒发生即 可。在创建信号量的时候,是需要用户自己定义信号量的句柄的,但是注意了,定义了信号量的句柄并不等于创建了信号量,创建信号量必须是调用rt_sem_create()函数进行创建,需要注意的是:二值信号量可用个数的取值范围是0~1,计 数信号量可用个数的取值范围是0~65535,用户可以根据需求选择。

/* 定义信号量控制块 */
static rt_sem_t test_sem = RT_NULL;
/* 创建一个信号量 */
test_sem = rt_sem_create("test_sem",/* 信号量名字 */
1, /* 信号量初始值,默认有一个信号量 */
RT_IPC_FLAG_FIFO); /* 信号量模式 FIFO(0x00)*/
if (test_sem != RT_NULL)
rt_kprintf("信号量创建成功!\n\n");

信号量删除函数rt_sem_delete()

信号量删除函数是根据信号量句柄直接删除的,删除之后这个信号量的所有信息都会被系统回收,并且用户无法再次使用这个信号量。但是需要注意的是,如果某个信号量没有被创建,那是无法被删除的。删除信号量的时候会把所有由于访问此信号量而阻塞的线程从阻塞链表中删除,并且返回一个 错误代码。 sem是rt_sem_delete()传入的参数,是信号量句柄,表示的是要删除哪个信号量。

调用这个函数时,系统将删除这个信号量。如果删除该信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是-RT_ERROR)。

/* 定义信号量控制块 */
static rt_sem_t test_sem = RT_NULL;
rt_err_t uwRet = RT_EOK;
uwRet = rt_sem_delete(test_sem);
if (RT_EOK == uwRet)
rt_kprintf("信号量删除成功!\n\n");

信号量释放函数rt_sem_release()

当信号量有效的时候,线程才能获取信号量,那么,是什么函数使得信号量变得 有效?其实有两个方式,一个是在创建的时候进行初始化,将它可用的信号量个数设置一个初始值;在二进制 信号量中,该初始值的范围是0~1,假如初始值为1个可用的信号量的话,被申请一次就变得无效了,那就需要我们释放信号量,RT-Thread提供了信号量释放函数rt_sem_release(),每调用一次该函数就释放一个信号量。但是有个问题,能不能一直释放呢 ?很显然,这是不能的,无论是你的信号量是用作二值信号量还是计数信号量, 都要注意可用信号量的范围,当用作二值信号量的时候,必须确保其可用值在01范围内**,所以使用**二值信号量的时候要在使用完毕应及时释放信号量**;而用作**计数信号量的话,其范围是065535,不允许超过释放65535个信号量,这代表我们不能一直调用rt_sem_release()函数来释放信号量

当线程完成资源的访问后,应尽快释放它持有的信号量,使得其他线程能获得该信号量

在中断中一样可以这样子调用信号量释放函数rt_sem_release(),因为这个函数是非阻塞的

static void send_thread_entry(void* parameter)
{
rt_err_t uwRet = RT_EOK;
/* 线程都是一个无限循环,不能返回 */
while (1) { //如果KEY2被单击
if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
/* 释放一个计数信号量 */
uwRet = rt_sem_release(test_sem);
if ( RT_EOK == uwRet )
rt_kprintf ( "KEY2被单击:释放1个停车位。\r\n" );
else
rt_kprintf ( "KEY2被单击:但已无车位可以释放!\r\n" );
}
rt_thread_delay(20); //每20ms扫描一次
}
}

信号量获取函数rt_sem_take()

当信号量有效的时候,线程才能获取信号量,当线程获取了某个信号量的时候,该信号量的有效值就会减一,也就是说该信号量的可用个数就减一,当它减到0的时候,线程就无法再获取了,并且获取的线程会进入阻塞态(假如使用了等待时间的话)。在二进制信号量中,该初始值的范围是0 ~1,假如初始值为1个可用的信号量的话,被获取一次就变得无效了,那么此时另外一个线程获取该信号量的时候, 就会无法获取成功,该线程便会进入阻塞态。每调用一次rt_sem_take()函数获取信号量的时候,信号量的可用个数便减少一个,直至为0的时候,线程就无法成功获取信号量了。

线程通过获取信号量来获得信号量资源,当信号量值大于零时,线程将获得信号量,并且相应的信号量值都会减1;如 果信号量的值等于零,那么说明当前信号量资源不可用,获取该信号量的线程将根据time参数的情况选择直接返回、或 挂起等待一段时间、或永久等待,直到其他线程或中断释放该信号量。如果在参数time指定的时间内依然得不到信号量, 线程将超时返回,返回值是-RT_ETIMEOUT。

rt_sem_take(test_sem, /* 获取信号量 */
RT_WAITING_FOREVER); /* 等待时间:一直等 */
uwRet = rt_sem_take(test_sem, /* 获取一个计数信号量 */
0); /* 等待时间:0 */
if ( RT_EOK == uwRet )
rt_kprintf( "获取信号量成功\r\n" );

互斥信号量

互斥信号量的基本概念

互斥量又称互斥型信号量,是一种特殊的二值信号量,它和信号量不同的是,它支持互斥量所有权、递归访问以及防 止优先级翻转的特性,用于实现对临界资源的独占式处理。任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量 被线程持有时,该互斥量处于闭锁状态,这个线程获得互斥量的所有权。当该线程释放这个互斥量时,该互斥量处于 开锁状态,线程失去该互斥量的所有权。当一个线程持有互斥量时,其他线程将不能再对该互斥量进行开锁或持有。 持有该互斥量的线程也能够再次获得这个锁而不被挂起,这就是递归访问,这个特性与一般的二值信号量有很大的不同,在信号量中,由于已经不存在可用的信号量,线程递归获取信号量时会发生主动挂起(最终形成死锁)。

如果想要用于实现同步(线程之间或者线程与中断之间),二值信号量或许是更好的选择,虽然互斥量也可以用于线 程与线程、线程与中断的同步,但是互斥量更多的是用于保护资源的互锁。

用于互锁的互斥量可以充当保护资源的令牌。当一个线程希望访问某个资源时,它必须先获取令牌。当线程使用完资 源后,必须还回令牌,以便其它线程可以访问该资源。

RT-Thread提供的互斥量通过优先级继承算法,可以降低优先级翻转问题产生 的影响,所以,用于临界资源的保护一般建议使用互斥量。

互斥信号量的继承机制

在RT-Thread操作系统中为了降低优先级翻转问题利用了优先级继承算法。优先级继承算法是指,暂时提高某个占有 某种资源的低优先级线程的优先级,使之与在所有等待该资源的线程中优先级最高那个线程的优先级相等,而当这个 低优先级线程执行完毕释放该资源时,优先级重新回到初始设定值。因此,继承优先级的线程避免了系统资源被任何 中间优先级的线程抢占。

互斥量与二值信号量最大的不同是:互斥量具有优先级继承机制,而信号量没有。也就是说,某个临界资源受到一个互斥量保护,如果这个资源正在被一个低优先级线程使用,那么此时的互斥量是闭锁状态,也代表了没有线程能申请 到这个互斥量,如果此时一个高优先级线程想要对这个资源进行访问,去申请这个互斥量,那么高优先级线程会因为申请不到互斥量而进入阻塞态,那么系统会将现在持有该互斥量的线程的优先级临时提升到与高优先级线程的优先级 相同,这个优先级提升的过程叫做优先级继承。这个优先级继承机制确保高优先级线程进入阻塞状态的时间尽可能短, 以及将已经出现的“优先级翻转”危害降低到最小。

没有理解?没问题,结合过程示意图再说一遍。我们知道线程的优先级在创建的时候就已经是设置好的,高优先级的 线程可以打断低优先级的线程,抢占CPU的使用权。但是在很多场合中,某些资源只有一个,当低优先级线程正在占 用该资源的时候,即便高优先级线程也只能乖乖的等待低优先级线程使用完该资源后释放资源。这里高优先级线程无 法运行而低优先级线程可以运行的现象称为“优先级翻转”。

为什么说优先级翻转在操作系统中是危害很大?因为在我们一开始创造这个系统的时候,我们就已经设置好了线程的 优先级了,越重要的线程优先级越高。但是发生优先级翻转,对我们操作系统是致命的危害,会导致系统的高优先级 线程阻塞时间过长。

举个例子,现在有3个线程分别为H线程(High)、M线程(Middle)、L线程(Low),3个线程的优先级顺序为H线 程>M线程>L线程。正常运行的时候H线程可以打断M线程与L线程,M线程可以打断L线程,假设系统中有一个资源被保 护了,此时该资源被L线程正在使用中,某一刻,H线程需要使用该资源,但是L线程还没使用完,H线程则因为申请不 到资源而进入阻塞态,L线程继续使用该资源,此时已经出现了“优先级翻转”现象,高优先级线程在等着低优先级的 线程执行,如果在L线程执行的时候刚好M线程被唤醒了,由于M线程优先级比L线程优先级高,那么会打断L线程,抢 占了CPU的使用权,直到M线程执行完,再把CUP使用权归还给L线程,L线程继续执行,等到执行完毕之后释放该资源, H线程此时才从阻塞态解除,使用该资源。这个过程,本来是最高优先级的H线程,在等待了更低优先级的L线程与M线 程,其阻塞的时间是M线程运行时间+L线程运行时间,这只是只有3个线程的系统,假如很多个这样子的线程打断最低 优先级的线程,那这个系统最高优先级线程岂不是崩溃了,这个现象是绝对不允许出现的,高优先级的线程必须能及 时响应。所以,没有优先级继承的情况下,使用资源保护,其危害极大,具体见图:

  • (1):L线程正在使用某临界资源, H线程被唤醒,执行H线程。但L线程并未执行完 毕,此时临界资源还未释放。
  • (2):这个时刻H线程也要对该临界资源进行访问,但 L线程还未释放资源,由于保护机制, H线程进入阻塞态,L线程得以继续运行,此时已经发生了优先级翻转现象。
  • (3):某个时刻M线程被唤醒,由于M线程的优先级高于L线程, M线程抢占了CPU的使用权, M线程开始运行,此时L线程尚未执行完,临界资源还没被释放。
  • (4):M线程运行结束,归还CPU使用权,L线程继续运行。
  • (5):L线程运行结束,释放临界资源,H线程得以对资源进行访问,H线程开始运行。

在这过程中,H线程的等待时间过长,这对系统来说这是很致命的,所以这种情况不允许出现,而互斥量就是用来降 低优先级翻转的产生的危害。

假如有优先级继承呢?那么,在H线程申请该资源的时候,由于申请不到资源会进入阻塞态,那么系统就会把当前正在使 用资源的L线程的优先级临时提高到与H线程优先级相同,此时M线程被唤醒了,因为它的优先级比H线程低,所以无法打 断L线程,因为此时L线程的优先级被临时提升到H,所以当L线程使用完该资源了,进行释放,那么此时H线程优先级最高, 将接着抢占CPU的使用权, H线程的阻塞时间仅仅是L线程的执行时间,此时的优先级的危害降到了最低,看!这就是优 先级继承的优势,具体见图:

  • (1):L线程正在使用某临界资源,L线程正在使用某临界资源, H线程被唤醒,执行H线程。 但L线程并未执行完毕,此时临界资源还未释放。
  • (2):某一时刻H线程也要对该资源进行访问,由于保护机制,H线程进入阻塞态。此时发生 优先级继承,系统将L线程的优先级暂时提升到与H线程优先级相同,L线程继续执行。
  • (3):在某一时刻M线程被唤醒,由于此时M线程的优先级暂时低于L线程,所以M线程仅在就 绪态,而无法获得CPU使用权。
  • (4):L线程运行完毕,H线程获得对资源的访问权,H线程从阻塞态变成运行态,此时L线程 的优先级会变回原来的优先级。
  • (5):当H线程运行完毕,M线程得到CPU使用权,开始执行。
  • (6):系统正常运行,按照设定好的优先级运行。

但是使用互斥量的时候一定需要注意:在获得互斥量后,请尽快释放互斥量,同时需要注意的是在线程持有互斥量的 这段时间,不得更改线程的优先级

互斥量的使用场景

互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。在初始化的时候,互斥量处于开锁的状态, 而被线程持有的时候则立刻转为闭锁的状态。互斥量更适合于:

  • 线程可能会多次获取互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题;
  • 可能会引起优先级翻转的情况;

多线程环境下往往存在多个线程竞争同一临界资源的应用场景,互斥量可被用于对临界资源的保护从而实现独占式访问。 另外,互斥量可以降低信号量存在的优先级翻转问题带来的影响。

比如有两个线程需要对串口进行发送数据,其硬件资源只有一个,那么两个线程肯定不能同时发送啦,不然导致数据错误,那么,就可以用互斥量对串口资源进行保护,当一个线程正在使用串口的时候,另一个线程则无法使用串口,等到线程使用串口完毕之后,另外一个线程才能获得串口的使用权。

另外需要注意的是互斥量不能在中断服务函数中使用。

互斥量的运作机制

多线程环境下会存在多个线程访问同一临界资源的场景,该资源会被线程独占处理。其他线程在资源被占用的情况下不 允许对该临界资源进行访问,这个时候就需要用到RT-Thread的互斥量来进行资源保护,那么互斥量是怎样来避免这种冲突?

用互斥量处理不同线程对临界资源的同步访问时,线程想要获得互斥量才能进行资源访问,如果一旦有线程成功获得了 互斥量,则互斥量立即变为闭锁状态,此时其他线程会因为获取不到互斥量而不能访问这个资源,线程会根据用户自定义的等待时间进行等待,直到互斥量被持有的线程释放后,其他线程才能获取互斥量从而得以访问该临界资源,此时互斥量再次上锁,如此一来就可以确保每个时刻只有一个线程正在访问这个临界资源,保证了临界资源操作的安全性。

互斥量控制块

说到 互斥量的使用就不得不说一下互斥量的控制块了,互斥量控制块与线程控制类似,每一个互斥量都有自己的互斥量控制块,互 斥量控制块中包含了互斥量的所有信息,比如互斥量的一些状态信息,使用情况等。

struct rt_mutex {
struct rt_ipc_object parent; (1)
rt_uint16_t value; (2)
rt_uint8_t original_priority; (3)
rt_uint8_t hold; (4)
struct rt_thread *owner; (5)
};
typedef struct rt_mutex *rt_mutex_t;
  • (1):互斥量属于内核对象,也会在自身结构体里面包含一个内核对象类型的成员, 通过这个成员可以将互斥量挂到系统对象容器里面。互斥量从rt_ipc_object中派生,由IPC容器管理。
  • (2):互斥量的值。初始状态下互斥量的值为1,因此,如果值大于0,表示可以使用互斥量。
  • (3):持有互斥量线程的原始优先级,用来做优先级继承的保存。
  • (4):持有互斥量的线程的持有次数,用于记录线程递归调用了多少次获取互斥量。
  • (5):当前持有互斥量的线程。

互斥量创建函数rt_mutex_create()

我们可以调用rt_mutex_create函数创建一个互斥量,它的名字由name所指定。创建成功返回指向互斥量的互斥 量句柄,否则返回RT_NULL。

/* 定义互斥量控制块 */
static rt_mutex_t test_mux = RT_NULL;
/* 创建一个互斥量 */
test_mux = rt_mutex_create("test_mux",RT_IPC_FLAG_PRIO);
if (test_mux != RT_NULL)
rt_kprintf("互斥量创建成功!\n\n");

互斥量删除函数rt_mutex_delete()

互斥量删除函数是根据互斥量句柄(mutex)直接删除的,删除之后这个互斥量的所有信息都会被系统回收清空,而且 不能再次使用这个互斥量。但是需要注意的是,如果互斥量没有被创建,那是无法被删除的,动脑子想想都知道,没创 建的东西就不存在,怎么可能被删除。删除互斥量的时候会把所有阻塞在互斥量的线程唤醒,被唤醒的线程则会得到一 个错误码-RT_ERROR; mutex是rt_sem_delete()传入的参数,是互斥量句柄,表示的是要删除哪个互斥量。

当删除一个互斥量时,所有等待此互斥量的线程都将被唤醒,等待线程获得的返回值是-RT_ERROR。然后系统将 该互斥量从内核对象管理器链表中删除并释放互斥量占用的内存空间。

/* 定义消息队列控制块 */
static rt_mutex_t test_mutex = RT_NULL;
rt_err_t uwRet = RT_EOK;
uwRet = rt_mutex_delete(test_mutex);
if (RT_EOK == uwRet)
rt_kprintf("互斥量删除成功!\n\n");

互斥量释放函数rt_mutex_release()

线程想要访问某个资源的时候,需要先获取互斥量,然后进行资源访问,在线程使用完该资源的时候,必须要及时归 还互斥量,这样别的线程才能对资源进行访问。在前面的讲解中,我们知道,当互斥量有效的时候,线程才能获取互 斥量,那么,是什么函数使得信号量变得有效呢?RT-Thread给我们提供了互斥量释放函数rt_mutex_release(), 线程可以调用rt_mutex_release()函数进行释放互斥量,表示我已经用完了,别人可以申请使用。

使用该函数接口时,只有已持有互斥量所有权的线程才能释放它,每释放一次该互斥量,它的持有计数就减1。当该 互斥量的持有计数为零时(即持有线程已经释放所有的持有操作),互斥量则变为开锁状态,等待在该互斥量上的线 程将被唤醒。如果线程的优先级被互斥量的优先级翻转机制临时提升,那么当互斥量被释放后,线程的优先级将恢复 为原本设定的优先级。

使用该函数接口时,只有已经拥有互斥量控制权的线程才能释放它,每释放一次该互斥量,它的持有计数就减1。当该互斥 量的持有计数为零时(即持有线程已经释放所有的持有操作),它变为可用,等待在该信号量上的线程将被唤醒。如果线 程的运行优先级被互斥量提升,那么当互斥量被释放后,线程恢复为持有互斥量前的优先级。

/* 定义消息队列控制块 */
static rt_mutex_t test_mutex = RT_NULL;
rt_err_t uwRet = RT_EOK;
uwRet = rt_mutex_release(test_mutex);
if (RT_EOK == uwRet)
rt_kprintf("互斥量释放成功!\n\n");

互斥量获取函数rt_mutex_take()

释放互斥量对应的是获取互斥量,我们知道,当互斥量处于开锁的状态,线程才能获取互斥量成功,当线程持有了某个互斥量 的时候,其它线程就无法获取这个互斥量,需要等到持有互斥量的线程进行释放后,其他线程才能获取成功,线程通过互斥量 rt_mutex_take()函数获取互斥量的所有权。线程对互斥量的所有权是独占的,任意时刻互斥量只能被一个线程持有,如果互 斥量处于开锁状态,那么获取该互斥量的线程将成功获得该互斥量,并拥有互斥量的使用权;如果互斥量处于闭锁状态,获取 该互斥量的线程将无法获得互斥量,线程将被挂起,直到持有互斥量线程释放它,而如果线程本身就持有互斥量,再去获取这 个互斥量却不会被挂起,只是将该互斥量的持有值加1。

/* 定义消息队列控制块 */
static rt_mutex_t test_mutex = RT_NULL;
rt_err_t uwRet = RT_EOK;
rt_mutex_take(test_mux, /* 获取互斥量 */
RT_WAITING_FOREVER); /* 等待时间:一直等 */
if (RT_EOK == uwRet)
rt_kprintf("互斥量获取成功!\n\n");

互斥量使用注意事项

使用互斥量时候需要注意几点:

  1. 两个线程不能对同时持有同一个互斥量。如果某线程对已被持有的互斥量进行获取,则该线程会被挂起,直 到持有该互斥量的线程将互斥量释放成功,其他线程才能申请这个互斥量。

  2. 互斥量不能在中断服务程序中使用。

  3. RT-Thread作为实时操作系统需要保证线程调度的实时性,尽量避免线程的长时间阻塞,因此在获得互斥 量之后,应该尽快释放互斥量。

  4. 持有互斥量的过程中,不得再调用rt_thread_control()等函数接口更改持有互斥量线程的优先级。

互斥量实验

互斥量同步实验是在RT-Thread中创建了两个线程,一个是申请互斥量线程,一个是释放互斥量线程,两个线 程独立运行,申请互斥量线程是一直在等待互斥量线程的释放互斥量,其等待时间是RT_WAITING_FOREVER, 一直在等待,等到获取到互斥量之后,进行处理完它又马上释放互斥量。

释放互斥量线程模拟占用互斥量,延时的时间接收线程无法获得互斥量,等到线程使用互斥量完毕,然后进行互 斥量的释放,接收线程获得互斥量,然后形成两个线程间的同步,若是线程正常同步,则在串口打印出信息

软件定时器

基本概念

软件定时器在被创建之后,当经过设定的时钟计数值后会触发用户定义的超时函数。定时精度与系统时钟的周期有 关。一般系统利用SysTick作为软件定时器的基础时钟,超时函数类似硬件的中断服务函数,所以,超时函数也要快进快出,而且超时函数中不能有任何阻塞线程运行的情况,比如rt_thread_delay()以及其它能阻塞线程运行的 函数,两次触发超时函数的时间间隔Tick叫定时器的定时周期。

RT-Thread操作系统提供软件定时器功能,软件定时器的使用相当于扩展了定时器的数量,允许创建更多的定时业 务。RT-Thread软件定时器功能上支持:

  • 静态裁剪:能通过宏关闭软件定时器功能。
  • 软件定时器创建。
  • 软件定时器启动。
  • 软件定时器停止。
  • 软件定时器删除。

RT-Thread提供的软件定时器支持单次模式和周期模式,单次模式和周期模式的定时时间到之后都会调用定时器的超时函数,用户可以在超时函数中加入要执行的工程代码。

单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次超时函数之后就将该定时器删除,不 再重新执行。

周期模式:这个定时器会按照设置的定时时间循环执行超时函数,直到用户将定时器删除。

事件

基本概念

事件是一种实现线程间通信的机制,主要用于实现线程间的同步,但事件通信只能是事件类型的通信,无数据传输。 与信号量不同的是,它可以实现一对多,多对多的同步。即一个线程可以等待多个事件的发生:可以是任意一个事 件发生时唤醒线程进行事件处理;也可以是几个事件都发生后才唤醒线程进行事件处理。同样,事件也可以是多个线程同步多个事件。

事件集合用32位无符号整型变量来表示,每一位代表一个事件,线程通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件集。事件的“逻辑或”也称作是独立型同步,指的是线程感兴趣的所有事件任一件发生即可被唤醒事件“逻辑与”也称为是关联型同步,指的是线程感兴趣的若干事件都发生时才被唤醒。

多线程环境下,线程之间往往需要同步操作,一个事件发生即是一个同步。事件可以提供一对多、多对多的同步操 作。一对多同步模型:一个线程等待多个事件的触发;多对多同步模型:多个线程等待多个事件的触发。

线程可以通过创建事件来实现事件的触发和等待操作。RT-Thread的事件仅用于同步,不提供数据传输功能。

RT-Thread提供的事件具有如下特点:

  • 事件只与线程相关联,事件相互独立,一个32位的事件集合(set变量),用于标识该线程发生的事件类型, 其中每一位表示一种事件类型(0表示该事件类型未发生、1表示该事件类型已经发生),一共32种事件类型。
  • 事件仅用于同步,不提供数据传输功能。
  • 事件无排队性,即多次向线程发送同一事件(如果线程还未来得及读走),等效于只发送一次。
  • 允许多个线程对同一事件进行读写操作。
  • 支持事件等待超时机制。

在RT-Thread实现中,每个线程都拥有一个事件信息标记,它有三个属性,分别是RT_EVENT_FLAG_AND(逻辑与), RT_EVENT_FLAG_OR(逻辑或)以及RT_EVENT_FLAG_CLEAR(清除标记)。当线程等待事件同步时,可以通过32个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。

事件的应用场景

用事件来做标志位,判断某些事件是否发生了,然后根据结果做处理。

在某些场合,可能需要多个时间发生了才能进行下一步操作,比如一些危险机器的启动,需要检查各项指标,当指标 不达标的时候,无法启动,但是检查各个指标的时候,不能一下子检测完毕啊,所以,需要事件来做统一的等待,当 所有的事件都完成了,那么机器才允许启动,这只是事件的其中一个应用。

事件可使用于多种场合,它能够在一定程度上替代信号量,用于线程间同步。一个线程或中断服务例程发送一个事件给事件对象,而后等待的线程被唤醒并对相应的事件进行处理。但是它与信号量不同的是,事件的发送操作是不可累 计的,而信号量的释放动作是可累计的。事件另外一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。同时按照线程等待的参数,可选择是“逻辑或”触发还是“逻辑与”触发。这个特性也是信号量等所不具备的,信号量只能识别单一同步动作,而不能同时等待多个事件的同步。

各个事件可分别发送或一起发送给事件对象,而线程可以等待多个事件,线程仅对感兴趣的事件进行关注。当有它们感兴趣的事件发生时并且符合感兴趣的条件,线程将被唤醒并进行后续的处理动作。

事件的运作机制

接收事件时,可以根据入感兴趣的参事件类型接收事件的单个或者多个事件类型。事件接收成功后,必须使用 RT_EVENT_FLAG_CLEA选项来清除已接收到的事件类型,否则不会清除已接收到的事件。用户可以自定义通过传入 参数选择读取模式option,是等待所有感兴趣的事件还是等待感兴趣的任意一个事件。

发送事件时,对指定事件写入指定的事件类型,设置事件集合set的对应事件位为1,可以一次同时写多个事件类型, 发送事件会触发线程调度。

清除事件时,根据入参数事件句柄和待清除的事件类型,对事件对应位进行清0操作。事件不与线程相关联,事件相 互独立,一个32位的变量(事件集合set),用于标识该线程发生的事件类型,其中每一位表示一种事件类型 (0表示该事件类型未发生、1表示该事件类型已经发生),一共32种事件类型。

事件唤醒机制,当线程因为等待某个或者多个事件发生而进入阻塞态,当事件发生的时候会被唤醒,其过程具体见 :

线程1对事件3或事件5感兴趣(逻辑或RT_EVENT_FLAG_OR),当发生其中的某一个事件都会被唤醒,并且执行相应操作。 而线程2对事件3与事件5感兴趣(逻辑与RT_EVENT_FLAG_AND),当且仅当事件3与事件5都发生的时候,线程2才会被唤 醒,如果只有一个其中一个事件发生,那么线程还是会继续等待事件发生。如果接在收事件函数中option设置了清除事 件位,那么当线程唤醒后将把事件3和事件5的事件标志清零,否则事件标志将依然存在。

事件控制块

事件的使用很简单,每个对事件的操作的函数都是根据事件控制块来进行操作的,事件控制块包含了一个32位的set变 量,其变量的各个位表示一个事件,每一位代表一个事件的发生,利用逻辑或、逻辑与等实现不同事件的不同唤醒处理。

struct rt_event {
struct rt_ipc_object parent;
rt_uint32_t set; /* 事件标志位 */
};
typedef struct rt_event *rt_event_t; /* rt_event_t是指向事件结构体的指针 */

事件属于内核对象,也会在自身结构体里面包含一个内核对象类型的成员,通过这个成员可以将事件挂到系统对象容器里 面。rt_event对象从rt_ipc_object中派生,由IPC容器管理。

事件创建函数rt_event_create()

创建一个事件,与其他内核对象一样,都是需要先创建才能使用的资源,RT-Thread给我们 提供了一个创建事件的函数rt_event_create(),当创建一个事件时,内核首先创建一个事件控制块,然后对该事件控制块 进行基本的初始化,创建成功返回事件句柄;创建失败返回RT_NULL。所以,在使用创建函数之前,我们需要先定义有个事 件的句柄。

/* 定义事件控制块(句柄) */
static rt_event_t test_event = RT_NULL;
/* 创建一个事件 */
test_event = rt_event_create("test_event",/* 事件标志组名字 */
RT_IPC_FLAG_PRIO); /* 事件模式 FIFO(0x00)*/
if (test_event != RT_NULL)
rt_kprintf("事件创建成功!\n\n");

事件删除函数rt_event_delete()

在很多场合,某些事件只用一次的,就好比在事件应用场景说的危险机器的启动,假如各项指标都达到了,并且机器启动成功了,那这个事件之后可能就没用了,那就可以进行销毁了。想要删除事件怎么办呢?RT-Thread给 我们提供了一个删除事件的函数——rt_event_delete(),使用它就能将事件进行删除了。当系统不再使用事件对 象时,可以通过删除事件对象控制块来释放系统资源。

/* 定义事件控制块(句柄) */
static rt_event_t test_event = RT_NULL;
rt_err_t uwRet = RT_EOK;
/* 删除一个事件 */
uwRet = rt_event_delete(test_event);
if (RT_EOK == uwRet)
rt_kprintf("事件删除成功!\n\n");

事件发送函数rt_event_send()

使用该函数接口时,通过参数set指定的事件标志来设定事件的标志位,然后遍历等待在event事件对象上的 等待线程链表,判断是否有线程的事件激活要求与当前事件对象标志值匹配,如果有,则唤醒该线程。简单 来说,就是设置我们自己定义的事件标志位为1,并且看看有没有线程在等待这个事件,有的话就唤醒它。

举个例子,比如我们要记录一个事件的发生,这个事件在事件集合的位置是bit0,当它还未发生的时候,那么事件 集合bit0的值也是0,当它发生的时候,我们往事件集合bit0中写入这个事件,也就是0x01,那这就表示事件已经 发生了,为了便于理解,一般操作我们都是用宏定义来实现 #define EVENT(0x01 << x), “<< x”表示写入事件 集合的bit x 。

#define KEY1_EVENT (0x01 << 0)//设置事件掩码的位0
#define KEY2_EVENT (0x01 << 1)//设置事件掩码的位1
static void send_thread_entry(void* parameter)
{
/* 线程都是一个无限循环,不能返回 */
while (1) {//如果KEY2被单击
if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
rt_kprintf ( "KEY1被单击\n" );
/* 发送一个事件1 */
rt_event_send(test_event,KEY1_EVENT);
}
//如果KEY2被单击
if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
rt_kprintf ( "KEY2被单击\n" );
/* 发送一个事件2 */
rt_event_send(test_event,KEY2_EVENT);
}
rt_thread_delay(20); //每20ms扫描一次
}
}

事件接受函数rt_event_recv()

RT-Thread提供了一个接收指定事件的函数——rt_event_recv(),通过这个函数,我们知道事件集合中 的哪一位,哪一个事件发生了,我们可以通过“逻辑与”、“逻辑或”等操作对感兴趣的事件进行接收,并且这个 函数实现了等待超时机制,如果此刻该事件没有发生,那么线程可以进入阻塞态进行等待,等到事件发生了就 会被唤醒,很有效的体现了操作系统的实时性,如果事件正确接收则返回RT_EOK,事件接收超时则返 回-RT_ETIMEOUT,其他情况返回-RT_ERROR。

当用户调用这个接口时,系统首先根据set参数和接收选项来判断它要接收的事件是否发生,如果已经发生,则根据参数 option上是否设置有RT_EVENT_FLAG_CLEAR来决定是否清除事件的相应标志位,其中recved参数用于保存收到的事件; 如果事件没有发生,则把线程感兴趣的事件和接收选项填写到线程控制块中,然后把线程挂起在此事件对象的阻塞列表上, 直到事件发生或等待时间超时。

static void receive_thread_entry(void* parameter)
{
rt_uint32_t recved;
/* 线程都是一个无限循环,不能返回 */
while (1) {
/* 等待接收事件标志 */
rt_event_recv(test_event, /* 事件对象句柄 */
KEY1_EVENT|KEY2_EVENT, /* 接收线程感兴趣的事件 */
RT_EVENT_FLAG_AND|RT_EVENT_FLAG_CLEAR,/* 接收选项 并且|接收后清除事件位 */
RT_WAITING_FOREVER, /* 指定超时事件,一直等 */
&recved); /* 指向接收到的事件 */
if (recved == (KEY1_EVENT|KEY2_EVENT)) { /* 如果接收完成并且正确 */
rt_kprintf ( "Key1与Key2都按下\n");
LED1_TOGGLE; //LED1 反转
} else
rt_kprintf ( "事件错误!\n");
}
}

内存管理

RT-Thread操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些 内存管理函数是如何实现的,所以在RT-Thread中提供了多种内存分配算法(分配策略),但是上层接口(API)却 是统一的。这样做可以增加系统的灵活性:用户可以选择对自己更有利的内存管理策略,在不同的应用场合使用不 同的内存分配策略。

一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反。

中断管理

异常与中断的基本概念

异常是导致处理器脱离正常运行转向执行特殊代码的任何事件,如果不及时进行处理,轻则系统出错,重则会导致系统毁灭性 瘫痪。所以正确地处理异常,避免错误的发生是提高软件鲁棒性(稳定性)非常重要的一环,对于实时系统更是如此。

异常是指任何打断处理器正常执行,并且迫使处理器进入一个由有特权的特殊指令执行的事件。异常通常可以分成两类:同步异常和异步异常。由内部事件(像处理器指令运行产生的事件)引起的异常称为同步异常,例如造成被零除的算术运算引发一 个异常,又如在某些处理器体系结构中,对于确定的数据尺寸必须从内存的偶数地址进行读和写操作。从一个奇数内存地址的 读或写操作将引起存储器存取一个错误事件并引起一个异常,(称为校准异常)。

异步异常主要是指由于外部异常源产生的异常,是一个由外部硬件装置产生的事件引起的异步异常。同步异常不同于异步异常 的地方是事件的来源,同步异常事件是由于执行某些指令而从处理器内部产生的,而异步异常事件的来源是外部硬件装置。例 如按下设备某个按钮产生的事件。同步异常与异步异常的区别还在于,同步异常触发后,系统必须立刻进行处理而不能够依然 执行原有的程序指令步骤;而异步异常则可以延缓处理甚至是忽略,例如按键中断异常,虽然中断异常触发了,但是系统可以 忽略它继续运行(同样也忽略了相应的按键事件)。

中断,中断属于异步异常。所谓中断是指中央处理器CPU正在处理某件事的时候,外部发生了某一事件,请求CPU迅速处理,CPU 暂时中断当前的工作,转入处理所发生的事件,处理完后,再回到原来被中断的地方,继续原来的工作,这样的过程称为中断。

中断能打断线程的运行,无论该线程具有什么样的优先级,因此中断一般用于处理比较紧急的事件,而且只做简 单处理,例如标记该事件,在使用 RT-Thread系统时,一般建议使用信号量、消息或事件标志组等标志中断的发生,将这些内核对象发布给处理线程,处理线程再做具体处理。

通过中断机制,在外设不需要CPU介入时,CPU可以执行其它线程,而当外设需要CPU时通过产生中断信号使CPU立即停止当前 线程转而来响应中断请求。这样可以使CPU避免把大量时间耗费在等待、查询外设状态的操作上,因此将大大提高系统实时性 以及执行效率。

RT-Thread的中断管理支持:

  • 开/关中断。
  • 恢复中断。
  • 中断使能。
  • 中断屏蔽。

中断的介绍

外设:当外设需要请求CPU时,产生一个中断信号,该信号连接至中断控制器。

中断控制器:中断控制器是CPU众多外设中的一个,它一方面接收其它外设中断信号的输入,另一方面,它会发出中断信号给 CPU。可以通过对中断控制器编程实现对中断源的优先级、触发方式、打开和关闭源等设置操作。在Cortex-M系列控制器中常 用的中断控制器是NVIC(内嵌向量中断控制器Nested Vectored Interrupt Controller)。

CPU:CPU会响应中断源的请求,中断当前正在执行的线程,转而执行中断处理程序。NVIC最多支持240个中断,每个中断最多 256个优先级。

中断相关名词解释

  • 中断号:每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号。
  • 中断请求:“紧急事件”需向CPU提出申请,要求CPU暂停当前执行的线程,转而处理该“紧急事件”,这一申请过程称为中断请求。
  • 中断优先级:为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称 作中断优先级。
  • 中断处理程序:当外设产生中断请求后,CPU暂停当前的线程,转而响应中断申请,即执行中断处理程序。
  • 中断触发:中断源发出并送给CPU控制信号,将中断触发器置“1”,表明该中断源产生了中断,要求CPU去响应该中断,CPU暂停 当前线程,执行相应的中断处理程序。
  • 中断触发类型:外部中断申请通过一个物理信号发送到NVIC,可以是电平触发或边沿触发。
  • 中断向量:中断服务程序的入口地址。
  • 中断向量表:存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储。
  • 临界段:代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断, 在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。RT-Thread支持中断屏蔽和中断使能。

中断的运作机制

当中断产生时,处理机将按如下的顺序执行:

  1. 保存当前处理机状态信息
  2. 载入异常或中断处理函数到PC寄存器
  3. 把控制权转交给处理函数并开始执行
  4. 当处理函数执行完成时,恢复处理器状态信息
  5. 从异常或中断中返回到前一个程序执行点

中断使得CPU可以在事件发生时才给予处理,而不必让CPU连续不断地查询是否有相应的事件发生。通过两条特殊指令:关中断和 开中断可以让处理器不响应或响应中断,在关闭中断期间,通常处理器会把新产生的中断挂起,当中断打开时立刻进行响应,所 以会有适当的延时响应中断,故用户在进入临界区的时候应快进快出。

中断延迟

断延迟是指从硬件中断发生到开始执行中断处理程序第一条指令之间的这段时间。也就是:系统接收到中断信号到操作系统作出 响应,并完成换到转入中断服务程序的时间。也可以简单地理解为:(外部)硬件(设备)发生中断,到系统执行中断服务子程序 (ISR)的第一条指令的时间。

中断延迟可以定义为,从中断开始的时刻到中断服务例程开始执行的时刻之间的时间段。中断延迟 = 识别中断时间 + [等待中断打开时间] + [关闭中断时间]。

注意:“[ ]”的时间是不一定都存在的,此处为最大可能的中断延迟时间。

中断管理

ARM Cortex-M内核的中断是不受RT-Thread管理的,所以RT- Thread中的中断使用其实跟裸机差不多的,需要我们自己配置中断,并且使能中断,编写中断服务函数,在中断服务函数中使用内 核IPC通信机制,一般建议使用信号量、消息或事件标志组等标志事件的发生,将事件发布给处理线程等退出中断后再由相关处理线程具体处理中断。由于中断不受RT-Thread管理,所以不需要使用RT-Thread提供的函数(中断屏蔽与使能除外)。

ARM Cortex-M NVIC支持中断嵌套功能:当一个中断触发并且系统进行响应时,处理器硬件会将当前运行的部 分上下文寄存器自动压入中断栈中,这部分的寄存器包括PSR,R0,R1,R2,R3以及R12寄存器。当系统正在 服务一个中断时,如果有一个更高优先级的中断触发,那么处理器同样的会打断当前运行的中断服务例程,然 后把老的中断服务例程上下文的PSR,R0,R1,R2,R3和R12寄存器自动保存到中断栈中。这些部分上下文寄存 器保存到中断栈的行为完全是硬件行为,这一点是与其他ARM处理器最大的区别(以往都需要依赖于软件保存上下文)。

另外,在ARM Cortex-M系列处理器上,所有中断都采用中断向量表的方式进行处理,即当一个中断触发时,处理 器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理。而在ARM7、ARM9中,一般是先跳转进 入IRQ入口,然后再由软件进行判断是哪个中断源触发,获得了相对应的中断服务例程入口地址后,再进行后续的 中断处理。ARM7、ARM9的好处在于,所有中断它们都有统一的入口地址,便于OS的统一管理。而ARM Cortex-M系列 处理器则恰恰相反,每个中断服务例程必须排列在一起放在统一的地址上(这个地址必须要设置到NVIC的中断向 量偏移寄存器中)。中断向量表一般由一个数组定义(或在起始代码中给出)。

RT-Thread在Cortex-M系列处理器上也遵循与裸机中断一致的方法,当用户需要使用自定义的中断服务例程时,只需要定义相同名 称的函数覆盖弱化符号即可。所以,RT-Thread在Cortex-M系列处理器的中断控制其实与裸机没什么差别。

中断实验举例

中断管理实验是在RT-Thread中创建了两个线程分别获取信号量与消息队列,并且定义了两个按键KEY1与KEY2的触发方式为中断触 发,其触发的中断服务函数则跟裸机一样,在中断触发的时候通过消息队列将消息传递给线程,线程接收到消息就将信息通过串口 调试助手显示出来。而且中断管理实验也实现了一个串口的DMA传输+空闲中断功能,当串口接收完不定长的数据之后产生一个空闲 中断,在中断中将信号量传递给线程,线程在收到信号量的时候将串口的数据读取出来并且在串口调试助手中回显。

简单对比

通讯方式 说明 生活场景类比 函数原型:获取/释放
信号量 有限资源的使用数量控制 停车场-车位 rt_sem_take() / rt_sem_release()
互斥量 信号量+锁=优先级反转 篮球运动-球权 rt_mutex_take() / rt_mutex_release()
事件 事件触发机制。一对多,多对多。逻辑方式:与,或,清除 flag rt_event_send() / rt_event_recv()
邮箱 4字节数据,可以是变量或指针。功能与消息队列重复。 rt_mb_send() / rt_mb_recv()
消息队列 邮箱的扩展,异步通讯,先进先出。 rt_mq_send() / rt_mq_recv()
信号 本质是软中断,用来通知线程发生了异步事件。用作线程间的异常通知,应急处理。 紧急事件 rt_thread_kill() / handler()

本文作者:夜泽大大

本文链接:https://www.cnblogs.com/songmingze/p/18098230

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   夜泽大大  阅读(337)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 星月神话 金莎
  2. 2 That Girl Olly Murs
星月神话 - 金莎
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

星月神话 - 金莎 (Kym)

词:金莎

曲:金莎

我的一生最美好的场景

就是遇见你

在人海茫茫中静静凝望着你

陌生又熟悉

尽管呼吸着同一天空的气息

尽管呼吸着同一天空的气息

却无法拥抱到你

如果转换了时空身份和姓名

但愿认得你眼睛

千年之后的你会在哪里

身边有怎样风景

我们的故事并不算美丽

却如此难以忘记

尽管呼吸着同一天空的气息

尽管呼吸着同一天空的气息

却无法拥抱到你

如果转换了时空身份和姓名

但愿认得你眼睛

千年之后的你会在哪里

身边有怎样风景

我们的故事并不算美丽

却如此难以忘记

如果当初勇敢的在一起

如果当初勇敢的在一起

会不会不同结局

你会不会也有千言万语

埋在沉默的梦里