工作笔记-----FreeRTOS中的lwIP网络任务为什么会让出CPU
@@ Author: 明月清了个风
@@ Date: 2025.7.30
@@ Ps:最近接触了在FreeRTOS中使用lwIP实现的网络任务,但是在看项目代码的过程中出现了一些疑问------网络任务的优先级为所有任务中最高的,并且任务框架中没有延时,为什么会让出CPU,记录一下这个寻找答案的过程(我也不知道对不对)。
基于FreeRTOS的lwIP任务框架
当使用了操作系统时,就可以使用lwIP的NETCONN接口,并且lwIP也提供了统一的ethernetif.c
供用户使用,使用该接口的网络任务的框架如下(删除了项目中的处理逻辑,只留下了基本的框架):
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,这里的代码应该都是差不多的
c
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()
函数,这个函数用于获得一个新的连接,这个函数原型挺长的就不放出来了,关注中间的一段代码:
c
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_RCVTIMEO
if (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;
}
#else
sys_arch_mbox_fetch(&conn->acceptmbox, &accept_ptr, 0);
#endif /* LWIP_SO_RCVTIMEO*/
}
分支1-----非阻塞
可以看到这个if调用了函数netconn_is_nonblocking(conn)
,进一步去看,这个函数为一个宏定义,原型如下:
c
/** 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
位是否置位,进一步去看这个宏定义的注释就能发现意思是:这个连接是否应该避免阻塞?
c
/** Should this netconn avoid blocking? */
#define NETCONN_FLAG_NON_BLOCKING 0x02
也就是说,如果置位说明该连接在当前操作下应该避免阻塞那么上面的if (netconn_is_nonblocking(conn))
为真,进入该判断内部逻辑,也就是调用sys_arch_mbox_tryfetch(&conn->acceptmbox, &accept_ptr)
,该函数原型如下
c
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_TIMEOUT
here is enough. */
return 1;
}
根据这个函数的命名和逻辑可以看出,该用于尝试去conn->acceptmbox
接收消息ret = xQueueReceive(mbox->mbx, &(*msg), 0);
,并且该操作是非阻塞的,最后一个接收延时参数为0
,符合之前进入该函数体的判断条件.函数下面的注释含义如下:
旧版本的lwIP要求该函数返回等待的时间,但现在不是了,只要返回的数值不等于SYS_ARCH_TIMEOUT就行了
进一步地,我们看这个宏定义的值:
c
/** 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
,该宏定义如下c/** 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
那么从这返回就会继续执行最上面的这段代码
cif (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之后的结果是一样的,最后都会到这个函数,这里我也没看了,因为已经知道为什么阻塞了,上面都是非阻塞的过程。那么另一边就是为什么会阻塞了,也就是这一段代码
c
#if LWIP_SO_RCVTIMEO
if (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;
}
#else
sys_arch_mbox_fetch(&conn->acceptmbox, &accept_ptr, 0);
#endif /* LWIP_SO_RCVTIMEO*/
}
该宏定义原型如下:
c
#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,那么就会按照设置的值进行对应的阻塞。
c
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_TIMEOUT
here is enough. */
return 1;
}
至此,就可以知道为什么任务函数中没有任何显式阻塞过程却能让出CPU了,关键在于LWIP_SO_RCVTIMEO
宏定义,另外netconn->recv_timeout
结构体成员在初始化时,也就是调用函数netconn_new()
时会被自动初始化为0,因此只要开启对应宏定义就能让该任务阻塞了,当然阻塞的地方并不止这一个地方,while循环中和netconn
有关的API应该都有对应的阻塞判断。