同步的概念在每一个操作系统里面基本都有实现,大致原理也差不多。同步主要是涉及两方面的同步:
1中断和任务之间的同步
2 任务和任务之间的同步
中断与任务之间的同步主要如下图所示:
file:///C:/Users/ASUS/AppData/Local/Temp/ksohtml/wps_clip_image-6678.png
上图中可以看到外部硬件触发中断的时候可以向任务发出事件,来通知任务有事件发生,需要处理。在raw os 里面主要有信号量,事件标志来处理同步。
为什么需要中断发出事件,然后让任务去处理呢?主要是因为中断内处理的时候通常cpu是关中断的, 中断内处理时间过长的话会影响实时性。如果中断内处理是打开中断的,此中断也会屏蔽底优先级中断的相应,对于实时性同样是不利的。
所以对于任何rtos来说,降低中断内的处理时间是最为紧迫的事情之一。
同样任务和任务之间的同步也是一样的原理,后面章节会详细的阐述。
信号量同步
这一章主要讲解任务与任务以及任务与中断间的同步。
当一个中断执行的时候,它可以发一个事件信号给外面的任务。当中断发送事件信号给任务时,会唤醒睡眠中的任务。然后出中断,在合适的时间会运行这个被唤醒的任务,取决于这个任务的优先级高低。被唤醒的这个任务然后会处理和这个中断事件相关的数据,处理中断事件相关的数据在任务中的好处是,可以减少中断上半部关中断的时间,进而可以在中断下半部做事情,中断上半部在这里是指中断内处理的逻辑,中断下半部也就是指任务空间去处理中断相关数据的逻辑。
raw os 提供了两种同步机制,分别是semaphore(信号量)和event(事件标志)。
几乎所有的操作系统提供了信号量的支持,信号量可以被设计为保护共享资源即临界区的安全,但互斥锁mutex 往往更适合使用在保护共享资源,读者可以参考资源管理这一个章节。信号量更适合用在同步中断和任务或者任务和任务之间的同步。
下图是任务1阻塞在信号量上睡眠,中断与任务与之同步的情形。
file:///C:/Users/ASUS/AppData/Local/Temp/ksohtml/wps_clip_image-2646.png
在这样的情况下,任务2以及中断都能发送一个信号量唤醒任务1。raw os信号量的内部会采用一个计数器去计数,这个计数的范围是从0到0xffffffff。所以信号量的同步往往适用于能同步累加的情况下。raw os内部的软件定时器的实现就是采用了信号量的同步方法。信号量的同步一般都不涉及到数据的传递,比如有一个任务初始化了一个硬件模块,然后等待初始化完了中断通知,这个时候任务就可以阻塞在一个信号量上,然后中断释放信号量通知唤醒任务继续执行。
示例代码如下:
ISR {
raw_semaphore_put(&sem1); (1)
……………………
}
task 1{
raw_semaphore_get(&sem1); (2)
……………………………………………
}
task1 等待一个信号量在(2)处代码,直到中断isr释放了这个信号量后,才能task1继续运行。
信号量也可以用来保护临界区的资源,用来防止多任务一起处理共享资源,但是信号量无法解决临界区反转的问题,所以保护临界区资源使用mutex更合适,因为mutex解决了临界区反转的问题,请参阅mutex这一章节叙述。
信号量的一个重要特性是内部累加性质。如果一个中断连续调用raw_semaphore_put(&sem1),假设sem1上并没有阻塞的任务,sem1的内部计数器会为2, 这一点和mutex以及事件标志是不同的。
任务与任务之间的同步具体有什么作用呢。两个任务之间可以采用两个信号量来彼此同步。具体的演示代码如下:
void sem_sync_task1(void * pParam)
{
while (1) {
raw_semaphore_put(&sem1); (1)
do some thing...........
raw_semaphore_get(&sem2, RAW_WAIT_FOREVER); (2)
do some thing...........
}
}
void sem_sync_task2(void * pParam)
{
while (1) {
raw_semaphore_get(&sem1, RAW_WAIT_FOREVER); (3)
do some thing...........
raw_semaphore_put(&sem2); (4)
do some thing...........
}
}
假设sem_sync_task2先运行, 当运行到(3)处代码时会阻塞在sem1上。sem_sync_task1接着会运行,当运行到(1)处代码时会唤醒sem_sync_task2,然后接着运行(4)处代码使得sem2内部计数器为1, 接着继续运行到(3)处代码时会阻塞在sem1上,sem_sync_task1接着会运行(2)处代码,然后再重复循环之前的过程就可以实现两个任务之间的同步。
如果有两个任务task1和task2阻塞在同一个信号量上, 假设task1的优先级高于task2。如果task3 调用raw_semaphore_put释放一个信号量的话,将会是什么情况呢?
file:///C:/Users/ASUS/AppData/Local/Temp/ksohtml/wps_clip_image-21981.png
上图中task3 毫无疑问将唤醒task1, 因为task1排在task2之前。唤醒之后是如下情形:
file:///C:/Users/ASUS/AppData/Local/Temp/ksohtml/wps_clip_image-2595.png
可以清晰地看到task1已经被唤醒,不在阻塞队列上了,而task2仍旧在上面。
以上同步过程读者完全可以利用已经搭建好的VC运行环境机制来来验证上述的运行过程。
下面举一个例子,来演示信号量在串口驱动中应用的一个例子。
void uart_isr()
{
接收串口数据 (1)
压入软件fifo (2)
调用函数raw_semaphore_put来释放信号量 (3)
}
void uart_receive_task()
{
raw_semaphore_get 获取信号量 (4)
从fifo里面取出串口数据 (5)
处理串口数据 (6)
}
串口来了数据后会触发串口中断,进入中断后在(1)处接收串口数据,然后在(2)处把数据压入软件fifo, (3)处会释放一个信号量用来通知外面的串口接收任务有数据了。(4)处的串口任务接收到数据后会唤醒过来,然后(5)处从软件fifo里面把数据取出来,(6)处处理取出来后的串口数据。
任务私有信号量
raw os的任务带有一个私有的信号量,意味着任务可以直接发送信号量给任务。举例如下:
ISR {
raw_task_semaphore_put (&task1); (1)
……………………
}
task 1{
raw_task_semaphore_get(RAW_WAIT_FOREVER); (2)
……………………………………………
}
(1)处代码为中断向task1发出信号量, (2)处代码为任务等待中断发出的信号量。
信号量(Semaphore API)应用
1 RAW_U16 raw_semaphore_create(RAW_SEMAPHORE *semaphore_ptr, RAW_U8 *name_ptr, RAW_U32 initial_count)
函数功能:创建一个信号量供用户使用。
此函数的参数有3个,含义如下:
semaphore_ptr为信号量的实体地址。
name_ptr为信号量的名字。
initial_count为信号量的内部计数器初始值。
函数的返回值:
RAW_NULL_OBJECT:semaphore_ptr为空指针。
RAW_SEMAPHORE_OVERFLOW:信号量内部计数器超过0xffffffff。
RAW_SUCCESS 函数操作成功。
2 RAW_U16 raw_semaphore_put(RAW_SEMAPHORE *semaphore_ptr)
函数功能:释放一个信号量,如果任务阻塞在信号量上的话,会唤醒此任务。如果没有任务阻塞在此信号量上的话,则信号量内部计数器加1。
此函数的参数有1个,含义如下:
semaphore_ptr为信号量的实体地址。
函数的返回值
RAW_NULL_OBJECT: semaphore_ptr为空指针
RAW_ERROR_OBJECT_TYPE:
RAW_INT_MSG_EXHAUSTED:
RAW_TASK_0_EVENT_EXHAUSTED:
RAW_SUCCESS:
3 RAW_U16 raw_semaphore_put_all(RAW_SEMAPHORE *semaphore_ptr)
函数功能:释放信号到所有被阻塞的任务上。所有阻塞的任务都会被唤醒进入就绪队列,如果被唤醒的最高优先级任务优先级大于当前任务的话,会发生任务切换。
此函数的参数有1个,含义如下:
semaphore_ptr:为信号量的实体地址。
事件标志同步
raw os 的另外一种同步方式是采用事件标志位来同步。当一个任务需要和多种事件同步的时候可以使用事件标志来同步。一组事件标志有32个bit。一个任务可以同步任何一个事件的发生,这个时候就叫事件或的机制,也可以同步所有事件的发生,这个时候叫事件与的机制。
理论上raw os可以无限的创建事件标志,但是实践上内存制约了这一点。事件标志的代码在raw_event.c。
假设一个任务需要等待几个事件标志位满足才能继续运行,这种关系叫做与(and), 假如一个任务等待几个事件标志位的任何一个满足条件,就能运行,这种关系叫做或(or)的关系。
利用事件标志的特性,可以创建32个互斥的锁,在实践中极大地节约了创建锁的资源。这个特性会在书专门配套的视频中仔细讲解源码实现。
事件标志和信号量最大的区别是,信号量内部有一个累加器,但是事件标志是没有的,如果满足事件标志了就相当于状态机的状态转换。
file:///C:/Users/ASUS/AppData/Local/Temp/ksohtml/wps_clip_image-9648.png
上图演示的是任务2调用函数raw_event_get,因为事件标志位没有满足所以阻塞在该事件标志上。中断以及任务1都可以调用raw_event_set设置标志位,只要任务2等待的标志位一被设置,任务2马上会处于就绪状态,如果是处于就绪态的最高优先级,任务2会被马上调度运行。
事件标志API应用
1 RAW_U16 raw_event_create(RAW_EVENT *event_ptr, RAW_U8 *name_ptr, RAW_U32 flags_init)
函数功能:创建一组32位的事件标志位。
此函数的参数有3个,含义如下:
event_ptr是事件标志的控制块指针。
name_ptr是事件标志的名字。
flags_init是事件标志的初始标志,通常为0。
函数的返回值
RAW_NULL_OBJECT: event_ptr为空指针。
RAW_SUCCESS:事件标志创建成功。
2 RAW_U16 raw_event_get(RAW_EVENT *event_ptr, RAW_U32 requested_flags, RAW_U8 get_option, RAW_U32 *actual_flags_ptr, RAW_TICK_TYPE wait_option)
函数功能:如果任务得到满足条件的事件标识位就返回,否则阻塞住,直到事件标识位符合条件。
此函数的参数有5个,含义如下:
event_ptr:事件控制块的指针。
requested_flags:用户想等待的事件标志位。
get_option:有以下4钟参数可以选择:
RAW_AND
RAW_AND_CLEAR
RAW_OR
RAW_OR_CLEAR
对于RAW_AND, RAW_AND_CLEAR, 必须要全部满足事件标志位才能继续运行,否则阻塞运行。对于RAW_OR和RAW_OR_CLEAR只要有任何一位事件标志位符合条件的就运行。
actual_flags_ptr:为用户得到满足条件后的事件标志位时,内部的flag的真实值。这个值返回,等到用户处理时可能会包含更多置位的事件标志,因为用户处理时可能会发生中断,中断里面也可以设置事件标志位。
wait_option 为具体的超时函数,有以下选择:
RAW_NO_WAIT 得不到事件标志位时立马返回
0x1-0xfffffffe 为超时时间,单位为系统tick,得不到事件标志位时,经过一定的超时时间会返回。
RAW_WAIT_FOREVER 得不到事件标志位时会永远阻塞住。
多任务的同步
多任务之间的同步,在有的场合使用也是必要的。使用信号量的广播机制能很好地简化实际问题。
对于复杂一点多任务同步可以依靠信号量的广播以及事件标志位的同步去解决问题。
file:///C:/Users/ASUS/AppData/Local/Temp/ksohtml/wps_clip_image-29465.png
现在以上图中所示为例子具体说明多任务之间是如何来同步的。
假设任务1的优先级低于任务2和任务3以及任务4,而且4个任务均处于就绪状态。
假设任务2首先运行,在1处阻塞在信号量上,任务3在2处阻塞在同一个信号量上,任务4在3处阻塞同一个在信号量上。此时轮到任务1运行,任务1广播信号量后,唤醒任务2,任务3以及任务4。这些唤醒的任务然后做各自的事情,等到事情都做完之后就各自去设置事件标志位。任务1广播信号量后就会阻塞在事件标志位对象上,等待任务2,任务3,任务4设置完事件标志位,很明显这个事件标志位是与(and)的关系,只有都设置了,任务1才能运行,然后再去广播信号量。
上面的过程演示了任务1与任务2,任务3,任务4之间的同步,这种同步关系比较复杂,但是在实战过程中是很可能使用到的技巧。