仓库源文

.. Kenneth Lee 版权所有 2019-2020

:Authors: Kenneth Lee :Version: 1.0

状态机退出方法


本文是对下面两个文档的实践介绍:

    :doc:`状态机方法`

    :doc:`锁使用设计`

有工程师在实施的时候被很多概念搞晕的,抓不住主线,我这里基于状态机退出的设计思 路来解释一下怎么关注这两个问题的重点。

我用一个典型的状态机模型来说明我的观点:比如你实现的是一个库abc,你可以通过这个 库分配资源x,然后对这个资源进行操作。API接口类似这样:::

    x = abc_alloc();
    abc_free(x);
    abc_op1(x);
    abc_op2(x);
    abc_op3(x);

这种接口非常典型了,因为x是有状态的,abc_opX对x的操作取决于x当前处于什么状态。 这种情况我们就需要做状态机,避免因为x在响应的时候,某种变化的情形没有考虑到。

但状态机有个基础的假设:状态的切换过程对于其他切换是原子的。比如我们有4个状态 sx1,sx2, sx3,sx4,我们遇到abc_op1(x)的时候有几种变迁可能:

  1. 如果是sx1,切换到sx2,同时做一个action:比如点亮一盏灯。

  2. 如果是sx2,切换到sx4,同时做另一个action

  3. 如果是其他状态,报错

我们认为我们要把这一系列行为都做完了,才能做另一个abc_opX(比如op2)。如果不是 这样,我们就失去“当前状态”这个概念了(因为你会存在sx1和sx2之间的一个“中间状态” )。

要保证原子的方法有很多,比如你本身就是个单线程程序,这天然就是原子的。或者你把 所有的abc_opX都先排队放到一个队列中,用一个线程去处理它,这样每个切换对于其他切 换也是原子的。在本例子中,由于是一个库,我们更常用的方法是用锁,如果abc_opX在切 换中操作到状态切换和action的时候,都加上锁(比如加上一个mutex),这个过程就是原 子的。我们在前面的链接中,提到“锁本质是一种排队”其实就是这个意思。

    .. figure:: _static/锁即排队.jpg

这是基本的框架,我们现在开始处理Dirty的问题。我们每个动作都是原子的,处理 abc_opX之间的变换,这好办,但怎么处理abc_free? 这不是状态机的切换,这是状态机的 退出,这个动作完成了,abc_opX()根本就不能做状态判断了,原子行为怎么实施?

这个问题我们前面的状态机方法链接没有说,有人就开始晕掉了,觉得“状态机”不能解决 问题了。而不是守稳:我的逻辑是没有问题的,我需要的是把我的条件适配到状态的条件 上。

既然状态机的变换需要相关数据一直存在,那么数据不存在了,当然是不允许进行状态变 换啊。

这是一个很基本的状态机维护技巧,你必须进行接口控制,你必须能够“切断信号链路”, 让刺激进不来。比如在这个例子中,你必须要求用户自己保证abc_free之后不能再调 abc_opX了。对于很多被动的系统,比如Linux内核的fd,会使用索引计数:分配给你一个 fd,你总可以通过这个fd进来,但你进来以后,我通过原子的getfd()帮你找到这个实体, 找得到fd加一,找不到你的所有op都无法实施了。这样close的原子操作中,可以删除fd在 资源池中的存在,然后fd减一。fd在减到0的时候再真正释放,这样通道切断,然后剩下的 操作者也完成自己的操作了,这个过程就结束了。

这些所有方法的核心是“切断信号链路”。这是一个独立的逻辑。

    .. figure:: _static/锁即排队2.jpg

好,现在我们再看一个Dirty问题。有人还遇到这种问题:现在我部分刺激是从中断来的怎 么办?

还是前面的思路,我们看看我们已有的条件哪里不满足。

我们前面方案的假设是:

  1. 状态机可以保证我们的行为全部和需求是一致的

  2. 维护状态原子切换的方法是通过锁排队把切换过程原子化

  3. 我们还可以通过“切断信号链路”的方法来保护状态机退出过程

部分事件来自中断,改变了哪个假设?对喽,锁所保护的原子性。怎么办?换锁呐,中断 本质不就是一种线程吗?mutex不适合这个排队了,换spinlock_irq不就结了?这样所有的 逻辑还是成立的。

我们做设计要做得丝丝入扣,就要保持我们的条件,范围,结论一直都是可控的。然后你 才真的对你的逻辑有信心。如果你对你自己的逻辑链都没有信心,一碰到问题就乱了阵脚 ,再好的理论也是帮不了你的。

这是一个例子,说明学会一个理论,和真能用它解决问题,还有很长一段路的。