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

工作笔记-----FreeRTOS中的lwIP网络任务为什么会让出CPU

工作笔记-----FreeRTOS中的lwIP网络任务为什么会让出CPU

@@ Author: 明月清了个风
@@ Date: 2025.7.30
@@ Ps:最近接触了在FreeRTOS中使用lwIP实现的网络任务,但是在看项目代码的过程中出现了一些疑问——网络任务的优先级为所有任务中最高的,并且任务框架中没有延时,为什么会让出CPU,记录一下这个寻找答案的过程(我也不知道对不对)。

基于FreeRTOS的lwIP任务框架

当使用了操作系统时,就可以使用lwIP的NETCONN接口,并且lwIP也提供了统一的ethernetif.c供用户使用,使用该接口的网络任务的框架如下(删除了项目中的处理逻辑,只留下了基本的框架):

static void EthThread(void *arg)
{struct netconn *conn;err_t err, accept_err;struct netbuf *buf;void *data;u16_t len;LWIP_UNUSED_ARG(arg);/* Create a new connection identifier. */conn = netconn_new(NETCONN_TCP);if (conn != NULL){  /* Bind connection to well known port number 7. */err = netconn_bind(conn, NULL, PORT);if (err == ERR_OK){/* Tell connection to go into listening mode. */netconn_listen(conn);while (1) {/* Grab new connection. */accept_err = netconn_accept(conn, &newconn);/* Process the new connection. */if (accept_err == ERR_OK) {while (netconn_recv(newconn, &buf) == ERR_OK) {do {netbuf_data(buf, &data, &len);// 数据处理逻辑} while (netbuf_next(buf) >= 0);netbuf_delete(buf);}netconn_close(newconn);netconn_delete(newconn);}}}else{netconn_delete(newconn);}} 
}

当然在这之前需要初始化lwIP,这里的代码应该都是差不多的

uint8_t IP_ADDRESS[4];
uint8_t NETMASK_ADDRESS[4];
uint8_t GATEWAY_ADDRESS[4];struct netif gnetif;  	// lwIP提供的网络接口结构体
ip4_addr_t ipaddr;  	// IP地址
ip4_addr_t netmask; 	// 子网掩码
ip4_addr_t gw;			// 网关void lwip_init(void)
{// 需要对IP_ADDRESS,NETMASK_ADDRESS,GATEWAY_ADDRESS内容进行设置,举例如下IP_ADDRESS[0] = 192;IP_ADDRESS[1] = 168;IP_ADDRESS[2] = 110;IP_ADDRESS[3] = 110;NETMASK_ADDRESS[0] = 255;NETMASK_ADDRESS[1] = 255;NETMASK_ADDRESS[2] = 255;NETMASK_ADDRESS[3] = 0;GATEWAY_ADDRESS[0] = 192;GATEWAY_ADDRESS[1] = 168;GATEWAY_ADDRESS[2] = 110;GATEWAY_ADDRESS[3] = 1;tcpip_init( NULL, NULL); // 这里会创建tcpip线程以及邮箱IP4_ADDR(&ipaddr, IP_ADDRESS[0], IP_ADDRESS[1], IP_ADDRESS[2], IP_ADDRESS[3]);IP4_ADDR(&netmask, NETMASK_ADDRESS[0], NETMASK_ADDRESS[1] , NETMASK_ADDRESS[2], NETMASK_ADDRESS[3]);IP4_ADDR(&gw, GATEWAY_ADDRESS[0], GATEWAY_ADDRESS[1], GATEWAY_ADDRESS[2], GATEWAY_ADDRESS[3]);// 初始化网口,这里传入了ethernetif_init作为初始化函数netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input);netif_set_default(&gnetif);   //设置netif为默认网口if (netif_is_link_up(&gnetif)){netif_set_up(&gnetif);      //打开netif网口}else{netif_set_down(&gnetif);}
}

其中netif_add传入了ethernetif_init函数作为网口初始化函数,如果这里传入NULL就会报错

在这里插入图片描述

这个函数中进行了netif的部分结构体成员初始化,并在最后调用了low_level_init(netif);进行初始化

为什么会让出CPU

可以看到在任务函数中没有会导致任务阻塞让出CPU的函数或操作,比如等待信号量或者等待队列消息,那也意味着如果该任务优先级最高,那么就会永远占用。

那么为了看在哪里会阻塞这个任务,就要在while循环中去寻找

首先是netconn_accept()函数,这个函数用于获得一个新的连接,这个函数原型挺长的就不放出来了,关注中间的一段代码:

if (netconn_is_nonblocking(conn)) {if (sys_arch_mbox_tryfetch(&conn->acceptmbox, &accept_ptr) == SYS_ARCH_TIMEOUT) {API_MSG_VAR_FREE_ACCEPT(msg);NETCONN_MBOX_WAITING_DEC(conn);return ERR_WOULDBLOCK;}} else {
#if LWIP_SO_RCVTIMEOif (sys_arch_mbox_fetch(&conn->acceptmbox, &accept_ptr, conn->recv_timeout) == SYS_ARCH_TIMEOUT) {API_MSG_VAR_FREE_ACCEPT(msg);NETCONN_MBOX_WAITING_DEC(conn);return ERR_TIMEOUT;}
#elsesys_arch_mbox_fetch(&conn->acceptmbox, &accept_ptr, 0);
#endif /* LWIP_SO_RCVTIMEO*/}
分支1-----非阻塞

可以看到这个if调用了函数netconn_is_nonblocking(conn),进一步去看,这个函数为一个宏定义,原型如下:

/** Get the blocking status of netconn calls (@todo: write/send is missing) */
#define netconn_is_nonblocking(conn)        (((conn)->flags & NETCONN_FLAG_NON_BLOCKING) != 0)

注释的意思是获取当前连接的阻塞状态,通过判断connflags成员的NETCONN_FLAG_NON_BLOCKING位是否置位,进一步去看这个宏定义的注释就能发现意思是:这个连接是否应该避免阻塞?

/** Should this netconn avoid blocking? */
#define NETCONN_FLAG_NON_BLOCKING             0x02

也就是说,如果置位说明该连接在当前操作下应该避免阻塞那么上面的if (netconn_is_nonblocking(conn))为真,进入该判断内部逻辑,也就是调用sys_arch_mbox_tryfetch(&conn->acceptmbox, &accept_ptr),该函数原型如下

u32_t
sys_arch_mbox_tryfetch(sys_mbox_t *mbox, void **msg)
{BaseType_t ret;void *msg_dummy;LWIP_ASSERT("mbox != NULL", mbox != NULL);LWIP_ASSERT("mbox->mbx != NULL", mbox->mbx != NULL);if (!msg) {msg = &msg_dummy;}ret = xQueueReceive(mbox->mbx, &(*msg), 0);if (ret == errQUEUE_EMPTY) {*msg = NULL;return SYS_MBOX_EMPTY;}LWIP_ASSERT("mbox fetch failed", ret == pdTRUE);/* Old versions of lwIP required us to return the time waited.This is not the case any more. Just returning != SYS_ARCH_TIMEOUThere is enough. */return 1;
}

根据这个函数的命名和逻辑可以看出,该用于尝试去conn->acceptmbox接收消息ret = xQueueReceive(mbox->mbx, &(*msg), 0);,并且该操作是非阻塞的,最后一个接收延时参数为0,符合之前进入该函数体的判断条件.函数下面的注释含义如下:

旧版本的lwIP要求该函数返回等待的时间,但现在不是了,只要返回的数值不等于SYS_ARCH_TIMEOUT就行了

进一步地,我们看这个宏定义的值:

/** Return code for timeouts from sys_arch_mbox_fetch and sys_arch_sem_wait */
#define SYS_ARCH_TIMEOUT 0xffffffffUL
  • 返回值为1,符合注释,那么就会判断失败并继续执行netconn_accept()函数下面的部分,

  • 若在上面就返回了,也就是if (ret == errQUEUE_EMPTY)判断成功,这意味着xQueueReceive()函数接收消息失败(这是FreeRtos中的队列操作函数,接收成功返回1,接收失败返回0),将传进来保存接收消息的指针赋值为*msg = NULL,并且返回SYS_MBOX_EMPTY,该宏定义如下

    /** sys_mbox_tryfetch() returns SYS_MBOX_EMPTY if appropriate.* For now we use the same magic value, but we allow this to change in future.*/
    #define SYS_MBOX_EMPTY SYS_ARCH_TIMEOUT
    

    那么从这返回就会继续执行最上面的这段代码

    if (sys_arch_mbox_tryfetch(&conn->acceptmbox, &accept_ptr) == SYS_ARCH_TIMEOUT) {API_MSG_VAR_FREE_ACCEPT(msg);NETCONN_MBOX_WAITING_DEC(conn);return ERR_WOULDBLOCK;}
    

    其中API_MSG_VAR_FREE_ACCEPT(msg);是一个经过层层宏定义的函数,原型为memp_free,应该是和内存管理相关的,我也没细看了这边。

然后就会执行NETCONN_MBOX_WAITING_DEC(conn);函数,这和上面返回1之后的结果是一样的,最后都会到这个函数,这里我也没看了,因为已经知道为什么阻塞了,上面都是非阻塞的过程。那么另一边就是为什么会阻塞了,也就是这一段代码

#if LWIP_SO_RCVTIMEOif (sys_arch_mbox_fetch(&conn->acceptmbox, &accept_ptr, conn->recv_timeout) == SYS_ARCH_TIMEOUT) {API_MSG_VAR_FREE_ACCEPT(msg);NETCONN_MBOX_WAITING_DEC(conn);return ERR_TIMEOUT;}
#elsesys_arch_mbox_fetch(&conn->acceptmbox, &accept_ptr, 0);
#endif /* LWIP_SO_RCVTIMEO*/}

该宏定义原型如下:

#define LWIP_SO_RCVTIMEO        1    /* set to 1 to enable receive timeout for sockets/netconns and SO_RCVTIMEO processing */

通过这个宏定义的注释可以看出,只要开启这个宏定义就会允许sockets/netconns阻塞延时。

那么现在看另一分支的判断条件,也就是会调用sys_arch_mbox_fetch()函数,该函数原型如下,可以发现,如果conn->recv_timeout为0,那么该接收过程就会一直阻塞直到收到消息,如果不为0,那么就会按照设置的值进行对应的阻塞。


u32_t
sys_arch_mbox_fetch(sys_mbox_t *mbox, void **msg, u32_t timeout_ms)
{BaseType_t ret;void *msg_dummy;LWIP_ASSERT("mbox != NULL", mbox != NULL);LWIP_ASSERT("mbox->mbx != NULL", mbox->mbx != NULL);if (!msg) {msg = &msg_dummy;}if (!timeout_ms) {/* wait infinite */ret = xQueueReceive(mbox->mbx, &(*msg), portMAX_DELAY);LWIP_ASSERT("mbox fetch failed", ret == pdTRUE);} else {TickType_t timeout_ticks = timeout_ms / portTICK_RATE_MS;ret = xQueueReceive(mbox->mbx, &(*msg), timeout_ticks);if (ret == errQUEUE_EMPTY) {/* timed out */*msg = NULL;return SYS_ARCH_TIMEOUT;}LWIP_ASSERT("mbox fetch failed", ret == pdTRUE);}/* Old versions of lwIP required us to return the time waited.This is not the case any more. Just returning != SYS_ARCH_TIMEOUThere is enough. */return 1;
}

至此,就可以知道为什么任务函数中没有任何显式阻塞过程却能让出CPU了,关键在于LWIP_SO_RCVTIMEO宏定义,另外netconn->recv_timeout结构体成员在初始化时,也就是调用函数netconn_new()时会被自动初始化为0,因此只要开启对应宏定义就能让该任务阻塞了,当然阻塞的地方并不止这一个地方,while循环中和netconn有关的API应该都有对应的阻塞判断。

http://www.lryc.cn/news/604311.html

相关文章:

  • 24串高边BMS全套设计方案!
  • 51单片机入门:数码管原理介绍及C代码实现
  • YOLO融合MogaNet中的ChannelAggregationFFN模块
  • 基于 Python 开发的信阳市天气数据可视化系统源代码+数据库+课程报告
  • 基于 Hadoop 生态圈的数据仓库实践 —— OLAP 与数据可视化(三)
  • C++ Qt网络编程实战:跨平台TCP调试工具开发
  • 基于 Hadoop 生态圈的数据仓库实践 —— OLAP 与数据可视化(四)
  • 北京理工大学医工交叉教学实践分享(1)|如何以实践破解数据挖掘教学痛点
  • 使用es实现全文检索并且高亮显示
  • ArcGIS以及ArcGIS Pro如何去除在线地图制作者名单
  • 6.Origin2021如何绘制Y轴截断图?
  • 技术速递|GitHub Copilot 的 Agent 模式现已全面上线 JetBrains、Eclipse 和 Xcode!
  • 2025Nacos安装Mac版本 少走弯路版本
  • 知识速查大全:python面向对象基础
  • 手撕设计模式——智能家居之外观模式
  • iOS 签名证书与上架流程详解,无 Mac 环境下的上架流程
  • 专题:2025机器人产业技术图谱与商业化指南|附130+份报告PDF、数据汇总下载
  • 2025光伏自动化破局!艾利特机器人用“智能感知+柔性控制”领跑行业
  • Scala实现常用排序算法
  • USB电源原理图学习笔记
  • 开源 Arkts 鸿蒙应用 开发(十二)传感器的使用
  • STM32 外部中断 和 定时器中断
  • 第六章第一节 TIM 定时中断
  • (RedmiBook)上禁用触摸板或自带键盘
  • mybatis-入门
  • 《Vuejs设计与实现》第 12 章(组件实现原理 下)
  • 量子图灵机 Quantum Turing Machine, QTM
  • 【从基础到实战】STL string 学习笔记(上)
  • 如何在出售Windows11/10/8/7前彻底清除电脑数据
  • Python 使用 asyncio 包处理并 发(使用asyncio包编写服务器)