原创
消息驱动机制【转】
2008-1-5 16:06
3135
5
5
分类:
软件与OS
计算机本质上是信息处理机,输入数字化的数据,按照程序规定的步骤进行处理,输出指定的动作和时序,从而完成控制任务和信息处理。这里的关键是信息的输入,有了输入,后续处理和输出是水到渠成的事。那么怎样高效率地得到输入信息呢?有一种叫做消息驱动的机制可以比较完美地实现这个目的。
消息有两层含义:1、消息发生的时间点(事件,动态概念);2、消息内容(信息,静态概念)。消息发生触发一个事件,用以主动通知信息的到来。有实际意义的系统中消息发生时间是随机的,由OS异步捕获此事件,再分发给对应的处理程序。之所以由OS负责通知消息,而不是由应用程序自己捕获,盖OS掌控系统的全部资源,一切事件均应知会OS,以便其全面参与系统管理。消息内容就是信息本身,是计算机的输入数据源,是实际要处理的内容。
信息处理是计算机的灵魂,消息就代表到达的信息。利用消息驱动计算机输入的机制,显得自然、简洁、富有美感。程序的作用是事先编排好信息到达后的处理流程,但是信息何时到达却不能事先预测。也就是说,程序员可以预知所有可能的信息范围,但无法肯定哪个信息一定会到,何时到。这种情况导致两种处理方法:被动轮询和主动通知。消息属于主动通知一类。
在硬件中,控制电路由有限状态机(FSM)实现,在软件中,消息处理由有限消息机(FMM)实现。关于竖着写的FSM和横着写的FMM,在《状态机的两种写法》一文中有详细说明,此处不再赘述。消息按到达次序被缓存在先入先出(FIFO)队列中(可以根据消息的实时性要求将消息队列分成高低两个优先级,但本质上消息还是要按照次序排队),依次由接收程序串行处理。
==============
*消息是什么?*
==============
--------------
|消息就是对象|
--------------
他有自己的属性(成员)和方法(动作),同时隐含代表信息到达事件。当发生msg.event事件后,程序开始处理消息体msg.body,怎么处理?调用初始化时注册的回调函数msg.callback。即:if( msg'event ) msg->callback(msg->body);。消息主动通知OS信息到达,并自行给出处理方法,甚至自己选择下一步的目的地。这样信息就有了生命力,可以自主流动,自行工作。消息驱动就是要给信息的流动提供一种机制,他可以分发消息,缺省处理消息,销毁消息,异常处理,按指定动作处理消息。一旦实现这种机制,能够普适所有的消息处理情况,增改删操作不过就是实现新的消息和动作罢了,机制完全不必变化。从很小的系统到很大的系统,这种机制都能很好地适应,因为他们的本质都是为信息的流动提供一个通用平台。
在PPP协议框架实现中,为节省代码量,LCP和IPCP复用同一个协商状态机FSM,完成相同的请求(req)、确认(ack)、否定(nak)、拒绝(rej)等一系列动作,LCP和IPCP可以看成协商状态机FSM的类实例。他们的动作完全相同,但属性,即协商的内容不同,一个是用于链路控制的选项协商,一个是用于NCP为IP协议时,IP地址、子网掩码、DNS1、DNS2的协商。在初始化时,将LCP的FSM回调函数注册为LCP的消息处理函数,将IPCP的FSM回调函数注册为IPCP的消息处理函数。当LCP消息到达时,FSM调用LCP注册的回调动作处理LCP的消息内容。回调函数准确地给出了当特定消息发生时要执行的私有动作,OS仅仅起到消息转发和通知的作用,他不提供专有的消息处理,而消息是对象,他本身就有处理私有属性的方法。从消息驱动的角度,PPP协议框架就是一堆消息对象的集合。当拨号成功时产生lowerup(底层已备)消息,通知PPP开始协商,LCP用req/ack/nak/rej消息协商链路选项,然后用约定的认证协议鉴权,继而IPCP获得IP地址信息,产生ipcpup消息,此时ip_input入口开始接收ip_arrive消息,并根据协议类型依次转发IP包消息到icmp_input、igmp_input、udp_input、tcp_input、raw_input等入口点。
在Windows的设备驱动模式WDM中,引入了驱动程序对象和设备对象概念,利用I/O请求包IRP在各层设备对象(xDO:FDO/FilterDO/PDO)间传递设备控制消息。I/O管理器将上层打开open,关闭close,I/O操作ioctl,读read,写write等操作转化为IRP消息传到核心层,核心层的设备驱动在DriverEntry入口点注册消息回调函数,完成对即插即用PNP、电源管理PM、上层IRP设备控制、WMI等消息的私有处理。每个设备对象都可以申请自己的设备扩展内存区,以便保存私有数据。撰写win设备驱动,就是在写回调函数,只不过win已经定义好大部分消息,我们只要提供对应的处理程序即可。常常感觉win编程不过就是在实现一坨一坨的动作代码,然后就等着win发消息来调用,再就是满眼的switch/case语句分别处理不同的消息。程序员的责任是选择正确的系统API函数,保证动作执行准确无误。除了内存在保护模式下映射来映射去,硬件串行化访问,DMA和中断处理,各种总线驱动API,各种协议驱动,剩下的就全是消息处理了。值得一提的是完成例程的设置IoSetCompletionRoutine,通过设置完成回调例程和上下文私有数据区,我们可以拦截下层消息。因为下层何时能完成处理的时间不确定,如果程序死等I/O处理完成将浪费大量CPU处理能力。如果设置回调后挂起,让出CPU,等到I/O处理完成后再被激活,那么这种程序结构是最合理的。例如在批量USB处理时,URB缓冲区可能装不下整个发送数据,需要分片传递。方法就是设置好完成回调,然后把IRP传到下层,挂起。一旦下层完成USB收发,就会发消息激活完成回调,完成回调根据保存的上下文实现分片传输,直至完成全部数据传送,然后向上层发消息通知I/O处理完毕。
类似消息激活在智能网程序里也有体现。主叫用户摘机后播放提示语音,在此期间如果用户拨号,马上停止放音。难点是不知用户何时拨号。如果设定一个固定延时,用户会感觉非常不友好。如果定时轮询用户是否拨号,至少要百毫秒周期,当用户量上到一万以上的时候,这种方式浪费太大。现在的放音机制是发个消息给语音板和交换网络,让其给主叫摘机用户放音,然后定时监视收号器,判断用户是否拨号。这样做用户数量不能做大。怎么办呢?和WDM的完成回调一样,创建一个信号量,发完放音消息后就把此信号量句柄连同post回调一起放在消息里发给收号器,然后挂起,超时等待在此信号量上,一旦收号器在任意时间收到用户拨号,就会把拨号消息反馈回来,系统调用我注册的私有Post回调响应此消息时就会释放此信号量,我的主程序就被唤醒激活,一点也不浪费CPU处理能力。而且超时机制能保证异常时的恢复。
对于这种不确定时延的操作,使用消息驱动机制是非常有效的。例如:TCP协议使用connect原语建立连接,但是连接何时建立事先并不知道,也许信道质量好,三次握手很快就完成了;也许信道误码率高,拖延了很长时间才连上;甚至因重连次数大于6次而导致连接失败。在lwip实现中,应用层调用connect函数进行主动连接,在该函数内部实际上是生成一个消息发送给tcpip_thread线程,在此消息里包含了conn->mbox信号量句柄,然后,该函数阻塞等待在此信号量上。一旦底层完成三次握手,连接成功,就会触发TCP已连接事件TCP_EVENT_CONNECTED回调do_connect释放此信号量,connect随即退出阻塞。在应用层看来,connect一直阻塞到连接成功,如果不成功就返回-1。connect运行在用户线程,实际连接运行在tcpip_thread线程,通过消息回调,使两个不同线程的函数建立了同步关系,虽然TCP协议时延动态范围很大,达到秒级,但这种消息驱动机制能很好地适应变化。
--------------------
|消息就是串行化处理|
--------------------
多任务并行处理的好处是改善并发特性,提高实时性。但代价是需要处理同步、互斥、重入、阻塞等新问题。OS切换时间一般是10ms的整数倍,如果只有几百个进程(或以下),多任务的工作性能会有很好的表现。然而难以想象上千上万的进程同时工作的情形,那是注定要崩溃的。因此,Windows的TCP/IP协议栈使用“完成端口”技术自行管理socket连接,而不是每次接收(accept)到新socket连接就创建新线程。同样,Linux里也使用select管理多个socket连接。这样连接数可以做得很大,而性能又不会急剧下降。总之,用多任务方式不能实现大容量系统。由于串行化处理不存在并行,也就不会遇到同步、互斥等问题,一切处理都简单了,而且,没有任务切换,效率也高。需要注意的是,因为消息排在队列里,所以,队列空间一定要充足,否则当队列满时,消息会丢失。如果消息收发在同一进程,还有可能造成死锁。lwip移植中就会遇到此问题,因为lwip为了兼容不带OS的情况,把消息收发做在了一个线程里,以便有/无OS时都能正常工作,当消息队列满时,发阻塞,收也被阻止,必然死锁。建议消息收发尽量放在不同进程里,以免死锁。实在不行,也要尽可能加大队列空间。另外注意划分任务优先级时把等待消息的线程设高,以免队列中堆积消息太多而溢出。当然,消息驱动最好基于OS,因为OS实现的队列自动具有互斥功能,使用方便,挂起的任务也不浪费CPU时间片。我说的串行化处理不是整个系统都是串行的,那样就是前后台系统了,我说的是在多进程的某一个进程里实现串行化。
--------------
|消息就是任务|
--------------
上面所说的串行化处理可以大大提高系统处理容量,但是当容量达到一定数值时,内存和总线瓶颈会限制容量的进一步增长(例如静态双口RAM读写时延为7ns)。为了进一步提高吞吐率,必须将单CPU处理模式转换成阵列处理,以便突破系统瓶颈。在多任务系统中,堆栈保存了任务的现场信息,通过保存和恢复现场,我们可以实现多任务切换。同样,消息中也保存了完整的现场信息,自然可以看成独立的任务,只不过原来的任务现场信息保存在堆栈里,现在的任务现场保存在队列中。我们可以设想把当前任务的现场保存在消息里,然后把消息发送到目的地,由接收者在自己的系统空间中恢复任务现场,只要信息不丢失,任务就能在这里复活。有些系统应用程序和OS分离,应用程序通过软中断调用系统API,即OS可以动态加载应用程序;还有一些系统实现了虚拟机,隔离了硬件,屏蔽了平台差异,应用程序可以跨平台执行。在这些系统上,可以借助消息机制,使任务在不同平台间移动。例如:从主处理机移动到子处理机上执行,从中心处理机移动到边缘处理机等,从而减轻中心内存和主CPU的压力。确保安全的前提下,阵列中的各处理机并行工作,打破了速度瓶颈,信息象水一样地流来流去。
--------------
|消息就是信件|
--------------
在HJD04程控电话交换机里,我们把消息形象地称为信件。
考虑日常工作中的例子:采购了一批电子器件,首先要填好入库申请单,呈负责人签字同意,然后到财务那里报账,财务再签章,最后带着器件找库管入库,库管核验通过后签字。这时,一式三份的入库单自己留一张,库管一张,财务一张。在整个过程中共有4人参与:我、负责人、财务、库管。除我之外,另外三人并不同时在场,甚至可能不在同一时间出现(例如,第一天找负责人,第二天找财务,第三天找库管),但这并不妨碍我顺利办理器件入库,只要我有入库单在手。整个过程中,3个人通过入库单建立了联系:负责人审批,财务入账,库管入库。换句话说,入库单将3人耦合到了一起。不过3人不存在直接调用,也没有复制入库单副本,而是全凭单子上的签章作业。这样,每个人(处理机)在处理入库单(消息)后,签上字(增加消息内容),然后发给下一个人,自己再去干别的,而过程中自己并不保留入库单副本(临时中间变量)。尽管每个人都没有刻意记住曾经签过字(只在签字前花些时间思索了一下,过后可能就忘了),但是入库单上保存了所有信息,入库工作会有条不紊地按程序进行。3个人可能都不知道完整的事态发展,但他们只要在自己的工作范围内付出一些处理能力,就能促进整个任务的完成。
04交换机采用阵列处理,消息驱动机制,处理能力等效20万用户线。当主叫摘机后产生主叫摘机信,信中存有主叫号码,用户级别等信息。主机收到此信后批示申请资源(交换网络和收号器),然后将其转发给交换网络板,网络板查找空闲链路,登记为占用,并在信中批注网络资源申请成功和链路号。将此信转给记发器板,记发器板查找空闲收号器,登记为占用,并在信中批注收号器申请成功和记发器号。最后,此信又传回了主机。主机了解到资源全部申请成功,就命令向主叫发拨号音并给记发器板发收号监视信,以便收号结束后传回收号结果。此过程中各个子处理板并不保留中间信息,如用户信息,结果信息等。所有信息全部保存在信里。各子处理板处理完信件,会立即忘掉刚才处理的内容。跳过中间处理过程的叙述,一旦被叫摘机,会立即产生主叫计费开始信,此后任何一方挂机将产生计费结束信。计费台根据主被叫号码,用户级别,时段,节假日,起止时间等分段计算费用,并将话单存入数据库。为了防止进程死机,设计了一个高优先级监控进程,其他各进程定期向监控进程发联络信,说明自己是谁,表明自己还活着。一旦超时未收到联络信,监控进程就重启那个死掉的进程。
采用信件处理的方式易于理解和实现。信件可以生成、销毁、转发、回复、修改、复制,给实际操作带来了便利。只要增加新的信件种类就可以扩充系统功能,而处理机制仍然保持不变。
在消息驱动的系统中,内存非常关键,不能出错。硬件上采用双机热备份,实时纠错等手段,能够提高系统稳定性。对于有限的内存空间,最好使用动态内存管理。
文章评论(0条评论)
登录后参与讨论