仓库源文

.. Kenneth Lee 版权所有 2020-2021

:Authors: Kenneth Lee :Version: 1.2

主语问题


本文是上一篇《\ doc:逻辑闭包\ 》的延续,通过一些具体的例子说明这个概念如何应 用。

很多工程师写设计,特别是架构设计描述的空间很容易乱,你看他们的这些描述,你没有 办法判断这个设计是合理的还是不合理的。

我们先看这样一个设计:我们在CPU(令为C)中加了一个协处理器(令为A),A内部也有 异步于C的执行逻辑,有自己的寄存器,我们在程序中增加控制接口去控制这个A的行为。

细节我们慢慢加,我现在先问个问题:下面这个对A的工作上下文描述正确吗?

    .. figure:: _static/一个协处理器上下文的例子.svg

当然不正确了,因为指令的主语是软件,是CPU提供给软件的接口,CPU给协处理器发指令 有什么意义呢?(当然,除非CPU要模拟软件行为,但在我们这个例子中,显然这种模拟 并不是我们的设计意图)

有人觉得这是枝节,只是遣词造句的小问题,没有必要较真。这话怎么说呢?你不用基于 这些描述来写代码或者执行具体操作,你当然觉得没有必要较真了。但你正经要基于这个 描述来加入你的设计,这个就很重要了。因为如果我要实现A,或者我要在A上写程序,我 必须清晰知道:这个A的概念,是谁的语义空间的一部分。

如果它是A的语义空间的一部分,我对它的建模就必须包含A要面对的所有可能接口,我必 须把它可以收到的全部刺激排出来,再构造它的行为逻辑。这个行为逻辑,就是我说的逻 辑闭包:

    .. figure:: _static/一个协处理器上下文的例子5.svg

上面这个图,给出了A的上下文(在系统中的位置),我有它的边界了,我才能推它内部 的行为逻辑,没有这个边界。你直接说它收到某某指令的时候就会如何如何,我能肯定它 在收到一个比如说中断的时候,你说的这些行为还可以保证吗?更关键的是,你的定义中 ,根本就没有描述在收到中断的时候会怎么样,这个定义就是“不完整”的。所以,无论你 在定义一个接口,还是在推演一个设计的可能性。你的这个表述都是不完整的。

不完整的推演意味着设计漏洞,不完整的定义意味着用户的使用模型中存在使用漏洞,无 论是那种情况,这个设计都不可靠。就算你把代码(无论是RTL还是软件源代码)都写出 来了,不对这个代码进行全可能性测试或者逻辑分析,我们都无法证明这个设计是合理的 。如果你以前不能理解为什么说代码不能取代设计,这里又是一个例证。

用词精准(再次强调,是精准,不是详细)在设计中非常重要。上面图中描述的A,指的 是A这个硬件,按我们最初考量的设计,我们想表现的是在C的指令空间中呈现的一个A( 的行为),所以它的上下文是这样的:

    .. figure:: _static/一个协处理器上下文的例子2.svg

我们在CPU的指令空间上创建了一个控制某个虚拟实体(A)的子空间,这个子空间就必须 和C的闭包一同推演,保证它和原来的闭包逻辑是可以互相包容的。这个原来的逻辑闭包 的边界是这样的:

    .. figure:: _static/一个协处理器上下文的例子4.svg

如果我们直接看到,协处理器指令是一种CPU指令,那么,对CPU指令的所有约束,也对协 处理器指令成立。这些约束包括而不限于:对指令顺序依赖,原子性,特权级,异常处理 ,中断响应的行为,也同时生效。我们推那个中每条指令的行为的时候,就要对这些东西 进行推演考量。

比如说,我们需要考虑我们发出一个其他的CPU锁总线指令,然后发出一个协处理器控制 指令,这个总线锁会不会被释放?如果你不把协处理器控制指令放到和CPU其他指令的同 一个闭包中考虑这个问题,只在你的那个协处理器空间里想:我给它发了请求,它是不是 响应我了,我又怎么回应它了……你会觉得你的逻辑挺完美的。因为你没想到,实现CPU的 那个人可不是你这么想的,在他的逻辑闭包中,他是要同时满足你新加的这些指令的要求 ,还要满足他在其他逻辑中满足的要求。我们重视逻辑闭包,一方面要保证我们我们如果 不改变某个闭包的边界,我们就不需要打开它。另一方面就是要保证,我们如果改变了它 的边界,我们一定打开了它。

我们再从程序的角度看看这个概念,你加了一个A,暴露给程序,程序总得有个视角看见 它吧?这又是一个主语,我看看它怎么看待这个问题,比如我构造这样一种可能性:

.. code-block:: python

def do_some_work(): m_data = c_prepare_data() c_call_a(m_data, cond) = { a_buffer[0] = m_data a_step1() a_step2() a_step3() a_step4() a_notify(cond) m_data[0]=result } c_wait(cond) return result

我这里,c开头的指令,是CPU执行的,a开头的指令是协处理器执行的。m开头的是内存数 据,被两个对象共享。我们这个有多少个主语?这就构成多少个逻辑闭包:

  1. A的硬件(活在C的指令空间中)

  2. C的硬件按

  3. 异构程序程序员对系统的逻辑空间

  4. C程序的逻辑空间

  5. A程序的逻辑空间

比如我们单独分析一下C的程序视角,我们怎么看这个问题:

你上面这个异构程序的逻辑,要求我去call_a,我就考虑在我的逻辑空间中,我怎么:

  1. 把程序指配给A?

  2. 把输入数据指配给A

  3. 从A中获得输出数据?

  4. 我如何定义我的编程接口?是否图灵自恰?是否支持函数调用等高级功能?

  5. 我能否访问内存,我访问内存的时候和C是什么关系?和其他CPU或者其他Bus Master 是什么关系?

  6. 等等

你看,你从上帝视角回到一个每个“小人物”自己的生存视角,你就会发现你要想的问题多 得多。站在皇帝视角你问农民“何不吃肉糜”,站在农民视角,他考虑的是从哪里得到肉糜 。你要推演一个方案是否可行,要把每个切面的逻辑的可能性给它打通了。

你这样推演问题:

    | C先调用load_a_app()指令,把A的代码加载到A中,然后再调用exec_a(),
    | 让A进入执行状态,C停在exec_a()上,如果A发生内存访问,就用自己的
    | 内存处理单元(LSU)为C进行内存访问,如果内存缺页,就进入异常处理
    | 向量,进行缺页处理,然后返回A继续执行。

这东西啊,你把它看作是个\ :doc:../道德经直译/恍惚\ 你还觉得它写得挺详细的。 你正经要用这东西写代码,你就会发现你根本不知道它在说啥。我写A的函数的时候,我 只关心我要完成的工作,我需要的是这些指令图灵完备性,我要知道的是我的入口在哪里 ,能不能调用函数,我能不能反过来调用实现在C一侧的函数,在我执行的过程中,C有可 能对我发出什么控制每个这种控制我应该如何响应。我可能基于这样一个程序模型来想这 个问题:

.. code-block:: python

def a_entry(a_input): a_inst1() a_inst2() a_inst3() a_inst4() other_function() return result

def other_function() ...

def event(c_event): case c_event: on event1: ... on event2: ...

我考量这个对象的时候,我关心的是我能否获得我的输入,有没有地方给出输出,中间能 做什么行为……这样我才能封闭我针对这个对象的闭包。并形成在整个系统中我对别人的要 求。

然后我分析C的程序空间呢,我考量的是这样一个程序模型:

.. code-block:: python

def c_call_a(a_input, a_app): c_load_a_app(a_app) c_load_a_input(a_input) c_call_a() #hold until finish return c_load_a_result()

def c_fault_on_a(where, fault_type): case fault_type: on page_fault: load_miss_page() on sys_error: report_error() exit()

这个模型我关心的是我能否把程序,参数,调用等信息传进去,我还关心出了缺页或者其 他异常的时候,是否每种情况我的程序都是受控的。

而对于硬件A呢?我们关心的是这些所有外部刺激对我们的要求,我需要做的是它的状态 机要求,我要把所有可能的外部刺激都列出来,然后看看是不是在每个刺激下,我都是有 我预期中的响应的:

    .. figure:: _static/一个协处理器上下文的例子6.svg

由于我不是真的做这么个产品,我也不推到细节上,上面这个只是个示意。我们先把所有 可能刺激A的行为的激励因素都给出来。然后我们把这些要素在每个状态上看看它成为系 统的激励,系统都可以有正常的反馈。如果你在用户手册中给别人定义这么一个状态机, 你缺少了几个状态的上某个刺激的说明,为A写程序的人就只能强行用软件手段给你关闭 这些刺激(这不一定能做到的),所以你以为你没写不影响,但其实没写就是写,你不严 肃,就是要外面的套子严肃,套子严肃不了,就是使用者承担这个损失。很多时候,这就 反映为Bug。

我们把问题再推一个复杂度。假设我们的A不完整,当你调用A的时候,它要使用C的MMU进 行地址翻译,所以我们的打算是这样的:C在内核优先级调用A,然后C让出自己的大部分 硬件停在调用指令上等A结束,当A使用MMU的时候需要使用C的MMU来完成地址翻译。这里 ,我们引入了地一个问题:C认为A处于内核优先级,还是用户优先级?

这当然需要问需求方了,假定我们的需求是要求把A限制在用户优先级,那么A(中的程序 )访问内存,MMU就应该认为这个访问在用户优先级访问的。但如果A(中的程序)没有这 个权限,就应该发生缺页,但一旦缺页,我们并没有定义A缺页的概念空间是什么。

我们可以认为这个缺页由C一侧来响应,但C并不认知A的行为,对C来说,它调用的是一条 c_call_a()指令,并且在这里等待a结束,所以MMU缺页想使用C的概念空间,我们只能认 为是c_call_a()触发了这个访问异常,但对MMU来说,一条内核优先级的指令发生了一个 用户态内存访问异常,它应该如何自处?

很多人就被这个问题绕晕了。为什么你会被这个问题绕晕?因为你还是在上帝视角上,认 为自己是一个人。但这里有好多个对象。在A的视角上,我访问内存,我对MMU的要求就是 你按页表用户权限给我要求就行,我不管你其他细节。这个逻辑闭包已经关闭了。

对于MMU,我现在认为执行体在用户空间,我触发异常,对CPU来说,现在我的状态要先切 换回内核特权级,并且停留在c_call_a()上,MMU的工作已经完成了,至于为什么一个特 权级指令出发了一个非特权级指令异常,这是特权级向量的问题了,只要在这里能区分, 这个问题就没有了。

但对于CPU的实现者来说,他还会面对这样一个问题:如果c_call_a()指令发出的时候, 它同时把这个状态广播出去,给了它的每个执行部件,说老子现在是内核优先级,然后才 开始让度执行功能给A,这在A执行的时候C就会觉得不知道怎么弄了,C的设计者就会觉得 这个需求不合理,觉得自己无法控制这个优先级了。说到底是你自己把C内部的逻辑空间 的关联关系弄成一堆麻。你没有确定的方法在每个时刻确认你自己的执行部件处在什么状 态上。如果是这种情况,要不你就一点点梳理你的设计,知道你在刚才这个A-C切换过程 中,能否让硬件切换特权级,要不你就只能承认,你已经把系统维护成了一团乱码,你没 有办法了。(但很多人就是不能接受这类结论,讨论的前提是“我不能被评价我的设计有 错,这我们就没有办法了)

架构设计都不是在做细节设计,但它的设计的目标就是为细节设计服务。所以,对于架构 设计,语义的精准变得非常重要,因为更多的细节的发展都以来这个语义的精准。这不能 靠补充细节来实现。而语义的精准首先要保证的是主语的精准和你对主语面对的逻辑闭包 的全集覆盖。

另一方面,在实践上,架构设计的过程很难是一开始就能控制所有细节的行为,它只能给 出一个期望,然后向下挖一层,看看这个期望的反馈是什么,然后再互相平衡,让每个独 立的逻辑空间变成闭包自恰,我们要分离这些逻辑闭包,先以每个用户或者实现者的眼光 观察它作为主语的视图,是最直接的分离每个独立闭包的方法了。

.. vim: set tw=78: