从 lwIP-2.0.0 开始,申请 tcp_pcb
控制块的逻辑发生了变化。
每个 tcp 连接都必须有一个 PCB 控制块 ,使用函数 tcp_new()
申请 PCB 控制块。tcp_new
函数代码如下所示:
/**
* Creates a new TCP protocol control block but doesn't place it on any of the TCP PCB lists.
* The pcb is not put on any list until binding using tcp_bind().
*
* @internal: Maybe there should be a idle TCP PCB list where these
* PCBs are put on. Port reservation using tcp_bind() is implemented but
* allocated pcbs that are not bound can't be killed automatically if wanting
* to allocate a pcb with higher prio (@see tcp_kill_prio())
*
* @return a new tcp_pcb that initially is in state CLOSED
*/
struct tcp_pcb *tcp_new(void)
{
return tcp_alloc(TCP_PRIO_NORMAL);
}
从代码可以看出,实际申请 tcp_pcb
控制块的是 tcp_alloc
函数。这个函数设计原则是尽一切可能返回一个有效的 tcp_pcb
控制块。因此,当 TCP 控制块数量不足时,该函数可能 “杀死”(kill)正在使用的连接,以释放 tcp_pcb
控制块!
从 lwIP-2.0.0 开始,当 TCP 控制块数量不足时,函数 tcp_alloc
“杀死”(kill)正在使用的连接的逻辑发生了变化。
这源于一次 BUG 反馈。
2013 年 7 月 25,lwIP-1.4.1 用户 Roman Trunov
反馈了一个 BUG :他的设备是一个 TCP 服务器,设置了 10 个 PCB 控制块,这意味着可以同时为 10 个客户端提供服务。但在测试过程中他发现,有时会连接不上服务器,一个客户端都连接不上,直到过了 2 分钟后才能恢复连接。
一番调试后,他发现不能连接服务器时,10 个 PCB 控制块都处于 LAST_ACK
状态,新的连接进来后,由于申请不到 PCB 控制块,所以连接不上。等到 2 分钟后, 处于 LAST_ACK
状态的连接超时,协议栈自动释放控制块内存后,才可以连接服务器。
LAST_ACK
状态,这是 TCP 状态机中的一个状态,处于断开 TCP 连接 4 次握手中的最后一步。在这个状态中,只要服务器收到客户端发来的 ACK
标志,服务器就能完成正常的连接关闭步骤,从而释放 PCB 空间。但是 Roman Trunov
的防火墙配置将客户端发来的最后一次 ACK
给拦截了,导致服务器处于 LAST_ACK
状态不得转变,直到超时事件发生。虽然这次是不合理的配置引起的,但现实世界中是有可能出现这个现象的,因为网络数据在现实世界中是可能丢失的。 Roman Trunov
指出,应该实施更积极的 PCB 控制块分配策略,就像处理 TIME_WAIT
状态那样。
2015 年 2 月 18 日,lwIP 开发人员 Simon Goldschmidt
接受了他的提议,认为这是一个 BUG,然后进行了修复,这就是我们在 lwIP-2.0.0 中看到的代码。
两个不同版本,函数 tcp_alloc
的逻辑是什么样的?它们又有什么不同?
lwIP-1.4.1 代码(有简化) :
/**
* Allocate a new tcp_pcb structure.
*
* @param prio priority for the new pcb
* @return a new tcp_pcb that initially is in state CLOSED
*/
struct tcp_pcb *tcp_alloc(u8_t prio)
{
struct tcp_pcb *pcb;
pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
if (pcb == NULL) {
/* Try killing oldest connection in TIME-WAIT. */
tcp_kill_timewait();
/* Try to allocate a tcp_pcb again. */
pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
if (pcb == NULL) {
/* Try killing active connections with lower priority than the new one. */
tcp_kill_prio(prio);
/* Try to allocate a tcp_pcb again. */
pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
}
}
if (pcb != NULL) {
// 初始化 pcb 代码
}
return pcb;
}
- 先调用
tcp_kill_timewait
函数,试图找到TIME_WAIT
状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用tcp_abort(pcb)
函数 “杀” 掉这个连接,这会发送RST
标志,以便通知远端释放连接; - 如果步骤 1 失败了,则调用
tcp_kill_prio(prio)
函数,试图找到小于等于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接!如果找到符合条件的控制块 pcb ,则调用tcp_abort(pcb)
函数 “杀” 掉这个连接,这会发送RST
标志。
lwIP-2.0.0 代码(有简化):
/**
* Allocate a new tcp_pcb structure.
*
* @param prio priority for the new pcb
* @return a new tcp_pcb that initially is in state CLOSED
*/
struct tcp_pcb *tcp_alloc(u8_t prio)
{
struct tcp_pcb *pcb;
pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
if (pcb == NULL) {
/* Try to send FIN for all pcbs stuck in TF_CLOSEPEND first */
tcp_handle_closepend();
/* Try killing oldest connection in TIME-WAIT. */
tcp_kill_timewait();
/* Try to allocate a tcp_pcb again. */
pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
if (pcb == NULL) {
/* Try killing oldest connection in LAST-ACK (these wouldn't go to TIME-WAIT). */
tcp_kill_state(LAST_ACK);
/* Try to allocate a tcp_pcb again. */
pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
if (pcb == NULL) {
/* Try killing oldest connection in CLOSING. */
tcp_kill_state(CLOSING);
/* Try to allocate a tcp_pcb again. */
pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
if (pcb == NULL) {
/* Try killing oldest active connection with lower priority than the new one. */
tcp_kill_prio(prio);
/* Try to allocate a tcp_pcb again. */
pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
}
}
}
}
if (pcb != NULL) {
// 初始化 pcb 代码
}
return pcb;
}
- 先调用
tcp_kill_timewait
函数,试图找到TIME_WAIT
状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用tcp_abort(pcb)
函数 “杀” 掉这个连接,这会发送RST
标志,以便通知远端释放连接;这一步与 lwIP-1.4.1 相同。 - 如果第 1 步失败了,则调用
tcp_kill_state
函数,试图找到LAST_ACK
和CLOSING
状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用tcp_abandon(pcb, 0)
函数 “杀” 掉这个连接,注意这个函数并不会发送RST
标志,处于这两种状态的连接都是等到对方发送的ACK
就会结束连接,不会有数据丢失;这一步与 lwIP-1.4.1 不同。 - 如果第 2 步也失败了,则调用
tcp_kill_prio(prio)
函数,试图找到小于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接!如果找到符合条件的控制块 pcb ,则调用tcp_abort(pcb)
函数 “杀” 掉这个连接,这会发送RST
标志。要特别注意这一步与 lwIP-1.4.1 也不同。lwIP-1.4.1 会杀死 小于等于指定优先级的连接,而 lwIP-2.0.0 只会杀死 小于指定优先级的连接。
两个版本的不同之处到这里已经讲完了,但是从中得出的 TCP 编程注意事项需要再强调一下,那就是:当 TCP 控制块数量不足时,新的连接可能 “杀死”(kill)正在使用的连接。
“杀死”(kill)正在使用的连接,意味着在无声无息之间,我们正常通讯的连接可能会被意外中止掉。
比如,我有一个服务器提供重要的数据通讯功能,还有一个 Telnet
服务器提供一些不重要的状态查询,当服务器正在提供数据通讯时,多个 Telnet
客户端进行了连接,那么正在数据通讯的连接有可能会被中止掉!这是不能允许的。
有什么解决方案?
有的,lwIP 协议栈已经提供避免这种情况的机制:每个 tcp 连接 都是有优先级的。tcp 连接优先级共有 127 级,lwIP 定义了其中 3 个:
#define TCP_PRIO_MIN 1
#define TCP_PRIO_NORMAL 64
#define TCP_PRIO_MAX 127
其中,使用函数 tcp_new()
新建 PCB 控制块时,默认的优先级是 TCP_PRIO_NORMAL
, 如果你的 tcp 连接比较重要,需要在连接回调函数(accept
)中,修改连接的优先级:
static err_t xxxx_accept(void *arg, struct tcp_pcb *pcb, err_t err)
{
if(pcb == NULL)
return ERR_OK;
tcp_setprio (pcb,TCP_PRIO_MAX); // <--- 这里,修改连接优先级
tcp_recv(pcb, xxxx_recv);
pcb->so_options |= SOF_KEEPALIVE;
return(ERR_OK);
}
上面反馈 BUG 的 Roman Trunov
也用类似方法修改了服务器连接的优先级,新的优先级高于 TCP_PRIO_NORMAL
,所以当 10 个连接都处于 LAST_ACK
状态时,再申请 PCB 时,内核不能中止处于更高优先级的 LAST_ACK
状态连接,PCB 数量不够,连接失败。
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)