当前位置: 首页 > article >正文

嵌入式C语言:结构体

目录

一、结构体的定义

二、结构体变量的声明

三、结构体成员的访问

四、结构体指针

五、结构体初始化

六、结构体数组

七、结构体嵌套

八、结构体作为函数参数

九、位字段(Bit-fields)

十、结构体在嵌入式系统中的应用

10.1. 硬件寄存器映射

10.2. 数据通信与协议解析

10.3. 任务与事件管理

10.4. 设备驱动与配置

10.5. 资源管理

10.6. 状态机实现

十一、注意事项

11.1. 字节对齐

11.2. 结构体初始化

11.3. 结构体指针

11.4. 作用域

11.5. 嵌套结构体

11.6. 与硬件交互


在嵌入式系统编程中,结构体(struct)是一种非常重要的数据类型,它允许将多个不同类型的数据项组合成一个单一的数据类型。这在组织和管理复杂数据、提高代码可读性和可维护性方面非常有用。

一、结构体的定义

结构体的定义使用 struct 关键字,后面跟着结构体标签(可选),以及花括号内的成员列表。例如,定义一个表示坐标点的结构体:

struct Point {
    int x;
    int y;
};

struct Point 是结构体类型,x 和 y 是结构体的成员,它们都是 int 类型。

二、结构体变量的声明

可以在定义结构体的同时声明变量,也可以在之后单独声明。

// 定义结构体的同时声明变量
struct Point {
    int x;
    int y;
} point1, point2;

// 单独声明变量
struct Point point3;

三、结构体成员的访问

通过结构体变量名和成员访问运算符.来访问结构体的成员。

struct Point point;
point.x = 10;
point.y = 20;

四、结构体指针

可以声明指向结构体的指针,通过指针访问结构体成员时,使用 -> 运算符。 在处理大型结构体或传递结构体到函数时特别有用,因为它可以避免复制整个结构体

struct Point *ptr;
struct Point point = {10, 20};
ptr = &point;
int x_value = ptr->x;  // 使用指针访问结构体成员

五、结构体初始化

在声明结构体变量时,可以对其进行初始化。

struct Point point1 = {10, 20};
struct Point point2 = {.y = 30,.x = 25};  // 命名初始化,可按任意顺序

六、结构体数组

可以定义结构体数组,用于存储多个相同结构体类型的数据。 

struct Point points[3] = {
    {10, 20},
    {30, 40},
    {50, 60}
};

七、结构体嵌套

结构体可以包含其他结构体作为成员,形成嵌套结构体。这对于表示复杂数据结构非常有用。

struct Address {
    char street[50];
    char city[50];
    char state[20];
    int zip;
};

struct Person {
    char name[50];
    int age;
    struct Address addr; // 嵌套结构体
};

八、结构体作为函数参数

结构体可以作为函数参数传递。按值传递会复制整个结构体,可能效率不高,特别是对于大型结构体。通常,通过指针传递结构体更为高效。

#include <stdio.h>
#include <stdint.h>

// 定义一个表示点的结构体
struct Point {
    int16_t x;
    int16_t y;
};

void printPoint(struct Point *p) {
    printf("Point: (%d, %d)\n", p->x, p->y);
}

int main() {
    struct Point p = {10, 20};
    printPoint(&p); // 传递结构体指针
    return 0;
}

九、位字段(Bit-fields)

在嵌入式系统中,内存是非常宝贵的资源。位字段允许在一个结构体中定义位级别的成员,对于硬件寄存器的映射非常有用。

struct BitFieldExample {
    uint32_t bit0 : 1;
    uint32_t bit1 : 1;
    uint32_t bit2_4 : 3; // 3位宽的字段
    uint32_t bit5_31 : 27; // 剩余的27位
};

十、结构体在嵌入式系统中的应用

10.1. 硬件寄存器映射

内存地址映射:嵌入式系统中,硬件设备的寄存器与特定内存地址关联,通过结构体可将这些寄存器映射为结构体成员,实现对硬件设备的便捷控制。例如,对于一个简单的 GPIO 控制寄存器,可定义如下结构体:

#include <stdint.h>
 
// 假设GPIO寄存器的地址(通常是微控制器数据手册中定义的)
#define GPIO_BASE_ADDR 0x40004000
 
// 定义GPIO寄存器结构体
typedef struct {
    volatile uint32_t DATA;       // 数据寄存器
    volatile uint32_t DIR;        // 方向寄存器
    volatile uint32_t IS;         // 中断敏感寄存器
    volatile uint32_t IBE;        // 边缘检测中断使能寄存器
    volatile uint32_t IEV;        // 边缘检测方向寄存器
    volatile uint32_t IM;         // 中断屏蔽寄存器
    volatile uint32_t RIS;        // 原始中断状态寄存器
    volatile uint32_t MIS;        // 屏蔽中断状态寄存器
    volatile uint32_t ICR;        // 中断清除寄存器
    // ... 可能还有其他寄存器
} GPIO_TypeDef;
 
// 将GPIO基地址转换为GPIO结构体指针
#define GPIO ((GPIO_TypeDef *)GPIO_BASE_ADDR)
 
// 使用示例:设置GPIO方向为输出并点亮LED
void LED_On(void) {
    GPIO->DIR |= (1 << 5);  // 假设LED连接在GPIO的第5位
    GPIO->DATA |= (1 << 5); // 设置GPIO第5位为高电平,点亮LED
}

10.2. 数据通信与协议解析

数据封装:在嵌入式系统间或与外部设备通信时,需将数据按特定协议封装与解析。结构体可用于构建符合通信协议的数据结构。以 SPI 通信协议为例,数据帧可能包含起始位、地址位、数据位和校验位等,使用结构体可清晰表示该数据帧:

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

// 定义SPI数据帧结构体
typedef struct {
    uint8_t startBit : 1;    // 起始位
    uint8_t addrBits : 7;    // 地址位(假设为7位)
    uint8_t data[8];         // 数据位(假设为8字节)
    uint8_t checksum;        // 校验位
} SPIFrameTypeDef;

// 初始化SPI数据帧
void SPIFrame_Init(SPIFrameTypeDef *frame, uint8_t start, uint8_t addr, uint8_t *data, uint8_t checksum) {
    frame->startBit = start;
    frame->addrBits = addr;
    memcpy(frame->data, data, 8);
    frame->checksum = checksum;
}

// 计算校验和(简单示例,实际可能更复杂)
uint8_t CalculateChecksum(uint8_t *data, uint8_t length) {
    uint8_t sum = 0;
    for (uint8_t i = 0; i < length; i++) {
        sum += data[i];
    }
    // 取反作为校验和(简单示例)
    return ~sum + 1;
}

// 封装数据到SPI数据帧
bool PackSPIFrame(SPIFrameTypeDef *frame, uint8_t addr, uint8_t *data) {
    // 假设起始位总是1
    uint8_t checksum = CalculateChecksum(data, 8);
    SPIFrame_Init(frame, 1, addr, data, checksum);
    // 在这里可以添加额外的错误检查,比如地址是否有效等
    // 为了简单起见,我们总是返回true
    return true;
}

// 解析SPI数据帧
bool UnpackSPIFrame(SPIFrameTypeDef *frame, uint8_t *addr, uint8_t *data, uint8_t *receivedChecksum) {
    // 检查起始位
    if (frame->startBit != 1) {
        return false; // 起始位错误
    }
    
    // 提取地址和数据
    *addr = frame->addrBits;
    memcpy(data, frame->data, 8);
    *receivedChecksum = frame->checksum;
    
    // 计算预期的校验和并比较
    uint8_t expectedChecksum = CalculateChecksum(data, 8);
    if (frame->checksum != expectedChecksum) {
        return false; // 校验和错误
    }
    
    return true; // 解析成功
}

int main() {
    // 示例数据
    uint8_t dataToSend[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
    uint8_t receivedAddr;
    uint8_t receivedData[8];
    uint8_t receivedChecksum;
    
    // 封装数据帧
    SPIFrameTypeDef frameToSend;
    if (PackSPIFrame(&frameToSend, 0x7F, dataToSend)) {
        // 打印封装后的数据帧
        printf("Packed Frame:\n");
        printf("Start Bit: %d\n", frameToSend.startBit);
        printf("Address: 0x%02X\n", frameToSend.addrBits);
        printf("Data: ");
        for (int i = 0; i < 8; i++) {
            printf("0x%02X ", frameToSend.data[i]);
        }
        printf("\nChecksum: 0x%02X\n", frameToSend.checksum);
        
        // 解析数据帧(模拟接收到的数据帧与发送的相同)
        if (UnpackSPIFrame(&frameToSend, &receivedAddr, receivedData, &receivedChecksum)) {
            // 打印解析后的数据
            printf("Unpacked Data:\n");
            printf("Received Address: 0x%02X\n", receivedAddr);
            printf("Received Data: ");
            for (int i = 0; i < 8; i++) {
                printf("0x%02X ", receivedData[i]);
            }
            printf("\nReceived Checksum: 0x%02X\n", receivedChecksum);
        } else {
            printf("Failed to unpack SPI frame.\n");
        }
    } else {
        printf("Failed to pack SPI frame.\n");
    }
    
    return 0;
}

 

10.3. 任务与事件管理

任务描述:在多任务嵌入式系统中,结构体可用于描述任务属性与状态,如任务 ID、优先级、堆栈指针、任务函数指针等。以简单的任务管理系统为例,定义任务结构体:

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
 
// 定义任务函数指针类型
typedef void (*TaskFunction)(void);
 
// 定义任务结构体
typedef struct {
    uint8_t task_id;
    uint8_t priority;
    void *stack_pointer; // 在实际的多任务系统中,堆栈指针的初始化和使用由RTOS管理
    TaskFunction task_function;
    bool is_active; // 标记任务是否处于活动状态
} Task;
 
// 假设的任务函数定义
void Task1(void) {
    printf("Task 1 is running\n");
    // 在实际的任务中,这里会包含任务的主体逻辑
}
 
void Task2(void) {
    printf("Task 2 is running\n");
    // 在实际的任务中,这里会包含任务的主体逻辑
}
 
// 定义任务实例(注意:在实际系统中,堆栈指针应由RTOS初始化)
Task tasks[] = {
    {1, 1, NULL, Task1, false}, // 初始时,任务未激活
    {2, 2, NULL, Task2, false}  // 初始时,任务未激活
};
 
// 简单的任务“调度”函数(注意:这不是真正的任务调度器)
void RunTasks(Task *tasks, size_t task_count) {
    for (size_t i = 0; i < task_count; ++i) {
        if (tasks[i].is_active) {
            tasks[i].task_function(); // 调用任务函数
        }
    }
}
 
int main() {
    // 在实际的多任务系统中,RTOS会初始化任务并管理它们的调度
    // 在这个示例中,我们将手动激活任务并“调度”它们
 
    // 激活任务
    tasks[0].is_active = true;
    tasks[1].is_active = true;
 
    // 运行任务(注意:这不是真正的多任务并行运行)
    // 在实际系统中,RTOS会负责在任务之间切换
    while (1) {
        RunTasks(tasks, sizeof(tasks) / sizeof(tasks[0]));
        
        // 在实际系统中,这里会有一个延迟或等待中断的机制
        // 在这个示例中,使用一个简单的sleep来模拟(注意:sleep函数可能不在所有嵌入式系统中都可用)
#ifdef _WIN32 // 如果在Windows上编译和运行此代码,则使用Sleep函数(注意:这是毫秒级的)
        Sleep(1000); // 休眠1秒
#else // 如果在类Unix系统(如Linux)上编译和运行,则使用sleep函数(注意:这是秒级的)
        sleep(1); // 休眠1秒
#endif
    }
 
    // 在实际的多任务系统中,main函数通常不会返回一个值,因为RTOS会接管控制权
    // 在这个示例中,由于我们没有实现RTOS,所以main函数会返回0以结束程序
    return 0; // 在实际的多任务嵌入式系统中,这行代码通常不会被执行
}

 

10.4. 设备驱动与配置

设备信息存储:不同硬件设备需不同配置参数,结构体可用于存储这些设备相关信息。以串口设备为例,其配置参数可能包括波特率、数据位、停止位和校验位等,定义如下结构体:

#include <stdio.h>
#include <stdint.h>

// 定义串口配置结构体
typedef struct {
    uint32_t baud_rate;  // 波特率
    uint8_t data_bits;   // 数据位
    uint8_t stop_bits;   // 停止位
    uint8_t parity;      // 校验位(0: 无校验, 1: 奇校验, 2: 偶校验, 等)
} UART_Config;

// 模拟的串口初始化函数(在实际应用中,这将与硬件接口)
void UART_Init(UART_Config *config) {
    // 在这里,我们仅打印配置以模拟初始化过程
    printf("UART Initialized with the following configuration:\n");
    printf("Baud Rate: %u\n", config->baud_rate);
    printf("Data Bits: %u\n", config->data_bits);
    printf("Stop Bits: %u\n", config->stop_bits);
    printf("Parity: %u\n", config->parity);

    // 在实际的应用中,这里会包含与硬件寄存器交互的代码来配置串口
}

int main() {
    // 配置串口参数
    UART_Config uart_config = {
        .baud_rate = 9600,
        .data_bits = 8,
        .stop_bits = 1,
        .parity = 0  // 无校验
    };

    // 初始化串口(在实际应用中,可能会启动串口通信)
    UART_Init(&uart_config);

    // 在实际应用中,这里会包含串口通信的代码
    // ...

    // 由于这是一个模拟示例,我们在这里结束程序
    return 0;
}

 

10.5. 资源管理

内存管理:在嵌入式系统中,内存是有限资源,结构体可用于管理内存分配与释放。例如,实现简单内存池时,定义内存块结构体:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>

// 定义内存块结构体
typedef struct MemoryBlock {
    struct MemoryBlock *next;  // 指向下一个内存块的指针
    uint8_t is_used;           // 标记内存块是否被使用
    // 这里可以添加一个数据字段来存储实际的数据,但为了简化示例,我们省略了它
} MemoryBlock;

// 定义内存池结构体
typedef struct {
    MemoryBlock *head;  // 内存池头部指针
    uint32_t block_size; // 每个内存块的大小(不包括结构体本身的大小)
    uint32_t total_blocks; // 内存池中内存块的总数
    uint32_t free_blocks;  // 当前可用的内存块数量
} MemoryPool;

// 初始化内存池
void MemoryPool_Init(MemoryPool *pool, uint32_t block_size, uint32_t total_blocks) {
    pool->head = NULL;
    pool->block_size = block_size;
    pool->total_blocks = total_blocks;
    pool->free_blocks = total_blocks;

    // 动态分配内存块并链接它们
    MemoryBlock *current = NULL;
    MemoryBlock *previous = NULL;
    for (uint32_t i = 0; i < total_blocks; i++) {
        MemoryBlock *new_block = (MemoryBlock*)malloc(sizeof(MemoryBlock) + block_size);
        if (new_block == NULL) {
            // 处理内存分配失败的情况(在实际应用中,可能需要更复杂的错误处理)
            fprintf(stderr, "Memory allocation failed\n");
            exit(EXIT_FAILURE);
        }
        new_block->is_used = 0; // 初始化为未使用状态
        new_block->next = NULL;
        if (previous == NULL) {
            // 这是第一个内存块,设置为内存池的头部
            pool->head = new_block;
        } else {
            // 将前一个内存块的next指针指向新的内存块
            previous->next = new_block;
        }
        previous = new_block;
    }
}

// 从内存池中分配一个内存块
void* MemoryPool_Alloc(MemoryPool *pool) {
    if (pool->free_blocks == 0) {
        // 没有可用的内存块
        return NULL;
    }

    MemoryBlock *current = pool->head;
    while (current != NULL && current->is_used != 0) {
        // 查找第一个未使用的内存块
        current = current->next;
    }

    if (current != NULL) {
        // 标记内存块为已使用
        current->is_used = 1;
        pool->free_blocks--;
        // 返回指向数据字段的指针(跳过MemoryBlock结构体本身)
        return (void*)((uint8_t*)current + sizeof(MemoryBlock));
    }

    // 在理论上,这里不应该发生,因为我们已经检查了free_blocks不为0
    // 但为了代码的健壮性,我们还是返回NULL
    return NULL;
}

// 释放一个内存块回内存池
void MemoryPool_Free(MemoryPool *pool, void *data) {
    if (data == NULL) {
        // 尝试释放NULL指针,什么也不做
        return;
    }

    // 将数据指针转换回MemoryBlock指针(考虑到MemoryBlock结构体的大小)
    MemoryBlock *block = (MemoryBlock*)((uint8_t*)data - sizeof(MemoryBlock));

    // 标记内存块为未使用
    block->is_used = 0;
    pool->free_blocks++;

    // 注意:在这个简单的实现中,我们没有重新链接内存块或进行碎片整理
    // 在实际应用中,可能需要更复杂的逻辑来处理这些情况
}

// 销毁内存池并释放所有内存块
void MemoryPool_Destroy(MemoryPool *pool) {
    MemoryBlock *current = pool->head;
    while (current != NULL) {
        MemoryBlock *next = current->next;
        free(current);
        current = next;
    }
    pool->head = NULL;
    pool->free_blocks = 0; // 可选,但有助于避免悬挂指针等潜在问题
}

int main() {
    // 初始化一个内存池,每个内存块大小为16字节,总共有10个内存块
    MemoryPool pool;
    MemoryPool_Init(&pool, 16, 10);

    // 分配两个内存块
    void *data1 = MemoryPool_Alloc(&pool);
    void *data2 = MemoryPool_Alloc(&pool);

    // 检查分配是否成功
    if (data1 == NULL || data2 == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        MemoryPool_Destroy(&pool);
        return EXIT_FAILURE;
    }

    // 使用分配的内存块(在实际应用中,这里会存储或处理数据)
    // ...

    // 释放内存块
    MemoryPool_Free(&pool, data1);
    MemoryPool_Free(&pool, data2);

    // 销毁内存池
    MemoryPool_Destroy(&pool);

    return EXIT_SUCCESS;
}

10.6. 状态机实现

状态表示:在实现有限状态机(FSM)时,结构体可用于表示状态和状态转移条件。例如,定义一个简单的 LED 闪烁状态机:

// 定义状态枚举
typedef enum {
    STATE_OFF,
    STATE_ON,
    STATE_BLINKING
} LED_State;

// 定义状态机结构体
typedef struct {
    LED_State current_state;
    void (*enter_state)(void);
    void (*exit_state)(void);
    void (*update)(void);
} LED_FSM;

// 状态机函数定义
void enter_off_state(void);
void exit_off_state(void);
void update_off_state(void);

// 初始化状态机
LED_FSM led_fsm = {
  .current_state = STATE_OFF,
  .enter_state = enter_off_state,
  .exit_state = exit_off_state,
  .update = update_off_state
};

十一、注意事项

11.1. 字节对齐

对齐规则:为了提高内存访问效率,编译器会按照特定规则对结构体成员进行字节对齐。例如,在 32 位系统中,编译器可能会将 4 字节的int类型成员对齐到 4 字节边界上。假设有如下结构体:

struct Example {
    char a;  // 1字节
    int b;   // 4字节
};

char类型成员a后面可能会填充 3 个字节,使得int类型成员b从 4 字节对齐的地址开始存储,这样整个结构体占用 8 字节,而不是 5 字节。

  • 影响:字节对齐会导致结构体实际占用内存大于所有成员大小之和。这在内存资源紧张的嵌入式系统中需要特别关注,避免不必要的内存浪费。
  • 控制对齐:可使用#pragma pack(n)预编译指令或__attribute__((packed))属性来控制对齐方式。例如__attribute__((packed))可使结构体按紧凑方式存储,取消字节对齐: 
struct __attribute__((packed)) CompactExample {
    char a;
    int b;
};

上述CompactExample结构体占用 5 字节,但可能会降低某些硬件平台的访问效率,因为未对齐的内存访问可能需要额外的周期。

11.2. 结构体初始化

顺序初始化:在初始化结构体时,按照成员声明顺序进行初始化。例如:

struct Point {
    int x;
    int y;
};
struct Point p = {10, 20};  // 先初始化x为10,再初始化y为20

 命名初始化:C99 标准引入命名初始化,可按任意顺序初始化成员:

struct Point p = {.y = 20,.x = 10};

部分初始化:可对部分成员初始化,未初始化的成员会被赋予默认值(对于普通变量,默认值通常是不确定的;对于全局变量,会初始化为 0):

struct Point p = {.x = 10};  // y未初始化,值不确定(如果是全局变量则为0)

11.3. 结构体指针

指针运算:结构体指针运算与普通指针类似,但要注意结构体成员的偏移。例如,假设有一个结构体数组:

struct Data {
    int value;
    char flag;
};
struct Data data_array[10];
struct Data *ptr = data_array;
ptr++;  // 指针移动的字节数为结构体Data的大小,而不是单个成员的大小

内存管理:当使用结构体指针指向动态分配的内存时,要确保正确释放内存,避免内存泄漏。例如:

struct Node {
    int data;
    struct Node *next;
};
struct Node *new_node = (struct Node *)malloc(sizeof(struct Node));
if (new_node!= NULL) {
    // 使用new_node
    free(new_node);  // 使用完毕后释放内存
}

11.4. 作用域

  • 局部与全局:在函数内部定义的结构体,其作用域仅限于该函数内部;在文件作用域(函数外部)定义的结构体,可被同一文件内的函数使用。若要在多个文件中使用同一结构体,可在头文件中定义,然后在需要的源文件中包含该头文件。
  • 结构体标签:结构体标签的作用域遵循 C 语言一般作用域规则。不同作用域内可定义同名结构体标签,但在同一作用域内,结构体标签必须唯一。

11.5. 嵌套结构体

成员访问:访问嵌套结构体成员时,需使用多个成员访问运算符。例如:

struct Inner {
    int a;
};
struct Outer {
    struct Inner inner;
    int b;
};
struct Outer outer;
outer.inner.a = 10;  // 访问嵌套结构体Inner的成员a
outer.b = 20;

初始化:初始化嵌套结构体时,可使用嵌套的初始化列表:

struct Outer outer = {
  .inner = {.a = 10 },
  .b = 20
};

11.6. 与硬件交互

  • 类型匹配:在进行硬件寄存器映射时,确保结构体成员类型与硬件寄存器位宽和功能匹配。例如,对于一个 16 位的硬件寄存器,应使用uint16_t类型成员与之对应。
  • volatile 关键字:当结构体用于访问硬件寄存器时,通常需使用volatile关键字修饰,防止编译器优化掉对硬件寄存器的读写操作。例如:
volatile struct {
    uint32_t register1;
    uint32_t register2;
} *hardware_registers = (volatile struct {
    uint32_t register1;
    uint32_t register2;
} *)0x10000000;  // 假设硬件寄存器地址

volatile关键字确保每次对hardware_registers成员的访问都是对实际硬件寄存器的操作。 

总之,结构体是嵌入式 C 语言编程中非常强大的工具,它帮助开发者有效地组织和管理数据,提高代码的可读性和可维护性,特别是在处理与硬件相关的数据和复杂的数据结构时。


http://www.kler.cn/a/521997.html

相关文章:

  • Vuex中的getter和mutation有什么区别
  • 2025美赛数学建模MCM/ICM选题建议与分析,思路+模型+代码
  • Vue.js `setup()` 函数的使用
  • mamba论文学习
  • 多头潜在注意力(MLA):让大模型“轻装上阵”的技术革新——从DeepSeek看下一代语言模型的高效之路
  • 关于WPF中ComboBox文本查询功能
  • KF-GINS 和 OB-GINS 的 Earth类 和 Rotation 类
  • 安卓日常问题杂谈(一)
  • Java-数据结构-二叉树习题(3)
  • 落地 基于特征的对象检测
  • leetcode 面试经典 150 题:简化路径
  • 鲁滨逊漂流记读后感
  • 【PySide6快速入门】QGridLayout 网格布局
  • 如何使用 DeepSeek API 结合 VSCode 提升开发效率
  • 深度学习笔记13-CIFAR彩色图片识别(Pytorch)
  • 供应链管理中的BOM 和 MRP 是什么,如何计算
  • 探索前端可观察性:如何使用Telemetry提高用户体验
  • 基于Java+Springboot+MySQL校园在线考试网站系统设计与实现
  • zyNo.19
  • 解析“in the wild”——编程和生活中的俚语妙用
  • 八股——Java基础(四)
  • 【PySide6拓展】QLCDNumber类lcd 显示数字
  • 多级缓存(亿级并发解决方案)
  • C#常用257单词
  • 基于RIP的MGRE实验
  • MySQL 主从同步报错:`Unknown or incorrect time zone` 问题全解析