随手记——静态Flag变量使用不规范导致的重大流程错误

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

1 问题引入

1.1 问题表象

问题出在使用PCIe设备的应用层,而且该问题是一个很偶现的问题。该问题出现时的表象是业务模块获取到的PCIe Bar空间基地址为NULL,并产生了任务异常(额,居然没判空就用了)。

1.2 问题背景

为了能够将该问题表述清楚,需要交代一下问题出现时的场景。

如下图所示,硬件上分为两个处理器,一个主处理器,一个从处理器。主处理器上p0进程为主进程,控制着主要的时序逻辑;主处理器p1进程负责主处理器上连接的PCIe设备的用户态服务。从处理器p0进程负责从处理器上连接的PCIe设备的用户态服务;从处理器p1进程负责业务逻辑,并且依赖PCIe的服务。

任务异常就出现在从处理器的p1进程上。

主处理器p1 主处理器p0 从处理器p0 从处理器p1 Msg1:PCIe初始化完成 Msg2:PCIe初始化完成 Msg3:所有PCIe任务初始化完成 主处理器p1 主处理器p0 从处理器p0 从处理器p1 进程间消息交互

2 问题分析和定位

查看业务层代码发现从处理器p1进程出现任务异常的原因:是主处理器p0进程通过一系列流程(简化之后就是上图的Msg3消息)告诉了从处理器p1进程所有PCIe任务已经初始化完成,可以进行下一步操作。因此,从处理器p1就进行了下一步操作,这步操作就是向从处理器p0进程获取PCIe设备的BAR空间基址。但是,问题出现了,获取到的基址为NULL,并且直接引用了这个地址导致任务异常。

那第一个思路肯定是现在就去确认当前的PCIe设备BAR空间是否真的是NULL,手动查看之后发现并不是NULL;而且查看从处理器p0进程初始化PCIe的打印,发现BAR空间基址也是正常的。

那么,真相只有一个——从处理器p1进程获取BAR空间基址的时间点过早(偶现)——一个典型的时序问题。

根据这个思路,我们再看上图。满足Msg3发送的条件是Msg1和Msg2发送成功(其实,还有好多业务条件也需要满足,前期也查询了这些业务条件都没有问题才开始怀疑到这里的,定位问题的真实场景往往都是道阻且长)。所以,只需要出一个定位版本持续压力就可以定位Msg2是否有问题了。

定位版本中只需要在发送Msg3前判断从处理器p0维护的BAR空间基址是否为NULL就可以了。定位发现,果然出现了NULL的情况。那么可以断定Msg2出现了问题。


时序问题,要么是事件触发错误(比如丢消息),要么是状态记录错误(比如状态转换缺失环节)。

由于系统对各个消息进行了监控,发现Msg2消息在历史上发送过,因此,不是丢消息,而是Msg2还没发送,Msg3就发送了。

这就表明是状态机的状态转换出现了问题。

先给出正确的状态迁移图。

主处理器上的PCIe设备初始化状态和从处理器上的PCIe设备初始化状态都记录在主处理器的主进程中。不同的PCIe设备的初始化都是并行的,因此,各自的状态迁移也是并行发生,如下图所示。

PCIe初始化并行状态迁移
收到初始化完成消息
主处理器复位
主处理器PCIe未初始化
主处理器PCIe已初始化
收到初始化完成消息
收到从处理器复位消息
从处理器PCIe未初始化
从处理器PCIe已初始化

汇总的状态迁移过程如下所示。

PCIe初始化整体状态迁移
非最后一个PCIe初始化完成消息到来
最后一个PCIe初始化完成消息到来
主处理器复位/从处理器复位消息到来
PCIe部分初始化完成
PCIe全部初始化完成

对照正确的状态迁移过程,果然发现了问题——从处理器的PCIe初始化状态机出现了问题。

错误的状态转换如下图所示。

从处理器PCIe初始化错误状态迁移
收到初始化完成消息
【注】这条转换丢失
从处理器的PCIe未初始化
从处理器的PCIe已初始化

定位发现错误的静态Flag变量使用导致了从处理器PCIe设备初始化的状态迁移出现错误——缺失了一个状态转换流程。这会导致什么问题呢?反正这个转换一般也不会发生啊,初始化完了就跑着完事儿了呗。

但是BUG通常不会出现,不代表永远不会出现。我们使用的从处理器恰恰就有个毛病——极低概率的出现自动复位。此时,主处理器发现与从处理器的心跳丢失,会重新走一遍PCIe设备的初始化流程。但是,但是,问题就出现了——从处理器的PCIe状态还是已经初始化化完成,而真实的状态是可能初始化完成了,也可能还没有初始化完成——主处理器已经无法准确掌握从处理器PCIe初始化的状态了。

从而,主处理器上汇总的状态迁移也跟随出现了问题,如下图所示。

PCIe初始化整体状态迁移出现错误
非最后一个PCIe初始化完成消息到来
最后一个【主处理器】PCIe初始化完成消息到来
主处理器复位/从处理器复位消息到来
PCIe部分初始化完成
PCIe全部初始化完成

所以,从处理器异常复位之后,只要主处理器上的PCIe设备初始化完成后,主处理器根本就不会再理睬从处理器的PCIe初始化是否完成了,它此时就会认为所有的PCIe设备初始化已经完成,直接就发送Msg3了,但此时,PCIe有可能还没有初始化完成,因此就会出现文章开头提到的问题了。


至此,问题已经分析和定位清楚了,但是在编写代码时为什么会出现这样看似并不高级的失误呢?——是错用了静态Flag变量导致的。

错误代码如下。g_pcie_init_flag变量使用不恰当,因为这个变量编程者意在记录初始化次数,所以根本就没考虑到处理器有复位情况,即使考虑到了复位情况,那这个变量是清零还是不清零呢,是不是混用变量很尴尬?记录次数就记录次数,就不要再用作状态机变量,混用很危险 ,即使是最细节的变量使用也要遵循 职责单一设计原则

/*定义全局变量,记录主处理器和从处理各进程PCIe初始化次数*/
unsigned int g_pcie_init_flag[10] = {0}; 

/* 主处理器上的主进程(p0)消息处理代码 */
...
case PCIE_INIT_FNISH: /* 主处理器上的各进程和从处理器使用同一个消息号 */
{
        if(从处理器进程Id == pMsgHeader->srcProcId)
        {
            /*就是这个变量使用不恰当,记录次数就记录次数;就不要再用作状态机变量(混用很危险)*/
            g_pcie_init_flag[从处理器进程索引]++; 
        }
        if (pMsgHeader->srcProcId 属于 主处理器进程Id)
        {
            /*主处理器上的进程使用这个变量还没有出问题的原因是主处理器复位之后该变量会自动清0。
              但是也不排除PCIe设备复位但是处理器不复位的情况在后续出现,所以这里也是雷区*/
            g_pcie_init_flag[主处理器进程索引]++; 
        }

        if(g_pcie_init_flag[从处理器进程索引] >= 1 && g_pcie_init_flag[主处理器进程索引] >= 1)
        {
            发送Msg3;
        }
		break;    
}
...

3 问题解决

问题根因找到之后,解决起来就容易了。

PCIe初始化消息处理代码修改如下:

/*定义全局变量,记录主处理器和从处理各进程PCIe初始化次数*/
unsigned int g_pcie_init_times[10] = {0}; 
unsigned int g_pcie_init_state[10] = {0}; 

/* 主处理器上的主进程(p0)消息处理代码 */
...
case PCIE_INIT_FNISH: /* 主处理器上的各进程和从处理器使用同一个消息号 */
{
        if(从处理器进程Id == pMsgHeader->srcProcId)
        {
            g_pcie_init_times[从处理器进程索引]++; 
            g_pcie_init_state[从处理器进程索引] = 1;
        }
        if (pMsgHeader->srcProcId 属于 主处理器进程Id)
        {
            g_pcie_init_times[主处理器进程索引]++; 
            g_pcie_init_state[主处理器进程索引] = 1;
        }
 
        if(g_pcie_init_state[从处理器进程索引]  && g_pcie_init_state[主处理器进程索引])
        {
            触发发送Msg3的流程;
        }
		break;    
}
...

增加从处理器异常复位流程处理逻辑如下:

/* 主处理器处理从处理器异常复位流程 */
if 从处理器心跳丢失 && 从处理器重新启动 /*主处理器会重新走一遍PCIe设备初始化流程*/
{
    g_pcie_init_state[从处理器进程索引] = 0; /* 补充丢失的状态迁移 */
    g_pcie_init_state[主处理器进程索引] = 0; /* 这里也同步给主进程的初始化状态清零(因为主处理器也会重新走一遍初始化流程)*/
}
    

4 复盘

状态机标识变量一定要随着状态迁移而变化,不变化的不要用来指示当前状态。

职责不同的变量不要混用,混用很危险 ,变量使用也要遵循 职责单一设计原则

借此问题将控制流程的变量使用方式总结如下:

  1. 事件标记和传递。函数内部进行事件传递,可以使用动态变量,也可以使用静态变量。这种情况下尽量将变量的生命周期控制的尽量短。
  2. 状态记录和标识。标识状态只能使用静态变量,否则函数第二次被调用时标识状态的内存已经被释放了。使用状态标识最最危险的一种情况是——实际状态已经改变,但是状态标识未跟随改变。

恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿越临界点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值