标准化模块接口--统一消息
关注、星标公众号,直达精彩内容
链接:https://www.cnblogs.com/hhao020/p/5018951.html
本来今晚想写如何搞动态加载和动态补丁的,但很不幸,翻遍了硬盘,也没找到以前的代码,连网盘里都没备份。这时候,才焕然大悟--半年前我换上现在的笔记本,淘汰了那台老掉牙的台式机。
所幸硬盘没丢,不过一时时也没法读里面的数据了。等过些日子,读出里面的数据再谈动态加载和动态补丁技术。今天说些简单的,能在软件设计中立即用得上的,模块间通信技术--统一消息。
统一消息模型,最早的启发是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次就够了!相比消息处理函数的指令数,这部分开销完全可以接受。而从另一个角度来说,统一消息让程序变得简单可控,系统内减少了消息的拷贝操作,所带来的系统效率和性能提升,远远大于查询路由表的开销。
当嵌入式世界有了统一消息后,哪些多线程的开发技巧还有很大价值么?一般应用开发者真的需要理解这些知识么?
版权声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。
嵌入式编程专辑 Linux 学习专辑 C/C++编程专辑 Qt进阶学习专辑
程序员的编程学习基地,定期分享,注重编程思想的培养,欢迎关注!
关注公众号『技术让梦想更伟大』,后台回复“加群”可加入技术交流群!
点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。