第29章 大型项目重构案例:Linux内核模块重构、嵌入式固件重构实战

说实话,讲完前面那么多理论,我知道大家最想看什么——真刀真枪的实战案例。这一章我就拿两个我亲手参与过的项目来拆解。一个是Linux内核模块的重构,另一个是嵌入式固件。这两个场景,基本覆盖了C语言重构的绝大多数痛点。

29.1 案例一:Linux内核模块——从“面条代码”到分层架构

几年前我接手过一个网络过滤内核模块。功能倒不复杂,就是做包过滤和流量统计。但代码嘛……嗯,我打开第一个源文件时,差点以为看到了意大利面条。一个函数1200行,全局变量满天飞,锁的粒度粗得吓人。

重构前的问题诊断:

  • 单文件3000+行,所有逻辑揉在一起
  • 全局变量超过20个,模块间耦合严重
  • 中断上下文和进程上下文混用,死锁风险高
  • 错误处理不统一,有的地方直接panic

我当时做了个决定:先画架构图,再动代码。你想想看,内核模块一旦跑飞,可不是重启个进程那么简单,那是整个系统挂掉。

核心原则:内核模块重构,安全第一。每改一处,必须能回滚。

29.2 重构后的分层架构

我把它拆成了三层:

层次 职责 关键文件
接口层 处理netfilter钩子注册、用户态通信 nf_interface.c
策略层 规则匹配、流量统计、日志记录 policy_engine.c
数据层 哈希表管理、规则存储、原子计数 rule_table.c

下面这张图,就是我重构时画的架构草图:

用户态程序 接口层 (nf_interface.c) netfilter钩子注册 / netlink通信 策略层 (policy_engine.c) 规则匹配 / 流量统计 / 日志 数据层 (rule_table.c) 哈希表 / 原子计数 / RCU锁 内核态

重构后的核心变化:每一层只通过接口函数交互。比如策略层要查规则,只能调用 rule_table_lookup(),不能直接操作哈希表。这样改起来心里踏实多了。

29.3 关键重构点:锁的精细化

原来的代码一把大锁锁全局。我改成RCU锁配合per-CPU变量。看这段对比:

// 重构前:粗粒度锁
static DEFINE_SPINLOCK(global_lock);
void update_rule(struct rule *r) {
    spin_lock(&global_lock);
    // 更新规则...
    spin_unlock(&global_lock);
}

// 重构后:RCU + per-CPU
static DEFINE_SPINLOCK(rule_update_lock);
void update_rule(struct rule *r) {
    spin_lock(&rule_update_lock);
    struct rule *old = rcu_dereference_protected(rule_table, ...);
    rcu_assign_pointer(rule_table, new);
    spin_unlock(&rule_update_lock);
    synchronize_rcu();
    kfree(old);
}

我的经验:内核模块重构,RCU是你的好朋友。但别滥用——写多读少的场景,老老实实用spin_lock反而更简单。

29.4 案例二:嵌入式固件——内存受限下的重构

另一个项目是某款IoT设备的固件。MCU只有64KB RAM,跑着FreeRTOS。代码是外包写的,质量嘛……我接手时,堆栈溢出已经导致设备随机重启了。

核心问题:

  • 动态内存分配满天飞,碎片化严重
  • 任务栈大小全靠猜,要么溢出要么浪费
  • 全局状态机用switch-case写了800行

我当时做了三件事:

  1. 干掉动态分配——所有内存池静态预分配
  2. 任务栈统一计算——用工具测量实际栈使用量,再加20%余量
  3. 状态机表格化——用查表代替switch-case

29.5 状态机重构:从switch到表格

原来的代码长这样:

// 重构前:800行switch-case
void device_state_machine(event_t ev) {
    switch(state) {
        case IDLE:
            if(ev == EV_START) { state = RUNNING; ... }
            else if(ev == EV_CONFIG) { state = CONFIG; ... }
            // 还有几十个分支...
            break;
        case RUNNING:
            // 又是几十个分支...
            break;
        // 十几个状态...
    }
}

我改成表格驱动:

// 重构后:表格驱动
typedef struct {
    state_t  curr_state;
    event_t  event;
    state_t  next_state;
    void     (*action)(void*);
} state_transition_t;

static const state_transition_t trans_table[] = {
    {IDLE,    EV_START,  RUNNING,  action_start},
    {IDLE,    EV_CONFIG, CONFIG,   action_enter_config},
    {RUNNING, EV_STOP,   IDLE,     action_stop},
    // ... 所有转移一目了然
};

void device_state_machine(event_t ev) {
    for(int i = 0; i < ARRAY_SIZE(trans_table); i++) {
        if(trans_table[i].curr_state == g_state &&
           trans_table[i].event == ev) {
            g_state = trans_table[i].next_state;
            trans_table[i].action(NULL);
            return;
        }
    }
    // 未匹配:记录错误
}

注意:表格驱动虽然清晰,但查表有性能开销。在中断处理这种高频路径上,还是用直接跳转更合适。我一般把表格放在低优先级任务里用。

29.6 嵌入式重构的避坑指南

我曾经在一个项目里,重构完发现设备功耗增加了30%。排查半天,原来是新加的日志打印在低功耗模式下没关掉。嗯,嵌入式重构,功耗和实时性是两座大山。

我的建议:

  • 每次提交前,先跑一遍功耗测试
  • 重构期间保留性能计数器,用数据说话
  • 别一次性改太多——我习惯每次只改一个模块,测试通过再继续

29.7 两个案例的共性总结

说回正题。这两个项目虽然场景不同,但重构思路是相通的:

维度 内核模块 嵌入式固件
首要约束 稳定性、并发安全 内存、功耗、实时性
重构手法 分层、RCU、per-CPU 静态内存、表格驱动、栈分析
测试策略 KUnit + 内核崩溃转储 硬件在环 + 功耗监测
回滚方案 kpatch热补丁 双区OTA升级

我个人觉得,大型项目重构最难的从来不是技术,而是勇气和耐心。你面对一团乱麻时,敢不敢先停下来画图?敢不敢一小步一小步地改?我见过太多人,一上来就大刀阔斧重写,结果三个月后项目黄了。

最后说一句:重构不是炫技,是让代码更好维护。如果你改完的代码,三个月后你自己都看不懂,那还不如不改。

公众号:蓝海资料掘金营,微信deep3321