资料目录
uCOS专区
什么是信号量?
发布日期:2011/11/27
信号量是60年代中期Edgser Dijkstra 发明的。信号量实际上是一种约定机制,在多任务内核中普遍使用.信号量用于:
l 控制共享资源的使用权(满足互斥条件)
l 标志某事件的发生
l 使两个任务的行为同步
(译者注:信号与信号量在英文中都叫做Semaphore,并不加以区分,而说它有两种类型,二进制型(binary)和计数器型(counting)。本书中的二进制型信号量实际上是只取两个值0和1的信号量。实际上 这个信号量只有一位,这种信号量翻译为信号更为贴切。而二进制信号量通常指若干位的组合。而本书中解释为事件标志的置位与清除(见2.21))。
信号像是一把钥匙,任务要运行下去,得先拿到这把钥匙。如果信号已被别的任务占用,该任务只得被挂起,直到信号被当前使用者释放。换句话说,申请信号的任务是在说:“把钥匙给我,如果谁正在用着,我只好等!”信号是只有两个值的变量,信号量是计数式的。只取两个值的信号是只有两个值0和1的量,因此也称之为信号量。计数式信号量的值可以是0到255或0到65535,或0到4294967295,取决于信号量规约机制使用的是8位、16位还是32位。到底是几位,实际上是取决于用的哪种内核。根据信号量的值,内核跟踪那些等待信号量的任务。
一般地说,对信号量只能实施三种操作:初始化(INITIALIZE),也可称作建立(CREATE);等信号(WAIT)也可称作挂起(PEND);给信号(SIGNAL)或发信号(POST)。信号量初始化时要给信号量赋初值,等待信号量的任务表(Waiting list)应清为空。
想要得到信号量的任务执行等待(WAIT)操作。如果该信号量有效(即信号量值大于0),则信号量值减1,任务得以继续运行。如果信号量的值为0,等待信号量的任务就被列入等待信号量任务表。多数内核允许用户定义等待超时,如果等待时间超过了某一设定值时,该信号量还是无效,则等待信号量的任务进入就绪态准备运行,并返回出错代码(指出发生了等待超时错误)。
任务以发信号操作(SIGNAL)释放信号量。如果没有任务在等待信号量,信号量的值仅仅是简单地加1。如果有任务在等待该信号量,那么就会有一个任务进入就绪态,信号量的值也就不加1。于是钥匙给了等待信号量的诸任务中的一个任务。至于给了那个任务,要看内核是如何调度的。收到信号量的任务可能是以下两者之一。
l 等待信号量任务中优先级最高的,或者是
l 最早开始等待信号量的那个任务,即按先进先出的原则(First In First Out ,FIFO)
有的内核有选择项,允许用户在信号量初始化时选定上述两种方法中的一种。但μC/OS-Ⅱ只支持优先级法。如果进入就绪态的任务比当前运行的任务优先级高(假设,是当前任务释放的信号量激活了比自己优先级高的任务)。则内核做任务切换(假设,使用的是可剥夺型内核),高优先级的任务开始运行。当前任务被挂起。直到又变成就绪态中优先级最高任务。
程序清单2.7示意在μC/OS-Ⅱ中如何用信号量处理共享数据。要与同一共享数据打交道的任务调用等待信号量函数OSSemPend()。处理完共享数据以后再调用释放信号量函数OSSemPost()。这两个函数将在以后的章节中描述。要注意的是,在使用信号量之前,一定要对该信号量做初始化。作为互斥条件,信号量初始化为1。使用信号量处理共享数据不增加中断延迟时间,如果中断服务程序或当前任务激活了一个高优先级的任务,高优先级的任务立即开始执行。
|
程序清单2.7 通过获得信号量处理共享数据 |
|
OS_EVENT *SharedDataSem; |
|
void Function (void) |
|
{ |
|
INT8U err; |
|
OSSemPend(SharedDataSem, 0, &err); |
|
. |
|
. /* You can access shared data in here (interrupts are recognized) */ |
|
. /*共享数据的处理在此进行,(中断是开着的)*/ |
|
OSSemPost(SharedDataSem); |
|
} |
当诸任务共享输入输出设备时,信号量特别有用。可以想象,如果允许两个任务同时给打印机送数据时会出现什么现象。打印机会打出相互交叉的两个任务的数据。例如任务1要打印“I am Task!”,而任务2要打印“I am Task2!”可能打印出来的结果是:“I Ia amm T Tasask k1!2!”
在这种情况下,使用信号量并给信号量赋初值1(用二进制信号量)。规则很简单,要想使用打印机的任务,先要得到该资源的信号量。图2.10两个任务竞争得到排它性打印机使用权,图中信号量用一把钥匙表示,想使用打印机先要得到这把钥匙。

图2.10用获取信号量来得到打印机使用权
上例中,每个任务都知道有个信号表示资源可不可以使用。要想使用该资源,要先得到这个信号。然而有些情况下,最好把信号量藏起来,各个任务在同某一资源打交道时,并不知道实际上是在申请得到一个信号量。例如,多任务共享一个RS-232C外设接口,各任务要送命令给接口另一端的设备并接收该设备的回应。如图2.11所示。
调用向串行口发送命令的函数CommSendCmd(),该函数有三个形式参数:Cmd指向送出的ASCII码字符串命令。Response指向外设回应的字符串。timeout指设定的时间间隔。如果超过这段时间外设还不响应,则返回超时错误信息。函数的示意代码如程序清单2.8所示。
|
程序清单 2.8 隐含的信号量。 |
|
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); |
|
} |
|
} |
要向外设发送命令的任务得调用上述函数。设信号量初值为1,表示允许使用。初始化是在通讯口驱动程序的初始化部分完成的。第一个调用CommSendCmd()函数的任务申请并得到了信号量,开始向外设发送命令并等待响应。而另一个任务也要送命令,此时外设正“忙”,则第二个任务被挂起,直到该信号量重新被释放。第二个任务看起来同调用了一个普通函数一样,只不过这个函数在没有完成其相应功能时不返回。当第一个任务释放了那个信号量,第二个任务得到了该信号量,第二个任务才能使用RS-232口。

图2.11在任务级看不到隐含的信号量
计数式信号量用于某资源可以同时为几个任务所用。例如,用信号量管理缓冲区阵列(buffer pool),如图2.12所示。缓冲区阵列中共有10个缓冲区,任务通过调用申请缓冲区函数BufReq()向缓冲区管理方申请得到缓冲区使用权。当缓冲区使用权还不再需要时,通过调用释放缓冲区函数BufRel()将缓冲区还给管方。函数示意码如程序清单2.9所示
|
程序清单 2.9 用信号量管理缓冲区。 |
|
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; |
|
} |

图2.12 计数式信号量的用法
缓冲区阵列管理方满足前十个申请缓冲区的任务,就好像有10把钥匙可以发给诸任务。当所有的钥匙都用完了,申请缓冲区的任务被挂起,直到信号量重新变为有效。缓冲区管理程序在处理链表指针时,为满足互斥条件,中断是关掉的(这一操作非常快)。任务使用完某一缓冲区,通过调用缓冲区释放函数BufRel()将缓冲区还给系统。系统先将该缓冲区指针插入到空闲缓冲区链表中(Linked list)然后再给信号量加1或释放该信号量。这一过程隐含在缓冲区管理程序BufReq()和BufRel()之中,调用这两个函数的任务不用管函数内部的详细过程。
信号量常被用过了头。处理简单的共享变量也使用信号量则是多余的。请求和释放信号量的过程是要花相当的时间的。有时这种额外的负荷是不必要的。用户可能只需要关中断、开中断来处理简单共享变量,以提高效率。(参见2.18.0.1 关中断和开中断)。假如两个任务共享一个32位的整数变量,一个任务给这个变量加1,另一个任务给这个变量清0。如果注意到不管哪种操作,对微处理器来说,只花极短的时间,就不会使用信号量来满足互斥条件了。每个任务只需操作这个任务前关中断,之后再开中断就可以了。然而,如果这个变量是浮点数,而相应微处理器又没有硬件的浮点协处理器,浮点运算的时间相当长,关中断时间长了会影响中断延迟时间,这种情况下就有必要使用信号量了。