工作笔记-----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, ðernetif_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)
注释的意思是获取当前连接的阻塞状态,通过判断conn
的flags
成员的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应该都有对应的阻塞判断。