LWIP 作为一个轻量级 TCP/IP 协议栈,其内部许多操作需要保证线程或中断安全,因此移植时必须实现临界段保护接口。由于这些接口调用极为频繁,其实现效率直接关系到整个协议栈的运行性能。本文将深入解析 LWIP 的临界段保护接口、其默认实现中存在的 BUG,并从效率角度给出使用建议。
临界段保护接口定义
相关接口宏定义在 sys.h 头文件中:
SYS_ARCH_DECL_PROTECT(lev): 声明一个变量,用于记录进入临界段前的状态。
SYS_ARCH_PROTECT(lev): 进入临界段保护,并返回进入前的状态。
SYS_ARCH_UNPROTECT(lev): 退出临界段保护,并恢复进入前的状态。
这里所说的“状态”通常指中断使能状态(开/关)或中断优先级等级。关键原则是:退出时必须恢复到进入前的状态。
LWIP 提供了两种实现方式:
- 用户可以直接定义上述三个宏。
- 若未直接定义,则需在
lwipopts.h 中配置 SYS_LIGHTWEIGHT_PROT 为 1,并在 sys_arch.c 中实现 sys_arch_protect() 和 sys_arch_unprotect(lev) 函数。
如果既未定义宏,SYS_LIGHTWEIGHT_PROT 也未使能,则这些宏被定义为空,即无保护。

在 sys_arch.c 中的标准实现
如果选择第二种方式(通过配置宏),则需要在 sys_arch.c 文件中实现以下核心函数:
接口函数
void sys_init(void): 初始化保护机制所需资源(如互斥量)。
sys_prot_t sys_arch_protect(void): 进入保护,可通过获取互斥量或关中断实现。
void sys_arch_unprotect(sys_prot_t pval): 退出保护,并恢复状态。
当使能 LWIP_SYS_ARCH_PROTECT_SANITY_CHECK 时,sys_arch_protect() 应返回嵌套计数,否则可返回 1。
异常检测机制
使能 LWIP_SYS_ARCH_PROTECT_SANITY_CHECK 后,LWIP 会使用一个嵌套计数器来检测临界段保护的异常使用:
- 进入时计数器加1,
sys_arch_protect() 返回加1前的值。
- 退出时检查计数器是否大于0(防止重复退出),然后减1,并断言减1后的值应与进入时返回的值相等,以此确保进入和退出匹配。
默认实现的 BUG 与修复
标准实现存在一个关键问题:按照设计,sys_arch_protect() 应返回原始的保护状态(如中断开关状态),以便 sys_arch_unprotect() 能够精确恢复。然而:
- 当使能
LWIP_SYS_ARCH_PROTECT_SANITY_CHECK 时,函数返回的是嵌套计数值,而非中断状态。
- 当使用关中断方式(未定义
LWIP_SYS_ARCH_PROTECT_USES_MUTEX)时,通常使用的 taskENTER_CRITICAL() 类函数不返回原始状态。
因此,若使用关中断方式,不能使能 LWIP_SYS_ARCH_PROTECT_SANITY_CHECK,且必须实现能返回并恢复原始中断状态的函数。修正后的实现示例如下:
sys_prot_t sys_arch_protect(void) {
#if LWIP_SYS_ARCH_PROTECT_USES_MUTEX
LWIP_ASSERT("sys_arch_protect_mutex != NULL", sys_arch_protect_mutex != NULL);
os_acquire_mutex(sys_arch_protect_mutex);
#if LWIP_SYS_ARCH_PROTECT_SANITY_CHECK
{
/* 返回递增的嵌套计数 */
sys_prot_t ret = sys_arch_protect_nesting;
sys_arch_protect_nesting++;
LWIP_ASSERT("sys_arch_protect overflow", sys_arch_protect_nesting > ret);
return ret;
}
#else
return 1;
#endif
#else /* LWIP_SYS_ARCH_PROTECT_USES_MUTEX */
/* 中断方式必须返回原始中断状态,因此不能使能异常检测 */
sys_prot_t ret = cpu_disable_irq();
return ret;
#endif /* LWIP_SYS_ARCH_PROTECT_USES_MUTEX */
}
void sys_arch_unprotect(sys_prot_t pval) {
#if LWIP_SYS_ARCH_PROTECT_USES_MUTEX
#if LWIP_SYS_ARCH_PROTECT_SANITY_CHECK
LWIP_ASSERT("unexpected sys_arch_protect_nesting", sys_arch_protect_nesting > 0);
sys_arch_protect_nesting--;
LWIP_ASSERT("unexpected sys_arch_protect_nesting", sys_arch_protect_nesting == pval);
#endif
LWIP_ASSERT("sys_arch_protect_mutex != NULL", sys_arch_protect_mutex != NULL);
os_release_mutex(sys_arch_protect_mutex);
LWIP_UNUSED_ARG(pval);
#else /* LWIP_SYS_ARCH_PROTECT_USES_MUTEX */
/* 中断方式必须恢复原始中断状态 */
cpu_restore_irq(pval);
#endif /* LWIP_SYS_ARCH_PROTECT_USES_MUTEX */
}
临界段保护的使用场景
1. 全局变量与状态更新
最常见的是保护全局变量或结构体成员的读写,防止数据竞争。例如在 netconn_err 函数中:
err_t netconn_err(struct netconn *conn) {
err_t err;
SYS_ARCH_DECL_PROTECT(lev);
if (conn == NULL) {
return ERR_OK;
}
SYS_ARCH_PROTECT(lev);
err = conn->pending_err;
conn->pending_err = ERR_OK;
SYS_ARCH_UNPROTECT(lev);
return err;
}
2. TCP/IP 核心锁 (TCP/IP Core Locking)
在 sockets.c 中,根据 LWIP_TCPIP_CORE_LOCKING 配置选择不同的保护机制:
#if LWIP_TCPIP_CORE_LOCKING
/* 使用核心锁保护 */
#define LWIP_SOCKET_SELECT_PROTECT(lev) LOCK_TCPIP_CORE()
#define LWIP_SOCKET_SELECT_UNPROTECT(lev) UNLOCK_TCPIP_CORE()
#else /* LWIP_TCPIP_CORE_LOCKING */
/* 使用轻量级保护(SYS_ARCH_PROTECT) */
#define LWIP_SOCKET_SELECT_DECL_PROTECT(lev) SYS_ARCH_DECL_PROTECT(lev)
#define LWIP_SOCKET_SELECT_PROTECT(lev) SYS_ARCH_PROTECT(lev)
#define LWIP_SOCKET_SELECT_UNPROTECT(lev) SYS_ARCH_UNPROTECT(lev)
#endif /* LWIP_TCPIP_CORE_LOCKING */
3. 内存管理 (mem.c)
当使能 LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT 时,内存的分配和释放操作会使用临界段进行保护。
4. 桥接接口 (bridgeif.h) 与 PPPoS
在这些模块中,根据特定配置,也可能复用 SYS_ARCH_PROTECT 系列的宏来实现模块内的资源保护。
效率优化建议
临界段保护的选择对性能至关重要:
- 对于保护全局变量、执行时间极短的操作,强烈建议使用关中断方式。其开销极小,效率最高。需要注意的是,许多互斥量的底层实现本身也依赖于关中断,但增加了额外的逻辑开销。
- 对于需要保护执行时间较长的代码段(例如某些 TCP/IP 核心操作),则应使用互斥量或其他调度器友好的同步机制,以避免长时间关闭中断影响系统实时性。此时可以像
LWIP_TCPIP_CORE_LOCKING 那样,为这些特定场景实现独立的保护接口。
总结
- 按场景选择实现:短操作用关中断,长操作用互斥量,分类实现以最大化系统效率。
- 注意默认实现的 BUG:标准
sys_arch.c 中关中断方式的实现,在使能了异常检测或使用不返回状态的关中断函数时,无法正确保存和恢复中断状态,需按前述方案修正。
- 理解使用模式:临界段保护广泛应用于 LWIP 内部各个模块,理解其配置和调用场景有助于进行更精准的移植和性能调优。