3.37. 架构设计中的“少了”和“多了”的问题

3.37.1. 上篇

最近在和一些团队讨论一些架构设计问题的时候,经常被问到哑口无言。我相信我是对的,但不抽离原来的讨论逻辑,我都不知道怎么说这个问题,所以,我独立写一个文档来谈谈架构设计中的“少了”和“多了”的问题。

先说“少了”, 我发现不少外设或者外设驱动工程师,设计的时候时候从来不画状态机的,只要有稍复杂一点状态切换的,如果你不画状态机,我就认为你的设计没有做完。但他们会反驳:你说我哪里会错?有时我会挑出几个错,这些他们就会修正了。有时我会挑不出来(最终我肯定会挑不出来),他们就说,你看,我没有问题嘛。

但你明显有问题,只要你没有状态机,你就是“有问题”。为了让更多读者理解我在说什么,我们先一个例子,看看我说的状态机到底是什么:比如你写一个驱动,外面有个设备,设备加电后,需要拿到配置才能工作,进入工作状态后,有两个状态,一个是主用,一个是备用,主用的时候会正常处理业务,备用的时候仅更新状态,不会真正处理业务。这个模型用状态机建模大概是这个样子的:

../_images/%E7%8A%B6%E6%80%81%E6%9C%BA.png

我没有画完所有的元素,部分跃迁的激励因子和跃迁行为我没有画,但这不影响要表达的意思了。状态机的问题不在于这个图是否正确地表述了你的行为,状态机的问题在乎你是否做了“全集推演”。

什么叫“全集推演”呢?就是说,你是否对系统可能面对的所有情况进行过推演。比如这里这个图,我没有画之前,我是没有想起还需要加那个“强制初始化”的激励的,我最初的设想是设备加电在Idle状态,OS probe这个驱动和这个设备了,就配置给硬件,如果硬件不响应,我就超时回到Idle。但这样一来,如果硬件真的发生超时了,除了系统Reset,我就没有任何余地重新激发系统进入状态了。

你也可以说,我的硬件非常可靠,这种情况不存在。但建模不是这个样子的,软件还需要演进,“硬件非常可靠”这种依赖,对软件来说,大部分时候都不是演进中可靠的依赖(当然,不反对你有目的地强制它)。我们建模的时候不依赖这种东西。你可以考虑不写那个人工激励流程,初始化不成功了就只能重启,但你得“考虑过”这个问题,而不是等着出事。

对一个状态机进行全集推演,就是对所有可能情况进行100%覆盖的推演,比如:

  1. 每个状态是否有可能陷住,没有任何激励可以把它拔出来

  2. 用所有的激励源,对所有的状态进行激励推演,看有没有可能进入不可料状态。

  3. 有没有状态可以合并

  4. 等等

这些动作做完了,我们才会对这个系统有“信心”。

缺乏状态机建模,就缺乏对系统的信心,设计就没有做完。这就是我说的“少了”。我说某个设计“没做完,不自洽”,都是这个意思。我要给你挑出所有的状态毛病来,我唯一的办法是帮你把这个状态机的模型建出来,但这是你的设计工作,你没有做完,让我来给你擦屁股吗?不少工程师动不动就Show me the code,觉得特霸气。Show me the code是沟通失败以后不得已的合作手段,Show me the code的时候,你发现你已经错了,或者已经投片了,已经量产了,已经上市了,你找谁哭去啊?

再者说了,Linus说Show me the code,人家也不看你的code。不说别的,每个公司给他一个网卡驱动他就看不下来。人家更关注架构逻辑呢,Show me the code是用来羞辱你的,你有那本事去到处羞辱别人吗?

说远了,回到状态机这个问题,这个事情其实比我这样表述出来微妙。首先,画状态机不是个通用任务,如果我提出一条规则,“所有的设计交付中必须包含状态机定义”,然后你根据你的代码写一个状态机,这个图解决不了任何问题,你还是没有做全集推演。本文的评论中就有人认为状态机是详细设计的工具,还有人认为类似定义是为后来的工程师更容易维护系统,这些都缺乏对架构设计重要性的认识。设计方法首先是为设计过程服务的,“科普”和“设计”是两个东西,“科普”离“设计”还很远,科普是事后嘴炮,设计是在无数个可能性中正确抉择。所以,不要以为DFD,STD很简单,只有你真明白。只有这些东西在你做开发的过程中起到不可替代的作用,你才是在做架构设计。代码都写完了,补一个状态机描述,这个描述和“产生代码”这件事没有任何关系,这就不是设计,这是科普。

大部分系统,其实不是只有一个状态,如何划分状态的维度,是个没有什么定势的问题,只能具体问题具体分析。某些简单的状态变化是否需要状态机推演,也只能具体问题具体分析。最后是,状态机模型是那种“你建还是不建,它就在那里”的东西。你不做状态机建模,但对很多系统来说,不建模是非常危险的。我曾经给一个中断控制器复盘它的状态机模型,写了我十几个状态,因为它设计的时候对外的响应在不同阶段是使用不同的内部控制因子的,你简单做几次状态机推演,你就会知道这样会把你的状态机(的状态数)扩展到什么地步。天道好还,这家伙果然惹麻烦了,Linux内核升一次级,就把它弄挂了。你总想世界是不变的,变了是别人的错,但别人的错就是你的错,它们同出而异名,要论耍嘴皮子,你永远耍不过这个世界。

后面这两点,其实都是第一点的解释,我们要了解什么是架构设计,首先要明白,架构设计是设计过程中不可或缺的步骤,而不是表面上看到的样子。所以,有一个很简单的判断架构设计好不好的方法:你不写这个架构描述了,是不是后面的编码就做不好了,如果你写不写,其实效果都差不多,这个架构设计就没有必要了。

我这两天在欧洲拜访一个合作伙伴的Kernel Maintainer,除了对齐一下设计上的合作策略外,聊了一些设计理念的问题。我问他写不写设计文档或者架构定义。他说他不写,原因他说,首先,软硬件接口已经有“架构设计团队”负责了,他们会推演整个软硬件之间的接口,那个部分已经有很多的限制了,Linux这一则,该有的框架已经稳固,变化的部分则不太受控,这种情况下,代码之外的设计意义就不大了,所以,很自然的,他就不写设计文档,直接上代码,内部Review,然后就出lkml review了。

所以,架构设计之道,存乎一心,肯定不是你学个样子可以学会的。

状态机问题,只是“少了”的其中一个例子,大部分设计都需要“全集推演”,设计的所有可能性判断,代码可以被移植的范围,代码可以被应用的方式,都需要“全局推演”,从而证明没有“少了”。很多人有时并不是不知道自己缺了这些推演,他们是不懂,或者没有那个经验。但他们真正不懂的,并非是不懂这些知识,而是不懂“守弱”!不知道自己可以勇敢地承认自己不懂的。他们的世界太死了,项目交付点是不可更改的,领导的要求是不可打折的,《编码精粹》的原则是不可动摇的……谁说的呀?交不出来的代码,会因为项目计划而交出来?领导的要求错了,领导最后会承认错误而不是告诉你“为什么不早点提醒我”?符合《编码精髓》的原则除了让你在知乎装逼有人给你加工资?

在全集推演中,“守弱”的作用在于,你清楚表明了:这个东西我推演了,我不懂,我没有精力做,我暂时轻信某某某的判断……所以我没有做,我背上这个风险,但它不影响我整个设计“逻辑自洽”,这样我的设计没有“少”,只是不够“精”,但我完成的设计是有价值的,因为别人很容易加进来参与这个设计,我也可以在不断取得的新的筹码和信息中保持我们的设计方向,这是个正向发展的过程。但如果我说了一堆“有板有眼”的设计,其实都有逻辑缺陷,都只是部分正确,其他人以及投资的发展就根本帮不了你,这样的设计就是一坨shit,鸟用没有,还不如直接看你的代码。“少了”的设计,一旦内容扩展了,就不是“少了”了,而是“乱了”。同样的问题一样存在于代码中,只是没有设计文档那么明显而已(毕竟可以运行)。

最后说远一点,很多中国学生,工程师写的论文和西方工程师比,差距也在这里。我们经常直接写结论,而不给出处,不说理由的。他们的“索引”只是泛泛放一些“大拿”的资料,以示自己的“传承”,不是用来当证据的。这些论文,你看了一大堆的论点,但你根本不知道这些家伙的这些论断有几句是可以相信的,这样的论文,也是一坨Shit。我们很多设计,代码,和西方竞争,短板也在这个地方。最近比较热的什么太极打MMA也是这个问题,以己昏昏,还不欲使人昭昭,最后都是一坨Shit。

没有逻辑链,就没有工业化,没有现代化!

3.37.2. 下篇

承上篇,接着谈“多了”的问题。

再找一个中断控制器来当例子(这个东西架构成分比较重:)),有人设计了一个中断控制器,硬件上大概是这个样子吧:

../_images/%E4%B8%AD%E6%96%AD%E6%8E%A7%E5%88%B6%E5%99%A8.png

好了,虽然不是我构思这个文档时的本意,实际上我已经可以开始谈这个问题了。这个图中,当我描述“中断系统内部总线体系”的时候,如果我们在讨论ARM的系统,这后面其实还有GICR和GICC的问题,还有Stream Protocol的能力限制问题。但如果我在画这幅图的时候,如果你把这个要素引入进来了,我就认为这个设计写“多了”。

系统设计里的要素极其复杂,少了是个错(这个我们上篇谈过了),但我们可能都没有注意到,多了,哪怕多的是个正确的信息,这个设计也有可能是错的。

现在假设我要做的是对图中那个“中断控制器”的配置/控制接口方案进行推演。这个控制器的目的是在基于ITS协议(ARM GIC Specification),把设备的中断线信号,转换为ITS的消息信号。这个地方的难处在于,GIC(Global Interrupt Controller)是有多个版本的,从构架设计的角度来说,你不一定需要遵循特定版本的要求。配置信息从CPU下发下来,是要通过操作系统的,操作系统也会产生限制,但OS的代码也是不断升级的,那些东西也是可以改的。这样,在建模的第一个阶段,如果你引入了OS的限制,我也认为你“多了”。

这样,这个事情就变得很微妙了,首先,根据经验,可能确实有些协议限制和OS限制是很Solid的,我第一阶段推演不考虑它,但第二阶段推演我是需要考虑它的,到时你又要跟我说“早说过了XXX”,但这个问题不是这样的,我这个阶段是要判断最优模型是什么,和最优模型对抗的都要“推倒”,但推倒的过程会遇到反抗,我要权衡代价。开发的过程就是消除反抗的过程,我要计算的不是“是否反抗”,我要计算的是“反抗投入和收益的对比”。所以,我首先要推演的是最优模型,而不是所有的限制。

有些工程师一直在边缘生活,所有限制都是他的限制,任何第三方都是他的老大,惯了基于所有条件来生存了,但这样永远都会被压着打。这对于没有条件竞争的企业来说只能这样,老老实实当打杂的就好了,打杂还是比造反吃得好一些的。但如果你要参与竞争,还是这样的设计理念,那就只有等着被对手踩在脚下了。

所以,做架构推演,太早引入额外的“条件”,是会损害竞争力的,这是“多了”。

回到上面这个模型,唯一真正限制我们的首先是“目的”,如果目的不存在了,所有限制都不存在。法拉利很贵,但我不开车,贵不贵这个事情就和我没有关系了。我的设备是线中断的,要转换为基于总线的ITS中断,这是硬需求,这个才是我的限制,和ARM Specific的设备互联,这个也是目的之一,这个目标也可以作为限制。所以,现在我要解决的问题是,怎么把我的中断线(line_id),转换为OS(这里用Linux内核来推演,其他OS类似)能够认识的全局唯一中断(irq_id),这个算法才是核心,用DFD图表达就是这样的:

../_images/int_dfd1.png

这个其实不是DFD图,因为它不自洽,你给定一个中断线号,不可能可以转换为Linux期望的中断号,所以这个号由Linux(从上面的图看,就是CPU)告诉中断控制器。

所以,完整的DFD应该是这样的:

../_images/int_dfd2.png

你看,只要你的需求还是line_id到irq_id的转换,这些才是你的基本依赖,其他那些,都是可以下一步再谈的。一旦我们把这个逻辑确认了,其实对每个部件的接口要求就被严格限制了:

../_images/%E4%B8%AD%E6%96%AD%E6%8E%A7%E5%88%B6%E5%99%A82.png

我们很多人推演的时候,被Linux里面的irqchip,irqdomain这些概念绑架了,推演这种模型的时候,反复拉那些概念,但你要知道,这些概念都在CPU里面,你连CPU外面的限制都没有推演完,你管里面那个概念干嘛呢?你要什么参数不能放到DSDT中,然后让设备读,然后通过任何API传递到任何一个irqchip/irqdomain的回调中的?你外面都是一团麻,你急着管里面干哈呢?

等你对核心数据确认了,那爱用cmd_queue来发这个数据,还是按io_space来放一张表,这重要吗?表里面按devid来分段,还是按一个全局的line-id index来做稀疏表,这有所谓吗?总能做到的啊。就算做不到,那也肯定有其他“数据”和“历史”方面的理由的啊。那个时候你再绕,你就可以计算成本了啊。

所以,其实,在架构设计上,“正确”的信息,出现在错误的阶段,也是错误。希望我们可以少点信誓旦旦的“能跑就好”,这种自信,其实也是我们鲜有有竞争力的软件的原因。