任务间同步与通信之管道
VxWorks提供了一种类似消息队列的任务间数据通信的机制----管道。管道的作用跟消息队列几乎完全一致,就是可以帮助把数据从一个任务发送给另一个任务,但是功能类似,使用方式却截然不同。管道属于一种虚拟的I/O设备,所以它的相关API跟统一I/O访问接口完全一致,比如open,close,read,write等。也就是说它提供了一组通用的I/O操作接口,对使用者来说非常方便。
下面可以看一段管道使用的代码:
// 创建管道虚拟I/O设备 pipeDevCreate("/pipe/try1", 10, 100); // 打开该设备 int fd = open ("/pipe/try1", O_RDWR, 0); // 向管道写入数据 write (fd, (char*)&inData, sizeof(inData)); // 从管道读取数据 int len = read (fd, (char*)&outData, sizeof(outData)); |
从如上代码可以看出,创建完管道后的处理完全跟通用I/O接口的使用是完全相同的,对C/C++程序原来说几乎没有额外的学习成本。所以你访问管道可以跟其他I/O访问的处理一样,优化了代码的通用性。同时,使用管道还带来另一种好处,它支持多路I/O复用----select。大家知道select支持绝大多数的异步I/O设备的操作,包括Socket,串行设备等。所以使用select可以使一个任务同时监听多组不同的I/O设备,大大提高程序性能。关于select,大家也可以参考我的《服务器模型分析与验证》中对多路I/O复用的描述。
看到这里,有人会说,那管道在使用和功能上不是完胜消息队列吗,是不是大家都应该使用管道啊?当然不是,其实两者各有特点,前文中也有提到,消息队列支持在读和写操作时设置超时时间,同时还支持设置消息类型(URGENT还是NORMAL)等,而管道并不支持这些,它只是支持通用I/O操作所具有的接口形式。所以具体如何选择还是看实际的应用。
总的来说,消息队列更符合通用的任务间通信的使用习惯,也更容易控制,而当需要通过多路I/O复用来优化程序处理时,则可以考虑使用管道。
任务间同步与通信之事件
从VxWorks 5.5开始,提供了新的任务间同步通信的机制----事件,事件可用于任务和中断服务程序ISR之间、任务和任务之间、任务和VxWorks资源之间进行通信。
任务用函数eventReceive()来接收它关心的事件,用eventSend()来向另一个任务发送事件。(是否看似简单?)
先来看第一个例子:
void eventRoute1(void* param) { int task_id = (int)param; eventSend(task_id , VXEV01); } void tryEvent() { int id = taskIdSelf(); taskSpawn("vxEvent1", 150, VX_FP_TASK, 30000, (FUNCPTR)eventRoute1, id,0,0,0,0,0,0,0,0,0 ); eventReceive(VXEV01, EVENTS_WAIT_ALL, WAIT_FOREVER, 0); } |
在tryEvent中启动一个任务,然后调用eventReceive通过设置EVENTS_WAIT_ALL来等待VXEV01事件的发生,子任务中调用eventSend向初始任务发送事件VXEV01,从而触发初始任务继续执行。
从这个例子,很多人也许会觉得事件跟信号量功能一样,没有太多意义。其实上面体现的功能只是事件的冰山一角,让我们慢慢来一窥全貌吧。
那么,事件有哪些特点呢?
第一,从上面的例子已经可以看出,使用事件时不需要创建任何对象,所以它是一种更轻量级的同步通信机制。因为每一个任务都有一个32位事件寄存器,其中高8位由VxWorks系统保留,开发者可以使用低24位(VXEV01~VXEV24),每一位表示一种事件。
第二,使用事件可以等待多个事件的发生,这里的多个事件发生的关系,可以是“与”也可以是“或”,在eventReceive方法中可以指定EVENTS_WAIT_ANY或EVENTS_WAIT_ALL,比如等待VXEV01或VXEV02之一的发生:
eventReceive(VXEV01 | VXEV02, EVENTS_WAIT_ANY, WAIT_FOREVER, 0); |
个人认为这是非常有用的一点,毕竟如果要自己实现多事件结合并可控制超时时间的话,可以说并非那么简单,而且性能也会有问题。
第三,除了上述例子中eventReceive和eventSend的使用外,事件还可用于任务跟VxWorks资源之间进行通信,这里所谓的资源有信号量,消息队列等。
比如再看第二个例子(假设已创建了sem_ev1和sem_ev2两个信号量):
void eventRoute1(void* param) { semGive(sem_ev1); } void eventRoute2(void* param) { semGive(sem_ev2); } void tryEvent() { semEvStart(sem_ev1, VXEV01, EVENTS_SEND_ONCE); semEvStart(sem_ev2, VXEV02, EVENTS_SEND_ONCE); taskSpawn("vxEvent1", 150, VX_FP_TASK, 30000, (FUNCPTR)eventRoute1, 0,0,0,0,0,0,0,0,0,0 ); taskSpawn("vxEvent2", 150, VX_FP_TASK, 30000, (FUNCPTR)eventRoute2, 0,0,0,0,0,0,0,0,0,0 ); eventReceive(VXEV01 | VXEV02, EVENTS_WAIT_ALL, WAIT_FOREVER, 0); semEvStop(sem_ev1); semEvStop(sem_ev2); } |
跟资源通信,需要把对应资源注册到特定事件,如上例中是通过semEvStart把信号量sem_ev1和sem_ev2分别注册到事件VXEV01和VXEV02,然后semGive信号量被释放且可用时,对应注册的事件会被触发。
第四,前面说到semGive信号量被释放且可用时,对应注册的事件会被触发,这里所谓可用的含义是,semGive释放信号量同时没有其他任务正在锁定该资源,也就是该时间点该资源可用时,才会触发事件。但不能保证该资源在事件被触发后还可用,因为后续有可能又被其他任务锁定。
第五,如果相关被注册到事件的资源被删除时,通过eventReceive等待该事件的任务会自动被触发,也就是说semDelete和msgQDelete会触发该资源注册过事件。但是需要注意的是,当调用semDelete时,任务执行于semEvStart和eventReceive之间的话,则后续eventReceive会被阻塞。
总上所述,VxWorks中,事件提供了非常丰富的功能,在实际应用场景中它的一些特性可以使很多复杂的事情变得极其简单。
任务间同步与通信之信号
VxWorks中的信号是从POSIX沿用过来的概念,如果你熟悉Linux下的signal,那么几乎就是一回事。信号跟信号量,那完全是两回事,信号有点类似事件,不过事件接口是同步的,而信号处理是异步的,所以其实信号更像中断,或可以称为软中断。
先看个简单的例子:
void signalRoute1(int sig) { logMsg("signalRoute1 signal id: %d\n", sig,0,0,0,0,0); } void trySignal() { signal(SIGBUS, signalRoute1); raise(SIGBUS); } |
在这个例子中,先通过signal方法,绑定信号SIGBUS的处理方法signalRoute1,然后通过raise向当前任务发送信号,信号处理函数被执行。
第一个概念,绑定的信号处理函数是在哪运行,后台新任务中?当前任务中?系统特殊任务中?答案是会在信号目标任务的上下文中运行,所以该例子中因为raise是向当前任务发送信号,所以之后会挂起当前任务的执行,但仍旧在本任务的上下文中响应signalRoute1。
再看一个例子:
void signalRoute2(int sig) { logMsg("signalRoute2 sig id: %d\n", sig,0,0,0,0,0); } void taskRoute(void* param) { int task_id = (int)param; kill(task_id, 0x10); } void trySignal() { int id = taskIdSelf(); signal(0x10, signalRoute2); taskSpawn("vxSignal", 150, VX_FP_TASK, 30000, (FUNCPTR)taskRoute, id,0,0,0,0,0,0,0,0,0 ); } |
先说明下,kill跟rasise一样都是给任务发送信号,但rasise只能给当前任务,而kill可以指定任务发送,指定任务会被挂起,而信号处理就会在任务id指定任务的上下文中运行。
第二个概念,不能向内核任务发送信号,可以给任务本身,系统public的任务或系统任何其他进程(6.x开始支持process)发送信号。
第三个概念,系统中有31个可用的信号(1~31),每个信号都有默认的含义和处理,但用户可以修改默认的设定。
第四个概念,应该在什么时候用信号?信号本身也可以很方便的达成任务间的通信,但是正常情况下我们应该尽量少用信号,为什么?因为信号会破坏既有处理的优先级控制。因为信号是异步处理的,所以很难避免在信号处理导致目标任务挂起时,是否会因为该任务锁定了某自愿导致正常处理逻辑被破坏,通俗点说就是信号处理很可能阻塞正常的逻辑。所以一般只有在错误处理或者异常等优先级相对很高的处理上使用信号。
第五个概念,即使任务处于挂起状态(比如等待某信号量),发送信号时还是可以触发该任务。
第六个概念,信号处理函数有何需要注意的?因为第四点中提到的问题,所以信号处理函数应该尽量保证处理短小,精简,不使用会导致阻塞的处理。其实有点类似中断的处理,关于这点可以参考我另一篇文章:《【VxWorks系列】中断服务程序中哪些可以做哪些不可以做》。
总结一下,对于常规的任务间通信,往往前几篇中提到的几种方式更合适,更通用,更安全。而对于信号,一般更多的被使用在错误处理或异常中,总之还是慎用。 |