目录
0. 任务间同步
- 在多任务实时系统中,一项工作的完成往往可以通过多个任务协调的方式共同来完成。例如一个任务是从传感器中接受数据并将数据写到共享内存中,同时另一个任务周期的从共享内存中读取数据并发送去显示。
- 对共享内存的访问不是排他性的,那么各个线程间可能同时访问它。这将引起数据一致性问题,例如,在显示线程试图显示数据之前,传感器线程还未完成数据的写入,那将包含不同的采样的数据,造成显示数据的迷惑。
- 将传感器数据写入到共享内存的代码是接收线程的关键代码段;将传感器数据从共享内
存中读出的代码是显示线程的关键代码段;这两段代码都会访问共享内存。正常的操作序列
应该是在一个线程对共享内存块操作完成后,才允许另一个线程去操作。对于操作/访问同
一块区域,称之为临界区。任务的同步方式有很多种,其核心思想都是:在访问临界区的时
候只允许一个(或一类)任务运行。
4. 同步就是几个线程同时访问共享资源,如何按顺序一个一个执行的问题。
1. 中断(锁)
关闭中断也叫中断锁,是禁止多任务访问临界区最简单的一种方式,即使是在分时操作
系统中也是如此。当中断关闭的时候,就意味着当前任务不会被其他事件打断(因为整个系
统已经不再响应那些可以触发线程重新调度的外部事件),也就是当前线程不会被抢占,除
非这个任务主动放弃了处理器控制权。关闭中断/恢复中断API接口由BSP实现,根据平台的
不同其实现方式也大不相同。
1.2代码
while(1)
{
/* 关闭中断*/
level = rt_hw_interrupt_disable();
cnt += no;
/* 恢复中断*/
rt_hw_interrupt_enable(level);
rt_kprintf(“thread[%d]’s counter is %d\n”, no, cnt);
rt_thread_delay(no);
}
1.3 注意事项
由于关闭中断会导致整个系统不能响应外部中断,所以在使用关闭中断做为互
斥访问临界区的手段时,首先必须需要保证关闭中断的时间非常短,例如数条机器指
令。
1.4 使用场合
- 只是使用中断锁最主要的问题在于,在中断关闭期间系统将不再响应任何中断,也就不能响应外部的事件。所以中断锁对系统的实时性影响非常巨大,当使用不当的时候会导致系统完全无实时性可言(可能导致系统完全偏离要求的时间需求);而使用得当,则会变成一种快速、高效的同步方式。
- 使用中断锁来操作系统的方法可以应用于任何场合。且其他几类同步方式都是依赖于中
断锁而实现的,可以说中断锁是最强大的和最高效的同步方法。
2. 调度器锁
同中断锁一样把调度器锁住也能让当前运行的任务不被换出,直到调度器解锁。但和中
断锁有一点不相同的是,对调度器上锁,系统依然能响应外部中断,中断服务例程依然能进
行相应的响应。所以在使用调度器上锁的方式进行任务同步时,需要考虑好任务访问的临界
资源是否会被中断服务例程所修改,如果可能会被修改,那么将不适合采用此种方式进行同
步。
2.1 代码
void rt_enter_critical(void); /* 进入临界区*/
调用这个函数后,调度器将被上锁。在系统锁住调度器的期间,系统依然响应中断,如
果中断唤醒了的更高优先级线程,调度器并不会立刻执行它,直到调用解锁调度器函数才尝
试进行下一次调度。
void rt_exit_critical(void); /* 退出临界区*/
当系统退出临界区的时候,系统会计算当前是否有更高优先级的线程就绪,如果有比当
前线程更高优先级的线程就绪,将切换到这个高优先级线程中执行;如果无更高优先级线程
就绪,将继续执行当前任务。
2.2 使用场合
调度器锁能够方便地使用于一些线程与线程间同步的场合,由于轻型,它不会对系统中
断响应造成负担;但它的缺陷也很明显,就是它不能被用于中断与线程间的同步或通知,并
且如果执行调度器锁的时间过长,会对系统的实时性造成影响(因为使用了调度器锁后,系
统将不再具备优先级的关系,直到它脱离了调度器锁的状态)。
3. 信号量
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从
而达到同步或互斥的目的。信号量就像一把钥匙,把一段临界区给锁住,只允许有钥匙的线
程进行访问:线程拿到了钥匙,才允许它进入临界区;而离开后把钥匙传递给排队在后面的
等待线程,让后续线程依次进入临界区。
3.1 代码操作
/*
* 程序清单:动态信号量
*
* 这个例子中将创建一个动态信号量(初始值为0)及一个动态线程,在这个动态线程中
* 将试图采用超时方式去持有信号量,应该超时返回。然后这个线程释放一次信号量,
* 并在后面继续采用永久等待方式去持有信号量, 成功获得信号量后返回。
*/
/* 创建一个信号量,初始值是0 */
rt_sem_create(“sem”, 0, RT_IPC_FLAG_FIFO);
/* 试图持有一个信号量,如果10个OS Tick依然没拿到,则超时返回*/
rt_sem_take(sem, 10);
/* 释放一次信号量*/
rt_sem_release(sem);
每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为5,则表示共有5个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。
信号量创建:
rt_sem_t rt_sem_create (const char* name, rt_uint32_t value, rt_uint8_t flag);
当调用这个函数时,系统将先分配一个semaphore对象,并初始化这个对象,然后初始化IPC对象以及与semaphore相关的部分。在创建信号量指定的参数中,信号量标志参数决定了当信号量不可用时,多个线程等待的排队方式。当选择FIFO方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;当选择PRIO(优先级等待)方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。
3.2 使用场合
- 信号量是一种非常灵活的同步方式,可以运用在多种场合中。形成锁,同步,资源计数
等关系,也能方便的用于线程与线程,中断与线程的同步中。
- 线程同步:线程同步是信号量最简单的一类应用。例如,两个线程用来进行任务间的执行控制转移,信号量的值初始化成具备0个信号量资源实例,而等待线程先直接在这个信号量上进行等待。当信号线程完成它处理的工作时,释放这个信号量,以把等待在这个信号量上的线程唤醒,让它执行下一部分工作。这类场合也可以看成把信号量用于工作完成标志:信号线程完成它
自己的工作,然后通知等待线程继续下一部分工作。
- 锁:锁,单一的锁常应用于多个线程间对同一临界区的访问。。信号量在作为锁来使用时,通常应将信号量资源实例初始化成1,代表系统默认有一个资源可用。当线程需要访问临界资
源时,它需要先获得这个资源锁。当这个线程成功获得资源锁时,其他打算访问临界区的线
程将被挂起在该信号量上,这是因为其他线程在试图获取这个锁时,这个锁已经被锁上(信
号量值是0)。当获得信号量的线程处理完毕,退出临界区时,它将会释放信号量并把锁解
开,而挂起在锁上的第一个等待线程将被唤醒从而获得临界区的访问权。因为信号量的值始终在
1和0之间变动,所以这类锁也叫做二值信号量,如图锁所示:
- 中断与线程的同步:信号量也能够方便的应用于中断与线程间的同步,,例如一个中断触发,中断服务例程需要通知线程进行相应的数据处理。。这个时候可以设置信号量的初始值是0,线程在试图持有这个信号量时,由于信号量的初始值是0,线程直接在这个信号量上挂起直到信号量被释放。当中断触发时,先进行与硬件相关的动作,例如从硬件的I/O口中读取相应的数据,并确认中断以清除中断源,而后释放一个信号量来唤醒相应的线程以做后续的数据处理。例如finsh shell线程的处理方式,如图finsh shell的中断、线程间同步所示:
- 警告: 中断与线程间的互斥不能采用信号量(锁)的方式,而应采用中断锁。
- 资源计数
4. 互斥量
互斥量又叫相互排斥的信号量,是一种特殊的二值性信号量。它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性。互斥锁和信号量,一个有中间管理,一个没有中间管理。互斥量的状态只有两种,开锁或闭锁(两种状态值)。当有线程持有它时,互斥量处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放它时,将对互斥量进行开锁,失去它的所有权。当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它,持有该互斥量的线程也能够再次获得这个锁而不被挂起。这个特性与一般的二值信号量有很大的不同,在信号量中,因为已经不存在实例,线程递归持有会发生主动挂起(最终形成死锁)。
4.1 示例:
/*
* 程序清单:互斥量使用例程
* 这个例子将创建3个动态线程以检查持有互斥量时,持有的线程优先级是否
* 被调整到等待线程优先级中的最高优先级。
* 线程1,2,3的优先级从高到低分别被创建,
* 线程3先持有互斥量,而后线程2试图持有互斥量,此时线程3的优先级应该
* 被提升为和线程2的优先级相同。线程1用于检查线程3的优先级是否被提升
* 为与线程2的优先级相同。
*/
/* 创建互斥锁*/
mutex = rt_mutex_create(“mutex”, RT_IPC_FLAG_FIFO);
/*
* 试图持有互斥锁,此时thread3持有,应把thread3的优先级提升
* 到thread2相同的优先级
*/
rt_mutex_take(mutex, RT_WAITING_FOREVER);
/* 释放互斥锁*/
rt_mutex_release(mutex);
4.2 使用场合:
互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。在初始化的时候,互斥量永远都处于开锁的状态,而被线程持有的时候则立刻转为闭锁的状态。互斥量更适合于
5. 事件()
事件主要用于线程间的同步,与信号量不同,它的特点是可以实现一对多,多对多的同步。即一个线程可等待多个事件的触发:可以是其中任意一个事件唤醒线程进行事件处理的操作;也可以是几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件,这种多个事件的集合可以用一个32位无符号整型变量来表示,变量的每一位代表一个事件,线程通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件集。事件的“逻辑或”也称为是独立型同步,指的是线程与任何事件之一发生同步;事件“逻辑与”也称为是关联型同步,指的是线程与若干事件都发生同步。
- 事件只与线程相关,事件间相互独立:每个线程拥有32个事件标志,采用一个32 bit
无符号整型数进行记录,每一个bit代表一个事件。若干个事件构成一个事件集;
- 事件仅用于同步,不提供数据传输功能;
- 事件无排队性,即多次向线程发送同一事件(如果线程还未来得及读走),其效果等同
于只发送一次。
在RT-Thread实现中,每个线程都拥有一个事件信息标记,它有三个属性,分别是
RT_EVENT_FLAG_AND(逻辑与),RT_EVENT_FLAG_OR(逻辑或)以及T_EVENT_FLAG_CLEAR
(清除标记)。当线程等待事件同步时,可以通过32个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。
线程1的事件标志中第2位和第29位被置位,如果事件信息标记位设为逻辑与,则表示线程#1只有在事件1和事件29都发生以后才会被触发唤醒,如果事件信息标记位设为逻辑或,则事件1或事件29中的任意一个发生都会触发唤醒线程#1。如果信息标记同时设置了清除标记位,则当线程#1唤醒后将主动把事件1和事件29清为零,否则事件标志将依然存在(即置1)。
5.1 示例:
/* 初始化事件对象*/
rt_event_init(&event, “event”, RT_IPC_FLAG_FIFO);
/* 线程2入口函数 线程2持续地发送事件#3 */
rt_event_send(&event, (1 << 3));
/* 线程3入口函数 线程3持续地发送事件#5*/
rt_event_send(&event, (1 << 5));
/* 线程1入口函数 以逻辑与的方式接收事件*/
rt_event_recv(&event, ((1 << 3) | (1 << 5)),RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,RT_WAITING_FOREVER, &e)
/* 线程1入口函数 以逻辑或的方式接收事件*/
rt_event_recv(&event, ((1 << 3) | (1 << 5)),RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,RT_WAITING_FOREVER, &e)
5.2 使用场合:
事件可使用于多种场合,它能够在一定程度上替代信号量,用于线程间同步。一个线程或中断服务例程发送一个事件给事件对象,而后等待的线程被唤醒并对相应的事件进行处理。但是它与信号量不同的是,事件的发送操作在事件未清除前,是不可累计的,而信号量的释放动作是累计的。事件另外一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。同时按照线程等待的参数,可选择是“逻辑或”触发还是“逻辑与”触发。这个特性也是信号量等所不具备的,信号量只能识别单一的释放动作,而不能同时等待多种类型的释放。如图多事件接收所示:
各个事件类型可分别发送或一起发送给事件对象,而事件对象可以等待多个线程,它们仅对它们感兴趣的事件进行关注。当有它们感兴趣的事件发生时,线程就将被唤醒并进行后续的处理动作。
6. 邮箱(任务间通信)
邮箱服务是实时操作系统中一种典型的任务间通信方法,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的4字节内容(针对32位处理系统,指针的大小即为4个字节,所以一封邮件恰好能够容纳一个指针)。典型的邮箱也称作交换消息,如图6-8所示,线程或中断服务例程把一封4字节长度的邮件发送到邮箱中。而一个或多个线程可以从邮箱中接收这些邮件进行处理。
RT-Thread操作系统采用的邮箱通信机制有点类似于传统意义上的管道,用于线程间通讯。非阻塞方式的邮件发送过程能够安全的应用于中断服务中,是线程,中断服务,定时器向线程发送消息的有效手段。通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。当邮箱中不存在邮件且超时时间不为0时,邮件收取过程将变成阻塞方式。所以在这类情况下,只能由线程进行邮件的收取。
RT-Thread操作系统的邮箱中可存放固定条数的邮件,邮箱容量在创建/初始化邮箱时设定,每个邮件大小为4字节。当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中。
在一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线程可以设置超时时间,选择是否等待挂起或直接返回-RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送的过程。
在一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或设置超时时间。当设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回-RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的4个字节邮件到接收线程中。
6.1 示例:
/* 初始化一个mailbox */
rt_mb_init(&mb,
“mbt”, /* 名称是mbt */
&mb_pool[0], /* 邮箱用到的内存池是mb_pool */
sizeof(mb_pool)/4, /* 大小是mb_pool/4,因为每封邮件的大小是4字节*/
RT_IPC_FLAG_FIFO); /* 采用FIFO方式进行线程等待*/
//可以在不同的线程中进行接受和发送相关的数据。
/* 从邮箱中收取邮件*/
rt_mb_recv(&mb, (rt_uint32_t*)&str, RT_WAITING_FOREVER)
/* 发送mb_str1地址到邮箱中*/mb_str1 要发送的字符串
rt_mb_send(&mb, (rt_uint32_t)&mb_str1[0]);
6.2 使用场合:
邮箱是一种简单的线程间消息传递方式,在RT-Thread操作系统的实现中能够一次传递4字节邮件,并且邮箱具备一定的存储功能,能够缓存一定数量的邮件数(邮件数由创建、初始化邮箱时指定的容量决定)。邮箱中一封邮件的最大长度是4字节,所以邮箱能够用于不超过4字节的消息传递,当传送的消息长度大于这个数目时就不能再采用邮箱的方式。最重要的是,在32位系统上4字节的内容恰好适合放置一个指针,所以邮箱也适合那种仅传递指针的情况,例如:
7. 消息队列(任务见通信)
消息队列是另一种常用的线程间通讯方式,它能够接收来自线程或中断服务例程中不固
定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应
的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程
将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。
通过消息队列服务,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常应将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。
RT-Thread操作系统的消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的msg_queue_head和msg_queue_tail;有些消息框可能是空的,它们通过msg_queue_free形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。
7.1 示例:
/*
* 程序清单:消息队列例程*
* 这个程序会创建3个动态线程:
* 一个线程会从消息队列中收取消息;
* 一个线程会定时给消息队列发送消息;
* 一个线程会定时给消息队列发送紧急消息。
*/
/* 初始化消息队列*/
rt_mq_init(&mq, “mqt”,
&msg_pool[0], /* 内存池指向msg_pool */
128 – sizeof(void*), /* 每个消息的大小是128 – void* */
sizeof(msg_pool), /* 内存池的大小是msg_pool的大小*/
RT_IPC_FLAG_FIFO); /* 如果有多个线程等待,按照FIFO的方法分配消息*/
/* 从消息队列中接收消息*/
rt_mq_recv(&mq, &buf[0], sizeof(buf), RT_WAITING_FOREVER)
/* 发送消息到消息队列中*/
result = rt_mq_send(&mq, &buf[0], sizeof(buf));
/* 发送紧急消息到消息队列中*/
rt_mq_urgent(&mq, &buf[0], sizeof(buf));
7.2 使用场合
消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及中断
服务例程中发送给线程的消息(中断服务例程不可能接收消息)。
消息队列和邮箱的明显不同是消息的长度并不限定在4个字节以内,另外消息队列也包括了一个发送紧急消息的函数接口。但是当创建的是一个所有消息的最大长度是4字节的消息队列时,消息队列对象将蜕化成邮箱。这个不限定长度的消息,也及时的反应到了代码编写的场合上,同样是类似邮箱的代码: