3.264. 逻辑如水

3.264.1. 介绍

本文推演一下子系统(包括软件不同模块,软件和硬件,软件和芯片等等等等,所有这些可以被我们单独看作一个独立系统)间的接口的形成规律。

讨论这个问题的背景是和别人讨论未来软硬件的接口应该怎么发展,我觉得要说明白我的观点,需要先建立很多基本的逻辑,而我自己其实也没有完整地建立过这些逻辑,所以我通过写这个文档让我的观点变得Consistent一些。

3.264.2. 精确和数字化的关系

我们先来讨论一下什么是“精确”,比如我们看到下面这样一幅图:

../_images/%E7%B2%BE%E7%A1%AE1.svg

图 3.5 模拟的特征

你有什么办法描述它吗?我想是没有办法的。因为你根本没有办法在另一个系统中严格去匹配它,更加没有办法在我们的脑海中严格去描述它。

也许你可以用一张纸蒙在上面,描绘它的样子,但这个图在放大后可能是这样的:

../_images/%E7%B2%BE%E7%A1%AE2.svg

图 3.6 放大后的模拟特征

你描红出来的线还是没法彻底还原它。

这就是《道德经》中说的“鱼不可脱于渊”的概念。真实系统中的信息是不能被精确复制的。但我们的脑海可以,我们把真实系统中的“道”,全部解释成了“名”,“名”是可以有相等的概念的。但脑海中的名相等,不表示现实中的“道”相等。

数字化是一种更加极端的“名”(正因为极端,所以好用),我们可以通过理解数字化的过程理解这种“名”的概念。

现实中其实不存在“1”这个物理实体。现实中要不就是一个苹果,要不就是一架飞机,没有1本身,1是我们脑子里的概念。但1=1是精确的。

备注

值得说明的是,我们可以认为,当1出现在我们的脑子中,它也成为了物理现实的一部分了。因为它的存在确实可以成为改变客观世界结果的一个原因。

数字化好用在于,数字化很容易被人自己控制。我们前面例子中的曲线,如果表示一段音频,我们很难在另一个系统中严格重现它,很难用其他系统去记录它,但如果我们用.wav格式去表示它,把每个时间域记录为一个确定的数字,这样,我们可以在不同的系统中记录,或者重现它。图示起来是这样的:

../_images/%E7%B2%BE%E7%A1%AE3.svg

图 3.7 模拟特征的数字化过程

我们忽略了它的其他细节,我们就把这些信息描述为一组数字,然后我们就可以“精确”定义它的匹配和不匹配了。这样,一个声音在这样的数字化系统中,既可以是晶体管的电平,也可以是磁道上的磁极方向,还可以是文件系统中的一个压缩Block(是的,不同系统中的信息甚至不需要一一对应)。我们通过忽略细节这样的“抽象”方法,获得了信息在各个不同系统中轻易流动的便利。我们通过了失去信息,获得了“精确”。所以,把模拟信号的音频,变成了数字化的码流,我们获得了“高保真”。

探讨这样一个过程,能让我们意识到。所谓的“精确”,本质是我们自己的YY,它真正的作用不是精确,而恰恰是通过不精确,增加了它的流动性。

这种观点同样可以推广到其他基于“名”进行通讯的系统,只是“名”没有数字这么极端而已。

所以,信息在不同的系统之间流动(和处理),其实受限于两个东西:

  1. 信息的数量

  2. 承载信息的实体(无论是物理实体还是抽象实体)本身的处理这种信息的能力

这两者决定了我们人类创造世界的时候,每个实体的边界在哪里。

备注

请注意,我这里说的是:人类创造物和世界创造物是不同的,因为自然世界没有人脑的限制,而人脑才有这个数字化的需要。这也是我们常常很容易分辨出人类创造物和自然创造物的区别的原因。)

3.264.3. 名字的信息范围

我们再探讨一下第二个问题,“名”表达信息的方式。这个在道德经里也有详细的推演了。说到底,名称是“对比”出来的,是我们有某个需要,在已有信息中补充我们关心的信息而生成的。比如说猫这个名字,字典是这样解释的:

哺乳动物,面呈圆形,脚有利爪,行动敏捷,会抓老鼠。

我们注意一下这个解释,猫这个字,是名称,而这个名称包含了一组信息:哺乳动物,面呈圆形……我们注意一下这组信息,里面是不包含“猫的肚子里面有花花肠子”这个信息的,难道猫的肚子里面没有花花肠子吗?为什么这个信息为什么不在里面?

这通常有两种可能:

  1. 这个信息依赖其他信息,比如“哺乳动物”

  2. 这个信息不是这个名字想讨论问题的一部分,被放弃了

这里描述的问题很简单,但我请读者注意一个很多人都会忽略的问题:名称的信息集合是具有依赖性的。但依赖不是包含。猫的定义中不存在哺乳动物的定义。一旦哺乳动物的定义被删除或者修改了,猫这个定义就要发生改变。名字的信息空间包含了信息,但这些信息依赖其他空间的信息。

这个依赖树一直跟踪下去,最终会到达一个“共识”空间。如果你从来没有看过猫,我怎么给你解释,都解释不清楚猫是什么。因为我们落不到“共识”空间上。也许你看过狗,我可以给你解释:猫和狗差不多,但要小一些,能怕书,不会游泳……等等。我们的共识空间落在了“狗”上(当然不仅仅是狗),当我的整个定义的基础变成了“狗”,我的“猫的名称空间”,就完全不同了。所以,基础空间对上层空间的渗透是无孔不入的。

比如,你不要觉得Android可以独立于Linux存在,Android是建立在Linux的逻辑上的,Linux的概念就可以像水一样,无所不入地侵蚀整个逻辑空间:你有进程的概念吗?你的进程可以Clone吗?你的进程用到uid,gid的概念吗?你的进程调度依赖到优先级反转规避算法吗?……你只要依赖一个逻辑,这个逻辑就渗透下来了,这个逻辑本身定义了什么是“Linux”这个名字。所以,依赖这个东西是无孔不入的,你要不别用这个概念,你用了这个概念,你就和那个系统绑定了。你可以说,我可以重新一个新的操作系统,把所依赖的概念都实现了,等这些概念足够多的时候,你做的很可能就是原来那个系统了。

备注

当然,Android对Linux的依赖不算太重,但显然也不是太轻。

备注

如果上面这个例子仍过于抽象,我们用一个更具体的例子来举例。

在PCIE标准中,开始的时候延续PCI标准的定义,用Bus-Device-Function三个数字来表示一个抽象的设备(或者用PCI的概念来说,应该叫“功能”)。其中Device是物理上的设备Function是抽象意义上的设备,你可以在一个物理的硬件上,同时做两张网卡,这就成了一个Device,两个Function了。这在开始很简单的时候没有什么问题,但发展了以后,部分设备功能复杂,标识消息类型的Tag位不够用了,它反正只有一个Function,就借了这个Function ID的内容当作Tag的扩展。这个修改在我们现在这个上下文中其实影响不大,反正消息发到物理设备上,再由设备自己解释Function然后把消息转发出去的。但因为Function这个概念已经被接纳了,很多地方都无法处理这种情况,于是,在PCIE5.0的标准中,就只能说,这个功能(叫Phantom Function)使能后,如下功能不能使用:ARI,VF,ATS,IDO。这就是技术上,概念对其他概念渗透的一个直接的例子。

所以,如果我们要创新,就要尽量找出某个子系统的关键依赖(概念),然后把那些概念(包括它指向的所有信息),都重新在你的“基础”(比如物理系统上),模拟出来。而你的基础,很可能和你要创新的那个系统的基础是一样的。这种情况下,这种创新会非常困难。

在猫的定义中,如果你的目标不变,也是想突出猫(相对狗,公鸡等)面圆,利爪,敏捷;基础也不变,也用了哺乳动物这些概念,你要得到一个新的猫的定义,其实是非常困难的。因为逻辑本质只是信息的不同分割方式。你的目标和基础空间已经把分割区限制得死死的了,中间再切割的自由度就很低了。

要创新,我们就要找到目标和依赖信息集合两者之间重大的Gap,把那个依赖整个去掉,这样我们才有创新的空间。

比如Linux都认为内存是易失的,但如果我们普遍使用非易失内存。为这个限制创造的概念空间就不存在。这个基础变了,我们就会有新的创新空间:比如原来都要存盘,现在不需要了,要关机就直接关就好了,开机的时候所有数据还是存在。我们可以满足原来“关机后数据不能丢失”这个目标,同时换掉基础系统。

但我们前面说的这个信息渗透的问题是无处不在的,如果你仅仅在自己的机器上考虑是否可以关机的问题,你当然可以不需要存盘。但如果我要把数据传输到其他机器上呢?难道把你所有数据段全部传输到另一台机器上?

好吧,我就算你不在意带宽成本,就把整个数据段传输过去。但我另一台机器的代码段不一定和你一模一样啊,我们有版本升级的问题的啊。你这种不经过抽象的裸数据,我这边怎么用呢?

所以呢,如果你要解决这个问题,要不还是回到原来的逻辑空间中,你该存盘还是得存盘,机器启动,就算留着原来的数据,该清除还是要清除,数据结构该重建重建,这样你就可以和原来的逻辑完全一致了,这样你的工作量就低。要不呢,你就得建一个新的逻辑空间,在这个空间中,可能没有存盘的概念,只有“信息收缩化”的概念,进程空间里不但有数据段,还有信息收缩化段,有信息收缩和展开的概念……你看,即使是这样来想,我为了能很快给各位读者说明白这个问题,我还不得不使用了“进程”的概念,其实也许整个概念空间都可以重建这个“进程”的概念。但要把这个概念渗透给所有的代码,所有的人心,这会花非常长的时间。

这是我们需要维持架构的核心原因,信息以什么形态存储可能可以轻易改变,但每个信息互相之间的依赖关系,是个长时间的积累过程,不因为你改变它的存储形态就可以轻易改变的。量子计算够创新吧?但如果它要解决“我想看新闻”这个期望,它就得有新闻采集,新闻加工,新闻广播这种基本的概念,这是这个逻辑空间中的硬约束,要结合这两部分(量子计算和新闻传播)逻辑空间,就需要一个长期的逻辑发展过程,这个过程无法被加速,除非人工智能有质的突破。

备注

其实我认为就算人工智能有质的突破,也不见得就能解决这个问题。因为现在的情形是,对于一个产品,我们无限量提供工程师,你也无法彻底加速它的进展,也不见得可以无限扩大它的规模。

3.264.4. 信息的精细化过程

在进入正文前,让我们再讨论一下信息系统的发展特征。图3.5展示的是现实,而我们在图3.7实现了它的数字化,我们引入了22个数字(名)去逼近我们的目标(目标可以包括我们的期望和物理世界的约束)。

如果我们希望获得更好的结果,我们可以进一步细化。比如变成这样:

../_images/%E7%B2%BE%E7%A1%AE4.svg

图 3.8 细化匹配

这里我们把数字的数量增加8个,让它更贴近目标对象。这是个微积分的过程。信息系统的发展基本上就遵循这样一个过程。

比如做指令,一开始,我们说加法,仅仅就是把内存A的值加到内存B上。这是简单的期望贴近,因为我们也不知道物理系统有什么限制,我们先得能解决期望的问题再说。

然后我们开始可以投入更多的转折去让这个逼近更加贴近我们真正的期望和物理限制。

我们意识到每个计算过程大部分时间需要消耗在CPU的内部,消耗完了,我们才需要和更慢的内存打交道。所以我们就加入了“寄存器”的概念,这就增加了新的“转折点”。由于有了这个转折点,我们可以更深地匹配硬件设计上的转折。寄存器是发明出来的,但其实它的发明是被硬件设计的现实转折所左右的。从期望来说,我们只需要把内存中的值加起来就可以了,我们没有寄存器这个要求。但对硬件设计来说,在我们的数字电路设计空间内,每个部件的节拍都是可控的,我本来可以实现流水线的,但就因为这个内存的存在,我没有办法控制这个行为完成的时间,我只好让它相对独立,为自己构造一个可控的空间,这样我就可以在一定的范围内实现流水线了。

同样,我们在现实中发现程序使用内存具有局部性,我们加入了Cache的概念。这个东西同样不是我们期望的一部分,但我们在物理世界中发现它具有这样的特征,所以我们加入了转折去响应这个变化。这种依赖是可以变化的,比如说,对于流式数据访问,Cache就是多余的转折,它就会变成负担。

备注

所谓“转折”,本质上是道德经中的“玄”的概念:每个名字都是一个集合,把全集分成了有(集合内)和无(集合外)的两个部分。站在全系统的角度,有,无都是构成系统的一部分。所以,就我们不关心有和无,我们只关心分割线在什么地方。这就是玄,也是本文中提到的“转折”。在数字化的过程中,我们只关心那个数字,我们不关心集合。从信息论的角度来说,只要那个数字存在,你说它表示的是集合内的部分,还是集合外的部分,信息都是一样的。

一旦我们创建了寄存器和Cache这些概念,它本身就变成了一个物理现实,所有其他逻辑都会以它们为基础空间,这些物理现实就会变得非常难以打破。这个道理,和新民主主义运动一样,白话文,不缠足,科学观,妇女解放……任何一个新概念的提出,都会导致眼下的具体问题解决不了:分过的财产怎么办?妇女读书家务谁来做?基于礼制的各种解决问题的方法怎么用?等等等等,这些都会成为障碍。

同样的,你取消了线程,换一个Tasklet的概念,那么我长时间执行的任务怎么办?我profile怎么做?我原来get_tid()的系统调用返回什么?我的signal属于哪个上下文?等等等等,我发出100个Tasklet后要终止进程怎么搞?这些所有的逻辑,都被线程的概念渗透了,你要重建所有这些逻辑,就不可能短时间内完成。你怎么填补这个空间?

3.264.5. 模块间的边界如何形成

好了,我们终于可以讨论正题了。

逻辑概念如水一般渗透。在物理世界中,我们通过密封的容器来防止水的渗透。

在架构设计上,我们用各种方法建立逻辑闭包(逻辑闭包和抽象概念定义),把每个高内聚的逻辑封闭在一个空间内,单独给出它的目标和基础。这样,虽然它里面很复杂,但对闭包外的对象来说,无论依赖它的条件,还是依赖它的结果,都是有限的,这样我们就能够整体替换它,因为它对外的影响是有限的。

备注

其实值得注意的是,就像物理世界水箱同样可以被水渗透,这种封闭到了精细化的时候,也会被渗透的。比如Cache号称是软件不感知的,结果你看现在又要求软件知道Cacheline长度,又要求软件知道perfetch,其实就已经构成渗透了。我们这里说的防渗透,只是在一定程度上的,只是相对来说。

所以,今天,无论什么样“颠覆性”的创新,看起来改变很多东西,其实还是架在一个现有的逻辑框架之内的替换。每个基本上还能发展的系统,基本上都是由一组相对独立的逻辑闭包组成的。比如Linux的spinlock,从简单的内存polling锁,到避免不公平的Ticket锁,到避免CC协议抢锁的MCS锁,每个都有独立的创新,但这些创新很大程度上并不会影响其他模块。这是我们创新的基础,否则你就必须准备重建整个逻辑空间。而我们前面说过了,重建整个逻辑空间,你必须重做所有其他部件的创新,因为它的依赖已经不成立了。其他的,数据结构,驱动框架,驱动,DMA方法,IOMMU方法,每个都有自己相对稳定的逻辑闭包。这些都是单独的独立创新的点,但即使如此,实际这些闭包其实一直被渗透,因为外部条件一直在改变。

所以,一个接口出现在什么位置,具有什么样的特征,被两个要素所左右的:

  1. 现在已经形成的逻辑空间

  2. 用户期望和物理世界的客观特征

这两个东西不同时发生改变,你都不可能大幅度改变它。比如说虚拟机,理论上我可以在任何平台上模拟任何平台的机器。你在虚拟机中执行一条move_string指令,我的硬件不能一次完成所有内存的移动,我可以一个字一个字去移动它,一样可以完成所有的功能,但它的性能就不可能高,因为move_string其实已经被可以一次移动整个字符串的硬件的概念所渗透了。你不用一样的基础设施,你就会有更多的转折,你就必须付出额外的代价(性能,功耗,面积都行)。从这个角度看,一个系统的概念表面上看互相隔离,其实一直都是平的,是可以互相渗透的,因为一个子系统,作为逻辑闭包,可以存在,必然依赖它的基础概念和它的目标。而它这些基础概念的成因,恰恰就是外部系统的客户约束。

世界一直在进步,条件一直在变化。所以,其实我们用这样的思维去看待一个新时代的创新:我们重新分析我们原来的期望,然后我们在我们现在的概念空间中,找到已经不需要的依赖,然后把这个依赖删除,它就会构成一个创新。

但你不能指望,你可以定义一个“完美的抽象机器”,然后让所有不同的硬件用不同的方法去“适应”这个完美机器的要求。因为你不让硬件的约束渗透过去,硬件就无法完美逼近这个目标空间。

而你硬件自己要取代软件去实现当前的期望,软件每个逻辑闭包都摆在你面前,没有人拦着你把它替换掉。

所以,在当前的位置上,硬件要向上吞掉软件的逻辑,或者软件要向下吞掉硬件的逻辑,这都不是不可以。但首先你不能指望你可以重建整个逻辑空间(至少你看完你打算重建的部分的逻辑再来作这种构想对吧?),其次,你只能把那个部件的逻辑闭包找出来,对那个逻辑进行单独的建模分析,设计你的方案,这种创新都是快不来的,这本来就是这么多年来大家都在用的方式。

同样的道理对OS和应用间的接口接口,虚拟机(语言虚拟机也好,Hypervisor虚拟机也罢)和被虚拟系统直接的接口,开发库和应用之间的接口,都是成立的。这个地方没有捷径。你要捷径,你必须大幅成片砍掉需求,或者改变物理条件限制,如果做不到这两条,就想简单改变集合的分割方式就改变现在的模型,那你就需要掂量掂量发明新逻辑的工作量和时间了。