自研UART接口网络驱动数次ping后报No buffer Space可用故障求助
UART自定义网络驱动缓冲区不足报错的根因与修复方案
报错说明:
ping: sendmsg: No buffer space available并非系统物理内存耗尽,而是内核socket发送缓冲区因为网络队列堵塞、资源泄漏触发了临时锁死,因此不会在内核日志留下通用内存不足的异常记录,和排查到的现象完全匹配。
核心根因定位
1. 发送路径逻辑缺陷
- 流控逻辑完全失效。代码中在发送入口调用
netif_stop_queue(ndev)停止队列后,无论DMA传输是否完成、无论DMA提交成功或失败,走到fallback_err分支立刻调用netif_start_queue(ndev)重启队列,等于流控操作没有任何实际作用。UART+DMA是单通道串行传输,没有并行处理能力,新的发送请求会在前一个报文还没传完时就覆盖lp->tx_buf的缓存,直接造成报文错乱。 - 存在持续的skb引用泄漏。代码中执行
sk_buff = skb_get(skb)获取了原skb的额外引用,但后续流程只对原始skb做了dev_kfree_skb(skb)释放,这个额外引用在所有代码分支中都没有被回收。每发一次报文就泄漏一个skb,内核网络缓存池、socket发送缓冲区会被这些未释放的skb逐步占满,等缓存耗尽后新的ping报文就无法分配发送空间,触发报错。因为泄漏是慢过程,不会触发内核的低内存告警,也符合查不到关联日志的现象。 - 发送完成事件跟踪缺失。代码中为DMA请求开了传输完成中断,但既没有注册DMA完成回调,也没有在UART的TC(发送完成)中断中跟踪通道的忙闲状态,发送流程完全是“丢给DMA就不管”的状态,无法基于实际传输进度做流控。
2. 接收路径逻辑缺陷
- skb分配失败分支没有做拦截。当
dev_alloc_skb返回空指针时,代码只做了丢包计数,没有中断后续流程,依然会对着空skb执行skb_put、eth_type_trans、netif_rx操作,轻则触发内核空指针异常,重则破坏协议栈接收队列的结构,造成缓存泄漏。 - DMA接收指针逻辑脱节。代码中靠手动维护的
last_res计数推算软件读取位置,没有和DMA硬件实际的写指针做同步,会出现软件读位置和硬件写位置不匹配的问题,要么重复读取旧数据,要么漏读新报文,甚至把无效数据交给协议栈,产生大量无效IP/ICMP包,进一步占满协议栈的缓存空间。
对应修复方案
- 重构发送流控逻辑
- 移除提交完DMA就立刻
netif_start_queue的错误代码,把netif_start_queue的调用时机从发送入口移到DMA传输完成的中断回调中,即等整个报文通过UART发完、DMA通道空闲时,再根据剩余缓存空间判断是否重启队列。 - 驱动内部维护DMA通道忙标记、待发送缓存长度计数:提交DMA前检查通道是否忙,如果忙就直接停队列;如果当前待发送长度超过
tx_buf的总容量,也直接停队列,避免覆盖未传输完成的数据。
- 移除提交完DMA就立刻
- 修复skb引用泄漏
- 移除无意义的
sk_buff = skb_get(skb)操作:因为代码中已经把skb的数据memcpy到了驱动独立的tx_buf,根本不需要给skb增加额外引用,直接在发送流程的入口memcpy完成后,就可以释放原skb,没有必要跨流程持有引用。如果必须保留这个引用,需要在所有代码分支(包括DMA提交失败的分支)对这个额外引用单独做dev_kfree_skb释放。
- 移除无意义的
- 补全发送完成中断处理
- 为DMA通道注册传输完成回调函数,在回调中完成三件事:清零DMA通道忙标记、更新tx统计计数、如果队列之前因为通道忙被停止,就调用
netif_start_queue重启队列。
- 为DMA通道注册传输完成回调函数,在回调中完成三件事:清零DMA通道忙标记、更新tx统计计数、如果队列之前因为通道忙被停止,就调用
- 修正接收路径的错误分支
- 恢复
dev_alloc_skb失败分支的跳转逻辑:当skb分配失败时,直接跳转至接收流程的退出点,不再执行后续的空指针操作。
- 恢复
- 修正接收缓冲区的指针同步逻辑
- 用DMA硬件提供的写指针位置同步软件的读取偏移,不再靠手动减计数的方式推算读取位置,避免软件和硬件读写位置脱节。
内容的提问来源于stack exchange,提问作者Gonzo Gonzales
相关产品推荐
相关产品推荐

