lwIP 细节之三:errf 回调函数是何时调用的

news/2024/7/5 7:36:35

使用 lwIP 协议栈进行 TCP 裸机编程,其本质就是编写协议栈指定的各种回调函数。将你的应用逻辑封装成函数,注册到协议栈,在适当的时候,由协议栈自动调用,所以称为回调

注:除非特别说明,以下内容针对 lwIP 2.0.0 及以上版本。

向协议栈注册回调函数有专门的接口,如下所示:

tcp_err(pcb, errf);							//注册 TCP 接到 RST 标志或发生错误回调函数 errf
tcp_connect(pcb, ipaddr, port, connected);	//注册 TCP 建立连接成功回调函数 connecter
tcp_accept(pcb, accept);					//注册 TCP 处于 LISTEN 状态时,监听到有新的连接接入
tcp_recv(pcb, recv);						//注册 TCP 接收到数据回调函数 recv
tcp_sent(pcb, sent);						//注册 TCP 发送数据成功回调函数 sent
tcp_poll(pcb, poll, interval);				//注册 TCP 周期性执行回调函数 poll

本节讲述 errf 回调函数。

errf 回调函数

在 TCP 控制块中,函数指针 errf 指向用户实现的 TCP 错误处理函数,当 TCP 连接发送错误时,由协议栈调用此函数。
函数指针 errf 的类型为 tcp_err_fn ,该类型定义在 tcp.h 中:

/** Function prototype for tcp error callback functions. Called when the pcb
 * receives a RST or is unexpectedly closed for any other reason.
 *
 * @note The corresponding pcb is already freed when this callback is called!
 *
 * @param arg Additional argument to pass to the callback function (@see tcp_arg())
 * @param err Error code to indicate why the pcb has been closed
 *            ERR_ABRT: aborted through tcp_abort or by a TCP timer
 *            ERR_RST: the connection was reset by the remote host
 */
typedef void  (*tcp_err_fn)(void *arg, err_t err);

从注释得知,错误处理函数在接收到 RST 标志,或者连接意外关闭时,由协议栈调用。
注意,当这个函数调用的时候,TCP 控制块已经释放掉了。

协议栈通过宏 TCP_EVENT_ERR(last_state,errf,arg,err) 调用 errf 指向的错误处理函数,宏 TCP_EVENT_ERR 定义在 tcp_priv.h 中:

#define TCP_EVENT_ERR(last_state,errf,arg,err)                 \
  do {                                                         \
    LWIP_UNUSED_ARG(last_state);                               \
    if((errf) != NULL)                                         \
      (errf)((arg),(err));                                     \
  } while (0)

可以看到这个宏的第 4 个参数就是传递给错误处理函数的错误码
以关键字 TCP_EVENT_ERR 搜索源码,可以搜索到 4 处使用:

TCP_EVENT_ERR(pcb->state, pcb->errf, pcb->callback_arg, ERR_RST);
TCP_EVENT_ERR(pcb->state, pcb->errf, pcb->callback_arg, ERR_CLSD);
TCP_EVENT_ERR(last_state, errf, errf_arg, ERR_ABRT);
TCP_EVENT_ERR(last_state, err_fn, err_arg, ERR_ABRT);

用到了 3 个错误码:ERR_RSTERR_CLSDERR_ABRT ,分别表示连接复位、连接关闭和连接异常。

1.连接复位

当远端连接发送 RST 标志,并且报文序号正确是,调用错误类型为 ERR_RST 的错误处理回调函数,这一过程在 tcp_input 函数中执行。

void
tcp_input(struct pbuf *p, struct netif *inp)
{
  // 经过一系列检测,没有错误

  flags = TCPH_FLAGS(tcphdr);	// 这里获取数据包的 [标志] 字段

  /* 在本地找到有效的控制块 pcb */
  if (pcb != NULL) {

    tcp_input_pcb = pcb;
    err = tcp_process(pcb);		// [标志]中有 RST, 且报文序号正确:recv_flags |= TF_RESET
    /* A return value of ERR_ABRT means that tcp_abort() was called
       and that the pcb has been freed. If so, we don't do anything. */
    if (err != ERR_ABRT) {
      if (recv_flags & TF_RESET) {
        /* TF_RESET means that the connection was reset by the other
           end. We then call the error callback to inform the
           application that the connection is dead before we
           deallocate the PCB. */
        TCP_EVENT_ERR(pcb->state, pcb->errf, pcb->callback_arg, ERR_RST);
        tcp_pcb_remove(&tcp_active_pcbs, pcb);
        tcp_free(pcb);
      } 
    }
  } 
  return;
}

tcp_process 函数中关于 RST 标志的判断代码:

static err_t
tcp_process(struct tcp_pcb *pcb)
{
  /* Process incoming RST segments. */
  if (flags & TCP_RST) {		// flags 保存数据包的 [标志] 字段,在 tcp_input 函数中取得		
    /* First, determine if the reset is acceptable. */
    if (pcb->state == SYN_SENT) {
      /* "In the SYN-SENT state (a RST received in response to an initial SYN),
          the RST is acceptable if the ACK field acknowledges the SYN." */
      if (ackno == pcb->snd_nxt) {
        acceptable = 1;
      }
    } else {
      /* "In all states except SYN-SENT, all reset (RST) segments are validated
          by checking their SEQ-fields." */
      if (seqno == pcb->rcv_nxt) {
        acceptable = 1;
      } else  if (TCP_SEQ_BETWEEN(seqno, pcb->rcv_nxt,
                                  pcb->rcv_nxt + pcb->rcv_wnd)) {
        /* If the sequence number is inside the window, we send a challenge ACK
           and wait for a re-send with matching sequence number.
           This follows RFC 5961 section 3.2 and addresses CVE-2004-0230
           (RST spoofing attack), which is present in RFC 793 RST handling. */
        tcp_ack_now(pcb);
      }
    }

    if (acceptable) {
      LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_process: Connection RESET\n"));
      recv_flags |= TF_RESET;
      tcp_clear_flags(pcb, TF_ACK_DELAY);
      return ERR_RST;
    }
  }
}

2.连接关闭

这是为了处理异常情况。

当接收到远端的 FIN 标志,表示远端要关闭 TCP 连接,此时协议栈会通知应用程序。通常情况下,应用程序应该调用 tcp_close 函数关闭连接。但是,若应用才程序没有执行这个步骤,则 TCP 状态处于 LADT_ACK 并且接收到远端的 ACK 标志后,调用错误类型为 ERR_CLSD 的错误回调函数,简化后的代码为:

void tcp_input(struct pbuf *p, struct netif *inp)
{
	err = tcp_process(pcb);
    /* A return value of ERR_ABRT means that tcp_abort() was called
       and that the pcb has been freed. If so, we don't do anything. */
    if (err != ERR_ABRT) {
       if (recv_flags & TF_CLOSED) {	/*处于 LAST_ACK 状态时收到 ACK 标识*/
			/* The connection has been closed and we will deallocate the PCB. */
			if (!(pcb->flags & TF_RXCLOSED)) {
				/* Connection closed although the application has only shut down the
					tx side: call the PCB's err callback and indicate the closure to
					ensure the application doesn't continue using the PCB. */
				TCP_EVENT_ERR(pcb->errf, pcb->callback_arg, ERR_CLSD);	// <--- 这里
			}
			tcp_pcb_remove(&tcp_active_pcbs, pcb);
			memp_free(MEMP_TCP_PCB, pcb);
		}
    }
}

3.连接异常

3.1 由 tcp_abandon 函数调用

tcp_abandon 函数会调用错误类型为 ERR_ABRT 的错误回调函数,简化后的代码为:

void
tcp_abandon(struct tcp_pcb *pcb, int reset)
{
  if (pcb->state == TIME_WAIT) {
    tcp_pcb_remove(&tcp_tw_pcbs, pcb);
    tcp_free(pcb);
  } else {
	// 从链表中移除 TCP_PCB
	// 按需释放[未应答]、[未发送]、[失序]报文内存
	// 按需发送 RST 标志
	// 释放 TCP_PCB :tcp_free(pcb)
    TCP_EVENT_ERR(last_state, errf, errf_arg, ERR_ABRT);	// <-- 这里
  }
}

tcp_abandon 函数又是谁在调用呢?

3.1.1 tcp_listen_input 函数中

tcp_listen_input 函数中,检测接收到 SYN 标志报文,则创建新的 TCP_PCB,然后发送 SYN|ACK 标志报文。在这一过程中,若发送 SYN|ACK 标志报文失败,则调用 tcp_abandon 函数放弃这个连接,在 tcp_abandon 函数内部会调用错误类型为 ERR_ABRT 的错误处理回调函数。简化后的代码为:

static void
tcp_listen_input(struct tcp_pcb_listen *pcb)
{
  /* In the LISTEN state, we check for incoming SYN segments,
     creates a new PCB, and responds with a SYN|ACK. */
  if (flags & TCP_SYN) {
    npcb = tcp_alloc(pcb->prio);
    
	/* 这里 TCP PCB 申请成功,初始化新的 PCB*/
	// ...
    npcb->state = SYN_RCVD;
    // ...

    /* Send a SYN|ACK together with the MSS option. */
    rc = tcp_enqueue_flags(npcb, TCP_SYN | TCP_ACK);
    if (rc != ERR_OK) {
      tcp_abandon(npcb, 0);		// <-- 这里
      return;
    }
    tcp_output(npcb);
  }
  return;
}

3.1.2 tcp_kill_state 函数中
在《lwIP 细节之二:协议栈什么情况下发送 RST 标志》博文中,有提到 tcp_alloc 函数,tcp_alloc 函数设计原则是尽一切可能返回一个有效的 TCP_PCB 控制块,因此,当 TCP_PCB 不足时,函数可能 “杀死”(kill)正在使用的连接,以释放 TCP_PCB 控制块!
具体就是:

  1. 先调用 tcp_kill_timewait 函数,试图找到 TIME_WAIT 状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用 tcp_abort(pcb) 函数 “杀” 掉这个连接,这会发送 RST 标志,以便通知远端释放连接;
  2. 如果第 1 步失败了,则调用 tcp_kill_state 函数,试图找到 LAST_ACKCLOSING 状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用 tcp_abandon(pcb, 0) 函数 “杀” 掉这个连接,注意这个函数并不会发送 RST 标志,处于这两种状态的连接都是等到对方发送的 ACK 就会结束连接,不会有数据丢失;
  3. 如果第 2 步也失败了,则调用 tcp_kill_prio(prio) 函数,试图找到小于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接!如果找到符合条件的控制块 pcb ,则调用 tcp_abort(pcb) 函数 “杀” 掉这个连接,这会发送 RST 标志。

这里的第 2 步,调用 tcp_abandon(pcb, 0) 函数 “杀” 掉这个连接时,会调用 tcp_abandon 函数放弃这个连接,在 tcp_abandon 函数内部会调用错误类型为 ERR_ABRT 的错误处理回调函数。简化后的代码为:

static void
tcp_kill_state(enum tcp_state state)
{
  inactivity = 0;
  inactive = NULL;
  /* Go through the list of active pcbs and get the oldest pcb that is in state
     CLOSING/LAST_ACK. */
  for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next) {
    if (pcb->state == state) {
      if ((u32_t)(tcp_ticks - pcb->tmr) >= inactivity) {
        inactivity = tcp_ticks - pcb->tmr;
        inactive = pcb;
      }
    }
  }
  if (inactive != NULL) {
    LWIP_DEBUGF(TCP_DEBUG, ("tcp_kill_closing: killing oldest %s PCB %p (%"S32_F")\n",
                            tcp_state_str[state], (void *)inactive, inactivity));
    /* Don't send a RST, since no data is lost. */
    tcp_abandon(inactive, 0);
  }
}

3.1.3 tcp_abort 函数中
tcp_abort 函数中止一个连接,会向远端主机发送一个 RST 标志。当从其中一个 TCP 回调函数中调用时,请确保一定返回 ERR_ABRT 错误码(其它情况绝不要返回 ERR_ABRT 错误码,否则可能会有内存泄漏的风险或者访问已经释放的内存!
tcp_abort 函数代码简单,原始无简化代码为:

void
tcp_abort(struct tcp_pcb *pcb)
{
  tcp_abandon(pcb, 1);
}
3.2 由 tcp_slowtmr 函数调用

tcp_slowtmr 函数中完成重传和超时处理,当重传达到设定次数,或者超时达到设定时间,则调用错误类型为 ERR_ABRT 的错误处理回调函数。

重传和超时事件有:

  • PCB 控制块处于 SYN_SENT 状态,重传次数达到 TCP_SYNMAXRTX 次(默认 6 次)
  • PCB 控制块处于其它状态,重传次数达到 TCP_MAXRTX 次(默认 12 次)
  • 坚持定时器探查窗口达到 TCP_MAXRTX 次(默认 12 次)
  • PCB 控制块处于 FIN_WAIT_2 状态,超时达到 TCP_FIN_WAIT_TIMEOUT 秒(默认 20 秒)
  • PCB 控制块处于 SYN_RCVD 状态,超时达到 TCP_SYN_RCVD_TIMEOUT 秒(默认 20 秒)
  • PCB 控制块处于 LAST_ACK 状态,超时达到 2 * TCP_MSL 秒(默认 120 秒)
  • 使能保活、PCB 控制块处于 ESTABLISHEDCLOSE_WAIT 状态,超时达到 pcb->keep_idle + TCP_KEEP_DUR(pcb) 秒(默认 2 小时 10 分 48 秒)

tcp_slowtmr 函数简化后的代码为:

/**
 * Called every 500 ms and implements the retransmission timer and the timer that
 * removes PCBs that have been in TIME-WAIT for enough time. It also increments
 * various timers such as the inactivity timer in each PCB.
 *
 * Automatically called from tcp_tmr().
 */
void
tcp_slowtmr(void)
{
  while (pcb != NULL) {
	/* 这里表明处于 CLOSED、LISTEN 和 TIME_WAIT 状态的连接不会有重传 */
    LWIP_ASSERT("tcp_slowtmr: active pcb->state != CLOSED\n", pcb->state != CLOSED);
    LWIP_ASSERT("tcp_slowtmr: active pcb->state != LISTEN\n", pcb->state != LISTEN);
    LWIP_ASSERT("tcp_slowtmr: active pcb->state != TIME-WAIT\n", pcb->state != TIME_WAIT);

    if (pcb->state == SYN_SENT && pcb->nrtx >= TCP_SYNMAXRTX) {
      ++pcb_remove;				// 处于SYN_SENT 状态,重传达到 6 次
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: max SYN retries reached\n"));
    } else if (pcb->nrtx >= TCP_MAXRTX) {
      ++pcb_remove;				// 其它状态,重传达到 12 次
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: max DATA retries reached\n"));
    } else {
      if (pcb->persist_backoff > 0) {
        if (pcb->persist_probe >= TCP_MAXRTX) {
          ++pcb_remove; 		// 探查次数达到 12 次 */
        }
    }

    if (pcb->state == FIN_WAIT_2) {
      if (pcb->flags & TF_RXCLOSED) {
        if ((u32_t)(tcp_ticks - pcb->tmr) >
            TCP_FIN_WAIT_TIMEOUT / TCP_SLOW_INTERVAL) {
          ++pcb_remove;			// 处于 FIN_WAIT_2 状态,超时达到 20 秒
        }
      }
    }

    /* 注意只有 ESTABLISHED 和 CLOSE_WAIT 状态才会有 KEEPALIVE(保活) */
    if (ip_get_option(pcb, SOF_KEEPALIVE) &&
        ((pcb->state == ESTABLISHED) ||
         (pcb->state == CLOSE_WAIT))) {
      if ((u32_t)(tcp_ticks - pcb->tmr) >
          (pcb->keep_idle + TCP_KEEP_DUR(pcb)) / TCP_SLOW_INTERVAL) {
        ++pcb_remove;			// 使能保活,超时 2 小时 10 分钟 48 秒
        ++pcb_reset;
      } 
    }

    if (pcb->state == SYN_RCVD) {
      if ((u32_t)(tcp_ticks - pcb->tmr) >
          TCP_SYN_RCVD_TIMEOUT / TCP_SLOW_INTERVAL) {
        ++pcb_remove;			// 处于 SYN_RCVD 状态,超时达到 20 秒
      }
    }

    if (pcb->state == LAST_ACK) {
      if ((u32_t)(tcp_ticks - pcb->tmr) > 2 * TCP_MSL / TCP_SLOW_INTERVAL) {
        ++pcb_remove;			// 处于 LAST_ACK 状态,超时达到 120 秒
      }
    }

    /* 需要移除 PCB 控制块 */
    if (pcb_remove) {
      tcp_pcb_purge(pcb);		// 释放 PCB 中的数据缓冲区(refused_data、unsent、unacked、ooseq)
      
      if (prev != NULL) {		// 从 tcp_active_pcbs 列表中移除 PCB
        prev->next = pcb->next;
      } else {
        tcp_active_pcbs = pcb->next;
      }

      if (pcb_reset) {			// 根据需要发送 RST 标志
        tcp_rst(pcb, pcb->snd_nxt, pcb->rcv_nxt, &pcb->local_ip, &pcb->remote_ip,
                pcb->local_port, pcb->remote_port);
      }
      tcp_free(pcb2);			// 释放 PCB 控制块内存
	  
	  /* 调用错误回调函数 */
      TCP_EVENT_ERR(last_state, err_fn, err_arg, ERR_ABRT);
    } 
  }
}






读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)
千金难买知识,但可以买好多奶粉


http://lihuaxi.xjx100.cn/news/1886164.html

相关文章

业内首份!工业领域数据安全政策汇编发布(附下载)

在工业领域&#xff0c;数据是贯穿工业互联网的“血液”&#xff0c;是提质降本增效的关键。工业数据增长迅速、种类繁多、体量庞大&#xff0c;数据安全已成为保障工业发展&#xff0c;保障社会稳定的要点。 随着工业企业信息化的普及以及工业互联网的快速发展&#xff0c;工…

索引的使用

索引是一种数据结构&#xff0c;用于快速查找数据库中的数据。索引可以加快查询的速度&#xff0c;并减少数据库的负载和响应时间。以下是使用索引的一些方法&#xff1a; 1.创建索引&#xff1a;可以通过CREATE INDEX语句创建索引。在创建索引时&#xff0c;需要指定要创建索…

关于代码质量度量和分析的一些总结

最近团队做CMMI3认证&#xff0c;这期间涉及到了代码质量度量。花了点时间做了总结&#xff0c;分享给大家。 先看一张整体的图&#xff0c;然后逐个指标展开说明。 一、单元测试覆盖率 单元测试覆盖率&#xff08;Coverage&#xff09;是一个度量单元测试覆盖了多少代码的指标…

java面试题-线程、线程池的了解及工作原理、拒绝策略

远离八股文&#xff0c;面试大白话&#xff0c;通俗且易懂 看完后试着用自己的话复述出来。有问题请指出&#xff0c;有需要帮助理解的或者遇到的真实面试题不知道怎么总结的也请评论中写出来&#xff0c;大家一起解决。 java面试题汇总-目录-持续更新中 这篇还蛮好理解的&…

2023年最新prometheus + grafana搭建和使用+gmail邮箱告警配置

一、安装prometheus 1.1 安装 prometheus官网下载地址 sudo -i mkdir -p /opt/prometheus #移动解压后的文件名到/opt/,并改名prometheus mv prometheus-2.45 /opt/prometheus/ #创建一个专门的prometheus用户&#xff1a; -M 不创建家目录&#xff0c; -s 不让登录 useradd…

AI日报:苹果为使用Mac的人工智能开发者推出开源工具

文章目录 总览主要介绍开发理念开发细节MLX功能用途 MLX可以用于商业软件吗&#xff1f; 总览 苹果正在为开发人员提供新的工具&#xff0c;用于在其硬件上训练和运行大型语言模型。 主要介绍 开发理念 苹果公司通过发布一系列新的开源人工智能工具&#xff0c;向开源人工智能…

最大公约数gcd的通俗理解和Java代码的实现

最大公约数 什么是最大公约数最大公约数的计算练习&#xff08;找出数组的最大公约数&#xff09; 什么是最大公约数 最大公约数&#xff08;Greatest CommonDivisor&#xff0c;简称GCD&#xff09;是指两个或多个整数共有的最大正因数&#xff0c;即能够同时整除这些数的最大…

BigDecimald简单使用

为什么要用BigDecimal运算 在计算浮点型数据时,往往会存在数据计算失真问题 例1 2.0 - 1.9 应该等于0.1,用float类型赋值运算得出的结果为0.100000024,有问题 例2 1.8 - 1.9 应该等于 -0.1,用double类型赋值得出的结果为-0.09999999999999987,明显有问题 BigDecimal使用 BigDec…