uC/OS-II系统学习笔记(2)——实时操作系统概念中篇

1 时间片轮番调度法

当两个任务拥有同样优先级的时候,内核允许一个任务先运行一段确定的时间,然后再把CPU控制权给另一个任务运行一段确定的时间。这个确定的时间是有规定时长的,叫做时间额度(quantum)。这种调度法则,被称作时间片轮番调度法。
该法则下CPU移交控制权实际上有两种情况:
1.当前任务已经无事可做了;
2.当前任务还没完成,但是已经到了时间额度。

2 任务的优先级

任务的优先级分为静态优先级和动态优先级。所谓静态优先级,就是程序执行过程中,任务的优先级已经提前设定并有其时间约束,这个优先级是在编译完成就确定了的。而动态优先级是在任务执行中,通过调用某些函数可以改变某些任务的优先级。

3 优先级反转

优先级反转,就是两个任务的优先级对调了,注意这并不是程序员本身想实现的功能,而是因为某些情况导致两个任务执行顺序不对,违背了优先级法则的问题,严格来说是一种错误。

如下图所示即为优先级反转的情况。有三个任务Task1,Task2,Task3。其中Task1优先级最高,其次是Task2,再是Task3优先级最低。并且已知Task1和Task3有一个共用且互斥的资源。根据图中的序号为按时间发展的顺序。
01)Task1、Task2和Task3同时被挂起;
02)Task3首先进入就绪态,获得了信号量,即Task1和Task3共用的资源;
03)Task1进入就绪态,抢占了Task3并开始运行;
04)Task1准备运行,Task3被挂起;
05)Task1请求资源,但发现资源不可用(被Task3占有),挂起等待
06)Task3重新获得了CPU控制权,继续运行;
07)Task2进入就绪态,抢占了Task3并开始运行;
08)Task3被挂起,持续到Task2运行结束;
09)Task2运行结束释放CPU控制权,Task3获得CPU控制权开始继续运行;
10)Task3运行直到结束;
11)Task3运行结束,并释放了与Task1共用的资源;
12)Task1终于等待到资源可用,并开始运行。
由此可见,虽然Task1优先级比Task2高,但是由于Task3一直占有共用资源导致Task1持续被挂起等待。更尴尬的是,由于Task2优先级高于Task3,使得Task1在等待Task3运行结束释放资源时,Task3又被挂起等待,这使得Task1的状况更加的恶劣。从这里看,Task1的优先级被拉低到和Task3一个等级,低于Task2,而Task1和Task2之间就是优先级反转。

这里写图片描述

优先级反转的解决办法,就是在Task3使用共享资源时,临时拉高Task3的优先级,使得Task3能够及时处理完并释放资源,而不会被Task1和Task3之间优先级的任务拖长时间。

但是Task1发现Task3优先级过低时,拉高Task3的优先级,Task3运行结束释放资源后再次拉低回原来的优先级,在优先级改变过程中时间较长,也同样影响系统的性能。故有些实时操作系统具有优先级继承(Priority Inheritance)的功能,即内核能够自动变换任务的优先级。但可惜uC/OS-II并没有优先级继承。

而具有优先级集成的系统,工作原理如下图所示。区别在于(6)中当Task1请求资源发现资源被Task3占有时,Task3任务临时提高优先级(7)得以继续运行而不会被Task2阻塞。

这里写图片描述

4 任务优先级的分配

任务优先级的分配是一件比较让人头大的事情,要考虑各个任务的实时性,以及各个任务之间可能存在的优先级反转等情况。我看的教程中给出了一个比较简单的任务优先级分配的技术,称作单调执行率调度法RMS(Rate Monotonic Scheduling)。这种方法是基于哪个任务执行的次数最频繁来决定优先级,执行次数多的频繁的任务拥有高优先级。

使用这个分配法则需要满足很多条件,个人感觉有一点比较重要,就是要求每个任务之间都是独立的,不依赖与其他任务的请求开始和完成,也不与其他任务共用资源。这个前提条件个人感觉已经比较苛刻,毕竟这只是其中一种优先级分配法则,不想详细做笔记,有需要的另外查吧。

5 互斥条件

前面有讲到多个任务可以共用一个资源,但是共用一个资源要保证互斥,就是一个任务正在使用时不允许另一个任务中间插一脚改变该资源。而如何避免其他任务的干涉,在此有四种解决办法。

5.1 关中断和开中断

任务的调度可以说都是由中断产生的,中断结束后CPU会选取就绪任务中优先级最高的任务来执行。包括系统节拍也是由中断产生,然后进行任务的调度,所以,想要避免其他任务干涉的办法之一,就是不进行任务调度,把中断全部关死。

在uC/OS-II系统代码中提供了关中断和开中断的两个函数OS_ENTER_CRITICAL()和 OS_EXIT_CRITICAL()。处理共享数据时把中断关闭,处理完后再把中断打开。但是值得注意的是,这个中断关闭的时间不宜过长,只能完成一些简单的操作,因为它会影响整个系统的中断相应时间,即中断延迟时间。这种处理共享数据的方法是在中断服务子程序中处理共享变量和共享数据结构的唯一办法。但任意情况,关闭中断的时间都要尽可能的短。

一般来说,关中断的时间不超过内核本身的关中断时间,就不会影响系统中断延迟。至于内核里中断关了多久,一般好的实时系统内核,都会提供这方面的数据。

5.2 测试并置位

测试并置位(TAS, Test-And-Set)说白了就是用if语句。先规定好,有一个全局变量flag来表示某一个变量是否可用,假设flag为1表示可用,flag为0表示不可用。每次要先判断flag的 值是否为1,是的话就先把flag置0,然后进行操作,操作完成再把flag置1;否的话就跳过。

需要注意的是,这个判断语句有可能是一条单独的指令不会被中断打断,也有可能是会被中断打断的。所以在判断时要先关中断,判断完不论什么结果都要先关中断再根据结果进行操作。如下即为TAS方法处理共享数据。

Disable interrupts; 
if (‘Access Variable’ is 0) {
Set variable to 1;
Reenable interrupts;
Access the resource;
Disable interrupts;
Set the ‘Access Variable’ back to 0;
Reenable interrupts;
} else {
Reenable interrupts;
/* You don’t have access to the resource, try back later; */
}

5.3 禁止,然后允许任务切换

有点类似5.1里的禁止中断,只是这里没有那么绝,中断可以有,但是中断完了不管谁的优先级最高,任务都不会被切换,也就是任务调度被失能了。利用uC/OS-II里的函数就可以实现,见下代码段。

void Function (void)
{
OSSchedLock();
.
. /* You can access shared data in here (interrupts are recognized) */
OSSchedUnlock();
}

5.4 信号量

前面三个都是比较简单的,个人觉得信号量还是一个相对复杂而且比较重要的,但是同样的信号量也比以上三种办法要多变且有效的多。

信号量是 60 年代中期 Edgser Dijkstra 发明的。信号量实际上是一种约定机制,在多
任务内核中普遍使用.信号量用于:
控制共享资源的使用权(满足互斥条件)
标志某事件的发生
使两个任务的行为同步

所谓信号或者信号量,英文即Semaphore。Semaphore有二值和N值两种。二值就是只有0和1;多值就是有值0~255,0~65535,0~4294967295,是根据信号量规约机制使用的是8位,16位,还是32位决定。而二值的Semaphore为二进制型(Binary),N值的Semaphore为计数器型(Counting)。

有的会把信号称作二值的Semaphore,把信号量称作N值的Semaphore。其实没啥大的区别,我还是喜欢统一叫信号量。

对信号量的操作只有三种方法,分别是初始化(INITIALIZE)或创建(CREATE),等待信号(WAIT)或挂起(PEND),给信号(SIGNAL)或发信号(POST)。个人比较喜欢CREATE,PEND,POST。而在对信号量初始化时,要给信号量赋初值,等待信号量的任务列表(Waiting List)为空。

信号量创建好之后,PEND和POST就是对信号量的使用,使用PEND就表示请求一个信号量,POST就表示释放所占有的信号量。每次PEND请求到一个信号量,信号量的值就会减1,当信号量的值为0时表示所有资源都被占用。对于二值信号量来说,一旦被请求减一了就变成0,不可用了,直到信号量再还回来。个人觉得二值信号量只是一个特殊的N值信号量。

当一个任务使用完之后POST出信号量后,要是没有任务等待信号量,那么信号量就进行加一,若有任务在等待,那么任务就会进入就绪态,信号量不进行加一。但是当有多个任务等待信号量时,收到信号量的任务可能是以下两者之一
1,等待信号量的任务列表中优先级最高的任务
2,最早开始等待信号量的那个任务,即FIFO原则

需要注意的是,有些实时操作系统内核允许用户在初始化信号量时选定上述的一种选择方法,但是在uC/OS-II系统内核中,是以优先级为尊(写到这有点想吐槽uC/OS-II到底有多精简,这没有那没有的)。

5.4.1 二值信号量

如以下代码段,即信号量的一种使用方法,要注意信号量在使用前要进行初始化的,作为互斥条件,信号量初始化值为1(是不是N值信号量初始化值为N?到底怎么进行初始化?我暂时也没看到,后面再说)。

OS_EVENT *SharedDataSem;
void Function (void)
{
INT8U err;
OSSemPend(SharedDataSem, 0, &err);
.
. /* You can access shared data in here (interrupts are recognized) */
. 
OSSemPost(SharedDataSem);
}

还有个例子,叫什么隐含信号量。就是说我虽然用了信号量,但是我可能不知道我用了信号量,典型的就如下,信号量是在调用的函数中的。多个任务共用一个串口发送数据,在串口初始化时就完成了对二值信号量的初始化,在发送数据时需要调用该函数,而该函数中需要请求信号量,否则任务就被挂起。嗯。。还是挺好用的,比TAS好用,我还可以自定义等待信号量的挂起时间。

INT8U CommSendCmd(char *cmd, char *response, INT16U timeout)
{
Acquire port's semaphore;
Send command to device;
Wait for response (with timeout);
if (timed out) {
    Release semaphore;
    return (error code);
    } else {
    Release semaphore;
    return (no error);
    }
}

5.4.2 N值信号量

说实话N值信号量理解是理解了,但是举得例子我之前没怎么仔细看。用信号量管理缓冲区阵列(Buffer Pool)。

嗯。。好像看懂了。。

如下代码段,我用一个N值信号管理缓冲区阵列中的10个缓冲区。还是很好理解的,10个缓冲区嘛,利用BufReq()来申请一个缓冲区的使用权,申请到了就用,申请不到就等;用完之后再用BufRel()交出缓冲区的使用权。这里缓冲区BUF用的是一个单向列表结构。

BUF *BufReq(void)
{
    BUF *ptr;
    Acquire a semaphore;
    Disable interrupts;
    ptr = BufFreeList;
    BufFreeList = ptr->BufNext;
    Enable interrupts;
    return (ptr);
}
void BufRel(BUF *ptr)
{
    Disable interrupts;
    ptr->BufNext = BufFreeList;
    BufFreeList = ptr;
    Enable interrupts;
    Release semaphore;
}

6 死锁(或抱死)

假如两个任务T1和T2共用两个资源S1和S2,现在T1占有S1,并请求资源S2,而T2占有S2,并请求S1。这样的话两个任务各自占有对方请求的资源,又同时因为等待另一个资源而被挂起的,导致程序无法正常执行,就是死锁。

解决死锁的办法:

  1. 先得到全部的资源再做下一步工作
  2. 用同样的顺序去申请资源
  3. 用与申请资源相反的顺序去释放资源

一般嵌入式系统出现死锁可能性还是比较小的,在大型操作系统容易出现死锁现象。就不多说了。

7 同步

同步分为单向同步(unilateral rendezvous)和双向同步(bilateral rendezvous),到底同步的定义是什么我也不是特别清楚,大致理解就是一个任务给出一个信号,另一个任务接收到信号后就可以执行了。这个信号就可以用二值信号量或N值信号量来表示,这里的二值信号量不表示互斥概念,只是作为一个标志。

如下图为单向同步,小旗子表示一个信号量,由中断或一个任务抛出一个信号量,另一个任务接收到这个信号量后开始执行。

这里写图片描述

如下图为双向同步,只不过双向同步只出现在任务对任务中,不能有中断,因为中断是不能接收信号量的。

这里写图片描述

Task1()
{
    for (;;) {
    Perform operation;
    Signal task #2;                 (1)
    Wait for signal from task #2;   (2)
    Continue operation;
    }
}
Task2()
{
    for (;;) {
    Perform operation;
    Signal task #1;                 (3)
    Wait for signal from task #1;   (4)
    Continue operation;
    }
}

8 事件标志

当某任务需要同多个事件同步时,需要用到事件标志(Event Flag)。若任务与任何事件之一发生同步,称作独立型同步(Disjunctive Synchronization )(即逻辑或关系);若任务可以与若干事件发生同步,称作关联型同步(Conjunctive Synchronization )(即逻辑与关系)。个人觉得逻辑或和逻辑与比较好理解。

这里写图片描述

啊。。了解一下就行,反正同样的uC/OS-II也不支持事件标志。

9 任务间的通讯

有时候需要任务间的或者中断和任务间的通讯,这种消息的传递称作任务间的通讯(Intertask Communication)。任务间信息的传递有两个途径,一是全局变量,二是发消息给另一个任务。

使用全局变量时,必须保证每个任务或中断使用变量时是独享该变量的。中断服务共享该变量的唯一办法是关中断。而任务间全局变量的共享可以使关中断,也可以利用信号量。

然而值得注意的一点,任务和中断共享数据时,只能通过关中断来保证互斥。而这样的话任务并不知道什么时候中断把这个变量改变了的。除非,中断以信号量的形式给任务发信号,或者任务不断的去查询变量的值。要避免这样的情况,可以考虑用邮箱或消息队列。

10 消息邮箱

典型的消息邮箱(Message Mail Boxes)也称作交换消息。一个任务或者是一个中断,通过内核服务把一则消息(即一个指针)放到邮箱里去。然后,一个或者多个任务,就可以通过内核服务接收邮箱里的这则消息。

每个邮箱,都有等待自己消息的任务列表,要得到该邮箱消息的任务发现邮箱是空的时候,就会被挂起,并记录到等待消息的任务表中,直到收到消息。一般的,内核允许用户自定义等待超时时间,如果一定时间后还没有等到消息,那么返回一个出错信息,任务直接进入就绪态。现在还不太清楚如果等待信号量超时会怎么样,任务是被休眠了还是也进入就绪态?以后再看吧。

内核一般提供几个邮箱服务

  1. 邮箱内消息的初始化,最初可以有个初值,也可以没有
  2. 将消息放入邮箱(POST)
  3. 任务等待消息,若为空则挂起等待(PEND)
  4. 若邮箱有消息就接收,没有直接返回是否收到消息不被挂起(ACCEPT)

11 消息队列

消息队列(Message Queue)用于给任务发消息,消息队列实际上是邮箱阵列。按我的理解有点类似于整型和整形数组的区别吧,邮箱是一个邮箱,队列是一串邮箱。

和邮箱一样,也是通过指针的形式来表示消息的,任务先得到的消息往往是队列先填入的消息,也就是先进先出FIFO了。当然uC/OS-II也支持后进先出(LIFO)。

大体上和邮箱是一个道理了,无非多了一个FIFO或者是LIFO。内核提供的服务也和邮箱类似

  1. 消息队列初始化,和邮箱不同的是,这个初始化总是清为空的
  2. 放一则消息到队列中去(POST)
  3. 等待一则消息的到来(PEND)
  4. 有消息就取走消息,没有消息则用特别代码通知调用者队列无消息(ACCEPT)

有一点疑问,无论是邮箱还是队列,获得消息之后,那消息,也就是指针,是被清空为NULL,还是不动,怎么表示消息被取走了,是还有另外的标志位来表示吗?

这篇写到这吧先,三天时间每天晚上看一点写一点。路还很长。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值