目录
同步概念:
互斥概念:
临界区概念:
任务时间概念:
信号量概念:
互斥信号量概念:
事件标志组概念:
消息邮箱和消息梯队概念:
内存管理概念:
如何从裸机开发转跳到OS编程:
轮询系统:
前后台系统:
多任务系统:
同步概念:
任务是独立的。有时候一个任务完成的前提是需要另一个任务给出一个结果,任务之间的这种制约性的合作运行机制叫做任务间的同步。
怎么来同步呢?
首先,信号量初始化成0(用来实现同步机制),接着,B任务开始就请求信号量,但是此时信号量的值为0,只能等待。然后A任务在完成任务后在输出信号量,B接受到信号量开始执任务。
如果内核支持计数式信号量,信号量的值表示尚未得到处理的事件数。请注意,可能会有一个以上的任务在等待同一事件的发生,则这种情况下内核会根据以下原则之一发信号给相应的任务:
- 发信号给等待事件发生的任务中优先级最高的任务,或者
- 发信号给最先开始等待事件发生的那个任务
根据不同的应用,发信号以标识事件发生的中断服务或任务也可以是多个
互斥概念:
互斥条件:实现任务之间通讯最简单的办法就是使用任务共享数据结构(类似于裸机的全局变量)。特别是当所有的任务都在一个单一的地址空间下,使用全局变量、指针、缓冲区、链表、循环缓冲区等,使用共享数据结构就容易理解。虽然共享数据区法简化了任务之间的信息交换,但是必须保证每一个任务在处理共享数据时候的排他性,以避免竞争和数据破坏。
与共享资源打交道时,使用满足互斥条件的一般方法有:
- 关中断
- 禁止任务切换
- 利用信号量
临界区概念:
代码的临界段也成为临界区,指处理时不可分割的代码。例如:每个任务中访问共享资源部分代码。这一部分一旦执行,则不允许任何中断打断,因为共享资源的访问是互斥的。在临界区不允许任务切换,这是最基本的原则。
为确保临界代码的执行,在进入临界区访问共享资源之前,采用关中断给调度器上锁,使用信号量等方式,达到互斥的目的。而在代码执行后要立即开启,中断,解锁以及释放信号量。
任务时间概念:
事件(Event)就是在操作系统运行过程中发生的事情。例如任务被挂起、唤醒、创建等。 μC/OS操作系统在处理任务的同步和通信等环节,大量的使用了事件这一概念,创建了事件控制块这样的数据结构以进行事件的管理。
信号量概念:
在一个时刻有些共享资源只可以被一个任务所占用,而有些可以被N多个任务所占用,前一种共享资源就好比有一把钥匙,钥匙发出去了,得到钥匙的任务可以访问共享资源,其他请求该资源的任务必须等得到钥匙的任务把钥匙归还。后者则可以有N把钥匙,如果N把钥匙都发完了,第N+1个请求访问共享资源的任务就必须等待。这些钥匙就可以用信号量(semaphore)来表示。
信号量标志了共享资源的有效可被访问数量,于是要获得共享资源的访问权,就首先要得到信号量这把钥匙。
信号量的三种操作:
1、建立(create)
建立并初始化信号量,执行的是给资源配置钥匙的操作。
2、请求(pand):
请求信号,如果还有钥匙(信号量大于0)就去领一把钥匙(信号量)执行下去,如果没有就把自己堵塞。
3、释放(post):
访问资源的操作完成之后就要把钥匙(信号量)交回,这时,如果有等待该钥匙的任务就绪,并比当前任务有更高的优先级,就执行任务调度。否则原任务在释放信号量后继续执行。
见图:
要与同一共享数据打交道的任务调用等待信号量函数OSSemPend()。处理完共享数据以后再调用释放信号量函数OSSemPost()。
在使用信号量之前,一定要对该信号量做初始化。作为互斥条件,信号量初始化为1。使用信号量处理共享数据不增加中断延迟时间,如果中断服务程序或当前任务激活了一个高优先级的任务,高优先级的任务立即开始执行。
伪代码:
sem = 1;// 信号量初始化(建立) 任务函数(void) { while(1) { OSSemPend();//请求 /* 对共享数据进行处理 */ OSSemPost();//释放 } }
我的理解是可以把信号量比作一个标志位,有它在其他任何中断不不能打断当前任务的执行。
互斥信号量概念:
互斥信号量是一种特殊的信号量,这不仅在于该信号量只用于互斥资源的访问,还在于使用互斥信号量管理需要解决的“优先级反转”问题。(优先级反转:CPU不去执行那些优先级高的任务反而去执行那些低优先级的任务。)
假设有三个任务,优先级从高到底分别是任务H(high)M(middle)L(low)。
任务H和任务L使用互斥信号量访问同一临界资源,任务M不使用临界资源。
- L任务正在使用临界资源,(信号量加锁),H任务被唤醒,执行H任务,但此时L任务还没有执行完,此时的临界资源还没有被释放,(互斥信号量还处于加锁状态,即使优先级更高的任务也无法访问这个临界资源)。
- 这个时刻H任务也要对该临界资源进行访问,但L任务还未释放资源,由于保护机制,H任务进入阻塞态,L任务得以继续运行,此时已经发生了优先级翻转现象。
- 某个时刻M任务被唤醒,由于M任务的优先级高于工任务,M任务抢占了CPU的使用权,M任务开始运行,此时L任务尚未执行完,临界资源还没被释放。
- M任务运行结束,归还CPU使用权,L任务继续运行。
- L任务运行结東,释放临界资源(信号量解锁),H任务得以对资源进行访问,H任务开始运行。
那到底什么是优先级反转呢?
我的理解是在低优先级中还有高优先级需要运行的条件,所以就从高优先级退到低优先级中(个人理解)。
事件标志组概念:
在信号量和互斥信号量的管理中,任务请求资源,如果资源未被占用就可继续运行,否则只能阻塞,等待资源释放的事件发生。
这种事件是单一的事件。如果任务要等待多个事件的发生,或多个事件中的某一个事件的发生就可以继续运行,那么就应该采用事件标志组管理。 事件标志组管理的条件组合可以是多个事件都发生,也可以是多个事件中有任何一个事件发生。尤其特别的是,还可以是多个事件都没有发生或多个事件中有任何一个事件没有发生。
消息邮箱和消息梯队概念:
uCOSIII中已经没有了消息邮箱这个说法;
有时很需要任务间的或中断服务与任务间的通讯。这种信息传递称为任务间的通讯。任务间信息的传递有两个途径:通过全程变量或发消息给另一个任务。
用全程变量时,必须保证每个任务或中断服务程序独享该变量。如果两个任务共享某变量,各任务实现独享该变量的办法可以是关中断再开中断,或使用信号量(如前面提到的那样)。请注意,任务通过全程变量与中断服务程序通讯,而任务并不知道什么时候全程变量被中断服务程序修改了,除非中断程序以信号量方式向任务发信号或者是该任务以查询方式不断周期性地查询变量的值 。要避免这种情况,用户可以考虑使用邮箱或消息队列。邮箱(MailBox)很明显是用于通信的,邮箱中的内容一般是信件。操作系统也通过邮箱来管理任务间的通信与同步,**发送和接收消息的任务约定,传递的消息实际上是传递的指针指向的内容**。意思就是邮箱中的内容却不是信件本身,而是指向消息内容的地址!这个指针是void类型的(指针占4个字节),可以指向任何的数据结构。因而这样的设计更经济,所发送的信息范围也就更宽,邮箱中可以容纳下任何长度的数据了。
消息队列(message queue)也用于给任务发消息,但是它是由多个消息邮箱组合形成的,是消息邮箱的集合,实质上是消息邮箱的队列,一个消息邮箱只能容纳一条消息。采用消息队列,一是可以容纳多条消息,二是消息是有序的 。通常,先进入消息队列的消息先传给任务,也就是说,任务先得到的是最先进入消息队列的消息,**即先进先出原则(FIFO)。然而μC/OS-Ⅱ也允许使
用后进先出方式(LIFO)。**像使用邮箱那样,当一个以上的任务要从消息队列接收消息时,每个消息队列有一张等待消息任务的等待列表(Waiting List)。如果消息队列中没有消息,即消息队列是空,等待消息的任务就被挂起并放入等待消息任务列表中,直到有消息到来。通常,内核允许等待消息的任务定义等待超时的时间。如果限定时间内任务没有收到消息,该任务就进入就绪态并开始运行,同时返回出错代码,指出出现等待超时错误。一旦一则消息放入消息队列,该消息将传给等待消息的任务中优先级最高的那个任务,或是最先进入等待消息任务列表的任务。
内存管理概念:
μC/OS-II中,采用分区的方式管理内存,即将连续的大块内存按照分区来管理,每个系统中有数个这样的分区, 每个分区又包含数个内存块,每个内存块的大小相同。这样,再分配内存的时候,根据需要从不同的分区中得到数个内存块,而在释放时,这些内存块重新释放回他们原来所在的分区。这样就不回产生内存越分越乱,没有整块连续分区的问题了。
如何从裸机开发转跳到OS编程:
裸机系统通常分成轮询系统和前后台系统
轮询系统:
轮询系统即是在裸机编程的时候,先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地做各种事情。最常用的应该就是while(1){};。轮询系统是一种非常简单的软件结构,通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动的就能完成的事情。在代码清单 中,如果只是实现 LED 翻转,串口输出,液晶显示等这些操作,那么使用轮询系统将会非常完
美。但是,如果加入了按键操作等需要检测外部信号的事件,用来模拟紧急报警,那么整个系统的实时响应能力就不会那么好了。//伪代码 int main () { //硬件相关初始化 HardwareInit(); while(1) { Dosomething1(); Dosomething2(); Dosomething3(); } }
假设DoSomething3 是按键扫描,当外部按键被按下,相当于一个警报,这个时候,需要立马响应,并做紧急处理,而这个时候程序刚好执行DoSomething1,要命的是 DoSomething1需要执行的时间比较久,久到按键释放之后都没有执行完毕,那么当执行到 DoSomething3的时候就会丢失掉一次事件。足见,轮询系统只适合顺序执行的功能代码,当有外部事件驱动时,实时性就会低。
前后台系统:
相比轮询系统,前后台系统是在轮询系统的基础上加入了中断。外部事件响应在中断里面完成,事件的处理还是回到轮询系统中完成,其中中断在这里我们称之为前台,main函数里边的无限循环我们称之为后台。并且把外部的紧急事件放到中断里处理。
多任务系统:
相比前后台系统,多任务系统的事件响应也是在中断中完成的,但是事件的处理是在任务中完成的(裸机是在中断中完成,操作系统在任务中完成由CPU去任务调度)。在多任务系统中,任务跟中断一样,也具有优先级,优先级高的任务回被优先的执行,当一个紧急的事件在中断被标记之后,如果事件对应的优先级足够高,就会立马响应。相比前后台系统,多任务系统的实时性又被提高了。
//伪代码(基本代码框架) int flag = 0;//全局变量 int main(void)//主函数 { init();//硬件初始化 osinit();//操作系统OS初始化 osstart();//开启操作系统OS } void ISR1()//中断处理函数 { flag = 1; } void task1(void)//任务函数 { Dosmoesting1();//任务实体 } void task2(void)//负责中断的任务 { while(1) { if(flag=1) Dosmoesting2();//任务实体 } }
相比较前后台系统中后台顺序执行程序主体,在多系统中,根据程序的功能我们把程序主体分割成一个个独立的,无限循环并且不能返回的任务,每个任务都是独立的互不干扰的,且具备优先级,由操作系统调度管理。加入操作系统后,我们在编程的时候不需要精心地去设计程序的执行流不用担心每个功能模块之间是否存在干扰。整个系统随之带来的额外开销就是操作系统占据的那一丁点的 FLASH 和RAM。现如今,单片机的 FLASH 和 RAM 是越来越大,完全足以抵挡 RTOS 那点开销。