C编程-状态机实现

Posted by 周思进 on June 26, 2020

本周看了一篇文章描述如何减少 if-else 这样的判断语句,让我想起了之前看 DHCP 客户端源码时,状态切换是通过 switch/case 的方式来实现的。便一同搜索了状态机实现的相关文章,学习并做了下笔记。

有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机(英语:finite-state automation,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型

其核心三要素:稳定的状态、触发状态转移的事件、转移过程中执行的动作。其中动作不是必须的,可以满足状态转移条件下,不做任何动作直接转移状态。

在描述状态机的时候,需要搞清楚什么是动作,什么是状态,不要把动作当成状态来描述。状态是稳定的,即如果没有任何事件触发来改变的话,是会一直维持在这个状态。

DHCP状态机

下面便以上图 dhcp 状态机中的一部分来做示例代码实现,比如我们只简单模拟 INIT、SELECTING、REQUESTING 三个状态之间的切换。

dhcp 初始状态为 INIT, 在发送了 DHCPDISCOVER 后,进入到 SELECTING 状态,这里假设直接收到服务器应答事件,触发动作 send DHCPREQUEST,进入到了 REQUESTING 状态,这里假设超时收不到应答(对应事件),直接切换到 INIT 状态(无动作执行)。

状态机图:
image


方式一、通过 if-else 语句实现

使用 if-else 语句是最简单及容易想到的方式,只需要针对各种状态编写对应的 if-else 语句即可。


enum dhcpState{
    INIT = 0,
    SELECTING,
    REQUESTING
};

void dhcpDiscover(void)
{
    printf("send dhcp discover.\n");
    return ;
}

void dhcpRequest(void)
{
    printf("send dhcp request.\n");
    return ;
}

void startDhcp(void)
{
    int state = INIT;
    int cnt = 0;

    while (1)
    {
        if (state == INIT)
        {
            // 初始状态直接发送 discover 包进入到 SELECTING 状态
            dhcpDiscover();
            state = SELECTING;
        }
        else if (state == SELECTING)
        {
            // 假设收到服务器应答,发送 request 包,进入 REQUESTING 状态
            printf("recv DHCPOFFER.\n");
            dhcpRequest();
            state = REQUESTING;
        }
        else if (state == REQUESTING)
        {
            // 假设3s超时收不到服务器应答,进入到 INIT 状态
            cnt++;
            if (3 == cnt)
            {
                cnt = 0;
                printf("timeout, back to init state\n");
                state = INIT;
            }
            
        }

        sleep(1);
    }
}


int main(void)
{
    startDhcp();

    return 0;
}


方式二、switch/case

此方法其实和 if-else 语句差不多,但看起来似乎更简洁些。

void startDhcp(void)
{
    int state = INIT;
    int cnt = 0;

    while (1)
    {
        switch (state)
        {
        case INIT:
            // 初始状态直接发送 discover 包进入到 SELECTING 状态
            dhcpDiscover();
            state = SELECTING;
            break;

        case SELECTING:
            // 假设收到服务器应答,发送 request 包,进入 REQUESTING 状态
            printf("recv DHCPOFFER.\n");
            dhcpRequest();
            state = REQUESTING;
            break;

        case REQUESTING:
            // 假设3s超时收不到服务器应答,进入到 INIT 状态
            cnt++;
            if (3 == cnt)
            {
                cnt = 0;
                printf("timeout, back to init state\n");
                state = INIT;
            }
        
        default:
            break;
        }

        sleep(1);
    }
}


方式三、查表法

此方式的核心思想就是定义一张状态转移表,这个表里记录每一种可能的状态转移,一条状态转移记录包含的信息包括当前状态、发生的事件、执行的动作函数、将要切换的状态。

核心函数则是事件处理函数,根据传入的触发事件及当前状态,在状态表中进行查询,如果有查找到对应的转移记录操作,则执行对应的动作函数(如果有的话),并切换到新的状态。

而后面如果要新增状态转移,只需要往原有的状态转移表中添加相应的转换记录即可,代码改动量非常小。具体实现如下:

//fsm.h
#ifndef __FSM_H__
#define __FSM_H__

#include <stdint.h>
#include <stddef.h>

typedef struct fsmTable_s {
    uint32_t event;      // 触发事件
    uint32_t curState;   // 当前状态
    void (*eventHandle)(void *);    // 执行函数
    uint32_t nextState;  // 切换状态
}FsmTable_T;

typedef struct FSM_s {
    FsmTable_T *fsmTable;   // 状态迁移表
    uint32_t curState;   //当前状态
    uint32_t size;      // 状态表大小
}FSM_T;

void FSM_Init(FSM_T *pFsm, FsmTable_T *pTable, u_int32_t curState, u_int32_t size);
void FSM_EventHandle(FSM_T *pFsm, u_int32_t event, void *param);

#endif


//fsm.c
#include "fsm.h"

void FSM_Init(FSM_T *pFsm, FsmTable_T *pTable, u_int32_t curState, u_int32_t size)
{
    pFsm->fsmTable = pTable;
    pFsm->curState = curState;
    pFsm->size = size;
    return ;
}

static void FSM_StateTransfer(FSM_T *pFsm, u_int32_t state)
{
    pFsm->curState = state;
    return ;
}

void FSM_EventHandle(FSM_T *pFsm, u_int32_t event, void *param)
{
    uint32_t i = 0;
    FsmTable_T *pTable = pFsm->fsmTable;
    uint32_t curState = pFsm->curState;
    uint32_t flag = 0;
    void (*eventHandle)(void *) = NULL;
    uint32_t nextState;

    for (i = 0; i < pFsm->size; i++)
    {
        if (event == pTable[i].event 
            && curState == pTable[i].curState)
        {
            flag = 1; // 在外面执行,避免过多嵌套
            eventHandle = pTable[i].eventHandle;
            nextState = pTable[i].nextState;
            break;
        }
    }

    if (1 == flag)
    {
        if (eventHandle != NULL)
        {
            eventHandle(param);
        }
        FSM_StateTransfer(pFsm, nextState);
    }
    else;

    return ;
}


//main.c

#include "fsm.h"

enum dhcpState {
    INIT = 0,
    SELECTING,
    REQUESTING
};

enum dhcpEvent {
    EVENT_INIT,
    EVENT_RECV_OFFER,
    EVENT_TIMEOUT,
};

static void dhcpDiscover(void *param);
static void dhcpRequest(void *param);

FsmTable_T dhcp_table[] = {
    {EVENT_INIT,        INIT,       dhcpDiscover,   SELECTING},
    {EVENT_RECV_OFFER,  SELECTING,  dhcpRequest,    REQUESTING},
    {EVENT_TIMEOUT,     REQUESTING, NULL,           INIT}
};

void dhcpDiscover(void *param)
{
    printf("send dhcp discover.\n");
    return ;
}

void dhcpRequest(void *param)
{
    printf("send dhcp request.\n");
    return ;
}

void startDhcp(void)
{
    FSM_T dhcp_fsm;

    memset(&dhcp_fsm, 0, sizeof(dhcp_fsm));
    FSM_Init(&dhcp_fsm, dhcp_table, INIT, sizeof(dhcp_table)/sizeof(FsmTable_T));

    int state = INIT;
    int cnt = 0;

    /*
     * 不知道该怎么结合使用 FSM_EventHandle ,仍旧按 switch/case 方式来调用,就当先学习下这种设计模式。  
     */
    while (1)
    {
        switch (dhcp_fsm.curState)
        {
        case INIT:
            FSM_EventHandle(&dhcp_fsm, EVENT_INIT, NULL);
            break;

        case SELECTING:
            FSM_EventHandle(&dhcp_fsm, EVENT_RECV_OFFER, NULL);
            break;

        case REQUESTING:
            // 假设3s超时收不到服务器应答,进入到 INIT 状态
            cnt++;
            if (3 == cnt)
            {
                cnt = 0;
                printf("timeout, back to init state\n");
                FSM_EventHandle(&dhcp_fsm, EVENT_TIMEOUT, NULL);
            }
        
        default:
            break;
        }

        sleep(1);
    }

    return;
}

int main(void)
{
    startDhcp();

    return 0;
}

总体而言 if-else 和 switch-case 在实现和理解上还是很简单的,对于状态机不是很复杂的情况,还是优先考虑使用这种方式吧。


DHCP 状态机图片来源
https://personal.utdallas.edu/~ravip/cs6390/fall01/dhcp.figure.pdf

参考链接:
https://blog.csdn.net/xinghuanmeiying/article/details/89387451