1:前置知识

1.1 sk_buf:

        一个结构体(内核维护的),全称:套接字缓冲区(socket buffer)

下图是对整体的结构的理解(可能会有不准确的地方),内核维护的这个sk_buff目的是为了,应对协议上以太网帧的复杂性诞生的

        有了这个sk_buff,驱动层就只需要处理这个sk_buff的结构体就行,否则的话,就会需要驱动层去适配各种各样的协议,徒增工作量罢了。

1.2 描述符环:

        描述符环(有Tx描述符环、Rx描述符环)

关于描述符环(Tx & Rx应该差不多)

        硬件有TDBAL/TDBAH/TDLEN/TDH/TDT等寄存器

        软件有tx_next_to use/tx_next_to_clean无符号int变量

最开始是只有TDH和tx_next_to_use就行(也就是硬件拥有一个,软件拥有一个,一个用来完成指定位置任务,一个用来向指定位置发布任务)

        很多人会疑惑为什么硬件不能只有 TDH 一个指针。答案是:TDT 是硬件能够独立、自主工作的前提。如果没有 TDT,硬件根本不知道任务的终点在哪里,只能不断中断 CPU 查询 "还有没有新任务",效率会极低。TDT 是软件写给硬件的原子任务标记,明确告诉硬件:从 TDH 开始,到 TDT 之前的所有描述符,都是已经提交的有效任务。硬件只需内部对比 TDH 和 TDT 的位置,就能自主决定继续工作还是进入空闲,彻底消除了硬件对 CPU 的主动查询开销。

        同样,软件维护两个影子指针也不是多此一举。tx_next_to_use是描述符填充的临时指针,保证先完整填充 DMA 地址、包长度、控制位等所有字段,再一次性写入 TDT 提交,避免硬件读到半完成的描述符;tx_next_to_clean则解决了直接读取 TDH 的两大致命缺陷:PCIe 寄存器读取延迟是内存访问的上百倍,且 TDH 更新滞后于描述符状态回写,可能引发严重的竞态错误。软件通过检查内存中描述符的 DD 位来判断完成状态,速度更快且绝对安全。

        硬件通过 TDT 接收软件的任务边界,软件通过 tx_next_to_clean 跟踪硬件的完成状态,双方各自维护自己的指针,只在必要的时候进行一次单向同步

  • 软件 → 硬件的同步:只有当软件填充完描述符后,才会将tx_next_to_use写入 TDT,通知硬件有新任务
  • 硬件 → 软件的同步:只有当硬件处理完描述符后,才会回写 DD 位到内存,通知软件可以回收资源

这种设计彻底避免了软件和硬件之间的双向查询开销,实现了真正的无锁并行,是高性能网卡驱动能够达到线速转发的核心基础。

1.3 NAPI:

        以下链接当中的配图,提到了关于这个 napi_schedule & poll_queue,
网卡的 Ring Buffer 详解 - MauriceWei - 博客园1. 网卡处理数据包流程 网卡处理网络数据流程图: 图片来自参考链接1 上图中虚线步骤的解释: 完整流程: 2. 多 CPU 下的 Ring Buffer 处理 因为分配给 Ring Buffer 的空间是有限的,当收到的数据包速率大于单个 CPU 处理速度的时候 Ring Buffer 可能被占满https://www.cnblogs.com/mauricewei/p/10502300.html        常规逻辑是,网卡收到数据包,就会触发一次中断,当网络速度上去之后,对CPU的中断次数,也会增加非常多,造成速率的下降,因此高速率数据传输的时候,需要的便是,采用轮询这种策略,因此NAPI承担的角色是结合了中断的低延迟和轮询的高吞吐量。

  • 低负载时:使用中断驱动,响应速度快
  • 高负载时:自动切换到轮询模式,批量处理数据包,减少中断开销

1.4 网络设备注册

        前期调试我们用字符设备实现了硬件基础读写控制;

        要开发能被内核网络栈识别、可收发真实数据包的标准网卡驱动,必须使用内核原生的网络设备驱动框架

两者单从注册流程上来讲的话,几乎没有任何的区别:

  • 字符设备(misc 为例):填充miscdevice结构体 + 实现file_operations → 一句misc_register()完成注册
  • 网络设备:分配net_device + 填充net_device_ops + 设置 MAC/MTU → 一句register_netdev()完成注册
  • 对比维度 字符设备(misc) 网络设备
    核心注册函数 misc_register() register_netdev()
    核心数据结构 struct miscdevice struct net_device
    驱动操作函数集 struct file_operations struct net_device_ops
    用户空间接口 /dev/xxx 设备文件,通过open/read/write访问 无对应文件,通过socket() API 访问
    数据流向 用户空间 ↔ 驱动(直接传递) 用户空间 ↔ 内核协议栈 ↔ 驱动
    系统管理方式 设备号管理,ls /dev查看 接口索引(ifindex)管理,ip link查看

补充:关于为啥网络设备不能像字符设备|块设备那样 抽象成文件接口呢?

        先说前两种驱动:

设备类型 数据模型 为什么适合文件接口
字符设备 顺序字节流(无边界) 串口、键盘、鼠标等设备,数据就是连续的字节流,read() 读多少拿多少,完美匹配文件语义
块设备 固定大小块(随机访问) 硬盘、U 盘等存储设备,数据以 512B/4KB 块为单位,lseek() 可以随机定位,也完美匹配文件语义

 而网络设备驱动数据传输的模式相较于前两者有比较大的区别:

  • 数据模型不匹配:文件是无边界字节流,网络是有边界的独立数据包,且每个数据包都携带源 IP、目的端口、协议类型等关键元数据,这些信息无法通过纯字节流接口传递。

  • 通信模型不匹配:文件是一对一独占模型(一个进程打开后其他进程无法访问),而网络是多对多共享模型(上百个进程可同时通过同一个网卡与上万台主机通信)。

  • 协议栈分层无法实现:文件接口会迫使所有 TCP 重传、拥塞控制、IP 路由、ARP 解析等协议逻辑都在用户空间实现,无法利用内核统一的高性能协议栈,导致代码极度冗余且性能低下。

  • 网络特有功能无法支持connect()/listen()/bind()/setsockopt()等网络核心操作,无法用read()/write()/ioctl()这套文件接口优雅实现,最终会变成臃肿混乱的补丁式设计。

2:网卡部分功能实

  1. 支持descriptor ring 进入“长期运行的环管理”状态
  2. 支持网卡接入Linux的网络栈

修改之后的文件结构:

drivers/net/ethernet/intel/e1000e/
├── include/                  # 所有头文件统一放在include目录
│   ├── e1000e_hw.h          # 硬件抽象层:寄存器、描述符、硬件常量(纯数据,无函数)
│   ├── e1000e_types.h       # 通用类型层:驱动私有结构前向声明、通用宏
│   ├── e1000e_dma.h         # DMA模块公共接口
│   ├── e1000e_ring.h        # 描述符环模块公共接口
│   ├── e1000e_irq.h         # 中断模块公共接口
│   └── e1000e_netdev.h      # 网络设备模块公共接口
├── e1000e_main.c        # PCI核心:probe/remove、资源总控、驱动入口
├── e1000e_dma.c         # DMA层:DMA内存分配/释放、映射/解映射
├── e1000e_ring.c        # 描述符环层:环初始化、填充、清理、指针管理
├── e1000e_irq.c         # 中断层:中断处理、NAPI调度、中断控制
├── e1000e_netdev.c      # 网络设备层:net_device注册、netdev_ops实现
└── Makefile

2.1 中断功能的实现:

1️⃣申请中断向量

2️⃣向子系统注册中断处理函数 

跟之前的处理差不多(Linux PCIe(5)————e1000e字符设备+中断-CSDN博客)

差异化的地方在中断函数的处理中

  • 传统中断模式:所有业务逻辑都在硬中断上下文(irq_handler)中同步完成,包括读取数据包、更新描述符环、向上层提交数据等。
  • NAPI 混合模式:硬中断处理函数只做最精简的两件事:立即禁用当前网卡的接收中断(相当于是把中断给劫持了),然后调用napi_schedule()将该网卡的 NAPI 实例加入内核调度队列。后续的数据包批量处理工作,将由内核在软中断上下文(NET_RX_SOFTIRQ)中通过轮询方式完成。

原先高频词频繁地中断,会把大量的时间浪费到频繁打断CPU,CPU需要很多时间在上下文保存,轮询则是只需要一次打断上下文即可。


2.2 NAPI混合模式实现:

        NAPI(New API)是 Linux 网络栈解决高流量下中断风暴的核心机制(只是起到调度作用,具体的轮询逻辑的实现,还是在驱动中写明的)。其实本质就是将原先单一的中断模式替换成中断+轮询的混合模式(于Linux2.4.20正式引入内核)

流程图(以下是内核干的活,驱动不做这些工作)

napi_schedule() 被调用
↓
1. 将网卡的 napi_struct 加入当前 CPU 的 softnet_data 链表
2. 触发 NET_RX_SOFTIRQ 软中断
↓
内核在合适时机执行软中断处理函数 net_rx_action()
↓
3. 遍历 softnet_data 链表上的所有 NAPI 实例
4. 调用每个 NAPI 实例注册的 poll() 函数(驱动实现)
↓
poll() 函数批量处理数据包
↓
5. 若所有数据包处理完毕:调用 napi_complete()
6. 重新开启网卡接收中断 → 回到初始状态

2.3 poll()函数实现

        它的工作就是批量从描述符环中取包

        本来设备完成一个任务(完成描述符环上一个任务)之后,就需要通过中断的方式,通知驱动来验收成果(驱动完成数据接收,描述符清理),但是poll则是要求设备在完成很多任务之后,统一交由驱动验收成果。

  1. 循环读取描述符环,检查是否有网卡 DMA 完成的数据包
  2. 为每个数据包分配sk_buff,填充数据和协议信息
  3. 调用netif_receive_skb()将 sk_buff 提交给上层协议栈
  4. 回收已处理的描述符,重新填充新的空闲缓冲区给网卡
  5. 返回本次实际处理的数据包数量

在当前poll函数当中

int e1000e_poll(struct napi_struct *napi, int budget)
{
    struct e1000e_dev *e1000e = container_of(napi, struct e1000e_dev, napi);
    int work_done = 0;

    /* 先处理接收,再处理发送 */
    work_done += e1000e_ring_clean_rx(e1000e, budget);
    work_done += e1000e_ring_clean_tx(e1000e);

    /* 如果处理完所有数据包,退出NAPI并重新开启中断 */
    if (work_done < budget) {
        napi_complete_done(napi, work_done);
        e1000e_irq_enable(e1000e);
    }

    return work_done;
}

2.4 网络设备注册

        1️⃣分配网络设备:netdev = alloc_etherdev(sizeof(struct e1000e_dev *));

网络设备子系统帮我完成很多脏活,包括但不限于分配地址空间,设置设备类型,设置MAC地址长度,设置默认的发送队列长度等等。手动设置,出现一个设置错了,整体就不会正常工作。

// 内核源码简化版
struct net_device *alloc_etherdev(int sizeof_priv)
{
    // 1. 调用底层的alloc_netdev,分配net_device + 私有数据空间
    struct net_device *dev = alloc_netdev(sizeof_priv, "eth%d", NET_NAME_UNKNOWN, ether_setup);
    return dev;
}

// 最关键的是这个ether_setup函数,它做了所有以太网设备的通用初始化
void ether_setup(struct net_device *dev)
{
    dev->type = ARPHRD_ETHER;          // 设置设备类型为以太网
    dev->hard_header_len = ETH_HLEN;   // 设置以太网头部长度14字节
    dev->mtu = ETH_DATA_LEN;           // 设置默认MTU 1500字节
    dev->addr_len = ETH_ALEN;          // 设置MAC地址长度6字节
    dev->tx_queue_len = 1000;          // 设置默认发送队列长度
    dev->flags = IFF_BROADCAST | IFF_MULTICAST; // 设置默认标志

    // 设置以太网通用的头部操作函数
    dev->header_ops = &eth_header_ops;
    // 设置以太网通用的MAC地址验证函数
    dev->netdev_ops->ndo_validate_addr = eth_validate_addr;
    // 设置以太网通用的MAC地址设置函数
    dev->netdev_ops->ndo_set_mac_address = eth_mac_addr;
}

        2️⃣网络操作符确定:netdev->netdev_ops = &e1000e_netdev_ops;

这个是在自定义驱动里边我们自己去实现的

static const struct net_device_ops e1000e_netdev_ops = {
    .ndo_open       = e1000e_open,
    .ndo_stop       = e1000e_close,
    .ndo_start_xmit = e1000e_start_xmit,
    .ndo_validate_addr = eth_validate_addr,
};

        3️⃣MAC地址确定:eth_hw_addr_random(netdev);

        4️⃣一句话注册:ret = register_netdev(netdev);

这个是把之前申请到的资源,注册的东西,凑齐交给内核,内核会去真正的把这个把自定义的网络设备注册成功,标志就是ip link命令之后可以看到注册的设备


3 调试过程遇到的问题以及排查过程

3.1 busybox中ping 10.0.2.2不通

前置背景:

0️⃣当前网络环境

[Windows 物理主机]
   └─ VMware 虚拟交换机 (VMnet 模式:桥接)
        └─ [Ubuntu 虚拟机] (运行 QEMU)
             └─ QEMU 用户模式网络 (SLIRP 后端)
                  └─ [BusyBox 客户机] ( e1000e 驱动测试环境)

1️⃣能够看到这个驱动eth0是被内核识别到的

/ # ifconfig -a
eth0      Link encap:Ethernet  HWaddr BA:C7:8F:5C:09:FD  
          inet addr:10.0.2.15  Bcast:10.0.2.255  Mask:255.255.255.0
          inet6 addr: fe80::b8c7:8fff:fe5c:9fd/64 Scope:Link
          inet6 addr: fec0::b8c7:8fff:fe5c:9fd/64 Scope:Site
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback  
          LOOPBACK  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)



//下边是正常情况(Ubuntu下输入命令 看到的结果)
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.108  netmask 255.255.255.0  broadcast 192.168.1.255
        inet6 fe80::e79b:8203:c8bf:2153  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:9b:b3:5a  txqueuelen 1000  (Ethernet)
        RX packets 1130797  bytes 971431953 (971.4 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 846636  bytes 727469729 (727.4 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

2️⃣丢包率100%(并且检查了busybox的启动命令 里边是有明确的标志说是 有网关的)

/ # route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.0.2.2        0.0.0.0         UG    0      0        0 eth0
10.0.2.0        0.0.0.0         255.255.255.0   U     0      0        0 eth0
/ # ping -c 100 -W 2 10.0.2.2
PING 10.0.2.2 (10.0.2.2): 56 data bytes

--- 10.0.2.2 ping statistics ---
100 packets transmitted, 0 packets received, 100% packet loss

也就相当于应用层ping,下达了指令,驱动层按理说应该接收到这个包,并向描述符环中添加传输指令。

排查原因:

1️⃣检查中断,因为无论是开启NAPI与否,在传输完成之后,都会通过中断的形式通知到CPU,以告知软件,说硬件已经完成描述符环上任务的处理。

cat /proc/interrupts | grep e1000e

执行完ping之后发现中断次数由2变成5了

26:          5  PCI-MSI-0000:02:00.0   0-edge      e1000e

就是说内核有收到消息,但是可能这些中断应该全是由于这个guest->网关单向通信导致的,网关就没有回复guest,所以出现中断上涨,RX | TX硬件通知了CPU,但是ping整体100%丢包率。


2️⃣为坐实以上论断,尝试去查看寄存器在ping前后发生变化的部分,根据这个由硬件维护的一个寄存器可以看出来这个硬件到底有没有去工作。

DEVICE REGISTERS
____________________________________________________________
TDH                0x00000002 -> 0x00000007
TDT                0x00000002 -> 0x00000007
RDH                0x00000000 -> 0x00000004
RDT                0x000000ff -> 0x00000001


RING POINTERS
____________________________________________________________
TX_NEXT_TO_USE     2 -> 7
TX_NEXT_TO_CLEAN   1 -> 5
RX_NEXT_TO_CLEAN   0 -> 2

  TDH/TDT: 2 -> 7、TX_NEXT_TO_USE: 2 -> 7
      ping 阶段驱动一共又提交了 5 个 Tx 描述符。
      这 不等于 5 个 ICMP,里面通常会混着 ARP/重试帧。
TX_NEXT_TO_CLEAN: 1 -> 5
      驱动回收了 4 个 Tx,还有 1 个已发但还没 clean。
      这更像时序/清理滞后,不像致命错误。
 RDH: 0 -> 4
      硬件端至少前进了 4 个 Rx 描述符,说明 设备确实收到过帧并 DMA 到 Rx ring。RX_NEXT_TO_CLEAN: 0 -> 2
      但驱动只真正 clean/上交了 2 个包。

所以 感觉像是Tx ring,在正常工作

证据有:硬件寄存器按照预期发展,软件端的两个指针也是正常表现(虽然ping 3次,但是真正的下发命令却是5个描述符环,这个可能是IP协议层,或者ping命令那边夹杂的一些辅助的描述吧)

Rx ring好像有点问题

证据是:硬件管理的RDH前进了4个,这个可能就是时序上的某些问题,至少可以说明设备确实收到过帧并 DMA 到 Rx ring,但是可能没有通过中断告诉软件,让软件的RX_NEXT_TO_CLEAN: 0 -> 2,正常应该是0->4.

由于之前开启了这个NAPI模式,所以有理由怀疑是不是哪里没有设置好,导致这个没有真正的进入NAPI模式进行调度,基本的中断模式也有点问题。


3️⃣查看代码,发现在这个irq_handler中我们是把中断直接关掉了,之后就紧接着进入NAPI的调度,内核决定去执行这个poll函数,在函数中完成相对应的描述符环清理工作(tx 和 rx的描述符环都要清理),poll函数正在清理工作的时候,虚拟网关那边已经产生了应答,对应的rx描述符发起中断请求,此时的硬件的中断权限已经被禁止掉了,也就是软件那边根本没有听到硬件的中断请求,于是只能被rx的请求只能被闲置,rx不会再次发起请求。

危险的窗口

- 危险是在这一步:

- poll() 看了一眼,觉得“现在没活了”

- 然后执行 napi_complete_done() + e1000e_irq_enable():

- 如果新包刚好落在“poll() 已经决定退出,但新的唤醒又没正确续上”的窗口里,就可能出现:

- ring 里其实已经有新包

- 但这轮 poll() 已经收工

- 新中断又没有把下一轮 NAPI 正常拉起来

- 结果这个包就卡在 ring 里,直到别的事件把驱动重新唤醒

        定位到原因就进行修改,措施就是在tx的poll执行完毕之后,将要开启这个硬件的中断的时候,主动去检查一下rx描述符的有关于中断的寄存器是啥状态,如果确实是把rx的中断请求漏掉了,那就在这个时候去响应一下。

        napi done -> 开启中断

            -> 检查rx中断相关寄存器是否被置位

                    ->被置位 就在关闭中断 运行napi的调度器

                   ->没有被置位 直接poll函数返回work done

这就是修好了

PING 10.0.2.2 (10.0.2.2): 56 data bytes
64 bytes from 10.0.2.2: seq=0 ttl=255 time=1.917 ms
64 bytes from 10.0.2.2: seq=1 ttl=255 time=0.756 ms
64 bytes from 10.0.2.2: seq=2 ttl=255 time=0.442 ms
64 bytes from 10.0.2.2: seq=3 ttl=255 time=0.565 ms
64 bytes from 10.0.2.2: seq=4 ttl=255 time=0.472 ms
64 bytes from 10.0.2.2: seq=5 ttl=255 time=0.625 ms
64 bytes from 10.0.2.2: seq=6 ttl=255 time=1.148 ms
64 bytes from 10.0.2.2: seq=7 ttl=255 time=0.967 ms
64 bytes from 10.0.2.2: seq=8 ttl=255 time=0.662 ms

4️⃣补充:(这个只能算是补丁,且这个修正方式正是导致下边压测不过的原因)


3.2 长时间ping不过

测试二次均在 第二百多次的时候出现问题(且两者前后的出现问题的位置不一样)

排查原因:

1️⃣先检查一下这个watchdog:BUG soft lockup是啥情况

Linux 内核为每个 CPU 维护了一个心跳定时器和一个看门狗线程

  1. 正常情况下,CPU 会每几毫秒发生一次时钟中断,更新一个 per-CPU 的时间戳
  2. 内核的 watchdog 线程会每隔几秒检查一次所有 CPU 的时间戳
  3. 如果发现某个 CPU 的时间戳连续 20 秒(默认阈值)没有更新,就会打印这个警告

这意味着:过去的一段时间之内,内核一直是在运行这个网卡驱动,看门狗线程一直未有被线程调度器使用过,从而导致看门狗出现问题。


2️⃣触发的原因是长时间运行之后,大概率就是我们之前引入的这个poll函数中主动检查这个硬件寄存器的rx描述符环是否被调用,然后再此开启调度器,然后再去进入poll函数,再去清理ring环上的内容。再次检查是否有rx环上是否有遗漏的内容,然后再进入调度器,再次执行poll函数,由此不断的循环往复。

- 系统一直很忙,也一直在做事

- 但很多 CPU 时间花在“来回折返、重新调度、重读 cause、重复进出 softirq”

- 真正有效的排空效率不高

- 调度器越来越难插进来运行别的东西

这个也刚好能解释为啥出现看门狗警告的时间不是恒定值的现象。

尝试修正:

1️⃣把原先的补丁替换成更合适的方式:

也就是在poll退出之前,要先检查一下rx环或者tx环,到底是不是真的被清理干净了,

检查是否清理干净的方式就是

🐸RX:是看这个budget是否被花完

        没花完就说明真的没任务了

        花完的话,就说明poll处理能力是满负载运行,还需要下一波处理

🤡TX:是看软件维护的tx_next_to_use&tx_next_to_clean之间的关系,以及计算清理描述符个数和描述符环长度之间的关系

        具体验收逻辑是GPT写的,大概就是这么个意思

之前的补丁模式是破坏了这个NAPI core,本来应该通过poll的返回值(budget || work_done等标志)交给内核去决定到底要不要开启调度器。

补充:当前的修改依旧无法规避所谓的危险窗口,也就是退出poll,开启中断之前,来了rx ring的中断请求,该请求不会被响应,只能等待别的什么事情唤醒一下驱动,再次进入poll,才会再次清理描述符环的时候,顺带着把上次未响应的部分清理一下。这个模式其实是没有问题的。

        真正出问题在

按之前“手工关中断”的思路,时序是这样的:

- 包 A 到达,硬件置位 cause,发出 IRQ1,硬件自动清理cause

- CPU 进 ISR,read ICR,把 IRQ1 对应 cause 清掉

- 这时中断还没关,因为你还没写 IMC

- 就在这几条指令之间,包 B 又到了

- 硬件再次置位 cause,发出 IRQ2,硬件再次自动清理cause位

- 然后你的 ISR 才写 IMC,把中断关掉,并 schedule NAPI


问题来了:

- IRQ2 可能已经在路上了,甚至已经被 CPU 接收了

- 但这时 NAPI 往往已经是 scheduled 状态

- 第二次进 ISR 时,napi_schedule_prep() 可能失败

- 于是 IRQ2 这次“通知机会”被消费掉了,但没有换来新的 poll 轮次

- 也就是这个IRQ2没有被内核听到

(至于具体为啥没被听到,不理解)


这时如果包 B:

- 没被当前这轮 poll() 及时扫到

- 或者刚好落在 poll() 已经过了那个 desc、正准备 complete 的点

那它就会变成:

- ring 里有数据

- cause 被清理,这时候就没有任何人知道 到底真的存在过IRQ2吗?

补充:此上内容,可能会因为各种原因偶发性正确,IRQ2有时候会被响应有时候就不会,真正靠谱的还是官方说的,等到时机成熟之间再次进行IRQ请求,比较稳妥。

2️⃣问题的根音就在于,读取中断原因和屏蔽中断之间或者会有新的中断请求,所以解决办法,就是依靠硬件能力,把此时间段内所有中断屏蔽掉,等到时机合适的时候,关掉屏蔽,此时之前被屏蔽的中断就会再次发起中断请求。

所谓的硬件能力就是,依靠寄存器驱动中断同时,屏蔽所有中断,之后关闭屏蔽,期间所有被屏蔽的中断会再次发起一次请求。

- 打开 CTRL_EXT.IAME

- 配置 IAM

- 之后 一旦 ISR 读取 ICR,硬件会自动把对应中断通知 mask 掉

这样时序就变成:

- 包 A 到达,发 IRQ1

- CPU 进 ISR,read ICR

- 在这一次 ICR 读取动作里,硬件同时完成两件事

- 清掉当前 cause

- 自动屏蔽后续中断通知

于是包 B 如果这时到来,会发生什么?

- 它照样写进 RX ring

- 它照样能把新的 cause bit 记到硬件里

- 但不会在这个“半交接状态”下再冒出一个 IRQ2

3️⃣修正完成之后的结果:

64 bytes from 10.0.2.2: seq=2503 ttl=255 time=0.485 ms
64 bytes from 10.0.2.2: seq=2504 ttl=255 time=0.500 ms
64 bytes from 10.0.2.2: seq=2505 ttl=255 time=0.501 ms
64 bytes from 10.0.2.2: seq=2506 ttl=255 time=0.436 ms
64 bytes from 10.0.2.2: seq=2507 ttl=255 time=0.366 ms
64 bytes from 10.0.2.2: seq=2508 ttl=255 time=0.548 ms
64 bytes from 10.0.2.2: seq=2509 ttl=255 time=0.610 ms
64 bytes from 10.0.2.2: seq=2510 ttl=255 time=0.366 ms
64 bytes from 10.0.2.2: seq=2511 ttl=255 time=0.541 ms
64 bytes from 10.0.2.2: seq=2512 ttl=255 time=0.413 ms
64 bytes from 10.0.2.2: seq=2513 ttl=255 time=0.488 ms
64 bytes from 10.0.2.2: seq=2514 ttl=255 time=0.474 ms
64 bytes from 10.0.2.2: seq=2515 ttl=255 time=0.709 ms
64 bytes from 10.0.2.2: seq=2516 ttl=255 time=0.809 ms
64 bytes from 10.0.2.2: seq=2517 ttl=255 time=0.615 ms
64 bytes from 10.0.2.2: seq=2518 ttl=255 time=0.535 ms
64 bytes from 10.0.2.2: seq=2519 ttl=255 time=0.699 ms

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐