<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>hyckleri</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <icon>https://2xpang.cn/blog/img/avatar.jpg</icon>
  <id>https://2xpang.cn/blog/</id>
  <link href="https://2xpang.cn/blog/" rel="alternate"/>
  <link href="https://2xpang.cn/blog/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, hyckleri</rights>
  <subtitle>探索 Linux，记录嵌入式成长</subtitle>
  <title>嵌入式开发笔记</title>
  <updated>2026-07-16T08:30:00.000Z</updated>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="NAPI" scheme="https://2xpang.cn/blog/tags/NAPI/"/>
    <category term="Linux-5.10" scheme="https://2xpang.cn/blog/tags/Linux-5-10/"/>
    <category term="RK3588" scheme="https://2xpang.cn/blog/tags/RK3588/"/>
    <category term="stmmac" scheme="https://2xpang.cn/blog/tags/stmmac/"/>
    <category term="Rockchip" scheme="https://2xpang.cn/blog/tags/Rockchip/"/>
    <category term="Linux内核" scheme="https://2xpang.cn/blog/tags/Linux%E5%86%85%E6%A0%B8/"/>
    <category term="网卡驱动" scheme="https://2xpang.cn/blog/tags/%E7%BD%91%E5%8D%A1%E9%A9%B1%E5%8A%A8/"/>
    <category term="DWMAC" scheme="https://2xpang.cn/blog/tags/DWMAC/"/>
    <category term="RTL8211F" scheme="https://2xpang.cn/blog/tags/RTL8211F/"/>
    <category term="PHY" scheme="https://2xpang.cn/blog/tags/PHY/"/>
    <category term="phylink" scheme="https://2xpang.cn/blog/tags/phylink/"/>
    <category term="DMA" scheme="https://2xpang.cn/blog/tags/DMA/"/>
    <category term="设备树" scheme="https://2xpang.cn/blog/tags/%E8%AE%BE%E5%A4%87%E6%A0%91/"/>
    <content>
      <![CDATA[<p>很多网卡问题都有一种迷惑性：</p><ul><li>MDIO 能读到 PHY ID，但接口始终 <code>NO-CARRIER</code>；</li><li><code>ethtool</code> 显示 1000 Mbps、Link detected: yes，却 ping 不通；</li><li>百兆稳定，千兆一跑 iperf3 就出现 CRC error；</li><li>开机前几秒能收包，随后 RX 停止；</li><li>低流量正常，并发或大包时 TX timeout；</li><li>关闭 checksum&#x2F;TSO&#x2F;GRO 后“神奇恢复”。</li></ul><p>这些现象之所以难排，是因为我们口中的“网卡驱动”其实横跨多个软硬件层：RK3588 内部的 Synopsys DWMAC、Rockchip 的 GRF&#x2F;clock&#x2F;reset glue、外置 RTL8211F PHY、RGMII 数据总线、MDIO 管理总线、Linux stmmac、phylink&#x2F;phylib、NAPI、DMA 和网络协议栈。</p><p>如果只盯着 <code>stmmac_main.c</code> 的某一个函数，或者只盯着 PHY 的 link 状态，就很容易在错误的层次上修问题。</p><p>本文以本地 Firefly RK3588 SDK 的 Linux 5.10.198 为样本，建立一套可迁移到其他 SoC 网卡的完整理解框架：</p><ol><li>MAC、PHY、RGMII、MDIO 各自负责什么；</li><li>一个 Linux 网卡驱动到底要实现哪些能力；</li><li><code>dwmac-rk</code>、stmmac、phylink、phylib 和 <code>realtek.c</code> 如何分工；</li><li>probe、open、link、TX、RX、IRQ&#x2F;NAPI 的完整调用链；</li><li>descriptor、DMA buffer 与 skb 的所有权如何转移；</li><li>为什么 RGMII delay 是 RK3588 千兆网口最关键的板级边界；</li><li>面对“找不到 PHY”“link up 不通”“千兆不稳”时如何分层定位。</li></ol><blockquote><p>本文讨论的是 RK3588 + 外置 RTL8211F 千兆 PHY 的典型 RGMII 场景。不同 Firefly 板型可能连接 GMAC0 或 GMAC1，也可能使用其他 PHY；最终应以原理图和实际启动 DTB 为准。</p></blockquote><hr><h2 id="一、先拆掉第一个误区：网卡不是一个硬件块"><a href="#一、先拆掉第一个误区：网卡不是一个硬件块" class="headerlink" title="一、先拆掉第一个误区：网卡不是一个硬件块"></a>一、先拆掉第一个误区：网卡不是一个硬件块</h2><p><img src="/blog/img/rk3588-stmmac/rk3588-ethernet-stack.svg" alt="RK3588 与 RTL8211F 的软硬件分层"></p><h3 id="1-MAC：处理以太网帧与-DMA"><a href="#1-MAC：处理以太网帧与-DMA" class="headerlink" title="1. MAC：处理以太网帧与 DMA"></a>1. MAC：处理以太网帧与 DMA</h3><p>RK3588 集成 Synopsys DesignWare Ethernet MAC。MAC 处于数字世界，主要负责：</p><ul><li>接收和发送以太网帧；</li><li>生成、检查 FCS；</li><li>MAC 地址过滤、组播过滤；</li><li>VLAN、checksum、TSO、RSS、时间戳等硬件卸载；</li><li>管理 MAC&#x2F;MTL FIFO 和多队列；</li><li>通过 DMA descriptor 在 FIFO 与 DDR packet buffer 之间搬运数据；</li><li>产生 RX、TX completion、DMA error、LPI 等中断。</li></ul><p>Linux stmmac 的主要工作对象就是这个 MAC、MTL 和 DMA。</p><h3 id="2-PHY：处理双绞线上的物理信号"><a href="#2-PHY：处理双绞线上的物理信号" class="headerlink" title="2. PHY：处理双绞线上的物理信号"></a>2. PHY：处理双绞线上的物理信号</h3><p>RTL8211F 是独立的千兆以太网 PHY。它负责：</p><ul><li>10BASE-T、100BASE-TX、1000BASE-T 的模拟前端和物理编码；</li><li>自动协商、千兆主从训练；</li><li>速度、双工和 pause 能力协商；</li><li>均衡、回波抵消等 PHY 内部信号处理；</li><li>报告 link、speed、duplex、pause；</li><li>PHY 私有低功耗、中断和 delay 配置。</li></ul><p>PHY 不知道 <code>struct sk_buff</code>，也不管理 DMA ring。它面对的是 RGMII 数字接口和 MDI 双绞线接口。</p><h3 id="3-RGMII：MAC-与-PHY-的数据通道"><a href="#3-RGMII：MAC-与-PHY-的数据通道" class="headerlink" title="3. RGMII：MAC 与 PHY 的数据通道"></a>3. RGMII：MAC 与 PHY 的数据通道</h3><p>RGMII 承载实际帧数据。GMAC 与 RTL8211F 之间通过 TXD&#x2F;RXD、TXC&#x2F;RXC 和控制线交换数据。</p><p>千兆模式下时钟为 125 MHz，并在时钟双沿传输。正因为采样窗口很窄，RGMII 对 clock-data skew 极其敏感，这也是“百兆正常、千兆异常”的高发根因。</p><h3 id="4-MDIO-MDC：MAC-管理-PHY-的控制通道"><a href="#4-MDIO-MDC：MAC-管理-PHY-的控制通道" class="headerlink" title="4. MDIO&#x2F;MDC：MAC 管理 PHY 的控制通道"></a>4. MDIO&#x2F;MDC：MAC 管理 PHY 的控制通道</h3><p>MDIO 是另一条独立总线：</p><ul><li>MAC 内的 MDIO controller 是 bus master；</li><li>PHY 是具有地址的从设备；</li><li>Linux 通过 MDIO 读取 PHY ID、基本状态和厂商扩展寄存器。</li></ul><p>因此必须牢记两条判断：</p><blockquote><p>MDIO 能读到 PHY ID，只能证明管理通路基本正常，不能证明 RGMII 数据通路正确。</p></blockquote><blockquote><p>PHY 显示 link up，只能证明 PHY 与链路对端完成了协商，不能证明 MAC DMA 与 Linux 收发路径正确。</p></blockquote><hr><h2 id="二、Linux-为什么把它拆成五层驱动"><a href="#二、Linux-为什么把它拆成五层驱动" class="headerlink" title="二、Linux 为什么把它拆成五层驱动"></a>二、Linux 为什么把它拆成五层驱动</h2><p>RK3588 网口不是靠一个驱动文件完成，而是多层组合：</p><table><thead><tr><th>软件层</th><th>本地源码</th><th>解决的问题</th></tr></thead><tbody><tr><td>设备树</td><td><code>arch/arm64/boot/dts/rockchip/</code></td><td>板子到底怎么连</td></tr><tr><td>Rockchip glue</td><td><code>drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c</code></td><td>通用 DWMAC 如何接入 RK3588</td></tr><tr><td>stmmac core</td><td><code>drivers/net/ethernet/stmicro/stmmac/</code></td><td>MAC&#x2F;DMA 如何作为 Linux netdev 工作</td></tr><tr><td>phylink&#x2F;phylib</td><td><code>drivers/net/phy/</code></td><td>MAC 与 PHY 如何协商能力和链路状态</td></tr><tr><td>Realtek PHY</td><td><code>drivers/net/phy/realtek.c</code></td><td>RTL8211F 有哪些私有初始化和寄存器</td></tr></tbody></table><h3 id="1-Rockchip-glue-不负责逐包收发"><a href="#1-Rockchip-glue-不负责逐包收发" class="headerlink" title="1. Rockchip glue 不负责逐包收发"></a>1. Rockchip glue 不负责逐包收发</h3><p><code>dwmac-rk.c</code> 的价值在于处理 SoC 差异：</p><ul><li>GRF&#x2F;PHP_GRF 接口 mux；</li><li>GMAC0&#x2F;GMAC1 选择；</li><li>RGMII&#x2F;RMII 模式；</li><li>参考时钟输入&#x2F;输出；</li><li>10&#x2F;100&#x2F;1000 Mbps 分频；</li><li>SoC 侧 TX&#x2F;RX delay line；</li><li>clock、reset、power domain；</li><li>suspend&#x2F;resume 包装。</li></ul><p>它完成板级准备后调用通用的 <code>stmmac_dvr_probe()</code>。</p><h3 id="2-stmmac-是-MAC-数据路径主体"><a href="#2-stmmac-是-MAC-数据路径主体" class="headerlink" title="2. stmmac 是 MAC 数据路径主体"></a>2. stmmac 是 MAC 数据路径主体</h3><p>stmmac 负责：</p><ul><li>创建和注册 <code>net_device</code>；</li><li><code>ndo_open</code>、<code>ndo_stop</code>、<code>ndo_start_xmit</code>；</li><li>DMA descriptor ring；</li><li>RX buffer 与 TX DMA mapping；</li><li>IRQ 和 NAPI；</li><li>多 TX&#x2F;RX queue；</li><li>checksum、TSO、VLAN、RSS、PTP、EEE；</li><li>ethtool、统计、TX timeout、PM；</li><li>注册 MDIO bus；</li><li>通过 phylink 接收链路变化。</li></ul><h3 id="3-phylink-是-MAC-与-PHY-的合同"><a href="#3-phylink-是-MAC-与-PHY-的合同" class="headerlink" title="3. phylink 是 MAC 与 PHY 的合同"></a>3. phylink 是 MAC 与 PHY 的合同</h3><p>PHY 能发现链路协商到了 1000 Mbps 全双工，但它不知道 RK3588 应该把哪个 GRF 分频位改成 DIV1；MAC 知道怎样配置自己，却不应该依赖某一个 Realtek 型号。</p><p>phylink 负责把双方隔离并协调：</p><ul><li>MAC 支持哪些 interface 和速率；</li><li>PHY&#x2F;PCS 当前解析出了什么状态；</li><li>link up&#x2F;down 时调用哪个 MAC 回调；</li><li>speed、duplex、pause 变化如何传递。</li></ul><h3 id="4-phylib-提供-PHY-通用状态机"><a href="#4-phylib-提供-PHY-通用状态机" class="headerlink" title="4. phylib 提供 PHY 通用状态机"></a>4. phylib 提供 PHY 通用状态机</h3><p>phylib 负责：</p><ul><li>创建 <code>phy_device</code>；</li><li>读取 PHY ID 并匹配 <code>phy_driver</code>；</li><li>通用自动协商；</li><li>定时状态机或 PHY IRQ；</li><li>link&#x2F;speed&#x2F;duplex 状态更新；</li><li>suspend&#x2F;resume 和通用 Clause 22&#x2F;45 操作。</li></ul><h3 id="5-Realtek-driver-只实现芯片差异"><a href="#5-Realtek-driver-只实现芯片差异" class="headerlink" title="5. Realtek driver 只实现芯片差异"></a>5. Realtek driver 只实现芯片差异</h3><p>RTL8211F 的驱动无需重新实现整个自动协商协议。它主要补充：</p><ul><li>芯片私有初始化；</li><li>RGMII internal delay；</li><li>PHY 中断 enable&#x2F;ack；</li><li>paged register 访问；</li><li>低功耗或硬件 quirk。</li></ul><p>这种分层不是“复杂化”，而是让一个 stmmac core 可以服务许多 SoC，让同一个 Realtek PHY driver 可以连接许多 MAC。</p><hr><h2 id="三、从设备树读懂一块板的网口"><a href="#三、从设备树读懂一块板的网口" class="headerlink" title="三、从设备树读懂一块板的网口"></a>三、从设备树读懂一块板的网口</h2><h3 id="1-SoC-节点声明-DWMAC-实例"><a href="#1-SoC-节点声明-DWMAC-实例" class="headerlink" title="1. SoC 节点声明 DWMAC 实例"></a>1. SoC 节点声明 DWMAC 实例</h3><p>RK3588 的 GMAC0 定义在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">firefly_rk3588_SDK/kernel/arch/arm64/boot/dts/rockchip/rk3588.dtsi:690</span><br></pre></td></tr></table></figure><p>GMAC1 定义在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">firefly_rk3588_SDK/kernel/arch/arm64/boot/dts/rockchip/rk3588s.dtsi:5500</span><br></pre></td></tr></table></figure><p>两个节点都包含：</p><figure class="highlight dts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">compatible</span> <span class="operator">=</span> <span class="string">&quot;rockchip,rk3588-gmac&quot;</span>, <span class="string">&quot;snps,dwmac-4.20a&quot;</span><span class="punctuation">;</span></span><br></pre></td></tr></table></figure><p>这两个 compatible 分别选择：</p><ul><li>RK3588 平台 glue；</li><li>Synopsys DWMAC 4.20a 通用能力模型。</li></ul><p>SoC 节点还描述 MMIO、MAC IRQ、wake IRQ、GRF、clock、reset、power domain、TSO 和 MDIO 子节点。</p><h3 id="2-板级-DTS-决定-PHY-和-RGMII-怎么接"><a href="#2-板级-DTS-决定-PHY-和-RGMII-怎么接" class="headerlink" title="2. 板级 DTS 决定 PHY 和 RGMII 怎么接"></a>2. 板级 DTS 决定 PHY 和 RGMII 怎么接</h3><p>Firefly SDK 的 <code>rk3588-firefly-port.dtsi:179</code> 提供了一个典型配置：</p><figure class="highlight dts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">&amp;gmac1</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">phy-mode</span> <span class="operator">=</span> <span class="string">&quot;rgmii-rxid&quot;</span><span class="punctuation">;</span></span><br><span class="line">    clock_in_<span class="attr">out</span> <span class="operator">=</span> <span class="string">&quot;output&quot;</span><span class="punctuation">;</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">snps,reset-gpio</span> <span class="operator">=</span> <span class="params">&lt;<span class="variable">&amp;gpio3</span> RK_PB7 GPIO_ACTIVE_LOW&gt;</span><span class="punctuation">;</span></span><br><span class="line">    <span class="attr">snps,reset-active-low</span><span class="punctuation">;</span></span><br><span class="line">    <span class="attr">snps,reset-delays-us</span> <span class="operator">=</span> <span class="params">&lt;<span class="number">0</span> <span class="number">20000</span> <span class="number">100000</span>&gt;</span><span class="punctuation">;</span></span><br><span class="line"></span><br><span class="line">    pinctrl<span class="number">-0</span> = <span class="params">&lt;<span class="variable">&amp;gmac1_miim</span></span></span><br><span class="line"><span class="params">                 <span class="variable">&amp;gmac1_tx_bus2</span></span></span><br><span class="line"><span class="params">                 <span class="variable">&amp;gmac1_rx_bus2</span></span></span><br><span class="line"><span class="params">                 <span class="variable">&amp;gmac1_rgmii_clk</span></span></span><br><span class="line"><span class="params">                 <span class="variable">&amp;gmac1_rgmii_bus</span>&gt;</span><span class="punctuation">;</span></span><br><span class="line"></span><br><span class="line">    tx_<span class="attr">delay</span> <span class="operator">=</span> <span class="params">&lt;<span class="number">0x42</span>&gt;</span><span class="punctuation">;</span></span><br><span class="line">    <span class="attr">phy-handle</span> <span class="operator">=</span> <span class="params">&lt;<span class="variable">&amp;rgmii_phy1</span>&gt;</span><span class="punctuation">;</span></span><br><span class="line"><span class="punctuation">&#125;;</span></span><br><span class="line"></span><br><span class="line"><span class="variable">&amp;mdio1</span> <span class="punctuation">&#123;</span></span><br><span class="line"><span class="symbol">    rgmii_phy1:</span> <span class="title class_">phy@1</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">compatible</span> <span class="operator">=</span> <span class="string">&quot;ethernet-phy-ieee802.3-c22&quot;</span><span class="punctuation">;</span></span><br><span class="line">        <span class="attr">reg</span> <span class="operator">=</span> <span class="params">&lt;<span class="number">0x1</span>&gt;</span><span class="punctuation">;</span></span><br><span class="line">    <span class="punctuation">&#125;;</span></span><br><span class="line"><span class="punctuation">&#125;;</span></span><br></pre></td></tr></table></figure><p>这段 DTS 同时回答了八个问题：</p><ol><li>使用 GMAC1；</li><li>数据接口是 RGMII；</li><li>PHY 负责插入 RX internal delay；</li><li>参考时钟由 SoC 输出；</li><li>PHY reset 使用哪个 GPIO；</li><li>reset 前后延迟多长；</li><li>RK3588 SoC 侧 TX delay tap 是多少；</li><li>PHY 的 MDIO 地址是 1。</li></ol><h3 id="3-不要只看某个-dtsi"><a href="#3-不要只看某个-dtsi" class="headerlink" title="3. 不要只看某个 dtsi"></a>3. 不要只看某个 dtsi</h3><p>这个示例文件中还出现了：</p><figure class="highlight dts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">status</span> <span class="operator">=</span> <span class="string">&quot;disbaled&quot;</span><span class="punctuation">;</span></span><br></pre></td></tr></table></figure><p>对设备树来说，通常只有 <code>okay</code>&#x2F;<code>ok</code> 表示可用，其他值都等价于不可用。真实板级文件可能在后面覆盖它，也可能没有。</p><p>所以判断运行配置时，证据优先级应当是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">实际启动 DTB &gt; 最终板级 DTS 展开结果 &gt; 某个被 include 的 dtsi</span><br></pre></td></tr></table></figure><p>可以反编译实际 DTB：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">dtc -I dtb -O dts -o /tmp/running.dts /sys/firmware/fdt</span><br><span class="line">rg -n <span class="string">&#x27;ethernet@fe1b0000|ethernet@fe1c0000|phy-mode|tx_delay|rx_delay|phy@1&#x27;</span> /tmp/running.dts</span><br></pre></td></tr></table></figure><hr><h2 id="四、最危险的板级参数：RGMII-delay"><a href="#四、最危险的板级参数：RGMII-delay" class="headerlink" title="四、最危险的板级参数：RGMII delay"></a>四、最危险的板级参数：RGMII delay</h2><h3 id="1-为什么-RGMII-需要-delay"><a href="#1-为什么-RGMII-需要-delay" class="headerlink" title="1. 为什么 RGMII 需要 delay"></a>1. 为什么 RGMII 需要 delay</h3><p>RGMII 为减少引脚数量，在时钟上升沿和下降沿都发送数据。接收端需要在数据稳定窗口中间采样，因此时钟与数据之间需要大约 1.5～2 ns 的偏移。</p><p>这个偏移可以来自：</p><ul><li>PCB 走线；</li><li>PHY 内部 delay；</li><li>SoC&#x2F;GMAC 内部 delay line。</li></ul><p>问题在于三者都可能“帮忙”。如果同一方向重复加入 delay，或者没有任何一方加入，就会把采样点推到窗口边缘之外。</p><h3 id="2-phy-mode-是从-PHY-视角定义的"><a href="#2-phy-mode-是从-PHY-视角定义的" class="headerlink" title="2. phy-mode 是从 PHY 视角定义的"></a>2. <code>phy-mode</code> 是从 PHY 视角定义的</h3><table><thead><tr><th>模式</th><th align="right">PHY TX delay</th><th align="right">PHY RX delay</th></tr></thead><tbody><tr><td><code>rgmii</code></td><td align="right">无</td><td align="right">无</td></tr><tr><td><code>rgmii-rxid</code></td><td align="right">无</td><td align="right">有</td></tr><tr><td><code>rgmii-txid</code></td><td align="right">有</td><td align="right">无</td></tr><tr><td><code>rgmii-id</code></td><td align="right">有</td><td align="right">有</td></tr></tbody></table><p>这里最容易发生方向混淆：</p><ul><li>PHY TX：PHY → MAC，对应 MAC RX；</li><li>PHY RX：MAC → PHY，对应 MAC TX。</li></ul><h3 id="3-RTL8211F-会真的读取-phy-mode"><a href="#3-RTL8211F-会真的读取-phy-mode" class="headerlink" title="3. RTL8211F 会真的读取 phy-mode"></a>3. RTL8211F 会真的读取 <code>phy-mode</code></h3><p><code>rtl8211f_config_init()</code> 位于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">firefly_rk3588_SDK/kernel/drivers/net/phy/realtek.c:181</span><br></pre></td></tr></table></figure><p>函数按 <code>phydev-&gt;interface</code> 选择：</p><ul><li><code>RGMII</code>：PHY 不加 delay；</li><li><code>RGMII_RXID</code>：PHY 加 RX delay；</li><li><code>RGMII_TXID</code>：PHY 加 TX delay；</li><li><code>RGMII_ID</code>：PHY 两边都加。</li></ul><p>所以 <code>phy-mode</code> 不是一个只供 MAC 参考的标签，它会改变 RTL8211F 私有寄存器。</p><h3 id="4-RK3588-自己也能加-delay"><a href="#4-RK3588-自己也能加-delay" class="headerlink" title="4. RK3588 自己也能加 delay"></a>4. RK3588 自己也能加 delay</h3><p>RK3588 专用代码位于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c:1881-2018</span><br></pre></td></tr></table></figure><p><code>rk3588_set_to_rgmii()</code> 会：</p><ul><li>选择 GMAC0&#x2F;1 的 RGMII mux；</li><li>配置 RGMII clock mode；</li><li>打开或关闭 SoC 侧 TX&#x2F;RX delay；</li><li>写入 <code>tx_delay</code>&#x2F;<code>rx_delay</code> tap。</li></ul><p>Firefly 示例选择 <code>rgmii-rxid</code>，PHY 负责 RX delay，DTS 又给 SoC 配 <code>tx_delay = &lt;0x42&gt;</code>。这是一种明确分工，而不是“两个地方都随便填一个值”。</p><h3 id="5-RGMII-时序问题的典型症状"><a href="#5-RGMII-时序问题的典型症状" class="headerlink" title="5. RGMII 时序问题的典型症状"></a>5. RGMII 时序问题的典型症状</h3><ul><li>10&#x2F;100 Mbps 正常，1000 Mbps 不稳定；</li><li>ping 小包正常，iperf3 大流量丢包；</li><li>单向正常、反向异常；</li><li><code>ethtool -S</code> 中 CRC、alignment、receive error 增长；</li><li>不同温度、不同线缆或不同对端表现不一致。</li></ul><p>调试时不要问“delay 应该填多大”，而要先问：</p><ol><li>原理图和 PCB 由哪一侧提供 skew？</li><li>PHY internal delay 开了哪一边？</li><li>SoC internal delay 开了哪一边？</li><li>最终 DTB 的配置是否和设想一致？</li><li>经过双向压力测试后，稳定窗口在哪里？</li></ol><hr><h2 id="五、probe：把一个-DWMAC-IP-注册成-Linux-网卡"><a href="#五、probe：把一个-DWMAC-IP-注册成-Linux-网卡" class="headerlink" title="五、probe：把一个 DWMAC IP 注册成 Linux 网卡"></a>五、probe：把一个 DWMAC IP 注册成 Linux 网卡</h2><p><img src="/blog/img/rk3588-stmmac/rk3588-stmmac-probe-link.svg" alt="RK3588 stmmac probe 与 link 建立流程"></p><h3 id="1-从-compatible-进入-rk-gmac-probe"><a href="#1-从-compatible-进入-rk-gmac-probe" class="headerlink" title="1. 从 compatible 进入 rk_gmac_probe()"></a>1. 从 compatible 进入 <code>rk_gmac_probe()</code></h3><p>RK3588 match 表在 <code>dwmac-rk.c:2879-2940</code>，其中：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123; .compatible = <span class="string">&quot;rockchip,rk3588-gmac&quot;</span>, .data = &amp;rk3588_ops &#125;,</span><br></pre></td></tr></table></figure><p>platform driver 的 probe 是 <code>rk_gmac_probe()</code>，位于 <code>dwmac-rk.c:2774</code>。</p><p>它的主干是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">rk_gmac_probe()</span><br><span class="line">  → stmmac_get_platform_resources()</span><br><span class="line">  → stmmac_probe_config_dt()</span><br><span class="line">  → rk_gmac_setup()</span><br><span class="line">  → rk_gmac_clk_init()</span><br><span class="line">  → rk_gmac_powerup()</span><br><span class="line">  → stmmac_dvr_probe()</span><br></pre></td></tr></table></figure><h3 id="2-平台层准备什么"><a href="#2-平台层准备什么" class="headerlink" title="2. 平台层准备什么"></a>2. 平台层准备什么</h3><p><code>stmmac_get_platform_resources()</code> 取得：</p><ul><li>MMIO resource；</li><li><code>macirq</code>；</li><li>可选 wake&#x2F;LPI IRQ。</li></ul><p><code>stmmac_probe_config_dt()</code> 解析：</p><ul><li>MAC 地址；</li><li><code>phy-mode</code> 和 <code>phy-handle</code>；</li><li>MDIO 子节点；</li><li>FIFO&#x2F;ring&#x2F;queue 参数；</li><li>DMA burst；</li><li>TSO、RSS 等能力；</li><li>AXI&#x2F;MTL 队列配置。</li></ul><p>Rockchip 私有路径再取得 GRF&#x2F;PHP_GRF、clock、reset、power 和 delay 属性。</p><h3 id="3-rk3588-ops-是-SoC-差异接口"><a href="#3-rk3588-ops-是-SoC-差异接口" class="headerlink" title="3. rk3588_ops 是 SoC 差异接口"></a>3. <code>rk3588_ops</code> 是 SoC 差异接口</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">rk3588_ops</span><br><span class="line">  ├─ set_to_rgmii</span><br><span class="line">  ├─ set_to_rmii</span><br><span class="line">  ├─ set_rgmii_speed</span><br><span class="line">  ├─ set_rmii_speed</span><br><span class="line">  └─ set_clock_selection</span><br></pre></td></tr></table></figure><p>链路速度变化时，stmmac 会经平台 <code>fix_mac_speed</code> 回调进入 <code>rk3588_set_gmac_speed()</code>：</p><table><thead><tr><th>链路速率</th><th>RGMII clock divider</th></tr></thead><tbody><tr><td>1000 Mbps</td><td>DIV1</td></tr><tr><td>100 Mbps</td><td>DIV5</td></tr><tr><td>10 Mbps</td><td>DIV50</td></tr></tbody></table><p>PHY 协商到 1 Gbps 后，如果 SoC clock 仍停留在百兆分频，link 灯可以亮，数据仍然无法正确传输。</p><h3 id="4-stmmac-dvr-probe-创建-Linux-netdev"><a href="#4-stmmac-dvr-probe-创建-Linux-netdev" class="headerlink" title="4. stmmac_dvr_probe() 创建 Linux netdev"></a>4. <code>stmmac_dvr_probe()</code> 创建 Linux netdev</h3><p>通用 probe 位于 <code>stmmac_main.c:5061-5332</code>，主要完成：</p><ul><li><code>devm_alloc_etherdev_mqs()</code>；</li><li>初始化 <code>stmmac_priv</code> 与多队列；</li><li>识别 DWMAC 版本和硬件 feature；</li><li>创建 phylink；</li><li>注册每个 channel 的 RX&#x2F;TX NAPI；</li><li>安装 netdev ops 和 ethtool ops；</li><li><code>register_netdev()</code>。</li></ul><p>probe 成功只代表接口被内核管理。真正启动 ring、DMA 和 PHY 要等 <code>ip link set ethX up</code> 调用 <code>ndo_open</code>。</p><h3 id="5-net-device-ops-是阅读入口"><a href="#5-net-device-ops-是阅读入口" class="headerlink" title="5. net_device_ops 是阅读入口"></a>5. <code>net_device_ops</code> 是阅读入口</h3><p><code>stmmac_netdev_ops</code> 位于 <code>stmmac_main.c:4789-4822</code>，最关键的三个回调是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">ndo_open       → stmmac_open</span><br><span class="line">ndo_start_xmit → stmmac_xmit</span><br><span class="line">ndo_stop       → stmmac_release</span><br></pre></td></tr></table></figure><p>如果要系统学习任意 Linux 网卡驱动，先找到这三个入口，再找 IRQ&#x2F;NAPI 注册位置，通常就能搭起主骨架。</p><hr><h2 id="六、MDIO、PHY-ID-与-RTL8211F-驱动绑定"><a href="#六、MDIO、PHY-ID-与-RTL8211F-驱动绑定" class="headerlink" title="六、MDIO、PHY ID 与 RTL8211F 驱动绑定"></a>六、MDIO、PHY ID 与 RTL8211F 驱动绑定</h2><h3 id="1-stmmac-注册一个-mii-bus"><a href="#1-stmmac-注册一个-mii-bus" class="headerlink" title="1. stmmac 注册一个 mii_bus"></a>1. stmmac 注册一个 <code>mii_bus</code></h3><p>MDIO 实现在 <code>stmmac_mdio.c</code>：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">stmmac_mdio_read()      :222</span><br><span class="line">stmmac_mdio_write()     :289</span><br><span class="line">stmmac_mdio_reset()     :354</span><br><span class="line">stmmac_mdio_register()  :406</span><br></pre></td></tr></table></figure><p><code>stmmac_mdio_register()</code> 创建 <code>mii_bus</code>，把 read&#x2F;write&#x2F;reset 回调指向 DWMAC MDIO controller，然后由 phylib 枚举 PHY。</p><h3 id="2-DTS-reg-必须与-strap-地址一致"><a href="#2-DTS-reg-必须与-strap-地址一致" class="headerlink" title="2. DTS reg 必须与 strap 地址一致"></a>2. DTS <code>reg</code> 必须与 strap 地址一致</h3><figure class="highlight dts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">phy@1</span> <span class="punctuation">&#123;</span> <span class="attr">reg</span> <span class="operator">=</span> <span class="params">&lt;<span class="number">1</span>&gt;</span><span class="punctuation">;</span> <span class="punctuation">&#125;;</span></span><br></pre></td></tr></table></figure><p>地址通常由 PHY strap 电阻决定。DTS 写 1、硬件实际 strap 为 0 时，常见现象是：</p><ul><li>MDC 有时钟；</li><li>MDIO 读回 <code>0xffff</code> 或 <code>0x0000</code>；</li><li><code>/sys/bus/mdio_bus/devices/</code> 没有期望设备；</li><li>open 报找不到 PHY。</li></ul><h3 id="3-phylib-用真实-PHY-ID-匹配-Realtek-driver"><a href="#3-phylib-用真实-PHY-ID-匹配-Realtek-driver" class="headerlink" title="3. phylib 用真实 PHY ID 匹配 Realtek driver"></a>3. phylib 用真实 PHY ID 匹配 Realtek driver</h3><p>RTL8211F 的 PHY ID 是 <code>0x001cc916</code>。驱动表位于 <code>realtek.c:691-700</code>，安装：</p><ul><li><code>rtl8211f_config_init()</code>；</li><li><code>rtl8211f_ack_interrupt()</code>；</li><li><code>rtl8211f_config_intr()</code>；</li><li>genphy suspend 与 Realtek resume；</li><li>paged register access。</li></ul><p>板级 DTS 使用通用的：</p><figure class="highlight dts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">compatible</span> <span class="operator">=</span> <span class="string">&quot;ethernet-phy-ieee802.3-c22&quot;</span><span class="punctuation">;</span></span><br></pre></td></tr></table></figure><p>并不妨碍匹配 Realtek driver，因为最终依据是 MDIO 读出的 PHY ID。</p><h3 id="4-PHY-IRQ-和-MAC-IRQ-完全不同"><a href="#4-PHY-IRQ-和-MAC-IRQ-完全不同" class="headerlink" title="4. PHY IRQ 和 MAC IRQ 完全不同"></a>4. PHY IRQ 和 MAC IRQ 完全不同</h3><p>RTL8211F 的 link interrupt enable&#x2F;ack 位于 <code>realtek.c:93-150</code>。这个中断只用于通知 PHY 链路状态变化。</p><p>MAC&#x2F;DMA IRQ 则通知：</p><ul><li>RX descriptor 已完成；</li><li>TX completion；</li><li>DMA error；</li><li>MAC&#x2F;MTL&#x2F;LPI&#x2F;PTP 事件。</li></ul><p>把 PHY IRQ 配错，通常影响 link 变化检测；把 MAC IRQ 配错，数据路径会停止。两者不能混为一谈。</p><hr><h2 id="七、phylink：link-up-时到底谁配置谁"><a href="#七、phylink：link-up-时到底谁配置谁" class="headerlink" title="七、phylink：link up 时到底谁配置谁"></a>七、phylink：link up 时到底谁配置谁</h2><p><code>stmmac_init_phy()</code> 位于 <code>stmmac_main.c:1145</code>，优先通过：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">phylink_of_phy_connect()</span><br></pre></td></tr></table></figure><p>按 <code>phy-handle</code> 连接 PHY。如果没有标准 handle，则尝试从 MDIO bus 按地址连接。</p><p>stmmac 提供的 phylink MAC ops 位于 <code>stmmac_main.c:865-1110</code>：</p><ul><li><code>stmmac_validate()</code>：限制 MAC 支持的接口和速率；</li><li><code>stmmac_mac_config()</code>：配置接口状态；</li><li><code>stmmac_mac_link_down()</code>：关闭 MAC TX&#x2F;RX；</li><li><code>stmmac_mac_link_up()</code>：配置 speed&#x2F;duplex&#x2F;pause，并通知平台改速率。</li></ul><p>完整链路是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">网线插入</span><br><span class="line">→ RTL8211F 自动协商</span><br><span class="line">→ PHY IRQ 或 phylib delayed work</span><br><span class="line">→ 读取 link/speed/duplex/pause</span><br><span class="line">→ phylink resolve</span><br><span class="line">→ stmmac_mac_link_up()</span><br><span class="line">→ rk_fix_speed()</span><br><span class="line">→ rk3588_set_gmac_speed()</span><br><span class="line">→ MAC 按协商结果工作</span><br></pre></td></tr></table></figure><p>phylib 的 <code>phy_state_machine()</code> 位于 <code>drivers/net/phy/phy.c:1197</code>，它运行在 delayed work 上下文，可以执行 MDIO 事务；这与收包的 hardirq&#x2F;NAPI 路径是两套不同上下文。</p><hr><h2 id="八、open：什么时候-ring、IRQ-和-DMA-真正开始工作"><a href="#八、open：什么时候-ring、IRQ-和-DMA-真正开始工作" class="headerlink" title="八、open：什么时候 ring、IRQ 和 DMA 真正开始工作"></a>八、open：什么时候 ring、IRQ 和 DMA 真正开始工作</h2><p><code>stmmac_open()</code> 位于 <code>stmmac_main.c:2901</code>。将接口置为 UP 后，典型流程是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">stmmac_open()</span><br><span class="line">  ├─ stmmac_init_phy()</span><br><span class="line">  ├─ 分配 descriptor ring 和 RX buffer</span><br><span class="line">  ├─ stmmac_hw_setup()</span><br><span class="line">  ├─ request_irq()</span><br><span class="line">  ├─ napi_enable()</span><br><span class="line">  ├─ 启动 DMA RX/TX</span><br><span class="line">  ├─ phylink_start()</span><br><span class="line">  └─ netif_tx_start_all_queues()</span><br></pre></td></tr></table></figure><p>其中 <code>stmmac_hw_setup()</code> 位于 <code>stmmac_main.c:2753-2898</code>，负责把软件配置落到硬件：</p><ul><li>DMA reset 和 channel 初始化；</li><li>descriptor base&#x2F;tail；</li><li>MAC core 与 packet filter；</li><li>RX&#x2F;TX queue 和 DMA channel 映射；</li><li>MTL scheduling；</li><li>checksum、TSO、RSS、CBS；</li><li>中断合并；</li><li>PTP 与 EEE。</li></ul><p>顺序非常重要：ring 未完成前不能启动 DMA；NAPI 未 enable 前不应开放 RX 中断；关闭路径也必须按相反方向阻止新工作后再释放 DMA memory。</p><hr><h2 id="九、TX：一个-skb-如何变成网线上的帧"><a href="#九、TX：一个-skb-如何变成网线上的帧" class="headerlink" title="九、TX：一个 skb 如何变成网线上的帧"></a>九、TX：一个 skb 如何变成网线上的帧</h2><p><img src="/blog/img/rk3588-stmmac/stmmac-rx-tx-context.svg" alt="stmmac TX&#x2F;RX 与执行上下文"></p><h3 id="1-调用链"><a href="#1-调用链" class="headerlink" title="1. 调用链"></a>1. 调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">send()/sendmsg()</span><br><span class="line">→ TCP/UDP/IP</span><br><span class="line">→ qdisc</span><br><span class="line">→ dev_hard_start_xmit()</span><br><span class="line">→ stmmac_xmit()</span><br><span class="line">→ TX descriptor ring</span><br><span class="line">→ GMAC DMA 读取 DDR</span><br><span class="line">→ MAC/MTL</span><br><span class="line">→ RGMII</span><br><span class="line">→ RTL8211F</span><br><span class="line">→ 网线</span><br></pre></td></tr></table></figure><p><code>stmmac_xmit()</code> 位于 <code>stmmac_main.c:3468</code>。</p><h3 id="2-ndo-start-xmit-必须完成什么"><a href="#2-ndo-start-xmit-必须完成什么" class="headerlink" title="2. ndo_start_xmit 必须完成什么"></a>2. <code>ndo_start_xmit</code> 必须完成什么</h3><ol><li>根据 <code>skb-&gt;queue_mapping</code> 选 TX queue；</li><li>计算 linear data 和 frags 需要多少 descriptor；</li><li>检查 ring 空间，不足则停止 netdev queue；</li><li>处理 checksum、TSO、VLAN 和 timestamp metadata；</li><li>用 DMA API 映射 skb 数据；</li><li>写 descriptor 地址、长度、first&#x2F;last segment；</li><li>保存 descriptor 到 skb&#x2F;DMA mapping 的软件关系；</li><li>用内存屏障保证描述符字段先发布；</li><li>设置 OWN，把 descriptor 交给 DMA；</li><li>更新 tail pointer&#x2F;doorbell。</li></ol><h3 id="3-TX-completion-不是可选项"><a href="#3-TX-completion-不是可选项" class="headerlink" title="3. TX completion 不是可选项"></a>3. TX completion 不是可选项</h3><p>DMA 完成发送后，TX NAPI 调用 <code>stmmac_tx_clean()</code>，入口在 <code>stmmac_main.c:2087</code>：</p><ul><li>检查 descriptor 已归 CPU；</li><li>解析发送错误；</li><li><code>dma_unmap_*()</code>；</li><li>释放 skb；</li><li>更新统计；</li><li>ring 空间恢复后唤醒 queue。</li></ul><p>TX 所有权变化可以概括为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">协议栈拥有 skb</span><br><span class="line">→ ndo_start_xmit 接管 skb</span><br><span class="line">→ DMA 拥有 descriptor 和读取 buffer 的权利</span><br><span class="line">→ completion 将 descriptor 归还 CPU</span><br><span class="line">→ TX clean 解除 mapping 并释放 skb</span><br></pre></td></tr></table></figure><p>如果 <code>ndo_start_xmit</code> 返回 <code>NETDEV_TX_OK</code>，驱动已经消费 skb；如果返回 <code>NETDEV_TX_BUSY</code>，驱动不能偷偷释放或修改 skb。</p><h3 id="4-常见-TX-bug"><a href="#4-常见-TX-bug" class="headerlink" title="4. 常见 TX bug"></a>4. 常见 TX bug</h3><ul><li>descriptor 数量估算不足；</li><li>DMA map 失败后回滚不完整；</li><li>OWN 设置过早，设备读到半写 descriptor；</li><li>忘记更新 tail pointer；</li><li>completion 漏掉 unmap&#x2F;free；</li><li>queue stop 后没有 wake；</li><li>close 时 DMA 仍访问已释放 ring。</li></ul><hr><h2 id="十、RX：DMA-buffer-如何变成-skb"><a href="#十、RX：DMA-buffer-如何变成-skb" class="headerlink" title="十、RX：DMA buffer 如何变成 skb"></a>十、RX：DMA buffer 如何变成 skb</h2><h3 id="1-调用链-1"><a href="#1-调用链-1" class="headerlink" title="1. 调用链"></a>1. 调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">网线</span><br><span class="line">→ RTL8211F</span><br><span class="line">→ RGMII</span><br><span class="line">→ GMAC MAC/MTL</span><br><span class="line">→ DMA 写 RX buffer</span><br><span class="line">→ RX IRQ</span><br><span class="line">→ NAPI poll</span><br><span class="line">→ stmmac_rx()</span><br><span class="line">→ 构造 skb 与 metadata</span><br><span class="line">→ napi_gro_receive()</span><br><span class="line">→ IP/TCP/UDP/socket</span><br></pre></td></tr></table></figure><h3 id="2-RX-先有-buffer，后有数据"><a href="#2-RX-先有-buffer，后有数据" class="headerlink" title="2. RX 先有 buffer，后有数据"></a>2. RX 先有 buffer，后有数据</h3><p>驱动必须提前为 RX ring 准备 buffer：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">分配 page</span><br><span class="line">→ DMA map</span><br><span class="line">→ 把 DMA address 写进 RX descriptor</span><br><span class="line">→ 设置 OWN</span><br><span class="line">→ DMA 可向该 buffer 写包</span><br></pre></td></tr></table></figure><p>如果 refill 断掉，网口可能在启动时收几个包，耗尽 ring 后完全停止。</p><h3 id="3-stmmac-rx-的核心职责"><a href="#3-stmmac-rx-的核心职责" class="headerlink" title="3. stmmac_rx() 的核心职责"></a>3. <code>stmmac_rx()</code> 的核心职责</h3><p><code>stmmac_rx()</code> 位于 <code>stmmac_main.c:3862-4069</code>，它会：</p><ul><li>检查 descriptor ownership；</li><li>解析 first&#x2F;last segment、长度和错误；</li><li>做 DMA sync；</li><li>构造 skb linear area 和 frags；</li><li>处理 checksum、VLAN、RSS hash、timestamp；</li><li>调用 <code>eth_type_trans()</code> 设置协议和 MAC header 语义；</li><li>记录来源 RX queue；</li><li>调用 <code>napi_gro_receive()</code>；</li><li>保存跨 descriptor 或跨 budget 的半包状态；</li><li>refill 新 buffer。</li></ul><p>关键提交点在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">stmmac_main.c:4048  skb_record_rx_queue()</span><br><span class="line">stmmac_main.c:4049  napi_gro_receive()</span><br><span class="line">stmmac_main.c:4064  stmmac_rx_refill()</span><br></pre></td></tr></table></figure><p>调用 <code>napi_gro_receive()</code> 后，skb 所有权已交给 GRO&#x2F;协议栈，驱动不能继续访问或释放它。</p><h3 id="4-page-所有权比-skb-更容易出错"><a href="#4-page-所有权比-skb-更容易出错" class="headerlink" title="4. page 所有权比 skb 更容易出错"></a>4. page 所有权比 skb 更容易出错</h3><p>RX 中必须区分两种情况：</p><ul><li>数据被 copy 到 skb linear head：原 RX page 可较早回收；</li><li>page 被挂到 skb frag：page 生命周期转交 skb。</li></ul><p>如果已经挂入 frag 的 page 又立即 refill 给 DMA，设备可能覆盖协议栈仍在读取的数据，造成 use-after-free、数据随机损坏或难以复现的 checksum error。</p><hr><h2 id="十一、IRQ-与-NAPI：为什么中断里不直接把包收完"><a href="#十一、IRQ-与-NAPI：为什么中断里不直接把包收完" class="headerlink" title="十一、IRQ 与 NAPI：为什么中断里不直接把包收完"></a>十一、IRQ 与 NAPI：为什么中断里不直接把包收完</h2><p>stmmac 主 ISR 是 <code>stmmac_interrupt()</code>，位于 <code>stmmac_main.c:4254-4307</code>。它负责：</p><ul><li>检查设备是否 DOWN；</li><li>读取 MAC&#x2F;MTL 状态；</li><li>处理 LPI、PCS、安全特性等事件；</li><li>进入 DMA interrupt handler；</li><li>对活跃 channel 屏蔽 IRQ 并调度 NAPI。</li></ul><p>RX&#x2F;TX NAPI 入口分别是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">stmmac_napi_poll_rx()  stmmac_main.c:4071</span><br><span class="line">stmmac_napi_poll_tx()  stmmac_main.c:4093</span><br></pre></td></tr></table></figure><p>NAPI 的设计目标不是“减少一次函数调用”，而是避免高包速率下每个包都打断 CPU：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">第一个包触发 IRQ</span><br><span class="line">→ 驱动 mask 当前队列 IRQ</span><br><span class="line">→ NAPI 在 softirq 中批量处理多个包/completion</span><br><span class="line">→ work_done &lt; budget 时 complete</span><br><span class="line">→ 驱动重新打开 IRQ</span><br></pre></td></tr></table></figure><h3 id="执行上下文必须分清"><a href="#执行上下文必须分清" class="headerlink" title="执行上下文必须分清"></a>执行上下文必须分清</h3><table><thead><tr><th>路径</th><th>上下文</th><th align="right">能否睡眠</th></tr></thead><tbody><tr><td>probe&#x2F;open&#x2F;close&#x2F;PM</td><td>进程上下文</td><td align="right">可以</td></tr><tr><td><code>ndo_start_xmit</code></td><td>网络发送 softirq 上下文</td><td align="right">不可以</td></tr><tr><td>MAC ISR</td><td>hardirq</td><td align="right">不可以</td></tr><tr><td>NAPI poll</td><td>NET_RX softirq</td><td align="right">不可以</td></tr><tr><td>PHY state machine</td><td>workqueue</td><td align="right">可以</td></tr></tbody></table><p>因此：</p><ul><li>hardirq 中不能做长时间 MDIO；</li><li>NAPI 中不能调用可能睡眠的 clock&#x2F;regulator API；</li><li>open&#x2F;close 要和 IRQ、NAPI、DMA 并发退出同步；</li><li>IRQ&#x2F;NAPI 共享短状态时通常使用 spinlock；</li><li>PHY paged register 应走 phylib 的锁与 helper。</li></ul><hr><h2 id="十二、DMA-驱动最核心的不是寄存器，而是所有权"><a href="#十二、DMA-驱动最核心的不是寄存器，而是所有权" class="headerlink" title="十二、DMA 驱动最核心的不是寄存器，而是所有权"></a>十二、DMA 驱动最核心的不是寄存器，而是所有权</h2><p>理解网卡驱动时，建议始终追踪三套 owner：</p><ol><li>descriptor 属于 CPU 还是 DMA；</li><li>DMA buffer&#x2F;page 属于 ring、DMA、驱动还是 skb；</li><li>skb 属于协议栈、qdisc、驱动还是 GRO&#x2F;socket。</li></ol><h3 id="1-TX-所有权"><a href="#1-TX-所有权" class="headerlink" title="1. TX 所有权"></a>1. TX 所有权</h3><table><thead><tr><th>时点</th><th>skb</th><th>descriptor</th><th>mapping</th></tr></thead><tbody><tr><td>进入 xmit</td><td>驱动接管</td><td>CPU</td><td>建立中</td></tr><tr><td>发布 descriptor</td><td>驱动保留回收责任</td><td>DMA</td><td>DMA 可读</td></tr><tr><td>completion</td><td>待释放</td><td>CPU</td><td>待 unmap</td></tr><tr><td>clean 完成</td><td>已释放</td><td>free</td><td>已解除</td></tr></tbody></table><h3 id="2-RX-所有权"><a href="#2-RX-所有权" class="headerlink" title="2. RX 所有权"></a>2. RX 所有权</h3><table><thead><tr><th>时点</th><th>page&#x2F;buffer</th><th>descriptor</th><th>skb</th></tr></thead><tbody><tr><td>refill</td><td>DMA&#x2F;ring</td><td>DMA</td><td>无</td></tr><tr><td>DMA 写完</td><td>驱动</td><td>CPU</td><td>无</td></tr><tr><td>构造 skb</td><td>驱动或 skb</td><td>CPU</td><td>驱动</td></tr><tr><td>GRO 提交</td><td>skb&#x2F;协议栈</td><td>待 refill</td><td>协议栈</td></tr></tbody></table><h3 id="3-为什么需要屏障"><a href="#3-为什么需要屏障" class="headerlink" title="3. 为什么需要屏障"></a>3. 为什么需要屏障</h3><p>CPU 写 descriptor 的普通字段后，必须保证设备先看见地址和长度，再看见 OWN：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">write address/length/control</span><br><span class="line">→ dma_wmb()</span><br><span class="line">→ set OWN</span><br><span class="line">→ write tail/doorbell</span><br></pre></td></tr></table></figure><p>这不是编译器风格问题，而是 CPU、互连、cache 和 DMA 对内存观察顺序的问题。</p><h3 id="4-为什么不能绕过-DMA-API"><a href="#4-为什么不能绕过-DMA-API" class="headerlink" title="4. 为什么不能绕过 DMA API"></a>4. 为什么不能绕过 DMA API</h3><p>即使 arm64 平台常见 cache coherent，DMA API 仍负责：</p><ul><li>DMA address translation；</li><li>IOMMU；</li><li>DMA mask；</li><li>mapping 生命周期；</li><li>非一致性平台的 cache sync。</li></ul><p>把 CPU virtual address 直接写到 descriptor，是不可移植也不可靠的。</p><hr><h2 id="十三、一个网卡驱动还要照顾哪些“非收发”能力"><a href="#十三、一个网卡驱动还要照顾哪些“非收发”能力" class="headerlink" title="十三、一个网卡驱动还要照顾哪些“非收发”能力"></a>十三、一个网卡驱动还要照顾哪些“非收发”能力</h2><p>一个可用于产品的驱动不能只实现 ping：</p><ul><li>MAC 地址管理和 RX filter；</li><li>MTU 与 jumbo frame；</li><li>multicast&#x2F;promiscuous；</li><li>checksum offload；</li><li>scatter-gather、GSO&#x2F;TSO；</li><li>VLAN insert&#x2F;strip&#x2F;filter；</li><li>RSS 与多队列；</li><li>interrupt coalescing；</li><li>ethtool feature&#x2F;statistics&#x2F;ring&#x2F;coalesce；</li><li>PTP hardware timestamp；</li><li>EEE&#x2F;LPI；</li><li>WoL；</li><li>tc&#x2F;CBS；</li><li>TX timeout 和 DMA error recovery；</li><li>system&#x2F;runtime suspend&#x2F;resume。</li></ul><p>RK3588 DTS 的 DWMAC 节点声明了 <code>snps,tso</code>。如果怀疑 offload 路径，可以先用最小配置缩小范围：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ethtool -k eth0</span><br><span class="line">ethtool -K eth0 rx off tx off tso off gro off gso off</span><br></pre></td></tr></table></figure><p>如果关闭某项后恢复正常，下一步应检查 descriptor bit、skb metadata、MTU&#x2F;frags 和 feature advertisement，而不是永久关闭功能后结束分析。</p><hr><h2 id="十四、关闭与挂起：先阻止新工作，再释放资源"><a href="#十四、关闭与挂起：先阻止新工作，再释放资源" class="headerlink" title="十四、关闭与挂起：先阻止新工作，再释放资源"></a>十四、关闭与挂起：先阻止新工作，再释放资源</h2><p><code>stmmac_release()</code> 位于 <code>stmmac_main.c:3060</code>。关闭路径的核心顺序是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">停止 TX queue</span><br><span class="line">→ 停止 phylink/PHY</span><br><span class="line">→ mask/disable IRQ</span><br><span class="line">→ napi_disable() 并等待 poll 退出</span><br><span class="line">→ stop DMA TX/RX</span><br><span class="line">→ free IRQ</span><br><span class="line">→ unmap/free packet buffer 和 descriptor</span><br></pre></td></tr></table></figure><p>最危险的错误是先释放 ring，后停止 DMA。此时设备仍可能按旧 DMA address 写内存。</p><p>RK3588 的 PM 包装位于 <code>dwmac-rk.c:2848-2877</code>：</p><ul><li>不需要 WoL 时，suspend 会关闭 Rockchip GMAC 电源&#x2F;时钟；</li><li>需要 WoL 时，必须保留足够的 PHY、MAC 和唤醒 IRQ 资源；</li><li>resume 先恢复平台资源，再恢复 stmmac。</li></ul><p>WoL 经常跨越 PHY 电源、MAC wake IRQ、clock domain、pinctrl sleep state 和系统电源策略，不能只看一个 <code>ethtool -s wol g</code>。</p><hr><h2 id="十五、故障定位：先判断坏在哪一层"><a href="#十五、故障定位：先判断坏在哪一层" class="headerlink" title="十五、故障定位：先判断坏在哪一层"></a>十五、故障定位：先判断坏在哪一层</h2><h3 id="场景-1：驱动没有-probe"><a href="#场景-1：驱动没有-probe" class="headerlink" title="场景 1：驱动没有 probe"></a>场景 1：驱动没有 probe</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">dmesg | grep -Ei <span class="string">&#x27;stmmac|dwmac|gmac|ethernet&#x27;</span></span><br><span class="line"><span class="built_in">ls</span> -l /sys/bus/platform/drivers/rk_gmac-dwmac/</span><br></pre></td></tr></table></figure><p>检查：</p><ul><li>最终 DT 节点是否 <code>okay</code>；</li><li>compatible 是否正确；</li><li>clock&#x2F;reset&#x2F;power-domain；</li><li>pinctrl 冲突；</li><li>内核配置。</li></ul><p>本地 BSP <code>.config</code> 中已启用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">CONFIG_STMMAC_ETH=y</span><br><span class="line">CONFIG_STMMAC_PLATFORM=y</span><br><span class="line">CONFIG_DWMAC_ROCKCHIP=y</span><br><span class="line">CONFIG_PHYLINK=y</span><br><span class="line">CONFIG_REALTEK_PHY=y</span><br></pre></td></tr></table></figure><h3 id="场景-2：probe-成功，但找不到-PHY"><a href="#场景-2：probe-成功，但找不到-PHY" class="headerlink" title="场景 2：probe 成功，但找不到 PHY"></a>场景 2：probe 成功，但找不到 PHY</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">dmesg | grep -Ei <span class="string">&#x27;mdio|phy|no phy|attached PHY&#x27;</span></span><br><span class="line"><span class="built_in">ls</span> /sys/bus/mdio_bus/devices/</span><br></pre></td></tr></table></figure><p>检查：</p><ul><li>MDC&#x2F;MDIO pinmux；</li><li>PHY 电源；</li><li>reset GPIO 与延时；</li><li>strap 地址与 DTS <code>reg</code>；</li><li>读取 ID 是否为 <code>0x001cc916</code>。</li></ul><h3 id="场景-3：PHY-存在，但-no-link"><a href="#场景-3：PHY-存在，但-no-link" class="headerlink" title="场景 3：PHY 存在，但 no link"></a>场景 3：PHY 存在，但 no link</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ethtool eth0</span><br><span class="line"><span class="built_in">cat</span> /sys/class/net/eth0/carrier</span><br></pre></td></tr></table></figure><p>优先检查：线缆、网络变压器、PHY 模拟供电、晶振、自动协商、PHY reset 和对端能力。此时 DMA ring 通常还不是首要矛盾。</p><h3 id="场景-4：link-up，但-ping-不通"><a href="#场景-4：link-up，但-ping-不通" class="headerlink" title="场景 4：link up，但 ping 不通"></a>场景 4：link up，但 ping 不通</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">ip -s <span class="built_in">link</span> show eth0</span><br><span class="line">ethtool -S eth0</span><br><span class="line"><span class="built_in">cat</span> /proc/interrupts | grep -Ei <span class="string">&#x27;eth|gmac&#x27;</span></span><br><span class="line">tcpdump -ni eth0 -e</span><br></pre></td></tr></table></figure><p>分层判断：</p><ul><li>没有 RX IRQ：看 RGMII、pinmux、clock、MAC enable；</li><li>有 IRQ、无 RX packet：看 descriptor ownership、NAPI、RX buffer；</li><li>CRC&#x2F;error 增长：看 RGMII delay 与信号完整性；</li><li>ARP 可以、大包不行：看 MTU、checksum、TSO、DMA&#x2F;cache；</li><li>单向不通：分别检查 TX 与 RX 时序。</li></ul><h3 id="场景-5：百兆正常、千兆异常"><a href="#场景-5：百兆正常、千兆异常" class="headerlink" title="场景 5：百兆正常、千兆异常"></a>场景 5：百兆正常、千兆异常</h3><p>优先级通常是：</p><ol><li>RGMII delay 重复或缺失；</li><li>link up 后 RK3588 clock divider 没切换；</li><li>125 MHz clock；</li><li>PCB 等长和信号完整性；</li><li>PHY 供电、变压器和四对线；</li><li>对端兼容性。</li></ol><p>可短暂强制百兆做对照：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">ethtool -s eth0 speed 100 duplex full autoneg off</span><br><span class="line"><span class="comment"># 测试完成后恢复</span></span><br><span class="line">ethtool -s eth0 autoneg on</span><br></pre></td></tr></table></figure><h3 id="场景-6：高负载丢包、hang-或-TX-timeout"><a href="#场景-6：高负载丢包、hang-或-TX-timeout" class="headerlink" title="场景 6：高负载丢包、hang 或 TX timeout"></a>场景 6：高负载丢包、hang 或 TX timeout</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">ethtool -S eth0</span><br><span class="line"><span class="built_in">cat</span> /proc/net/softnet_stat</span><br><span class="line"><span class="built_in">cat</span> /proc/interrupts | grep -Ei <span class="string">&#x27;eth|gmac&#x27;</span></span><br><span class="line">mpstat -P ALL 1</span><br><span class="line">iperf3 -c &lt;peer&gt; -P 4 -t 300</span><br><span class="line">iperf3 -c &lt;peer&gt; -R -P 4 -t 300</span><br></pre></td></tr></table></figure><p>重点检查：</p><ul><li>RX missed&#x2F;overflow、buffer unavailable；</li><li>NAPI budget 和 softnet backlog；</li><li>IRQ affinity、RPS&#x2F;XPS；</li><li>ring size、coalescing；</li><li>RX refill；</li><li>TX queue stop&#x2F;wake；</li><li>DMA mapping&#x2F;IOMMU error；</li><li>温度、clock 和电源管理。</li></ul><hr><h2 id="十六、推荐的真实开发板-bring-up-路线"><a href="#十六、推荐的真实开发板-bring-up-路线" class="headerlink" title="十六、推荐的真实开发板 bring-up 路线"></a>十六、推荐的真实开发板 bring-up 路线</h2><h3 id="第一步：静态硬件闭环"><a href="#第一步：静态硬件闭环" class="headerlink" title="第一步：静态硬件闭环"></a>第一步：静态硬件闭环</h3><ul><li>原理图确认 GMAC0&#x2F;GMAC1；</li><li>PHY 型号、strap 地址、reset、IRQ、clock source；</li><li>RGMII delay 由 PCB、PHY、SoC 哪一方承担；</li><li>DTS status、pinmux、phy-mode、delay、phy-handle；</li><li>Kconfig。</li></ul><h3 id="第二步：只验证控制面"><a href="#第二步：只验证控制面" class="headerlink" title="第二步：只验证控制面"></a>第二步：只验证控制面</h3><ul><li>platform probe；</li><li>clock&#x2F;reset&#x2F;power；</li><li>MDIO 稳定读取 PHY ID；</li><li><code>ethtool</code> 能显示 PHY 能力。</li></ul><h3 id="第三步：验证链路状态机"><a href="#第三步：验证链路状态机" class="headerlink" title="第三步：验证链路状态机"></a>第三步：验证链路状态机</h3><ul><li>10&#x2F;100&#x2F;1000 自动协商；</li><li>网线插拔 carrier 变化；</li><li>speed change 时 SoC 分频变化；</li><li>PHY IRQ 或轮询稳定。</li></ul><h3 id="第四步：最小数据路径"><a href="#第四步：最小数据路径" class="headerlink" title="第四步：最小数据路径"></a>第四步：最小数据路径</h3><ul><li>ARP、ping；</li><li>临时关闭复杂 offload；</li><li>观察 RX&#x2F;TX packets、IRQ、NAPI、DMA error。</li></ul><h3 id="第五步：性能、温度和电源回归"><a href="#第五步：性能、温度和电源回归" class="headerlink" title="第五步：性能、温度和电源回归"></a>第五步：性能、温度和电源回归</h3><ul><li>单向与双向 iperf3；</li><li>小包、大包、并发流；</li><li>长时间压力；</li><li>suspend&#x2F;resume；</li><li>网线热插拔和对端速率变化；</li><li>逐项恢复 offload。</li></ul><blockquote><p>QEMU <code>virt</code> 无法真实模拟 RK3588 GRF、RGMII 时序、外置 RTL8211F、变压器和板级信号完整性。这类问题可以在 QEMU 中学习通用网络栈，却必须在真实开发板上完成最终验证。</p></blockquote><hr><h2 id="十七、源码阅读地图"><a href="#十七、源码阅读地图" class="headerlink" title="十七、源码阅读地图"></a>十七、源码阅读地图</h2><p>如果希望沿本文继续读代码，推荐按这个顺序：</p><ol><li><code>rk3588.dtsi:690</code>、<code>rk3588s.dtsi:5500</code>：SoC 资源；</li><li><code>rk3588-firefly-port.dtsi:179</code>：板级 RGMII 与 PHY；</li><li><code>dwmac-rk.c:1881-2018</code>：RK3588 interface&#x2F;clock&#x2F;delay；</li><li><code>dwmac-rk.c:2774</code>：平台 probe；</li><li><code>stmmac_main.c:5061</code>：通用 probe；</li><li><code>stmmac_main.c:4789</code>：netdev ops；</li><li><code>stmmac_main.c:2901</code>：open；</li><li><code>stmmac_main.c:3468</code>：TX；</li><li><code>stmmac_main.c:4254</code> 与 <code>:4071</code>：IRQ&#x2F;NAPI；</li><li><code>stmmac_main.c:3862</code>：RX；</li><li><code>stmmac_mdio.c:406</code>：MDIO bus；</li><li><code>stmmac_main.c:865-1110</code>：phylink MAC callbacks；</li><li><code>realtek.c:181</code>：RTL8211F internal delay；</li><li><code>drivers/net/phy/phy.c:1197</code>：PHY 状态机。</li></ol><p>把这些入口串起来后，一个 SoC 网卡驱动的全景就会从“很多不相关的文件”变成四条清晰主线：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">板级资源线：DTS → dwmac-rk → clock/reset/GRF/delay</span><br><span class="line">链路控制线：MDIO → phylib → RTL8211F → phylink → MAC link up</span><br><span class="line">发送数据线：skb → stmmac_xmit → TX DMA → TX clean</span><br><span class="line">接收数据线：RX DMA → IRQ → NAPI → stmmac_rx → GRO → 协议栈</span><br></pre></td></tr></table></figure><hr><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>RK3588 + RTL8211F 网卡驱动最值得掌握的，不是某个寄存器魔数，而是分层边界和状态闭环：</p><ul><li><strong>MAC 与 PHY 是两个硬件世界</strong>，RGMII 传数据，MDIO 做管理；</li><li><strong>dwmac-rk 解决 SoC 接入问题</strong>，stmmac 解决通用 MAC&#x2F;DMA 数据路径；</li><li><strong>phylink 协调 MAC 与 PHY</strong>，Realtek driver 只实现 RTL8211F 差异；</li><li><strong>probe 与 open 不同</strong>：前者注册设备，后者才启动 ring、IRQ、DMA 和 PHY；</li><li><strong>IRQ 只负责快速调度，NAPI 批量处理数据</strong>；</li><li><strong>TX&#x2F;RX 的核心是不丢失 descriptor、DMA mapping、page 和 skb 的所有权</strong>；</li><li><strong>RGMII delay 是板级时序设计，不是可以盲目复制的经验值</strong>；</li><li><strong>排错要先判断层次</strong>：probe、MDIO、PHY link、RGMII、DMA&#x2F;NAPI、协议栈，不能看到 link up 就跳过中间所有环节。</li></ul><p>一旦用这套模型分析问题，<code>link up but no traffic</code>、<code>100M works but 1G fails</code>、<code>RX stops after seconds</code> 这类现象就不再是随机故障，而会变成可以逐层收集证据、逐层排除的工程问题。</p>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/37748d8f4369/</id>
    <link href="https://2xpang.cn/blog/posts/37748d8f4369/"/>
    <published>2026-07-16T08:30:00.000Z</published>
    <summary>以 Firefly RK3588 BSP Linux 5.10.198 为样本，从 MAC、PHY、RGMII、MDIO 的硬件边界出发，完整走读 dwmac-rk、stmmac、phylink/phylib 与 RTL8211F 的 probe、链路协商、TX/RX、IRQ/NAPI、DMA 所有权和板级排错方法。</summary>
    <title>从一根网线到一个 skb：RK3588 STMMAC 与 RTL8211F 网卡驱动全景解析</title>
    <updated>2026-07-16T08:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="Linux" scheme="https://2xpang.cn/blog/tags/Linux/"/>
    <category term="网络子系统" scheme="https://2xpang.cn/blog/tags/%E7%BD%91%E7%BB%9C%E5%AD%90%E7%B3%BB%E7%BB%9F/"/>
    <category term="内核源码" scheme="https://2xpang.cn/blog/tags/%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81/"/>
    <category term="sk_buff" scheme="https://2xpang.cn/blog/tags/sk-buff/"/>
    <category term="Linux-5.10" scheme="https://2xpang.cn/blog/tags/Linux-5-10/"/>
    <category term="series:内核网络栈" scheme="https://2xpang.cn/blog/tags/series-%E5%86%85%E6%A0%B8%E7%BD%91%E7%BB%9C%E6%A0%88/"/>
    <category term="RK3588" scheme="https://2xpang.cn/blog/tags/RK3588/"/>
    <category term="Linux-6.1" scheme="https://2xpang.cn/blog/tags/Linux-6-1/"/>
    <category term="零拷贝" scheme="https://2xpang.cn/blog/tags/%E9%9B%B6%E6%8B%B7%E8%B4%9D/"/>
    <content>
      <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p><code>sk_buff</code> 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”，而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的<strong>控制对象和所有权载体</strong>。</p><p>从 socket truesize&#x2F;owner、MSG_ZEROCOPY、error queue、drop reason 讲到 Linux 5.10 与 6.1 的 skb 演进，并总结 RK3588 BSP 的验证边界。</p><p>本文以本地 <strong>Linux 5.10.209</strong> 源码为主线；涉及演进时对照 <strong>Linux 6.1.99</strong>，涉及网卡驱动时结合 <strong>Firefly RK3588 SDK Linux 5.10.198</strong>。本文是源码分析，不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。</p><p><img src="/blog/img/sk_buff/socket-accounting.svg" alt="Linux sk_buff 深度解析（八）：Socket 内存、零拷贝、丢包与版本演进"></p><h2 id="阅读前先建立四个问题"><a href="#阅读前先建立四个问题" class="headerlink" title="阅读前先建立四个问题"></a>阅读前先建立四个问题</h2><p>阅读任何 skb 代码路径，都建议反复追问：</p><ol><li>当前 <code>skb-&gt;data</code> 指向哪一层协议头，linear 与 non-linear 数据分别在哪里？</li><li>哪些字段刚被修改，下一层为什么依赖这些字段？</li><li>当前谁持有 skb shell、head、frag page 以及 socket&#x2F;dst&#x2F;conntrack 等外部引用？</li><li>成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb？</li></ol><h2 id="本篇核心结论"><a href="#本篇核心结论" class="headerlink" title="本篇核心结论"></a>本篇核心结论</h2><ul><li>socket 用 <code>truesize</code> 而不是 wire length 计量 skb 占用，并通过 destructor 归还记账。</li><li>MSG_ZEROCOPY 主要避免 payload copy；user page、ubuf_info、segment 和 error queue 形成额外生命周期。</li><li>6.1 的重点是 hot-path cache、page_pool recycle、multi-buffer XDP 和结构化丢包原因。</li><li>RK3588 BSP 必须区分通用 skb 语义、stmmac 实现和真实硬件能力，静态分析不能替代实机验证。</li></ul><h2 id="1-Socket-内存计量与-MSG-ZEROCOPY"><a href="#1-Socket-内存计量与-MSG-ZEROCOPY" class="headerlink" title="1. Socket 内存计量与 MSG_ZEROCOPY"></a>1. Socket 内存计量与 MSG_ZEROCOPY</h2><h3 id="结论摘要"><a href="#结论摘要" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>socket 使用 skb <code>truesize</code> 进行收发内存记账，owner helper 把 <code>sk</code> 和 destructor 绑定到 skb。<code>skb_orphan()</code> 解除 socket owner 并执行 destructor。MSG_ZEROCOPY 不等于完全不复制所有 header，而是让 TX payload 引用用户页，通过 <code>ubuf_info</code> 跟踪 pinned pages；skb clone&#x2F;segment&#x2F;completion 共同持有该引用，最终通过 error queue 通知用户可重用内存。</p><h3 id="一、接收内存"><a href="#一、接收内存" class="headerlink" title="一、接收内存"></a>一、接收内存</h3><p><code>skb_set_owner_r(skb, sk)</code> 定义于 <code>kernel-5.10/include/net/sock.h:2281</code>。典型动作：</p><ul><li>设置 <code>skb-&gt;sk</code>；</li><li>设置 destructor&#x3D;<code>sock_rfree</code>；</li><li>按 <code>skb-&gt;truesize</code> 增加 <code>sk_rmem_alloc</code>。</li></ul><p>最终释放调用 <code>sock_rfree()</code> 归还计量并唤醒&#x2F;更新 socket 状态。</p><p>为什么不用 <code>len</code>：一个 64 字节包可能占用 skb slab、head allocation、page、frag 和对齐开销，真实内存远大于 wire length。</p><h3 id="二、发送内存"><a href="#二、发送内存" class="headerlink" title="二、发送内存"></a>二、发送内存</h3><p><code>skb_set_owner_w()</code> 位于 <code>kernel-5.10/net/core/sock.c:2091</code>，通常：</p><ul><li>绑定 sk；</li><li>设置 destructor&#x3D;<code>sock_wfree</code>；</li><li>增加 <code>sk_wmem_alloc</code>；</li><li>关联 socket ref&#x2F;lifetime。</li></ul><p>TCP write&#x2F;retransmit queue 长期持有 skb，发送内存只有在 ACK&#x2F;释放相应 skb 引用后才真正归还；网卡 completion 与 TCP ACK 的生命周期层次不同。</p><h3 id="三、skb-orphan"><a href="#三、skb-orphan" class="headerlink" title="三、skb_orphan"></a>三、skb_orphan</h3><p><code>skb_orphan()</code> 位于 <code>kernel-5.10/include/linux/skbuff.h:2811</code>，用于清除 socket owner：</p><ol><li>若有 destructor，调用它归还 rmem&#x2F;wmem 或其它 owner 状态；</li><li><code>skb-&gt;sk=NULL</code>；</li><li><code>destructor=NULL</code>。</li></ol><p>常见于转发、进入设备、某些 clone&#x2F;重传或需要解除 socket lifetime 的路径。orphan 不是释放 packet data，skb 仍然存在。</p><p>错误地 orphan 太早会让 socket accounting 过早下降；太晚会让 skb 不必要地持有 socket 引用。</p><h3 id="四、MSG-ZEROCOPY-目标"><a href="#四、MSG-ZEROCOPY-目标" class="headerlink" title="四、MSG_ZEROCOPY 目标"></a>四、MSG_ZEROCOPY 目标</h3><p>用户发送大 payload 时，普通路径从用户空间复制到 kernel skb&#x2F;page。MSG_ZEROCOPY 尝试：</p><ul><li>pin 用户页；</li><li>把 page fragment 引用挂入 skb；</li><li>header 仍可在 kernel linear head 中构造；</li><li>DMA 直接读取用户页；</li><li>完成后异步通知用户。</li></ul><p>因此是 payload zerocopy，不保证系统调用、协议头、分段和所有元数据都零成本。</p><h3 id="五、ubuf-info"><a href="#五、ubuf-info" class="headerlink" title="五、ubuf_info"></a>五、ubuf_info</h3><p><code>struct ubuf_info</code> 记录：</p><ul><li>completion callback；</li><li>refcount；</li><li>zerocopy success 状态；</li><li>用户页 accounting；</li><li>ID&#x2F;range 等通知信息。</li></ul><p>skb shared info 的 <code>destructor_arg</code> 指向 <code>ubuf_info</code>，<code>tx_flags</code> 标记 <code>SKBTX_DEV_ZEROCOPY</code>&#x2F;相关状态。定义见 <code>kernel-5.10/include/linux/skbuff.h:466</code>，相关 helper 见 <code>kernel-5.10/include/linux/skbuff.h:1471-1519</code>。</p><h3 id="六、clone、GSO-与-zerocopy-ref"><a href="#六、clone、GSO-与-zerocopy-ref" class="headerlink" title="六、clone、GSO 与 zerocopy ref"></a>六、clone、GSO 与 zerocopy ref</h3><p>一个用户 buffer 可能经过：</p><ul><li>大 skb；</li><li>GSO segment 多个 skb；</li><li>retransmit clone；</li><li>设备发送副本。</li></ul><p>每个仍可能访问用户页的对象必须持有 ubuf&#x2F;page 引用。最后一个完成前不能通知用户复用内存。</p><p><code>skb_zcopy_set/clear</code>、clone&#x2F;copy&#x2F;segment 路径维护 <code>ubuf_info</code>。某个 segment fallback copy 后，可更新 overall zerocopy success，但仍需等待其它引用。</p><h3 id="七、completion-与-error-queue"><a href="#七、completion-与-error-queue" class="headerlink" title="七、completion 与 error queue"></a>七、completion 与 error queue</h3><p>TX completion 或协议确认最终调用 zerocopy callback；通知排队入口见 <code>kernel-5.10/net/core/skbuff.c:4602</code>。通知通常作为 extended error skb 放入 socket error queue：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">completion</span><br><span class="line">→ build notification skb</span><br><span class="line">→ sock_queue_err_skb()</span><br><span class="line">→ poll/recvmsg(MSG_ERRQUEUE)</span><br><span class="line">→ user receives completed ID range</span><br></pre></td></tr></table></figure><p>error queue 中“error”不一定表示发送失败，也用于 timestamp、zerocopy completion、Wi-Fi ACK 等异步状态。</p><h3 id="八、失败与-fallback"><a href="#八、失败与-fallback" class="headerlink" title="八、失败与 fallback"></a>八、失败与 fallback</h3><ul><li>pin 超过 memlock&#x2F;accounting：返回错误或 fallback copy；</li><li>设备不支持 SG：软件复制&#x2F;linearize；</li><li>加密&#x2F;协议修改要求可写：部分 copy；</li><li>DMA map 失败：释放当前 refs，通知失败；</li><li>clone&#x2F;segment 中途失败：已创建对象和 ubuf refs 必须回滚；</li><li>socket close：仍需等待&#x2F;清理 outstanding completion。</li></ul><p>用户必须在收到 completion 前保持 buffer 不修改&#x2F;不复用。</p><h3 id="九、truesize-与-zerocopy"><a href="#九、truesize-与-zerocopy" class="headerlink" title="九、truesize 与 zerocopy"></a>九、truesize 与 zerocopy</h3><p>zerocopy payload page 仍占系统资源：</p><ul><li>pinned user pages；</li><li>skb shell&#x2F;head；</li><li>page refs；</li><li>DMA mappings；</li><li>retransmit&#x2F;queue 生命周期。</li></ul><p>内存 accounting 不能因为“没有 memcpy”就忽略。不同路径可能同时使用 socket wmem、locked_vm、ubuf refs 和 device queue accounting。</p><h3 id="十、所有权表"><a href="#十、所有权表" class="headerlink" title="十、所有权表"></a>十、所有权表</h3><table><thead><tr><th>对象</th><th>owner&#x2F;ref</th><th>释放&#x2F;完成</th></tr></thead><tbody><tr><td>skb shell</td><td>users</td><td>consume&#x2F;kfree</td></tr><tr><td>kernel head</td><td>dataref</td><td>skb release data</td></tr><tr><td>user page</td><td>pin&#x2F;page ref</td><td>ubuf final completion</td></tr><tr><td>ubuf_info</td><td>callback refcount</td><td>last skb&#x2F;segment complete</td></tr><tr><td>socket wmem</td><td>destructor&#x2F;truesize</td><td>sock_wfree&#x2F;orphan</td></tr><tr><td>notification skb</td><td>error queue</td><td>user recvmsg&#x2F;free</td></tr></tbody></table><h3 id="十一、常见误区"><a href="#十一、常见误区" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li>MSG_ZEROCOPY 不代表完全没有 CPU copy 或协议头分配。</li><li>网卡 completion 后用户页不一定立即可复用，可能还有 clone&#x2F;segment&#x2F;retransmit 引用。</li><li>error queue 不只传错误。</li><li>orphan 不释放 skb 数据，只解除 owner&#x2F;destructor。</li><li>truesize 仍然重要，zerocopy 不是免费内存。</li><li>users、dataref、page pin、ubuf ref 和 socket wmem 是不同计数。</li></ol><h3 id="字段与所有权统一核对表"><a href="#字段与所有权统一核对表" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>sk/destructor/truesize</code></td><td>owner helper</td><td>destructor 归还 rmem&#x2F;wmem 和记账引用</td></tr><tr><td><code>destructor_arg/tx_flags</code></td><td>zerocopy helper</td><td>skb&#x2F;shared info 持有 ubuf_info</td></tr><tr><td>user page pin&#x2F;ref</td><td>MSG_ZEROCOPY</td><td>最后一个 completion 前用户不得复用</td></tr></tbody></table><h3 id="最小调用链"><a href="#最小调用链" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">sendmsg(MSG_ZEROCOPY)</span><br><span class="line">→ pin user pages/创建 ubuf_info</span><br><span class="line">→ skb/GSO segments 持有引用</span><br><span class="line">→ TX completion/协议释放</span><br><span class="line">→ error queue 通知可复用范围</span><br></pre></td></tr></table></figure><h2 id="2-从-kfree-skb-到结构化-drop-reason"><a href="#2-从-kfree-skb-到结构化-drop-reason" class="headerlink" title="2. 从 kfree_skb 到结构化 drop reason"></a>2. 从 kfree_skb 到结构化 drop reason</h2><p><img src="/blog/img/sk_buff/zerocopy-completion.svg" alt="从 kfree_skb 到结构化 drop reason"></p><h3 id="结论摘要-1"><a href="#结论摘要-1" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>释放 skb 不等于发生丢包：正常 TX completion、用户读取和协议消费也会释放。Linux 5.10 主要通过 <code>kfree_skb</code> tracepoint 记录释放位置，难以表达语义原因；Linux 6.1 的 <code>kfree_skb_reason()</code> 和 <code>enum skb_drop_reason</code> 让调用者携带结构化原因，tracepoint 同时记录 location、protocol 和 reason，从而区分 backlog 满、checksum、header、socket filter、route 等类别。</p><h3 id="一、正常消费与丢弃"><a href="#一、正常消费与丢弃" class="headerlink" title="一、正常消费与丢弃"></a>一、正常消费与丢弃</h3><ul><li><code>consume_skb()</code>：正常路径消费完成；</li><li><code>napi_consume_skb()</code>：NAPI&#x2F;TX completion 正常消费并优化回收；</li><li><code>kfree_skb()</code>：传统上常用于 drop&#x2F;error，但也存在历史调用不够严格的情况。</li></ul><p>内存结果可能相同，观测语义不同。分析 drop 不能只统计所有 skb free。</p><h3 id="二、Linux-5-10-trace-kfree-skb"><a href="#二、Linux-5-10-trace-kfree-skb" class="headerlink" title="二、Linux 5.10 trace_kfree_skb"></a>二、Linux 5.10 trace_kfree_skb</h3><p><code>kfree_skb()</code> 位于 <code>kernel-5.10/net/core/skbuff.c:705</code>，在真正释放前调用：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">trace_kfree_skb(skb, __builtin_return_address(<span class="number">0</span>));</span><br></pre></td></tr></table></figure><p>tracepoint 定义于 <code>kernel-5.10/include/trace/events/skb.h</code>。可看到：</p><ul><li>skb 地址；</li><li>location&#x2F;调用地址；</li><li>protocol 等有限字段。</li></ul><p>主要局限：</p><ul><li>location 需要符号化；</li><li>同一 helper 可能因多个条件 drop；</li><li>wrapper 后 location 不够接近根因；</li><li>没有统一 reason taxonomy；</li><li>正常&#x2F;异常历史调用混杂。</li></ul><h3 id="三、Linux-6-1-drop-reason"><a href="#三、Linux-6-1-drop-reason" class="headerlink" title="三、Linux 6.1 drop reason"></a>三、Linux 6.1 drop reason</h3><p><code>enum skb_drop_reason</code> 定义于 <code>kernel-6.1/include/net/dropreason.h:78</code>。<code>kfree_skb_reason()</code> 位于 <code>kernel-6.1/net/core/skbuff.c:885</code>。</p><p>调用者：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kfree_skb_reason(skb, SKB_DROP_REASON_...);</span><br></pre></td></tr></table></figure><p>tracepoint 记录 reason。普通 <code>kfree_skb()</code> 可映射为 <code>NOT_SPECIFIED</code>，便于逐步迁移旧调用点。</p><h3 id="四、reason-分类"><a href="#四、reason-分类" class="headerlink" title="四、reason 分类"></a>四、reason 分类</h3><p>枚举覆盖的类型随版本演进，典型类别：</p><ul><li>NOT_SPECIFIED；</li><li>NO_SOCKET &#x2F; SOCKET_FILTER；</li><li>PKT_TOO_SMALL &#x2F; TCP_CSUM &#x2F; UDP_CSUM；</li><li>NETFILTER_DROP；</li><li>IP_INHDR &#x2F; IPV6_BAD_EXTHDR；</li><li>NO_ROUTE；</li><li>CPU_BACKLOG；</li><li>FULL_RING &#x2F; QDISC_DROP；</li><li>XFRM_POLICY；</li><li>PROTO_MEM；</li><li>TCP_* 状态原因。</li></ul><p>reason 应描述最接近根因的逻辑，而不是简单写“调用了 kfree”。</p><h3 id="五、reason-传递"><a href="#五、reason-传递" class="headerlink" title="五、reason 传递"></a>五、reason 传递</h3><p>深层 helper 发现错误时可能：</p><ul><li>返回 enum reason 给上层；</li><li>通过输出参数设置 reason；</li><li>上层统一释放并 trace；</li><li>skb list&#x2F;segment 释放对每个成员传播同一 reason。</li></ul><p>好处是把“判断原因”和“真正释放地点”分开后仍保留根因。</p><h3 id="六、CPU-backlog-示例"><a href="#六、CPU-backlog-示例" class="headerlink" title="六、CPU backlog 示例"></a>六、CPU backlog 示例</h3><p>RPS enqueue 时目标 CPU 队列达到上限，5.10 通常只能从 <code>enqueue_to_backlog()</code> 的 kfree location 推断。6.1 可标记 <code>SKB_DROP_REASON_CPU_BACKLOG</code>，更直接区分：</p><ul><li>驱动 ring drop；</li><li>qdisc drop；</li><li>socket rcvbuf drop；</li><li>softnet backlog drop。</li></ul><h3 id="七、协议与-reason"><a href="#七、协议与-reason" class="headerlink" title="七、协议与 reason"></a>七、协议与 reason</h3><p>tracepoint 还可记录 <code>skb-&gt;protocol</code>，但 protocol 字段在不同层可能：</p><ul><li>尚未设置；</li><li>为 outer protocol；</li><li>VLAN&#x2F;隧道后变化；</li><li>非 Ethernet skb。</li></ul><p>reason 与 location 仍需结合调用链、设备和协议字段解释，不能单独作为完整诊断。</p><h3 id="八、释放链与-trace-时机"><a href="#八、释放链与-trace-时机" class="headerlink" title="八、释放链与 trace 时机"></a>八、释放链与 trace 时机</h3><p>reason trace 在 users 归零并即将释放时触发。若 skb 仍有 users，当前调用只减引用，不一定产生最终 free trace。clone 共享 data 的最后一个 shell&#x2F;dataref 释放时机也不同。</p><p>因此 trace 数量不等于收到包数或 drop packet 数，需要理解 clone&#x2F;list&#x2F;segment。</p><h3 id="九、设计价值"><a href="#九、设计价值" class="headerlink" title="九、设计价值"></a>九、设计价值</h3><ul><li>统一 drop taxonomy；</li><li>eBPF&#x2F;perf&#x2F;drop monitor 可按 reason 聚合；</li><li>减少依赖调用地址和版本行号；</li><li>支持协议逐步细化；</li><li>让生产环境统计更可读；</li><li>区分资源压力、策略、安全校验和协议错误。</li></ul><h3 id="十、常见误区"><a href="#十、常见误区" class="headerlink" title="十、常见误区"></a>十、常见误区</h3><ol><li>所有 <code>kfree_skb</code> 都算丢包是错误的。</li><li>reason 不是 errno，二者服务不同层次。</li><li><code>NOT_SPECIFIED</code> 不代表正常，只表示调用点尚未细化。</li><li>clone 的 free event 不能简单按一个 event&#x3D;一个 wire packet。</li><li>reason 仍需结合 location&#x2F;dev&#x2F;protocol&#x2F;调用链。</li><li>5.10 没有 reason 枚举时仍可用 location 和协议统计，但准确性较弱。</li></ol><h3 id="字段与所有权统一核对表-1"><a href="#字段与所有权统一核对表-1" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td>skb pointer</td><td>free&#x2F;consume&#x2F;drop helper</td><td>调用后原持有者不得再访问</td></tr><tr><td>drop reason</td><td><code>kfree_skb_reason()</code></td><td>结构化原因随 tracepoint 暴露</td></tr><tr><td>location&#x2F;protocol</td><td>旧式 tracepoint</td><td>只能辅助定位，不能替代原因语义</td></tr></tbody></table><h2 id="3-Linux-5-10-到-6-1-的关键演进"><a href="#3-Linux-5-10-到-6-1-的关键演进" class="headerlink" title="3. Linux 5.10 到 6.1 的关键演进"></a>3. Linux 5.10 到 6.1 的关键演进</h2><h3 id="结论摘要-2"><a href="#结论摘要-2" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>5.10 到 6.1 的 skb 演进重点不是四指针模型改变，而是高性能内存回收、多 buffer XDP、GRO 分桶和丢包可观测性加强。6.1 用 NAPI skb cache bulk 操作降低 shell slab 开销，用 <code>pp_recycle</code> 把 page_pool 回收信息带入 skb free，用 multi-buffer XDP 对接 shared info，用结构化 drop reason 替代仅靠 location 的诊断。</p><h3 id="一、基础模型保持稳定"><a href="#一、基础模型保持稳定" class="headerlink" title="一、基础模型保持稳定"></a>一、基础模型保持稳定</h3><p>两版仍保持：</p><ul><li>skb shell 与 head 分离；</li><li><code>head/data/tail/end</code>；</li><li>shared info 位于 end；</li><li><code>len/data_len/truesize</code>；</li><li>frags&#x2F;frag_list；</li><li>users&#x2F;dataref&#x2F;page ref；</li><li>clone&#x2F;copy&#x2F;COW&#x2F;free 分层。</li></ul><p>因此 5.10 是理解核心机制的可靠基线。</p><h3 id="二、NAPI-skb-shell-cache"><a href="#二、NAPI-skb-shell-cache" class="headerlink" title="二、NAPI skb shell cache"></a>二、NAPI skb shell cache</h3><p>6.1：</p><ul><li><code>napi_skb_cache_get()</code>：<code>kernel-6.1/net/core/skbuff.c:252</code>；</li><li><code>napi_skb_cache_put()</code>：<code>kernel-6.1/net/core/skbuff.c:1054</code>。</li></ul><p>per-CPU cache 配合 <code>kmem_cache_alloc_bulk/free_bulk</code>：</p><ul><li>一次从 slab 获取&#x2F;归还多个 skb shell；</li><li>NAPI hot path 从本地数组 pop&#x2F;push；</li><li>降低 allocator lock、freelist 和 cacheline 成本；</li><li>适合高 PPS 小包。</li></ul><p>5.10 已有 NAPI 分配&#x2F;延迟回收思想，但 6.1 的 bulk cache 路径更系统。</p><h3 id="三、pp-recycle-与-page-pool"><a href="#三、pp-recycle-与-page-pool" class="headerlink" title="三、pp_recycle 与 page_pool"></a>三、pp_recycle 与 page_pool</h3><p>6.1 <code>struct sk_buff</code> 增加&#x2F;使用：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pp_recycle:<span class="number">1</span></span><br></pre></td></tr></table></figure><p>位置：<code>kernel-6.1/include/linux/skbuff.h:926</code>。</p><p>释放 head&#x2F;frag 时：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">pp_recycle set</span><br><span class="line">→ page_pool_return_skb_page(page)</span><br><span class="line">  ├─ page 属于合适 page_pool：直接回池</span><br><span class="line">  └─ 不能回池：普通 put_page/free</span><br></pre></td></tr></table></figure><p>关键位置：</p><ul><li>head recycle：<code>kernel-6.1/net/core/skbuff.c:758</code>；</li><li>frag unref：<code>kernel-6.1/net/core/skbuff.c:785</code>；</li><li>helper：<code>kernel-6.1/include/linux/skbuff.h:5104</code>；</li><li>page_pool API：<code>kernel-6.1/include/net/page_pool.h:242</code>。</li></ul><p>5.10 驱动更常显式区分 copy page 直接 recycle 与 frag page 通用释放，跨 clone&#x2F;协议栈自动回池能力较弱、驱动耦合更高。</p><h3 id="四、clone-与-recycle-风险"><a href="#四、clone-与-recycle-风险" class="headerlink" title="四、clone 与 recycle 风险"></a>四、clone 与 recycle 风险</h3><p>clone 会复制 <code>pp_recycle</code>，但共享 page&#x2F;dataref 带来竞态：不能让某个 clone 过早把 page 回池。6.1 在 clone&#x2F;copy&#x2F;expand 路径检查 recycle 状态与共享条件，例如 <code>kernel-6.1/net/core/skbuff.c:793-802</code>、<code>kernel-6.1/net/core/skbuff.c:5459-5463</code>。</p><p>这说明 recycle flag 不是“free 时无条件放回 pool”，必须与 page ownership&#x2F;refcount 协同。</p><h3 id="五、XDP-multi-buffer"><a href="#五、XDP-multi-buffer" class="headerlink" title="五、XDP multi-buffer"></a>五、XDP multi-buffer</h3><p>6.1：</p><ul><li><code>xdp_buff_has_frags()</code>：<code>include/net/xdp.h:88</code>；</li><li><code>xdp_update_skb_shared_info()</code>：<code>include/net/xdp.h:223</code>。</li></ul><p>XDP packet 可由主 buffer + frags 组成。PASS 转 skb 时把 fragment descriptors 写入 shared info，更新 len&#x2F;data_len&#x2F;truesize。5.10 native XDP 设计主要围绕单 buffer，jumbo&#x2F;multi-descriptor 支持受限。</p><h3 id="六、GRO-重构与-hash-bucket"><a href="#六、GRO-重构与-hash-bucket" class="headerlink" title="六、GRO 重构与 hash bucket"></a>六、GRO 重构与 hash bucket</h3><p>5.10 GRO 主要在 <code>net/core/dev.c</code>，一个 NAPI 上的候选 list 比较成本随活跃 flow 增长。</p><p>6.1：</p><ul><li>实现拆到 <code>net/core/gro.c</code>；</li><li><code>GRO_HASH_BUCKETS=8</code>：<code>include/linux/netdevice.h:339</code>；</li><li><code>gro_hash[]</code>：<code>include/linux/netdevice.h:362</code>；</li><li><code>dev_gro_receive()</code>：<code>net/core/gro.c:483</code>。</li></ul><p>先按 skb hash 分桶，再进行协议级 same-flow 比较，减少多 flow 场景线性扫描。</p><h3 id="七、drop-reason"><a href="#七、drop-reason" class="headerlink" title="七、drop reason"></a>七、drop reason</h3><p>5.10 <code>trace_kfree_skb</code> 主要记录 location。6.1：</p><ul><li><code>enum skb_drop_reason</code>；</li><li><code>kfree_skb_reason()</code>；</li><li>tracepoint reason；</li><li>协议函数逐步返回具体 reason。</li></ul><p>对 backlog、checksum、header、socket、policy 等根因可结构化统计。</p><h3 id="八、skb-extensions"><a href="#八、skb-extensions" class="headerlink" title="八、skb extensions"></a>八、skb extensions</h3><p>6.1 extension ID&#x2F;类型继续增加，例如 <code>SKB_EXT_MCTP</code>：</p><ul><li>enum：<code>kernel-6.1/include/linux/skbuff.h:4626</code>；</li><li>size table：<code>kernel-6.1/net/core/skbuff.c:4512</code>。</li></ul><p>extension 让低频&#x2F;可选 metadata 不必永久膨胀核心 <code>struct sk_buff</code>，代价是独立分配&#x2F;引用和 clone&#x2F;free 管理。</p><h3 id="九、API-代码位置变化"><a href="#九、API-代码位置变化" class="headerlink" title="九、API&#x2F;代码位置变化"></a>九、API&#x2F;代码位置变化</h3><p>阅读跨版本代码时需要注意：</p><ul><li>GRO 函数从 dev.c 移到 gro.c；</li><li>API 可能增加 reason&#x2F;recycle 参数；</li><li>inline helper 行号和字段布局变化；</li><li>驱动 backport 可能只有部分新机制；</li><li>同名函数实现细节不一定相同。</li></ul><p>不能只用文件名 diff 判断功能有无，应追踪 symbol、config 和调用点。</p><h3 id="十、对驱动开发的影响"><a href="#十、对驱动开发的影响" class="headerlink" title="十、对驱动开发的影响"></a>十、对驱动开发的影响</h3><table><thead><tr><th>主题</th><th>5.10 常见责任</th><th>6.1 改进</th></tr></thead><tbody><tr><td>skb shell</td><td>普通&#x2F;NAPI alloc&#x2F;free</td><td>bulk per-CPU cache</td></tr><tr><td>RX page</td><td>驱动显式 recycle 分支</td><td>skb free 可 pp_recycle</td></tr><tr><td>jumbo XDP</td><td>单 buffer 限制明显</td><td>multi-buffer metadata</td></tr><tr><td>GRO flow list</td><td>线性候选较多</td><td>hash buckets</td></tr><tr><td>drop</td><td>location 推断</td><td>structured reason</td></tr><tr><td>optional metadata</td><td>extensions 较少</td><td>extension 类型扩展</td></tr></tbody></table><h3 id="十一、常见误区-1"><a href="#十一、常见误区-1" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li>6.1 并没有推翻 skb 基础模型。</li><li><code>pp_recycle</code> 不等于跳过 page ref 安全。</li><li>multi-buffer XDP 需要驱动和 BPF&#x2F;redirect 全链支持，不只是 helper 存在。</li><li>GRO 移文件不只是代码整理，还伴随数据结构&#x2F;分桶演进。</li><li>drop reason 的覆盖是渐进式，仍会有 NOT_SPECIFIED。</li><li>BSP 5.10 可能 backport 部分 6.x 特性，必须检查本地源码而非仅看版本号。</li></ol><h3 id="字段与所有权统一核对表-2"><a href="#字段与所有权统一核对表-2" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>pp_recycle</code></td><td>page_pool skb 路径</td><td>free 时可归还 page_pool</td></tr><tr><td>NAPI skb cache</td><td>alloc&#x2F;free hot path</td><td>per-CPU cache 批量管理 skb shell</td></tr><tr><td>drop reason&#x2F;XDP frags</td><td>6.1 新路径</td><td>增强可观测性和 multi-buffer 表达</td></tr></tbody></table><h3 id="最小调用链-1"><a href="#最小调用链-1" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">5.10 基线 skb alloc/build/free</span><br><span class="line">→ 6.1 NAPI cache/page_pool recycle/XDP frags/drop reason 路径</span><br><span class="line">→ 对具体 backport/BSP 做符号级核对</span><br></pre></td></tr></table></figure><h2 id="4-RK3588-BSP-中的-skb-生命周期"><a href="#4-RK3588-BSP-中的-skb-生命周期" class="headerlink" title="4. RK3588 BSP 中的 skb 生命周期"></a>4. RK3588 BSP 中的 skb 生命周期</h2><p><img src="/blog/img/sk_buff/version-evolution.svg" alt="RK3588 BSP 中的 skb 生命周期"></p><h3 id="结论摘要-3"><a href="#结论摘要-3" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>RK3588 BSP 保持 Linux 5.10 skb 核心模型，平台特性主要体现在 stmmac RX&#x2F;TX、page_pool、DMA descriptor、checksum&#x2F;hash&#x2F;offload 和 vendor patch。源码结论必须分为三层：通用 skb 语义、stmmac 驱动实现、RK3588 平台 glue&#x2F;硬件能力。QEMU virt 不能验证真实 GMAC DMA&#x2F;page_pool&#x2F;offload，最终硬件结论需开发板串口、ethtool、抓包和性能实验。</p><h3 id="一、版本与源码边界"><a href="#一、版本与源码边界" class="headerlink" title="一、版本与源码边界"></a>一、版本与源码边界</h3><ul><li>通用主线学习树：Linux 5.10.209，commit <code>6c60e1d</code>；</li><li>演进树：Linux 6.1.99，commit <code>5ab5f07</code>；</li><li>Firefly BSP：Linux 5.10.198，commit <code>11a3d9327b6f</code>。</li></ul><p>BSP 版本更早，且可能包含 Android KABI、Rockchip&#x2F;Firefly 和 stmmac backport。判断差异需对具体文件&#x2F;symbol diff，不能按 5.10.198 数字推断全部能力。</p><h3 id="二、通用-skb-机制"><a href="#二、通用-skb-机制" class="headerlink" title="二、通用 skb 机制"></a>二、通用 skb 机制</h3><p>BSP 仍使用：</p><ul><li><code>struct sk_buff</code> shell；</li><li>独立 head + tail shared info；</li><li>linear&#x2F;frags&#x2F;frag_list；</li><li>users&#x2F;dataref&#x2F;page ref；</li><li>alloc&#x2F;build&#x2F;clone&#x2F;copy&#x2F;free；</li><li>NAPI&#x2F;GRO&#x2F;RPS&#x2F;qdisc&#x2F;socket ownership。</li></ul><p>因此阶段 0～4、6～21 的通用概念仍适用，行号和 vendor 字段需以 BSP 树重新检索。</p><h3 id="三、RK3588-GMAC-软件层次"><a href="#三、RK3588-GMAC-软件层次" class="headerlink" title="三、RK3588 GMAC 软件层次"></a>三、RK3588 GMAC 软件层次</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Rockchip platform glue (dwmac-rk.c)</span><br><span class="line">  → clocks/resets/GRF/PHY mode/delay</span><br><span class="line">stmmac core (stmmac_main.c)</span><br><span class="line">  → netdev/NAPI/RX/TX/ring/page_pool/skb</span><br><span class="line">DMA descriptor ops</span><br><span class="line">  → OWN/status/address/length</span><br><span class="line">GMAC hardware</span><br><span class="line">  → DMA/MTL/MAC/PHY</span><br></pre></td></tr></table></figure><p><code>dwmac-rk.c</code> 不直接定义 skb 核心语义，它配置平台硬件；真正构造&#x2F;消费 skb 的主路径在 stmmac core。</p><h3 id="四、RX-生命周期"><a href="#四、RX-生命周期" class="headerlink" title="四、RX 生命周期"></a>四、RX 生命周期</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">page_pool/RX buffer</span><br><span class="line">→ DMA descriptor OWN to hardware</span><br><span class="line">→ frame received, OWN to CPU</span><br><span class="line">→ stmmac_rx()</span><br><span class="line">  ├─ first data copy to napi_alloc_skb linear head</span><br><span class="line">  ├─ later segments skb_add_rx_frag</span><br><span class="line">  ├─ checksum/hash/rx_queue/protocol</span><br><span class="line">  └─ napi_gro_receive</span><br><span class="line">→ GRO/RX core/IP/socket</span><br><span class="line">→ skb free</span><br><span class="line">→ head/frags page release/recycle</span><br></pre></td></tr></table></figure><p>关键入口：<code>firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:3862</code>。</p><p>BSP 实现的核心特点：</p><ul><li>首段 copy 优化 header locality；</li><li>后续 page frag 降低 payload copy；</li><li>copy page 可直接 recycle；</li><li>frag page 生命周期交给 skb；</li><li>refill 在 descriptor 重新 OWN 前准备新 page。</li></ul><h3 id="五、TX-生命周期"><a href="#五、TX-生命周期" class="headerlink" title="五、TX 生命周期"></a>五、TX 生命周期</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">socket/IP/qdisc</span><br><span class="line">→ stmmac_xmit() :3468</span><br><span class="line">  → map linear/frags</span><br><span class="line">  → fill descriptors</span><br><span class="line">  → OWN to DMA</span><br><span class="line">  → BQL sent</span><br><span class="line">→ hardware transmit</span><br><span class="line">→ stmmac_tx_clean() :2087</span><br><span class="line">  → check OWN/status</span><br><span class="line">  → unmap</span><br><span class="line">  → consume skb at packet completion</span><br><span class="line">  → BQL completed</span><br><span class="line">  → wake queue</span><br></pre></td></tr></table></figure><p>一个 skb 占多个 descriptor 时，软件通常只在最后 descriptor 关联 skb，确保所有 mapping 完成后再 free。</p><h3 id="六、page-pool-适用性"><a href="#六、page-pool-适用性" class="headerlink" title="六、page_pool 适用性"></a>六、page_pool 适用性</h3><p>page_pool 为 RX 提供：</p><ul><li>page cache&#x2F;recycle；</li><li>DMA mapping 生命周期优化；</li><li>NAPI-local fast path；</li><li>与 XDP&#x2F;frag 的 memory model。</li></ul><p>BSP 5.10 的回收更多由驱动显式管理。Linux 6.1 <code>pp_recycle</code> 可把回收意图带入通用 skb free。若向 BSP backport，不能只加一个 flag，还需：</p><ul><li>page 标识&#x2F;pool ownership；</li><li>clone&#x2F;copy&#x2F;expand 安全；</li><li>frag unref helper 参数；</li><li>head recycle；</li><li>驱动 build_skb&#x2F;page_pool API 对齐。</li></ul><h3 id="七、checksum-hash-offload"><a href="#七、checksum-hash-offload" class="headerlink" title="七、checksum&#x2F;hash&#x2F;offload"></a>七、checksum&#x2F;hash&#x2F;offload</h3><p>硬件能力通过 netdev features 和 descriptor status 进入 skb：</p><ul><li>RX checksum → <code>ip_summed</code>；</li><li>RX hash → <code>hash/l4_hash</code>；</li><li>RX queue → <code>queue_mapping</code>；</li><li>TX checksum → PARTIAL descriptor flags；</li><li>TSO&#x2F;GSO → MSS&#x2F;header&#x2F;descriptor；</li><li>VLAN → tag insertion&#x2F;stripping metadata。</li></ul><p>必须同时检查：设备树&#x2F;平台配置、stmmac capability、DMA feature register、netdev feature negotiation 和实际 descriptor path。</p><h3 id="八、RPS-RFS-XPS-与单-多队列"><a href="#八、RPS-RFS-XPS-与单-多队列" class="headerlink" title="八、RPS&#x2F;RFS&#x2F;XPS 与单&#x2F;多队列"></a>八、RPS&#x2F;RFS&#x2F;XPS 与单&#x2F;多队列</h3><p>stmmac 驱动可为软件流调度提供 hash 和 RX queue 信息。实际收益取决于：</p><ul><li>GMAC queue 数；</li><li>RSS 硬件能力和配置；</li><li>IRQ affinity；</li><li>RPS&#x2F;XPS maps；</li><li>CPU&#x2F;cluster&#x2F;NUMA（RK3588 big.LITTLE）；</li><li>应用绑核；</li><li>GRO 和包长。</li></ul><p>源码能确定机制位置，不能仅凭源码宣称某个 mask 在实机性能更好。</p><h3 id="九、XDP-能力边界"><a href="#九、XDP-能力边界" class="headerlink" title="九、XDP 能力边界"></a>九、XDP 能力边界</h3><p>判断 native XDP 必须在 BSP stmmac 中找到完整闭环：</p><ul><li>XDP program attach；</li><li>RXQ info&#x2F;memory model；</li><li>RX poll 在 skb 前执行 XDP；</li><li>PASS 转 skb；</li><li>TX ring；</li><li>redirect&#x2F;flush；</li><li>page_pool return。</li></ul><p>若缺失，只能依赖 generic XDP。Linux 6.1 multi-buffer helper 不会自动出现在 BSP 5.10，也不能只复制 helper 而忽略驱动&#x2F;API 链。</p><h3 id="十、BSP-差异分析方法"><a href="#十、BSP-差异分析方法" class="headerlink" title="十、BSP 差异分析方法"></a>十、BSP 差异分析方法</h3><p>对每个主题执行：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">1. rg 找 BSP symbol 和调用点</span><br><span class="line">2. 与 kernel-5.10 同文件/函数 diff</span><br><span class="line">3. git log/blame 查看 vendor/backport 来源</span><br><span class="line">4. 检查 CONFIG、Kconfig、Makefile</span><br><span class="line">5. 检查 DTS 与 platform glue 是否启用硬件能力</span><br><span class="line">6. 区分编译存在、运行启用、硬件实际支持</span><br></pre></td></tr></table></figure><p>不要只比较 <code>include/linux/skbuff.h</code> 就判断完整网络能力；驱动、page_pool、net&#x2F;core&#x2F;dev、offload 和平台配置必须一起看。</p><h3 id="十一、QEMU-与实机边界"><a href="#十一、QEMU-与实机边界" class="headerlink" title="十一、QEMU 与实机边界"></a>十一、QEMU 与实机边界</h3><p>QEMU virt 可以验证 arm64 Linux 协议栈、socket、qdisc、Netfilter 等通用机制，但不能真实模拟：</p><ul><li>RK3588 GMAC&#x2F;MTL&#x2F;DMA descriptor；</li><li>Rockchip GRF&#x2F;clock&#x2F;delay；</li><li>真实 PHY；</li><li>stmmac hardware checksum&#x2F;RSS&#x2F;TSO；</li><li>page_pool 与真实 DMA 性能。</li></ul><p>硬件验证最终需要：串口、dmesg、ethtool -k&#x2F;-S、debugfs、trace、perf、抓包、iperf 和必要的 IRQ&#x2F;RPS&#x2F;XPS 配置实验。本轮笔记仅完成源码分析。</p><h3 id="十二、通用结论与-BSP-结论"><a href="#十二、通用结论与-BSP-结论" class="headerlink" title="十二、通用结论与 BSP 结论"></a>十二、通用结论与 BSP 结论</h3><table><thead><tr><th>结论</th><th>类型</th></tr></thead><tbody><tr><td>len&#x2F;data_len、clone&#x2F;free 引用规则</td><td>通用 skb 语义</td></tr><tr><td>stmmac 首段 copy + 后续 frag</td><td>当前驱动实现</td></tr><tr><td>RK3588 时钟&#x2F;GRF&#x2F;PHY delay</td><td>平台 glue</td></tr><tr><td>某 offload 在板上实际有效</td><td>需实机验证</td></tr><tr><td>6.1 pp_recycle&#x2F;drop reason</td><td>上游版本演进</td></tr><tr><td>BSP 是否 backport 某能力</td><td>本地源码事实</td></tr></tbody></table><h3 id="十三、常见误区"><a href="#十三、常见误区" class="headerlink" title="十三、常见误区"></a>十三、常见误区</h3><ol><li>RK3588 BSP 顶层不是单一 Git 仓库，kernel 子仓提交需单独记录。</li><li>stmmac 通用代码存在不等于板级能力已启用。</li><li>QEMU virt 启动 BSP kernel 不等于模拟真实 GMAC。</li><li>5.10 BSP 可能 backport 新机制，也可能缺少上游 5.10.209 修复。</li><li>page_pool 存在不等于所有 frag 都能自动 recycle。</li><li>源码支持 XDP 配置不等于 native RX path 完整。</li></ol><h3 id="字段与所有权统一核对表-3"><a href="#字段与所有权统一核对表-3" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td>DMA descriptor&#x2F;page</td><td>RK stmmac RX&#x2F;TX</td><td>驱动、page_pool 与 DMA completion 转移所有权</td></tr><tr><td>skb offload metadata</td><td>stmmac&#x2F;BSP</td><td>硬件能力决定 checksum&#x2F;hash&#x2F;GSO 处理</td></tr><tr><td>通用 skb refs</td><td>内核 core</td><td>vendor glue 不改变 users&#x2F;dataref&#x2F;page ref 基本规则</td></tr></tbody></table><h3 id="最小调用链-2"><a href="#最小调用链-2" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">RK3588 GMAC descriptor/page_pool</span><br><span class="line">→ stmmac RX 构造 skb</span><br><span class="line">→ 通用协议栈/socket 或转发</span><br><span class="line">→ qdisc/stmmac TX</span><br><span class="line">→ DMA completion 与 page/skb 回收</span><br></pre></td></tr></table></figure><h2 id="源码阅读路线"><a href="#源码阅读路线" class="headerlink" title="源码阅读路线"></a>源码阅读路线</h2><p>建议不要从 <code>struct sk_buff</code> 声明开始逐字段背诵，而应使用下面的阅读顺序：</p><ol><li>先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造；</li><li>沿成功主线记录 <code>data/len/data_len</code>、header offset、checksum、hash、queue mapping 等字段变化；</li><li>再回到每个失败分支，确认 skb、page、DMA、socket 和记账引用是否回滚；</li><li>最后对照 5.10、6.1 或 BSP 的同名 symbol，区分语义变化、性能优化和 vendor backport。</li></ol><h2 id="本篇总结"><a href="#本篇总结" class="headerlink" title="本篇总结"></a>本篇总结</h2><ol><li>socket 用 <code>truesize</code> 而不是 wire length 计量 skb 占用，并通过 destructor 归还记账。</li><li>MSG_ZEROCOPY 主要避免 payload copy；user page、ubuf_info、segment 和 error queue 形成额外生命周期。</li><li>6.1 的重点是 hot-path cache、page_pool recycle、multi-buffer XDP 和结构化丢包原因。</li><li>RK3588 BSP 必须区分通用 skb 语义、stmmac 实现和真实硬件能力，静态分析不能替代实机验证。</li></ol><p><code>sk_buff</code> 的难点从来不只是字段多，而是<strong>同一个对象不断改变数据视图、元数据状态和持有者</strong>。只要把“布局、字段、所有权、释放”四条线同时画出来，绝大多数网络栈源码都会从零散函数变成可推导的生命周期。</p><h2 id="系列目录"><a href="#系列目录" class="headerlink" title="系列目录"></a>系列目录</h2><ol><li>Linux sk_buff 深度解析（一）：数据结构、四指针与核心字段</li><li>Linux sk_buff 深度解析（二）：非线性数据、引用计数与生命周期</li><li>Linux sk_buff 深度解析（三）：从网卡 DMA 到 Socket 的 RX 路径</li><li>Linux sk_buff 深度解析（四）：从 sendmsg 到 TX completion</li><li>Linux sk_buff 深度解析（五）：GRO、GSO、Checksum 与多核流调度</li><li>Linux sk_buff 深度解析（六）：Netfilter、tc、eBPF 与 XDP</li><li>Linux sk_buff 深度解析（七）：VLAN、隧道、分片与重组</li><li>Linux sk_buff 深度解析（八）：Socket 内存、零拷贝、丢包与版本演进（本文）</li></ol>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/3b78371dc6f3/</id>
    <link href="https://2xpang.cn/blog/posts/3b78371dc6f3/"/>
    <published>2026-07-15T00:00:00.000Z</published>
    <summary>从 socket truesize/owner、MSG_ZEROCOPY、error queue、drop reason 讲到 Linux 5.10 与 6.1 的 skb 演进，并总结 RK3588 BSP 的验证边界。</summary>
    <title>Linux sk_buff 深度解析（八）：Socket 内存、零拷贝、丢包与版本演进</title>
    <updated>2026-07-15T00:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="Linux" scheme="https://2xpang.cn/blog/tags/Linux/"/>
    <category term="网络子系统" scheme="https://2xpang.cn/blog/tags/%E7%BD%91%E7%BB%9C%E5%AD%90%E7%B3%BB%E7%BB%9F/"/>
    <category term="内核源码" scheme="https://2xpang.cn/blog/tags/%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81/"/>
    <category term="sk_buff" scheme="https://2xpang.cn/blog/tags/sk-buff/"/>
    <category term="Linux-5.10" scheme="https://2xpang.cn/blog/tags/Linux-5-10/"/>
    <category term="series:内核网络栈" scheme="https://2xpang.cn/blog/tags/series-%E5%86%85%E6%A0%B8%E7%BD%91%E7%BB%9C%E6%A0%88/"/>
    <category term="VLAN" scheme="https://2xpang.cn/blog/tags/VLAN/"/>
    <category term="VXLAN" scheme="https://2xpang.cn/blog/tags/VXLAN/"/>
    <category term="IP分片" scheme="https://2xpang.cn/blog/tags/IP%E5%88%86%E7%89%87/"/>
    <content>
      <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p><code>sk_buff</code> 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”，而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的<strong>控制对象和所有权载体</strong>。</p><p>解释 VLAN metadata、inner&#x2F;outer header、encapsulation、隧道 GSO&#x2F;checksum，以及 IPv4&#x2F;IPv6 分片队列和 frag_list 重组后的非线性布局。</p><p>本文以本地 <strong>Linux 5.10.209</strong> 源码为主线；涉及演进时对照 <strong>Linux 6.1.99</strong>，涉及网卡驱动时结合 <strong>Firefly RK3588 SDK Linux 5.10.198</strong>。本文是源码分析，不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。</p><p><img src="/blog/img/sk_buff/encapsulation-layout.svg" alt="Linux sk_buff 深度解析（七）：VLAN、隧道、分片与重组"></p><h2 id="阅读前先建立四个问题"><a href="#阅读前先建立四个问题" class="headerlink" title="阅读前先建立四个问题"></a>阅读前先建立四个问题</h2><p>阅读任何 skb 代码路径，都建议反复追问：</p><ol><li>当前 <code>skb-&gt;data</code> 指向哪一层协议头，linear 与 non-linear 数据分别在哪里？</li><li>哪些字段刚被修改，下一层为什么依赖这些字段？</li><li>当前谁持有 skb shell、head、frag page 以及 socket&#x2F;dst&#x2F;conntrack 等外部引用？</li><li>成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb？</li></ol><h2 id="本篇核心结论"><a href="#本篇核心结论" class="headerlink" title="本篇核心结论"></a>本篇核心结论</h2><ul><li>VLAN tag 可能在报文字节中，也可能只存在 skb metadata 中。</li><li>隧道 skb 同时保存 outer&#x2F;inner header offset，offload 依赖这些元数据理解内层协议。</li><li>分片产生多个独立 skb，重组队列必须持有并在超时&#x2F;错误时释放全部片段。</li><li>重组结果常用非线性布局或 <code>frag_list</code>，不能假设重新拼成一块连续内存。</li></ul><h2 id="1-VLAN、VXLAN、Geneve、GRE-与多层-Header"><a href="#1-VLAN、VXLAN、Geneve、GRE-与多层-Header" class="headerlink" title="1. VLAN、VXLAN、Geneve、GRE 与多层 Header"></a>1. VLAN、VXLAN、Geneve、GRE 与多层 Header</h2><h3 id="结论摘要"><a href="#结论摘要" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>skb 可用两种方式表达 VLAN：tag 仍在 packet bytes 中，或硬件已剥离并存入 <code>vlan_tci/vlan_proto</code> metadata。隧道通过 outer 与 inner header offset、<code>encapsulation</code>、inner protocol 和 GSO&#x2F;checksum metadata 同时描述两套协议栈。封装需要 headroom&#x2F;COW，解封装需要正确 pull&#x2F;reset metadata；错误维护会影响路由、checksum、GSO 和 BPF&#x2F;tc 解析。</p><h3 id="一、VLAN-两种形态"><a href="#一、VLAN-两种形态" class="headerlink" title="一、VLAN 两种形态"></a>一、VLAN 两种形态</h3><h3 id="包内-VLAN-header"><a href="#包内-VLAN-header" class="headerlink" title="包内 VLAN header"></a>包内 VLAN header</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Ethernet dst/src → 802.1Q/802.1ad header → inner EtherType → payload</span><br></pre></td></tr></table></figure><p>占用 packet bytes，data&#x2F;len&#x2F;header offset 反映真实 4 字节或多层 tag。</p><h3 id="hardware-accelerated-metadata"><a href="#hardware-accelerated-metadata" class="headerlink" title="hardware-accelerated metadata"></a>hardware-accelerated metadata</h3><p>网卡 RX 可剥离 tag，把：</p><ul><li>VLAN protocol；</li><li>TCI（VID&#x2F;PCP&#x2F;DEI）；</li><li>present bit</li></ul><p>写入 skb 字段。packet bytes 中已没有该 tag，但协议栈仍能通过 helper 查询。</p><p><code>__vlan_hwaccel_put_tag()</code> 写 metadata。TX 网卡若支持 VLAN insertion，可根据 metadata 在硬件发包时重新插入。</p><h3 id="二、skb-vlan-untag"><a href="#二、skb-vlan-untag" class="headerlink" title="二、skb_vlan_untag"></a>二、skb_vlan_untag</h3><p><code>skb_vlan_untag()</code> 在 <code>kernel-5.10/net/core/skbuff.c:5499</code>。它把 packet bytes 中的 VLAN header 解析&#x2F;移除并转为 metadata，涉及：</p><ul><li>确保 header 线性可读；</li><li>pull&#x2F;memmove；</li><li>protocol 更新；</li><li>MAC header&#x2F;len 调整；</li><li>COW&#x2F;错误释放。</li></ul><p>不能只减 <code>len</code>，否则 Ethernet 地址和 header offset 会错位。</p><h3 id="三、VLAN-push-pop"><a href="#三、VLAN-push-pop" class="headerlink" title="三、VLAN push&#x2F;pop"></a>三、VLAN push&#x2F;pop</h3><p>TC&#x2F;BPF&#x2F;bridge&#x2F;VLAN device 可 push&#x2F;pop tag：</p><ul><li>push 需要 headroom 或 expand；</li><li>pop 需要确保 header linear；</li><li>修改 EtherType；</li><li>更新 MAC&#x2F;network header；</li><li>处理 checksum&#x2F;GSO；</li><li>metadata 与包内 tag 之间转换。</li></ul><p>QinQ 可能有多层 tag，单个 <code>vlan_tci</code> 只表达当前 offload metadata 层，其余仍可能在 bytes 中。</p><h3 id="四、隧道封装布局"><a href="#四、隧道封装布局" class="headerlink" title="四、隧道封装布局"></a>四、隧道封装布局</h3><p>以 VXLAN 为例：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">outer Ethernet</span><br><span class="line">outer IP</span><br><span class="line">outer UDP</span><br><span class="line">VXLAN</span><br><span class="line">inner Ethernet</span><br><span class="line">inner IP</span><br><span class="line">inner TCP/UDP</span><br><span class="line">payload</span><br></pre></td></tr></table></figure><p>skb 需要同时保存：</p><ul><li>outer MAC&#x2F;network&#x2F;transport header；</li><li>inner MAC&#x2F;network&#x2F;transport header；</li><li>outer protocol 与 inner protocol；</li><li><code>encapsulation=1</code>；</li><li>GSO tunnel type；</li><li>outer&#x2F;inner checksum 状态；</li><li>dst&#x2F;tunnel metadata。</li></ul><h3 id="五、inner-header-helper"><a href="#五、inner-header-helper" class="headerlink" title="五、inner header helper"></a>五、inner header helper</h3><p><code>skb_reset/set_inner_mac_header()</code>、network、transport helper 位于 <code>kernel-5.10/include/linux/skbuff.h:2480-2553</code> 附近。</p><p>典型封装前：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">原 mac/network/transport → 保存为 inner_*</span><br><span class="line">skb_push outer headers</span><br><span class="line">重设 outer mac/network/transport</span><br><span class="line">encapsulation = 1</span><br></pre></td></tr></table></figure><p>由于 offset 相对 head，push data 不会自动破坏已保存 inner header 地址，但 expand head 后必须由 helper&#x2F;复制逻辑保持 offset。</p><h3 id="六、headroom-与-COW"><a href="#六、headroom-与-COW" class="headerlink" title="六、headroom 与 COW"></a>六、headroom 与 COW</h3><p>封装向前添加几十字节 outer header，必须：</p><ol><li>计算 <code>LL_RESERVED_SPACE</code>、隧道 header 等需求；</li><li><code>skb_cow_head()</code> 确保 head 可写且空间足；</li><li>重新获取旧 header 指针；</li><li>push 并填写 outer header；</li><li>更新 len&#x2F;protocol&#x2F;checksum&#x2F;GSO。</li></ol><p>clone 的 skb 若直接写共享 head，会同时破坏其它路径。</p><h3 id="七、VXLAN-Geneve-GRE-代表路径"><a href="#七、VXLAN-Geneve-GRE-代表路径" class="headerlink" title="七、VXLAN&#x2F;Geneve&#x2F;GRE 代表路径"></a>七、VXLAN&#x2F;Geneve&#x2F;GRE 代表路径</h3><ul><li>VXLAN 实现在 <code>kernel-5.10/drivers/net/vxlan/vxlan_core.c</code>；</li><li>Geneve 在 <code>kernel-5.10/drivers/net/geneve.c</code>；</li><li>GRE 在 <code>kernel-5.10/net/ipv4/ip_gre.c</code>。</li></ul><p>它们差异在 tunnel header、metadata&#x2F;options 和 checksum，但 skb 层共同模式是：保存 inner header → COW&#x2F;headroom → push outer → 设置 encapsulation&#x2F;GSO → route&#x2F;output。</p><h3 id="八、隧道-GSO"><a href="#八、隧道-GSO" class="headerlink" title="八、隧道 GSO"></a>八、隧道 GSO</h3><p>一个大 inner TCP skb 可先封装再由网卡执行 tunnel TSO。<code>gso_type</code> 必须说明：</p><ul><li>inner TCP&#x2F;UDP 类型；</li><li>outer UDP tunnel&#x2F;GRE；</li><li>outer checksum 是否需要；</li><li>partial GSO 等。</li></ul><p>设备不支持时，软件可能在正确层次分段。错误的 inner header offset 会让分段器复制&#x2F;修改错误 header。</p><h3 id="九、解封装"><a href="#九、解封装" class="headerlink" title="九、解封装"></a>九、解封装</h3><p>RX 隧道 handler：</p><ol><li>验证 outer header&#x2F;checksum；</li><li>pull outer header；</li><li>恢复&#x2F;重设 inner MAC&#x2F;network&#x2F;transport 为当前层；</li><li>更新 <code>dev/protocol/hash/checksum</code>；</li><li>清理或转换 tunnel metadata；</li><li>重新进入 GRO&#x2F;RX stack。</li></ol><p>outer checksum 已验证不自动等于 inner checksum 已验证；<code>csum_level</code> 表达层级。</p><h3 id="十、字段变化表"><a href="#十、字段变化表" class="headerlink" title="十、字段变化表"></a>十、字段变化表</h3><table><thead><tr><th>操作</th><th>data&#x2F;headroom</th><th>header offset</th><th>metadata</th></tr></thead><tbody><tr><td>VLAN hw RX</td><td>bytes 中 tag 被剥离</td><td>L2 视图已调整</td><td>vlan_tci&#x2F;proto present</td></tr><tr><td>VLAN push</td><td>data 向前&#x2F;内容移动</td><td>MAC&#x2F;network 调整</td><td>metadata 可能清除</td></tr><tr><td>tunnel encap</td><td>push outer headers</td><td>原 header 保存为 inner</td><td>encapsulation&#x2F;GSO&#x2F;dst</td></tr><tr><td>tunnel decap</td><td>pull outer</td><td>inner 提升为当前</td><td>dev&#x2F;protocol&#x2F;csum 更新</td></tr></tbody></table><h3 id="十一、常见误区"><a href="#十一、常见误区" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li><code>vlan_present</code> 时 packet bytes 不一定含 VLAN header。</li><li>inner header 不是通过扫描自动得出，封装代码必须正确设置。</li><li>encapsulation 不只是一个 flag，还依赖 inner protocol&#x2F;GSO&#x2F;checksum。</li><li>push outer header 前必须 COW，不能只检查 headroom。</li><li>解封装后 hash&#x2F;checksum&#x2F;dev 可能需要重新评估。</li><li>隧道 GSO 支持取决于目标设备 feature，不是设置 gso_type 就一定可硬件执行。</li></ol><h3 id="字段与所有权统一核对表"><a href="#字段与所有权统一核对表" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td>outer&#x2F;inner header offsets</td><td>隧道封装&#x2F;解封</td><td>同时定位两层协议头</td></tr><tr><td><code>encapsulation/inner_protocol</code></td><td>GSO&#x2F;checksum</td><td>告诉 offload 如何解析内层</td></tr><tr><td>VLAN metadata</td><td>硬件剥离&#x2F;插入</td><td>tag 可在 skb metadata 或报文数据中</td></tr></tbody></table><h3 id="最小调用链"><a href="#最小调用链" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">原始 skb</span><br><span class="line">→ VLAN metadata 或 push outer header</span><br><span class="line">→ 设置 inner/outer offsets 与 encapsulation</span><br><span class="line">→ GSO/checksum 处理</span><br><span class="line">→ 解封装恢复内层视图</span><br></pre></td></tr></table></figure><h2 id="2-IP-分片、重组与-frag-list"><a href="#2-IP-分片、重组与-frag-list" class="headerlink" title="2. IP 分片、重组与 frag_list"></a>2. IP 分片、重组与 frag_list</h2><p><img src="/blog/img/sk_buff/vlan-metadata.svg" alt="IP 分片、重组与 frag_list"></p><h3 id="结论摘要-1"><a href="#结论摘要-1" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>IP 分片是网络协议语义，skb <code>frags[]</code> 是内存布局，两者不是同一概念。输出分片把一个 IP datagram 变为多个独立 skb，每个有自己的 IP header；接收重组把多个 fragment skb 排入 fragment queue，验证 offset&#x2F;overlap&#x2F;长度后生成一个重组 skb，可能通过 frag_list&#x2F;非线性布局避免全量复制。</p><h3 id="一、不要混淆两种-frag"><a href="#一、不要混淆两种-frag" class="headerlink" title="一、不要混淆两种 frag"></a>一、不要混淆两种 frag</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">skb frags[]:</span><br><span class="line">  一个 skb 内部由多个 page 片段组成</span><br><span class="line"></span><br><span class="line">IP fragments:</span><br><span class="line">  多个独立 IP packet/skb，共同组成一个 datagram</span><br></pre></td></tr></table></figure><p>一个 IP fragment skb 自己仍可拥有多个 <code>frags[]</code>；重组后的 skb 也可能非线性。</p><h3 id="二、IPv4-输出分片入口"><a href="#二、IPv4-输出分片入口" class="headerlink" title="二、IPv4 输出分片入口"></a>二、IPv4 输出分片入口</h3><p><code>ip_fragment()</code> 位于 <code>kernel-5.10/net/ipv4/ip_output.c:582</code>。触发条件通常是：</p><ul><li>skb 长度超过输出 MTU；</li><li>DF 未禁止分片或本地策略允许；</li><li>GSO&#x2F;设备 feature 不能在更后面解决；</li><li>PMTU&#x2F;协议规则允许。</li></ul><p>输出产生多个 skb，每个包含：</p><ul><li>复制并调整的 IP header；</li><li>fragment offset&#x2F;MF flag；</li><li>独立 total length&#x2F;checksum；</li><li>datagram ID；</li><li>对应 payload slice。</li></ul><h3 id="三、fast-path-与-slow-path"><a href="#三、fast-path-与-slow-path" class="headerlink" title="三、fast path 与 slow path"></a>三、fast path 与 slow path</h3><p>若原 skb 已按合适 frag_list 组织、各子 skb headroom&#x2F;长度满足要求，分片器可较少复制地重用&#x2F;调整 skb 链。否则需要：</p><ul><li>分配新 skb；</li><li>复制 header；</li><li>用 <code>skb_copy_bits()</code> 从原逻辑数据提取每片 payload；</li><li>维护 page ref、dst、metadata；</li><li>逐片调用 output。</li></ul><p>分片后每个 skb 的 owner 独立，某一片输出失败时剩余链必须释放。</p><h3 id="四、IPv4-接收重组"><a href="#四、IPv4-接收重组" class="headerlink" title="四、IPv4 接收重组"></a>四、IPv4 接收重组</h3><p><code>ip_defrag()</code> 位于 <code>kernel-5.10/net/ipv4/ip_fragment.c:475</code>。fragment key 通常包括：</p><ul><li>src&#x2F;dst；</li><li>IP ID；</li><li>protocol；</li><li>user&#x2F;zone 等上下文。</li></ul><p>每片进入 queue：</p><ol><li>校验 offset、MF、长度和边界；</li><li>按 offset 插入树&#x2F;队列；</li><li>处理 overlap&#x2F;duplicate；</li><li>更新已收到范围和总长度；</li><li>未完整则 queue 持有 skb；</li><li>完整后 <code>ip_frag_reasm()</code> 生成重组 skb。</li></ol><p>queue timeout、内存阈值、overlap policy 或非法片都会释放相应 skb。</p><h3 id="五、重组数据布局"><a href="#五、重组数据布局" class="headerlink" title="五、重组数据布局"></a>五、重组数据布局</h3><p>重组不必把所有 payload memcpy 到一块大 linear buffer。常见结果：</p><ul><li>第一片 skb 成为 head；</li><li>必要 header 保持 linear；</li><li>后续数据通过 frags&#x2F;frag_list 连接；</li><li>调整 <code>len/data_len/truesize</code>；</li><li>清除 fragment flag、重算 header；</li><li>后续协议使用 <code>pskb_may_pull()</code> 获取所需连续 header。</li></ul><p>这也是 frag_list 能表达“多个完整子 skb 组成一个逻辑包”的典型用途。</p><h3 id="六、IPv6-差异"><a href="#六、IPv6-差异" class="headerlink" title="六、IPv6 差异"></a>六、IPv6 差异</h3><p>IPv6 路由器不进行中途分片，源主机通过 Fragment extension header 分片。接收重组实现位于 <code>kernel-5.10/net/ipv6/reassembly.c</code>。</p><p>差异包括：</p><ul><li>fragment header 格式和位置；</li><li>atomic fragment 处理；</li><li>extension header 解析；</li><li>overlap 安全规则；</li><li>ICMPv6&#x2F;PMTU 语义。</li></ul><p>skb queue、所有权、重组非线性布局的基本问题仍相似。</p><h3 id="七、Netfilter-defrag"><a href="#七、Netfilter-defrag" class="headerlink" title="七、Netfilter defrag"></a>七、Netfilter defrag</h3><p>conntrack&#x2F;NAT 需要完整 L4 header，因此 Netfilter 可在 PRE_ROUTING&#x2F;LOCAL_OUT 早期 defrag。重组后的包完成 conntrack&#x2F;NAT 后，输出可能根据 MTU 再次分片。</p><p>不同 <code>ip_defrag</code> user 值区分调用场景，避免错误复用 fragment queue 上下文。</p><h3 id="八、GRO-frag-list-与-IP-frag-list"><a href="#八、GRO-frag-list-与-IP-frag-list" class="headerlink" title="八、GRO frag_list 与 IP frag_list"></a>八、GRO frag_list 与 IP frag_list</h3><p>两者都可能使用 <code>frag_list</code>，但语义不同：</p><ul><li>GRO：多个可聚合的同 flow packet 被当作大 skb 批处理，GSO metadata 保留 segment 边界；</li><li>IP reassembly：多个 IP fragments 组成原始 datagram，重组后对 L4 表现为一个 IP 包。</li></ul><p>不能仅看到 frag_list 就判断来源。</p><h3 id="九、所有权时间线"><a href="#九、所有权时间线" class="headerlink" title="九、所有权时间线"></a>九、所有权时间线</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">fragment skb arrives</span><br><span class="line">→ defrag queue owns skb</span><br><span class="line">  ├─ incomplete: retained until more fragments/timeout</span><br><span class="line">  ├─ invalid/timeout: queue frees</span><br><span class="line">  └─ complete: refs/data transferred into reassembled skb</span><br><span class="line">→ L4 stack owns reassembled skb</span><br></pre></td></tr></table></figure><p>输出方向原 skb 被分片器消费，产生的新 skb 链分别交 output；调用者需遵守 <code>ip_fragment()</code> 的消费合同。</p><h3 id="十、资源与安全"><a href="#十、资源与安全" class="headerlink" title="十、资源与安全"></a>十、资源与安全</h3><p>fragment queue 容易被攻击消耗内存。内核使用：</p><ul><li>高低阈值；</li><li>timeout；</li><li>LRU&#x2F;eviction；</li><li>最大 datagram 长度；</li><li>overlap&#x2F;duplicate 检查；</li><li>hash&#x2F;secret；</li><li>per-netns accounting。</li></ul><p>truesize 而非单纯 payload 长度更接近真实资源消耗。</p><h3 id="十一、常见误区-1"><a href="#十一、常见误区-1" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li><code>skb_frag_t</code> 与 IP fragment 不是一个层次。</li><li>重组完成不代表数据一定全 linear。</li><li>分片器可能消费原 skb，不能按普通只读函数理解。</li><li>IPv6 路由器不会像 IPv4 一样中途分片。</li><li>GRO 聚合与 IP 重组的 frag_list 语义不同。</li><li>fragment queue 必须计时和限内存，否则可被 DoS。</li></ol><h3 id="字段与所有权统一核对表-1"><a href="#字段与所有权统一核对表-1" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>frag_off/len</code></td><td>IP 分片</td><td>每个片 skb 独立持有 data&#x2F;page</td></tr><tr><td>fragment queue</td><td>defrag</td><td>队列在重组前持有所有片 skb</td></tr><tr><td><code>frag_list/data_len</code></td><td>重组结果</td><td>首 skb 持有后续片链并统一释放</td></tr></tbody></table><h3 id="最小调用链-1"><a href="#最小调用链-1" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">大 IP skb output 分片或网络接收 fragments</span><br><span class="line">→ fragment queue 持有各片</span><br><span class="line">→ 完整性检查/重组</span><br><span class="line">→ frag_list/nonlinear skb</span><br><span class="line">→ 后续协议处理与统一释放</span><br></pre></td></tr></table></figure><h2 id="从局部机制回到完整路径"><a href="#从局部机制回到完整路径" class="headerlink" title="从局部机制回到完整路径"></a>从局部机制回到完整路径</h2><p><img src="/blog/img/sk_buff/fragment-reassembly.svg" alt="完整路径关系"></p><p>前面的局部机制只有放回完整生命周期才不容易误判：数据布局决定 helper 能否直接访问；引用计数决定能否原地修改；队列和驱动返回值决定谁仍然拥有 skb。遇到异常路径时，应按“外部引用 → 数据区 → shell”的逆序检查回收。</p><h2 id="源码阅读路线"><a href="#源码阅读路线" class="headerlink" title="源码阅读路线"></a>源码阅读路线</h2><p>建议不要从 <code>struct sk_buff</code> 声明开始逐字段背诵，而应使用下面的阅读顺序：</p><ol><li>先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造；</li><li>沿成功主线记录 <code>data/len/data_len</code>、header offset、checksum、hash、queue mapping 等字段变化；</li><li>再回到每个失败分支，确认 skb、page、DMA、socket 和记账引用是否回滚；</li><li>最后对照 5.10、6.1 或 BSP 的同名 symbol，区分语义变化、性能优化和 vendor backport。</li></ol><h2 id="本篇总结"><a href="#本篇总结" class="headerlink" title="本篇总结"></a>本篇总结</h2><ol><li>VLAN tag 可能在报文字节中，也可能只存在 skb metadata 中。</li><li>隧道 skb 同时保存 outer&#x2F;inner header offset，offload 依赖这些元数据理解内层协议。</li><li>分片产生多个独立 skb，重组队列必须持有并在超时&#x2F;错误时释放全部片段。</li><li>重组结果常用非线性布局或 <code>frag_list</code>，不能假设重新拼成一块连续内存。</li></ol><p><code>sk_buff</code> 的难点从来不只是字段多，而是<strong>同一个对象不断改变数据视图、元数据状态和持有者</strong>。只要把“布局、字段、所有权、释放”四条线同时画出来，绝大多数网络栈源码都会从零散函数变成可推导的生命周期。</p><h2 id="系列目录"><a href="#系列目录" class="headerlink" title="系列目录"></a>系列目录</h2><ol><li>Linux sk_buff 深度解析（一）：数据结构、四指针与核心字段</li><li>Linux sk_buff 深度解析（二）：非线性数据、引用计数与生命周期</li><li>Linux sk_buff 深度解析（三）：从网卡 DMA 到 Socket 的 RX 路径</li><li>Linux sk_buff 深度解析（四）：从 sendmsg 到 TX completion</li><li>Linux sk_buff 深度解析（五）：GRO、GSO、Checksum 与多核流调度</li><li>Linux sk_buff 深度解析（六）：Netfilter、tc、eBPF 与 XDP</li><li>Linux sk_buff 深度解析（七）：VLAN、隧道、分片与重组（本文）</li><li>Linux sk_buff 深度解析（八）：Socket 内存、零拷贝、丢包与版本演进</li></ol>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/9a2aec9bcbf6/</id>
    <link href="https://2xpang.cn/blog/posts/9a2aec9bcbf6/"/>
    <published>2026-07-14T23:00:00.000Z</published>
    <summary>解释 VLAN metadata、inner/outer header、encapsulation、隧道 GSO/checksum，以及 IPv4/IPv6 分片队列和 frag_list 重组后的非线性布局。</summary>
    <title>Linux sk_buff 深度解析（七）：VLAN、隧道、分片与重组</title>
    <updated>2026-07-14T23:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="Linux" scheme="https://2xpang.cn/blog/tags/Linux/"/>
    <category term="网络子系统" scheme="https://2xpang.cn/blog/tags/%E7%BD%91%E7%BB%9C%E5%AD%90%E7%B3%BB%E7%BB%9F/"/>
    <category term="内核源码" scheme="https://2xpang.cn/blog/tags/%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81/"/>
    <category term="sk_buff" scheme="https://2xpang.cn/blog/tags/sk-buff/"/>
    <category term="Linux-5.10" scheme="https://2xpang.cn/blog/tags/Linux-5-10/"/>
    <category term="series:内核网络栈" scheme="https://2xpang.cn/blog/tags/series-%E5%86%85%E6%A0%B8%E7%BD%91%E7%BB%9C%E6%A0%88/"/>
    <category term="Linux-6.1" scheme="https://2xpang.cn/blog/tags/Linux-6-1/"/>
    <category term="Netfilter" scheme="https://2xpang.cn/blog/tags/Netfilter/"/>
    <category term="eBPF" scheme="https://2xpang.cn/blog/tags/eBPF/"/>
    <category term="XDP" scheme="https://2xpang.cn/blog/tags/XDP/"/>
    <content>
      <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p><code>sk_buff</code> 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”，而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的<strong>控制对象和所有权载体</strong>。</p><p>从 Hook、verdict 和所有权出发，比较 Netfilter&#x2F;Conntrack&#x2F;NAT、tc action、eBPF __sk_buff 视图，以及 XDP 进入 skb 世界前后的边界。</p><p>本文以本地 <strong>Linux 5.10.209</strong> 源码为主线；涉及演进时对照 <strong>Linux 6.1.99</strong>，涉及网卡驱动时结合 <strong>Firefly RK3588 SDK Linux 5.10.198</strong>。本文是源码分析，不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。</p><p><img src="/blog/img/sk_buff/hook-pipeline.svg" alt="Linux sk_buff 深度解析（六）：Netfilter、tc、eBPF 与 XDP"></p><h2 id="阅读前先建立四个问题"><a href="#阅读前先建立四个问题" class="headerlink" title="阅读前先建立四个问题"></a>阅读前先建立四个问题</h2><p>阅读任何 skb 代码路径，都建议反复追问：</p><ol><li>当前 <code>skb-&gt;data</code> 指向哪一层协议头，linear 与 non-linear 数据分别在哪里？</li><li>哪些字段刚被修改，下一层为什么依赖这些字段？</li><li>当前谁持有 skb shell、head、frag page 以及 socket&#x2F;dst&#x2F;conntrack 等外部引用？</li><li>成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb？</li></ol><h2 id="本篇核心结论"><a href="#本篇核心结论" class="headerlink" title="本篇核心结论"></a>本篇核心结论</h2><ul><li>Hook 框架的核心不是“调用规则”，而是 verdict 如何改变控制流和 skb 所有权。</li><li>Conntrack、dst、socket、BPF metadata 都有独立于 skb shell 的引用语义。</li><li><code>struct __sk_buff</code> 是 verifier 管理的 ABI 视图，不等于程序可任意解引用内核 <code>struct sk_buff</code>。</li><li>XDP 在 skb 分配前处理 RX buffer；只有 XDP_PASS 才通常进入完整 skb 生命周期。</li></ul><h2 id="1-Netfilter、Conntrack-与-NAT"><a href="#1-Netfilter、Conntrack-与-NAT" class="headerlink" title="1. Netfilter、Conntrack 与 NAT"></a>1. Netfilter、Conntrack 与 NAT</h2><h3 id="结论摘要"><a href="#结论摘要" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>Netfilter hook 把 skb 交给按优先级注册的 hook function。verdict 不只是允许&#x2F;拒绝，还决定 skb 所有权：ACCEPT 继续，DROP 释放，STOLEN 表示 hook 已接管，QUEUE 把 skb 交给用户队列并等待 reinject，REPEAT 重跑当前 hook。conntrack 通过 skb <code>_nfct</code> 挂接共享连接引用；NAT 修改 header 前必须确保线性、可写并同步 checksum、route 状态。</p><h3 id="一、hook-位置"><a href="#一、hook-位置" class="headerlink" title="一、hook 位置"></a>一、hook 位置</h3><p>IPv4 典型：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">RX:</span><br><span class="line">  PRE_ROUTING → route → LOCAL_IN 或 FORWARD → POST_ROUTING</span><br><span class="line"></span><br><span class="line">TX local:</span><br><span class="line">  LOCAL_OUT → route/output → POST_ROUTING</span><br></pre></td></tr></table></figure><p>bridge、ARP、IPv6 有各自 hook family。skb 在不同 hook 时的 data&#x2F;header&#x2F;dev&#x2F;dst 状态不同，hook 不能假定所有层都已解析。</p><h3 id="二、nf-hook-slow"><a href="#二、nf-hook-slow" class="headerlink" title="二、nf_hook_slow"></a>二、nf_hook_slow</h3><p>入口：<code>kernel-5.10/net/netfilter/core.c:577</code>。</p><p>它从 hook entries 指定索引开始遍历：</p><ol><li>调用 hook function；</li><li>读取 verdict；</li><li>ACCEPT 继续下一个；</li><li>DROP 释放并返回错误；</li><li>QUEUE 调用 <code>nf_queue()</code>；</li><li>STOLEN 停止，skb 已被 hook 接管；</li><li>最终全部 ACCEPT 后调用 okfn 继续原协议路径。</li></ol><h3 id="三、verdict-与所有权"><a href="#三、verdict-与所有权" class="headerlink" title="三、verdict 与所有权"></a>三、verdict 与所有权</h3><p>定义：<code>kernel-5.10/include/uapi/linux/netfilter.h:11-15</code>。</p><table><thead><tr><th>Verdict</th><th>skb 语义</th></tr></thead><tbody><tr><td><code>NF_ACCEPT</code></td><td>Netfilter 不保留，继续下一 hook&#x2F;okfn</td></tr><tr><td><code>NF_DROP</code></td><td>当前路径丢弃，core 负责或已执行释放</td></tr><tr><td><code>NF_STOLEN</code></td><td>hook 已接管 skb，调用者不能释放或继续</td></tr><tr><td><code>NF_QUEUE</code></td><td>queue subsystem 持有 skb，等待 verdict&#x2F;reinject</td></tr><tr><td><code>NF_REPEAT</code></td><td>从当前 hook 再执行，需要避免无限循环</td></tr></tbody></table><p><code>NF_DROP_ERR()</code> 还能把 errno 编码进 verdict。</p><h3 id="四、NFQUEUE-与-reinject"><a href="#四、NFQUEUE-与-reinject" class="headerlink" title="四、NFQUEUE 与 reinject"></a>四、NFQUEUE 与 reinject</h3><p><code>nf_queue()</code> 位于 <code>kernel-5.10/net/netfilter/nf_queue.c:246</code>。queue entry 保存：</p><ul><li>skb；</li><li>hook state；</li><li>当前 hook index；</li><li>设备&#x2F;socket&#x2F;netns 等引用。</li></ul><p>用户空间给出 verdict 后 <code>nf_reinject()</code> 从保存位置继续。排队期间 skb 生命周期跨越异步用户交互，所以必须增加所有关联引用；queue overflow、进程退出或 verdict drop 都要释放。</p><h3 id="五、conntrack"><a href="#五、conntrack" class="headerlink" title="五、conntrack"></a>五、conntrack</h3><p>conntrack 识别 tuple、状态和方向，把连接对象引用编码在 <code>skb-&gt;_nfct</code>。skb clone&#x2F;copy 时 <code>__nf_copy()</code> 增加引用，释放时 <code>nf_conntrack_put()</code>，见 <code>skb_release_head_state()</code>。</p><p>conntrack entry 是共享 flow 状态，不在 skb 数据区中。一个连接的多个 skb 指向同一对象；错误漏 ref 会 use-after-free，漏 put 会连接表泄漏。</p><h3 id="六、NAT"><a href="#六、NAT" class="headerlink" title="六、NAT"></a>六、NAT</h3><p><code>nf_nat_packet()</code> 位于 <code>kernel-5.10/net/netfilter/nf_nat_core.c:695</code>。NAT 根据 conntrack 方向修改：</p><ul><li>IPv4&#x2F;IPv6 地址；</li><li>TCP&#x2F;UDP 端口；</li><li>ICMP 内嵌报文；</li><li>checksum；</li><li>可能触发 route 重新计算。</li></ul><p>修改前必须保证：</p><ol><li>相关 header 在线性区；</li><li>skb header 可写，必要时 <code>skb_ensure_writable()</code>&#x2F;COW；</li><li>修改长度时 tailroom&#x2F;headroom 合法；</li><li>checksum metadata 与实际字节一致；</li><li>cloned skb 不直接污染其它共享者。</li></ol><h3 id="七、skb-字段影响"><a href="#七、skb-字段影响" class="headerlink" title="七、skb 字段影响"></a>七、skb 字段影响</h3><ul><li><code>_nfct</code>：连接引用与 ctinfo；</li><li><code>mark</code>：规则、策略路由和 tc 共享的通用标记；</li><li><code>nf_trace</code>：trace 状态；</li><li><code>secmark</code>：安全标记；</li><li><code>dev/sk/dst</code>：hook state 与 route；</li><li>header offsets&#x2F;checksum：NAT 修改；</li><li>extensions：可携带 secpath、bridge 等额外状态。</li></ul><h3 id="八、fragment-与-conntrack"><a href="#八、fragment-与-conntrack" class="headerlink" title="八、fragment 与 conntrack"></a>八、fragment 与 conntrack</h3><p>L4 conntrack&#x2F;NAT 需要端口，后续分片不一定含 L4 header。Netfilter defrag 在合适 hook 前重组，确保 conntrack 看到完整报文；之后转发&#x2F;输出可能再次分片。</p><p>这意味着 skb 可能在 Netfilter 前后从多个 fragment skb 变成一个重组 skb，数据布局和 truesize 都会改变。</p><h3 id="九、bridge-Netfilter"><a href="#九、bridge-Netfilter" class="headerlink" title="九、bridge Netfilter"></a>九、bridge Netfilter</h3><p>桥接包可能同时经历 L2 bridge hook 与伪装成 L3 的 IPv4&#x2F;IPv6 hook。<code>nf_bridge</code>&#x2F;相关 extension 保存原设备、物理接口和 header 状态，避免二层&#x2F;三层视图切换丢失信息。</p><p>不能只看 <code>skb-&gt;dev</code> 就判断原物理入口。</p><h3 id="十、错误与释放"><a href="#十、错误与释放" class="headerlink" title="十、错误与释放"></a>十、错误与释放</h3><ul><li>hook DROP：drop path 释放；</li><li>STOLEN：hook 自己最终释放或转交；</li><li>QUEUE：queue&#x2F;reinject 负责；</li><li>NAT COW 失败：返回 drop&#x2F;error 并释放；</li><li>conntrack invalid：按规则 accept&#x2F;drop，引用仍需平衡；</li><li>clone 给日志&#x2F;tap：每个引用独立释放。</li></ul><h3 id="十一、常见误区"><a href="#十一、常见误区" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li><code>NF_STOLEN</code> 不等于 drop，它表示所有权被 hook 接管。</li><li>QUEUE 后原协议路径不能继续访问 skb。</li><li>conntrack 数据不在 packet bytes 中，而是 skb 外部引用。</li><li>NAT 不只是改地址，还要维护端口、checksum、route 和 clone 可写性。</li><li>mark 不是 conntrack state，但规则可互相读写。</li><li>hook 所在位置决定 header&#x2F;data 是否可用，不能通用地直接 <code>ip_hdr()</code>。</li></ol><h3 id="字段与所有权统一核对表"><a href="#字段与所有权统一核对表" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>_nfct</code></td><td>conntrack lookup&#x2F;confirm</td><td>skb 持有 conntrack 引用</td></tr><tr><td><code>mark</code></td><td>规则、策略路由、tc</td><td>通用元数据跨 hook 传递</td></tr><tr><td>data&#x2F;header&#x2F;checksum</td><td>NAT&#x2F;mangle</td><td>修改前确保可写并修正 checksum</td></tr></tbody></table><h3 id="最小调用链"><a href="#最小调用链" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">协议栈进入 Netfilter hook</span><br><span class="line">→ conntrack lookup/attach</span><br><span class="line">→ rule verdict/mangle</span><br><span class="line">→ NAT/confirm</span><br><span class="line">→ ACCEPT 继续或 DROP/QUEUE 等路径消费 skb</span><br></pre></td></tr></table></figure><h2 id="2-tc-ingress-egress-的分类与动作"><a href="#2-tc-ingress-egress-的分类与动作" class="headerlink" title="2. tc ingress&#x2F;egress 的分类与动作"></a>2. tc ingress&#x2F;egress 的分类与动作</h2><p><img src="/blog/img/sk_buff/verdict-ownership.svg" alt="tc ingress&#x2F;egress 的分类与动作"></p><h3 id="结论摘要-1"><a href="#结论摘要-1" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>tc ingress 位于 RX core 中，egress 位于 <code>__dev_queue_xmit()</code> 早期。classifier 读取 skb 字段&#x2F;包数据生成 class result，action 可以修改、drop、redirect、mirror、reclassify 或继续。<code>TC_ACT_STOLEN/REDIRECT/SHOT</code> 都涉及消费语义，但具体由 action&#x2F;core 合同决定。mirred mirror 通常 clone，redirect 通常把原 skb 改 dev 后转交。</p><h3 id="一、入口位置"><a href="#一、入口位置" class="headerlink" title="一、入口位置"></a>一、入口位置</h3><ul><li>ingress：<code>sch_handle_ingress()</code>，<code>kernel-5.10/net/core/dev.c:5000</code>，RX core 调用点约 5241；</li><li>egress：<code>sch_handle_egress()</code>，<code>kernel-5.10/net/core/dev.c:3904</code>，<code>__dev_queue_xmit()</code> 调用点约 4134。</li></ul><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">RX driver/GRO → ingress tc → rx handler/protocol stack</span><br><span class="line">TX IP output → egress tc → TX queue/qdisc → driver</span><br></pre></td></tr></table></figure><p>redirect 后可能重新进入另一个设备的 RX&#x2F;TX 路径，必须有递归&#x2F;loop 防护。</p><h3 id="二、classifier"><a href="#二、classifier" class="headerlink" title="二、classifier"></a>二、classifier</h3><p><code>tcf_classify()</code> 位于 <code>kernel-5.10/net/sched/cls_api.c:1583</code>，内部 <code>__tcf_classify()</code> 在 1529。</p><p>classifier 可依据：</p><ul><li>protocol&#x2F;VLAN；</li><li>IP&#x2F;port；</li><li><code>mark/priority/tc_index</code>；</li><li>skb hash；</li><li>BPF 程序结果；</li><li>flower&#x2F;u32 等 key。</li></ul><p>结果可能选择 classid 或执行 action chain。classful qdisc 也可调用同一分类框架。</p><h3 id="三、action-执行"><a href="#三、action-执行" class="headerlink" title="三、action 执行"></a>三、action 执行</h3><p><code>tcf_action_exec()</code> 位于 <code>kernel-5.10/net/sched/act_api.c:675</code>。action chain 按序执行，每个 action 返回控制码：</p><ul><li><code>TC_ACT_OK</code>：停止 action chain并正常继续；</li><li><code>TC_ACT_PIPE</code>：继续下一个 action；</li><li><code>TC_ACT_RECLASSIFY</code>：回到分类；</li><li><code>TC_ACT_SHOT</code>：丢弃；</li><li><code>TC_ACT_STOLEN</code>：action 接管 skb；</li><li><code>TC_ACT_REDIRECT</code>：重定向并消费当前路径；</li><li>trap 等其它扩展语义。</li></ul><p>常量见 <code>kernel-5.10/include/uapi/linux/pkt_cls.h:60-67</code>。</p><h3 id="四、mirred"><a href="#四、mirred" class="headerlink" title="四、mirred"></a>四、mirred</h3><p><code>tcf_mirred_act()</code> 位于 <code>kernel-5.10/net/sched/act_mirred.c:228</code>。</p><h3 id="mirror"><a href="#mirror" class="headerlink" title="mirror"></a>mirror</h3><p>为了让原路径继续，同时给目标设备一个副本，通常 clone skb：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">original skb → continue</span><br><span class="line">clone skb → target dev</span><br></pre></td></tr></table></figure><p>clone 共享数据，修改前仍需 COW。</p><h3 id="redirect"><a href="#redirect" class="headerlink" title="redirect"></a>redirect</h3><p>原 skb 改变 <code>dev</code> 后交给目标路径，当前路径停止：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">original skb → target dev</span><br><span class="line">current path → consumed</span><br></pre></td></tr></table></figure><p>需要处理 MAC header push&#x2F;pull、ingress&#x2F;egress 方向、redirected&#x2F;from_ingress 标志和 nesting level。</p><h3 id="五、skb-字段"><a href="#五、skb-字段" class="headerlink" title="五、skb 字段"></a>五、skb 字段</h3><ul><li><code>mark</code>：规则&#x2F;action 通用标记；</li><li><code>priority</code>：qdisc&#x2F;class 优先级；</li><li><code>tc_index</code>：tc index；</li><li><code>tc_skip_classify</code>：避免重复分类；</li><li><code>tc_at_ingress</code>：当前方向；</li><li><code>redirected/from_ingress</code>：redirect 状态；</li><li><code>dev</code>：目标设备；</li><li><code>queue_mapping</code>：跨设备后可能重置；</li><li><code>cb</code>：qdisc&#x2F;tc 临时数据；</li><li>checksum&#x2F;header offsets：改包 action 必须同步。</li></ul><h3 id="六、修改-packet-data"><a href="#六、修改-packet-data" class="headerlink" title="六、修改 packet data"></a>六、修改 packet data</h3><p>pedit、BPF、VLAN push&#x2F;pop、tunnel key 等 action 修改 skb 时必须考虑：</p><ol><li>header 是否在线性区；</li><li>cloned head 是否可写；</li><li>headroom&#x2F;tailroom；</li><li>checksum 更新；</li><li>GSO&#x2F;encapsulation metadata；</li><li>旧 data pointer 失效；</li><li>redirect 目标设备 feature 差异。</li></ol><p>修改 GSO skb 的 header 比普通包更敏感，因为一个逻辑修改会影响所有未来 segment。</p><h3 id="七、ingress-与-egress-的-data-视图"><a href="#七、ingress-与-egress-的-data-视图" class="headerlink" title="七、ingress 与 egress 的 data 视图"></a>七、ingress 与 egress 的 data 视图</h3><p>ingress 时 MAC header 可能已 pull，但 <code>skb_mac_header()</code> 仍保存原 L2；某些 action 需要临时 push MAC header 再向 L2 设备 redirect。</p><p>egress 时 data 通常指向待发送 L2 header，但隧道&#x2F;虚拟设备可能仍在构造过程中。action 应使用 header helper，而不是固定假设 data 指向 IP。</p><h3 id="八、所有权表"><a href="#八、所有权表" class="headerlink" title="八、所有权表"></a>八、所有权表</h3><table><thead><tr><th>Action&#x2F;result</th><th>原路径</th><th>skb&#x2F;clone owner</th></tr></thead><tbody><tr><td>OK&#x2F;PIPE</td><td>继续</td><td>tc 返回调用者</td></tr><tr><td>SHOT</td><td>停止</td><td>tc&#x2F;core 释放</td></tr><tr><td>STOLEN</td><td>停止</td><td>action 接管</td></tr><tr><td>REDIRECT</td><td>停止</td><td>目标设备路径</td></tr><tr><td>MIRROR</td><td>原路径继续</td><td>clone 交目标，原 skb 返回</td></tr><tr><td>RECLASSIFY</td><td>重跑</td><td>tc 仍持有，需循环限制</td></tr></tbody></table><h3 id="九、与-qdisc-的关系"><a href="#九、与-qdisc-的关系" class="headerlink" title="九、与 qdisc 的关系"></a>九、与 qdisc 的关系</h3><ul><li>egress tc 通常在选择&#x2F;进入 qdisc 前运行；</li><li>qdisc 内部也可绑定 classifier；</li><li>action 可改变 priority&#x2F;classid&#x2F;queue mapping；</li><li>redirect 可能完全绕开当前设备 qdisc，进入目标设备的新 TX&#x2F;RX 流程；</li><li>qdisc cb 与其它 cb overlay 共享空间，必须遵守当前层所有权。</li></ul><h3 id="十、常见误区"><a href="#十、常见误区" class="headerlink" title="十、常见误区"></a>十、常见误区</h3><ol><li>tc 不只是 qdisc；ingress&#x2F;egress classifier&#x2F;action 可在排队外执行。</li><li>mirror 与 redirect 的关键区别是原路径是否继续及是否 clone。</li><li>STOLEN 后调用者不能再次释放 skb。</li><li>修改包后只改 bytes 不更新 checksum&#x2F;GSO metadata 会产生坏包。</li><li>redirect 后原 queue mapping&#x2F;设备 feature 不一定有效。</li><li>cb 是临时空间，不能把 tc 私有数据假定为跨协议层永久存在。</li></ol><h3 id="字段与所有权统一核对表-1"><a href="#字段与所有权统一核对表-1" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>tc_index/priority/mark</code></td><td>classifier&#x2F;action</td><td>影响分类、策略与后续队列</td></tr><tr><td><code>redirected/dev</code></td><td>mirred&#x2F;redirect</td><td>动作改变设备和再次进入路径</td></tr><tr><td>data&#x2F;header</td><td>pedit、vlan、tunnel action</td><td>修改前 COW，失败时 action 消费或丢弃</td></tr></tbody></table><h3 id="最小调用链-1"><a href="#最小调用链-1" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">ingress/egress hook</span><br><span class="line">→ classifier 读取 skb 元数据</span><br><span class="line">→ action 修改/clone/redirect/drop</span><br><span class="line">→ 重新进入设备或继续 qdisc/协议栈</span><br></pre></td></tr></table></figure><h2 id="3-eBPF-的-struct-sk-buff-受控视图"><a href="#3-eBPF-的-struct-sk-buff-受控视图" class="headerlink" title="3. eBPF 的 struct __sk_buff 受控视图"></a>3. eBPF 的 struct __sk_buff 受控视图</h2><h3 id="结论摘要-2"><a href="#结论摘要-2" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>BPF 程序看到的 <code>struct __sk_buff</code> 是稳定的 UAPI 上下文，不是内核 <code>struct sk_buff</code> 的真实内存布局。verifier&#x2F;JIT 把字段访问转换为底层 skb helper 或偏移访问。direct packet access 使用 <code>data/data_end</code> 边界；任何可能 pull、COW、扩展或重分配 head 的 helper 都会使已验证的数据指针失效，程序必须重新加载和检查。</p><h3 id="一、两种结构不能混淆"><a href="#一、两种结构不能混淆" class="headerlink" title="一、两种结构不能混淆"></a>一、两种结构不能混淆</h3><p>UAPI 定义：<code>kernel-5.10/include/uapi/linux/bpf.h:4088</code>。</p><p><code>struct __sk_buff</code> 暴露：</p><ul><li>len、pkt_type、mark、queue_mapping、protocol、vlan；</li><li>priority、ingress_ifindex、ifindex；</li><li>tc_index、cb[5]；</li><li>hash、tc_classid；</li><li>data&#x2F;data_end；</li><li>family、remote&#x2F;local IP&#x2F;port；</li><li>data_meta、flow_keys、tstamp、wire_len、gso_segs 等。</li></ul><p>它的字段和大小属于 BPF ABI。内核 <code>struct sk_buff</code> 受配置、版本和 KABI 影响，不能直接暴露给程序。</p><h3 id="二、上下文字段转换"><a href="#二、上下文字段转换" class="headerlink" title="二、上下文字段转换"></a>二、上下文字段转换</h3><p>不同 BPF program type 有自己的 <code>convert_ctx_access</code>：</p><ul><li>tc classifier&#x2F;action；</li><li>socket filter；</li><li>cgroup skb；</li><li>sk_skb&#x2F;stream parser&#x2F;verdict；</li><li>lwt 等。</li></ul><p>verifier 根据字段、读写方向和 program type：</p><ol><li>判断是否允许访问；</li><li>生成读取底层 skb 字段或调用 helper 的指令；</li><li>对写操作增加范围&#x2F;语义检查；</li><li>某些字段返回派生值，而非一一映射。</li></ol><p>因此相同 <code>__sk_buff</code> 字段在不同 program type 可能只读、可写或不可用。</p><h3 id="三、direct-packet-access"><a href="#三、direct-packet-access" class="headerlink" title="三、direct packet access"></a>三、direct packet access</h3><p>程序常见模式：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> *data = (<span class="type">void</span> *)(<span class="type">long</span>)skb-&gt;data;</span><br><span class="line"><span class="type">void</span> *data_end = (<span class="type">void</span> *)(<span class="type">long</span>)skb-&gt;data_end;</span><br><span class="line"><span class="keyword">if</span> (data + <span class="keyword">sizeof</span>(<span class="keyword">struct</span> ethhdr) &gt; data_end)</span><br><span class="line">    <span class="keyword">return</span> ...;</span><br></pre></td></tr></table></figure><p>verifier 追踪 data 指针和边界证明。<code>data_end</code> 通常只覆盖当前可直接线性访问区域，不保证 frags 全部连续。</p><p>协议 header 访问前必须逐层边界检查，不能因为 <code>skb-&gt;len</code> 足够就直接解引用 linear data 之外的字节。</p><h3 id="四、bpf-skb-pull-data"><a href="#四、bpf-skb-pull-data" class="headerlink" title="四、bpf_skb_pull_data"></a>四、bpf_skb_pull_data</h3><p>实现：<code>kernel-5.10/net/core/filter.c:1830</code>。</p><p>用途：</p><ul><li>确保前 len 字节在线性区；</li><li>必要时调用 pull&#x2F;linearize；</li><li>对 cloned skb 做 unclone，使后续 direct write 安全。</li></ul><p>调用后所有旧的 packet pointers 都被 verifier 视为失效，必须重新读取 data&#x2F;data_end 并重新边界检查。</p><h3 id="五、修改-helper"><a href="#五、修改-helper" class="headerlink" title="五、修改 helper"></a>五、修改 helper</h3><h3 id="bpf-skb-store-bytes"><a href="#bpf-skb-store-bytes" class="headerlink" title="bpf_skb_store_bytes"></a>bpf_skb_store_bytes</h3><p>实现：<code>kernel-5.10/net/core/filter.c:1684</code>。在指定逻辑 offset 写数据，可选择重新计算&#x2F;使 checksum 失效。需要处理 non-linear、clone 和边界。</p><h3 id="bpf-skb-change-head-change-tail"><a href="#bpf-skb-change-head-change-tail" class="headerlink" title="bpf_skb_change_head &#x2F; change_tail"></a>bpf_skb_change_head &#x2F; change_tail</h3><p>改变 headroom 或 packet length，可能重新分配 head、移动 data、trim&#x2F;expand。调用后 packet pointers 全失效。</p><h3 id="bpf-skb-adjust-room"><a href="#bpf-skb-adjust-room" class="headerlink" title="bpf_skb_adjust_room"></a>bpf_skb_adjust_room</h3><p>UAPI 文档约 <code>kernel-5.10/include/uapi/linux/bpf.h:1712</code>。可在 MAC&#x2F;NET 等位置增减空间，用于封装&#x2F;解封装；会更新或要求调用者更新：</p><ul><li>header offsets；</li><li>checksum；</li><li>GSO&#x2F;encapsulation metadata；</li><li>protocol 相关内容。</li></ul><h3 id="六、redirect-与-clone-redirect"><a href="#六、redirect-与-clone-redirect" class="headerlink" title="六、redirect 与 clone_redirect"></a>六、redirect 与 clone_redirect</h3><h3 id="bpf-redirect"><a href="#bpf-redirect" class="headerlink" title="bpf_redirect"></a>bpf_redirect</h3><p>程序记录目标 ifindex&#x2F;map，返回 redirect action；实际执行阶段把原 skb 交给目标路径。当前路径不再继续。</p><h3 id="bpf-clone-redirect"><a href="#bpf-clone-redirect" class="headerlink" title="bpf_clone_redirect"></a>bpf_clone_redirect</h3><p>实现：<code>kernel-5.10/net/core/filter.c:2430</code>。先 clone skb，把 clone 发送到目标，原 skb 可继续在当前 BPF 程序&#x2F;路径处理。代价包括新 shell、引用计数、目标发送路径。</p><p>clone 共享数据，因此目标路径或原路径修改 header 前仍需 COW。</p><h3 id="七、checksum-helper"><a href="#七、checksum-helper" class="headerlink" title="七、checksum helper"></a>七、checksum helper</h3><p>BPF 修改地址、端口或 payload 后可使用：</p><ul><li><code>bpf_l3_csum_replace()</code>；</li><li><code>bpf_l4_csum_replace()</code>；</li><li><code>bpf_csum_diff()</code>；</li><li>store_bytes flags。</li></ul><p>仅修改字节不维护 skb <code>ip_summed</code>、checksum 字段和 pseudo header，会使硬件 offload 或接收验证产生错误。</p><h3 id="八、cb-的差异"><a href="#八、cb-的差异" class="headerlink" title="八、cb 的差异"></a>八、cb 的差异</h3><p><code>struct __sk_buff.cb[5]</code> 对 BPF 暴露 20 字节，不等于内核完整 <code>skb-&gt;cb[48]</code>。可用范围和跨 tail call&#x2F;clone&#x2F;层级的持久性受 program type 约束。</p><p>内核其它层仍会复用完整 cb，所以 BPF 元数据要跨 hook 传递时通常更适合 mark、priority、BPF metadata、maps 或明确的协议字段，而不是假定 cb 永久保存。</p><h3 id="九、不同-program-type-的-skb-语义"><a href="#九、不同-program-type-的-skb-语义" class="headerlink" title="九、不同 program type 的 skb 语义"></a>九、不同 program type 的 skb 语义</h3><table><thead><tr><th>Program type</th><th>典型位置</th><th>主要动作</th></tr></thead><tbody><tr><td>socket filter</td><td>socket 入队前</td><td>accept&#x2F;truncate&#x2F;drop</td></tr><tr><td>tc cls&#x2F;act</td><td>ingress&#x2F;egress</td><td>classify&#x2F;edit&#x2F;redirect</td></tr><tr><td>cgroup_skb</td><td>socket&#x2F;cgroup 边界</td><td>policy&#x2F;mark&#x2F;drop</td></tr><tr><td>sk_skb</td><td>sockmap&#x2F;skmsg 路径</td><td>parser&#x2F;verdict&#x2F;redirect</td></tr><tr><td>lwt</td><td>route&#x2F;lightweight tunnel</td><td>encap&#x2F;redirect</td></tr></tbody></table><p>相同 helper 不一定在所有类型可用，verifier 根据 <code>bpf_func_proto</code> 限制。</p><h3 id="十、所有权规则"><a href="#十、所有权规则" class="headerlink" title="十、所有权规则"></a>十、所有权规则</h3><p>BPF 程序本身通常借用 skb 上下文，不直接持有 C 引用。返回 action 决定由框架：</p><ul><li>继续；</li><li>drop&#x2F;free；</li><li>redirect&#x2F;consume；</li><li>clone 后双路径；</li><li>socket queue。</li></ul><p>程序不能保存 skb 指针到 map 跨调用使用。</p><h3 id="十一、常见误区-1"><a href="#十一、常见误区-1" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li><code>__sk_buff</code> 不是可用 BTF 直接解释的内核 skb 布局副本。</li><li><code>len</code> 足够不代表 direct data 区连续足够。</li><li>pull&#x2F;adjust&#x2F;change helper 后旧 data 指针必须重新验证。</li><li>clone_redirect 与 redirect 的所有权和成本不同。</li><li>修改 header 后必须维护 checksum&#x2F;GSO&#x2F;header metadata。</li><li>BPF cb 只暴露内核 cb 的一部分，且有生命周期限制。</li></ol><h3 id="字段与所有权统一核对表-2"><a href="#字段与所有权统一核对表-2" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>__sk_buff</code> 字段</td><td>verifier 转译</td><td>程序看到受控上下文，不直接暴露全部内核结构</td></tr><tr><td>data&#x2F;data_end</td><td>direct packet access</td><td>helper 或写操作后需重新验证指针</td></tr><tr><td>mark&#x2F;priority&#x2F;cb</td><td>BPF helper</td><td>修改效果和可用 helper 由 attach type 决定</td></tr></tbody></table><h3 id="最小调用链-2"><a href="#最小调用链-2" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">attach point 生成 __sk_buff 上下文</span><br><span class="line">→ verifier 将字段访问转换到真实 skb/helper</span><br><span class="line">→ 程序读写 packet/metadata</span><br><span class="line">→ verdict 决定继续、redirect 或 drop</span><br></pre></td></tr></table></figure><h2 id="4-XDP-与-sk-buff-的边界"><a href="#4-XDP-与-sk-buff-的边界" class="headerlink" title="4. XDP 与 sk_buff 的边界"></a>4. XDP 与 sk_buff 的边界</h2><p><img src="/blog/img/sk_buff/xdp-skb-boundary.svg" alt="XDP 与 sk_buff 的边界"></p><h3 id="结论摘要-3"><a href="#结论摘要-3" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>native XDP 在驱动 RX buffer 上运行，发生在 skb 分配之前；generic XDP 在 RX core 中运行，操作已经存在的 skb。<code>xdp_buff</code> 是驱动&#x2F;NAPI 周期中的可变 buffer 视图，<code>xdp_frame</code> 是可跨 redirect&#x2F;TX 异步持有的固定 frame。XDP_PASS 后驱动才构造 skb；redirect&#x2F;TX&#x2F;drop 必须正确转移或归还 page_pool buffer。</p><h3 id="一、native-XDP-位置"><a href="#一、native-XDP-位置" class="headerlink" title="一、native XDP 位置"></a>一、native XDP 位置</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">NIC DMA → RX page/xdp_buff → BPF_PROG_RUN_XDP</span><br><span class="line">  ├─ PASS → build_skb/napi_alloc_skb → GRO/stack</span><br><span class="line">  ├─ DROP/ABORTED → recycle page</span><br><span class="line">  ├─ TX → same device TX path</span><br><span class="line">  └─ REDIRECT → xdp_frame/target → flush</span><br></pre></td></tr></table></figure><p>它省去了被丢弃&#x2F;重定向包的 skb shell 分配、初始化和协议栈成本。</p><h3 id="二、generic-XDP"><a href="#二、generic-XDP" class="headerlink" title="二、generic XDP"></a>二、generic XDP</h3><p><code>do_xdp_generic()</code> 位于 <code>kernel-5.10/net/core/dev.c:4784</code>，在 <code>__netif_receive_skb_core()</code> 中调用。此时：</p><ul><li>skb 已分配；</li><li>packet 可能非线性；</li><li>helper 需要围绕 skb 构造 XDP 视图；</li><li>性能优势小于 native；</li><li>语义用于兼容没有驱动 native XDP 的设备。</li></ul><p>不能用 generic XDP 的成功运行推断驱动支持 native XDP 或 XDP_TX 零拷贝。</p><h3 id="三、xdp-buff"><a href="#三、xdp-buff" class="headerlink" title="三、xdp_buff"></a>三、xdp_buff</h3><p>定义于 <code>include/net/xdp.h</code>，主要字段：</p><ul><li><code>data</code>；</li><li><code>data_end</code>；</li><li><code>data_meta</code>；</li><li><code>data_hard_start</code>；</li><li><code>rxq</code>；</li><li>frame size&#x2F;flags（版本相关）。</li></ul><p>它描述当前 RX buffer 的窗口，不负责像 skb 一样携带 socket、dst、Netfilter、qdisc 等丰富元数据。</p><p>BPF 可用 <code>bpf_xdp_adjust_head/tail/meta</code> 改变视图，但必须保持在驱动提供的 hard start&#x2F;frame 范围内。</p><h3 id="四、xdp-frame"><a href="#四、xdp-frame" class="headerlink" title="四、xdp_frame"></a>四、xdp_frame</h3><p>redirect 或 XDP_TX 需要在当前驱动 poll 之外持有数据，不能长期持有栈上的 <code>xdp_buff</code> 描述。转换为 <code>xdp_frame</code> 后保存：</p><ul><li>data 相对 frame 起始；</li><li>len；</li><li>headroom；</li><li>memory model；</li><li>frame size&#x2F;metadata。</li></ul><p>目标完成后通过 <code>xdp_return_frame()</code> 等根据 memory model 归还 page&#x2F;page_pool。</p><h3 id="五、XDP-PASS-到-skb"><a href="#五、XDP-PASS-到-skb" class="headerlink" title="五、XDP_PASS 到 skb"></a>五、XDP_PASS 到 skb</h3><p>驱动常见策略：</p><ol><li>XDP 程序可能调整 data&#x2F;meta；</li><li>根据调整后的范围构造 skb；</li><li><code>build_skb()</code> 包装原 page，或分配 skb 后 copy&#x2F;attach frag；</li><li>设置 headroom、len、protocol、checksum、hash、queue；</li><li>交给 GRO。</li></ol><p>若 build_skb 直接复用 page，skb free 必须知道如何返回内存池；若 copy，原 page 可立即 recycle。</p><h3 id="六、redirect"><a href="#六、redirect" class="headerlink" title="六、redirect"></a>六、redirect</h3><p><code>xdp_do_redirect()</code> 查找 devmap&#x2F;cpumap&#x2F;xskmap 等目标，把 frame&#x2F;buffer 入目标批次。poll 尾部 <code>xdp_do_flush()</code> 才批量提交。</p><p>所有权时间线：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">RX driver owns buffer</span><br><span class="line">→ redirect accepted: redirect subsystem owns</span><br><span class="line">→ target device/CPU/AF_XDP owns</span><br><span class="line">→ completion/drop: memory model return callback</span><br></pre></td></tr></table></figure><p>redirect 失败时当前路径必须回收，不能既交目标又 recycle。</p><h3 id="七、XDP-TX"><a href="#七、XDP-TX" class="headerlink" title="七、XDP_TX"></a>七、XDP_TX</h3><p>XDP_TX 通常把收到的 frame 从同设备发出：</p><ul><li>交换&#x2F;修改 MAC；</li><li>映射&#x2F;复用 RX page；</li><li>放入专用 XDP TX ring；</li><li>completion 后归还 page_pool。</li></ul><p>它不是普通 skb <code>ndo_start_xmit</code>，通常没有 qdisc&#x2F;socket owner&#x2F;truesize 语义。</p><h3 id="八、multi-buffer-XDP"><a href="#八、multi-buffer-XDP" class="headerlink" title="八、multi-buffer XDP"></a>八、multi-buffer XDP</h3><p>Linux 5.10 的 XDP 主要假设单 buffer。Linux 6.1 提供：</p><ul><li><code>xdp_buff_has_frags()</code>：<code>kernel-6.1/include/net/xdp.h:88</code>；</li><li><code>xdp_update_skb_shared_info()</code>：<code>kernel-6.1/include/net/xdp.h:223</code>。</li></ul><p>多 buffer packet 在 XDP PASS 转 skb 时，需要把额外 page fragments 写入 <code>skb_shared_info</code>，同步：</p><ul><li><code>nr_frags</code>；</li><li><code>len/data_len</code>；</li><li><code>truesize</code>；</li><li>frag page ownership。</li></ul><p>BPF 程序对 multi-buffer 数据访问能力也受版本&#x2F;program support 限制。</p><h3 id="九、RK3588-stmmac-边界"><a href="#九、RK3588-stmmac-边界" class="headerlink" title="九、RK3588 stmmac 边界"></a>九、RK3588 stmmac 边界</h3><p>本地 Firefly BSP stmmac RX 笔记显示传统 skb&#x2F;page_pool 构造路径。是否支持 native XDP 必须检查：</p><ul><li><code>ndo_bpf</code>&#x2F;XDP setup；</li><li>RX poll 中 <code>bpf_prog</code> 执行；</li><li><code>xdp_rxq_info</code> 注册；</li><li>XDP_TX ring；</li><li>redirect flush；</li><li>page_pool memory model。</li></ul><p>如果这些闭环缺失，只能使用 generic XDP，不能因为内核启用 CONFIG_BPF 就声称 native 支持。</p><h3 id="十、XDP-与-skb-功能边界"><a href="#十、XDP-与-skb-功能边界" class="headerlink" title="十、XDP 与 skb 功能边界"></a>十、XDP 与 skb 功能边界</h3><table><thead><tr><th>能力</th><th>XDP</th><th>skb stack</th></tr></thead><tbody><tr><td>时机</td><td>驱动 RX 早期</td><td>构造 skb 后</td></tr><tr><td>元数据</td><td>精简 buffer&#x2F;rxq&#x2F;meta</td><td>socket&#x2F;dst&#x2F;nfct&#x2F;qdisc&#x2F;offload 等</td></tr><tr><td>排队</td><td>devmap&#x2F;cpumap&#x2F;xsk&#x2F;XDP TX</td><td>backlog&#x2F;socket&#x2F;qdisc&#x2F;TCP 等</td></tr><tr><td>典型目标</td><td>drop&#x2F;redirect&#x2F;fast TX</td><td>完整协议栈与 socket</td></tr><tr><td>内存</td><td>page_pool&#x2F;driver model</td><td>slab head + page refs</td></tr></tbody></table><h3 id="十一、常见误区-2"><a href="#十一、常见误区-2" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li>XDP 不是“更快的 skb hook”，native XDP 根本还没有 skb。</li><li>generic XDP 已支付 skb 分配成本。</li><li>xdp_buff 不能异步长期持有，需转 frame。</li><li>redirect 成功后驱动不能再次 recycle page。</li><li>XDP_TX 不经过普通 qdisc&#x2F;ndo_start_xmit 语义。</li><li>5.10 单 buffer 结论不能直接推广到 6.1 multi-buffer。</li></ol><h3 id="字段与所有权统一核对表-3"><a href="#字段与所有权统一核对表-3" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>xdp_buff</code> data pointers</td><td>驱动 RX&#x2F;XDP</td><td>动作完成前仍是 RX buffer 所有权</td></tr><tr><td><code>xdp_frame</code></td><td>redirect&#x2F;TX</td><td>转换后由目标设备或 map 完成路径持有</td></tr><tr><td>skb&#x2F;shared_info</td><td>PASS&#x2F;build_skb</td><td>跨入 skb 世界后遵守 skb&#x2F;page_pool 回收语义</td></tr></tbody></table><h3 id="最小调用链-3"><a href="#最小调用链-3" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">driver RX buffer/xdp_buff</span><br><span class="line">→ BPF XDP verdict</span><br><span class="line">→ PASS 构造 skb，TX/REDIRECT 转换 xdp_frame，DROP 直接回收</span><br><span class="line">→ 对应 completion 释放</span><br></pre></td></tr></table></figure><h2 id="源码阅读路线"><a href="#源码阅读路线" class="headerlink" title="源码阅读路线"></a>源码阅读路线</h2><p>建议不要从 <code>struct sk_buff</code> 声明开始逐字段背诵，而应使用下面的阅读顺序：</p><ol><li>先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造；</li><li>沿成功主线记录 <code>data/len/data_len</code>、header offset、checksum、hash、queue mapping 等字段变化；</li><li>再回到每个失败分支，确认 skb、page、DMA、socket 和记账引用是否回滚；</li><li>最后对照 5.10、6.1 或 BSP 的同名 symbol，区分语义变化、性能优化和 vendor backport。</li></ol><h2 id="本篇总结"><a href="#本篇总结" class="headerlink" title="本篇总结"></a>本篇总结</h2><ol><li>Hook 框架的核心不是“调用规则”，而是 verdict 如何改变控制流和 skb 所有权。</li><li>Conntrack、dst、socket、BPF metadata 都有独立于 skb shell 的引用语义。</li><li><code>struct __sk_buff</code> 是 verifier 管理的 ABI 视图，不等于程序可任意解引用内核 <code>struct sk_buff</code>。</li><li>XDP 在 skb 分配前处理 RX buffer；只有 XDP_PASS 才通常进入完整 skb 生命周期。</li></ol><p><code>sk_buff</code> 的难点从来不只是字段多，而是<strong>同一个对象不断改变数据视图、元数据状态和持有者</strong>。只要把“布局、字段、所有权、释放”四条线同时画出来，绝大多数网络栈源码都会从零散函数变成可推导的生命周期。</p><h2 id="系列目录"><a href="#系列目录" class="headerlink" title="系列目录"></a>系列目录</h2><ol><li>Linux sk_buff 深度解析（一）：数据结构、四指针与核心字段</li><li>Linux sk_buff 深度解析（二）：非线性数据、引用计数与生命周期</li><li>Linux sk_buff 深度解析（三）：从网卡 DMA 到 Socket 的 RX 路径</li><li>Linux sk_buff 深度解析（四）：从 sendmsg 到 TX completion</li><li>Linux sk_buff 深度解析（五）：GRO、GSO、Checksum 与多核流调度</li><li>Linux sk_buff 深度解析（六）：Netfilter、tc、eBPF 与 XDP（本文）</li><li>Linux sk_buff 深度解析（七）：VLAN、隧道、分片与重组</li><li>Linux sk_buff 深度解析（八）：Socket 内存、零拷贝、丢包与版本演进</li></ol>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/7e8eec7306f9/</id>
    <link href="https://2xpang.cn/blog/posts/7e8eec7306f9/"/>
    <published>2026-07-14T22:00:00.000Z</published>
    <summary>从 Hook、verdict 和所有权出发，比较 Netfilter/Conntrack/NAT、tc action、eBPF __sk_buff 视图，以及 XDP 进入 skb 世界前后的边界。</summary>
    <title>Linux sk_buff 深度解析（六）：Netfilter、tc、eBPF 与 XDP</title>
    <updated>2026-07-14T22:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="Linux" scheme="https://2xpang.cn/blog/tags/Linux/"/>
    <category term="网络子系统" scheme="https://2xpang.cn/blog/tags/%E7%BD%91%E7%BB%9C%E5%AD%90%E7%B3%BB%E7%BB%9F/"/>
    <category term="内核源码" scheme="https://2xpang.cn/blog/tags/%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81/"/>
    <category term="sk_buff" scheme="https://2xpang.cn/blog/tags/sk-buff/"/>
    <category term="Linux-5.10" scheme="https://2xpang.cn/blog/tags/Linux-5-10/"/>
    <category term="series:内核网络栈" scheme="https://2xpang.cn/blog/tags/series-%E5%86%85%E6%A0%B8%E7%BD%91%E7%BB%9C%E6%A0%88/"/>
    <category term="GRO" scheme="https://2xpang.cn/blog/tags/GRO/"/>
    <category term="GSO" scheme="https://2xpang.cn/blog/tags/GSO/"/>
    <category term="Checksum" scheme="https://2xpang.cn/blog/tags/Checksum/"/>
    <content>
      <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p><code>sk_buff</code> 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”，而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的<strong>控制对象和所有权载体</strong>。</p><p>把 GRO&#x2F;GSO&#x2F;TSO、checksum 四态以及 RSS&#x2F;RPS&#x2F;RFS&#x2F;XPS 放在同一性能模型下，解释 skb 元数据如何替代重复解析和拷贝。</p><p>本文以本地 <strong>Linux 5.10.209</strong> 源码为主线；涉及演进时对照 <strong>Linux 6.1.99</strong>，涉及网卡驱动时结合 <strong>Firefly RK3588 SDK Linux 5.10.198</strong>。本文是源码分析，不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。</p><p><img src="/blog/img/sk_buff/gro-gso-symmetry.svg" alt="Linux sk_buff 深度解析（五）：GRO、GSO、Checksum 与多核流调度"></p><h2 id="阅读前先建立四个问题"><a href="#阅读前先建立四个问题" class="headerlink" title="阅读前先建立四个问题"></a>阅读前先建立四个问题</h2><p>阅读任何 skb 代码路径，都建议反复追问：</p><ol><li>当前 <code>skb-&gt;data</code> 指向哪一层协议头，linear 与 non-linear 数据分别在哪里？</li><li>哪些字段刚被修改，下一层为什么依赖这些字段？</li><li>当前谁持有 skb shell、head、frag page 以及 socket&#x2F;dst&#x2F;conntrack 等外部引用？</li><li>成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb？</li></ol><h2 id="本篇核心结论"><a href="#本篇核心结论" class="headerlink" title="本篇核心结论"></a>本篇核心结论</h2><ul><li>GRO 在 RX 聚合，GSO&#x2F;TSO 在 TX 延迟分段，二者通过 shared info 元数据衔接。</li><li><code>ip_summed</code> 表达 checksum 的责任归属，而不是简单的“正确&#x2F;错误”。</li><li>hash、CPU 和 queue mapping 把包内容映射为处理位置与发送队列。</li><li>offload 的本质是延后工作或把工作交给硬件，但软件始终要维护可验证的语义。</li></ul><h2 id="1-GRO、GSO-与-TSO-的对称关系"><a href="#1-GRO、GSO-与-TSO-的对称关系" class="headerlink" title="1. GRO、GSO 与 TSO 的对称关系"></a>1. GRO、GSO 与 TSO 的对称关系</h2><h3 id="结论摘要"><a href="#结论摘要" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>GRO 在 RX 方向把同流小包聚合成大 skb，减少后续协议栈每包开销；GSO 在 TX 方向用一个大 skb 描述多个线速报文，把分段推迟到软件出口或网卡；TSO 是 TCP GSO 的硬件执行形式。三者都依赖 skb 的非线性布局、header offset、checksum 状态和 <code>skb_shared_info</code> 中的 GSO 元数据。</p><h3 id="一、收发方向的对称关系"><a href="#一、收发方向的对称关系" class="headerlink" title="一、收发方向的对称关系"></a>一、收发方向的对称关系</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">RX: many wire packets → GRO → one large skb → protocol stack</span><br><span class="line">TX: one large skb → GSO/TSO → many wire packets</span><br></pre></td></tr></table></figure><p>它们优化的不是字节复制本身，而是把固定 per-packet 成本摊薄：协议解析、路由、Netfilter、socket queue、qdisc、descriptor 等。</p><h3 id="二、GRO-入口"><a href="#二、GRO-入口" class="headerlink" title="二、GRO 入口"></a>二、GRO 入口</h3><p>Linux 5.10：</p><ul><li><code>gro_list_prepare()</code>：<code>kernel-5.10/net/core/dev.c:5877</code>；</li><li><code>dev_gro_receive()</code>：<code>kernel-5.10/net/core/dev.c:5991</code>；</li><li><code>napi_gro_receive()</code>：<code>kernel-5.10/net/core/dev.c:6166</code>。</li></ul><p>驱动交入 skb 后，GRO 根据设备、协议、hash、header 和协议回调判断：</p><ul><li>是否与已有 skb 同流；</li><li>当前包能否 merge；</li><li>旧 flow 是否必须 flush；</li><li>当前 skb 是 held、merged、normal 还是 drop。</li></ul><h3 id="三、napi-gro-cb"><a href="#三、napi-gro-cb" class="headerlink" title="三、napi_gro_cb"></a>三、napi_gro_cb</h3><p><code>NAPI_GRO_CB(skb)</code> overlay 在 <code>skb-&gt;cb</code> 上，保存：</p><ul><li><code>same_flow</code>；</li><li><code>flush</code>；</li><li><code>free</code>；</li><li><code>count</code>；</li><li><code>age</code>；</li><li><code>last</code>；</li><li>checksum&#x2F;GSO 相关临时状态。</li></ul><p>GRO 期间 cb 属于 GRO；交给下一层后不能再假设这些字段有效。</p><h3 id="四、GRO-合并后的数据形态"><a href="#四、GRO-合并后的数据形态" class="headerlink" title="四、GRO 合并后的数据形态"></a>四、GRO 合并后的数据形态</h3><p>GRO 不只有一种布局：</p><ol><li>把新数据页合入 head skb 的 <code>frags[]</code>；</li><li>使用 <code>frag_list</code> 串联完整 skb；</li><li>调整 <code>len/data_len/truesize</code>；</li><li>设置 GSO size&#x2F;segs&#x2F;type，让后续知道逻辑分段边界。</li></ol><p>是否使用 frags 或 frag_list 取决于协议回调、输入 skb 布局、frag 数量、feature 和实现路径。不能把“GRO 后一定是 frag_list”当作通用结论。</p><h3 id="五、flow-匹配与-flush"><a href="#五、flow-匹配与-flush" class="headerlink" title="五、flow 匹配与 flush"></a>五、flow 匹配与 flush</h3><p>典型匹配条件包括：</p><ul><li>同一接收设备&#x2F;虚拟设备语义；</li><li>L2&#x2F;L3&#x2F;L4 header 与 flow key 一致；</li><li>TCP seq 连续、flag 合法；</li><li>checksum 状态兼容；</li><li>不超过 frag&#x2F;GSO&#x2F;长度限制；</li><li>没有要求立即 flush 的协议事件。</li></ul><p>TCP FIN&#x2F;RST、序列不连续、header option 差异、超限、时间&#x2F;批次结束等会触发 flush。</p><h3 id="六、GSO-元数据"><a href="#六、GSO-元数据" class="headerlink" title="六、GSO 元数据"></a>六、GSO 元数据</h3><p>位于 <code>skb_shared_info</code>：</p><ul><li><code>gso_size</code>：每个 segment 的 payload&#x2F;MSS 规模；</li><li><code>gso_segs</code>：预计 segment 数；</li><li><code>gso_type</code>：TCPv4&#x2F;TCPv6&#x2F;UDP tunnel&#x2F;partial&#x2F;fraglist 等类型。</li></ul><p><code>skb_is_gso()</code> 通常依据 <code>gso_size</code>。大 skb 仍保持完整逻辑 <code>len</code>，网卡或软件分段器根据 header offset 和 GSO metadata 产生各 segment。</p><h3 id="七、软件-GSO"><a href="#七、软件-GSO" class="headerlink" title="七、软件 GSO"></a>七、软件 GSO</h3><p>关键路径：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">validate_xmit_skb()</span><br><span class="line">  → skb_gso_segment()/__skb_gso_segment()</span><br><span class="line">  → protocol gso_segment callback</span><br><span class="line">      → tcp_gso_segment()</span><br><span class="line">      → skb_segment()</span><br></pre></td></tr></table></figure><p>位置：</p><ul><li><code>__skb_gso_segment()</code>：<code>kernel-5.10/net/core/dev.c:3391</code>；</li><li><code>validate_xmit_skb()</code>：<code>kernel-5.10/net/core/dev.c:3659</code>；</li><li><code>tcp_gso_segment()</code>：<code>kernel-5.10/net/ipv4/tcp_offload.c:54</code>；</li><li><code>skb_segment()</code>：<code>kernel-5.10/net/core/skbuff.c:3819</code>。</li></ul><p><code>skb_segment()</code> 尽量 clone&#x2F;share page 数据并为每个 segment 构造独立 header，避免完整 payload copy。segment 链通过 skb <code>next</code> 返回。</p><h3 id="八、TSO"><a href="#八、TSO" class="headerlink" title="八、TSO"></a>八、TSO</h3><p>如果设备 feature 支持对应 GSO type，网络栈可把大 skb 原样交给网卡：</p><ul><li>descriptor 提供 MSS、header 长度、checksum 等；</li><li>网卡生成多个 wire packet；</li><li>驱动只管理一个逻辑 skb 的 DMA&#x2F;completion。</li></ul><p>若 feature 不支持，<code>validate_xmit_skb()</code> 先软件 GSO，再逐个 segment 发送。</p><h3 id="九、checksum-关系"><a href="#九、checksum-关系" class="headerlink" title="九、checksum 关系"></a>九、checksum 关系</h3><p>GSO skb 通常是 <code>CHECKSUM_PARTIAL</code>：header 中 checksum 字段留给软件分段器或网卡基于每个 segment 计算。GRO 输入 checksum 状态必须正确传播，否则聚合后一次错误标记会影响多个逻辑包。</p><h3 id="十、5-10-与-6-1-GRO-组织差异"><a href="#十、5-10-与-6-1-GRO-组织差异" class="headerlink" title="十、5.10 与 6.1 GRO 组织差异"></a>十、5.10 与 6.1 GRO 组织差异</h3><p>5.10 GRO 主实现位于 <code>net/core/dev.c</code>，NAPI 使用相对简单的 GRO list。6.1 把实现拆到 <code>net/core/gro.c</code>，并在 <code>struct napi_struct</code> 中使用：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">gro_list</span> <span class="title">gro_hash</span>[<span class="title">GRO_HASH_BUCKETS</span>];</span></span><br></pre></td></tr></table></figure><p><code>GRO_HASH_BUCKETS</code> 在 <code>kernel-6.1/include/linux/netdevice.h:339</code> 为 8。先按 hash 分桶减少同一 list 的线性比较，改善多 flow 批次成本。</p><h3 id="十一、关键不变量"><a href="#十一、关键不变量" class="headerlink" title="十一、关键不变量"></a>十一、关键不变量</h3><ol><li>GSO type 必须与外层&#x2F;内层协议和 feature 匹配。</li><li>header offset 必须能定位每个 segment 需要复制&#x2F;修改的 header。</li><li><code>gso_size/gso_segs/len</code> 必须一致或能由 helper 修正。</li><li>frag 数不能超过上限。</li><li>segment&#x2F;merge 过程必须维护 page ref、truesize 和 checksum 状态。</li><li>GRO merge 消费当前 skb 后，调用者不能继续访问。</li></ol><h3 id="十二、常见误区"><a href="#十二、常见误区" class="headerlink" title="十二、常见误区"></a>十二、常见误区</h3><ol><li>GRO 不是 LRO；GRO 是协议栈可控的软件聚合，保持更严格语义。</li><li>GSO 不等于硬件 TSO；不支持时可软件分段。</li><li>大 skb 不代表线上存在一个超 MTU Ethernet frame。</li><li>GRO 后不一定全部 linear，也不一定总是 frag_list。</li><li><code>gso_segs</code> 可能需要重新计算，不能在所有路径视作绝对可信。</li><li>checksum offload 是 GSO 正确性的组成部分，不是独立附加功能。</li></ol><h3 id="字段与所有权统一核对表"><a href="#字段与所有权统一核对表" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td>GRO cb&#x2F;count</td><td>GRO receive&#x2F;complete</td><td>聚合链持有成员或合并后的 skb</td></tr><tr><td><code>gso_size/gso_segs/type</code></td><td>协议 GSO</td><td>segment 前由大 skb 携带分段契约</td></tr><tr><td>frags&#x2F;frag_list</td><td>聚合与分段</td><td>page&#x2F;子 skb 引用随 segment 转移</td></tr></tbody></table><h3 id="最小调用链"><a href="#最小调用链" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">小 skb 进入 GRO</span><br><span class="line">→ flow 匹配并聚合</span><br><span class="line">→ GRO complete 写 GSO 元数据</span><br><span class="line">→ output/GSO segment</span><br><span class="line">→ software 或 TSO 发送各 segment</span><br></pre></td></tr></table></figure><h2 id="2-Checksum-不是布尔值：四态责任模型"><a href="#2-Checksum-不是布尔值：四态责任模型" class="headerlink" title="2. Checksum 不是布尔值：四态责任模型"></a>2. Checksum 不是布尔值：四态责任模型</h2><p><img src="/blog/img/sk_buff/checksum-state.svg" alt="Checksum 不是布尔值：四态责任模型"></p><h3 id="结论摘要-1"><a href="#结论摘要-1" class="headerlink" title="结论摘要"></a>结论摘要</h3><p><code>ip_summed</code> 不是“checksum 是否正确”的布尔值，而是网络栈与驱动之间的责任状态机。RX 常见 NONE、UNNECESSARY、COMPLETE；TX 常见 PARTIAL。<code>csum</code> 与 <code>csum_start/csum_offset</code> 通过 union 复用，具体解释由 <code>ip_summed</code> 决定。GSO、隧道和多层 checksum 又通过 <code>csum_level</code>、inner header 和 gso type 扩展该模型。</p><h3 id="一、四个状态"><a href="#一、四个状态" class="headerlink" title="一、四个状态"></a>一、四个状态</h3><p>定义：<code>kernel-5.10/include/linux/skbuff.h:221-225</code>。</p><h3 id="CHECKSUM-NONE"><a href="#CHECKSUM-NONE" class="headerlink" title="CHECKSUM_NONE"></a>CHECKSUM_NONE</h3><p>网络栈不能依赖硬件 checksum 结果。可能是：</p><ul><li>驱动未验证；</li><li>协议不支持 offload；</li><li>硬件报告失败&#x2F;未知；</li><li>状态被修改后失效。</li></ul><p>L4 通常需要软件验证。</p><h3 id="CHECKSUM-UNNECESSARY"><a href="#CHECKSUM-UNNECESSARY" class="headerlink" title="CHECKSUM_UNNECESSARY"></a>CHECKSUM_UNNECESSARY</h3><p>驱动或上层已经确认协议 checksum 有效，协议栈无需再次计算。它不表示 <code>skb-&gt;csum</code> 保存了完整 checksum 数值，只表示验证责任已经完成。</p><h3 id="CHECKSUM-COMPLETE"><a href="#CHECKSUM-COMPLETE" class="headerlink" title="CHECKSUM_COMPLETE"></a>CHECKSUM_COMPLETE</h3><p>RX 硬件提供了从某范围计算的完整 checksum 值，保存在 <code>skb-&gt;csum</code>。协议层可结合 pseudo header 验证或在 pull&#x2F;encapsulation 时增量调整。它与 UNNECESSARY 的区别是“有可继续使用的 checksum 数值”。</p><h3 id="CHECKSUM-PARTIAL"><a href="#CHECKSUM-PARTIAL" class="headerlink" title="CHECKSUM_PARTIAL"></a>CHECKSUM_PARTIAL</h3><p>TX 数据尚未完成 checksum，网卡或软件 helper 应从 <code>csum_start</code> 开始计算，并把结果写到 <code>csum_start + csum_offset</code>。</p><h3 id="二、union-解释"><a href="#二、union-解释" class="headerlink" title="二、union 解释"></a>二、union 解释</h3><p><code>struct sk_buff</code> 中：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">union</span> &#123;</span></span><br><span class="line">    __wsum csum;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">        __u16 csum_start;</span><br><span class="line">        __u16 csum_offset;</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li>RX COMPLETE 时解释为 <code>csum</code>；</li><li>TX PARTIAL 时解释为 start&#x2F;offset；</li><li>不能同时把两种解释都当作有效。</li></ul><p><code>csum_start</code> 通常是相对 <code>head</code> 的 offset，不是相对当前 data 的稳定绝对地址。</p><h3 id="三、RX-驱动责任"><a href="#三、RX-驱动责任" class="headerlink" title="三、RX 驱动责任"></a>三、RX 驱动责任</h3><p>驱动只有在硬件明确验证相应协议和封装层时，才能设置 UNNECESSARY。常见限制：</p><ul><li>硬件只验证 IPv4 header，不代表 TCP&#x2F;UDP checksum；</li><li>分片、隧道、未知 extension header 可能使结果无效；</li><li>错误状态应保持 NONE；</li><li><code>skb_checksum_none_assert()</code> 只是调试断言当前为 NONE，不会把包变成已验证。</li></ul><p><code>csum_level</code> 表示连续已验证的额外 checksum 层数减一，用于隧道内外层，但最大值有限。</p><h3 id="四、TX-PARTIAL"><a href="#四、TX-PARTIAL" class="headerlink" title="四、TX PARTIAL"></a>四、TX PARTIAL</h3><p>典型设置：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">ip_summed = CHECKSUM_PARTIAL</span><br><span class="line">csum_start = transport header offset</span><br><span class="line">csum_offset = checksum field 在 transport header 内偏移</span><br></pre></td></tr></table></figure><p><code>skb_partial_csum_set()</code> 位于 <code>kernel-5.10/net/core/skbuff.c:4857</code>，会检查 start&#x2F;offset 是否落在线性 head 范围内，设置 transport header 和 PARTIAL 状态。对不可信输入必须校验，否则网卡 DMA 可能向越界位置写 checksum。</p><h3 id="五、软件-fallback"><a href="#五、软件-fallback" class="headerlink" title="五、软件 fallback"></a>五、软件 fallback</h3><p><code>skb_checksum_help()</code> 位于 <code>kernel-5.10/net/core/dev.c:3230</code>。当设备不支持当前 checksum offload 时：</p><ol><li>确保 checksum 写入位置可写&#x2F;线性；</li><li>软件计算 checksum；</li><li>写入协议 checksum 字段；</li><li>把状态转换为不再需要硬件处理。</li></ol><p><code>skb_csum_hwoffload_help()</code> 位于 <code>kernel-5.10/net/core/dev.c:3648</code>，结合设备 features 决定继续硬件 offload 或软件 fallback。</p><h3 id="六、pull-push-与-checksum"><a href="#六、pull-push-与-checksum" class="headerlink" title="六、pull&#x2F;push 与 checksum"></a>六、pull&#x2F;push 与 checksum</h3><p>改变 data 视图时，COMPLETE checksum 可能需要通过增量 helper 调整已经移除&#x2F;加入的字节范围。直接修改 packet bytes 则会使原 checksum 状态失效，必须：</p><ul><li>增量更新 checksum；</li><li>或降级为 NONE&#x2F;重新计算；</li><li>PARTIAL 时更新 start&#x2F;offset 和 header offset。</li></ul><p>因此修改 header 的 NAT、tc&#x2F;BPF、隧道代码通常同时调用 checksum helper。</p><h3 id="七、GSO-TSO"><a href="#七、GSO-TSO" class="headerlink" title="七、GSO&#x2F;TSO"></a>七、GSO&#x2F;TSO</h3><p>GSO skb 通常保持 PARTIAL。每个 segment 的 TCP&#x2F;UDP length、sequence、IP length 等不同，分段器或网卡基于 segment header 计算最终 checksum。</p><p>设备是否支持某个 GSO type，不自动意味着支持所有封装 checksum；feature negotiation 必须同时匹配 outer&#x2F;inner checksum 能力。</p><h3 id="八、隧道-checksum"><a href="#八、隧道-checksum" class="headerlink" title="八、隧道 checksum"></a>八、隧道 checksum</h3><p>隧道可能同时存在：</p><ul><li>outer UDP&#x2F;IP checksum；</li><li>inner TCP&#x2F;UDP checksum。</li></ul><p>skb 通过 outer&#x2F;inner header offset、<code>encapsulation</code>、GSO type、<code>csum_level</code> 和 PARTIAL 字段表达当前最外层待 offload checksum。部分硬件支持两层 offload，部分只支持一层，网络栈需要 fallback。</p><h3 id="九、字段变化表"><a href="#九、字段变化表" class="headerlink" title="九、字段变化表"></a>九、字段变化表</h3><table><thead><tr><th>场景</th><th><code>ip_summed</code></th><th>union</th><th>后续责任</th></tr></thead><tbody><tr><td>普通 RX 未验证</td><td>NONE</td><td>不可信</td><td>L4 软件验证</td></tr><tr><td>RX 已验证</td><td>UNNECESSARY</td><td>通常不使用</td><td>L4 跳过重复验证</td></tr><tr><td>RX 完整和</td><td>COMPLETE</td><td><code>csum</code></td><td>协议结合 pseudo header</td></tr><tr><td>TX offload</td><td>PARTIAL</td><td>start&#x2F;offset</td><td>网卡或软件 helper</td></tr><tr><td>软件 fallback 完成</td><td>NONE&#x2F;已完成语义</td><td>checksum 字段已写</td><td>驱动无需 offload</td></tr></tbody></table><h3 id="十、常见误区"><a href="#十、常见误区" class="headerlink" title="十、常见误区"></a>十、常见误区</h3><ol><li>UNNECESSARY 不是 checksum 值为零。</li><li>COMPLETE 不是“已经完全验证”，它提供可验证的和。</li><li>PARTIAL 不是坏包，而是明确的 TX 工作合同。</li><li>修改 header 后不能保留旧 checksum 状态不管。</li><li>IPv4 header checksum 与 TCP&#x2F;UDP checksum 是不同层。</li><li>GSO feature 和 checksum feature 必须组合检查。</li></ol><h3 id="字段与所有权统一核对表-1"><a href="#字段与所有权统一核对表-1" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>ip_summed</code></td><td>RX 驱动、协议栈、TX</td><td>编码校验和责任状态</td></tr><tr><td><code>csum</code></td><td>COMPLETE 验证</td><td>随 pull&#x2F;push 和协议处理调整</td></tr><tr><td><code>csum_start/offset</code></td><td>PARTIAL TX</td><td>设备或软件 helper 最终完成</td></tr></tbody></table><h3 id="最小状态调用链"><a href="#最小状态调用链" class="headerlink" title="最小状态调用链"></a>最小状态调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">RX descriptor/checksum metadata</span><br><span class="line">→ driver 设置 ip_summed/csum/csum_level</span><br><span class="line">→ GRO 与 L3/L4 验证或调整状态</span><br><span class="line">→ 转发/本地发送重新决定 CHECKSUM_PARTIAL</span><br><span class="line">→ GSO/软件 checksum helper 或网卡完成最终 checksum</span><br><span class="line">→ skb 进入下一持有者或被释放</span><br></pre></td></tr></table></figure><h2 id="3-RSS、RPS、RFS-与-XPS-的多核流调度"><a href="#3-RSS、RPS、RFS-与-XPS-的多核流调度" class="headerlink" title="3. RSS、RPS、RFS 与 XPS 的多核流调度"></a>3. RSS、RPS、RFS 与 XPS 的多核流调度</h2><p><img src="/blog/img/sk_buff/rss-rps-rfs-xps.svg" alt="RSS、RPS、RFS 与 XPS 的多核流调度"></p><h3 id="结论摘要-2"><a href="#结论摘要-2" class="headerlink" title="结论摘要"></a>结论摘要</h3><p><code>skb-&gt;hash</code> 是硬件 RSS 与软件 flow dissector、GRO、RPS&#x2F;RFS、reuseport 等机制之间的公共 flow identity。RSS 在网卡接收前选硬件 RX queue；RPS 在 skb 已构造后选协议栈 CPU；RFS 在 RPS 基础上参考应用 socket CPU；XPS 在发送侧选 TX queue。它们解决的层次不同，不能互相等同。</p><h3 id="一、hash-来源"><a href="#一、hash-来源" class="headerlink" title="一、hash 来源"></a>一、hash 来源</h3><h3 id="硬件-RSS"><a href="#硬件-RSS" class="headerlink" title="硬件 RSS"></a>硬件 RSS</h3><p>网卡解析包头并计算 hash，按 indirection table 选择 RX queue。驱动读取 descriptor hash，通过 <code>skb_set_hash()</code> 写入：</p><ul><li><code>skb-&gt;hash</code>；</li><li><code>l4_hash</code> 类型；</li><li><code>sw_hash=0</code>。</li></ul><h3 id="软件-hash"><a href="#软件-hash" class="headerlink" title="软件 hash"></a>软件 hash</h3><p>若驱动没有提供，<code>skb_get_hash()</code> 检查 hash 是否为零，必要时调用 <code>__skb_get_hash()</code>，见：</p><ul><li><code>kernel-5.10/include/linux/skbuff.h:1388-1393</code>；</li><li><code>kernel-5.10/net/core/flow_dissector.c:1607</code>。</li></ul><p>flow dissector 从 L2&#x2F;L3&#x2F;L4 key 计算对称或规范 hash，并设置 <code>sw_hash=1</code>。</p><h3 id="二、l4-hash-与-sw-hash"><a href="#二、l4-hash-与-sw-hash" class="headerlink" title="二、l4_hash 与 sw_hash"></a>二、l4_hash 与 sw_hash</h3><ul><li><code>l4_hash=1</code>：hash 基于规范四元组&#x2F;传输端口，可用于更强的 flow 粒度；</li><li><code>l4_hash=0</code>：可能只有 L3 或其它 hash；</li><li><code>sw_hash=1</code>：软件计算；</li><li><code>sw_hash=0</code>：通常来自硬件或外部设置。</li></ul><p>hash 相同不保证包绝对属于同一流，仍可能碰撞；它是快速分桶索引，不替代完整 header 比较。</p><h3 id="三、RSS"><a href="#三、RSS" class="headerlink" title="三、RSS"></a>三、RSS</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">packet arrives</span><br><span class="line">→ NIC computes hash</span><br><span class="line">→ indirection table selects RX queue</span><br><span class="line">→ queue IRQ/NAPI CPU</span><br><span class="line">→ driver builds skb and records hash/queue</span><br></pre></td></tr></table></figure><p>RSS 在 skb 产生之前完成，影响 DMA ring 和中断负载。它依赖硬件多队列。</p><h3 id="四、RPS"><a href="#四、RPS" class="headerlink" title="四、RPS"></a>四、RPS</h3><p><code>get_rps_cpu()</code> 位于 <code>kernel-5.10/net/core/dev.c:4359</code>。大致流程：</p><ol><li>获取&#x2F;计算 skb hash；</li><li>查看 RX queue 的 <code>rps_map</code>；</li><li>根据 hash 选择候选 CPU；</li><li>可结合 RFS flow table；</li><li>若目标 CPU 不同，<code>enqueue_to_backlog()</code> 排入目标 CPU。</li></ol><p><code>enqueue_to_backlog()</code> 位于 <code>kernel-5.10/net/core/dev.c:4572</code>。目标 CPU 的 <code>softnet_data</code> 使用 <code>input_pkt_queue</code> 接收远端 enqueue，再由 backlog NAPI 交换到 <code>process_queue</code> 批量处理。</p><p>RPS 发生在 skb 已分配之后，会增加跨 CPU enqueue、IPI&#x2F;cacheline 成本，但可让单队列网卡利用多核协议栈。</p><h3 id="五、RFS"><a href="#五、RFS" class="headerlink" title="五、RFS"></a>五、RFS</h3><p>RFS 目标是让 flow 在运行其应用线程的 CPU 上处理，提高 socket&#x2F;cache locality。</p><p>核心结构：</p><ul><li><code>rps_sock_flow_table</code>：全局 hash → 应用最近 CPU；</li><li><code>rps_dev_flow_table</code>：设备 RX queue flow 状态；</li><li><code>rps_dev_flow</code>：记录 CPU 与 last_qtail，避免乱序迁移。</li></ul><p>socket 收发路径通过 <code>rps_record_sock_flow()</code> 更新应用 CPU。<code>get_rps_cpu()</code> 比较 flow table，在安全队列边界后迁移 CPU，避免同一 TCP flow 因 CPU 切换产生重排。</p><h3 id="六、XPS"><a href="#六、XPS" class="headerlink" title="六、XPS"></a>六、XPS</h3><p>XPS 是发送侧 queue steering：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">socket/CPU sends skb</span><br><span class="line">→ netdev_pick_tx()/get_xps_queue()</span><br><span class="line">→ 根据 CPU/socket/flow map 选择 TX queue</span><br><span class="line">→ skb-&gt;queue_mapping</span><br><span class="line">→ qdisc/driver ring</span><br></pre></td></tr></table></figure><p>目的包括：</p><ul><li>让 CPU 固定使用局部 TX queue；</li><li>减少多个 CPU 对同一 queue lock&#x2F;cacheline 竞争；</li><li>保持 flow 与 queue 稳定；</li><li>配合 IRQ&#x2F;RSS 建立 RX&#x2F;TX affinity。</li></ul><h3 id="七、queue-mapping"><a href="#七、queue-mapping" class="headerlink" title="七、queue_mapping"></a>七、queue_mapping</h3><p>RX 时驱动可用 <code>skb_record_rx_queue()</code> 记录来源 queue；TX 时 <code>queue_mapping</code> 表示选择的发送 queue。字段复用但路径语义不同。</p><p>routing、tc redirect、设备层级变化后 queue mapping 可能需要重置或重新选择，不能把输入 queue 机械当作输出 queue。</p><h3 id="八、四者对比"><a href="#八、四者对比" class="headerlink" title="八、四者对比"></a>八、四者对比</h3><table><thead><tr><th>机制</th><th>执行位置</th><th>选择对象</th><th>是否需要 skb</th><th>主要目标</th></tr></thead><tbody><tr><td>RSS</td><td>网卡硬件</td><td>RX queue</td><td>否</td><td>硬件分流&#x2F;IRQ 多队列</td></tr><tr><td>RPS</td><td>RX core 软件</td><td>协议栈 CPU</td><td>是</td><td>软件多核处理</td></tr><tr><td>RFS</td><td>RPS + socket 反馈</td><td>应用所在 CPU</td><td>是</td><td>socket cache locality</td></tr><tr><td>XPS</td><td>TX queue selection</td><td>TX queue</td><td>是</td><td>发送队列局部性&#x2F;降竞争</td></tr></tbody></table><h3 id="九、GRO-与-hash"><a href="#九、GRO-与-hash" class="headerlink" title="九、GRO 与 hash"></a>九、GRO 与 hash</h3><p>GRO 使用 hash 快速寻找候选 flow，但仍由协议回调确认 header&#x2F;sequence。硬件 hash 类型错误可能降低聚合效率或错误分桶；软件可在必要时重新计算。</p><p>6.1 GRO hash bucket 把同一 NAPI 的 flow 按 hash 分桶，进一步体现 <code>skb-&gt;hash</code> 的跨子系统价值。</p><h3 id="十、性能与顺序约束"><a href="#十、性能与顺序约束" class="headerlink" title="十、性能与顺序约束"></a>十、性能与顺序约束</h3><ul><li>RSS&#x2F;RPS map 应结合 NUMA、IRQ affinity、应用绑核；</li><li>RPS 过宽会增加 IPI 和 cache miss；</li><li>同 flow CPU 迁移必须考虑 backlog qtail，避免乱序；</li><li>XPS queue 选择需稳定，TCP 乱序&#x2F;锁竞争都受影响；</li><li>hash collision 只影响候选分桶，协议逻辑仍应验证完整 flow。</li></ul><h3 id="十一、常见误区"><a href="#十一、常见误区" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li>RSS 与 RPS 都“分流”，但一个在硬件 skb 前，一个在软件 skb 后。</li><li>RFS 不是让应用线程直接执行 NAPI，而是让协议栈 CPU 靠近应用。</li><li>XPS 选 TX queue，不是选发送 CPU。</li><li><code>queue_mapping</code> 不是永久属性，跨设备 redirect 后可能失效。</li><li><code>hash</code> 不一定含端口，需看 <code>l4_hash</code>。</li><li>开启所有 RPS CPU 不一定更快，可能增加跨核成本。</li></ol><h3 id="字段与所有权统一核对表-2"><a href="#字段与所有权统一核对表-2" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>hash/l4_hash</code></td><td>RSS 或 flow dissector</td><td>RPS&#x2F;RFS&#x2F;GRO&#x2F;reuseport 读取</td></tr><tr><td><code>queue_mapping</code></td><td>XPS&#x2F;queue select</td><td>TX qdisc 与驱动队列读取</td></tr><tr><td>CPU backlog&#x2F;flow table</td><td>RPS&#x2F;RFS</td><td>enqueue 后目标 CPU 持有 skb</td></tr></tbody></table><h3 id="最小调用链-1"><a href="#最小调用链-1" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">驱动/RSS 或 flow dissector 产生 hash</span><br><span class="line">→ RPS/RFS 选择 RX CPU/socket locality</span><br><span class="line">→ TX XPS/queue selection</span><br><span class="line">→ qdisc 与设备队列</span><br></pre></td></tr></table></figure><h2 id="源码阅读路线"><a href="#源码阅读路线" class="headerlink" title="源码阅读路线"></a>源码阅读路线</h2><p>建议不要从 <code>struct sk_buff</code> 声明开始逐字段背诵，而应使用下面的阅读顺序：</p><ol><li>先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造；</li><li>沿成功主线记录 <code>data/len/data_len</code>、header offset、checksum、hash、queue mapping 等字段变化；</li><li>再回到每个失败分支，确认 skb、page、DMA、socket 和记账引用是否回滚；</li><li>最后对照 5.10、6.1 或 BSP 的同名 symbol，区分语义变化、性能优化和 vendor backport。</li></ol><h2 id="本篇总结"><a href="#本篇总结" class="headerlink" title="本篇总结"></a>本篇总结</h2><ol><li>GRO 在 RX 聚合，GSO&#x2F;TSO 在 TX 延迟分段，二者通过 shared info 元数据衔接。</li><li><code>ip_summed</code> 表达 checksum 的责任归属，而不是简单的“正确&#x2F;错误”。</li><li>hash、CPU 和 queue mapping 把包内容映射为处理位置与发送队列。</li><li>offload 的本质是延后工作或把工作交给硬件，但软件始终要维护可验证的语义。</li></ol><p><code>sk_buff</code> 的难点从来不只是字段多，而是<strong>同一个对象不断改变数据视图、元数据状态和持有者</strong>。只要把“布局、字段、所有权、释放”四条线同时画出来，绝大多数网络栈源码都会从零散函数变成可推导的生命周期。</p><h2 id="系列目录"><a href="#系列目录" class="headerlink" title="系列目录"></a>系列目录</h2><ol><li>Linux sk_buff 深度解析（一）：数据结构、四指针与核心字段</li><li>Linux sk_buff 深度解析（二）：非线性数据、引用计数与生命周期</li><li>Linux sk_buff 深度解析（三）：从网卡 DMA 到 Socket 的 RX 路径</li><li>Linux sk_buff 深度解析（四）：从 sendmsg 到 TX completion</li><li>Linux sk_buff 深度解析（五）：GRO、GSO、Checksum 与多核流调度（本文）</li><li>Linux sk_buff 深度解析（六）：Netfilter、tc、eBPF 与 XDP</li><li>Linux sk_buff 深度解析（七）：VLAN、隧道、分片与重组</li><li>Linux sk_buff 深度解析（八）：Socket 内存、零拷贝、丢包与版本演进</li></ol>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/df2c12300107/</id>
    <link href="https://2xpang.cn/blog/posts/df2c12300107/"/>
    <published>2026-07-14T21:00:00.000Z</published>
    <summary>把 GRO/GSO/TSO、checksum 四态以及 RSS/RPS/RFS/XPS 放在同一性能模型下，解释 skb 元数据如何替代重复解析和拷贝。</summary>
    <title>Linux sk_buff 深度解析（五）：GRO、GSO、Checksum 与多核流调度</title>
    <updated>2026-07-14T21:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="Linux" scheme="https://2xpang.cn/blog/tags/Linux/"/>
    <category term="网络子系统" scheme="https://2xpang.cn/blog/tags/%E7%BD%91%E7%BB%9C%E5%AD%90%E7%B3%BB%E7%BB%9F/"/>
    <category term="内核源码" scheme="https://2xpang.cn/blog/tags/%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81/"/>
    <category term="sk_buff" scheme="https://2xpang.cn/blog/tags/sk-buff/"/>
    <category term="Linux-5.10" scheme="https://2xpang.cn/blog/tags/Linux-5-10/"/>
    <category term="series:内核网络栈" scheme="https://2xpang.cn/blog/tags/series-%E5%86%85%E6%A0%B8%E7%BD%91%E7%BB%9C%E6%A0%88/"/>
    <category term="stmmac" scheme="https://2xpang.cn/blog/tags/stmmac/"/>
    <category term="qdisc" scheme="https://2xpang.cn/blog/tags/qdisc/"/>
    <content>
      <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p><code>sk_buff</code> 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”，而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的<strong>控制对象和所有权载体</strong>。</p><p>打通 UDP&#x2F;TCP sendmsg、IPv4 output、qdisc、ndo_start_xmit、stmmac DMA 映射与 TX clean，重点解释队列和驱动的 skb 所有权契约。</p><p>本文以本地 <strong>Linux 5.10.209</strong> 源码为主线；涉及演进时对照 <strong>Linux 6.1.99</strong>，涉及网卡驱动时结合 <strong>Firefly RK3588 SDK Linux 5.10.198</strong>。本文是源码分析，不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。</p><p><img src="/blog/img/sk_buff/tx-full-path.svg" alt="Linux sk_buff 深度解析（四）：从 sendmsg 到 TX completion"></p><h2 id="阅读前先建立四个问题"><a href="#阅读前先建立四个问题" class="headerlink" title="阅读前先建立四个问题"></a>阅读前先建立四个问题</h2><p>阅读任何 skb 代码路径，都建议反复追问：</p><ol><li>当前 <code>skb-&gt;data</code> 指向哪一层协议头，linear 与 non-linear 数据分别在哪里？</li><li>哪些字段刚被修改，下一层为什么依赖这些字段？</li><li>当前谁持有 skb shell、head、frag page 以及 socket&#x2F;dst&#x2F;conntrack 等外部引用？</li><li>成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb？</li></ol><h2 id="本篇核心结论"><a href="#本篇核心结论" class="headerlink" title="本篇核心结论"></a>本篇核心结论</h2><ul><li>协议层、qdisc、驱动和硬件 descriptor 依次成为 skb 的持有者。</li><li><code>dev_queue_xmit()</code> 不等于立即发包，绝大多数 skb 会先经历 queue selection 与 qdisc。</li><li><code>NETDEV_TX_OK</code> 表示驱动已经消费 skb；<code>NETDEV_TX_BUSY</code> 表示所有权仍归上层。</li><li>TX completion 负责 DMA unmap、descriptor 回收和最终 skb 消费，不能与 TCP ACK 生命周期混淆。</li></ul><h2 id="1-从-sendmsg-到网卡完成的完整-TX-生命周期"><a href="#1-从-sendmsg-到网卡完成的完整-TX-生命周期" class="headerlink" title="1. 从 sendmsg 到网卡完成的完整 TX 生命周期"></a>1. 从 sendmsg 到网卡完成的完整 TX 生命周期</h2><h3 id="结论摘要"><a href="#结论摘要" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>发送方向由 socket&#x2F;传输层创建并拥有 skb，经 IP 构造 header、Netfilter&#x2F;routing、邻居与二层输出进入 <code>dev_queue_xmit()</code>。qdisc 排队后把 skb 交给驱动 <code>ndo_start_xmit</code>。驱动成功接收后拥有 skb，完成 DMA mapping 与 descriptor 发布；TX completion 才 unmap、完成 BQL 记账并 <code>napi_consume_skb()</code>。<code>NETDEV_TX_BUSY</code> 是关键例外：驱动不得消费 skb，调用者仍需保留&#x2F;重试。</p><h3 id="一、UDP-主链"><a href="#一、UDP-主链" class="headerlink" title="一、UDP 主链"></a>一、UDP 主链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">sock_sendmsg()</span><br><span class="line">  → inet_sendmsg()</span><br><span class="line">  → udp_sendmsg()</span><br><span class="line">  → ip_make_skb() 或 ip_append_data()</span><br><span class="line">  → udp_send_skb()</span><br><span class="line">  → ip_send_skb()</span><br><span class="line">  → ip_local_out()</span><br><span class="line">  → ip_output()</span><br><span class="line">  → ip_finish_output()</span><br><span class="line">  → neighbour output</span><br><span class="line">  → dev_queue_xmit()</span><br></pre></td></tr></table></figure><p>关键位置：</p><ul><li><code>udp_sendmsg()</code>：<code>kernel-5.10/net/ipv4/udp.c:1040</code>；</li><li><code>__ip_append_data()</code>：<code>kernel-5.10/net/ipv4/ip_output.c:967</code>；</li><li><code>ip_append_data()</code>：<code>kernel-5.10/net/ipv4/ip_output.c:1314</code>；</li><li><code>ip_make_skb()</code>：<code>kernel-5.10/net/ipv4/ip_output.c:1633</code>；</li><li><code>ip_local_out()</code>：<code>kernel-5.10/net/ipv4/ip_output.c:120</code>；</li><li><code>ip_output()</code>：<code>kernel-5.10/net/ipv4/ip_output.c:430</code>。</li></ul><h3 id="二、UDP-构造-skb"><a href="#二、UDP-构造-skb" class="headerlink" title="二、UDP 构造 skb"></a>二、UDP 构造 skb</h3><p>UDP 需要决定：</p><ul><li>connected&#x2F;unconnected 目的地址；</li><li>route 与 MTU；</li><li>cork&#x2F;MSG_MORE；</li><li>checksum；</li><li>GSO&#x2F;UDP segmentation；</li><li>是否从用户 iterator 复制或 zerocopy。</li></ul><p><code>ip_append_data()</code> 可把数据追加到 socket write queue，形成一个或多个 skb；<code>ip_make_skb()</code> 使用临时 queue 一次构造并返回 skb。构造过程中预留 L2&#x2F;L3&#x2F;L4 headroom，payload 可在线性区或 frags 中。</p><p>socket 发送内存通过 <code>skb_set_owner_w()</code>、<code>sk_wmem_alloc</code> 与 <code>sock_wfree()</code> 记账。skb 进入不再需要 socket owner 的路径时可能 orphan，但 TCP 重传队列会长期保留 owner&#x2F;计量。</p><h3 id="三、TCP-与-UDP-的差异"><a href="#三、TCP-与-UDP-的差异" class="headerlink" title="三、TCP 与 UDP 的差异"></a>三、TCP 与 UDP 的差异</h3><p>TCP 不是每次 sendmsg 都立即生成并发送一个独立 datagram：</p><ul><li>数据进入 TCP write queue；</li><li><code>TCP_SKB_CB</code> 保存 seq&#x2F;end_seq&#x2F;flags；</li><li>skb 可能被合并、分段、clone；</li><li>原 skb&#x2F;clone 可能留在 retransmit queue；</li><li>ACK 后才释放发送内存；</li><li>实际发送副本与重传状态必须分开管理引用。</li></ul><p>因此 TCP skb 生命周期可能跨多个 RTT，远长于 UDP 单次输出。</p><h3 id="四、IP-output"><a href="#四、IP-output" class="headerlink" title="四、IP output"></a>四、IP output</h3><p>IP 层完成：</p><ul><li>设置 network header；</li><li>填写 IPv4 header、total length、ID、protocol、checksum；</li><li>附着 dst；</li><li>运行 LOCAL_OUT&#x2F;POST_ROUTING Netfilter；</li><li>检查 MTU、分片或 GSO；</li><li>通过 neighbour&#x2F;ARP 添加二层 header。</li></ul><p>headroom 不足或 header 共享时，需要 COW&#x2F;expand。任何扩展后旧 header 指针必须重新获取。</p><h3 id="五、dev-queue-xmit-与-qdisc"><a href="#五、dev-queue-xmit-与-qdisc" class="headerlink" title="五、dev_queue_xmit 与 qdisc"></a>五、dev_queue_xmit 与 qdisc</h3><p>入口：</p><ul><li><code>__dev_queue_xmit()</code>：<code>kernel-5.10/net/core/dev.c:4108</code>；</li><li><code>dev_queue_xmit()</code>：<code>kernel-5.10/net/core/dev.c:4219</code>。</li></ul><p>主要工作：</p><ol><li>确定输出设备和 TX queue；</li><li>运行 egress tc；</li><li>处理 VLAN&#x2F;offload feature；</li><li>选择 qdisc；</li><li>enqueue 或 noqueue 直接发送；</li><li>调度 dequeue；</li><li><code>dev_hard_start_xmit()</code> 调用 <code>ndo_start_xmit</code>。</li></ol><p>进入 qdisc 后 skb owner 是 qdisc；drop 时 qdisc 负责释放。dequeue 交给驱动后 owner 转移取决于驱动返回值。</p><h3 id="六、驱动所有权合同"><a href="#六、驱动所有权合同" class="headerlink" title="六、驱动所有权合同"></a>六、驱动所有权合同</h3><p><code>ndo_start_xmit()</code> 典型返回：</p><ul><li><code>NETDEV_TX_OK</code>：驱动已经消费&#x2F;接管 skb，调用者不能再访问；</li><li><code>NETDEV_TX_BUSY</code>：驱动没有消费 skb，网络栈仍持有并重试。</li></ul><p>驱动在返回 BUSY 前不能：</p><ul><li>释放 skb；</li><li>部分不可逆地提交 descriptor；</li><li>丢失 DMA mapping 回滚信息。</li></ul><p>现代驱动应尽量提前 stop queue，避免频繁 BUSY。</p><h3 id="七、stmmac-xmit"><a href="#七、stmmac-xmit" class="headerlink" title="七、stmmac_xmit"></a>七、stmmac_xmit</h3><p>入口：<code>firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:3468</code>。</p><p>主要步骤：</p><ol><li>选择 TX queue&#x2F;ring；</li><li>检查可用 descriptor；不足则 stop queue&#x2F;BUSY；</li><li>处理 GSO&#x2F;TSO、VLAN、checksum offload；</li><li>DMA map skb linear head；</li><li>遍历 <code>frags[]</code>，逐片 DMA map；</li><li>填写 descriptor 长度、地址和 offload flags；</li><li>在软件 <code>tx_skbuff[]</code>&#x2F;相关数组保存 skb 与 mapping；</li><li>设置 OWN，使用屏障保证 descriptor 内容先可见；</li><li>更新 <code>cur_tx</code>、BQL sent bytes；</li><li>通知 DMA tail pointer。</li></ol><h3 id="八、为什么-skb-常挂在最后一个-descriptor"><a href="#八、为什么-skb-常挂在最后一个-descriptor" class="headerlink" title="八、为什么 skb 常挂在最后一个 descriptor"></a>八、为什么 skb 常挂在最后一个 descriptor</h3><p>一个 skb 可能占多个 descriptor。completion 按 descriptor 顺序清理；只有最后一个 descriptor 完成，才能确定整个 skb 的所有 DMA segment 都不再被硬件访问。</p><p>因此常见做法：</p><ul><li>每个 descriptor 保存 DMA mapping 信息，便于逐个 unmap；</li><li>只在 packet 的最后一个 descriptor 保存 skb 指针；</li><li>clean 到最后一个时 consume skb。</li></ul><p>若过早释放 skb，frag page 可能在 DMA 仍读取时被复用，造成数据损坏。</p><h3 id="九、stmmac-tx-clean"><a href="#九、stmmac-tx-clean" class="headerlink" title="九、stmmac_tx_clean"></a>九、stmmac_tx_clean</h3><p>入口：<code>firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:2087</code>。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">检查 descriptor OWN</span><br><span class="line">→ 读取 TX status</span><br><span class="line">→ dma_unmap_single/page</span><br><span class="line">→ 到 packet last descriptor 时取回 skb</span><br><span class="line">→ napi_consume_skb()</span><br><span class="line">→ netdev_tx_completed_queue() / BQL</span><br><span class="line">→ 推进 dirty_tx</span><br><span class="line">→ 队列有空间时 wake</span><br></pre></td></tr></table></figure><p>TX completion 是正常消费，语义上优先 <code>napi_consume_skb()</code>&#x2F;<code>consume_skb()</code>，不是 drop 型 <code>kfree_skb()</code>。</p><h3 id="十、错误与回滚"><a href="#十、错误与回滚" class="headerlink" title="十、错误与回滚"></a>十、错误与回滚</h3><ul><li>route&#x2F;MTU&#x2F;Netfilter 失败：IP&#x2F;socket 层释放或返回错误；</li><li>qdisc enqueue drop：qdisc 释放；</li><li>driver ring 不足：BUSY，不消费；</li><li>DMA map 中途失败：驱动必须 unmap 已成功部分并释放&#x2F;返回正确状态；</li><li>hardware TX error completion：仍需 unmap、记账并释放 skb；</li><li>timeout&#x2F;reset：驱动 reset 路径必须清理所有 outstanding skb&#x2F;mapping。</li></ul><h3 id="十一、所有权时间线"><a href="#十一、所有权时间线" class="headerlink" title="十一、所有权时间线"></a>十一、所有权时间线</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">用户 buffer</span><br><span class="line">  → socket/transport owns skb</span><br><span class="line">  → IP/Netfilter/routing owns skb</span><br><span class="line">  → qdisc owns skb</span><br><span class="line">  → ndo_start_xmit:</span><br><span class="line">       OK   → driver owns skb</span><br><span class="line">       BUSY → qdisc/stack still owns skb</span><br><span class="line">  → DMA owns packet bytes（driver 保存软件引用）</span><br><span class="line">  → TX completion</span><br><span class="line">  → driver unmap + consume skb</span><br></pre></td></tr></table></figure><h3 id="十二、常见误区"><a href="#十二、常见误区" class="headerlink" title="十二、常见误区"></a>十二、常见误区</h3><ol><li><code>dev_queue_xmit()</code> 返回后不能默认 skb 仍有效。</li><li><code>NETDEV_TX_OK</code> 不等于包已经上线路，只表示驱动接管。</li><li>completion 才是普通异步驱动释放 skb 的安全点。</li><li>skb linear head 和 frags 需要不同 DMA mapping&#x2F;unmap API。</li><li>TCP ACK 释放的通常是协议发送队列引用，与网卡 completion 的发送副本生命周期不能混淆。</li><li>BQL 统计字节完成不替代 skb 释放，两者必须同时正确。</li></ol><h3 id="字段与所有权统一核对表"><a href="#字段与所有权统一核对表" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>sk/truesize</code></td><td>sendmsg 与协议队列</td><td>socket write memory 持有直到相应生命周期结束</td></tr><tr><td><code>queue_mapping</code></td><td>queue select&#x2F;qdisc</td><td>qdisc 或设备队列持有 skb</td></tr><tr><td>DMA mapping&#x2F;descriptor</td><td>stmmac xmit&#x2F;clean</td><td>驱动提交后由 TX completion 归还</td></tr></tbody></table><h3 id="最小调用链"><a href="#最小调用链" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">sendmsg()</span><br><span class="line">→ UDP/TCP 构造和排队 skb</span><br><span class="line">→ IP output</span><br><span class="line">→ dev_queue_xmit/qdisc</span><br><span class="line">→ stmmac_xmit/DMA</span><br><span class="line">→ TX completion/clean/free</span><br></pre></td></tr></table></figure><h2 id="2-qdisc-排队、调度与驱动所有权"><a href="#2-qdisc-排队、调度与驱动所有权" class="headerlink" title="2. qdisc 排队、调度与驱动所有权"></a>2. qdisc 排队、调度与驱动所有权</h2><p><img src="/blog/img/sk_buff/qdisc-driver-contract.svg" alt="qdisc 排队、调度与驱动所有权"></p><h3 id="结论摘要-1"><a href="#结论摘要-1" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>qdisc 位于协议栈和驱动之间，持有 skb 并实施分类、整形、调度、拥塞和丢包。<code>__dev_queue_xmit()</code> 选择 TX queue&#x2F;qdisc，enqueue 成功后 qdisc 成为 owner；<code>__qdisc_run()</code> dequeue，<code>sch_direct_xmit()</code> 交给驱动。驱动返回 BUSY 时 skb 必须 requeue，返回 OK 时驱动接管。BQL 控制驱动队列在途字节，与 qdisc 队列形成上下游反馈。</p><h3 id="一、主路径"><a href="#一、主路径" class="headerlink" title="一、主路径"></a>一、主路径</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">__dev_queue_xmit()</span><br><span class="line">  → egress tc</span><br><span class="line">  → netdev_pick_tx()/queue_mapping</span><br><span class="line">  → qdisc lookup</span><br><span class="line">  → __dev_xmit_skb()</span><br><span class="line">      ├─ enqueue</span><br><span class="line">      └─ qdisc_run()</span><br><span class="line">          → __qdisc_run()</span><br><span class="line">          → dequeue_skb()</span><br><span class="line">          → sch_direct_xmit()</span><br><span class="line">          → dev_hard_start_xmit()</span><br><span class="line">          → ndo_start_xmit()</span><br></pre></td></tr></table></figure><p>关键位置：</p><ul><li><code>__dev_queue_xmit()</code>：<code>kernel-5.10/net/core/dev.c:4108</code>；</li><li><code>dev_hard_start_xmit()</code>：<code>kernel-5.10/net/core/dev.c:3611</code>；</li><li><code>sch_direct_xmit()</code>：<code>kernel-5.10/net/sched/sch_generic.c:308</code>；</li><li><code>__qdisc_run()</code>：<code>kernel-5.10/net/sched/sch_generic.c:404</code>。</li></ul><h3 id="二、qdisc-的职责"><a href="#二、qdisc-的职责" class="headerlink" title="二、qdisc 的职责"></a>二、qdisc 的职责</h3><ul><li>FIFO&#x2F;优先级&#x2F;公平队列；</li><li>shaping&#x2F;pacing；</li><li>classful 分类；</li><li>AQM，如丢包&#x2F;ECN；</li><li>多队列设备的 queue qdisc；</li><li>统计 backlog、drops、overlimits。</li></ul><p>skb 中参与决策的字段包括：</p><ul><li><code>priority</code>；</li><li><code>mark</code>；</li><li><code>queue_mapping</code>；</li><li><code>protocol</code>；</li><li><code>hash</code>；</li><li><code>tc_index</code>；</li><li>qdisc cb 中的 packet length&#x2F;classid 等。</li></ul><h3 id="三、enqueue-所有权"><a href="#三、enqueue-所有权" class="headerlink" title="三、enqueue 所有权"></a>三、enqueue 所有权</h3><p>qdisc enqueue 的返回语义决定 skb 是否：</p><ul><li>已入队；</li><li>被丢弃并释放；</li><li>被替换&#x2F;重排；</li><li>需要调用者释放。</li></ul><p>通用调用层根据 <code>NET_XMIT_*</code> 处理统计和返回。不能看到 enqueue 失败就无条件再 <code>kfree_skb()</code>，否则可能 double free。</p><p>入队后 qdisc 长期持有 skb，协议层不能再修改。qdisc dequeue 前可根据时间、token、flow、公平策略决定顺序。</p><h3 id="四、运行与锁"><a href="#四、运行与锁" class="headerlink" title="四、运行与锁"></a>四、运行与锁</h3><p><code>qdisc_run()</code> 使用运行状态位&#x2F;锁确保同一 qdisc 不被并发重复执行。<code>__qdisc_run()</code> 在 quota 内循环 dequeue，避免一次占用 CPU 无上限。</p><p>dequeue 返回 skb 后，qdisc backlog accounting 相应减少；若后续驱动 BUSY，需要 requeue 并恢复相应状态。</p><h3 id="五、sch-direct-xmit"><a href="#五、sch-direct-xmit" class="headerlink" title="五、sch_direct_xmit"></a>五、sch_direct_xmit</h3><p><code>sch_direct_xmit()</code>：</p><ol><li>从 qdisc 取出的 skb 交给 <code>dev_hard_start_xmit()</code>；</li><li>驱动 OK：skb 被驱动消费；</li><li>BUSY：skb 未消费，重新入队；</li><li>其它错误&#x2F;部分 segment：按返回和剩余 skb 链处理；</li><li>更新 queue state 和统计。</li></ol><p>源码在 <code>kernel-5.10/net/sched/sch_generic.c:308-401</code>，其中 351 附近明确说明 BUSY requeue。</p><h3 id="六、noqueue"><a href="#六、noqueue" class="headerlink" title="六、noqueue"></a>六、noqueue</h3><p>loopback、某些虚拟设备或明确 noqueue 设备可绕过普通 qdisc 排队直接调用驱动。noqueue 不代表无任何同步或不会 drop，只是没有标准软件排队层。</p><p>错误给需要排队的物理设备配置 noqueue，会把背压直接暴露到驱动 BUSY 路径。</p><h3 id="七、queue-stop-wake"><a href="#七、queue-stop-wake" class="headerlink" title="七、queue stop&#x2F;wake"></a>七、queue stop&#x2F;wake</h3><p>驱动 ring 接近满时：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">netif_tx_stop_queue()</span><br><span class="line">→ qdisc 不继续向该 TX queue 下发</span><br></pre></td></tr></table></figure><p>completion 释放足够 descriptor 后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">netif_tx_wake_queue()</span><br><span class="line">→ qdisc 可重新运行</span><br></pre></td></tr></table></figure><p>stop 必须在真正没有空间前与 producer 竞态正确同步，否则可能返回 BUSY 或 ring overflow。</p><h3 id="八、BQL"><a href="#八、BQL" class="headerlink" title="八、BQL"></a>八、BQL</h3><p>Byte Queue Limits 按字节控制驱动硬件队列在途数据：</p><ul><li>提交时 <code>netdev_tx_sent_queue()</code>；</li><li>completion 时 <code>netdev_tx_completed_queue()</code>；</li><li>动态调整允许的在途字节。</li></ul><p>BQL 目标是减少网卡 ring 过深造成的 bufferbloat，同时保持设备不饿。它不替代 qdisc：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">qdisc：选择哪些 skb 何时下发</span><br><span class="line">BQL：限制驱动/硬件当前在途多少字节</span><br></pre></td></tr></table></figure><h3 id="九、watchdog-与-timeout"><a href="#九、watchdog-与-timeout" class="headerlink" title="九、watchdog 与 timeout"></a>九、watchdog 与 timeout</h3><p>若 TX queue 长时间 stop 且没有 completion，watchdog 触发 <code>ndo_tx_timeout</code>。reset 路径必须清理：</p><ul><li>outstanding descriptor；</li><li>DMA mappings；</li><li>保存的 skb；</li><li>BQL&#x2F;accounting；</li><li>queue state。</li></ul><p>否则会泄漏 skb&#x2F;page 或重复 completion。</p><h3 id="十、字段与所有权"><a href="#十、字段与所有权" class="headerlink" title="十、字段与所有权"></a>十、字段与所有权</h3><table><thead><tr><th>阶段</th><th>owner</th><th>关键字段</th></tr></thead><tbody><tr><td><code>__dev_queue_xmit</code> 前</td><td>IP&#x2F;stack</td><td>dev、dst、priority、mark</td></tr><tr><td>egress tc</td><td>tc&#x2F;stack</td><td>mark、priority、redirect</td></tr><tr><td>qdisc enqueue 后</td><td>qdisc</td><td>queue_mapping、qdisc cb、hash</td></tr><tr><td>dequeue</td><td>qdisc&#x2F;发送临界区</td><td>packet len、class&#x2F;accounting</td></tr><tr><td>ndo OK</td><td>driver</td><td>DMA mappings、descriptor</td></tr><tr><td>ndo BUSY</td><td>qdisc</td><td>skb 必须 intact&#x2F;requeue</td></tr><tr><td>completion</td><td>driver</td><td>unmap&#x2F;BQL&#x2F;consume</td></tr></tbody></table><h3 id="十一、常见误区"><a href="#十一、常见误区" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li>qdisc 不只是一条 FIFO；它可以分类、整形和 AQM。</li><li>驱动返回 OK 只表示接管，不表示已发送完成。</li><li>BUSY 时驱动不能释放 skb。</li><li>qdisc drop 的 skb 可能已被 enqueue API 消费，调用者需遵守返回合同。</li><li>BQL 不是 qdisc，也不是按 packet 数量限制。</li><li>queue mapping 选择错误会把流量送到错误 ring&#x2F;qdisc，增加乱序和锁竞争。</li></ol><h3 id="字段与所有权统一核对表-1"><a href="#字段与所有权统一核对表-1" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>queue_mapping/priority</code></td><td>TX select&#x2F;tc</td><td>选择 netdev queue 与 class</td></tr><tr><td>qdisc node&#x2F;cb</td><td>enqueue&#x2F;dequeue</td><td>排队期间 qdisc 持有 skb</td></tr><tr><td>driver return code</td><td><code>ndo_start_xmit</code></td><td>OK 消费；BUSY 时所有权仍归上层</td></tr></tbody></table><h3 id="最小调用链-1"><a href="#最小调用链-1" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">dev_queue_xmit()</span><br><span class="line">→ 选择 netdev queue/qdisc</span><br><span class="line">→ enqueue</span><br><span class="line">→ schedule/dequeue</span><br><span class="line">→ ndo_start_xmit()</span><br><span class="line">→ OK 消费或 BUSY 退还所有权</span><br></pre></td></tr></table></figure><h2 id="从局部机制回到完整路径"><a href="#从局部机制回到完整路径" class="headerlink" title="从局部机制回到完整路径"></a>从局部机制回到完整路径</h2><p><img src="/blog/img/sk_buff/stmmac-tx-lifecycle.svg" alt="完整路径关系"></p><p>前面的局部机制只有放回完整生命周期才不容易误判：数据布局决定 helper 能否直接访问；引用计数决定能否原地修改；队列和驱动返回值决定谁仍然拥有 skb。遇到异常路径时，应按“外部引用 → 数据区 → shell”的逆序检查回收。</p><h2 id="源码阅读路线"><a href="#源码阅读路线" class="headerlink" title="源码阅读路线"></a>源码阅读路线</h2><p>建议不要从 <code>struct sk_buff</code> 声明开始逐字段背诵，而应使用下面的阅读顺序：</p><ol><li>先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造；</li><li>沿成功主线记录 <code>data/len/data_len</code>、header offset、checksum、hash、queue mapping 等字段变化；</li><li>再回到每个失败分支，确认 skb、page、DMA、socket 和记账引用是否回滚；</li><li>最后对照 5.10、6.1 或 BSP 的同名 symbol，区分语义变化、性能优化和 vendor backport。</li></ol><h2 id="本篇总结"><a href="#本篇总结" class="headerlink" title="本篇总结"></a>本篇总结</h2><ol><li>协议层、qdisc、驱动和硬件 descriptor 依次成为 skb 的持有者。</li><li><code>dev_queue_xmit()</code> 不等于立即发包，绝大多数 skb 会先经历 queue selection 与 qdisc。</li><li><code>NETDEV_TX_OK</code> 表示驱动已经消费 skb；<code>NETDEV_TX_BUSY</code> 表示所有权仍归上层。</li><li>TX completion 负责 DMA unmap、descriptor 回收和最终 skb 消费，不能与 TCP ACK 生命周期混淆。</li></ol><p><code>sk_buff</code> 的难点从来不只是字段多，而是<strong>同一个对象不断改变数据视图、元数据状态和持有者</strong>。只要把“布局、字段、所有权、释放”四条线同时画出来，绝大多数网络栈源码都会从零散函数变成可推导的生命周期。</p><h2 id="系列目录"><a href="#系列目录" class="headerlink" title="系列目录"></a>系列目录</h2><ol><li>Linux sk_buff 深度解析（一）：数据结构、四指针与核心字段</li><li>Linux sk_buff 深度解析（二）：非线性数据、引用计数与生命周期</li><li>Linux sk_buff 深度解析（三）：从网卡 DMA 到 Socket 的 RX 路径</li><li>Linux sk_buff 深度解析（四）：从 sendmsg 到 TX completion（本文）</li><li>Linux sk_buff 深度解析（五）：GRO、GSO、Checksum 与多核流调度</li><li>Linux sk_buff 深度解析（六）：Netfilter、tc、eBPF 与 XDP</li><li>Linux sk_buff 深度解析（七）：VLAN、隧道、分片与重组</li><li>Linux sk_buff 深度解析（八）：Socket 内存、零拷贝、丢包与版本演进</li></ol>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/7e7c7beeb63f/</id>
    <link href="https://2xpang.cn/blog/posts/7e7c7beeb63f/"/>
    <published>2026-07-14T20:00:00.000Z</published>
    <summary>打通 UDP/TCP sendmsg、IPv4 output、qdisc、ndo_start_xmit、stmmac DMA 映射与 TX clean，重点解释队列和驱动的 skb 所有权契约。</summary>
    <title>Linux sk_buff 深度解析（四）：从 sendmsg 到 TX completion</title>
    <updated>2026-07-14T20:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="Linux" scheme="https://2xpang.cn/blog/tags/Linux/"/>
    <category term="网络子系统" scheme="https://2xpang.cn/blog/tags/%E7%BD%91%E7%BB%9C%E5%AD%90%E7%B3%BB%E7%BB%9F/"/>
    <category term="内核源码" scheme="https://2xpang.cn/blog/tags/%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81/"/>
    <category term="sk_buff" scheme="https://2xpang.cn/blog/tags/sk-buff/"/>
    <category term="Linux-5.10" scheme="https://2xpang.cn/blog/tags/Linux-5-10/"/>
    <category term="series:内核网络栈" scheme="https://2xpang.cn/blog/tags/series-%E5%86%85%E6%A0%B8%E7%BD%91%E7%BB%9C%E6%A0%88/"/>
    <category term="RK3588" scheme="https://2xpang.cn/blog/tags/RK3588/"/>
    <category term="stmmac" scheme="https://2xpang.cn/blog/tags/stmmac/"/>
    <content>
      <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p><code>sk_buff</code> 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”，而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的<strong>控制对象和所有权载体</strong>。</p><p>以 RK3588 stmmac 为驱动实例，追踪 DMA page 构造 skb、NAPI&#x2F;GRO&#x2F;RPS、IPv4、TCP&#x2F;UDP 到 Socket 接收队列的完整所有权变化。</p><p>本文以本地 <strong>Linux 5.10.209</strong> 源码为主线；涉及演进时对照 <strong>Linux 6.1.99</strong>，涉及网卡驱动时结合 <strong>Firefly RK3588 SDK Linux 5.10.198</strong>。本文是源码分析，不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。</p><p><img src="/blog/img/sk_buff/rx-full-path.svg" alt="Linux sk_buff 深度解析（三）：从网卡 DMA 到 Socket 的 RX 路径"></p><h2 id="阅读前先建立四个问题"><a href="#阅读前先建立四个问题" class="headerlink" title="阅读前先建立四个问题"></a>阅读前先建立四个问题</h2><p>阅读任何 skb 代码路径，都建议反复追问：</p><ol><li>当前 <code>skb-&gt;data</code> 指向哪一层协议头，linear 与 non-linear 数据分别在哪里？</li><li>哪些字段刚被修改，下一层为什么依赖这些字段？</li><li>当前谁持有 skb shell、head、frag page 以及 socket&#x2F;dst&#x2F;conntrack 等外部引用？</li><li>成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb？</li></ol><h2 id="本篇核心结论"><a href="#本篇核心结论" class="headerlink" title="本篇核心结论"></a>本篇核心结论</h2><ul><li>驱动不只交付字节，还要把长度、协议、checksum、hash、VLAN 等硬件结果编码进 skb。</li><li>NAPI&#x2F;GRO&#x2F;RPS 决定包何时批量处理、是否聚合以及在哪个 CPU 继续执行。</li><li>L3&#x2F;L4 逐层消费 header 并把 skb 交给 socket receive&#x2F;backlog queue。</li><li>每个返回码都隐含所有权契约：继续传递、排队、克隆、丢弃或已经消费。</li></ul><h2 id="1-RK3588-stmmac：从-DMA-page-构造-skb"><a href="#1-RK3588-stmmac：从-DMA-page-构造-skb" class="headerlink" title="1. RK3588 stmmac：从 DMA page 构造 skb"></a>1. RK3588 stmmac：从 DMA page 构造 skb</h2><h3 id="结论摘要"><a href="#结论摘要" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>RK3588 BSP 的 stmmac RX 路径把 DMA descriptor 指向的 page 转换为一个 skb：首段通常复制到新分配 skb 的 linear head，后续段通过 <code>skb_add_rx_frag()</code> 零拷贝挂载。copy 路径中的 RX page 可立即返回 page_pool；frag 路径把 page 所有权转交给 skb，必须等 skb 释放。完整包补充 checksum、hash、RX queue、protocol 后交给 <code>napi_gro_receive()</code>。</p><h3 id="一、调用位置"><a href="#一、调用位置" class="headerlink" title="一、调用位置"></a>一、调用位置</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">stmmac_napi_poll_rx()</span><br><span class="line">  → stmmac_rx(priv, budget, channel)</span><br><span class="line">      → 读取 RX descriptor</span><br><span class="line">      → 处理首段/后续段</span><br><span class="line">      → 完成 skb metadata</span><br><span class="line">      → napi_gro_receive()</span><br><span class="line">      → stmmac_rx_refill()</span><br></pre></td></tr></table></figure><p>关键入口：</p><ul><li><code>stmmac_rx()</code>：<code>firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:3862</code>；</li><li><code>stmmac_rx_refill()</code>：<code>firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:3740</code>；</li><li>NAPI 调用点：<code>firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:4081</code>。</li></ul><h3 id="二、descriptor-与-buffer-所有权"><a href="#二、descriptor-与-buffer-所有权" class="headerlink" title="二、descriptor 与 buffer 所有权"></a>二、descriptor 与 buffer 所有权</h3><p>RX descriptor 的 OWN 位区分 DMA 和 CPU 所有权：</p><ul><li>OWN 属于 DMA：CPU 不能读取正在被硬件写入的 buffer；</li><li>OWN 被硬件清除：驱动可以读取状态和 packet data；</li><li>refill 完成并设置 OWN：buffer 再次交给 DMA。</li></ul><p>驱动推进 <code>cur_rx</code> 只是软件消费进度，真正把 descriptor 重新交给硬件需要 refill、地址写入和内存屏障。</p><h3 id="三、为什么首段复制到-linear-head"><a href="#三、为什么首段复制到-linear-head" class="headerlink" title="三、为什么首段复制到 linear head"></a>三、为什么首段复制到 linear head</h3><p>典型逻辑：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">napi_alloc_skb()</span><br><span class="line">→ skb_copy_to_linear_data()</span><br><span class="line">→ skb_put()</span><br></pre></td></tr></table></figure><p>首段包含 Ethernet&#x2F;IP&#x2F;TCP 等协议头。将小的前部数据放入 linear 区有利于：</p><ul><li>协议解析直接访问；</li><li>避免每层都调用 <code>pskb_may_pull()</code>；</li><li>改写 header 时更容易 COW；</li><li>RX page 复制后可立即返回 page_pool；</li><li>copybreak 在小包场景避免长期占用整个 page。</li></ul><p>代价是一次 CPU copy，因此不是所有驱动和包长都采用完全相同策略。</p><h3 id="四、后续段为什么挂-frags"><a href="#四、后续段为什么挂-frags" class="headerlink" title="四、后续段为什么挂 frags"></a>四、后续段为什么挂 frags</h3><p>多 descriptor 包的后续 page 通常不包含热协议头。驱动调用：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">skb_add_rx_frag(skb, index, page, offset, size, truesize);</span><br></pre></td></tr></table></figure><p>它把 page 描述加入 shared info，并更新 <code>nr_frags/len/data_len/truesize</code>。优点是避免复制大 payload，便于 GRO、协议栈和 TX scatter-gather 继续使用 page。</p><p>所有权变化：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">挂 frag 前：RX ring/page_pool 管理 page</span><br><span class="line">挂 frag 后：skb 持有该 page 引用</span><br><span class="line">skb free：frag unref，5.10 BSP 由相应回收路径处理</span><br></pre></td></tr></table></figure><p>驱动必须从 RX buffer bookkeeping 中移除或标记已转交的 page，再为 descriptor 分配&#x2F;获取替代 page。</p><h3 id="五、copy-与-frag-回收差异"><a href="#五、copy-与-frag-回收差异" class="headerlink" title="五、copy 与 frag 回收差异"></a>五、copy 与 frag 回收差异</h3><table><thead><tr><th>路径</th><th>包数据去向</th><th>原 RX page</th><th>释放时机</th></tr></thead><tbody><tr><td>首段 copy</td><td>skb linear head</td><td>数据已复制，可直接 recycle</td><td>当前 poll&#x2F;refill 周期</td></tr><tr><td>后续 frag</td><td>skb <code>frags[]</code></td><td>所有权转交 skb</td><td>skb 数据引用归零时</td></tr></tbody></table><p>如果驱动把已经挂入 frag 的 page 又直接 recycle，会造成 use-after-free&#x2F;DMA corruption；如果忘记转交后的回收路径，会泄漏 page。</p><h3 id="六、半包状态"><a href="#六、半包状态" class="headerlink" title="六、半包状态"></a>六、半包状态</h3><p>一个 frame 可能跨多个 descriptor，而 NAPI budget 或 ring 当前可用 descriptor 可能让处理暂时停止。驱动需要保存：</p><ul><li>当前正在构造的 skb；</li><li>已消费长度；</li><li>下一个 descriptor；</li><li>是否已看到 last segment。</li></ul><p>恢复后继续向同一个 skb 添加 frag。错误 descriptor、超长 frag 数、分配失败等路径必须释放已构造 skb，并正确处理尚未转交&#x2F;已经转交的 page。</p><h3 id="七、完成包的-metadata"><a href="#七、完成包的-metadata" class="headerlink" title="七、完成包的 metadata"></a>七、完成包的 metadata</h3><h3 id="checksum"><a href="#checksum" class="headerlink" title="checksum"></a>checksum</h3><p>若硬件确认 L3&#x2F;L4 checksum 有效，驱动可设置 <code>CHECKSUM_UNNECESSARY</code>；否则保持 <code>CHECKSUM_NONE</code>。<code>skb_checksum_none_assert()</code> 只是断言当前状态，不等于硬件校验成功。</p><h3 id="hash"><a href="#hash" class="headerlink" title="hash"></a>hash</h3><p>硬件 RSS&#x2F;hash 可通过 <code>skb_set_hash()</code> 写入 <code>skb-&gt;hash</code> 并设置 hash 类型。后续 GRO、RPS&#x2F;RFS、reuseport 等可以复用，避免再次解析。</p><h3 id="RX-queue"><a href="#RX-queue" class="headerlink" title="RX queue"></a>RX queue</h3><p><code>skb_record_rx_queue()</code> 把来源硬件队列记录到 <code>queue_mapping</code>，用于统计、socket RX queue 查询和流调度。</p><h3 id="protocol"><a href="#protocol" class="headerlink" title="protocol"></a>protocol</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">skb-&gt;protocol = eth_type_trans(skb, dev);</span><br></pre></td></tr></table></figure><p><code>eth_type_trans()</code> 不只是读取 EtherType，还保存&#x2F;调整 MAC header、推进 data 到 L3 位置，并处理 packet type 等二层语义。因此不应在驱动中只手工赋一个常量替代。</p><h3 id="八、交给-GRO"><a href="#八、交给-GRO" class="headerlink" title="八、交给 GRO"></a>八、交给 GRO</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">napi_gro_receive(&amp;rx_q-&gt;napi, skb);</span><br></pre></td></tr></table></figure><p>调用后 skb 所有权交给 GRO&#x2F;网络栈。驱动不能继续访问或释放它。GRO 可能：</p><ul><li>hold；</li><li>merge 并消费当前 skb；</li><li>flush 旧 skb；</li><li>normal receive。</li></ul><p>返回值用于统计，不代表驱动重新获得 skb。</p><h3 id="九、RX-refill"><a href="#九、RX-refill" class="headerlink" title="九、RX refill"></a>九、RX refill</h3><p><code>stmmac_rx_refill()</code> 为已经消费的 descriptor 准备新 buffer：</p><ol><li>从 page_pool&#x2F;分配器获取 page；</li><li>写入 descriptor buffer address；</li><li>清理软件状态；</li><li>使用正确屏障；</li><li>设置 OWN 交给 DMA；</li><li>更新 dirty&#x2F;clean index。</li></ol><p>refill 与构造 skb 是同一生命周期闭环：取 page → DMA 写入 → CPU 构造 skb&#x2F;回收 → 补充新 page。</p><h3 id="十、字段与所有权变化表"><a href="#十、字段与所有权变化表" class="headerlink" title="十、字段与所有权变化表"></a>十、字段与所有权变化表</h3><table><thead><tr><th>时点</th><th>linear&#x2F;frags</th><th><code>len/data_len</code></th><th>page owner</th><th>skb owner</th></tr></thead><tbody><tr><td>descriptor 完成</td><td>无 skb</td><td>-</td><td>RX ring&#x2F;driver</td><td>-</td></tr><tr><td>首段 copy</td><td>linear 增长</td><td>len 增、data_len 0</td><td>原 page 可 recycle</td><td>driver</td></tr><tr><td>后续 frag</td><td>nr_frags 增</td><td>len&#x2F;data_len 增</td><td>skb</td><td>driver</td></tr><tr><td>metadata 完成</td><td>header&#x2F;hash&#x2F;csum&#x2F;queue 有效</td><td>不变</td><td>skb</td><td>driver</td></tr><tr><td><code>napi_gro_receive</code></td><td>可能被 GRO 重组</td><td>可能变化</td><td>network stack</td><td>GRO&#x2F;stack</td></tr></tbody></table><h3 id="十一、BSP-适用边界"><a href="#十一、BSP-适用边界" class="headerlink" title="十一、BSP 适用边界"></a>十一、BSP 适用边界</h3><p>这是 Firefly RK3588 SDK 的 stmmac 具体实现，不应推广为所有网卡的唯一方式。其他驱动可能：</p><ul><li>用 <code>build_skb()</code> 直接包装整页；</li><li>全部使用 frags；</li><li>使用 XDP 后再决定是否构造 skb；</li><li>使用不同 copybreak；</li><li>在更新版本中通过 <code>pp_recycle</code> 自动 page_pool 回收。</li></ul><p>通用不变量仍然是：明确 descriptor、page 和 skb 三者所有权，metadata 完成后再把 skb 交给上层。</p><h3 id="字段与所有权统一核对表"><a href="#字段与所有权统一核对表" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td>RX page</td><td>page_pool&#x2F;DMA</td><td>驱动与 page_pool 在 descriptor 和 skb 间转移</td></tr><tr><td><code>len/data_len</code></td><td>stmmac RX 构包</td><td>决定 linear 与 frag 数据边界</td></tr><tr><td>checksum&#x2F;hash&#x2F;VLAN</td><td>descriptor 状态解析</td><td>驱动写元数据，协议栈消费</td></tr></tbody></table><h3 id="最小调用链"><a href="#最小调用链" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">stmmac RX descriptor 完成</span><br><span class="line">→ page_pool/DMA 同步</span><br><span class="line">→ build_skb/napi_build_skb</span><br><span class="line">→ 填写长度和 offload 元数据</span><br><span class="line">→ napi_gro_receive()</span><br></pre></td></tr></table></figure><h2 id="2-NAPI、GRO、RPS-与-RX-core"><a href="#2-NAPI、GRO、RPS-与-RX-core" class="headerlink" title="2. NAPI、GRO、RPS 与 RX core"></a>2. NAPI、GRO、RPS 与 RX core</h2><p><img src="/blog/img/sk_buff/stmmac-rx-build.svg" alt="NAPI、GRO、RPS 与 RX core"></p><h3 id="结论摘要-1"><a href="#结论摘要-1" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>驱动通过 <code>napi_gro_receive()</code> 交出 skb 后，GRO 决定 hold&#x2F;merge&#x2F;flush&#x2F;normal。进入 <code>__netif_receive_skb_core()</code> 后，skb 依次经过时间戳&#x2F;设备与输入接口初始化、generic XDP、VLAN、rx handler、ingress tc、Netfilter ingress、ptype tap 和协议 handler。每个 hook 都可能替换、clone、redirect、queue 或消费 skb，因此主路径使用 <code>pt_prev</code> 延迟投递以减少 clone，并严格维护所有权。</p><h3 id="一、主调用链"><a href="#一、主调用链" class="headerlink" title="一、主调用链"></a>一、主调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">napi_gro_receive()</span><br><span class="line">  → dev_gro_receive()</span><br><span class="line">  → napi_skb_finish()</span><br><span class="line">      ├─ GRO_HELD / GRO_MERGED：当前 skb 被 GRO 持有或消费</span><br><span class="line">      └─ GRO_NORMAL：gro_normal_one()/netif_receive_skb_internal()</span><br><span class="line">          → __netif_receive_skb()</span><br><span class="line">          → __netif_receive_skb_core()</span><br><span class="line">          → packet_type handler</span><br><span class="line">              → ip_rcv()/ipv6_rcv()/ARP/bridge/...</span><br></pre></td></tr></table></figure><p>关键位置：</p><ul><li><code>dev_gro_receive()</code>：<code>kernel-5.10/net/core/dev.c:5991</code>；</li><li><code>napi_gro_receive()</code>：<code>kernel-5.10/net/core/dev.c:6166</code>；</li><li><code>__netif_receive_skb_core()</code>：<code>kernel-5.10/net/core/dev.c:5168</code>；</li><li><code>netif_receive_skb()</code>：<code>kernel-5.10/net/core/dev.c:5652</code>。</li></ul><h3 id="二、GRO-交接语义"><a href="#二、GRO-交接语义" class="headerlink" title="二、GRO 交接语义"></a>二、GRO 交接语义</h3><p>GRO result 不是简单成功&#x2F;失败：</p><ul><li><code>GRO_MERGED</code>：当前 skb 数据已并入已有 skb，当前 skb 通常被消费；</li><li><code>GRO_MERGED_FREE</code>：合并后还需特定释放；</li><li><code>GRO_HELD</code>：skb 留在 NAPI GRO list；</li><li><code>GRO_NORMAL</code>：不能合并，进入普通协议栈；</li><li><code>GRO_DROP</code>：丢弃。</li></ul><p>驱动调用后无论返回哪个结果，都不再拥有 skb。</p><h3 id="三、进入-core-前的上下文"><a href="#三、进入-core-前的上下文" class="headerlink" title="三、进入 core 前的上下文"></a>三、进入 core 前的上下文</h3><p><code>netif_receive_skb()</code> 适用于进程&#x2F;软中断等通用入口，内部可根据 RPS 决定本 CPU 直接处理或 enqueue 到目标 CPU backlog。<code>__netif_receive_skb()</code> 是更内部的直接版本。</p><p>RPS 可能让 skb 先进入另一个 CPU 的 <code>softnet_data.input_pkt_queue</code>，之后由 backlog NAPI 再调用 core。于是“驱动 poll CPU”不必等于“协议栈执行 CPU”。</p><h3 id="四、-netif-receive-skb-core-的关键阶段"><a href="#四、-netif-receive-skb-core-的关键阶段" class="headerlink" title="四、__netif_receive_skb_core 的关键阶段"></a>四、__netif_receive_skb_core 的关键阶段</h3><h3 id="1-初始化输入语义"><a href="#1-初始化输入语义" class="headerlink" title="1. 初始化输入语义"></a>1. 初始化输入语义</h3><p>函数保存 <code>orig_dev</code>，设置&#x2F;维护 <code>skb_iif</code>，处理 pfmemalloc 和时间戳等状态。<code>skb-&gt;dev</code> 在 bridge、bond、VLAN、redirect 等路径中可能变化，所以原始设备和当前设备需要区分。</p><h3 id="2-generic-XDP"><a href="#2-generic-XDP" class="headerlink" title="2. generic XDP"></a>2. generic XDP</h3><p>若设备挂载 generic XDP，<code>do_xdp_generic()</code> 在 <code>kernel-5.10/net/core/dev.c:4784</code>，调用点约 5201。此时 skb 已经存在，与 native XDP 的“构造 skb 之前”不同。</p><p>XDP verdict 可能 pass、drop、tx、redirect；非 pass 时 skb 可能已消费，core 不能继续使用原指针。</p><h3 id="3-VLAN-untag-metadata"><a href="#3-VLAN-untag-metadata" class="headerlink" title="3. VLAN untag&#x2F;metadata"></a>3. VLAN untag&#x2F;metadata</h3><p><code>skb_vlan_untag()</code> 可把包内 VLAN header 转成 skb VLAN metadata，并调整 data&#x2F;header。硬件已经剥离的 VLAN tag 则可能直接存在 <code>vlan_tci/vlan_proto</code>。</p><h3 id="4-ptype-all-tap"><a href="#4-ptype-all-tap" class="headerlink" title="4. ptype_all tap"></a>4. ptype_all tap</h3><p>AF_PACKET&#x2F;tcpdump 等 tap 通过 <code>ptype_all</code> 观察包。一个 skb 需要交给多个 handler 时不能让第一个 handler 独占原 skb，因此核心通过 clone 或 <code>deliver_skb()</code> 增加引用。</p><h3 id="5-ingress-tc"><a href="#5-ingress-tc" class="headerlink" title="5. ingress tc"></a>5. ingress tc</h3><p><code>sch_handle_ingress()</code> 位于 <code>kernel-5.10/net/core/dev.c:5000</code>，调用点约 5241。tc action 可修改、drop、redirect、mirror 或 consume skb。</p><h3 id="6-Netfilter-ingress"><a href="#6-Netfilter-ingress" class="headerlink" title="6. Netfilter ingress"></a>6. Netfilter ingress</h3><p>若配置 ingress hook，Netfilter verdict 决定继续、queue、stolen 或 drop。所有权取决于 verdict，不能按普通函数返回理解。</p><h3 id="7-rx-handler"><a href="#7-rx-handler" class="headerlink" title="7. rx_handler"></a>7. rx_handler</h3><p>bridge、bond、macvlan 等设备可注册 rx_handler。它可能：</p><ul><li>消费 skb；</li><li>修改 <code>skb-&gt;dev</code> 后重新开始；</li><li>精确传递；</li><li>让 skb 继续普通协议分发。</li></ul><h3 id="8-protocol-handler"><a href="#8-protocol-handler" class="headerlink" title="8. protocol handler"></a>8. protocol handler</h3><p>根据 <code>skb-&gt;protocol</code> 在 <code>ptype_base</code> 中找到 IPv4、IPv6、ARP 等 handler。IPv4 通常进入 <code>ip_rcv()</code>。</p><h3 id="五、pt-prev-延迟投递"><a href="#五、pt-prev-延迟投递" class="headerlink" title="五、pt_prev 延迟投递"></a>五、pt_prev 延迟投递</h3><p>core 遍历多个 packet_type handler 时，用 <code>pt_prev</code> 暂存前一个 handler：</p><ul><li>发现下一个匹配者时，才把 skb 引用交给前一个；</li><li>最后一个 handler 可直接获得原 skb；</li><li>减少不必要 clone&#x2F;ref 操作。</li></ul><p>这是一种热路径所有权优化。阅读代码时需同时追踪 <code>skb</code> 与 <code>pt_prev</code>，否则容易误判一个 handler 是否拿到原 skb。</p><h3 id="六、header-与字段变化"><a href="#六、header-与字段变化" class="headerlink" title="六、header 与字段变化"></a>六、header 与字段变化</h3><table><thead><tr><th>阶段</th><th>典型变化</th></tr></thead><tbody><tr><td>驱动完成</td><td><code>dev/protocol/hash/ip_summed/queue_mapping</code> 已设置</td></tr><tr><td>GRO</td><td><code>len/data_len/frags/frag_list/GRO cb</code> 可能变化</td></tr><tr><td>generic XDP</td><td>data&#x2F;head&#x2F;len&#x2F;redirect 可能变化</td></tr><tr><td>VLAN</td><td>VLAN metadata、protocol、data&#x2F;header 变化</td></tr><tr><td>rx_handler</td><td><code>dev</code>、protocol 或所有权变化</td></tr><tr><td>ingress tc&#x2F;NF</td><td>mark、priority、data、redirect、nfct 等变化</td></tr><tr><td>ptype&#x2F;L3</td><td>network header、dst、cb overlay 变化</td></tr></tbody></table><p>任何可能重分配 head 的 hook 之后，旧 header 指针都不能继续使用。</p><h3 id="七、所有权退出点"><a href="#七、所有权退出点" class="headerlink" title="七、所有权退出点"></a>七、所有权退出点</h3><ul><li>GRO merge&#x2F;drop；</li><li>XDP drop&#x2F;redirect&#x2F;tx；</li><li>tc shot&#x2F;stolen&#x2F;redirect；</li><li>Netfilter drop&#x2F;queue&#x2F;stolen；</li><li>rx_handler consumed；</li><li>protocol handler 接管；</li><li>无 handler 时 core 释放。</li></ul><p>core 的返回码通常用于统计，调用者不能因为返回“成功”就认为 skb 仍有效。</p><h3 id="八、常见误区"><a href="#八、常见误区" class="headerlink" title="八、常见误区"></a>八、常见误区</h3><ol><li><code>netif_receive_skb()</code> 不保证在当前 CPU 立即执行协议栈，RPS 可排队。</li><li>generic XDP 不是 native XDP，它已经有 skb。</li><li><code>skb-&gt;dev</code> 在 core 内可能变化，<code>orig_dev</code> 用于保留原输入设备。</li><li>ptype tap 可能让一个包有多个观察者，需要引用&#x2F;clone。</li><li>hook 返回后原 skb 可能已被消费或替换。</li><li><code>napi_gro_receive()</code> 返回并不把所有权还给驱动。</li></ol><h3 id="字段与所有权统一核对表-1"><a href="#字段与所有权统一核对表-1" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>dev/protocol</code></td><td>驱动与 <code>eth_type_trans()</code></td><td>RX core 接管后逐层转交</td></tr><tr><td><code>hash/napi_id</code></td><td>GRO&#x2F;RPS</td><td>决定聚合与 CPU 调度</td></tr><tr><td>skb shell</td><td>NAPI&#x2F;GRO&#x2F;backlog</td><td>返回码和 enqueue 结果决定消费方</td></tr></tbody></table><h2 id="3-IPv4、TCP-UDP-与-Socket-接收队列"><a href="#3-IPv4、TCP-UDP-与-Socket-接收队列" class="headerlink" title="3. IPv4、TCP&#x2F;UDP 与 Socket 接收队列"></a>3. IPv4、TCP&#x2F;UDP 与 Socket 接收队列</h2><p><img src="/blog/img/sk_buff/rx-ownership.svg" alt="IPv4、TCP&#x2F;UDP 与 Socket 接收队列"></p><h3 id="结论摘要-2"><a href="#结论摘要-2" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>IPv4 RX 先验证并规范化 skb，再经 PRE_ROUTING、路由输入和 LOCAL_IN 到达 L4 handler。UDP 查找 socket 后把 skb 作为完整 datagram 排入 receive queue；TCP 把 skb 交给连接状态机，可能进入 backlog、out-of-order queue 或 receive queue。socket 通过 <code>truesize</code> 而非包长进行内存计量，owner&#x2F;destructor 负责在 skb 最终释放时归还计数。</p><h3 id="一、IPv4-主链"><a href="#一、IPv4-主链" class="headerlink" title="一、IPv4 主链"></a>一、IPv4 主链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">ip_rcv()</span><br><span class="line">  → ip_rcv_core()</span><br><span class="line">  → NF_INET_PRE_ROUTING</span><br><span class="line">  → ip_rcv_finish()</span><br><span class="line">  → route input</span><br><span class="line">  → ip_local_deliver()</span><br><span class="line">  → NF_INET_LOCAL_IN</span><br><span class="line">  → ip_local_deliver_finish()</span><br><span class="line">  → ip protocol handler</span><br><span class="line">      ├─ udp_rcv()</span><br><span class="line">      └─ tcp_v4_rcv()</span><br></pre></td></tr></table></figure><p>关键位置：</p><ul><li><code>ip_rcv_core()</code>：<code>kernel-5.10/net/ipv4/ip_input.c:442</code>；</li><li><code>ip_rcv()</code>：<code>kernel-5.10/net/ipv4/ip_input.c:537</code>；</li><li><code>ip_local_deliver()</code>：<code>kernel-5.10/net/ipv4/ip_input.c:240</code>；</li><li><code>ip_local_deliver_finish()</code>：<code>kernel-5.10/net/ipv4/ip_input.c:226</code>。</li></ul><h3 id="二、ip-rcv-core-对-skb-的规范化"><a href="#二、ip-rcv-core-对-skb-的规范化" class="headerlink" title="二、ip_rcv_core 对 skb 的规范化"></a>二、ip_rcv_core 对 skb 的规范化</h3><p>典型工作：</p><ul><li>确保 IPv4 基本头在线性区；</li><li>校验 version、IHL、total length；</li><li>trim 掉 L2 padding，使 <code>skb-&gt;len</code> 与 IP total length 一致；</li><li>校验 IPv4 header checksum；</li><li>设置&#x2F;清理 IPv4 控制块 <code>IPCB(skb)</code>；</li><li>丢弃 malformed packet。</li></ul><p>trim 可能改变 skb 长度和非线性布局。失败路径会释放 skb，后续不能继续访问。</p><h3 id="三、路由与-dst"><a href="#三、路由与-dst" class="headerlink" title="三、路由与 dst"></a>三、路由与 dst</h3><p>PRE_ROUTING 后通过 route input 决定：</p><ul><li>本机接收；</li><li>转发；</li><li>multicast&#x2F;broadcast；</li><li>unreachable&#x2F;drop。</li></ul><p>路由结果附着为 skb dst，后续通过 dst input&#x2F;output 回调继续。clone 时 dst 需要增加引用，free 时 <code>skb_dst_drop()</code>。</p><p>本地流量进入 <code>ip_local_deliver()</code>；分片包可能先调用 <code>ip_defrag()</code>，重组完成后才进入 L4。</p><h3 id="四、L4-分发"><a href="#四、L4-分发" class="headerlink" title="四、L4 分发"></a>四、L4 分发</h3><p><code>ip_local_deliver_finish()</code> pull&#x2F;定位 IP header 后，根据 <code>iph-&gt;protocol</code> 查找 <code>net_protocol</code>：</p><ul><li>TCP → <code>tcp_v4_rcv()</code>；</li><li>UDP → <code>udp_rcv()</code>；</li><li>其它协议对应 handler；</li><li>raw socket 可能同时获得副本。</li></ul><p>协议 handler 接管 skb 所有权。IP 层不应在 handler 返回后再次释放同一 skb，除非约定明确。</p><h3 id="五、UDP-接收"><a href="#五、UDP-接收" class="headerlink" title="五、UDP 接收"></a>五、UDP 接收</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">udp_rcv()</span><br><span class="line">  → __udp4_lib_rcv()</span><br><span class="line">  → socket lookup</span><br><span class="line">  → checksum/filter/policy</span><br><span class="line">  → udp_queue_rcv_skb()</span><br><span class="line">  → sock_queue_rcv_skb()</span><br><span class="line">  → sk_receive_queue</span><br></pre></td></tr></table></figure><p>UDP 保留 datagram 边界，一个 skb 通常对应一个用户可读取的数据报。排队前会处理：</p><ul><li>UDP length；</li><li>checksum；</li><li>multicast&#x2F;broadcast 多 socket 分发；</li><li>socket filter；</li><li>policy&#x2F;xfrm；</li><li>rcvbuf 限额。</li></ul><p>一个 multicast 包可能需要 clone 给多个 socket。</p><h3 id="六、sock-queue-rcv-skb-与-owner"><a href="#六、sock-queue-rcv-skb-与-owner" class="headerlink" title="六、sock_queue_rcv_skb 与 owner"></a>六、sock_queue_rcv_skb 与 owner</h3><p><code>sock_queue_rcv_skb()</code> 位于 <code>kernel-5.10/net/core/sock.c:474</code>。通用逻辑会检查接收内存和 filter，然后把 skb 入 <code>sk_receive_queue</code>。</p><p><code>skb_set_owner_r(skb, sk)</code> 通常：</p><ul><li>设置 <code>skb-&gt;sk = sk</code>；</li><li>设置 destructor 为 <code>sock_rfree</code>；</li><li>按 <code>skb-&gt;truesize</code> 增加 socket 接收内存计量。</li></ul><p>最终 skb free 时 destructor 归还 rmem。不能只在 dequeue 时手工减包长，因为 clone、frag、truesize 和异步释放会让计量失真。</p><h3 id="七、socket-backlog"><a href="#七、socket-backlog" class="headerlink" title="七、socket backlog"></a>七、socket backlog</h3><p>如果 socket 当前被用户上下文持锁，软中断不能直接执行完整协议处理，通常通过 <code>sk_add_backlog()</code> 把 skb 放入 <code>sk_backlog</code>。</p><ul><li>backlog 仍属于该 socket；</li><li>socket 解锁时调用 <code>sk_backlog_rcv</code>；</li><li>TCP 对应 <code>tcp_v4_do_rcv()</code>；</li><li>backlog 同样受内存 limit；</li><li>入队失败必须释放并计数 drop。</li></ul><p><code>sk_add_backlog()</code> 定义位于 <code>kernel-5.10/include/net/sock.h:1032</code>。</p><h3 id="八、TCP-接收"><a href="#八、TCP-接收" class="headerlink" title="八、TCP 接收"></a>八、TCP 接收</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">tcp_v4_rcv()</span><br><span class="line">  → 校验 header/checksum</span><br><span class="line">  → socket lookup</span><br><span class="line">  → 未锁：tcp_v4_do_rcv()</span><br><span class="line">  → 已锁：sk_add_backlog()</span><br></pre></td></tr></table></figure><p>关键位置：</p><ul><li><code>tcp_v4_rcv()</code>：<code>kernel-5.10/net/ipv4/tcp_ipv4.c:1936</code>；</li><li><code>tcp_v4_do_rcv()</code>：<code>kernel-5.10/net/ipv4/tcp_ipv4.c:1670</code>。</li></ul><p>TCP 不一定把原 skb 直接放入用户 receive queue。状态机可能：</p><ul><li>消费纯 ACK；</li><li>合并相邻数据；</li><li>把乱序段放入 out-of-order rbtree&#x2F;queue；</li><li>把按序数据进入 receive queue；</li><li>clone&#x2F;split skb；</li><li>因窗口、状态、checksum、RST 等释放。</li></ul><p><code>TCP_SKB_CB</code> 使用 cb 保存 seq&#x2F;end_seq&#x2F;flags&#x2F;sacked&#x2F;timestamp 等 TCP 私有状态。</p><h3 id="九、用户读取与最终释放"><a href="#九、用户读取与最终释放" class="headerlink" title="九、用户读取与最终释放"></a>九、用户读取与最终释放</h3><p>UDP <code>recvmsg</code> 通常从 receive queue dequeue 一个 datagram，复制或映射数据后 consume skb。TCP <code>recvmsg</code> 按字节流消费，一个 skb 可能部分读取、被切分或继续留队。</p><p>最终 users 归零后：</p><ul><li>socket destructor 归还 rmem；</li><li>dst&#x2F;nfct&#x2F;extensions 释放；</li><li>frags&#x2F;frag_list&#x2F;head 释放；</li><li>shell 释放。</li></ul><h3 id="十、字段-所有权变化"><a href="#十、字段-所有权变化" class="headerlink" title="十、字段&#x2F;所有权变化"></a>十、字段&#x2F;所有权变化</h3><table><thead><tr><th>阶段</th><th>关键字段</th><th>owner</th></tr></thead><tbody><tr><td>IP 入口</td><td>network header、IPCB、len trim</td><td>IP stack</td></tr><tr><td>route</td><td>dst、dev 语义</td><td>routing&#x2F;IP</td></tr><tr><td>LOCAL_IN</td><td>nfct&#x2F;mark 可能变化</td><td>Netfilter&#x2F;IP</td></tr><tr><td>UDP&#x2F;TCP lookup</td><td><code>sk</code> 候选</td><td>protocol</td></tr><tr><td>backlog&#x2F;receive queue</td><td>cb、owner、truesize accounting</td><td>socket</td></tr><tr><td>recv&#x2F;free</td><td>data 指针&#x2F;len 可能推进</td><td>user syscall&#x2F;socket</td></tr></tbody></table><h3 id="十一、常见误区"><a href="#十一、常见误区" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li>socket lookup 成功不等于立即进入 receive queue，TCP 状态机和 socket lock 会改变路径。</li><li>UDP datagram 与 TCP byte stream 对 skb 队列语义不同。</li><li>socket 内存按 truesize 计量，不按 <code>skb-&gt;len</code>。</li><li>backlog 不是 netdev backlog；前者是 socket 锁竞争路径。</li><li>IP 分片重组发生在 L4 分发之前。</li><li><code>skb-&gt;sk</code> 不是从驱动开始始终有效，通常在 socket ownership 阶段建立。</li></ol><h3 id="字段与所有权统一核对表-2"><a href="#字段与所有权统一核对表-2" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td>network&#x2F;transport header</td><td>IPv4 与 TCP&#x2F;UDP</td><td>L3&#x2F;L4 解析前保证可访问</td></tr><tr><td><code>sk/destructor</code></td><td>socket queue</td><td>入队后 socket&#x2F;队列持有 skb</td></tr><tr><td>dst&#x2F;nfct</td><td>路由和 Netfilter</td><td>引用随接收、转发或丢弃释放</td></tr></tbody></table><h3 id="最小调用链-1"><a href="#最小调用链-1" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">ip_rcv()</span><br><span class="line">→ Netfilter PRE_ROUTING</span><br><span class="line">→ 路由/local delivery</span><br><span class="line">→ tcp_v4_rcv()/udp_rcv()</span><br><span class="line">→ socket receive/backlog queue</span><br><span class="line">→ recvmsg()/free</span><br></pre></td></tr></table></figure><h2 id="源码阅读路线"><a href="#源码阅读路线" class="headerlink" title="源码阅读路线"></a>源码阅读路线</h2><p>建议不要从 <code>struct sk_buff</code> 声明开始逐字段背诵，而应使用下面的阅读顺序：</p><ol><li>先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造；</li><li>沿成功主线记录 <code>data/len/data_len</code>、header offset、checksum、hash、queue mapping 等字段变化；</li><li>再回到每个失败分支，确认 skb、page、DMA、socket 和记账引用是否回滚；</li><li>最后对照 5.10、6.1 或 BSP 的同名 symbol，区分语义变化、性能优化和 vendor backport。</li></ol><h2 id="本篇总结"><a href="#本篇总结" class="headerlink" title="本篇总结"></a>本篇总结</h2><ol><li>驱动不只交付字节，还要把长度、协议、checksum、hash、VLAN 等硬件结果编码进 skb。</li><li>NAPI&#x2F;GRO&#x2F;RPS 决定包何时批量处理、是否聚合以及在哪个 CPU 继续执行。</li><li>L3&#x2F;L4 逐层消费 header 并把 skb 交给 socket receive&#x2F;backlog queue。</li><li>每个返回码都隐含所有权契约：继续传递、排队、克隆、丢弃或已经消费。</li></ol><p><code>sk_buff</code> 的难点从来不只是字段多，而是<strong>同一个对象不断改变数据视图、元数据状态和持有者</strong>。只要把“布局、字段、所有权、释放”四条线同时画出来，绝大多数网络栈源码都会从零散函数变成可推导的生命周期。</p><h2 id="系列目录"><a href="#系列目录" class="headerlink" title="系列目录"></a>系列目录</h2><ol><li>Linux sk_buff 深度解析（一）：数据结构、四指针与核心字段</li><li>Linux sk_buff 深度解析（二）：非线性数据、引用计数与生命周期</li><li>Linux sk_buff 深度解析（三）：从网卡 DMA 到 Socket 的 RX 路径（本文）</li><li>Linux sk_buff 深度解析（四）：从 sendmsg 到 TX completion</li><li>Linux sk_buff 深度解析（五）：GRO、GSO、Checksum 与多核流调度</li><li>Linux sk_buff 深度解析（六）：Netfilter、tc、eBPF 与 XDP</li><li>Linux sk_buff 深度解析（七）：VLAN、隧道、分片与重组</li><li>Linux sk_buff 深度解析（八）：Socket 内存、零拷贝、丢包与版本演进</li></ol>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/7b2595132f8d/</id>
    <link href="https://2xpang.cn/blog/posts/7b2595132f8d/"/>
    <published>2026-07-14T19:00:00.000Z</published>
    <summary>以 RK3588 stmmac 为驱动实例，追踪 DMA page 构造 skb、NAPI/GRO/RPS、IPv4、TCP/UDP 到 Socket 接收队列的完整所有权变化。</summary>
    <title>Linux sk_buff 深度解析（三）：从网卡 DMA 到 Socket 的 RX 路径</title>
    <updated>2026-07-14T19:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="Linux" scheme="https://2xpang.cn/blog/tags/Linux/"/>
    <category term="网络子系统" scheme="https://2xpang.cn/blog/tags/%E7%BD%91%E7%BB%9C%E5%AD%90%E7%B3%BB%E7%BB%9F/"/>
    <category term="内核源码" scheme="https://2xpang.cn/blog/tags/%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81/"/>
    <category term="sk_buff" scheme="https://2xpang.cn/blog/tags/sk-buff/"/>
    <category term="Linux-5.10" scheme="https://2xpang.cn/blog/tags/Linux-5-10/"/>
    <category term="series:内核网络栈" scheme="https://2xpang.cn/blog/tags/series-%E5%86%85%E6%A0%B8%E7%BD%91%E7%BB%9C%E6%A0%88/"/>
    <category term="内存管理" scheme="https://2xpang.cn/blog/tags/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"/>
    <content>
      <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p><code>sk_buff</code> 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”，而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的<strong>控制对象和所有权载体</strong>。</p><p>深入 linear、frags、frag_list、users、dataref、page ref，串联 sk_buff 的分配、clone、copy、COW 与分层释放。</p><p>本文以本地 <strong>Linux 5.10.209</strong> 源码为主线；涉及演进时对照 <strong>Linux 6.1.99</strong>，涉及网卡驱动时结合 <strong>Firefly RK3588 SDK Linux 5.10.198</strong>。本文是源码分析，不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。</p><p><img src="/blog/img/sk_buff/nonlinear-layout.svg" alt="Linux sk_buff 深度解析（二）：非线性数据、引用计数与生命周期"></p><h2 id="阅读前先建立四个问题"><a href="#阅读前先建立四个问题" class="headerlink" title="阅读前先建立四个问题"></a>阅读前先建立四个问题</h2><p>阅读任何 skb 代码路径，都建议反复追问：</p><ol><li>当前 <code>skb-&gt;data</code> 指向哪一层协议头，linear 与 non-linear 数据分别在哪里？</li><li>哪些字段刚被修改，下一层为什么依赖这些字段？</li><li>当前谁持有 skb shell、head、frag page 以及 socket&#x2F;dst&#x2F;conntrack 等外部引用？</li><li>成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb？</li></ol><h2 id="本篇核心结论"><a href="#本篇核心结论" class="headerlink" title="本篇核心结论"></a>本篇核心结论</h2><ul><li><code>len</code> 是逻辑总长度，<code>data_len</code> 是非线性长度，二者不能与 <code>tail-data</code> 混为一谈。</li><li>skb shell、head buffer、frag page 分别由 <code>users</code>、<code>dataref</code>、page ref 保护。</li><li>clone 复制控制对象但共享数据；copy 创建独立数据；修改共享 head 前必须 COW。</li><li>释放不是一次 <code>kfree()</code>，而是按外部引用、数据区和 shell 分层回收。</li></ul><h2 id="1-非线性数据布局：linear、frags-与-frag-list"><a href="#1-非线性数据布局：linear、frags-与-frag-list" class="headerlink" title="1. 非线性数据布局：linear、frags 与 frag_list"></a>1. 非线性数据布局：linear、frags 与 frag_list</h2><h3 id="结论摘要"><a href="#结论摘要" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>一个 skb 的逻辑数据可由 linear head、<code>skb_shared_info.frags[]</code> 和 <code>frag_list</code> 三部分构成。<code>frags[]</code> 描述 page 的片段，适合 scatter-gather、RX page、sendpage 和零拷贝；<code>frag_list</code> 链接完整子 skb，常见于重组、GRO fraglist 或大消息组织。<code>len</code> 统计全部逻辑数据，<code>data_len</code> 统计非线性部分，遍历与修改必须使用能够跨三种布局的 helper。</p><h3 id="一、三种数据组织"><a href="#一、三种数据组织" class="headerlink" title="一、三种数据组织"></a>一、三种数据组织</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">struct sk_buff shell</span><br><span class="line">        │</span><br><span class="line">        ├── head buffer</span><br><span class="line">        │     └── [data, tail): linear data</span><br><span class="line">        │</span><br><span class="line">        └── skb_shared_info</span><br><span class="line">              ├── frags[0..nr_frags-1] → page + offset + size</span><br><span class="line">              └── frag_list → skb → skb → ...</span><br></pre></td></tr></table></figure><p>逻辑顺序通常是：linear 数据在前，随后是 frags，再随后是 frag_list 中的 skb 数据。</p><h3 id="二、skb-frag-t"><a href="#二、skb-frag-t" class="headerlink" title="二、skb_frag_t"></a>二、skb_frag_t</h3><p>本地 5.10 中：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">bio_vec</span> <span class="title">skb_frag_t</span>;</span></span><br></pre></td></tr></table></figure><p>见 <code>kernel-5.10/include/linux/skbuff.h:324</code>。<code>bio_vec</code> 本质描述：</p><ul><li>page；</li><li>page 内 offset；</li><li>片段长度。</li></ul><p>访问应使用：</p><ul><li><code>skb_frag_page()</code>；</li><li><code>skb_frag_off()</code>；</li><li><code>skb_frag_size()</code>；</li><li>对应 set&#x2F;add&#x2F;sub helper。</li></ul><p>不要直接依赖 <code>bio_vec</code> 内部字段名，这些 helper 同时表达 skb 层的抽象边界。</p><h3 id="三、MAX-SKB-FRAGS"><a href="#三、MAX-SKB-FRAGS" class="headerlink" title="三、MAX_SKB_FRAGS"></a>三、MAX_SKB_FRAGS</h3><p><code>kernel-5.10/include/linux/skbuff.h:305-317</code> 说明设计目标：允许一个约 64 KiB frame 由 page fragments 表达，同时至少保留 16 个 frag 供 GRO 使用。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">if</span> (65536/PAGE_SIZE + 1) &lt; 16</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MAX_SKB_FRAGS 16UL</span></span><br><span class="line"><span class="meta">#<span class="keyword">else</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MAX_SKB_FRAGS (65536/PAGE_SIZE + 1)</span></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure><p><code>nr_frags</code> 必须不超过有效上限。驱动或 GSO 合并前需要检查 frag 数量，不能无限追加。</p><h3 id="四、frags-的生命周期"><a href="#四、frags-的生命周期" class="headerlink" title="四、frags[] 的生命周期"></a>四、frags[] 的生命周期</h3><p><code>skb_add_rx_frag()</code> 声明于 <code>kernel-5.10/include/linux/skbuff.h:2236-2237</code>。它把 page&#x2F;offset&#x2F;size 写入指定 frag，并同步更新：</p><ul><li><code>nr_frags</code>；</li><li><code>skb-&gt;len</code>；</li><li><code>skb-&gt;data_len</code>；</li><li><code>skb-&gt;truesize</code>。</li></ul><p>重要点：把 page 放入 frag 后，skb 释放路径会对该 page 执行 frag unref。驱动必须明确是否已经把 page 所有权转交给 skb，不能在转交后又直接回收同一引用。</p><p>clone 时多个 skb 可能共享 frag page，需要增加 page ref；最后一个引用释放时 page 才真正回收。Linux 6.1 还可结合 <code>pp_recycle</code> 把 RX page 返回 page_pool。</p><h3 id="五、frag-list"><a href="#五、frag-list" class="headerlink" title="五、frag_list"></a>五、frag_list</h3><p><code>skb_shared_info.frag_list</code> 是一个 <code>struct sk_buff *</code> 链表，而不是 page 数组。每个成员本身又可以有 linear&#x2F;frags&#x2F;frag_list。</p><p>适用场景：</p><ul><li>IP 重组后用一个 head skb 组织后续分片 skb；</li><li>某些 GRO fraglist 路径；</li><li>大消息或已有 skb 链包装。</li></ul><p>遍历使用：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">skb_walk_frags(skb, iter)</span><br></pre></td></tr></table></figure><p>释放时 <code>skb_release_data()</code> 调用 <code>kfree_skb_list(shinfo-&gt;frag_list)</code>，见 <code>kernel-5.10/net/core/skbuff.c:621-622</code>。clone frag_list 时需要对每个子 skb <code>skb_get()</code>。</p><h3 id="六、长度关系"><a href="#六、长度关系" class="headerlink" title="六、长度关系"></a>六、长度关系</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">skb-&gt;len       = linear + frags + frag_list</span><br><span class="line">skb-&gt;data_len  = frags + frag_list</span><br><span class="line">skb_headlen()  = skb-&gt;len - skb-&gt;data_len</span><br></pre></td></tr></table></figure><p><code>skb_is_nonlinear()</code> 直接检查 <code>data_len</code>，见 <code>kernel-5.10/include/linux/skbuff.h:2159-2167</code>。</p><p><code>skb_pagelen()</code> 主要统计 linear + page frags；若还存在 frag_list，完整逻辑遍历需要继续进入子 skb，不能把 pagelen 与总 len 混为一谈。</p><h3 id="七、truesize"><a href="#七、truesize" class="headerlink" title="七、truesize"></a>七、truesize</h3><p>追加 frag 时调用者传入 <code>truesize</code> 增量。它反映 skb 对内存系统和 socket accounting 的成本，不一定等于 frag size：</p><ul><li>一个小片段可能独占较大 page；</li><li>page_pool、page fragment 或复用策略会改变计量；</li><li>frag_list 子 skb 自身也有 truesize。</li></ul><p>错误的 truesize 会造成 socket 内存限制失真：过小可能让 socket 持有过多真实内存，过大则过早触发限额。</p><h3 id="八、skb-copy-bits"><a href="#八、skb-copy-bits" class="headerlink" title="八、skb_copy_bits"></a>八、skb_copy_bits</h3><p><code>skb_copy_bits()</code> 是跨布局读取的通用 helper。它按逻辑 offset 依次处理：</p><ol><li>linear 区；</li><li><code>frags[]</code> 中的 page 片段；</li><li><code>frag_list</code> 子 skb，必要时递归。</li></ol><p>因此协议、加密、分段等代码不应自行假设数据连续。只要需要跨越不确定边界，就应使用能够处理非线性布局的 helper。</p><p>同类 helper 包括 <code>skb_store_bits()</code>、checksum&#x2F;copy iterator 等。</p><h3 id="九、linearize"><a href="#九、linearize" class="headerlink" title="九、linearize"></a>九、linearize</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">skb_linearize(skb)</span><br></pre></td></tr></table></figure><p>若 skb 已是线性，直接返回 0；否则调用 <code>__skb_linearize()</code>，见 <code>kernel-5.10/include/linux/skbuff.h:3368-3371</code>。</p><p>linearize 的目标是把逻辑数据整理到连续 head buffer。它可能：</p><ul><li>分配更大的 head；</li><li>从 pages&#x2F;frag_list 复制数据；</li><li>释放或减少原 frag 引用；</li><li>改变 head&#x2F;data&#x2F;tail 的绝对地址；</li><li>失败并返回 <code>-ENOMEM</code>。</li></ul><p>代价与包长有关，热路径应尽量避免无条件 linearize。协议只需要前 N 字节连续时，优先 <code>pskb_may_pull(N)</code>。</p><h3 id="十、frags-与-frag-list-对比"><a href="#十、frags-与-frag-list-对比" class="headerlink" title="十、frags 与 frag_list 对比"></a>十、frags 与 frag_list 对比</h3><table><thead><tr><th>维度</th><th><code>frags[]</code></th><th><code>frag_list</code></th></tr></thead><tbody><tr><td>元素</td><td>page 片段</td><td>完整 skb</td></tr><tr><td>数量</td><td><code>nr_frags</code>、有上限</td><td>skb 链表</td></tr><tr><td>引用</td><td>page ref</td><td>skb <code>users</code></td></tr><tr><td>常见来源</td><td>RX page、sendpage、SG、zerocopy</td><td>重组、GRO fraglist、大消息</td></tr><tr><td>遍历</td><td>frag helper</td><td><code>skb_walk_frags()</code></td></tr><tr><td>释放</td><td><code>__skb_frag_unref()</code></td><td><code>kfree_skb_list()</code></td></tr><tr><td>每个元素可带元数据</td><td>否，只有 page&#x2F;offset&#x2F;size</td><td>是，每个子 skb 有完整 metadata</td></tr></tbody></table><h3 id="十一、数据布局不变量"><a href="#十一、数据布局不变量" class="headerlink" title="十一、数据布局不变量"></a>十一、数据布局不变量</h3><ol><li><code>nr_frags &lt;= MAX_SKB_FRAGS</code>。</li><li>每个 frag 的 page 引用在 skb 持有期间有效。</li><li><code>len &gt;= data_len</code>。</li><li><code>skb_headlen = len - data_len</code>。</li><li>frag_list 成员不能形成非法循环。</li><li>修改 frag size&#x2F;offset 时必须同步长度和 accounting。</li><li>需要写 page 数据前必须确认可写性和共享状态。</li></ol><h3 id="十二、常见误区"><a href="#十二、常见误区" class="headerlink" title="十二、常见误区"></a>十二、常见误区</h3><ol><li>非线性不代表“数据不连续的顺序不确定”，逻辑字节顺序仍明确。</li><li><code>frags[]</code> 不是 IP fragments；前者是内存布局，后者是网络层分片协议。</li><li><code>frag_list</code> 不是普通 <code>sk_buff_head</code>，它是通过 skb <code>next</code> 串起来的子 skb 链。</li><li><code>data_len</code> 不只统计 <code>frags[]</code>，也包含 frag_list 数据。</li><li>linearize 不是零成本，且可能让旧 header 指针失效。</li><li>page 放入 skb frag 后不能继续按驱动私有所有权随意释放。</li></ol><h3 id="字段与所有权统一核对表"><a href="#字段与所有权统一核对表" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>data_len/nr_frags</code></td><td>追加 page frag</td><td>skb 持有 page 引用，free 时归还</td></tr><tr><td><code>frag_list</code></td><td>聚合&#x2F;重组路径</td><td>父 skb 持有子 skb 链</td></tr><tr><td><code>len/truesize</code></td><td>追加、分段、重组</td><td>必须同步逻辑长度和内存计量</td></tr></tbody></table><h3 id="最小调用链"><a href="#最小调用链" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">linear head</span><br><span class="line">→ 添加 frags[] 或 frag_list</span><br><span class="line">→ 更新 len/data_len/truesize</span><br><span class="line">→ 协议处理或分段</span><br><span class="line">→ 逐层释放 child skb/page ref/head</span><br></pre></td></tr></table></figure><h2 id="2-分配、clone、copy、COW-与释放"><a href="#2-分配、clone、copy、COW-与释放" class="headerlink" title="2. 分配、clone、copy、COW 与释放"></a>2. 分配、clone、copy、COW 与释放</h2><p><img src="/blog/img/sk_buff/clone-copy.svg" alt="分配、clone、copy、COW 与释放"></p><h3 id="结论摘要-1"><a href="#结论摘要-1" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>skb 生命周期至少包含三类对象：skb shell、共享 head buffer&#x2F;shared info、frag page&#x2F;frag_list。<code>users</code> 保护 shell，<code>dataref</code> 保护共享 head，page ref 或子 skb users 保护非线性数据。clone 创建新 shell 并共享数据；copy 创建新 shell 和新 head 并复制数据；COW 在真正写入前把需要修改的 header&#x2F;head 变为独占。释放路径按 head state、data、shell 分层进行。</p><h3 id="一、分配路径"><a href="#一、分配路径" class="headerlink" title="一、分配路径"></a>一、分配路径</h3><h3 id="alloc-skb"><a href="#alloc-skb" class="headerlink" title="__alloc_skb"></a>__alloc_skb</h3><p>入口：<code>kernel-5.10/net/core/skbuff.c:183-263</code>。</p><p>主要步骤：</p><ol><li>根据 <code>SKB_ALLOC_FCLONE</code> 选择 <code>skbuff_fclone_cache</code> 或 <code>skbuff_head_cache</code>；</li><li>从 slab 分配 skb shell；</li><li>对请求 size 做 cacheline 对齐，并增加 shared info 空间；</li><li><code>kmalloc_reserve()</code> 分配 head buffer；</li><li>根据实际 <code>ksize()</code> 把 shared info 放到分配区尾部；</li><li>初始化 <code>head=data=tail</code>、end、truesize；</li><li>设置 <code>users=1</code>、<code>dataref=1</code>；</li><li>初始化 fclone 状态。</li></ol><p>skb shell 与 head 分开分配，使 clone 可以只创建新 shell 而共享大块数据。</p><h3 id="build-skb"><a href="#build-skb" class="headerlink" title="build_skb"></a>build_skb</h3><p><code>build_skb()</code> 位于 <code>kernel-5.10/net/core/skbuff.c:331</code>，用于把调用者提供的已有 buffer 包装成 skb。驱动可把 page fragment、RX buffer 或专用分配器得到的内存转为 skb，避免再次复制 payload。</p><p>调用者必须为尾部 shared info 留出空间，并明确 head buffer 的释放方式。<code>head_frag</code> 标志影响 <code>skb_free_head()</code> 使用 <code>skb_free_frag()</code> 还是 <code>kfree()</code>。</p><h3 id="napi-alloc-skb"><a href="#napi-alloc-skb" class="headerlink" title="napi_alloc_skb"></a>napi_alloc_skb</h3><p>NAPI 场景使用 <code>napi_alloc_skb()</code>，本地 5.10 的实现会针对小 buffer&#x2F;page fragment 和 NAPI 上下文优化分配。释放时 <code>napi_consume_skb()</code> 还能把 skb shell 延迟放入 per-CPU cache 路径，而不是立即 slab free。</p><h3 id="二、三层引用关系"><a href="#二、三层引用关系" class="headerlink" title="二、三层引用关系"></a>二、三层引用关系</h3><h3 id="skb-users"><a href="#skb-users" class="headerlink" title="skb-&gt;users"></a>skb-&gt;users</h3><p>保护 <code>struct sk_buff</code> shell。<code>skb_get()</code> 增加 users，<code>kfree_skb()</code>&#x2F;<code>consume_skb()</code> 先通过 <code>skb_unref()</code> 减少引用，只有归零才进入真正释放。</p><h3 id="skb-shared-info-dataref"><a href="#skb-shared-info-dataref" class="headerlink" title="skb_shared_info.dataref"></a>skb_shared_info.dataref</h3><p>保护共享 head&#x2F;data 区。clone 增加 dataref。低 16 位表示完整数据引用，高 16 位表示 payload-only 引用，相关说明位于 <code>kernel-5.10/include/linux/skbuff.h:543-550</code>。</p><h3 id="frag-page-frag-list-引用"><a href="#frag-page-frag-list-引用" class="headerlink" title="frag page &#x2F; frag_list 引用"></a>frag page &#x2F; frag_list 引用</h3><ul><li><code>frags[]</code>：page ref；</li><li><code>frag_list</code>：每个子 skb 的 users；</li><li>zerocopy：<code>ubuf_info</code> 等额外引用。</li></ul><p>这解释了为什么 <code>users</code> 归零并不代表可以直接 kfree head&#x2F;page。</p><h3 id="三、skb-clone"><a href="#三、skb-clone" class="headerlink" title="三、skb_clone"></a>三、skb_clone</h3><p><code>__skb_clone()</code> 位于 <code>kernel-5.10/net/core/skbuff.c:991-1021</code>。</p><p>它创建或取得一个新 skb shell，然后：</p><ul><li>清空新 shell 的 queue 链接；</li><li>不复制旧 <code>sk</code>；</li><li>复制协议&#x2F;header&#x2F;offload 元数据；</li><li>复制 len&#x2F;data_len&#x2F;tail&#x2F;end&#x2F;head&#x2F;data&#x2F;truesize；</li><li>新 shell <code>users=1</code>；</li><li>新 shell destructor 置 NULL；</li><li><code>dataref++</code>；</li><li>原 skb 与新 skb 都标记 cloned。</li></ul><p>结果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">skb A shell ─┐</span><br><span class="line">             ├── shared head buffer + shared_info + frags</span><br><span class="line">skb B shell ─┘</span><br></pre></td></tr></table></figure><p>两个 shell 的 next&#x2F;prev、部分 metadata 可以独立变化，但直接修改共享 head 中的字节前必须 COW。</p><h3 id="fclone"><a href="#fclone" class="headerlink" title="fclone"></a>fclone</h3><p>fclone slab 一次预留 original 和一个常用 clone shell，减少典型 TX clone 场景再次从 slab 分配的成本。它优化 shell 分配，不改变 head 数据共享语义。</p><h3 id="四、skb-copy"><a href="#四、skb-copy" class="headerlink" title="四、skb_copy"></a>四、skb_copy</h3><p><code>skb_copy()</code> 位于 <code>kernel-5.10/net/core/skbuff.c:1519</code>。它分配新的 skb 和足够 headroom，然后复制整个逻辑数据，包括非线性内容，形成独立 linear 数据副本，再复制 header metadata。</p><p>结果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">skb A shell → head A</span><br><span class="line">skb B shell → head B</span><br></pre></td></tr></table></figure><p>一般不共享 payload，代价是按包长度复制和更大内存开销。需要完全独立修改、设备不支持 SG、或必须线性数据时使用。</p><h3 id="五、pskb-copy"><a href="#五、pskb-copy" class="headerlink" title="五、pskb_copy"></a>五、pskb_copy</h3><p><code>__pskb_copy_fclone()</code> 位于 <code>kernel-5.10/net/core/skbuff.c:1558</code>。partial copy 的核心是：</p><ul><li>复制 linear head；</li><li>共享或增加引用保存 frags&#x2F;frag_list；</li><li>新 skb 可以独立修改 linear header；</li><li>非线性 payload 仍然共享。</li></ul><p>因此可概括为：</p><table><thead><tr><th>API</th><th>shell</th><th>linear head</th><th>frags&#x2F;page</th></tr></thead><tbody><tr><td><code>skb_clone</code></td><td>新</td><td>共享</td><td>共享</td></tr><tr><td><code>pskb_copy</code></td><td>新</td><td>复制</td><td>共享</td></tr><tr><td><code>skb_copy</code></td><td>新</td><td>复制</td><td>数据也复制&#x2F;线性化为独立内容</td></tr></tbody></table><p>具体实现还受 headroom、fclone、zerocopy、frag_list 等影响，不能只按名字判断所有细节。</p><h3 id="六、headerless-clone-与-nohdr"><a href="#六、headerless-clone-与-nohdr" class="headerlink" title="六、headerless clone 与 nohdr"></a>六、headerless clone 与 nohdr</h3><p>传输层可能只希望 clone payload，让下层为每个 clone 构造不同 header。<code>nohdr</code> 和 <code>hdr_len</code> 配合 dataref 高位区分 payload-only 引用。</p><p>基本目标：</p><ul><li>payload 可共享；</li><li>原 skb 保留可修改 header 的能力；</li><li>clone 不应擅自修改共享 header。</li></ul><p>这也是 <code>skb_header_cloned()</code> 与普通 <code>skb_cloned()</code> 分开的原因：数据区共享不必然意味着 header 当前不可写，要结合 full-data ref 与 payload-only ref 判断。</p><h3 id="七、COW"><a href="#七、COW" class="headerlink" title="七、COW"></a>七、COW</h3><p>clone 之后若要改 header：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">检查 head 是否共享、headroom 是否足够</span><br><span class="line">  ├─ 已独占且空间足：直接写</span><br><span class="line">  └─ 共享或空间不足：pskb_expand_head() 创建新 head</span><br></pre></td></tr></table></figure><p><code>skb_cow_head()</code> 是常见入口。成功后调用者可写 header；失败通常是 <code>-ENOMEM</code>，必须停止修改并走错误释放路径。</p><p>COW 的性能价值是推迟复制：只读转发或观察路径不复制，只有真正写入时付出成本。</p><h3 id="八、释放入口语义"><a href="#八、释放入口语义" class="headerlink" title="八、释放入口语义"></a>八、释放入口语义</h3><h3 id="kfree-skb"><a href="#kfree-skb" class="headerlink" title="kfree_skb"></a>kfree_skb</h3><p><code>kernel-5.10/net/core/skbuff.c:705-713</code>。它表示丢弃或错误释放语义，并触发 <code>trace_kfree_skb</code>。先减少 users，归零后 <code>__kfree_skb()</code>。</p><h3 id="consume-skb"><a href="#consume-skb" class="headerlink" title="consume_skb"></a>consume_skb</h3><p><code>kernel-5.10/net/core/skbuff.c:844</code>。表示正常消费完成，例如成功 TX completion。内存释放结果可能相同，但 trace&#x2F;可观测语义不同。</p><h3 id="napi-consume-skb"><a href="#napi-consume-skb" class="headerlink" title="napi_consume_skb"></a>napi_consume_skb</h3><p><code>kernel-5.10/net/core/skbuff.c:908-930</code>。在有效 NAPI budget 场景，普通 skb shell 可进入延迟&#x2F;per-CPU 回收；fclone 等特殊情况直接完整释放。budget 为 0 时回退通用上下文路径。</p><h3 id="九、分层释放"><a href="#九、分层释放" class="headerlink" title="九、分层释放"></a>九、分层释放</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">kfree_skb()/consume_skb()</span><br><span class="line">  → skb_unref(users)</span><br><span class="line">  → __kfree_skb()</span><br><span class="line">      → skb_release_all()</span><br><span class="line">          → skb_release_head_state()</span><br><span class="line">          → skb_release_data()</span><br><span class="line">      → kfree_skbmem()</span><br></pre></td></tr></table></figure><h3 id="skb-release-head-state"><a href="#skb-release-head-state" class="headerlink" title="skb_release_head_state"></a>skb_release_head_state</h3><p><code>kernel-5.10/net/core/skbuff.c:661-672</code>：</p><ul><li><code>skb_dst_drop()</code>；</li><li>调用 destructor；</li><li><code>nf_conntrack_put()</code>；</li><li><code>skb_ext_put()</code>。</li></ul><p>这是 skb shell 附带的外部状态。</p><h3 id="skb-release-data"><a href="#skb-release-data" class="headerlink" title="skb_release_data"></a>skb_release_data</h3><p><code>kernel-5.10/net/core/skbuff.c:608-626</code>：</p><ol><li>若 cloned，减少 dataref；仍有人共享则立即返回；</li><li>unref 每个 frag page；</li><li>释放 frag_list 子 skb；</li><li>清理 zerocopy；</li><li>根据 <code>head_frag</code> 释放 head。</li></ol><h3 id="kfree-skbmem"><a href="#kfree-skbmem" class="headerlink" title="kfree_skbmem"></a>kfree_skbmem</h3><p>根据 fclone 状态释放普通 shell 或整个 fclone 对象。</p><h3 id="十、所有权转移原则"><a href="#十、所有权转移原则" class="headerlink" title="十、所有权转移原则"></a>十、所有权转移原则</h3><p>阅读任何返回 <code>struct sk_buff *</code> 或接受 skb 的 API 时，都要问：</p><ol><li>成功后调用者是否仍持有 skb？</li><li>失败时函数是否已经消费 skb？</li><li>clone 是增加 shell 引用还是创建新 shell？</li><li>数据引用是否独立增加？</li><li>destructor、dst、nfct、zerocopy 是否被转移或复制？</li></ol><p>网络栈常见 bug 不是算法错误，而是 double free、漏 ref、错误 orphan 或 busy 返回后错误消费 skb。</p><h3 id="十一、常见误区"><a href="#十一、常见误区" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><ol><li>clone 不是浅拷贝一个 C 结构体；它有明确的引用和字段复制规则。</li><li><code>cloned=1</code> 是快速提示，最终共享状态还看 dataref。</li><li><code>skb_copy()</code> 与 <code>pskb_copy()</code> 不同，后者仍可共享非线性 payload。</li><li><code>kfree_skb()</code> 不等于 <code>kfree()</code>。</li><li>destructor 主要处理 head state&#x2F;owner，不直接替代 frag&#x2F;head 的通用释放。</li><li><code>consume_skb()</code> 与 <code>kfree_skb()</code> 的差异对 drop 可观测性很重要。</li><li>users、dataref、page ref、socket ref、dst ref 是不同层次，不能用一个“skb 引用计数”笼统概括。</li></ol><h3 id="5-10-到-6-1-的预告"><a href="#5-10-到-6-1-的预告" class="headerlink" title="5.10 到 6.1 的预告"></a>5.10 到 6.1 的预告</h3><p>Linux 6.1 在这些路径上的重要演进包括：</p><ul><li><code>napi_skb_cache_get/put()</code> 与 bulk slab 操作；</li><li><code>pp_recycle</code> 让 frag&#x2F;head page 在 skb free 路径回到 page_pool；</li><li><code>kfree_skb_reason()</code> 改进丢包语义；</li><li>XDP multi-buffer 与 shared info 更新。</li></ul><p>详细差异放在阶段 21。</p><h3 id="字段与所有权统一核对表-1"><a href="#字段与所有权统一核对表-1" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>users</code></td><td>skb_get&#x2F;consume</td><td>保护 skb shell</td></tr><tr><td><code>dataref</code></td><td>clone&#x2F;COW&#x2F;free</td><td>保护共享 head 和 shared info</td></tr><tr><td>frag page ref</td><td>clone&#x2F;copy&#x2F;free</td><td>保护 page frag；深拷贝与共享路径不同</td></tr></tbody></table><h3 id="最小调用链-1"><a href="#最小调用链-1" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">__alloc_skb()/build_skb()</span><br><span class="line">→ skb_clone() 共享数据</span><br><span class="line">→ skb_copy()/COW 获得私有数据</span><br><span class="line">→ consume/kfree</span><br><span class="line">→ users/dataref/page ref 分层归零</span><br></pre></td></tr></table></figure><h2 id="从局部机制回到完整路径"><a href="#从局部机制回到完整路径" class="headerlink" title="从局部机制回到完整路径"></a>从局部机制回到完整路径</h2><p><img src="/blog/img/sk_buff/release-lifecycle.svg" alt="完整路径关系"></p><p>前面的局部机制只有放回完整生命周期才不容易误判：数据布局决定 helper 能否直接访问；引用计数决定能否原地修改；队列和驱动返回值决定谁仍然拥有 skb。遇到异常路径时，应按“外部引用 → 数据区 → shell”的逆序检查回收。</p><h2 id="源码阅读路线"><a href="#源码阅读路线" class="headerlink" title="源码阅读路线"></a>源码阅读路线</h2><p>建议不要从 <code>struct sk_buff</code> 声明开始逐字段背诵，而应使用下面的阅读顺序：</p><ol><li>先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造；</li><li>沿成功主线记录 <code>data/len/data_len</code>、header offset、checksum、hash、queue mapping 等字段变化；</li><li>再回到每个失败分支，确认 skb、page、DMA、socket 和记账引用是否回滚；</li><li>最后对照 5.10、6.1 或 BSP 的同名 symbol，区分语义变化、性能优化和 vendor backport。</li></ol><h2 id="本篇总结"><a href="#本篇总结" class="headerlink" title="本篇总结"></a>本篇总结</h2><ol><li><code>len</code> 是逻辑总长度，<code>data_len</code> 是非线性长度，二者不能与 <code>tail-data</code> 混为一谈。</li><li>skb shell、head buffer、frag page 分别由 <code>users</code>、<code>dataref</code>、page ref 保护。</li><li>clone 复制控制对象但共享数据；copy 创建独立数据；修改共享 head 前必须 COW。</li><li>释放不是一次 <code>kfree()</code>，而是按外部引用、数据区和 shell 分层回收。</li></ol><p><code>sk_buff</code> 的难点从来不只是字段多，而是<strong>同一个对象不断改变数据视图、元数据状态和持有者</strong>。只要把“布局、字段、所有权、释放”四条线同时画出来，绝大多数网络栈源码都会从零散函数变成可推导的生命周期。</p><h2 id="系列目录"><a href="#系列目录" class="headerlink" title="系列目录"></a>系列目录</h2><ol><li>Linux sk_buff 深度解析（一）：数据结构、四指针与核心字段</li><li>Linux sk_buff 深度解析（二）：非线性数据、引用计数与生命周期（本文）</li><li>Linux sk_buff 深度解析（三）：从网卡 DMA 到 Socket 的 RX 路径</li><li>Linux sk_buff 深度解析（四）：从 sendmsg 到 TX completion</li><li>Linux sk_buff 深度解析（五）：GRO、GSO、Checksum 与多核流调度</li><li>Linux sk_buff 深度解析（六）：Netfilter、tc、eBPF 与 XDP</li><li>Linux sk_buff 深度解析（七）：VLAN、隧道、分片与重组</li><li>Linux sk_buff 深度解析（八）：Socket 内存、零拷贝、丢包与版本演进</li></ol>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/ecc83587102b/</id>
    <link href="https://2xpang.cn/blog/posts/ecc83587102b/"/>
    <published>2026-07-14T18:00:00.000Z</published>
    <summary>深入 linear、frags、frag_list、users、dataref、page ref，串联 sk_buff 的分配、clone、copy、COW 与分层释放。</summary>
    <title>Linux sk_buff 深度解析（二）：非线性数据、引用计数与生命周期</title>
    <updated>2026-07-14T18:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="Linux" scheme="https://2xpang.cn/blog/tags/Linux/"/>
    <category term="网络子系统" scheme="https://2xpang.cn/blog/tags/%E7%BD%91%E7%BB%9C%E5%AD%90%E7%B3%BB%E7%BB%9F/"/>
    <category term="内核源码" scheme="https://2xpang.cn/blog/tags/%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81/"/>
    <category term="sk_buff" scheme="https://2xpang.cn/blog/tags/sk-buff/"/>
    <category term="Linux-5.10" scheme="https://2xpang.cn/blog/tags/Linux-5-10/"/>
    <category term="series:内核网络栈" scheme="https://2xpang.cn/blog/tags/series-%E5%86%85%E6%A0%B8%E7%BD%91%E7%BB%9C%E6%A0%88/"/>
    <content>
      <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p><code>sk_buff</code> 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”，而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的<strong>控制对象和所有权载体</strong>。</p><p>从 sk_buff 为什么不是一个简单 buffer 出发，系统解释 skb shell、linear head、shared info、四指针、header offset、核心字段与数据区操作。</p><p>本文以本地 <strong>Linux 5.10.209</strong> 源码为主线；涉及演进时对照 <strong>Linux 6.1.99</strong>，涉及网卡驱动时结合 <strong>Firefly RK3588 SDK Linux 5.10.198</strong>。本文是源码分析，不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。</p><p><img src="/blog/img/sk_buff/memory-layout.svg" alt="Linux sk_buff 深度解析（一）：数据结构、四指针与核心字段"></p><h2 id="阅读前先建立四个问题"><a href="#阅读前先建立四个问题" class="headerlink" title="阅读前先建立四个问题"></a>阅读前先建立四个问题</h2><p>阅读任何 skb 代码路径，都建议反复追问：</p><ol><li>当前 <code>skb-&gt;data</code> 指向哪一层协议头，linear 与 non-linear 数据分别在哪里？</li><li>哪些字段刚被修改，下一层为什么依赖这些字段？</li><li>当前谁持有 skb shell、head、frag page 以及 socket&#x2F;dst&#x2F;conntrack 等外部引用？</li><li>成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb？</li></ol><h2 id="本篇核心结论"><a href="#本篇核心结论" class="headerlink" title="本篇核心结论"></a>本篇核心结论</h2><ul><li>skb 是控制对象，包数据可能分散在 linear head、page frags 和子 skb 中。</li><li><code>head &lt;= data &lt;= tail &lt;= end</code> 描述 linear head，不等于整个逻辑包。</li><li>协议头使用相对 <code>head</code> 的 offset，因而能够承受 push&#x2F;pull 和 head 重分配。</li><li>读字段时要同时问：谁写入、谁读取、当前谁持有 skb。</li></ul><h2 id="1-为什么-Linux-网络栈需要-sk-buff"><a href="#1-为什么-Linux-网络栈需要-sk-buff" class="headerlink" title="1. 为什么 Linux 网络栈需要 sk_buff"></a>1. 为什么 Linux 网络栈需要 sk_buff</h2><h3 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h3><ol><li>Linux 网络栈为什么不只传递一个普通 buffer 指针？</li><li><code>head/data/tail/end</code> 分别表示什么？</li><li>headroom、linear data、tailroom 和 <code>skb_shared_info</code> 如何布局？</li><li>skb 本体、head buffer、page frag 为什么有不同生命周期？</li><li>一个新 skb 从分配、构造到释放的基本过程是什么？</li></ol><h3 id="结论摘要"><a href="#结论摘要" class="headerlink" title="结论摘要"></a>结论摘要</h3><p><code>struct sk_buff</code> 不是“网络包数据本身”，而是网络包的<strong>控制对象和元数据容器</strong>。真正的线性数据通常位于独立的 head buffer 中，非线性 payload 还可能位于 page fragments 或其它 skb 组成的 <code>frag_list</code> 中。</p><p>贯穿本阶段的长度恒等式是：<code>skb-&gt;len = skb_headlen(skb) + skb-&gt;data_len</code>。</p><p>四指针模型描述 head buffer 的线性部分：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">head &lt;= data &lt;= tail &lt;= end</span><br></pre></td></tr></table></figure><ul><li><code>head</code>：已分配 head buffer 的起始地址；</li><li><code>data</code>：当前协议层看到的线性数据起点；</li><li><code>tail</code>：当前线性数据末尾；</li><li><code>end</code>：线性可用区末尾，也是 <code>skb_shared_info</code> 的起始位置；</li><li><code>[head, data)</code>：headroom；</li><li><code>[data, tail)</code>：当前线性数据；</li><li><code>[tail, end)</code>：tailroom，仅在线性 skb 语义下可直接追加。</li></ul><p>需要特别注意：在 64 位内核中，本地源码定义了 <code>NET_SKBUFF_DATA_USES_OFFSET</code>，所以 <code>tail</code> 和 <code>end</code> 在结构体里实际是相对 <code>head</code> 的无符号 offset，而不是 C 指针。所谓“四指针模型”是概念模型；访问实际地址应使用 <code>skb_tail_pointer()</code>、<code>skb_end_pointer()</code> 等 helper。</p><h3 id="一、为什么不能只使用普通-buffer-指针"><a href="#一、为什么不能只使用普通-buffer-指针" class="headerlink" title="一、为什么不能只使用普通 buffer 指针"></a>一、为什么不能只使用普通 buffer 指针</h3><p>网络包经过驱动、二层、三层、四层、Netfilter、qdisc、socket 等多个子系统。仅传递 <code>void *data</code> 无法同时解决以下问题。</p><h3 id="1-协议头需要频繁增加和移除"><a href="#1-协议头需要频繁增加和移除" class="headerlink" title="1. 协议头需要频繁增加和移除"></a>1. 协议头需要频繁增加和移除</h3><p>收包时，协议栈会逐层解析并“移过”当前视图；发包时，会从 payload 开始逐层向前添加 TCP&#x2F;UDP、IP 和 Ethernet header。</p><p>使用 <code>data</code> 指针和预留 headroom 后，可以通过移动指针表达当前协议层，不必每经过一层就复制整个包。</p><h3 id="2-网络包需要排队和调度"><a href="#2-网络包需要排队和调度" class="headerlink" title="2. 网络包需要排队和调度"></a>2. 网络包需要排队和调度</h3><p>skb 自带 <code>next/prev</code>，可以进入 socket queue、backlog、qdisc、TCP 重传队列等。包数据不需要为了排队而改变布局。</p><h3 id="3-网络栈需要携带大量元数据"><a href="#3-网络栈需要携带大量元数据" class="headerlink" title="3. 网络栈需要携带大量元数据"></a>3. 网络栈需要携带大量元数据</h3><p>例如：</p><ul><li>输入&#x2F;输出设备；</li><li>socket 所有者；</li><li>路由 <code>dst</code>；</li><li>MAC、network、transport header offset；</li><li>checksum offload 状态；</li><li>hash、RX&#x2F;TX queue；</li><li>VLAN、GSO、timestamp、mark、priority；</li><li>destructor 和引用计数。</li></ul><p>这些内容不是线速报文的一部分，不能直接塞入报文字节流。</p><h3 id="4-数据可能不是一段连续内存"><a href="#4-数据可能不是一段连续内存" class="headerlink" title="4. 数据可能不是一段连续内存"></a>4. 数据可能不是一段连续内存</h3><p>高性能收发包会使用 page fragment、scatter-gather、GRO&#x2F;GSO、零拷贝。skb 允许“一个逻辑网络包”由 linear data、<code>frags[]</code> 和 <code>frag_list</code> 共同组成。</p><h3 id="5-元数据与数据需要独立共享"><a href="#5-元数据与数据需要独立共享" class="headerlink" title="5. 元数据与数据需要独立共享"></a>5. 元数据与数据需要独立共享</h3><p>clone 可以创建新的 skb shell，同时共享 head buffer 或 page 数据。这样不同路径可以拥有独立的队列节点和协议元数据，而不复制完整 payload。</p><p>因此可以把 skb 理解为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sk_buff = 包的控制块 + 数据布局描述 + 协议元数据 + 生命周期管理</span><br></pre></td></tr></table></figure><h3 id="二、三个不同的对象"><a href="#二、三个不同的对象" class="headerlink" title="二、三个不同的对象"></a>二、三个不同的对象</h3><p>初学时最重要的是不要把下面三个对象混在一起。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">对象 1：struct sk_buff</span><br><span class="line">  - 从 skbuff_head_cache / skbuff_fclone_cache 分配</span><br><span class="line">  - 保存元数据、队列节点、四指针/offset、len、users 等</span><br><span class="line"></span><br><span class="line">对象 2：linear head buffer</span><br><span class="line">  - 通常通过 kmalloc_reserve() 分配</span><br><span class="line">  - 包含 headroom、linear data、tailroom</span><br><span class="line">  - 尾部紧跟 struct skb_shared_info</span><br><span class="line"></span><br><span class="line">对象 3：非线性数据 page / frag_list skb</span><br><span class="line">  - 由 skb_shared_info.frags[] 或 frag_list 描述</span><br><span class="line">  - 有独立的 page ref 或 skb ref</span><br></pre></td></tr></table></figure><p>它们不是一次分配，也不是靠同一个引用计数保护。</p><h3 id="三、linear-head-buffer-的内存布局"><a href="#三、linear-head-buffer-的内存布局" class="headerlink" title="三、linear head buffer 的内存布局"></a>三、linear head buffer 的内存布局</h3><p>典型布局如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">低地址                                                               高地址</span><br><span class="line"></span><br><span class="line">head          data                     tail              end</span><br><span class="line"> │             │                         │                 │</span><br><span class="line"> ▼             ▼                         ▼                 ▼</span><br><span class="line"> +-------------+-------------------------+-----------------+------------------+</span><br><span class="line"> |  headroom   |      linear data        |    tailroom     | skb_shared_info  |</span><br><span class="line"> +-------------+-------------------------+-----------------+------------------+</span><br><span class="line">               &lt;------- skb_headlen ------&gt;</span><br><span class="line"></span><br><span class="line"> skb_shared_info:</span><br><span class="line">   nr_frags</span><br><span class="line">   gso_size / gso_segs / gso_type</span><br><span class="line">   frag_list</span><br><span class="line">   dataref</span><br><span class="line">   destructor_arg</span><br><span class="line">   frags[MAX_SKB_FRAGS]</span><br></pre></td></tr></table></figure><p><code>skb_shinfo(skb)</code> 的定义直接把 <code>skb_end_pointer(skb)</code> 转换为 <code>struct skb_shared_info *</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> skb_shinfo(SKB) \</span></span><br><span class="line"><span class="meta">    ((struct skb_shared_info *)(skb_end_pointer(SKB)))</span></span><br></pre></td></tr></table></figure><p>位置见 <code>kernel-5.10/include/linux/skbuff.h:1464</code>。</p><p>因此：</p><ul><li><code>end</code> 不是整块 kmalloc 内存的真正末尾；</li><li><code>end</code> 是 linear 可用区的末尾；</li><li>从 <code>end</code> 开始存放 <code>skb_shared_info</code>；</li><li>分配 head buffer 时必须额外给 shared info 留空间。</li></ul><h3 id="四、四指针分别代表什么"><a href="#四、四指针分别代表什么" class="headerlink" title="四、四指针分别代表什么"></a>四、四指针分别代表什么</h3><h3 id="1-head"><a href="#1-head" class="headerlink" title="1. head"></a>1. <code>head</code></h3><p><code>head</code> 是 head buffer 的基址。正常的 push&#x2F;pull&#x2F;put&#x2F;reserve 不改变它，只有重新分配 head 的操作才可能改变它。</p><h3 id="2-data"><a href="#2-data" class="headerlink" title="2. data"></a>2. <code>data</code></h3><p><code>data</code> 是当前线性数据起点，也是协议栈当前视图的起点。</p><ul><li><code>skb_push()</code>：<code>data</code> 向低地址移动，通常用于添加协议头；</li><li><code>skb_pull()</code>：<code>data</code> 向高地址移动，通常用于移除或越过协议头。</li></ul><p><code>data</code> 不一定始终指向 Ethernet header、IP header 或 payload；它的含义取决于当前执行阶段。</p><h3 id="3-tail"><a href="#3-tail" class="headerlink" title="3. tail"></a>3. <code>tail</code></h3><p><code>tail</code> 表示 linear data 的结束位置，也就是下一个可追加字节的位置。<code>skb_put()</code> 会向后推进 tail 并增加 <code>len</code>。</p><p>对于非线性 skb，tail 仍只描述 linear 区结束位置，不描述 page fragment 的末尾。</p><h3 id="4-end"><a href="#4-end" class="headerlink" title="4. end"></a>4. <code>end</code></h3><p><code>end</code> 表示 linear 可用区的末尾，<code>skb_shared_info</code> 从这里开始。</p><p>在 64 位配置下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">skb_end_pointer(skb) = skb-&gt;head + skb-&gt;end;</span><br><span class="line">skb_tail_pointer(skb) = skb-&gt;head + skb-&gt;tail;</span><br></pre></td></tr></table></figure><p>对应：</p><ul><li><code>kernel-5.10/include/linux/skbuff.h:606-614</code></li><li><code>kernel-5.10/include/linux/skbuff.h:1432-1439</code></li><li><code>kernel-5.10/include/linux/skbuff.h:2244-2258</code></li></ul><h3 id="五、必须记住的长度关系"><a href="#五、必须记住的长度关系" class="headerlink" title="五、必须记住的长度关系"></a>五、必须记住的长度关系</h3><h3 id="1-headroom"><a href="#1-headroom" class="headerlink" title="1. headroom"></a>1. headroom</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">headroom = data - head</span><br></pre></td></tr></table></figure><p>实现位于 <code>kernel-5.10/include/linux/skbuff.h:2401-2404</code>。</p><h3 id="2-linear-data-长度"><a href="#2-linear-data-长度" class="headerlink" title="2. linear data 长度"></a>2. linear data 长度</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">skb_headlen(skb) = skb-&gt;len - skb-&gt;data_len</span><br></pre></td></tr></table></figure><p>实现位于 <code>kernel-5.10/include/linux/skbuff.h:2164-2167</code>。</p><h3 id="3-总长度"><a href="#3-总长度" class="headerlink" title="3. 总长度"></a>3. 总长度</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">skb-&gt;len = linear data length + non-linear data length</span><br><span class="line">         = skb_headlen(skb) + skb-&gt;data_len</span><br></pre></td></tr></table></figure><p><code>data_len == 0</code> 表示 skb 是纯线性的；<code>skb_is_nonlinear()</code> 直接检查 <code>data_len</code>，见 <code>kernel-5.10/include/linux/skbuff.h:2159-2162</code>。</p><h3 id="4-tailroom"><a href="#4-tailroom" class="headerlink" title="4. tailroom"></a>4. tailroom</h3><p>概念上：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tailroom = end - tail</span><br></pre></td></tr></table></figure><p>但本地 5.10 的 <code>skb_tailroom()</code> 对非线性 skb 直接返回 0：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> skb_is_nonlinear(skb) ? <span class="number">0</span> : skb-&gt;end - skb-&gt;tail;</span><br></pre></td></tr></table></figure><p>见 <code>kernel-5.10/include/linux/skbuff.h:2412-2415</code>。</p><p>这是一条重要语义：即使 head buffer 中从 tail 到 end 可能仍有物理空间，也不能简单假定普通 <code>skb_put()</code> 能安全用于非线性 skb。</p><h3 id="5-truesize"><a href="#5-truesize" class="headerlink" title="5. truesize"></a>5. <code>truesize</code></h3><p><code>truesize</code> 不是报文长度，而是用于内存计量的近似实际占用。宏 <code>SKB_TRUESIZE()</code> 把以下内容计入：</p><ul><li>数据区容量；</li><li>对齐后的 <code>struct sk_buff</code>；</li><li>对齐后的 <code>struct skb_shared_info</code>。</li></ul><p>定义见 <code>kernel-5.10/include/linux/skbuff.h:238-241</code>。</p><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">truesize 通常大于 len</span><br></pre></td></tr></table></figure><p>后续 socket 内存限制使用的是 <code>truesize</code>，而不是线速包长。</p><h3 id="六、一个新-skb-的初始状态"><a href="#六、一个新-skb-的初始状态" class="headerlink" title="六、一个新 skb 的初始状态"></a>六、一个新 skb 的初始状态</h3><p><code>__alloc_skb()</code> 的关键步骤位于 <code>kernel-5.10/net/core/skbuff.c:183-263</code>。</p><h3 id="1-分配-skb-shell"><a href="#1-分配-skb-shell" class="headerlink" title="1. 分配 skb shell"></a>1. 分配 skb shell</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">skb = kmem_cache_alloc_node(cache, ...);</span><br></pre></td></tr></table></figure><h3 id="2-为数据区和-shared-info-分配连续内存"><a href="#2-为数据区和-shared-info-分配连续内存" class="headerlink" title="2. 为数据区和 shared info 分配连续内存"></a>2. 为数据区和 shared info 分配连续内存</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">size = SKB_DATA_ALIGN(size);</span><br><span class="line">size += SKB_DATA_ALIGN(<span class="keyword">sizeof</span>(<span class="keyword">struct</span> skb_shared_info));</span><br><span class="line">data = kmalloc_reserve(size, ...);</span><br></pre></td></tr></table></figure><p>然后用 <code>SKB_WITH_OVERHEAD()</code> 把 shared info 占用从 linear 可用容量中扣除。</p><h3 id="3-初始化四指针-offset"><a href="#3-初始化四指针-offset" class="headerlink" title="3. 初始化四指针&#x2F;offset"></a>3. 初始化四指针&#x2F;offset</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">skb-&gt;head = data;</span><br><span class="line">skb-&gt;data = data;</span><br><span class="line">skb_reset_tail_pointer(skb);</span><br><span class="line">skb-&gt;end = skb-&gt;tail + size;</span><br></pre></td></tr></table></figure><p>所以刚分配完成时：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">head == data == tail</span><br><span class="line">len == 0</span><br><span class="line">data_len == 0</span><br><span class="line">headroom == 0</span><br><span class="line">tailroom &gt;= requested size</span><br><span class="line">users == 1</span><br><span class="line">shared_info.dataref == 1</span><br></pre></td></tr></table></figure><h3 id="4-初始化两个不同的引用计数"><a href="#4-初始化两个不同的引用计数" class="headerlink" title="4. 初始化两个不同的引用计数"></a>4. 初始化两个不同的引用计数</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">refcount_set(&amp;skb-&gt;users, <span class="number">1</span>);</span><br><span class="line"><span class="type">atomic_set</span>(&amp;shinfo-&gt;dataref, <span class="number">1</span>);</span><br></pre></td></tr></table></figure><ul><li><code>users</code> 保护 skb shell；</li><li><code>dataref</code> 保护共享 head&#x2F;data 区。</li></ul><p>当前阶段只记住两者不是同一个引用计数；clone 细节放到阶段 4。</p><h3 id="七、reserve、put、push、pull-如何改变模型"><a href="#七、reserve、put、push、pull-如何改变模型" class="headerlink" title="七、reserve、put、push、pull 如何改变模型"></a>七、reserve、put、push、pull 如何改变模型</h3><p>假设刚分配的 skb 状态为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">head = data = tail</span><br><span class="line">len = 0</span><br></pre></td></tr></table></figure><h3 id="1-skb-reserve-skb-R"><a href="#1-skb-reserve-skb-R" class="headerlink" title="1. skb_reserve(skb, R)"></a>1. <code>skb_reserve(skb, R)</code></h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">data += R</span><br><span class="line">tail += R</span><br><span class="line">len 不变</span><br></pre></td></tr></table></figure><p>结果是预留 R 字节 headroom。源码明确说明只允许用于空 skb，见 <code>kernel-5.10/include/linux/skbuff.h:2433-2444</code>。</p><h3 id="2-skb-put-skb-N"><a href="#2-skb-put-skb-N" class="headerlink" title="2. skb_put(skb, N)"></a>2. <code>skb_put(skb, N)</code></h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">tail += N</span><br><span class="line">len += N</span><br></pre></td></tr></table></figure><p>它在尾部扩展 linear data，并返回原 tail 地址供调用者写入，见 <code>kernel-5.10/include/linux/skbuff.h:2291-2299</code>。</p><h3 id="3-skb-push-skb-H"><a href="#3-skb-push-skb-H" class="headerlink" title="3. skb_push(skb, H)"></a>3. <code>skb_push(skb, H)</code></h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">data -= H</span><br><span class="line">len += H</span><br></pre></td></tr></table></figure><p>它消耗 headroom，在当前数据前增加 H 字节，常用于添加协议头，见 <code>kernel-5.10/include/linux/skbuff.h:2347-2353</code>。</p><h3 id="4-skb-pull-skb-H"><a href="#4-skb-pull-skb-H" class="headerlink" title="4. skb_pull(skb, H)"></a>4. <code>skb_pull(skb, H)</code></h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">data += H</span><br><span class="line">len -= H</span><br></pre></td></tr></table></figure><p>它越过前 H 字节，使当前视图进入下一层协议，见 <code>kernel-5.10/include/linux/skbuff.h:2355-2361</code>。</p><h3 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">初始：</span><br><span class="line">head/data/tail                                           end</span><br><span class="line">    │                                                     │</span><br><span class="line">    +-----------------------------------------------------+</span><br><span class="line"></span><br><span class="line">reserve(64)：</span><br><span class="line">head        data/tail                                    end</span><br><span class="line"> │             │                                          │</span><br><span class="line"> +-------------+------------------------------------------+</span><br><span class="line">   headroom=64</span><br><span class="line"></span><br><span class="line">put(100)：</span><br><span class="line">head        data                         tail             end</span><br><span class="line"> │             │                           │               │</span><br><span class="line"> +-------------+---------------------------+---------------+</span><br><span class="line">   headroom=64       linear data=100          tailroom</span><br><span class="line"></span><br><span class="line">push(20)：</span><br><span class="line">head   data                              tail             end</span><br><span class="line"> │      │                                  │               │</span><br><span class="line"> +------+----------------------------------+---------------+</span><br><span class="line">   44       linear data=120                   tailroom</span><br></pre></td></tr></table></figure><p>整个过程中没有搬移原有 payload，只修改地址&#x2F;offset 和长度。</p><h3 id="八、为什么-skb-shared-info-放在-end-后面"><a href="#八、为什么-skb-shared-info-放在-end-后面" class="headerlink" title="八、为什么 skb_shared_info 放在 end 后面"></a>八、为什么 skb_shared_info 放在 end 后面</h3><p>源码注释说明 shared info 是跨 clone 共享的不变量，位于 header data 的末尾，见 <code>kernel-5.10/include/linux/skbuff.h:512-515</code>。</p><p>这种布局有几个好处：</p><ol><li>skb shell 与数据 buffer 可以独立分配和共享；</li><li>frag&#x2F;GSO&#x2F;dataref 元数据天然跟随共享数据区；</li><li>linear 数据可以从前向后增长，直到 <code>end</code>；</li><li><code>skb_shinfo(skb)</code> 可以由 end 直接计算，不需要 skb 额外保存一个 shared-info 指针；</li><li>分配器可能给出比请求更大的 kmalloc 块，代码可以把 shared info 放到实际分配区尾部，最大化 linear 可用空间，见 <code>kernel-5.10/net/core/skbuff.c:204-218</code>。</li></ol><h3 id="九、生命周期总览"><a href="#九、生命周期总览" class="headerlink" title="九、生命周期总览"></a>九、生命周期总览</h3><p>当前只建立高层流程：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">分配 skb shell + head buffer</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">reserve headroom</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">put/push 构造数据和协议头</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">驱动 / 协议栈 / Netfilter / qdisc / socket 之间传递或排队</span><br><span class="line">        │</span><br><span class="line">        ├── 可能 clone/copy</span><br><span class="line">        ├── 可能增加 frags/frag_list</span><br><span class="line">        └── 可能绑定 dst/socket/conntrack/destructor</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">consume_skb() 或 kfree_skb()</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">skb_release_head_state() 释放 dst/destructor/conntrack/extensions</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">skb_release_data() 释放 frags/frag_list/head buffer</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">kfree_skbmem() 释放 skb shell</span><br></pre></td></tr></table></figure><p>本地释放路径：</p><ul><li><code>kernel-5.10/net/core/skbuff.c:598-626</code> — head&#x2F;data 与 frags；</li><li><code>kernel-5.10/net/core/skbuff.c:661-680</code> — 外部状态和整体释放；</li><li><code>kernel-5.10/net/core/skbuff.c:691-713</code> — <code>__kfree_skb()</code> &#x2F; <code>kfree_skb()</code>。</li></ul><p>这一流程再次证明：skb shell、外部状态、head buffer 和 frag page 是分层释放的。</p><h3 id="十、阶段-0-的关键不变量"><a href="#十、阶段-0-的关键不变量" class="headerlink" title="十、阶段 0 的关键不变量"></a>十、阶段 0 的关键不变量</h3><h3 id="不变量-1：四指针顺序"><a href="#不变量-1：四指针顺序" class="headerlink" title="不变量 1：四指针顺序"></a>不变量 1：四指针顺序</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">head &lt;= data &lt;= tail &lt;= end</span><br></pre></td></tr></table></figure><p>如果 push 超过 headroom，可能破坏 <code>head &lt;= data</code>；如果 put 超过 tailroom，可能越过 end。checked API 会做更多检查，但不能依赖非法调用自动恢复。</p><h3 id="不变量-2：长度关系"><a href="#不变量-2：长度关系" class="headerlink" title="不变量 2：长度关系"></a>不变量 2：长度关系</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">len &gt;= data_len</span><br><span class="line">skb_headlen = len - data_len</span><br></pre></td></tr></table></figure><p><code>__skb_pull()</code> 中存在：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">BUG_ON(skb-&gt;len &lt; skb-&gt;data_len);</span><br></pre></td></tr></table></figure><p>因为 pull 只能从当前 linear 头部移除数据，不能让总长度小于仍然挂载的非线性长度。</p><h3 id="不变量-3：end-指向-shared-info-起始位置"><a href="#不变量-3：end-指向-shared-info-起始位置" class="headerlink" title="不变量 3：end 指向 shared info 起始位置"></a>不变量 3：end 指向 shared info 起始位置</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">skb_shinfo(skb) == (struct skb_shared_info *)skb_end_pointer(skb)</span><br></pre></td></tr></table></figure><p>任何错误越过 end 的写操作都会破坏 frag、GSO 或引用计数元数据，后果通常不是简单丢一个包，而是内存破坏。</p><h3 id="不变量-4：data-是动态视图，不是固定协议头"><a href="#不变量-4：data-是动态视图，不是固定协议头" class="headerlink" title="不变量 4：data 是动态视图，不是固定协议头"></a>不变量 4：<code>data</code> 是动态视图，不是固定协议头</h3><p>不能看到 <code>skb-&gt;data</code> 就假定它永远指向 Ethernet&#x2F;IP&#x2F;TCP。必须结合当前调用点以及 MAC&#x2F;network&#x2F;transport header offset 判断。</p><h3 id="不变量-5：逻辑包长度不等于-linear-长度"><a href="#不变量-5：逻辑包长度不等于-linear-长度" class="headerlink" title="不变量 5：逻辑包长度不等于 linear 长度"></a>不变量 5：逻辑包长度不等于 linear 长度</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">len != tail - data       // 非线性 skb 时通常不相等</span><br><span class="line">len == tail - data       // 仅可作为纯线性 skb 的典型关系</span><br></pre></td></tr></table></figure><p>更通用的关系是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">linear length = skb_headlen(skb) = len - data_len</span><br></pre></td></tr></table></figure><h3 id="十一、常见误区"><a href="#十一、常见误区" class="headerlink" title="十一、常见误区"></a>十一、常见误区</h3><h3 id="误区-1：“struct-sk-buff-里面装着整个网络包”"><a href="#误区-1：“struct-sk-buff-里面装着整个网络包”" class="headerlink" title="误区 1：“struct sk_buff 里面装着整个网络包”"></a>误区 1：“struct sk_buff 里面装着整个网络包”</h3><p>错误。skb 主要保存元数据，linear 数据位于独立 head buffer，payload 还可能在 page 中。</p><h3 id="误区-2：“四个成员在所有架构上都是指针”"><a href="#误区-2：“四个成员在所有架构上都是指针”" class="headerlink" title="误区 2：“四个成员在所有架构上都是指针”"></a>误区 2：“四个成员在所有架构上都是指针”</h3><p>错误。64 位本地内核中 <code>tail/end</code> 是 offset；四指针是概念模型。</p><h3 id="误区-3：“end-是整块分配内存的最后一个字节”"><a href="#误区-3：“end-是整块分配内存的最后一个字节”" class="headerlink" title="误区 3：“end 是整块分配内存的最后一个字节”"></a>误区 3：“end 是整块分配内存的最后一个字节”</h3><p>错误。<code>end</code> 是 linear 可用区末尾，后面还有 <code>skb_shared_info</code>。</p><h3 id="误区-4：“tail-是整个包的末尾”"><a href="#误区-4：“tail-是整个包的末尾”" class="headerlink" title="误区 4：“tail 是整个包的末尾”"></a>误区 4：“tail 是整个包的末尾”</h3><p>错误。对非线性 skb，tail 只是 linear data 的末尾。</p><h3 id="误区-5：“len-等于-tail-data”"><a href="#误区-5：“len-等于-tail-data”" class="headerlink" title="误区 5：“len 等于 tail - data”"></a>误区 5：“len 等于 tail - data”</h3><p>只对纯线性 skb 成立。非线性 skb 还要包含 <code>data_len</code>。</p><h3 id="误区-6：“kfree-skb-就是-kfree-skb-”"><a href="#误区-6：“kfree-skb-就是-kfree-skb-”" class="headerlink" title="误区 6：“kfree_skb 就是 kfree(skb)”"></a>误区 6：“kfree_skb 就是 kfree(skb)”</h3><p>错误。它需要先处理 <code>users</code>，释放 dst、destructor、conntrack、extensions、frags、frag_list 和 head buffer，最后才释放 skb shell。</p><h3 id="相关知识横向扩展"><a href="#相关知识横向扩展" class="headerlink" title="相关知识横向扩展"></a>相关知识横向扩展</h3><ul><li><code>struct sk_buff_head</code> 如何把 skb 组织为带锁队列；</li><li>header offset 为什么比裸指针更适合 head reallocation；</li><li>clone 时 <code>dataref</code> 如何拆分 full-data ref 与 payload-only ref；</li><li>page_pool 如何让 frag page 返回 RX 内存池；</li><li>GRO&#x2F;GSO 为什么依赖 <code>skb_shared_info</code>；</li><li>socket 为什么使用 <code>truesize</code> 做收发内存记账。</li></ul><h3 id="最小生命周期调用链"><a href="#最小生命周期调用链" class="headerlink" title="最小生命周期调用链"></a>最小生命周期调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">alloc_skb()/napi_alloc_skb()</span><br><span class="line">→ 初始化 skb shell、linear head 与 skb_shared_info</span><br><span class="line">→ reserve/put/push/pull 建立当前协议视图</span><br><span class="line">→ 协议栈、队列或驱动转移 skb 所有权</span><br><span class="line">→ clone/COW/segment 维护 users、dataref 与 page ref</span><br><span class="line">→ consume_skb()/kfree_skb() 分层释放外部引用、数据区和 shell</span><br></pre></td></tr></table></figure><h3 id="字段与所有权统一核对表"><a href="#字段与所有权统一核对表" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>表达内容</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td>skb shell &#x2F; <code>users</code></td><td>控制对象及其共享次数</td><td>最后一个 shell 引用归零后才释放结构体</td></tr><tr><td>head &#x2F; <code>dataref</code></td><td>linear buffer 与尾部 shared info</td><td>clone 可共享；最后一个 dataref 释放 head</td></tr><tr><td><code>frags[]</code> &#x2F; page ref</td><td>非线性 page payload</td><td>shared info 记录片段，free 时逐页归还引用</td></tr><tr><td><code>frag_list</code></td><td>子 skb 链</td><td>父 skb 持有并在释放路径递归消费</td></tr><tr><td><code>sk</code>、dst、nfct</td><td>外部子系统状态</td><td>各自有独立引用或 destructor，不能由 <code>users</code> 代替</td></tr></tbody></table><h2 id="2-struct-sk-buff-字段地图"><a href="#2-struct-sk-buff-字段地图" class="headerlink" title="2. struct sk_buff 字段地图"></a>2. struct sk_buff 字段地图</h2><p><img src="/blog/img/sk_buff/field-map.svg" alt="struct sk_buff 字段地图"></p><h3 id="结论摘要-1"><a href="#结论摘要-1" class="headerlink" title="结论摘要"></a>结论摘要</h3><p><code>struct sk_buff</code> 不应按声明顺序死记。更有效的方式是按“谁在使用字段”分为：队列与调度、设备与 socket、外部引用、长度与数据布局、协议头位置、checksum&#x2F;offload、流与队列选择、VLAN&#x2F;隧道、临时控制块、引用计数与扩展九组。字段布局同时承担三类工程目标：热路径访问、结构体空洞填充和 clone 时批量复制。</p><h3 id="一、队列与节点字段"><a href="#一、队列与节点字段" class="headerlink" title="一、队列与节点字段"></a>一、队列与节点字段</h3><p><code>struct sk_buff</code> 的开头是：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sk_buff</span> *<span class="title">next</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sk_buff</span> *<span class="title">prev</span>;</span></span><br></pre></td></tr></table></figure><p>见 <code>kernel-5.10/include/linux/skbuff.h:720-738</code>。它们必须位于最前面，因为 <code>struct sk_buff_head</code> 的前两个成员同样是 <code>next/prev</code>，见 <code>kernel-5.10/include/linux/skbuff.h:294-301</code>。队列 helper 把 queue head 当作循环双链表哨兵，例如 <code>__skb_queue_head()</code> 会把 <code>(struct sk_buff *)list</code> 传给插入逻辑。</p><p>开头的 union 还允许同一块空间作为：</p><ul><li>普通 skb 双链表节点；</li><li><code>struct rb_node</code>，供 netem、IPv4 defrag、TCP 等红黑树场景使用；</li><li><code>struct list_head</code>。</li></ul><p>关键约束是一个 skb 在同一时刻不能被无约束地同时挂入多种容器；当前持有者必须清楚节点空间的用途。</p><h3 id="二、设备、socket-与外部状态"><a href="#二、设备、socket-与外部状态" class="headerlink" title="二、设备、socket 与外部状态"></a>二、设备、socket 与外部状态</h3><table><thead><tr><th>字段</th><th>主要语义</th><th>生命周期关注点</th></tr></thead><tbody><tr><td><code>dev</code></td><td>当前输入或输出网络设备</td><td>redirect、routing 后可能变化</td></tr><tr><td><code>sk</code></td><td>关联 socket</td><td>clone 默认不复制 owner；由 destructor 配合记账</td></tr><tr><td><code>_skb_refdst</code></td><td>路由&#x2F;dst 缓存及低位标志</td><td>clone 需增加引用，free 时 <code>skb_dst_drop()</code></td></tr><tr><td><code>destructor</code></td><td>skb head state 的释放回调</td><td>常见为 socket 收发内存回调</td></tr><tr><td><code>_nfct</code></td><td>conntrack 引用与状态位</td><td>clone&#x2F;copy&#x2F;free 必须维护引用</td></tr><tr><td><code>extensions</code></td><td>可选 skb 扩展</td><td>先检查 <code>active_extensions</code></td></tr></tbody></table><p><code>__copy_skb_header()</code> 明确不复制旧 <code>sk</code>，但会复制 <code>dev</code>、控制块、dst、extensions 和 Netfilter 状态，见 <code>kernel-5.10/net/core/skbuff.c:939-985</code>。因此“复制 skb header”并不等于机械复制整个结构体。</p><h3 id="三、长度字段"><a href="#三、长度字段" class="headerlink" title="三、长度字段"></a>三、长度字段</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">unsigned</span> <span class="type">int</span> len;</span><br><span class="line"><span class="type">unsigned</span> <span class="type">int</span> data_len;</span><br><span class="line">__u16 mac_len;</span><br><span class="line">__u16 hdr_len;</span><br><span class="line"><span class="type">unsigned</span> <span class="type">int</span> truesize;</span><br></pre></td></tr></table></figure><ul><li><code>len</code>：逻辑包总长度，包含 linear、frags 和 frag_list；</li><li><code>data_len</code>：非线性部分长度；</li><li><code>mac_len</code>：MAC header 长度，通常由 network header 与 MAC header offset 之差得到；</li><li><code>hdr_len</code>：headerless clone 场景记录可写 header 空间；</li><li><code>truesize</code>：内存计量值，不是线速包长。</li></ul><p>核心关系：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">skb_headlen(skb) = len - data_len</span><br><span class="line">len = linear length + non-linear length</span><br></pre></td></tr></table></figure><h3 id="四、协议头字段为什么是-offset"><a href="#四、协议头字段为什么是-offset" class="headerlink" title="四、协议头字段为什么是 offset"></a>四、协议头字段为什么是 offset</h3><p>主要字段：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">transport_header</span><br><span class="line">network_header</span><br><span class="line">mac_header</span><br><span class="line">inner_transport_header</span><br><span class="line">inner_network_header</span><br><span class="line">inner_mac_header</span><br></pre></td></tr></table></figure><p>见 <code>kernel-5.10/include/linux/skbuff.h:900-912</code>。访问 helper 通过 <code>head + offset</code> 得到地址，例如：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">skb_network_header(skb) = skb-&gt;head + skb-&gt;network_header;</span><br></pre></td></tr></table></figure><p>见 <code>kernel-5.10/include/linux/skbuff.h:2555-2626</code>。</p><p>使用 offset 而不是裸指针的主要价值：</p><ol><li><code>data</code> 会被 push&#x2F;pull，header 位置仍可独立保存；</li><li><code>pskb_expand_head()</code> 可能更换 head buffer，复制 offset 后无需逐个修正绝对指针；</li><li>16 位 offset 比 64 位指针节省结构体空间；</li><li>inner&#x2F;outer header 可以同时表达隧道封装。</li></ol><p>需要区分：</p><ul><li><code>skb_reset_network_header()</code>：把 network header 设为当前 <code>data</code>；</li><li><code>skb_set_network_header(skb, n)</code>：设为当前 <code>data + n</code>；</li><li><code>skb_network_header()</code>：从 <code>head + offset</code> 取地址。</li></ul><h3 id="五、临时控制块-cb-48"><a href="#五、临时控制块-cb-48" class="headerlink" title="五、临时控制块 cb[48]"></a>五、临时控制块 cb[48]</h3><p><code>cb[48]</code> 定义于 <code>kernel-5.10/include/linux/skbuff.h:749-755</code>。源码注释给出的所有权规则是：</p><blockquote><p>当前把 skb 排队的层拥有控制块；如果要跨层保留，必须明确复制或保存。</p></blockquote><p>典型 overlay：</p><ul><li>IPv4：<code>IPCB(skb)</code> → <code>struct inet_skb_parm</code>，<code>kernel-5.10/include/net/ip.h:47-63,104</code>；</li><li>IPv6：<code>IP6CB(skb)</code> → <code>struct inet6_skb_parm</code>；</li><li>TCP：<code>TCP_SKB_CB(skb)</code> → <code>struct tcp_skb_cb</code>，<code>kernel-5.10/include/net/tcp.h:838-899</code>；</li><li>GRO：<code>NAPI_GRO_CB(skb)</code> → <code>struct napi_gro_cb</code>，<code>kernel-5.10/include/linux/netdevice.h:2487-2554</code>；</li><li>qdisc：<code>struct qdisc_skb_cb</code>，<code>kernel-5.10/include/net/sch_generic.h:420-429</code>。</li></ul><p>这些结构共享同一 48 字节空间，所以不能同时假设多个 overlay 都有效。进入新子系统时，旧层 cb 内容可能被覆盖。</p><p><code>__copy_skb_header()</code> 使用 <code>memcpy(new-&gt;cb, old-&gt;cb, sizeof(old-&gt;cb))</code> 复制控制块，但这只保证字节被复制，不保证其跨层语义仍然有效。</p><h3 id="六、checksum-与-offload-字段"><a href="#六、checksum-与-offload-字段" class="headerlink" title="六、checksum 与 offload 字段"></a>六、checksum 与 offload 字段</h3><p>主要字段包括：</p><ul><li><code>ip_summed</code>；</li><li><code>csum</code>，或 union 中的 <code>csum_start/csum_offset</code>；</li><li><code>csum_valid</code>、<code>csum_level</code>、<code>csum_complete_sw</code>、<code>csum_not_inet</code>；</li><li>shared info 中的 <code>gso_size/gso_segs/gso_type</code>。</li></ul><p><code>ip_summed</code> 只有两位，编码 <code>CHECKSUM_NONE/UNNECESSARY/COMPLETE/PARTIAL</code>。它表达的是“checksum 责任和状态”，不是简单的布尔值。GSO 元数据放在 shared info，是因为它属于共享数据布局而不是某个独立 skb shell 的私有队列状态。</p><h3 id="七、hash、CPU-与队列字段"><a href="#七、hash、CPU-与队列字段" class="headerlink" title="七、hash、CPU 与队列字段"></a>七、hash、CPU 与队列字段</h3><table><thead><tr><th>字段</th><th>用途</th></tr></thead><tbody><tr><td><code>hash</code></td><td>保存硬件 RSS 或软件 flow dissector hash</td></tr><tr><td><code>l4_hash</code></td><td>hash 是否为规范的四元组&#x2F;L4 hash</td></tr><tr><td><code>sw_hash</code></td><td>hash 是否由软件计算</td></tr><tr><td><code>queue_mapping</code></td><td>RX&#x2F;TX 队列映射；TX 时影响 qdisc&#x2F;设备队列</td></tr><tr><td><code>napi_id</code></td><td>RX 来源 NAPI 标识</td></tr><tr><td><code>sender_cpu</code></td><td>与 <code>napi_id</code> 复用空间，供 XPS 使用</td></tr><tr><td><code>skb_iif</code></td><td>输入接口 ifindex</td></tr></tbody></table><p>这些字段把“包内容”转换为调度决策：在哪个 CPU 处理、进入哪个 backlog、选择哪个 TX queue。</p><h3 id="八、VLAN、隧道和策略字段"><a href="#八、VLAN、隧道和策略字段" class="headerlink" title="八、VLAN、隧道和策略字段"></a>八、VLAN、隧道和策略字段</h3><ul><li><code>protocol</code>：驱动&#x2F;二层解析得到的上层协议；</li><li><code>vlan_proto/vlan_tci/vlan_present</code>：硬件剥离或 metadata 形式的 VLAN tag；</li><li><code>inner_protocol</code> 与 inner header offset：隧道内层协议；</li><li><code>encapsulation</code>：inner header 是否有效；</li><li><code>mark</code>：策略路由、Netfilter、tc 等通用标记；</li><li><code>priority</code>：排队优先级；</li><li><code>tc_index</code>、<code>tc_skip_classify</code>、<code>tc_at_ingress</code>、<code>redirected</code>：tc 数据路径状态。</li></ul><p>注意 union 复用：<code>mark</code> 与 <code>reserved_tailroom</code> 共享空间，<code>napi_id</code> 与 <code>sender_cpu</code> 共享空间。字段当前语义取决于所在路径。</p><h3 id="九、headers-start-headers-end-的意义"><a href="#九、headers-start-headers-end-的意义" class="headerlink" title="九、headers_start&#x2F;headers_end 的意义"></a>九、headers_start&#x2F;headers_end 的意义</h3><p><code>headers_start</code> 和 <code>headers_end</code> 是零长度数组标记，不保存数据。两者之间的字段在 clone&#x2F;copy header 时由一次 <code>memcpy()</code> 批量复制，见：</p><ul><li>标记：<code>kernel-5.10/include/linux/skbuff.h:798-919</code>；</li><li>复制：<code>kernel-5.10/net/core/skbuff.c:954-983</code>。</li></ul><p><code>CHECK_SKB_FIELD()</code> 用编译期断言确保 protocol、checksum、hash、priority、header offset、VLAN、mark 等字段位于批量复制范围内。</p><p>这说明字段排列不只是可读性问题，还与复制热路径、空洞和 ABI&#x2F;KABI 约束相关。<code>queue_mapping</code> 因避免 16 位空洞而留在该区间外，单独复制。</p><h3 id="十、结构尾部字段"><a href="#十、结构尾部字段" class="headerlink" title="十、结构尾部字段"></a>十、结构尾部字段</h3><p>结构尾部为：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tail, end, head, data, truesize, users, extensions</span><br></pre></td></tr></table></figure><p>见 <code>kernel-5.10/include/linux/skbuff.h:939-950</code>。<code>__alloc_skb()</code> 只清零到 <code>tail</code> 之前，然后显式初始化尾部，源码还警告不要随意在 tail 后增加字段，见 <code>kernel-5.10/net/core/skbuff.c:221-241</code>。</p><p>在 64 位内核中 tail&#x2F;end 是 offset，head&#x2F;data 是指针。<code>users</code> 保护 skb shell；共享 head buffer 的引用计数位于 <code>skb_shared_info.dataref</code>。</p><h3 id="十一、字段修改者索引"><a href="#十一、字段修改者索引" class="headerlink" title="十一、字段修改者索引"></a>十一、字段修改者索引</h3><table><thead><tr><th>字段组</th><th>典型写入者</th><th>典型读取者</th></tr></thead><tbody><tr><td><code>dev/protocol</code></td><td>驱动、<code>eth_type_trans()</code>、routing&#x2F;redirect</td><td>RX core、L3、qdisc、driver</td></tr><tr><td>header offsets</td><td>Ethernet&#x2F;IP&#x2F;TCP&#x2F;隧道 helper</td><td>协议解析、checksum、BPF</td></tr><tr><td><code>hash</code></td><td>驱动 RSS、flow dissector</td><td>GRO、RPS&#x2F;RFS、socket reuseport</td></tr><tr><td><code>queue_mapping</code></td><td>RX 驱动、TX queue selection</td><td>qdisc、ndo_start_xmit</td></tr><tr><td>checksum</td><td>RX driver、协议栈、GSO helper</td><td>L3&#x2F;L4、TX offload</td></tr><tr><td><code>cb</code></td><td>当前协议或队列层</td><td>同一层后续函数</td></tr><tr><td><code>sk/destructor</code></td><td>socket ownership helper</td><td>memory accounting&#x2F;free</td></tr><tr><td>dst&#x2F;nfct&#x2F;extensions</td><td>routing&#x2F;Netfilter&#x2F;扩展子系统</td><td>output、clone、free</td></tr></tbody></table><h3 id="十二、常见误区"><a href="#十二、常见误区" class="headerlink" title="十二、常见误区"></a>十二、常见误区</h3><ol><li><code>skb-&gt;data</code> 不是固定协议头；header offset 才保存各层位置。</li><li><code>cb</code> 不是全局稳定元数据，不同层会覆盖它。</li><li><code>queue_mapping</code> 不是 CPU 编号，而是设备 RX&#x2F;TX queue 语义。</li><li><code>hash</code> 不保证来自硬件，也不保证一定包含 L4 端口。</li><li><code>mark</code>、<code>priority</code>、<code>tc_index</code> 语义不同，不能互换。</li><li><code>headers_start/end</code> 不是缓存行边界，而是批量复制边界。</li><li>条件编译会改变结构体实际大小和字段存在性，不能只凭某个配置的 <code>sizeof</code> 推广到所有内核。</li></ol><h3 id="字段与所有权统一核对表-1"><a href="#字段与所有权统一核对表-1" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>data/tail/end</code></td><td>分配和 data helper</td><td>linear head 布局；shell 私有</td></tr><tr><td><code>_skb_refdst/_nfct</code></td><td>路由、Netfilter</td><td>引用随 clone&#x2F;copy&#x2F;free 转移</td></tr><tr><td><code>sk/destructor/truesize</code></td><td>socket owner helper</td><td>destructor 归还 socket 记账</td></tr></tbody></table><h3 id="最小调用链"><a href="#最小调用链" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">alloc/init</span><br><span class="line">→ 子系统写入字段与 header offset</span><br><span class="line">→ queue/protocol/driver 读取</span><br><span class="line">→ clone/copy 复制规定区间</span><br><span class="line">→ free 释放外部引用</span><br></pre></td></tr></table></figure><h2 id="3-四指针、数据区与-Header-操作"><a href="#3-四指针、数据区与-Header-操作" class="headerlink" title="3. 四指针、数据区与 Header 操作"></a>3. 四指针、数据区与 Header 操作</h2><p><img src="/blog/img/sk_buff/pointer-operations.svg" alt="四指针、数据区与 Header 操作"></p><h3 id="结论摘要-2"><a href="#结论摘要-2" class="headerlink" title="结论摘要"></a>结论摘要</h3><p>skb 数据操作分为三类：移动当前视图的 <code>push/pull</code>，扩展有效数据的 <code>put</code>，预留未来头部空间的 <code>reserve</code>。header helper 用相对 head 的 offset 保存协议头位置。只要操作不重新分配 head，已保存的 header offset 仍有效；<code>pskb_expand_head()</code>、COW、linearize 等可能更换或重排数据，调用者必须遵守 helper 的返回和指针失效规则。</p><h3 id="一、基础操作的状态转换"><a href="#一、基础操作的状态转换" class="headerlink" title="一、基础操作的状态转换"></a>一、基础操作的状态转换</h3><h3 id="skb-reserve"><a href="#skb-reserve" class="headerlink" title="skb_reserve"></a>skb_reserve</h3><p><code>kernel-5.10/include/linux/skbuff.h:2433-2444</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">skb-&gt;data += len;</span><br><span class="line">skb-&gt;tail += len;</span><br></pre></td></tr></table></figure><p>前置条件是空 skb。它不改变 <code>len</code>，只把 headroom 增大、tailroom 减小。典型用途是在构造包之前为 L2&#x2F;L3&#x2F;L4 header 或对齐保留空间。</p><h3 id="skb-put"><a href="#skb-put" class="headerlink" title="skb_put"></a>skb_put</h3><p>checked 版本声明于 <code>kernel-5.10/include/linux/skbuff.h:2291</code>，unchecked <code>__skb_put()</code> 位于 2292-2299：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> *tmp = skb_tail_pointer(skb);</span><br><span class="line">skb-&gt;tail += len;</span><br><span class="line">skb-&gt;len += len;</span><br><span class="line"><span class="keyword">return</span> tmp;</span><br></pre></td></tr></table></figure><p>它把原 tail 之后的空间纳入 linear data，返回新区域起点。<code>__skb_put()</code> 要求调用者已经证明空间足够，并用 <code>SKB_LINEAR_ASSERT</code> 拒绝非线性 skb。</p><h3 id="skb-push"><a href="#skb-push" class="headerlink" title="skb_push"></a>skb_push</h3><p><code>kernel-5.10/include/linux/skbuff.h:2347-2353</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">skb-&gt;data -= len;</span><br><span class="line">skb-&gt;len += len;</span><br></pre></td></tr></table></figure><p>它消耗 headroom，用于向前添加 header。调用前必须保证 headroom 足够。</p><h3 id="skb-pull"><a href="#skb-pull" class="headerlink" title="skb_pull"></a>skb_pull</h3><p><code>kernel-5.10/include/linux/skbuff.h:2355-2365</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">skb-&gt;len -= len;</span><br><span class="line">BUG_ON(skb-&gt;len &lt; skb-&gt;data_len);</span><br><span class="line">skb-&gt;data += len;</span><br></pre></td></tr></table></figure><p>它只能从 linear 前部移除数据。若 pull 后总长度小于非线性长度，说明试图跨过不存在的 linear 数据，违反不变量。</p><h3 id="二、checked-与-unchecked-API"><a href="#二、checked-与-unchecked-API" class="headerlink" title="二、checked 与 unchecked API"></a>二、checked 与 unchecked API</h3><p>带双下划线的 helper 通常省略部分边界检查，适用于上层已证明不变量的热路径。普通 <code>skb_put/push/pull</code> 更适合作为外部 API，但也不能代替调用者理解前置条件。</p><p>典型规则：</p><ul><li><code>__skb_put()</code>：必须有足够 tailroom，且 skb 为线性；</li><li><code>__skb_push()</code>：必须有足够 headroom；</li><li><code>__skb_pull()</code>：必须保证 len 合法；</li><li><code>skb_pull_inline()</code>：<code>len &gt; skb-&gt;len</code> 时返回 NULL。</li></ul><p>错误调用可能不是“返回失败”，而是破坏 shared info 或触发 BUG。</p><h3 id="三、pskb-may-pull-与非线性-header"><a href="#三、pskb-may-pull-与非线性-header" class="headerlink" title="三、pskb_may_pull 与非线性 header"></a>三、pskb_may_pull 与非线性 header</h3><p>协议解析经常需要保证前 N 字节连续。例如读取完整 IPv4 header 前，不能假定它全部位于 linear 区。</p><p><code>pskb_may_pull()</code> 位于 <code>kernel-5.10/include/linux/skbuff.h:2384-2391</code>：</p><ol><li>若 <code>len &lt;= skb_headlen(skb)</code>，直接成功；</li><li>若 <code>len &gt; skb-&gt;len</code>，失败；</li><li>否则调用 <code>__pskb_pull_tail()</code>，从 frags&#x2F;frag_list 把需要的数据复制或整理进 linear 区。</li></ol><p>注意名字中的 pull 不代表它会像 <code>skb_pull()</code> 一样推进 <code>data</code>。它的目标是“保证从当前 data 开始的前 len 字节线性可读”。成功后 skb 布局可能改变，所以调用前缓存的 header 指针应重新获取。</p><p><code>pskb_pull()</code> 则同时保证线性并真正推进 data、减少 len。</p><h3 id="四、header-offset-helper"><a href="#四、header-offset-helper" class="headerlink" title="四、header offset helper"></a>四、header offset helper</h3><p>以 network header 为例：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">skb_reset_network_header(skb)</span><br><span class="line">    skb-&gt;network_header = skb-&gt;data - skb-&gt;head;</span><br><span class="line"></span><br><span class="line">skb_set_network_header(skb, off)</span><br><span class="line">    skb_reset_network_header(skb);</span><br><span class="line">    skb-&gt;network_header += off;</span><br><span class="line"></span><br><span class="line">skb_network_header(skb)</span><br><span class="line">    <span class="keyword">return</span> skb-&gt;head + skb-&gt;network_header;</span><br></pre></td></tr></table></figure><p>见 <code>kernel-5.10/include/linux/skbuff.h:2577-2590</code>。MAC、transport、inner header 使用同样模式。</p><h3 id="reset-与-set-的区别"><a href="#reset-与-set-的区别" class="headerlink" title="reset 与 set 的区别"></a>reset 与 set 的区别</h3><ul><li>reset：header 就在当前 data；</li><li>set(offset)：header 在当前 data 之后 offset；</li><li>accessor：从 head 基址计算绝对地址。</li></ul><h3 id="push-pull-后-header-offset-是否变化"><a href="#push-pull-后-header-offset-是否变化" class="headerlink" title="push&#x2F;pull 后 header offset 是否变化"></a>push&#x2F;pull 后 header offset 是否变化</h3><p>普通 push&#x2F;pull 只改 <code>data</code>，不会自动改已保存的 header offset。因此：</p><ul><li>已保存 header 仍指向原来的字节位置；</li><li>当前 data 与 header 的相对距离发生变化；</li><li>协议层需要在正确时机 reset&#x2F;set 对应 header。</li></ul><p>这正是 offset 独立于 data 的价值。</p><h3 id="五、header-指针失效规则"><a href="#五、header-指针失效规则" class="headerlink" title="五、header 指针失效规则"></a>五、header 指针失效规则</h3><p>以下操作通常不更换 head，仅移动 data&#x2F;tail：</p><ul><li>reserve；</li><li>put；</li><li>push；</li><li>pull。</li></ul><p>它们不会使 <code>skb_network_header(skb)</code> 计算出的地址因 head 更换而失效，但调用者缓存的“相对 data 指针”和当前视图语义可能改变。</p><p>以下操作可能更换 head 或重排数据：</p><ul><li><code>pskb_expand_head()</code>；</li><li><code>skb_cow_head()</code>；</li><li><code>skb_unclone()</code>；</li><li><code>__pskb_pull_tail()</code>；</li><li><code>skb_linearize()</code>；</li><li>BPF change head&#x2F;adjust room 等。</li></ul><p>调用这些函数后，应重新通过 <code>ip_hdr()</code>、<code>tcp_hdr()</code>、<code>skb_network_header()</code> 等 helper 获取指针，不保留旧的 <code>struct iphdr *</code>。</p><h3 id="六、pskb-expand-head"><a href="#六、pskb-expand-head" class="headerlink" title="六、pskb_expand_head"></a>六、pskb_expand_head</h3><p>实现位于 <code>kernel-5.10/net/core/skbuff.c:1622</code>。它为 head buffer 增加 <code>nhead/ntail</code>，核心步骤包括：</p><ol><li>检查 skb shell 不被多个 <code>users</code> 共享；</li><li>计算新 head 长度并分配新 buffer；</li><li>复制 linear 数据和 shared info；</li><li>对 frags、frag_list、zerocopy 和 clone 引用进行必要处理；</li><li>根据新 headroom 调整 data、tail、end 和各 header offset；</li><li>释放旧数据引用。</li></ol><p>它可能改变 <code>skb-&gt;head</code> 和 <code>skb-&gt;data</code> 的绝对地址，但保持逻辑包内容和 header offset 语义。</p><p>不能把它理解成单纯的 <code>realloc()</code>：旧 head 可能被 clone，共享信息和 page refs 必须正确转移。</p><h3 id="七、COW：写之前先获得独占数据"><a href="#七、COW：写之前先获得独占数据" class="headerlink" title="七、COW：写之前先获得独占数据"></a>七、COW：写之前先获得独占数据</h3><h3 id="skb-cloned"><a href="#skb-cloned" class="headerlink" title="skb_cloned"></a>skb_cloned</h3><p><code>cloned</code> 位只表示“可能共享”，真正判断还需要查看 shared info 的 <code>dataref</code>。这是热路径先看位、再看原子计数的优化。</p><h3 id="skb-cow"><a href="#skb-cow" class="headerlink" title="skb_cow"></a>skb_cow</h3><p>目标是确保：</p><ul><li>需要修改的数据区域可写；</li><li>必要 headroom 足够；</li><li>若数据被 clone，则扩展&#x2F;复制得到独占 head。</li></ul><h3 id="skb-cow-head"><a href="#skb-cow-head" class="headerlink" title="skb_cow_head"></a>skb_cow_head</h3><p>它针对 header 写入场景，常用于 Netfilter NAT、隧道封装、tc&#x2F;BPF 等将要修改或新增 header 的路径。若 head 已独占且 headroom 足够，返回 0；否则通过 <code>pskb_expand_head()</code> 创建可写 head。</p><p>COW 不意味着一定复制全部 payload。非线性 page 可能继续共享，关键是保证要写的 head&#x2F;header 区域独占。</p><h3 id="八、数据操作前置-后置条件表"><a href="#八、数据操作前置-后置条件表" class="headerlink" title="八、数据操作前置&#x2F;后置条件表"></a>八、数据操作前置&#x2F;后置条件表</h3><table><thead><tr><th>API</th><th>前置条件</th><th>主要变化</th><th>可能失败</th><th>旧指针风险</th></tr></thead><tbody><tr><td><code>skb_reserve</code></td><td>空 skb、空间足够</td><td>data&#x2F;tail 同时前移</td><td>无返回值</td><td>data 相关指针变化</td></tr><tr><td><code>skb_put</code></td><td>线性、tailroom 足够</td><td>tail&#x2F;len 增加</td><td>checked 路径可报错&#x2F;BUG</td><td>原 tail 之后可写</td></tr><tr><td><code>skb_push</code></td><td>headroom 足够</td><td>data 前移、len 增加</td><td>越界会破坏内存</td><td>data 变化</td></tr><tr><td><code>skb_pull</code></td><td>len 足够、不能越过 linear</td><td>data 前移、len 减少</td><td>NULL 或 BUG</td><td>data 变化</td></tr><tr><td><code>pskb_may_pull</code></td><td>请求不超过总 len</td><td>必要数据线性化</td><td>内存不足返回 false</td><td>head&#x2F;header 指针可能失效</td></tr><tr><td><code>pskb_expand_head</code></td><td>skb shell 独占等</td><td>更换&#x2F;扩展 head</td><td><code>-ENOMEM</code></td><td>所有绝对数据指针失效</td></tr><tr><td><code>skb_cow_head</code></td><td>指定所需 headroom</td><td>确保 header 可写</td><td><code>-ENOMEM</code></td><td>发生复制时失效</td></tr></tbody></table><h3 id="九、典型-RX-TX-使用模式"><a href="#九、典型-RX-TX-使用模式" class="headerlink" title="九、典型 RX&#x2F;TX 使用模式"></a>九、典型 RX&#x2F;TX 使用模式</h3><h3 id="RX"><a href="#RX" class="headerlink" title="RX"></a>RX</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">驱动构造 linear Ethernet frame</span><br><span class="line">→ eth_type_trans() 保存 MAC header 并 pull Ethernet header</span><br><span class="line">→ IP 层 reset network header</span><br><span class="line">→ L4 层 set/reset transport header</span><br></pre></td></tr></table></figure><h3 id="TX"><a href="#TX" class="headerlink" title="TX"></a>TX</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">先 reserve 足够 headroom</span><br><span class="line">→ 写 payload / skb_put</span><br><span class="line">→ skb_push 添加 L4 header</span><br><span class="line">→ skb_push 添加 IP header</span><br><span class="line">→ skb_push 添加 Ethernet header</span><br></pre></td></tr></table></figure><p>真实代码可能使用专用 helper，但四指针变化遵循同一模型。</p><h3 id="十、常见误区"><a href="#十、常见误区" class="headerlink" title="十、常见误区"></a>十、常见误区</h3><ol><li><code>pskb_may_pull()</code> 不是普通 <code>skb_pull()</code>，它主要保证连续可读。</li><li>push&#x2F;pull 不会自动替调用者重设所有协议 header offset。</li><li>有 <code>cloned</code> 位不等于一定仍共享，必须结合 <code>dataref</code> helper。</li><li>COW 不一定复制 frags 中的全部 payload。</li><li><code>pskb_expand_head()</code> 后旧的 <code>iphdr *</code> 不能继续使用。</li><li><code>skb_put()</code> 不是 memcpy；它只把空间纳入有效数据，调用者还需写入内容。</li><li>非线性 skb 不能直接按普通 tailroom 追加。</li></ol><h3 id="字段与所有权统一核对表-2"><a href="#字段与所有权统一核对表-2" class="headerlink" title="字段与所有权统一核对表"></a>字段与所有权统一核对表</h3><table><thead><tr><th>对象或字段</th><th>主要修改者&#x2F;使用者</th><th>所有权与释放要点</th></tr></thead><tbody><tr><td><code>data/tail/len</code></td><td>push&#x2F;pull&#x2F;put&#x2F;trim</td><td>调用者持有 skb，操作前必须满足空间不变量</td></tr><tr><td>header offsets</td><td>reset&#x2F;set helper</td><td>head 重分配后 offset 保持可重定位</td></tr><tr><td><code>dataref</code></td><td>COW&#x2F;expand</td><td>共享 head 只有在获得独占可写副本后才能改写</td></tr></tbody></table><h3 id="最小调用链-1"><a href="#最小调用链-1" class="headerlink" title="最小调用链"></a>最小调用链</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">reserve 建立 headroom</span><br><span class="line">→ put/push/pull 改变当前数据视图</span><br><span class="line">→ reset/set 保存 header offset</span><br><span class="line">→ may_pull/COW/expand 保证可访问和可写</span><br></pre></td></tr></table></figure><h2 id="源码阅读路线"><a href="#源码阅读路线" class="headerlink" title="源码阅读路线"></a>源码阅读路线</h2><p>建议不要从 <code>struct sk_buff</code> 声明开始逐字段背诵，而应使用下面的阅读顺序：</p><ol><li>先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造；</li><li>沿成功主线记录 <code>data/len/data_len</code>、header offset、checksum、hash、queue mapping 等字段变化；</li><li>再回到每个失败分支，确认 skb、page、DMA、socket 和记账引用是否回滚；</li><li>最后对照 5.10、6.1 或 BSP 的同名 symbol，区分语义变化、性能优化和 vendor backport。</li></ol><h2 id="本篇总结"><a href="#本篇总结" class="headerlink" title="本篇总结"></a>本篇总结</h2><ol><li>skb 是控制对象，包数据可能分散在 linear head、page frags 和子 skb 中。</li><li><code>head &lt;= data &lt;= tail &lt;= end</code> 描述 linear head，不等于整个逻辑包。</li><li>协议头使用相对 <code>head</code> 的 offset，因而能够承受 push&#x2F;pull 和 head 重分配。</li><li>读字段时要同时问：谁写入、谁读取、当前谁持有 skb。</li></ol><p><code>sk_buff</code> 的难点从来不只是字段多，而是<strong>同一个对象不断改变数据视图、元数据状态和持有者</strong>。只要把“布局、字段、所有权、释放”四条线同时画出来，绝大多数网络栈源码都会从零散函数变成可推导的生命周期。</p><h2 id="系列目录"><a href="#系列目录" class="headerlink" title="系列目录"></a>系列目录</h2><ol><li>Linux sk_buff 深度解析（一）：数据结构、四指针与核心字段（本文）</li><li>Linux sk_buff 深度解析（二）：非线性数据、引用计数与生命周期</li><li>Linux sk_buff 深度解析（三）：从网卡 DMA 到 Socket 的 RX 路径</li><li>Linux sk_buff 深度解析（四）：从 sendmsg 到 TX completion</li><li>Linux sk_buff 深度解析（五）：GRO、GSO、Checksum 与多核流调度</li><li>Linux sk_buff 深度解析（六）：Netfilter、tc、eBPF 与 XDP</li><li>Linux sk_buff 深度解析（七）：VLAN、隧道、分片与重组</li><li>Linux sk_buff 深度解析（八）：Socket 内存、零拷贝、丢包与版本演进</li></ol>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/0ae52f8028ea/</id>
    <link href="https://2xpang.cn/blog/posts/0ae52f8028ea/"/>
    <published>2026-07-14T17:00:00.000Z</published>
    <summary>从 sk_buff 为什么不是一个简单 buffer 出发，系统解释 skb shell、linear head、shared info、四指针、header offset、核心字段与数据区操作。</summary>
    <title>Linux sk_buff 深度解析（一）：数据结构、四指针与核心字段</title>
    <updated>2026-07-14T17:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="Linux" scheme="https://2xpang.cn/blog/tags/Linux/"/>
    <category term="NAPI" scheme="https://2xpang.cn/blog/tags/NAPI/"/>
    <category term="网络子系统" scheme="https://2xpang.cn/blog/tags/%E7%BD%91%E7%BB%9C%E5%AD%90%E7%B3%BB%E7%BB%9F/"/>
    <category term="软中断" scheme="https://2xpang.cn/blog/tags/%E8%BD%AF%E4%B8%AD%E6%96%AD/"/>
    <category term="内核源码" scheme="https://2xpang.cn/blog/tags/%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81/"/>
    <content>
      <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p>如果你曾经好奇——网卡每秒收到几十万个包，为什么系统没有崩溃？为什么 Linux 网络栈在高 PPS 下仍能保持吞吐？答案的核心之一就是 <strong>NAPI（New API）</strong>。</p><p>NAPI 不是某个单一函数，而是一套”中断触发、软中断批量轮询、按预算公平调度”的收包负载控制框架。理解它，是读懂 Linux 网络收包路径的必经之路。</p><p>本文以 <strong>Linux 5.10</strong> 为主线，<strong>6.1</strong> 做对比，结合 e1000 和 RK3588 stmmac 驱动实例，从问题动机到源码细节，完整拆解 NAPI 机制。</p><h2 id="一、问题：为什么不能每个包一个硬中断？"><a href="#一、问题：为什么不能每个包一个硬中断？" class="headerlink" title="一、问题：为什么不能每个包一个硬中断？"></a>一、问题：为什么不能每个包一个硬中断？</h2><p>千兆&#x2F;万兆网卡可能每秒产生几十万到数百万个包。如果每个包都触发一次硬中断，CPU 会进入 <strong>interrupt livelock</strong>——看起来一直忙，但业务处理反而推进很少。具体问题包括：</p><ol><li><strong>中断频率过高</strong>：CPU 主要时间花在中断入口&#x2F;出口、寄存器保存恢复、上下文切换上，而不是协议栈本身。</li><li><strong>硬中断上下文限制多</strong>：不能睡眠，不适合做复杂协议栈处理、内存回收和大批量逻辑。</li><li><strong>缓存局部性差</strong>：每个包独立打断当前执行流，频繁污染 I-cache&#x2F;D-cache。</li><li><strong>公平性差</strong>：一个高流量网卡可能长期占用 CPU，其他软中断、进程调度被拖慢。</li></ol><p>NAPI 的解法是：<strong>第一次包到达仍靠中断唤醒；进入 NAPI 后，关闭该队列中断，由软中断一次处理一批包，并用 budget 控制单次处理量。</strong></p><h2 id="二、核心模型：中断-轮询的混合机制"><a href="#二、核心模型：中断-轮询的混合机制" class="headerlink" title="二、核心模型：中断 + 轮询的混合机制"></a>二、核心模型：中断 + 轮询的混合机制</h2><p>NAPI 不是”纯轮询替代中断”，更准确地说，它是<strong>中断触发的自适应批量轮询</strong>。基本流程如下：</p><p><img src="/blog/img/napi/napi-flow.svg" alt="NAPI 收包主流程"></p><p>三种流量下的行为：</p><table><thead><tr><th>流量状态</th><th>NAPI 行为</th></tr></thead><tbody><tr><td><strong>无流量&#x2F;低流量</strong></td><td>网卡硬中断唤醒 CPU，延迟低</td></tr><tr><td><strong>流量升高</strong></td><td>驱动关闭 RX&#x2F;TX 中断，把 NAPI 放入 softirq 轮询队列</td></tr><tr><td><strong>高流量持续</strong></td><td>softirq 不断 poll RX ring，避免每个包再次硬中断</td></tr><tr><td><strong>流量下降&#x2F;本轮清空</strong></td><td><code>napi_complete_done()</code> 后重新打开中断，回到低流量中断唤醒模式</td></tr></tbody></table><p>以 e1000 为例，中断处理函数 <code>e1000_intr()</code> 读取并清除 ICR，写 IMC 关闭中断，再 <code>__napi_schedule()</code> 调度 NAPI（<code>e1000_main.c:3745</code>）；poll 完成后 <code>e1000_clean()</code> 调用 <code>napi_complete_done()</code>，返回 true 才 <code>e1000_irq_enable()</code>（<code>e1000_main.c:3795</code>）。</p><h2 id="三、核心数据结构：struct-napi-struct"><a href="#三、核心数据结构：struct-napi-struct" class="headerlink" title="三、核心数据结构：struct napi_struct"></a>三、核心数据结构：<code>struct napi_struct</code></h2><p>NAPI 的所有状态都围绕 <code>struct napi_struct</code>（<code>include/linux/netdevice.h:330</code>）组织，可按职责分为五类：</p><h3 id="3-1-调度和状态字段"><a href="#3-1-调度和状态字段" class="headerlink" title="3.1 调度和状态字段"></a>3.1 调度和状态字段</h3><table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td><code>poll_list</code></td><td>挂到当前 CPU 的 <code>softnet_data-&gt;poll_list</code> 或临时 <code>repoll</code> 链表</td></tr><tr><td><code>state</code></td><td>保存 <code>SCHED</code>、<code>MISSED</code>、<code>DISABLE</code> 等状态位</td></tr><tr><td><code>weight</code></td><td>单次 poll 的建议预算</td></tr><tr><td><code>poll</code></td><td>驱动注册的回调，签名 <code>int (*poll)(struct napi_struct *, int)</code></td></tr></tbody></table><h3 id="3-2-设备关联字段"><a href="#3-2-设备关联字段" class="headerlink" title="3.2 设备关联字段"></a>3.2 设备关联字段</h3><table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td><code>dev</code></td><td>所属 <code>struct net_device</code></td></tr><tr><td><code>dev_list</code></td><td>挂入 <code>dev-&gt;napi_list</code></td></tr></tbody></table><h3 id="3-3-GRO-字段"><a href="#3-3-GRO-字段" class="headerlink" title="3.3 GRO 字段"></a>3.3 GRO 字段</h3><table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td><code>gro_hash[]</code> &#x2F; <code>gro_bitmask</code></td><td>GRO 聚合哈希表与位掩码</td></tr><tr><td><code>skb</code> &#x2F; <code>rx_list</code> &#x2F; <code>rx_count</code></td><td>GRO 暂存 skb 链表</td></tr></tbody></table><p>NAPI 不只是调度结构，还承载当前 poll 周期内的 GRO 聚合状态。</p><h3 id="3-4-定时和延迟开中断字段"><a href="#3-4-定时和延迟开中断字段" class="headerlink" title="3.4 定时和延迟开中断字段"></a>3.4 定时和延迟开中断字段</h3><table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td><code>timer</code></td><td>GRO flush &#x2F; NAPI watchdog 定时器</td></tr><tr><td><code>defer_hard_irqs_count</code></td><td>延迟重新打开硬中断的计数</td></tr></tbody></table><h3 id="3-5-netpoll-busy-poll-字段"><a href="#3-5-netpoll-busy-poll-字段" class="headerlink" title="3.5 netpoll &#x2F; busy poll 字段"></a>3.5 netpoll &#x2F; busy poll 字段</h3><table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td><code>poll_owner</code></td><td><code>CONFIG_NETPOLL</code> 下使用</td></tr><tr><td><code>napi_hash_node</code> &#x2F; <code>napi_id</code></td><td>busy poll 按 ID 查找 NAPI</td></tr></tbody></table><h3 id="3-6-状态位详解"><a href="#3-6-状态位详解" class="headerlink" title="3.6 状态位详解"></a>3.6 状态位详解</h3><table><thead><tr><th>状态位</th><th>含义</th></tr></thead><tbody><tr><td><code>NAPI_STATE_SCHED</code></td><td>已被调度或正在 poll——NAPI 互斥执行的核心位</td></tr><tr><td><code>NAPI_STATE_MISSED</code></td><td>poll 期间又来了事件，需要再跑一轮</td></tr><tr><td><code>NAPI_STATE_DISABLE</code></td><td>正在禁用，<code>napi_schedule_prep()</code> 会拒绝调度</td></tr><tr><td><code>NAPI_STATE_NPSVC</code></td><td>netpoll 服务相关，<code>napi_disable()</code> 会等待该状态</td></tr><tr><td><code>NAPI_STATE_IN_BUSY_POLL</code></td><td>当前由 busy poll 路径持有</td></tr></tbody></table><p>核心状态机：</p><p><img src="/blog/img/napi/napi-state-machine.svg" alt="NAPI 状态机"></p><h2 id="四、驱动如何接入-NAPI"><a href="#四、驱动如何接入-NAPI" class="headerlink" title="四、驱动如何接入 NAPI"></a>四、驱动如何接入 NAPI</h2><h3 id="4-1-注册与启用：两步走"><a href="#4-1-注册与启用：两步走" class="headerlink" title="4.1 注册与启用：两步走"></a>4.1 注册与启用：两步走</h3><p><strong>第一步——probe 阶段注册 NAPI</strong>：调用 <code>netif_napi_add(dev, &amp;priv-&gt;napi, poll_fn, weight)</code>。e1000 在 <code>e1000_main.c:1015</code> 注册 <code>adapter-&gt;napi</code>，poll 函数为 <code>e1000_clean</code>，weight 为 64。</p><p><strong>第二步——open&#x2F;up 阶段启用 NAPI</strong>：调用 <code>napi_enable()</code>，随后打开设备中断。e1000 在 <code>e1000_up()</code> 中先 <code>napi_enable()</code> 再 <code>e1000_irq_enable()</code>。</p><p>为什么分两步？注册是把 NAPI 对象初始化并挂到 netdev；启用是设备真正开始运行时解除初始禁用状态。</p><h3 id="4-2-netif-napi-add-做了哪些初始化？"><a href="#4-2-netif-napi-add-做了哪些初始化？" class="headerlink" title="4.2 netif_napi_add() 做了哪些初始化？"></a>4.2 <code>netif_napi_add()</code> 做了哪些初始化？</h3><p>该函数（<code>net/core/dev.c:6759</code>）不只是填一个回调，它还把 NAPI 接入 netdev、GRO、timer、busy poll&#x2F;hash、状态机：</p><ul><li>设置 <code>poll</code>、<code>weight</code>、<code>dev</code></li><li>初始化 <code>poll_list</code>、<code>gro_hash</code>、<code>timer</code>（回调为 <code>napi_watchdog</code>）</li><li><strong>设置 <code>NAPI_STATE_SCHED</code> 和 <code>NAPI_STATE_NPSVC</code></strong>——让刚注册但尚未 <code>napi_enable()</code> 的 NAPI 处于”不可被正常调度”的初始状态</li><li>把 NAPI 挂入 <code>dev-&gt;napi_list</code></li><li>加入 NAPI hash，供 busy poll 按 ID 查找</li></ul><p><code>napi_enable()</code> 会清除 <code>NAPI_STATE_NPSVC</code> 和 <code>NAPI_STATE_SCHED</code>，使 NAPI 进入可调度状态。这样可以防止设备尚未 open、ring 尚未准备好时 NAPI 被错误调度。</p><h3 id="4-3-open-close-的典型顺序"><a href="#4-3-open-close-的典型顺序" class="headerlink" title="4.3 open&#x2F;close 的典型顺序"></a>4.3 open&#x2F;close 的典型顺序</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">open/up:</span><br><span class="line">  分配/初始化 ring → 配置硬件 → 清 DOWN 标志</span><br><span class="line">  → napi_enable() → 打开设备中断 → netif_start_queue()</span><br><span class="line"></span><br><span class="line">close/down:</span><br><span class="line">  停止发送队列 → 关闭设备中断 → napi_disable()</span><br><span class="line">  → 清理 ring / 释放资源</span><br></pre></td></tr></table></figure><p><code>napi_disable()</code>（<code>net/core/dev.c:6789</code>）设置 <code>NAPI_STATE_DISABLE</code>，并等待 <code>SCHED</code>、<code>NPSVC</code> 状态可被占有，确保没有正在运行或待运行的 NAPI poll。</p><h3 id="4-4-多队列网卡：一个-netdev-多个-NAPI"><a href="#4-4-多队列网卡：一个-netdev-多个-NAPI" class="headerlink" title="4.4 多队列网卡：一个 netdev 多个 NAPI"></a>4.4 多队列网卡：一个 netdev 多个 NAPI</h3><p>一个 <code>struct net_device</code> 有 <code>napi_list</code>，可以挂多个 <code>struct napi_struct</code>。多队列网卡通常每个 RX queue（或每个 channel）一个 NAPI。</p><p>RK3588 SDK 的 stmmac 驱动就是典型例子：<code>stmmac_napi_add()</code> 遍历 queue，对每个 channel 按需注册 <code>rx_napi</code> 和 <code>tx_napi</code>（<code>stmmac_main.c:4963</code>）。</p><h2 id="五、从硬中断到软中断：NAPI-调度链路"><a href="#五、从硬中断到软中断：NAPI-调度链路" class="headerlink" title="五、从硬中断到软中断：NAPI 调度链路"></a>五、从硬中断到软中断：NAPI 调度链路</h2><h3 id="5-1-核心调度流程"><a href="#5-1-核心调度流程" class="headerlink" title="5.1 核心调度流程"></a>5.1 核心调度流程</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">驱动 ISR</span><br><span class="line">  → napi_schedule_prep()          ← 原子状态切换</span><br><span class="line">  → 设置 NAPI_STATE_SCHED 或 MISSED</span><br><span class="line">  → ____napi_schedule()</span><br><span class="line">  → list_add_tail(&amp;napi-&gt;poll_list, &amp;sd-&gt;poll_list)</span><br><span class="line">  → __raise_softirq_irqoff(NET_RX_SOFTIRQ)</span><br><span class="line">  → net_rx_action()</span><br><span class="line">  → napi_poll()</span><br><span class="line">  → 驱动 napi-&gt;poll()</span><br></pre></td></tr></table></figure><p>真正把 NAPI 挂入每 CPU <code>softnet_data-&gt;poll_list</code> 并触发 <code>NET_RX_SOFTIRQ</code> 的代码是 <code>____napi_schedule()</code>（<code>net/core/dev.c:4289</code>）。</p><h3 id="5-2-napi-schedule-prep-的原子状态机"><a href="#5-2-napi-schedule-prep-的原子状态机" class="headerlink" title="5.2 napi_schedule_prep() 的原子状态机"></a>5.2 <code>napi_schedule_prep()</code> 的原子状态机</h3><p>该函数（<code>net/core/dev.c:6459</code>）循环读取 <code>n-&gt;state</code>，计算新状态，用 <code>cmpxchg()</code> 原子替换。逻辑如下：</p><ol><li>读取旧状态 <code>val</code>。</li><li>如果有 <code>NAPI_STATE_DISABLE</code>，说明设备正在关闭，返回 false。</li><li>计算新状态 <code>new = val | SCHED</code>。如果旧状态已经有 SCHED，则额外设置 <strong>MISSED</strong>。</li><li><code>cmpxchg()</code> 原子替换，失败则并发修改了 state，重试。</li><li>返回 <code>!(val &amp; SCHED)</code>：只有原来没被调度时，调用者才应真正入队。</li></ol><p><strong>返回值含义</strong>：</p><ul><li><strong>true</strong>：你抢到了从 idle → scheduled 的权利，请入队。</li><li><strong>false</strong>：已经 scheduled 或 disabled；如果是已 scheduled，MISSED 已记录，不要重复入队。</li></ul><p>为什么用原子操作？因为 NAPI 的调度状态可能被多个上下文并发修改：硬中断、softirq poll、busy poll、设备关闭路径、同一设备不同 CPU 上的中断。如果不用原子操作，可能导致同一 NAPI 被重复挂入链表、poll 和 complete 同时改状态导致丢事件。</p><h3 id="5-3-MISSED：解决-poll-与中断的竞态"><a href="#5-3-MISSED：解决-poll-与中断的竞态" class="headerlink" title="5.3 MISSED：解决 poll 与中断的竞态"></a>5.3 MISSED：解决 poll 与中断的竞态</h3><p>“已经 SCHED” 时再次 schedule，不能重复入 <code>poll_list</code>，也不能丢掉”新事件”。所以设置 <code>NAPI_STATE_MISSED</code> 作为补偿信号。</p><p><code>napi_complete_done()</code>（<code>net/core/dev.c:6502</code>）会检查旧状态是否包含 MISSED。如果包含，它不会回到 idle，而是重新设置 SCHED 并调用 <code>__napi_schedule(n)</code>，返回 false。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">poll 期间又来包/事件</span><br><span class="line">  → schedule 发现已 SCHED → 设置 MISSED</span><br><span class="line">  → poll 本轮准备 complete</span><br><span class="line">  → complete 发现 MISSED → 再跑一轮 NAPI</span><br></pre></td></tr></table></figure><p>这避免了”poll 刚清空旧事件、准备开中断时，新事件刚好到来但被屏蔽&#x2F;丢失”的竞态。</p><h3 id="5-4-napi-schedule-vs-napi-schedule-irqoff"><a href="#5-4-napi-schedule-vs-napi-schedule-irqoff" class="headerlink" title="5.4 napi_schedule() vs napi_schedule_irqoff()"></a>5.4 <code>napi_schedule()</code> vs <code>napi_schedule_irqoff()</code></h3><p>两者都先调用 <code>napi_schedule_prep()</code>，区别在入队时是否假设硬中断已经关闭：</p><table><thead><tr><th>版本</th><th>说明</th></tr></thead><tbody><tr><td><code>napi_schedule()</code></td><td>内部 <code>__napi_schedule()</code> 会 <code>local_irq_save()</code> &#x2F; <code>local_irq_restore()</code></td></tr><tr><td><code>napi_schedule_irqoff()</code></td><td>调用者保证 hard IRQ 已关闭，省掉本地 IRQ 保存恢复</td></tr></tbody></table><p>注意：在 <code>CONFIG_PREEMPT_RT</code> 下，<code>__napi_schedule_irqoff()</code> 会退化为 <code>__napi_schedule()</code>，因为 RT 内核中硬中断可能被线程化，”hard IRQ 已关闭”的假设不一定成立。</p><h3 id="5-5-为什么-poll-通常运行在触发中断的-CPU-上？"><a href="#5-5-为什么-poll-通常运行在触发中断的-CPU-上？" class="headerlink" title="5.5 为什么 poll 通常运行在触发中断的 CPU 上？"></a>5.5 为什么 poll 通常运行在触发中断的 CPU 上？</h3><p>因为 <code>__napi_schedule()</code> 使用 <code>this_cpu_ptr(&amp;softnet_data)</code>，把 NAPI 加到当前 CPU 的 <code>poll_list</code>。好处：</p><ol><li><strong>缓存局部性</strong>：中断 CPU 刚访问了设备状态、ring 指针和驱动私有数据。</li><li><strong>避免跨 CPU 锁竞争</strong>：每 CPU <code>poll_list</code> 减少全局队列竞争。</li><li><strong>符合 IRQ affinity&#x2F;RSS 设计</strong>：多队列网卡常把不同 RX queue 中断绑到不同 CPU，NAPI 跟随中断 CPU 自然实现并行。</li></ol><p>但 RPS&#x2F;RFS、busy poll、threaded NAPI 等机制可能改变后续处理所在 CPU。</p><h2 id="六、net-rx-action-：每-CPU-的-NAPI-调度器"><a href="#六、net-rx-action-：每-CPU-的-NAPI-调度器" class="headerlink" title="六、net_rx_action()：每 CPU 的 NAPI 调度器"></a>六、<code>net_rx_action()</code>：每 CPU 的 NAPI 调度器</h2><p><code>net_rx_action()</code> 是 <code>NET_RX_SOFTIRQ</code> 的处理函数（<code>net/core/dev.c:6899</code>），本质是一个<strong>每 CPU 的 NAPI 调度器</strong>。核心逻辑分 7 段：</p><ol><li>取当前 CPU 的 <code>softnet_data</code>。</li><li>计算本轮时间上限：当前 jiffies + <code>netdev_budget_usecs</code>。</li><li>读取全局包预算 <code>netdev_budget</code>。</li><li>关本地中断，把 <code>sd-&gt;poll_list</code> 搬到局部 <code>list</code>，减少持有共享队列时间。</li><li>循环取 <code>list</code> 上的第一个 NAPI，调用 <code>napi_poll()</code>，从全局 budget 中扣除返回的 work。</li><li>如果 budget 用尽或时间到，递增 <code>sd-&gt;time_squeeze</code> 并退出本轮。</li><li>把新来的 <code>sd-&gt;poll_list</code>、<code>repoll</code> 和剩余 <code>list</code> 合并回 <code>sd-&gt;poll_list</code>，如果仍有待处理 NAPI，再次 raise <code>NET_RX_SOFTIRQ</code>。</li></ol><h3 id="6-1-budget、weight、netdev-budget-三层控制"><a href="#6-1-budget、weight、netdev-budget-三层控制" class="headerlink" title="6.1 budget、weight、netdev_budget 三层控制"></a>6.1 budget、weight、netdev_budget 三层控制</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">net_rx_action 总循环受 netdev_budget + netdev_budget_usecs 限制</span><br><span class="line">  每个 napi_poll 又受 napi-&gt;weight 限制</span><br><span class="line">    驱动 poll 的入参 budget 通常就是 napi-&gt;weight</span><br></pre></td></tr></table></figure><table><thead><tr><th>概念</th><th>说明</th><th>默认值&#x2F;典型值</th></tr></thead><tbody><tr><td><code>netdev_budget</code></td><td>一次 <code>NET_RX_SOFTIRQ</code> 总 budget</td><td>300（<code>/proc/sys/net/core/netdev_budget</code>）</td></tr><tr><td><code>netdev_budget_usecs</code></td><td>一次 softirq 的时间上限</td><td>2000μs</td></tr><tr><td><code>napi-&gt;weight</code></td><td>单个 NAPI 每次 poll 的 budget</td><td>e1000 为 64，stmmac 为 64</td></tr></tbody></table><p>类比：<code>netdev_budget</code> 是总饭票，<code>weight</code> 是单个队列单次最多能拿的饭票。</p><h3 id="6-2-repoll-链表"><a href="#6-2-repoll-链表" class="headerlink" title="6.2 repoll 链表"></a>6.2 repoll 链表</h3><p><code>napi_poll()</code> 如果发现驱动返回 <code>work &gt;= weight</code> 且没有 complete，就把该 NAPI 加入 <code>repoll</code>。<code>net_rx_action()</code> 在本轮结束时把 <code>repoll</code> 合并回 <code>sd-&gt;poll_list</code>，如果非空则再次 raise <code>NET_RX_SOFTIRQ</code>。</p><p>repoll 解决两个问题：</p><ol><li>防止单 NAPI 一次调用无限处理（用完 weight 让出机会）。</li><li>保留未完成工作（没有 complete 的 NAPI 不丢失）。</li></ol><h3 id="6-3-time-squeeze：软中断压力信号"><a href="#6-3-time-squeeze：软中断压力信号" class="headerlink" title="6.3 time_squeeze：软中断压力信号"></a>6.3 time_squeeze：软中断压力信号</h3><p><code>time_squeeze</code> 在全局 budget 用完或时间窗口耗尽时递增。它表示：本 CPU 的 NET_RX softirq 本轮来不及处理完所有待收包工作，被迫提前退出。</p><p>如果 <code>/proc/net/softnet_stat</code> 中对应 CPU 的 <code>time_squeeze</code> 持续增长，通常说明：</p><ul><li>收包压力较大</li><li><code>netdev_budget</code> 或 <code>netdev_budget_usecs</code> 可能偏小</li><li>单 CPU 承担过多 RX queue</li><li>中断亲和性&#x2F;RSS&#x2F;RPS 配置不均衡</li></ul><h2 id="七、驱动-poll-回调：收包的核心战场"><a href="#七、驱动-poll-回调：收包的核心战场" class="headerlink" title="七、驱动 poll 回调：收包的核心战场"></a>七、驱动 poll 回调：收包的核心战场</h2><h3 id="7-1-RX-ring-生命周期"><a href="#7-1-RX-ring-生命周期" class="headerlink" title="7.1 RX ring 生命周期"></a>7.1 RX ring 生命周期</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">驱动初始化 RX ring descriptor，分配 DMA buffer</span><br><span class="line">  → 网卡收到包，DMA 写入 buffer，设置 descriptor 状态位（如 DD 位）</span><br><span class="line">  → 网卡产生中断</span><br><span class="line">  → ISR 关闭中断并 schedule NAPI</span><br><span class="line">  → NAPI poll 扫描 RX ring</span><br><span class="line">  → 构造或复用 skb</span><br><span class="line">  → napi_gro_receive() 上交协议栈</span><br><span class="line">  → 补充新的 RX buffer 给 descriptor</span><br><span class="line">  → ring 清空后 complete 并重新开中断</span><br></pre></td></tr></table></figure><h3 id="7-2-驱动-poll-的标准模板"><a href="#7-2-驱动-poll-的标准模板" class="headerlink" title="7.2 驱动 poll 的标准模板"></a>7.2 驱动 poll 的标准模板</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">driver_poll</span><span class="params">(<span class="keyword">struct</span> napi_struct *napi, <span class="type">int</span> budget)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> work_done = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    clean_tx_irq(...);              <span class="comment">/* 清 TX completion */</span></span><br><span class="line">    clean_rx(..., &amp;work_done, budget); <span class="comment">/* 清 RX ring */</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (work_done &lt; budget) &#123;</span><br><span class="line">        <span class="comment">/* RX ring 已清空 */</span></span><br><span class="line">        <span class="keyword">if</span> (napi_complete_done(napi, work_done))</span><br><span class="line">            enable_rx_irq();        <span class="comment">/* 只有返回 true 才重新开中断 */</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> work_done;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键规则</strong>：</p><ul><li>返回值 <strong>不能超过 budget</strong>，否则违反 NAPI 调度契约（<code>napi_poll()</code> 中有检查，超限会打印错误）。</li><li>返回 <code>budget</code> → ring 可能还有包，NAPI 不 complete，继续 repoll。</li><li>返回小于 <code>budget</code> → 本轮清空，可以 complete 并重新开中断。</li></ul><p>注意：如果驱动 poll 正好处理了 budget 个包但 ring 已空，传统 NAPI 语义仍倾向于返回 budget 并让下一轮确认——这是为了避免边界歧义。</p><h3 id="7-3-e1000-实例"><a href="#7-3-e1000-实例" class="headerlink" title="7.3 e1000 实例"></a>7.3 e1000 实例</h3><p><code>e1000_clean()</code> 先调 <code>e1000_clean_tx_irq()</code> 清 TX，再调 <code>e1000_clean_rx_irq()</code> 清 RX。后者循环检查 <code>rx_desc-&gt;status &amp; E1000_RXD_STAT_DD</code>，用 <code>work_done</code> 与 <code>work_to_do</code> 控制最多处理数量（<code>e1000_main.c:4349</code>）。</p><h3 id="7-4-现代-multi-queue-驱动：stmmac"><a href="#7-4-现代-multi-queue-驱动：stmmac" class="headerlink" title="7.4 现代 multi-queue 驱动：stmmac"></a>7.4 现代 multi-queue 驱动：stmmac</h3><p>RK3588 SDK 的 stmmac 驱动为每个 channel 注册独立的 RX&#x2F;TX NAPI：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">注册：stmmac_napi_add()</span><br><span class="line">  → 每个 channel: rx_napi + tx_napi</span><br><span class="line"></span><br><span class="line">中断调度：stmmac_napi_check()</span><br><span class="line">  → 读 DMA 中断状态</span><br><span class="line">  → RX 事件: disable RX DMA irq + __napi_schedule(&amp;ch-&gt;rx_napi)</span><br><span class="line">  → TX 事件: 同理</span><br><span class="line"></span><br><span class="line">RX poll：stmmac_napi_poll_rx()</span><br><span class="line">  → stmmac_rx(priv, budget, chan)</span><br><span class="line">  → work_done &lt; budget 且 napi_complete_done() 返回 true → 重开 RX DMA irq</span><br><span class="line"></span><br><span class="line">TX poll：stmmac_napi_poll_tx()</span><br><span class="line">  → stmmac_tx_clean() → 完成后重开 TX DMA irq</span><br></pre></td></tr></table></figure><p>与 e1000 核心模式一致，但 stmmac 更接近现代多队列&#x2F;分 RX-TX NAPI 的设计。</p><h2 id="八、napi-complete-done-：从轮询回到中断的关键同步点"><a href="#八、napi-complete-done-：从轮询回到中断的关键同步点" class="headerlink" title="八、napi_complete_done()：从轮询回到中断的关键同步点"></a>八、<code>napi_complete_done()</code>：从轮询回到中断的关键同步点</h2><h3 id="8-1-为什么-work-done-budget-才-complete？"><a href="#8-1-为什么-work-done-budget-才-complete？" class="headerlink" title="8.1 为什么 work_done &lt; budget 才 complete？"></a>8.1 为什么 <code>work_done &lt; budget</code> 才 complete？</h3><p>因为 <code>work_done &lt; budget</code> 通常表示在预算耗尽前 ring 已无更多包，本轮工作完成，可以退出 polling 模式并重新依赖中断唤醒。</p><p>如果 <code>work_done == budget</code>，可能 ring 还有更多包，或刚好处理完但驱动无法可靠区分。保守起见，NAPI 约定<strong>不要在返回 budget 时 complete</strong>——避免高流量下反复开关中断，也避免边界竞态导致漏处理。</p><h3 id="8-2-napi-complete-done-返回-false-时绝不能开中断"><a href="#8-2-napi-complete-done-返回-false-时绝不能开中断" class="headerlink" title="8.2 napi_complete_done() 返回 false 时绝不能开中断"></a>8.2 <code>napi_complete_done()</code> 返回 false 时绝不能开中断</h3><p>返回 false 表示 NAPI 没有真正回到”由硬中断唤醒”的状态。常见原因：</p><ul><li><code>NAPI_STATE_MISSED</code> 存在，已重新 schedule</li><li>当前处于 busy poll 状态 <code>NAPI_STATE_IN_BUSY_POLL</code></li><li>配置了 GRO flush timeout &#x2F; defer hard IRQ，需要延迟开中断</li><li>当前处于 netpoll 服务状态 <code>NAPI_STATE_NPSVC</code></li></ul><p>如果驱动不管返回值直接开中断，可能导致 poll 仍在进行时又收到中断、busy poll 语义被破坏、相同事件被双重处理。</p><p>所以 e1000 的写法是：<strong>只有 <code>napi_complete_done()</code> 返回 true，才 <code>e1000_irq_enable()</code></strong>。</p><h3 id="8-3-GRO-flush-与-hard-IRQ-defer"><a href="#8-3-GRO-flush-与-hard-IRQ-defer" class="headerlink" title="8.3 GRO flush 与 hard IRQ defer"></a>8.3 GRO flush 与 hard IRQ defer</h3><p><code>napi_complete_done()</code> 会根据 <code>gro_bitmask</code>、<code>gro_flush_timeout</code>、<code>napi_defer_hard_irqs</code> 决定是否立即 flush GRO、是否启动 NAPI timer、是否返回 false 延迟驱动开中断。</p><ul><li>GRO 希望多等一点以便合并更多同流包，提高吞吐。</li><li>hard IRQ defer 允许 NAPI 暂时不重新打开硬中断，通过 timer 或后续轮询继续观察。</li><li>这是一种吞吐与延迟的折中：延迟开中断提高批处理效率，但可能增加低流量包延迟。</li></ul><h2 id="九、GRO：NAPI-承载的批处理优化"><a href="#九、GRO：NAPI-承载的批处理优化" class="headerlink" title="九、GRO：NAPI 承载的批处理优化"></a>九、GRO：NAPI 承载的批处理优化</h2><p>NAPI 不只是调度框架，也承载 GRO（Generic Receive Offload）批处理上下文。GRO 相关状态放在 <code>struct napi_struct</code> 中。</p><h3 id="9-1-工作原理"><a href="#9-1-工作原理" class="headerlink" title="9.1 工作原理"></a>9.1 工作原理</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">NAPI poll 提供批处理时机和上下文</span><br><span class="line">  → GRO 利用这个上下文聚合同流 skb</span><br><span class="line">  → NAPI complete / budget耗尽 / timeout 触发 GRO flush</span><br></pre></td></tr></table></figure><p>驱动常调用 <code>napi_gro_receive(napi, skb)</code>（<code>net/core/dev.c:6166</code>）上交包，该函数进入 GRO 逻辑，尝试把同流包合并，最后通过正常收包路径交给协议栈。</p><h3 id="9-2-napi-gro-receive-vs-netif-receive-skb"><a href="#9-2-napi-gro-receive-vs-netif-receive-skb" class="headerlink" title="9.2 napi_gro_receive() vs netif_receive_skb()"></a>9.2 <code>napi_gro_receive()</code> vs <code>netif_receive_skb()</code></h3><table><thead><tr><th>函数</th><th>说明</th></tr></thead><tbody><tr><td><code>netif_receive_skb()</code></td><td>直接把 skb 交给协议栈收包路径</td></tr><tr><td><code>napi_gro_receive()</code></td><td>NAPI 上下文中的 GRO 入口，先尝试聚合，再决定 hold&#x2F;merge&#x2F;flush&#x2F;normal receive</td></tr></tbody></table><p>现代高性能驱动应优先使用 <code>napi_gro_receive()</code>，让 TCP&#x2F;IPv4&#x2F;IPv6 等流量通过 GRO 降低协议栈处理次数。</p><h2 id="十、backlog-NAPI：非驱动收包路径"><a href="#十、backlog-NAPI：非驱动收包路径" class="headerlink" title="十、backlog NAPI：非驱动收包路径"></a>十、backlog NAPI：非驱动收包路径</h2><p>每 CPU 的 <code>softnet_data</code> 中有一个特殊的 <code>backlog</code> NAPI（<code>include/linux/netdevice.h:3307</code>），它不是某个物理网卡队列的 NAPI，而是内核为 backlog 收包队列准备的通用 NAPI 上下文。</p><p>两条收包路径的对比：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">驱动 NAPI 主路径:</span><br><span class="line">  硬件 RX ring → 驱动 NAPI poll → 构造 skb → napi_gro_receive() → 协议栈</span><br><span class="line"></span><br><span class="line">backlog 路径:</span><br><span class="line">  已有 skb → netif_rx()/enqueue_to_backlog()</span><br><span class="line">  → softnet_data input_pkt_queue/process_queue</span><br><span class="line">  → process_backlog() → __netif_receive_skb() → 协议栈</span><br></pre></td></tr></table></figure><p>backlog 路径处理已形成 skb 后被放入 CPU backlog 的场景，例如部分虚拟设备、老路径、跨 CPU RPS 入队等。</p><h2 id="十一、NAPI-在收包路径中的位置"><a href="#十一、NAPI-在收包路径中的位置" class="headerlink" title="十一、NAPI 在收包路径中的位置"></a>十一、NAPI 在收包路径中的位置</h2><p>把 NAPI、RPS&#x2F;RFS、XDP、page_pool 放在一起看，它们在收包路径中的位置如下：</p><p><img src="/blog/img/napi/rx-path-overview.svg" alt="Linux 网络收包路径全景"></p><table><thead><tr><th>机制</th><th>位置</th><th>作用</th></tr></thead><tbody><tr><td><strong>NAPI</strong></td><td>调度框架</td><td>调度和执行驱动收包 poll</td></tr><tr><td><strong>XDP</strong></td><td>驱动 RX 路径，skb 构造前</td><td>可 drop&#x2F;pass&#x2F;tx&#x2F;redirect</td></tr><tr><td><strong>page_pool</strong></td><td>RX buffer 分配&#x2F;回收</td><td>降低 page 分配释放成本</td></tr><tr><td><strong>RPS&#x2F;RFS</strong></td><td>skb 存在后，协议栈前</td><td>按 flow&#x2F;hash&#x2F;socket 亲和性分发到目标 CPU</td></tr></tbody></table><h2 id="十二、观测与调优"><a href="#十二、观测与调优" class="headerlink" title="十二、观测与调优"></a>十二、观测与调优</h2><h3 id="12-1-关键观测点"><a href="#12-1-关键观测点" class="headerlink" title="12.1 关键观测点"></a>12.1 关键观测点</h3><table><thead><tr><th>观测项</th><th>方法</th></tr></thead><tbody><tr><td>NET_RX softirq 次数</td><td><code>/proc/softirqs</code></td></tr><tr><td>dropped &#x2F; time_squeeze</td><td><code>/proc/net/softnet_stat</code></td></tr><tr><td>驱动&#x2F;网卡队列统计</td><td><code>ethtool -S &lt;ifname&gt;</code></td></tr><tr><td>中断合并参数</td><td><code>ethtool -c &lt;ifname&gt;</code></td></tr><tr><td>全局 budget</td><td><code>sysctl net.core.netdev_budget</code></td></tr><tr><td>全局时间限制</td><td><code>sysctl net.core.netdev_budget_usecs</code></td></tr><tr><td>NAPI poll tracepoint</td><td><code>tracepoint:napi:napi_poll</code></td></tr></tbody></table><h3 id="12-2-ftrace-观察-NAPI-poll"><a href="#12-2-ftrace-观察-NAPI-poll" class="headerlink" title="12.2 ftrace 观察 NAPI poll"></a>12.2 ftrace 观察 NAPI poll</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">mount -t tracefs nodev /sys/kernel/tracing</span><br><span class="line"><span class="built_in">cd</span> /sys/kernel/tracing</span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> 0 &gt; tracing_on</span><br><span class="line"><span class="built_in">echo</span> &gt; trace</span><br><span class="line"><span class="built_in">echo</span> 1 &gt; events/napi/napi_poll/enable</span><br><span class="line"><span class="built_in">echo</span> 1 &gt; tracing_on</span><br><span class="line"><span class="comment"># 产生网络流量（如 iperf3）</span></span><br><span class="line"><span class="built_in">echo</span> 0 &gt; tracing_on</span><br><span class="line"><span class="built_in">cat</span> trace</span><br></pre></td></tr></table></figure><h3 id="12-3-time-squeeze-持续增长时的排查思路"><a href="#12-3-time-squeeze-持续增长时的排查思路" class="headerlink" title="12.3 time_squeeze 持续增长时的排查思路"></a>12.3 time_squeeze 持续增长时的排查思路</h3><ol><li>检查 <code>netdev_budget</code> 和 <code>netdev_budget_usecs</code> 是否偏小。</li><li>检查单 CPU 是否承担过多 RX queue（<code>/proc/interrupts</code>）。</li><li>检查中断亲和性&#x2F;RSS&#x2F;RPS 配置是否均衡。</li><li>检查驱动或上层协议栈处理是否过慢。</li></ol><h2 id="十三、Linux-5-10-vs-6-1：NAPI-关键差异"><a href="#十三、Linux-5-10-vs-6-1：NAPI-关键差异" class="headerlink" title="十三、Linux 5.10 vs 6.1：NAPI 关键差异"></a>十三、Linux 5.10 vs 6.1：NAPI 关键差异</h2><table><thead><tr><th>维度</th><th>Linux 5.10</th><th>Linux 6.1</th></tr></thead><tbody><tr><td><code>struct napi_struct</code></td><td>无 <code>thread</code> 字段</td><td>增加 <code>struct task_struct *thread</code>，支持 threaded NAPI</td></tr><tr><td>状态位</td><td>无 threaded&#x2F;busy poll 相关位</td><td>增加 <code>THREADED</code>、<code>SCHED_THREADED</code>、<code>PREFER_BUSY_POLL</code></td></tr><tr><td>NAPI 注册 API</td><td><code>netif_napi_add(dev, napi, poll, weight)</code></td><td><code>netif_napi_add_weight(dev, napi, poll, weight)</code> + 默认 weight 的 <code>netif_napi_add()</code></td></tr><tr><td><code>napi_enable()</code></td><td>inline 实现</td><td>变成普通函数实现</td></tr><tr><td><code>net_rx_action()</code></td><td>基本框架</td><td>主体相似，增加 <code>skb_defer_free_flush()</code> 等新逻辑</td></tr><tr><td>threaded NAPI</td><td>不支持</td><td>支持把 NAPI poll 从 softirq 转移到可调度线程</td></tr></tbody></table><p><strong>threaded NAPI 的意义</strong>：把 NAPI poll 从 softirq 上下文转移到可调度线程，改善实时性控制、允许调度器管理 NAPI 执行、与 PREEMPT_RT 更好配合。代价是线程调度开销和行为复杂度增加，并非所有场景都需要。</p><p><strong>迁移注意事项</strong>：驱动代码从 5.10 迁移到 6.1 时，<code>netif_napi_add()</code> 参数数量变化，指定 weight 应改用 <code>netif_napi_add_weight()</code>。</p><h2 id="十四、总结"><a href="#十四、总结" class="headerlink" title="十四、总结"></a>十四、总结</h2><p>回到那句话：<strong>NAPI 不是”纯轮询替代中断”，而是”中断触发、软中断批量轮询、按预算公平调度、完成后再开中断”的收包负载控制框架。</strong></p><p>理解 NAPI 的四个关键点：</p><ol><li><strong>MISSED 状态</strong>——理解并发中断与 poll 竞态的核心。</li><li><strong>budget 体系</strong>——理解 NAPI 公平性和延迟的核心（weight、netdev_budget、netdev_budget_usecs 三层控制）。</li><li><strong><code>napi_complete_done()</code> 返回值</strong>——驱动重新打开中断前的关键同步点。</li><li><strong>GRO 与 NAPI 的绑定</strong>——NAPI 不只是调度框架，也是 GRO 批处理上下文。</li></ol><p>NAPI 的核心路径在 <code>net/core/dev.c</code>，但真正理解必须结合一个驱动 poll 函数。建议以 e1000 为入门，再过渡到 stmmac 等现代多队列驱动。</p><hr><p><strong>关键源码阅读顺序（Linux 5.10）</strong>：</p><ol><li><code>include/linux/netdevice.h:330</code> — <code>struct napi_struct</code> &#x2F; NAPI 状态位</li><li><code>include/linux/netdevice.h:454</code> — <code>napi_schedule()</code> &#x2F; <code>napi_schedule_irqoff()</code></li><li><code>net/core/dev.c:6459</code> — <code>napi_schedule_prep()</code></li><li><code>net/core/dev.c:4289</code> — <code>____napi_schedule()</code></li><li><code>net/core/dev.c:6899</code> — <code>net_rx_action()</code></li><li><code>net/core/dev.c:6833</code> — <code>napi_poll()</code></li><li><code>net/core/dev.c:6502</code> — <code>napi_complete_done()</code></li><li><code>net/core/dev.c:6759</code> — <code>netif_napi_add()</code></li><li><code>drivers/net/ethernet/intel/e1000/e1000_main.c:3795</code> — <code>e1000_clean()</code></li><li><code>net/core/dev.c:6166</code> — <code>napi_gro_receive()</code></li></ol>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/a164b3330642/</id>
    <link href="https://2xpang.cn/blog/posts/a164b3330642/"/>
    <published>2026-07-14T16:00:00.000Z</published>
    <summary>从中断风暴问题出发，系统剖析 Linux NAPI 机制：核心数据结构、调度状态机、budget 公平策略、驱动接入模板、GRO 批处理，以及 5.10 与 6.1 的关键差异。</summary>
    <title>Linux 内核 NAPI 机制深度解析：从硬中断到软中断轮询</title>
    <updated>2026-07-14T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>hyckleri</name>
    </author>
    <category term="Linux嵌入式" scheme="https://2xpang.cn/blog/categories/Linux%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <category term="Linux" scheme="https://2xpang.cn/blog/tags/Linux/"/>
    <category term="嵌入式" scheme="https://2xpang.cn/blog/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    <content>
      <![CDATA[<p>欢迎来到 <strong>嵌入式开发笔记</strong>。</p><p>这里用于记录 Linux、驱动开发、系统移植、C&#x2F;C++、设备树以及嵌入式开发中的实践与思考。</p><h2 id="内容方向"><a href="#内容方向" class="headerlink" title="内容方向"></a>内容方向</h2><ul><li>Linux 内核与驱动开发</li><li>U-Boot、设备树与系统移植</li><li>C&#x2F;C++ 编程实践</li><li>嵌入式问题定位与调试</li></ul><h2 id="代码示例"><a href="#代码示例" class="headerlink" title="代码示例"></a>代码示例</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">puts</span>(<span class="string">&quot;Hello, embedded Linux!&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://2xpang.cn/blog/posts/76865079d5fc/</id>
    <link href="https://2xpang.cn/blog/posts/76865079d5fc/"/>
    <published>2026-07-14T16:00:00.000Z</published>
    <summary>记录 Linux、驱动开发、系统移植与嵌入式学习过程。</summary>
    <title>欢迎来到嵌入式开发笔记</title>
    <updated>2026-07-14T16:00:00.000Z</updated>
  </entry>
</feed>
