网络二<网卡的接收报文>

分类:  嵌入式  C  网络2011-03-30 11:34  2782人阅读  评论(0)  收藏  举报

纠结了好多天,终于弄懂了B440X的处理。

上篇讲到通过中断,最终网卡调用了b44_rx()来接收报文

 

对这个函数中的一些参数,可以这样理解:

bp->rx_cons – 处理器处理到的缓冲区号

bp->rx_pending – 分配的缓冲区个数

bp->rx_prod – 当前缓冲区的最后一个缓冲号

 

这里要参数B440X的手册了解下寄存器的作用:

#define B44_DMARX_ADDR        0x0214UL /* DMA RX Descriptor Ring Address */

#define B44_DMARX_PTR  0x0218UL /* DMA RX Last Posted Descriptor */

#define B44_DMARX_STAT 0x021CUL /* DMA RX Current Active Desc. + Status */

b44_rx()来说,B44_DMARX_ADDR储存了环形缓冲的基地址,B44_DMARX_PTR存储了环形缓冲最后一个缓冲区号,这两个寄存器都由处理来设置;B44_DMARX_STAT储存了状态及网卡当前处理到的缓冲区号,这个寄存器只能由网卡来设置。

 

网卡中DMA也很重要:

在网卡初始化阶段,b44_open() -> b44_alloc_consistent()

bp->rx_buffers = kzalloc(size, gfp);  // size = B44_RX_RING_SIZE * sizeof(struct ring_info)

bp->rx_ring = ssb_dma_alloc_consistent(bp->sdev, size, &bp->rx_ring_dma, gfp);

     // size = DMA_TABLE_BYTES

rx_ringDMA映射的虚拟地址,rx_rind_dmaDMA映射的总线地址,这个地址将会写入B44_DMARX_ADDR寄存器,作为环形缓冲的基地址。

bw32(bp, B44_DMARX_ADDR, bp->rx_ring_dma + bp->dma_offset);

稍后在rx_init_rings() -> b44_alloc_rx_skb()

mapping = ssb_dma_map_single(bp->sdev, skb->data,RX_PKT_BUF_SZ,DMA_FROM_DEVICE);

rx_buffers进行DMA映射,并将映射地址存储在rx_ring

dp->addr = cpu_to_le32((u32) mapping + bp->dma_offset); // dprx_ring中一个

 

DMA的大致流程:

       不准确,但可以参考下大致意思

 

网卡读取B44_DMARX_ADDRB44_DMARX_STAT寄存器,得到下一个处理的struct dma_desc,然后根据dma_desc中的addr找到报文缓冲区,通过DMA处理器将网卡收到报文拷贝到addr地址处,这个过程CPU是不参与的。

 

        prod – 网卡[硬件]处理到的缓冲区号

prod  = br32(bp, B44_DMARX_STAT) & DMARX_STAT_CDMASK;

prod /= sizeof(struct dma_desc);

cons = bp->rx_cons;

根据上面分析,prod读取B44_DMARX_STAT寄存器,存储网卡当前处理到的缓冲区号;cons存储处理器处理到的缓冲区号。

 

while (cons != prod && budget > 0) {

处理报文当前时刻网卡接收到的所有报文,每处理一个报文cons都会加1,由于是环形缓冲,因此这里用相等,而不是大小比较。

 

struct ring_info *rp = &bp->rx_buffers[cons];

struct sk_buff *skb = rp->skb;

dma_addr_t map = rp->mapping;

skbmap保存了当关地址,下面在交换缓冲区后会用到。

 

ssb_dma_sync_single_for_cpu(bp->sdev, map,RX_PKT_BUF_SZ,DMA_FROM_DEVICE);

CPU取得rx_buffer[cons]的控制权,此时网卡不能再处理该缓冲区。

 

rh = (struct rx_header *) skb->data;

len = le16_to_cpu(rh->len);

….

len -= 4;

CPU取得控制权后,取得报文头,再从报文头取出报文长度lenlen-=4表示忽略了最后4节字的CRC,从这里可以看出,B440X网卡驱动不会检查CRC校验。而每个报文数据最前面添加了网卡的头部信息struct rx_header,这里是28字节。

 

struct sk_buff *copy_skb;

b44_recycle_rx(bp, cons, bp->rx_prod);

copy_skb = netdev_alloc_skb(bp->dev, len + 2);

copy_skb作为传送报文的中间量,在第三句为其分配了len + 2的空间(为了IP头对齐,稍后提到)b44_recycle_rx()函数很关键,它作了如下工作:

1.       将缓冲区号cons赋值给缓冲区号rx_prod

2.       rx_buffers[cons].skb = NULL

3.       将缓冲区号rx_prod控制权给网卡

简单来说,就是将cons号缓冲区交由CPU处理,而用rx_prod号缓冲区代替其给网卡使用。

                         

a.       b44_recycle_rx                          b. b44_recycle_rx

以起始状态为例,缓冲区rx_ring分配了512个,但rx_buffers仅分配了200个,此时cons = 0rx_prod = 200。执行b44_recycle_rx()后,网卡处理缓冲区变为1~200,而0号缓冲区交由CPU处理,将报文拷贝,并向上送至协议栈。注意rx_ringrx_buffer是不同的。

这样做的好处在于,不用等待CPU处理完0号缓冲区,网卡的缓冲区数保持200,而不会减少,这也是rx_pending = 200的原因所在。

 

skb_reserve(copy_skb, 2);

skb_put(copy_skb, len);

关于skb的操作自己去了解,这里skb_reserve()在报文头部保留了两个字节,我们知道链路层报头是14字节,正常IP报文会从14字节开始,这样就不是4字节对齐了,所以在头部保留2字节,使IP报文从16字节开始。

 

 

skb_copy_from_linear_data_offset(skb, RX_PKT_OFFSET,copy_skb->data, len);

skb = copy_skb;

CPU将报文从skb拷贝到copy_skb中,跳过了网卡报头的额外信息,因为这部分信息在上层协议站是没用的,所以去掉。在函数开始时说过skb是保存了cons号的地址,因为在b44_recycle_rx()cons号不再引用skb指向的空间,而仅由skb引用,这样便可以向上层传送,而不用额外复制。

 

netif_receive_skb(skb);

received++;

budget--;

next_pkt:

     bp->rx_prod = (bp->rx_prod + 1) & (B44_RX_RING_SIZE - 1);

     cons = (cons + 1) & (B44_RX_RING_SIZE - 1);

netif_receive_skb()将报文交由上层协议栈处理,这是下一节的内容,然后CPU处理下一个报文,rx_prodcons各加1,它们代表的含义开头有说明。

 

如此循环,直到cons == prod,此时网卡收到的报文都已被CPU处理,更新变量:

 

bp->rx_cons = cons;

bw32(bp, B44_DMARX_PTR, cons * sizeof(struct dma_desc));

INFO: rcu_sched self-detected stall on CPU { 0} (t=21003 jiffies g=535 c=534 q=5736) CPU: 0 PID: 1161 Comm: sh Tainted: G O 3.10.14 #11 Stack : 815d5db2 0000003c 00000000 81570000 8157b700 00000000 81507668 815d4d1c 84f4fce8 8156f4e7 8157b700 00000000 81570000 81570000 81560000 814266ec 87005edc 81029a38 00000000 00000000 81509404 83d17624 83d17624 81507668 8157b700 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 83d175b8 ... Call Trace: [<8100b110>] show_stack+0x64/0x7c [<81089cdc>] rcu_check_callbacks+0x230/0x78c [<81039c08>] update_process_times+0x40/0x80 [<8106fab8>] tick_handle_periodic+0x38/0x170 [<81070594>] tick_do_broadcast.constprop.4+0xb4/0x138 [<81070988>] tick_do_periodic_broadcast+0x3c/0x60 [<810709d8>] tick_handle_periodic_broadcast+0x2c/0xdc [<810022d4>] ra_systick_interrupt+0x24/0x34 [<81080f00>] handle_irq_event_percpu+0x78/0x288 [<81085104>] handle_percpu_irq+0x8c/0xc0 [<81080590>] generic_handle_irq+0x40/0x50 [<8100788c>] do_IRQ+0x18/0x2c [<810020d0>] plat_irq_dispatch+0x70/0x80 [<81005acc>] ret_from_irq+0x0/0x4 [<8142d244>] _raw_write_lock+0x0/0x24 [<813bdcf4>] ipv6_add_addr+0x398/0x4e0 [<813c2980>] addrconf_prefix_rcv+0x8c4/0x958 [<813d1600>] ndisc_rcv+0x8e8/0xf58 [<813d9eb0>] icmpv6_rcv+0x6cc/0xad8 [<813ba708>] ip6_input_finish+0x1b8/0x6ec [<812e317c>] __netif_receive_skb_core+0x760/0x9c0 [<812e7048>] process_backlog+0xbc/0x1bc [<812e7854>] net_rx_action+0xe8/0x1ec [<81032704>] __do_softirq+0x124/0x224 [<81032b78>] irq_exit+0x78/0x84 [<81005acc>] ret_from_irq+0x0/0x4 [<813d0840>] ndisc_send_rs+0xb0/0x10c [<813bc608>] addrconf_sendrs+0xe0/0x134 [<81123a88>] proc_sys_call_handler+0xd0/0xdc [<81123aa8>] proc_sys_write+0x14/0x20 [<810c1970>] vfs_write+0xc4/0x1d0 [<810c1e3c>] SyS_write+0x58/0x94 [<81010350>] stack_done+0x20/0x40 什么意思
最新发布
11-27
你贴出的这段日志是 **Linux 内核打印的 RCU stall 警告(RCU Stall Detection)**,属于内核级严重警告,表示系统在某个 CPU 上出现了长时间无法响应调度的情况。我们来逐段详细解析它的含义、原因和可能的解决方案。 --- ## 🚨 日志核心信息 ``` INFO: rcu_sched self-detected stall on CPU { 0} (t=21003 jiffies g=535 c=534 q=5736) ``` 这是最关键的一行。 ### 🔍 解释: | 字段 | 含义 | |------|------| | `rcu_sched` | 表示这是针对 **可被调度的任务(scheduling-capable tasks)** 的 RCU 检测机制 | | `self-detected stall on CPU { 0 }` | CPU 0 检测到自己或其他任务“卡住”了,导致 RCU 回调无法完成 | | `t=21003 jiffies` | 卡顿持续了 21003 个 tick(jiffy)。假设 HZ=100,则约 **210 秒 = 3.5 分钟!** 非常严重 | | `g=535` | 当前的 RCU grace period 编号为 535 | | `c=534` | 最近完成的 grace period 是 534 → 表明当前正在等待第 535 个结束,但迟迟未完成 | | `q=5736` | 等待处理的 RCU 回调函数数量为 5736 个 → 积压严重 | > ✅ 结论:CPU 0 上有一个或多个任务长期占用 CPU,没有让出控制权,导致内核无法完成同步操作(如模块卸载、内存回收等),触发了 RCU stall 报警。 --- ## 🧾 其他信息分析 ``` CPU: 0 PID: 1161 Comm: sh Tainted: G O 3.10.14 #11 ``` - `CPU: 0`:问题发生在 CPU 0。 - `PID: 1161`:当时运行的进程 ID 是 1161。 - `Comm: sh`:该进程命令名是 `sh` —— 一个 shell 进程。 - `Tainted: G O`: - `G`:使用了 **外部/专有模块**(可能是闭源驱动) - `O`:加载了 **out-of-tree module**(非主线内核模块) > ⚠️ 这意味着内核状态“被污染”,某些调试结果可能不可信。 - `3.10.14 #11`:内核版本为 3.10.14,编译了 11 次(自定义固件常见) --- ## 📜 Call Trace 堆栈分析 ```text Call Trace: [<8100b110>] show_stack+0x64/0x7c [<81089cdc>] rcu_check_callbacks+0x230/0x78c ... [<813d1600>] ndisc_rcv+0x8e8/0xf58 [<8139eb0>] icmpv6_rcv+0x6cc/0xad8 [<813ba708>] ip6_input_finish+0x1b8/0x6ec [<812e317c>] __netif_receive_skb_core+0x760/0x9c0 [<812e7048>] process_backlog+0xbc/0x1bc [<812e7854>] net_rx_action+0xe8/0x1ec [<81032704>] __do_softirq+0x124/0x224 [<81032b78>] irq_exit+0x78/0x84 ... [<813d0840>] ndisc_send_rs+0xb0/0x10c [<813bc608>] addrconf_sendrs+0xe0/0x134 [<81123a88>] proc_sys_call_handler+0xd0/0xdc [<81123aa8>] proc_sys_write+0x14/0x20 [<810c1970>] vfs_write+0xc4/0x1d0 [<810c1e3c>] SyS_write+0x58/0x94 [<81010350>] stack_done+0x20/0x40 ``` ### 🔍 关键路径解读: 1. 用户通过写 `/proc/sys/...` 文件修改内核参数(`proc_sys_write`) 2. 触发 IPv6 地址自动配置逻辑(`addrconf_sendrs` → `ndisc_send_rs`) 3. 发送 Router Solicitation 报文 4. 接收大量网络数据包(`__netif_receive_skb_core`, `net_rx_action`) 5. 在软中断中处理 IPv6 邻居发现协议(NDISC) 6. 最终进入 `rcu_check_callbacks` 检测到超时 📌 特别注意: - 堆栈中有 `icmpv6_rcv`, `ndisc_rcv`, `ipv6_add_addr` → 表明涉及 **IPv6 初始化或重复地址检测(DAD)** - `sh` 进程执行了 `echo xxx > /proc/sys/net/ipv6/...` 导致内核行为异常 --- ## ❓ 为什么会发生 RCU Stall? RCU(Read-Copy Update)是 Linux 中一种高性能同步机制,广泛用于链表遍历、路由表、文件系统等。 ### ✅ 正常流程: - RCU 需要每个 CPU 经历一次“上下文切换”或“自愿让出 CPU”才能标记为“已通过静默期(quiescent state)” - 所有 CPU 都经历后,grace period 完成,可以安全释放旧数据 ### ❌ 出现 Stall 的原因: > 一个任务连续运行太久,不发生调度,也不调用 `cond_resched()`,导致 RCU 认为它“卡住了”。 常见场景包括: | 原因 | 示例 | |------|------| | 1. 关中断太久 | `local_irq_disable()` 后长时间不恢复 | | 2. 循环中无调度点 | 如 `while(1) { do_something(); }` 且无 `cond_resched()` | | 3. 用户态程序触发内核密集操作 | 如你的 `sh` 脚本频繁写 `/proc/sys` 引发大量网络事件 | | 4. 驱动 bug 或硬件故障 | 尤其是 NAPI 收包循环太长 | | 5. 内核抢占被禁用 | 使用 `preempt_disable()` 后忘记开启 | --- ## 💡 结合你的日志推断:最可能的原因 > **你在 shell 脚本中写了类似如下命令:** ```bash echo 1 > /proc/sys/net/ipv6/conf/all/forwarding echo 0 > /proc/sys/net/ipv6/conf/all/accept_dad # ... 多次快速写入 ``` 这会立即触发内核重新配置 IPv6 网络栈,引发: - DAD(Duplicate Address Detection)取消 - RS(Router Solicitation)广播发送 - NDISC 收包风暴 - 软中断持续处理,CPU 占用率飙升 - 如果此时有其他内核路径禁止抢占或关中断 → 导致 RCU stall 而且你的堆栈显示 `proc_sys_write` → `addrconf_sendrs` → `ndisc_send_rs`,完全吻合! --- ## ✅ 如何解决? ### ✅ 1. 避免频繁写 `/proc/sys` 改为批量设置,并加入延迟: ```bash #!/bin/sh sysctl -w net.ipv6.conf.all.forwarding=1 sleep 1 sysctl -w net.ipv6.conf.all.accept_dad=0 sleep 1 ``` ### ✅ 2. 检查是否有死循环代码(内核模块) 如果你有加载自定义模块(比如闭源 WiFi 驱动),检查是否在中断上下文中做耗时操作。 ### ✅ 3. 增加内核配置容忍度(临时缓解) 在启动参数中添加: ```text rcu_cpu_stall_timeout=60 ``` 默认是 10 秒报错,设大一点避免误报(但不能解决问题根本)。 ### ✅ 4. 禁用不必要的 IPv6 功能(若不用) ```bash sysctl -w net.ipv6.conf.all.disable_ipv6=1 ``` 减少 NDISC 相关开销。 ### ✅ 5. 使用 `cond_resched()` 或 `schedule_timeout()` 主动让出 CPU 如果你在编写内核代码,任何长循环都要加: ```c while (doing_work) { if (need_resched()) cond_resched(); } ``` --- ## 🛠️ 如何调试这类问题? ### 方法一:查看 `/proc/interrupts` 看哪个 IRQ 占用过高(如网卡中断)。 ### 方法:使用 `ftrace` 或 `perf` 抓取 CPU 占用热点: ```bash echo function_graph > /sys/kernel/debug/tracing/current_tracer echo 1 > /sys/kernel/debug/tracing/tracing_on # 复现问题 cat /sys/kernel/debug/tracing/trace ``` ### 方法三:检查 dmesg 时间戳 看看是不是每次写某个文件就出错。 --- ## ✅ 总结 | 项目 | 内容 | |------|------| | **错误类型** | 内核 RCU 调度阻塞警告 | | **根本原因** | 某个任务(这里是 `sh` 进程触发的内核网络配置)长时间占用 CPU,未给 RCU 提供静默期 | | **直接诱因** | 修改 `/proc/sys/net/ipv6/...` 参数引发 NDISC 高频操作 | | **影响** | 可能导致系统无响应、模块无法卸载、内存泄漏 | | **解决方案** | 减少频繁 sysctl 写入、优化驱动、避免长循环 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值