AWorks软件设计,邮箱、消息队列和自旋锁使用方法

电子说

1.2w人已加入

描述

本文导读

本文介绍了邮箱、消息队列和自旋锁的使用方法。信号量只能用于任务间的同步,不能传递更多的信息,为此,AWorks提供了邮箱和消息队列服务,它们的主要区别在于支持的消息长度不同,在邮箱中,每条消息的长度固定为4字节,而在消息队列中,消息的长度可以自定义。本文为《面向AWorks框架和接口的编程(上)》第三部分软件篇——第10章——第3~5小节:邮箱、消息队列和自旋锁。

10.3  邮箱

前面介绍了用于任务间同步的三种信号量,它们相当于资源的钥匙,获取到钥匙的任务可以访问相关的资源,任务以此确定可以运行的时刻。但是,信号量不能够提供更多的信息内容。比如,按键按下时,释放一个信号量,任务获取到该信号量时,只能知道有按键按下了,但不能知道按键相关的更多信息,比如:具体是哪个按键按下了?

当需要在任务间传递的更多信息时,可以使用AWorks提供的邮箱服务。邮箱服务是实时内核中一种典型的任务间通信方法,特点是开销比较低,效率较高。一个邮箱中可以存储多封邮件,邮箱中的每一封邮件只能容纳固定的4字节内容(针对32位处理系统,指针的大小即为4个字节,所以一封邮件恰好能够容纳一个指针)。发送邮件的任务(或中断服务程序)负责将邮件存入邮箱,接收邮件的任务负责从邮箱中提取邮件。示意图详见图10.4。

周立功

图10.4 邮箱工作示意图

当邮箱中存在多封邮件时,默认按照先进先出(FIFO)的原则传递给接收邮件的任务。邮件的大小固定为4字节,当需要传递的消息内容大于4字节时,则可以仅将消息的地址作为邮件内容,任务接收到邮件时,通过该地址即可查找到相应的消息。这种方式使得使用邮箱进行消息传递的效率非常高。

AWorks提供了使用邮箱的几个宏,宏的原型详见表10.7。

表10.7 邮箱相关的宏(aw_mailbox.h)周立功

1.  定义邮箱实体

AW_MAILBOX_DECL()和 

AW_MAILBOX_DECL_STATIC()宏均用于定义一个邮箱实体,为邮箱分配必要的内存空间,包括用于存储邮件的空间。它们的原型为:

周立功

其中,参数mailbox为邮箱实体的标识名。mail_num表示邮箱的容量,即邮箱中存储邮件的最大条数,由于每封邮件的大小为4字节,因此用于存储邮件的总内存大小为:mail_num×4。

两个宏的区别在于:

AW_MAILBOX_DECL_STATIC() 在定义邮箱所需内存时, 使用了关键字static ,如此一来, 便可以将邮箱实体的作用域限制在模块内(文件内), 从而避免模块之间的邮箱命名冲突,同时,还可以在函数内使用本宏定义邮箱实体。

如使用AW_MAILBOX_DECL()定义一个标识名为mailbox_test的邮箱实体,邮件的最大数目为10,其范例程序详见程序清单10.49。

程序清单10.49 定义邮箱实体的范例程序

周立功

使用AW_MAILBOX_DECL()定义邮箱实体时,可以将邮箱的实体嵌入到另一个数据结构中,其范例程序程序清单10.50。

程序清单10.50 将邮箱实体嵌入到结构体中

周立功

也可以使用AW_MAILBOX_DECL_STATIC()定义一个标识名为mailbox_test的邮箱实体,其范例程序详见程序清单10.51。

程序清单10.51 定义邮箱实体(静态)的范例程序

周立功

2.  初始化邮箱

定义邮箱实体后,必须使用AW_MAILBOX_INIT()初始化后才能使用。其原型为:

周立功

其中,mailbox为由AW_MAILBOX_DECL() 或 AW_MAILBOX_DECL_STATIC()定义的邮箱。mail_num表示邮箱可以存储的邮件条数,其值必须与定义邮箱实体时的mail_num相同。options为邮箱的选项,其决定了阻塞于此邮箱(等待消息中)的任务的排队方式,可以按照任务优先级或先进先出的顺序排队,它们对应的宏详见表10.8。

表10.8 邮箱初始化选项宏(aw_mailbox.h)

周立功

注意,前面讲述了三种信号量,在初始化时同样可以通过选项指定阻塞于信号量的任务的排队方式,分为按照优先级和先进先出两种方式,它们对应的宏名分别为AW_SEM_Q_PRIORITY 和AW_SEM_Q_FIFO。与邮箱选项的命名不同,不可混用。

通常排队方式都选择按照优先级排队,初始化邮箱的范例程序详见程序清单10.52。

程序清单10.52 初始化邮箱的范例程序

周立功

AW_MAILBOX_INIT()用于初始化一个邮箱实体,初始化完毕后,将返回该消邮箱的ID,其类型为aw_mailbox_id_t。定义一个该类型的变量保存返回的ID如下:

周立功

aw_mailbox_id_t的具体定义用户无需关心,该ID可作为后文介绍的其它邮箱相关接口函数的参数,用于指定要操作的邮箱。特别地,若返回ID的值为NULL,表明初始化失败。一般地,若无特殊需求,不会使用该ID,可以不用保存该ID。

3.  从邮箱中获取一条信息

从邮箱中获取一条消息的宏原型为:

周立功

其中,mailbox为由AW_MAILBOX_DECL() 或 AW_MAILBOX_DECL_STATIC()定义的邮箱。p_data指向用于保存消息的缓冲区,消息获取成功后,将存储到p_data指向的缓冲区中,由于消息的大小固定为4字节(32位),因此缓冲区的大小也必须为4字节,例如,可以是一个指向32位数据的指针。timeout指定了超时时间。该宏的返回值为aw_err_t类型的标准错误号。注意,由于中断服务程序不能被阻塞,因此,该函数禁止在中断中调用。

如果邮箱不为空,包含有效的消息,则本次操作将成功获取到一条消息,同时,会从邮箱中将该条消息删除,有效消息的条数减1。此时,AW_MAILBOX_RECV()的返回值为:AW_OK。

如果邮箱为空,没有任何有效的消息,则不能立即成功获取到消息,接下来具体的行为将由超时时间timeout的值决定。

(1)若timeout的值为

AW_MAILBOX_WAIT_FOREVER。则任务会阻塞于此,一直等待,直到邮箱中有可用的消息,即其它任务或中断发送了消息。范例程序详见程序清单10.53。

程序清单10.53 永久阻塞等待邮箱的范例程序

周立功

(2)若timeout的值为

AW_MAILBOX_NO_WAIT。则任务不会被阻塞,立即返回,但不会成功获取到消息,此时,AW_MAILBOX_RECV()的返回值为:

-AW_EAGAIN(表示当前资源无效,需要重试)。范例程序详见程序清单10.54。

程序清单10.54 不阻塞等待邮箱的范例程序

周立功

(3)若tiemout的值为一个正整数,则表示最长的等待时间(单位为系统节拍),任务会阻塞于此,在timeout规定的时间内,若成功获取到一条消息,则AW_MAILBOX_RECV()的返回值为:AW_OK;若在timeout规定的时间内,没有获取到消息,则返回值为-AW_ETIME(表明超时)。范例程序详见程序清单10.55。

程序清单10.55 等待邮箱的超时时间为500ms的范例程序

周立功

4.  发送一条消息到邮箱中

发送消息到邮箱中的宏原型为:

周立功

其中,mailbox为由AW_MAILBOX_DECL() 或 AW_MAILBOX_DECL_STATIC()定义的邮箱。data为发送的32位数据。timeout指定了超时时间。priority指定了消息的优先级。该宏的返回值为aw_err_t类型的标准错误号。

若邮箱未满,可以继续存储消息,则该消息发送成功,邮箱中的有效消息条数加1。此时,AW_MAILBOX_SEND()的返回值为:AW_OK。

若邮箱已满,暂时不能继续存储消息,则不能立即成功发送消息,接下来具体的行为将由超时时间timeout的值决定。

(1)若timeout的值为

AW_MAILBOX_WAIT_FOREVER。则任务会阻塞于此,一直等待,直到消息成功放入邮箱中。即其它任务从邮箱中获取了消息,邮箱中留出了空闲空间。范例程序详见程序清单10.56。

程序清单10.56 永久阻塞等待邮箱的范例程序

周立功

注意,priority参数指定了消息的优先级,其可能的取值有两个:AW_MAILBOX_PRI_NORMAL和AW_MAILBOX_PRI_URGENT,分别表示普通优先级和紧急优先级。一般地,均使用普通优先级,此时,新的消息按照先进先出的原则,依次排队存入邮箱,该消息将后于当前邮箱中其它消息被取出。若使用紧急优先级,则新的消息插队放在邮箱的最前面,该消息将先于当前邮箱中其它消息被取出,即在下次从邮箱中获取消息时被取出。

(2)若timeout的值为

AW_MAILBOX_NO_WAIT。则任务不会被阻塞,立即返回,但不会成功发送消息,此时,AW_MAILBOX_SEND()的返回值为:-AW_EAGAIN(表示当前资源无效,需要重试)。范例程序详见程序清单10.57。

程序清单10.57 不阻塞等待邮箱的范例程序

周立功

(3)若tiemout的值为一个正整数,则表示最长的等待时间(单位为系统节拍),任务会阻塞于此,在timeout规定的时间内,成功发送了消息,则AW_MAILBOX_SEND()的返回值为:AW_OK;若在timeout规定的时间内,没有成功发送消息,则返回值为-AW_ETIME(表明超时)。范例程序详见程序清单10.58。

程序清单10.58 等待邮箱的超时时间为500ms的范例程序

周立功

注意,在中断服务程序中(如按键回调函数),可以使用该接口发送消息至邮箱,这是中断和任务之间很重要的一种通信方式:即在中断中发送消息,在任务中接收消息并处理,从而减小中断服务程序的时间。但是,由于中断不能被阻塞,因此,当在中断中发送消息时,timeout标志只能为AW_MAILBOX_NO_WAIT。在这种应用中,为了避免消息丢失,应该尽可能避免邮箱被填满,可以通过增加邮箱的容量以及提高处理消息任务的优先级,使邮箱中的消息被尽快处理。

5.  终止邮箱

当一个邮箱不再使用时,可以终止该邮箱,宏原型为:

周立功

其中,mailbox为由AW_MAILBOX_DECL() 或 AW_MAILBOX_DECL_STATIC()定义的邮箱。当邮箱被终止后,若当前系统中还存在等待该邮箱的任务,则任何等待此邮箱的任务将会解阻塞, 并返回-AW_ENXIO(表明资源已不存在),其范例详见程序清单10.59。

程序清单10.59 终止邮箱的范例程序

周立功

在讲述计数信号量时,使用了单个按键控制LED翻转,由于只使用到了一个按键,因此,在发送按键消息时,只需要使用计数信号量对按键事件进行计数,无需发送更多的消息。若需要使用多个按键控制LED,则在发送按键消息时,必须携带按键编码信息,以便针对不同的按键作不同的处理。

例如,要实现一个简单的应用,通过LED显示当前按键的编码:

  • KEY_0按下,则LED0熄灭,LED1熄灭,显示“00”;

  • KEY_1按下,则LED0熄灭,LED1点亮,显示“01”;

  • KEY_2按下,则LED0点亮,LED1熄灭,显示“02”;

  • KEY_3按下,则LED0点亮,LED1点亮,显示“03”。

显然,为了区分不同按键,发送按键消息时,需要携带按键的编码信息,由于按键编码是int类型的数据,在32位系统中,其恰好为32位,因此,可以使用邮箱来管理按键消息。范例程序详见程序清单10.60。

程序清单10.60 邮箱使用范例程序

周立功

周立功

周立功

在按键事件回调函数中发送消息,由于只需要处理按键按下事件,因此,仅当按键按下时(key_state不为0),才向邮箱中发送消息(按键编码)。在task_led任务中接收消息,当成功获取到一条消息时,根据消息内容(按键编码)控制LED。

实际上,这里的按键处理程序仅仅用于控制LED灯,耗时时间是非常短的,往往比发送一条消息的时间还短,这里使用邮箱并不能优化程序,仅仅只是作为一个使用邮箱的范例,实际应用按键的处理通常会复杂得多,则建议使用这种通用的模式,即在按键事件的回调函数中,仅仅只是将按键编码发送到邮箱中,实际的处理在任务中完成。这样可以避免长时间占用中断,影响系统的实时性,使其它紧急事务得不到处理,同时,邮箱还有一个缓冲的作用,当按键来不及处理时,可以暂存到邮箱中,后续空闲时再及时去处理,很大程度上避免了“丢键”的可能性,就像PC一样,有时候系统卡顿,显示屏卡住,但按键输入的信息后续还是会显示出来,一般不会丢失。

在上面的范例程序中,由于按键编码的大小为4字节,邮件恰好可以容纳,因此,可以直接将按键编码作为消息内容,发送到邮箱中(拷贝一份,存储至邮箱中)。若要发送的消息大于4字节,显然不能直接发送了,此时,可以将传输内容的地址作为邮件内容,发送到邮箱中,接收者接收到邮件后,再将邮件内容作为地址,从中取出实际的消息内容。范例程序详见程序清单10.61。

程序清单10.61 邮箱使用范例程序——消息内容大于4字节

周立功

周立功

周立功

周立功

程序中,定义了两个任务:task0和task1。task0负责发送消息,每隔1s将count值加1,若count为奇数,则发送__g_str1字符数组中的信息;若count为偶数,则发送__g_str2字符数组中的信息。__g_str1和__g_str2两个字符数组分别存放了字符串:

"The count is an odd number!"和

"The count is an even number!"。显然,字符串的长度超过了4个字节,因此,两个数组的大小也都超过了4字节。在tsak1发送消息时,将字符串数组的地址作为消息发送到了邮箱中。task1用于接收消息,当接收到消息时,将其作为字符数组的地址,使用aw_kprintf()将接收到的字符信息打印出来。以此完成了消息的传递。

由于邮箱仅传输了两个字符数组的地址,为了保证接收任务正确提取地址中的实际消息,必须确保接收任务接收到邮件时,地址中的数据仍然有效。因此,在范例程序中,将两个数组定义为了全局变量,使其内存一直有效。甚至在消息处理完成后,数组的内存空间还是有效的。

在一些应用中,当消息处理完毕后,消息将没有任何实际意义,其地址中对应的数据可以丢弃,以释放相关内存。此时,可以使用动态内存来管理消息:发送者动态获取一段内存空间,填充相关内容后,将这段内存空间的首地址发送到邮箱中,接收者从邮箱中获取到该地址,然后从地址中提取出实际的消息内容进行处理,处理完毕后,释放内存。

例如,在程序清单10.60的基础上,对功能进行简单的修改:当按键按下时,LED显示当前按键的编码,当按键释放时,熄灭所有LED。显然,由于需要对按键按下和释放作不同的处理,这就要求在按键事件产生后,除需要发送按键编码信息外,还要发送按键的状态(按下或释放)。此时,消息就需要包含按键编码和按键状态,共计8字节。范例程序详见程序清单10.62。

程序清单10.62 邮箱使用范例程序——消息内存动态分配

周立功

周立功

周立功

周立功

程序中,使用了aw_mem_alloc()和

aw_mem_free()进行消息内存的申请和释放。

aw_mem_alloc()和aw_mem_free()与标准C的malloc()和free()功能相同,用于动态内存的管理。它们在aw_mem.h文件中声明。

aw_mem_alloc()的函数原型为:

周立功

其用于分配size字节的内存空间,返回void*类型的指针,该指针即指向分配空间的首地址,若内存分配失败,则返回值为NULL。

aw_mem_free()的函数原型为:

周立功

其用于释放由aw_mem_alloc()分配的空间,ptr参数即为内存空间的首地址,其值必须是由aw_mem_alloc()函数返回的。

10.4  消息队列

前面介绍了邮箱服务,邮箱固定了消息的大小为4字节,当需要传输多余4字节的内容时,往往需要使用指针的形式,即使用邮箱传递实际消息的首地址,任务间通过传递的地址共享信息。使用地址共享信息的效率很高。但是,这种情况下,就需要特别小心的进行内存的申请和释放,一个地址中的消息使用完毕后,需要释放相关内存空间。对于初学者来讲,使用起来相对繁琐,容易出错。

为此,AWorks提供了另外一种消息通信机制:消息队列。其和邮箱类似,均用于任务见消息的传输,但其支持的消息大小由用户指定,可以超过4字节。

消息队列可以存放多条消息,发消息的任务负责将消息发送至队列,接收消息的任务负责从队列中提取消息。AWorks提供了使用消息队列的几个宏,宏的原型详见表10.9。

表10.9 消息队列相关的宏(aw_msgq.h)

周立功

1.  定义消息队列实体

AW_MSGQ_DECL()和

AW_MSGQ_DECL_STATIC()宏均用于定义一个消息队列实体,为消息队列分配必要的内存空间,包括用于存储消息的空间。它们的原型为:

周立功

其中,参数msgq为消息队列实体的标识名。msg_num和msg_size用于分配存储消息的空间,msg_num表示消息的最大条数,msg_size表示每条消息的大小(字节数)。用于存储消息的总内存大小即为:msg_num×msg_size。

两个宏的区别在于,AW_MSGQ_DECL_STATIC() 在定义消息队列所需内存时, 使用了关键字static ,如此一来, 便可以将消息队列实体的作用域限制在模块内(文件内), 从而避免模块之间的消息队列命名冲突,同时,还可以在函数内使用本宏定义消息队列实体。

如使用AW_MSGQ_DECL()定义一个标识名为msgq_test的消息队列实体,消息的最大数目为10,每条消息为一个int类型数据,则消息的大小为4个字节(32位平台中),其范例程序详见程序清单10.63。

程序清单10.63 定义消息队列实体的范例程序

周立功

通常,当每条消息为一个int类型的数据时,其长度最好使用sizeof表示,其范例程序详见程序清单10.64。

程序清单10.64 定义消息队列实体的范例程序

周立功

使用AW_MSGQ_DECL()定义消息队列实体时,可以将消息队列的实体嵌入到另一个数据结构中,其范例程序程序清单10.65。

程序清单10.65 将消息队列实体嵌入到结构体中

周立功

也可以使用AW_MSGQ_DECL_STATIC()定义一个标识名为msgq_test的消息队列实体,其范例程序详见程序清单10.66。

程序清单10.66 定义消息队列实体(静态)的范例程序

周立功

2.  初始化消息队列

定义消息队列实体后,必须使用

AW_MSGQ_INIT()初始化后才能使用。其原型为:

周立功

其中,msgq为由AW_MSGQ_DECL() 或  

AW_MSGQ_DECL_STATIC()定义的消息队列。msg_num表示消息队列可以存储的消息条数,其值必须与定义消息队列实体时的msg_num相同。msg_size表示每条消息的大小(字节数),其值必须与定义消息队列实体时的msg_size相同。

options为消息队列的选项,其决定了阻塞于此消息队列(等待消息中)的任务的排队方式,可以按照任务优先级或先进先出的顺序排队,它们对应的宏详见表10.10。

表10.10 消息队列初始化选项宏(aw_msgq.h)

周立功

注意,前面讲述了三种信号量,在初始化时同样可以通过选项指定阻塞于信号量的任务的排队方式,分为按照优先级和先进先出两种方式,它们对应的宏名分别为AW_SEM_Q_PRIORITY 和AW_SEM_Q_FIFO。与消息队列的命名不同,不可混用。

通常排队方式都选择按照优先级排队,初始化消息队列的范例程序详见程序清单10.67。

程序清单10.67 初始化消息队列的范例程序

周立功

AW_MSGQ_INIT()用于初始化一个消息队列实体,初始化完毕后,将返回该消息队列的ID,其类型为aw_msgq_id_t。定义一个该类型的变量保存返回的ID如下:

周立功

aw_msgq_id_t的具体定义用户无需关心,该ID可作为后文介绍的其它消息队列相关接口函数的参数,用于指定要操作的消息队列。特别地,若返回ID的值为NULL,表明初始化失败。一般地,若无特殊需求,不会使用该ID,可以不用保存该ID。

3.  从消息队列中获取一条信息

从消息队列中获取一条消息的宏原型为:

周立功

其中,msgq为由AW_MSGQ_DECL() 或  

AW_MSGQ_DECL_STATIC()定义的消息队列。p_buf指向用于保存消息的缓冲区,消息成功获取后,将存储到p_buf指向的缓冲区中。nbytes指定了缓冲区的大小,缓冲区大小必须能够容纳一条消息,其值不得小于定义消息队列实体时指定的一条消息的长度,通常情况下,nbytes与一条消息的长度是相等的,例如,msgq_test中的消息长度为4字节,则nbytes的值也为4,即p_buf指向的缓存区大小为4字节。timeout指定了超时时间。该宏的返回值为aw_err_t类型的标准错误号。注意,由于中断服务程序不能被阻塞,因此,该函数禁止在中断中调用。

如果消息队列不为空,包含有效的消息,则本次操作将成功获取到一条消息,同时,会从消息队列中将该条消息删除,有效消息的条数减1。此时,AW_MSGQ_RECEIVE()的返回值为:AW_OK。

如果消息队列为空,没有任何有效的消息,则不能立即成功获取到消息,接下来具体的行为将由超时时间timeout的值决定。

(1)若timeout的值为

AW_MSGQ_WAIT_FOREVER。则任务会阻塞于此,一直等待,直到消息队列中有可用的消息,即其它任务或中断发送了消息。范例程序详见程序清单10.68。

程序清单10.68 永久阻塞等待消息队列的范例程序

周立功

(2)若timeout的值为AW_MSGQ_NO_WAIT。则任务不会被阻塞,立即返回,但不会成功获取到消息,此时,AW_MSGQ_RECEIVE()的返回值为:-AW_EAGAIN(表示当前资源无效,需要重试)。范例程序详见程序清单10.69。

程序清单10.69 不阻塞等待消息队列的范例程序

周立功

(3)若tiemout的值为一个正整数,则表示最长的等待时间(单位为系统节拍),任务会阻塞于此,在timeout规定的时间内,若成功获取到一条消息,则AW_MSGQ_RECEIVE()的返回值为:AW_OK;若在timeout规定的时间内,没有获取到消息,则返回值为-AW_ETIME(表明超时)。范例程序详见程序清单10.33。

程序清单10.70 等待消息队列的超时时间为500ms的范例程序

周立功

4.  发送一条消息到消息队列中

发送消息到消息队列中的宏原型为:

周立功

其中,msgq为由AW_MSGQ_DECL() 或  

AW_MSGQ_DECL_STATIC()定义的消息队列。p_buf指向待发送的消息缓冲区。nbytes为消息缓冲区的大小,消息缓冲区的长度不得不得大于定义消息队列实体时指定的一条消息的长度,通常情况下,nbytes与一条消息的长度是相等的,例如,msgq_test中的消息长度为4字节,则nbytes的值也为4,即p_buf指向的缓存区大小为4字节。timeout指定了超时时间。priority指定了消息的优先级。该宏的返回值为aw_err_t类型的标准错误号。

若消息队列未满,可以继续存储消息,则该消息发送成功,消息队列的有效消息条数加1。此时,AW_MSGQ_SEND()的返回值为:AW_OK。

若消息队列已满,暂时不能继续存储消息,则不能立即成功发送消息,接下来具体的行为将由超时时间timeout的值决定。

(1)若timeout的值为

AW_MSGQ_WAIT_FOREVER。则任务会阻塞于此,一直等待,直到消息成功放入消息队列。即其它任务从消息队列中获取了消息,消息队列留出空闲空间。范例程序详见程序清单10.71。

程序清单10.71 永久阻塞等待消息队列的范例程序

周立功

注意,priority参数指定了消息的优先级,其可能的取值有两个:AW_MSGQ_PRI_NORMAL和AW_MSGQ_PRI_URGENT,分别表示普通优先级和紧急优先级。一般地,均使用普通优先级,此时,新的消息按照队列的组织形式,放在队列的尾部,将最后被取出。若使用紧急优先级,则新的消息放在队列的头部,将在下次从消息队列中获取消息时被取出。

(2)若timeout的值为AW_MSGQ_NO_WAIT。则任务不会被阻塞,立即返回,但不会成功发送消息,此时,AW_MSGQ_SEND()的返回值为:-AW_EAGAIN(表示当前资源无效,需要重试)。范例程序详见程序清单10.72。

程序清单10.72 不阻塞等待消息队列的范例程序

周立功

(3)若tiemout的值为一个正整数,则表示最长的等待时间(单位为系统节拍),任务会阻塞于此,在timeout规定的时间内,成功发送了消息,则AW_MSGQ_SEND()的返回值为:AW_OK;若在timeout规定的时间内,没有成功发送消息,则返回值为-AW_ETIME(表明超时)。范例程序详见程序清单10.73。

程序清单10.73 等待消息队列的超时时间为500ms的范例程序

周立功

注意,在中断服务程序中(如按键回调函数),可以使用该接口发送消息至消息队列,这是中断和任务之间很重要的一种通信方式:即在中断中发送消息,在任务中接收消息并处理,从而减小中断服务程序的时间。但是,由于中断不能被阻塞,因此,当在中断中发送消息时,timeout标志只能为AW_MSGQ_NO_WAIT。在这种应用中,为了避免消息丢失,应该尽可能避免消息队列被填满,可以通过增加消息队列的大小以及提高处理消息任务的优先级,使消息队列中的消息被尽快处理。

5.  终止消息队列

当一个消息队列不再使用时,可以终止该消息队列,宏原型为:

周立功

其中,msgq为由AW_MSGQ_DECL() 或 AW_MSGQ_DECL_STATIC()定义的消息队列。当消息队列被终止后,若当前系统中还存在等待该消息队列的任务,则任何等待此消息队列的任务将会解阻塞, 并返回-AW_ENXIO(表明资源已不存在),其范例详见程序清单10.74。

程序清单10.74 终止消息队列的范例程序

周立功

在程序清单10.62中,使用了动态内存分配来管理消息的存储空间,为了避免使用动态内存分配,可以使用消息队列,将每条消息的长度定义为8,以便存储按键编码和按键状态。范例程序详见程序清单10.75。

程序清单10.75 消息队列使用范例程序

周立功

周立功

周立功

周立功

该程序与程序清单10.62所示的程序分别使用邮箱和消息队列实现了相同的功能。消息队列避免了使用动态内存分配,在定义消息队列实体时,就完成了相关内存的静态分配。避免了使用动态内存分配的种种缺点。但是,当使用消息队列时,若定义的容量过大,可能造成不必要的内存浪费。

此外,邮箱和消息队列发送消息的方式是不同的,对于邮箱,其仅仅发送了消息的首地址,接收者接收到地址后,直接从地址中取出相应的消息,这种方式下,消息传递的效率很高。

但对于消息队列,发送消息时,是将整个消息内容(如按键编码和按键状态)拷贝到消息队列的缓冲区中,接收消息时,再将存储在消息队列缓冲区中的消息完整的拷贝到用户缓冲区中。由此可见,一次消息传输存在两次消息内容的完全拷贝过程,这种传输方式效率很低,特别是对于一条消息很大的情况。

因此,建议当一条消息很大时,使用邮箱;而当一条消息较小时,消息的拷贝对性能的影响较弱,则使用消息队列更加方便快捷。

当使用邮箱时,为了避免使用常规动态内存分配方法造成内存碎片、内存泄漏、分配效率等问题。可以使用AWorks提供的静态内存池管理技术,其为了避免内存碎片和分配效率等问题,将每次分配内存的大小设定为一个固定值。内存池管理技术将在“内存管理”章节中详细介绍。

10.5  自旋锁

互斥信号量用于任务间对共享资源的互斥访问,在一个任务获取互斥信号量时,若互斥信号量无效,需要等待时,则任务会主动释放CPU,内核调度器进而调度CPU去执行其它任务,当互斥信号量恢复有效时,再重新调度CPU继续执行之前的任务。这样,在任务等待互斥信号量有效的这段时间里,CPU可以被充分利用,去处理其他任务。

但是,调度过程是需要耗费一定时间的,有些时候,对共享资源的访问可能非常简单,消耗CPU的时间很短,也就是说,一个任务占用共享资源的时间非常短,其获得互斥信号量后很快就会释放。这种情况下,当一个任务获取互斥信号量时,即使当前的信号量无效,也意味着该信号量很快就会被释放,变为有效。若任务在此时释放CPU,执行任务调度,很可能在任务调度过程中,信号量就被释放了,系统又不得不在调度结束后重新将CPU再调度回来,这使系统在任务调度上花费了太多的时间成本。这种情况下,任务不释放CPU将是一种更好的选择,可以提高任务执行的效率。

AWorks提供了自旋锁,所谓“自旋”,就是一个“自我轮询检查”,当获取自旋锁时,若自旋锁处于无效(被锁)状态,则不会释放CPU,而是轮询检查自旋锁,直到自旋锁被释放(解锁)。检查到自旋锁被释放后,立即获取该自旋锁,使之成为锁住状态,接着尽快迅速完成对共享资源的访问,访问结束后,释放自旋锁。

由于当自旋锁不可用时,任务将一直循环检查自旋锁的状态直到可用而不会释放CPU, CPU在轮询等待期间不做任何其它有效的工作,因此,只有在共享资源占用时间极短的情况下,使用自旋锁才是合理的。否则,应该使用互斥信号量。需要特别注意的是,自旋锁不支持递归使用。

AWorks提供了使用自旋锁的通用接口,接口的原型详见表10.11。

表10.11 自旋锁通用接口(aw_spinlock.h)

周立功

在AWorks中,自旋锁可以在中断中使用,因而在接口命名中,含有“isr”关键字。之所以可以在中断中使用,是由于在获取到自旋锁后,会关闭总中断,释放自旋锁时,再打开总中断,使得在访问自旋锁保护的共享资源时,可以独占CPU,保证其不会被中断打断。否则,若任务在获取到自旋锁还未释放时被中断打断,在中断上下文中再次获取自旋锁将造成“死锁”:任务未释放自旋锁,中断只能等待;中断占用了CPU,任务只有等待中断结束返回后才能继续执行,以释放自旋锁。

换句话说,在AWorks中,自旋锁可以在中断中使用,任务和中断对共享资源的访问是互斥的,当任务访问共享资源时,中断会被关闭,以实现互斥。

1.  定义自旋锁实体

在使用自旋锁前,必须先使用aw_spinlock_isr_t类型定义自旋锁实体,该类型在aw_spinlock.h中定义,具体类型的定义用户无需关心,仅需使用该类型定义自旋锁实体,即:

周立功

其地址即可作为各个接口中p_lock参数的实参传递,表示具体要操作的自旋锁。

2.  初始化自旋锁

定义自旋锁实体后,必须使用该接口初始化后才能使用。其原型为:

周立功

其中,p_lock指向待初始化的自旋锁。flags为自旋锁的标志,当前无任何可用标志,该值需设置为0。初始化自旋锁的范例程序详见程序清单10.76。

程序清单10.76 初始化自旋锁

周立功

3.  获取自旋锁

获取自旋锁的函数原型为:

周立功

其中,p_lock指向需要获取的自旋锁。若自旋锁有效,则获取成功,并将自旋锁设置为无效状态;若自旋锁无效,则会轮询等待(不会像互斥信号量那样释放CPU),直到自旋锁有效(占用该锁的任务释放自旋锁)后返回。该接口可以在中断上下文中使用。获取自旋锁的范例程序详见程序清单10.77。

程序清单10.77 获取自旋锁

周立功

4.  释放自旋锁

释放自旋锁的函数原型为:

周立功

其中,p_lock指向需要释放的自旋锁。自旋锁的获取和释放操作应该成对出现,即在一个任务(或中断上下文)中,先获取自旋锁,再访问由该自旋锁保护的共享资源,访问结束后释放自旋锁。不可一个任务(或中断上下文)仅获取自旋锁,另一个任务(或中断上下文)仅释放自旋锁。释放自旋锁的范例程序详见程序清单10.78。

程序清单10.78 释放自旋锁

周立功

在互斥信号量的范例程序中(详见程序清单10.25),使用了两个任务互斥访问共享资源(调试串口)进行了举例说明,由于调试串口输出信息的速度慢,输出一条字符串信息耗时往往在毫秒级别,因此,这种情况下,使用自旋锁是不合适的。一般来讲,操作硬件设备都不建议使用自旋锁,自旋锁往往用于互斥访问类似于全局变量的共享资源。

例如,有两个任务task1和task2。在task1中,每隔50ms对全局变量进行加1操作,在task2中,检查全局变量的值,若达到10,则将全局变量的值重置为0,并翻转一次LED。

由于两个任务均需对全局变量进行操作,为了避免冲突,需要两个任务互斥访问该全局变量,显然,加值操作是非常快的,占用时间极短,可以使用自旋锁实现互斥访问,范例程序详见程序清单10.79。

程序清单10.79 自旋锁使用范例程序

周立功

周立功

周立功

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分