仓库源文

.. Kenneth Lee 版权所有 2016-2020

:Authors: Kenneth Lee :Version: 1.0

让代码变立体


最近刻意回答了一组问题,用来支持本文的讨论:

  1. 编程中把变量以二进制存储在文件中和变量在内存中的结构是一样的么,C语言中的结 构体

  2. 涉及字节对齐的问题,存储为二进制文件时还有这些问题么? - in nek 的回答C语言 中的 最受限制类型 和 最小的对齐限制 什么意思?对象的对齐要求,是只有在结构或 联合中才有的吗? - in nek 的回答

  3. CPU检测到中断信号时,怎么知道是发给哪个进程的? - in nek 的回答4. Linux 中 mmap() 函数的内存映射问题理解? - in nek 的回答

其实这个也是这个问题:KVM他说的基于内核的虚拟机是什么意思呢? - in nek 的回答

这些问题,全部都是名称定义问题,人很容易被来自另一个名称空间的东西影响了自己对 本名称空间的事情的判断,这就是为什么原题的题主和下面一些答案在问题上纠缠不清, 不少人不注意每个定义的范围,或者对名字的唯一性有不可动摇的依赖感,这样的结果就 是,名和名纠结在一起,然后你就看不到(名不出)其中的“道”了。

我解决这个问题的方法是,把平直的代码“立”起来。上面我列出的回答中,我都尝试先让 名字在一个简单的语境中被解释,然后谈它的下一个变体,然后再谈下一个变体……这样, 一个这样的代码:

    .. figure:: _static/代码立体1.png

可以变成这样:

    .. figure:: _static/代码立体2.png

同一个代码,从架构的视角中,就是完全不一样的,我把这称为,把实现“立”起来。这实 际是实现过程的反向拆解,因为架构的生成过程就是这样的。你一个内存访问,本来就仅 仅是在地址总线上发起一个读写请求,但加上多层的cache,语义就有一次变化,加上总线 原来的基于时序的读写改为基于Burst的读写,语义又有一次变化,编译器对读写过程进行 合并,又是一次变化,CPU执行多发射,又是一次变化。你非要用一个逻辑把这些所有的变 化描述出来,你完全没有机会清楚地把这个逻辑表达出来。

软件很多概念空间的化简,都基于这种技术,比如分层,就是一个典型,Ethernet头,不 认识IP头,IP头,不认识TCP头,这样,每一层可以聚焦一个逻辑,把那层逻辑的优势充分 利用起来。这样其实是影响系统的性能的,但这样同时降低了熵增(参考前一篇《反者道 之动》),留给了软件更多生存下去的余量。我前面的博文中举了不少例子了,很多人在 分层间加飞线来提升软件的效率,这是一种“使用软件”的方法,并非完全是一种开发软件 的方法。当然这两者的界限不是那么清楚就是了。

但分层是一种很简单的立起来的方法,表达能力远远不够。

前段时间有一种新的编程概念很流行,叫面向“角度”的编程。大概的意思是让程序分不同 的角度来写,比如主流程的功能逻辑写在一个模块中,日志和跟踪逻辑写在另一个模块中 ,备份和恢复的逻辑又写在一个模块中。这其实更贴近人的思维,你看代码的时候,其实 也总希望看望懂了主流程,然后才开始以此为基础看跟踪功能怎么做的,看备份功能怎么 做的。当然,我认为面向角度的编程现在发展得并不成功,需求是存在的,但如何展现为 线性代码的形式,是个非常困难的问题,现在我认为真正解决得比较好的是git,我们不要 简单把git类比为cvs一样的版本管理工具,实际上git背后是Linux的版本管理策略,它要 求每个Patch都是一个独立成型的“角度”,说明的是一个增量的过程,所以这个Patch可以 不是一个线性增长过程的一部分,而可以在一定程度上被拉到任何同源分支上的一组独立 描述。这时最接近面向角度编程原始诉求的实现了。

但这还远远不够。

在编程语言可以解决这个问题前,现在真正能把代码立起来的,就是架构设计文档了。所 以,这里想讨论的是构架文档的写作策略问题。我们已经看到了,真正能解构一个设计的 是把代码立起来,而软件架构本身就是这样发展的,比如我们前面举的那个访存的地址, 又比如Linux的softirq,最初它的含义仅仅是bh,表示开中断控制器到CPU中断返回的一段 中断执行过程,后来变成了一组不可重入的多CPU中断处理例程(理论上处理压力可以被分 解到其他CPU核上),后来又变成可以线程化,softirq就不再具有原来那个简单的语义了 ,它是独一无二的,softirq就是softirq,不看代码谁都说不清它是什么,而且这个代码 还不断升级,没有人承诺它一定会如何,但我们总是保证它基本上不会违背原先的承诺。

从这个角度来说,我们根本就不应该像代码一样维护构架文档,让它变成一个不断被修改 的对象,这样得到的架构设计文档就是平的,和我们想解构的代码一样。我们需要它是立 体的,就需要把构架文档分层,和分版本。就是说,我们就不应该认为构架设计文档是和 代码一样升级的,我们是通过写新的增量设计文档,来描述我们如何升级原来的设计,而 之前版本构架设计,仅仅应该作为第一层模型来使用,并仅仅做一些Bugfixing的修改。

这将称为在软件飞速发展以后,构架设计的新常态。现在新的标准,比如ARM的GIC的标准 ,PICEv3的标准,已经都是这样写的了。而我们如果尝试用一份设计文档对应一份代码这 样的形式组织我们的构架设计,最终只会把整个架构维护成一团乱麻。

这是一方面。

另一方面,从这个角度看这个问题,我们也更容易控制我们在当前版本的构架设计中应该 写什么——它应该写——为了满足用户核心需求,某些设计部件的最小承诺。

我们常常有人纠结于“架构设计应该写到多细”,但如果从这个角度看,这个根本就不是问 题。架构设计是架构师,根据所认知的核心需求(也应该表示在架构设计中),对模块的 第一层分解,表述这些被分解的视角(或者模块)为实现需求需要满足的最小承诺。

架构设计写到什么程度呢?它有两个终止条件:

  1. 这些约束要进一步分解的内容,用代码比用文档表述能力更强

  2. 这些约束,不是本名称空间的一部分,对那个约束进行细化,可以在这个约束之内自恰 地被分解。就如同TCP只需要懂IP层给它的承诺就可以了,它不需要知道IBTA层( RoCEv1的传输层)的细节,这样,在架构设计的这一层(或者多层),就可以形成立体 结构的其中一个位面了。