本来今晚想写如何搞动态加载和动态补丁的,但很不幸,翻遍了硬盘,也没找到以前的代码,连网盘里都没备份。这时候,才焕然大悟--半年前我换上现在的笔记本,淘汰了那台老掉牙的台式机。所幸硬盘没丢,不过一时时也没法读里面的数据了。等过些日子,读出里面的数据再谈动态加载和动态补丁技术。今天说些简单的,能在软件设计中立即用得上的,模块间通信技术--统一消息。
统一消息模型,最早的启发是UT的Wacos SSI。那是一个很不错的通信模型,允许模块间的通信统一成队列通信;而在物理上,模块可能位于各种网络中的不同的实体,又或者是不同的进程,线程。记得那会调试核心网的程序,在板卡上是没有什么调试环境的,除了WindShell(同CSHELL)外,就没什么支撑了。于是我们就把软件用GDB加载到目标机(无盘工作站),然后开始测试。有人不理解了,这没啥啊!现实是这价值很大,大型系统的嵌入式开发,能争取到的机房空间、设备和板卡总是奇缺,就当时的情况来说,我们三四个人才能分到一套设备。Wacos_SSI的队列通信技术,让我们可以把目标机做成功能板块,且只需要极少量的修改,就能和实际系统的主控板进行通讯联测,工作效率的提升自不待言。
再后来,哥在Nortel的时候知道了TIPC协议,好象是E///和IBM捣腾出来的东西。思路上,和Wacos SSI很接近。所不同的是,Wacos SSI在消息头里使用了IP地址,而TIPC则是自定义的节点地址,也因此包含了一个额外的节点地址和特定网络间的地址翻译过程。另外一个区别是,Wacos SSI考虑了远程节点间通信和本地通信的差别,只有远程通信时才传递消息实体,而本地则是传递标识(Handle)来快速完成。TIPC则没讲述这个层次的程序设计问题,也因此在工程实践中应用寥寥。
现如今,UT没了,Nortel也没了。特别是UT,十多年过去了,哥特别怀念那段日子,和我的那个团队。无奈,哥就是灾星,跟喜欢的公司相克。很多局外人都说UT不咋的,就一个做小灵通的;可哥的眼里,那的许多软件开发团队,战斗力一点不比Huawei差。就说哥做的网关城域交换机,才十来个人,而huawei是几十人,好几倍啊,最后市场表现还是平分秋色。当然,我还是蛮佩服huawei的,他们的东西真心做的漂亮,维护界面人性化,不像我们的,很多事情要命令行来实现。不过我们也有特点,就是架构做的非常好,以至于客户的需求,总是能很快实现,而且基本上对现有功能是0风险。呵呵,据说气死不少人!
这当中,有三大功臣:
- Wacos SSI;
- 状态机;
- 数据驱动模型。
状态机的代码,已经在昨晚的内存泄漏里的链接里提供了,有兴趣可以下载或是用在喜欢的地方,哥只希望它有更多机会发挥价值。
嗯,Wacos SSI排在第一!是的,Wacos SSI的消息通信让我们的系统变得非常柔性,模块与模块间几乎没有什么复杂的耦合。想想现在那些公司招聘需求里,要求什么多任务多线程编程能力,精通什么信号量和同步技术,哥就想哭,这就是我们的软件水平,时刻准备着处在玩死自己。哥做程序,只考虑CPU有几个线程核,至于系统有几个进程线程,都是这个决定的,而且合并拆解任务,都是分分钟能改代码实现的事。跟哥一起做软件,就只要记住几点:无论你和谁通信,你只要知道他的地址,然后发消息给他就好了;而你也只要看着自己的队列,有消息就干活,没消息就歇着。至于发消息,就一个标准的函数,而消息封装格式,也是统一的。至于系统函数库里提供的什么信号量,管道啥的,千万别尝试在应用里面使用,否则,编译器会用编译错误来告诉你行不通。
有点扯远了,回到正题。
统一消息的定义,包含两个部分,消息标签和消息头,具体如下:
typedef struct _MSG_TAG_TYPE_ { zAddr_t srcAddr; zAddr_t dstAddr; zHandle_t msgHandle; } PACKED zMsg_t; typedef struct _MSG_HEAD_TYPE_ { byte_t sysrsvd[8]; //reserved for adding src & dst addresses on network. word_t msgLen; word_t msgId; dword_t srcInst; dword_t dstInst; } PACKED zMsgHdr_t; typedef struct _MSG_HEAD_EX_TYPE_ { zAddr_t srcAddr; zAddr_t dstAddr; word_t msgLen; word_t msgId; dword_t srcInst; dword_t dstInst; byte_t msgBuf[1]; } PACKED zMsgHdrEx_t;
zMsg_t结构是消息标签,应用程序收、发消息时,都是收发的这个数据结构,如下:
int zMsgSend(zMsg_t *msg);
通常来说,我们应该把这个消息标签做的比较小,因为做的太大,来回复制它的内容是需要耗费CPU时间的。比如,你可以将zAddr_t定义成word,zHandle_t定义成dword,这样只需要8字节就够了。不过记得字节对齐,一般来说,要保证长度是4的倍数。
消息头就是消息内容的头部格式段,除了这个头部,剩下的就是应用自定义的payload部分。zMsgHdr_t和zMsgHdrEx_t实质上是一样的。这里面的地址部分,不是必须的,只有当消息透过网络或是总线传递时,才是必须的,否则没法由边界模块还原。而对于应用,如无特别约定,那几个字节是无意义且内容不确定的。
消息标签和消息间是通过msgHandle关联。这样,当消息在本地传递时,msgHandle指向的是一块普通内存;而当消息在本地进程间通信时,则指向共享内存;至于网络或是某个总线传递,边界模块负责本地内存数据和网络数据间的转换。如此一来,最大程度的减少实际消息体的拷贝开销,让消息传递变得高效,且细节处理对应用透明。
Wacos SSI的地址部分,填的是IP地址;当然,它还定义了一个模块号来配合这个地址使用。整个通信过程很简单,应用只需要申请一个队列,并告知SSI,这个队列和哪个目的模块号使用。正常情况下,这个做法都能满足需求,但碰上程序模块重新规划或是特俗测试目的,就有点力不从心了。因此,哥在zMsg_t标签里彻底放弃了IP+module的地址组成,改为TIPC的地址方式。不过这也就让系统必须维护一个路由表,用来完成特定目的地址到队列的映射。
统一消息路由表定义如下:
typedef struct Z_UDP_ADDR_TYPE { dword_t ip; word_t port; } zUDPAddr_t; typedef struct Z_MSGQ_ADDR_TYPE { void *qid; } zQueAddr_t; typedef struct Z_MSGQ_OUT_TYPE { zAddr_t addr; zUDPAddr_t udpAddr; zQueAddr_t queAddr; } zMsgRoute_t;
路由表项里首先是地址,对应的是消息的目的地址。接下来是网络地址和队列地址,可以有一个或是都有。
- 仅队列地址:说明是本地(或者是需要经隐形边界代理转发)的消息,目的地址为队列所有者;
- 仅网络地址:说明是远程消息,且应该直接网络发送,无需经过边界代理,目的地址为远端模块地址;
- 含两类地址:远程消息,应用发送时通过队列地址送入边界模块,再通过网络地址发送,,目的地址为远端模块地址。
总上面的关系可以看出,队列和地址间的关系是一对多的关系,即多个地址的消息可能被投送到同一个队列。这就让模块合并变得异常容易,当然,不安规则出牌的模块什么时候什么方法都白搭。通常来说,如果有IP网络的通信要求,系统就需要创建一个基础的网络边界模块。这个模块本身可能并不需要地址,而只需要提供一个消息聚合的队列。当然,在一个开放的网络环境下,这个边界模块可能还需要做些安全性的工作,比如过滤非法消息等,这可以通过在模块内额外配置源IP地址,端口或是源目的地址等实现。如果远端并不支持zMsg_t工作,则这时候的边界模块就需要做好消息的翻译过程,为远端模块分配映射模块地址。当然,这些都是本地的,不属于路由表内容。
从地址映射到真实的目的队列或是网络地址,是个频繁的操作,设计上必须要非常高效。对于地址非常少的系统,比如总共才七八个模块,可以用一个紧凑的数据来做,简单且不妨碍效率。但对于有数十或是上百个地址的系统来说,遍历方法就不可取了。这时应该用二分搜索,或是平衡二叉树。比如城域交换机,有十来块子功能卡,每张卡上有十来个模块,整个系统的地址空间有一百多,采用二分搜索,最多8次就够了!相比消息处理函数的指令数,这部分开销完全可以接受。而从另一个角度来说,统一消息让程序变得简单可控,系统内减少了消息的拷贝操作,所带来的系统效率和性能提升,远远大于查询路由表的开销。
当嵌入式世界有了统一消息后,哪些多线程的开发技巧还有很大价值么?一般应用开发者真的需要理解这些知识么?