首先说一下为什么去提交这个补丁 🤔…

最近在搞的一个研究项目中用到了 DPDK 框架,但是做的过程中发现框架的驱动并没有实现网卡规定的全部功能。因为其中有一个实时发包的功能是我需要用到的,所以我花了一些时间去修改了驱动程序去支持这个功能。

在把论文提交以后,我决定花费一些时间把这个补丁整合到 DPDK 框架里。第一个原因是这个功能对于实时系统比较关键,我在网上也看到一些对这个功能的讨论 ,感觉实现这个功能可以帮助到更多的人;第二个原因是,我之前没有为大型开源项目贡献过代码,想借此机会学习一下提交补丁的基本流程。

Update: 本文已被 DPDK 官方转载


准备流程 📚

在准备提交补丁之前,首先要了解项目的开发流程。DPDK 作为一个 Linux Foundation 下的项目,它的开发流程类似于 Linux 内核,而不是大家更熟悉的 Git Flow。这意味着我们不能简单地使用 git commit 提交补丁,然后通过pull request合并。相反,我们需要通过邮件来提交补丁。因此,在提交之前,我们需要完成一些额外的准备工作,具体包括:

  • 注册以及订阅 DPDK 的邮件列表 mailing list
  • 配置邮箱以使用 git-send-email
  • 确定自己需要修改的模块的维护者和subtree

邮件列表: 邮件列表的注册和订阅过程相对简单,可以按照 DPDK 官方文档 里的流程进行操作,这里就不重复了。同时建议一起注册 Patchwork,这样可以方便后续查看代码审查结果。这里我个人的经验是最好用一个单独的邮箱去订阅,因为 DPDK 一天会产生好几十个 Patch,如果跟自己日常邮箱混用很容易错过重要邮件 🤯。

更新 :在 mailing list 上可以设置 disable 所有邮件。这个 option disable 以后还是可以收到关于你自己 patch 的邮件的。 这样就不用注册单独邮箱了。(Thanks to Thomas Monjalon for pointing it out)

电子邮件配置: 邮箱的设置也很简单,我是用的 Google 邮箱,参考了这篇文章 进行配置。中间没有遇到什么问题。

寻找维护者: DPDK是一个庞大的开源项目,采用模块化设计以便于分工合作。每个模块都有指定的维护者,有些模块甚至有特定的维护分支(branch)。因此,我们需要找到自己所改动模块的维护者及其维护的 branch,并将补丁提交给他们,由他们帮助将更新合并到主线(mainline)库中。这里有一个需要注意的点是在GitHub的根目录下 MAINTAINERS 文件中查找维护者信息,而不是文档中提供的名单,因为文档中的名单可能不是最新的。我在第一次提交时就犯了这个错误,将补丁发送给了之前的维护人员。

更新:不需要手动去找到对应的 maintainer,直接用这个脚本就可以 devtools/get-maintainer.sh。比如像这样:git send-email --to dev@dpdk.org --cc-cmd devtools/get-maintainer.sh

至于一些基础的准备工作,比如从 GitHub 克隆代码到本地,了解项目的编码风格等,我不建议花太多时间研究官方提供的代码风格手册,因为你可以通过后面提到的自动化审查脚本来确保代码风格的一致性。


编写 Patch 🔧

编写 Patch 的过程其实就是你去更改被原始的代码的过程,只需要根据你的需求对代码进行修改,然后修改内容在格式化后就变成了 patch。这里有一个重要的建议是禁用IDE的自动格式化功能。许多项目的代码格式并不是标准格式,自动格式化可能会导致许多不必要的格式更改 🫠。

测试

测试分成三个主要部分

  • 编译以及功能测试
  • 代码风格检查
  • ABI Policy

编译与功能测试:因为我这次只是做了很小范围的修改,所以编译测试我只是在本机上编译了一下。具体过程和安装 DPDK 的过程基本一致,除了第一步使用 meson setup --wipe build -Dc_args=-DRTE_LIBRTE_IEEE1588 要清除之前的 build 目录。在功能测试上建议把可能受影响的 DPDK 内置 APP 都测试一遍,比如 dpdk-testpmd

代码风格检查: 这部分可以参考 dpdk 官方教程里面 Checking the Patches 章节 5.7. 其中需要注意的是 checkpatch.pl 需要自己到网上下载。按照教程生成 codespell-dpdk.txt 之后,在同一个目录运行 devtools/checkpatches.sh patch_name.patch 就可以了。从输出的日志中可以看到检查结果,里面的 error 和 warning 都是要改掉的,但是 checks 有些如果不合理应该就不需要改了。

ABI Policy: ABI Policy 指的是对变量名和函数名的要求,可以对比官方 ABI Guideline 自查自检。通常不是新建文件的话基本不会错,只要比对周围的 code 就大概能知道自己的变量名怎么写合适了。

提交 Patch

在测试结束以后就可以生成 patch 了,在生成之前建议再检查一下 git 的用户名和邮箱,使用 git config --global user.name & git config --global user.email 进行确认。

我生成补丁的方式可能不是 best practice。我是先使用 git add & git commit 简单 commit 修改,然后再通过 git commit --amend 去修改 commit message。最后通过 git format-patch -1 -o ~/patch/ 生成 patch。这里 -1 表示 patch-set 包含过去几个 commit,因为我只需要生成一个 patch/

在提交之前可以检查下 patch 里面有没有 Signed-off-by: xxxxx <xxxxxx@xxx>。我的这一行并没有自动生成,所以我手动添加了。

最后就可以通过 git send-email 提交 patch 了,这里一般 --to 给 maintainer,然后 --cc 给 dev@dpdk.org

git send-email --to="xxxxxxx@xxx.com" --to="xxxxxxx@xxx.com" --cc="dev@dpdk.org" patch_name.patch

在发送之前可以用下面的命令先发给自己看一下收到的邮件格式是否正确

git send-email --to=[your email] patch_name.patch

如果注册了 patchwork,过几个小时就可以收到 patchwork 发送的 patch 提交信息了。

Patch 具体内容

后面我会简单介绍一下我的 Patch 做了什么,如果不关心 DPDK 开发或者对技术细节不感兴趣的话可以跳过到下一个章节【代码审查】。

我这个 Patch 主要的贡献是给 igb 驱动增加一个网卡实时调度的功能,通过这个功能可以使报文按照我们设定的时间精准发送,根据网卡手册 的介绍精度可以保证 16 纳秒以内,精度非常高。下面是 Patch 主要的细节:

首先在 e1000_regs.h 中暴露出对应功能需要的寄存器地址

/* QAV Tx mode control register */
 #define E1000_I210_TQAVCTRL  0x3570
 #define E1000_I210_LAUNCH_OS0 0x3578

然后 igb_ethdev.c 在设备初始化的过程中根据 port 的参数设置寄存器的值去 enable 相关功能。这里如何确定寄存器的地址和值就需要去设备对应手册里面找了,可能会花费比较多的时间。

if (igb_tx_timestamp_dynflag > 0) {
  tqavctrl = E1000_READ_REG(hw, E1000_I210_TQAVCTRL);
  tqavctrl |= E1000_TQAVCTRL_MODE; /* Enable Qav mode */
  tqavctrl |= E1000_TQAVCTRL_FETCH_ARB; /* ARB fetch, no Round Robin*/
  tqavctrl |= E1000_TQAVCTRL_LAUNCH_TIMER_ENABLE; /* Enable Tx launch time*/
  E1000_WRITE_REG(hw, E1000_I210_TQAVCTRL, tqavctrl);
  E1000_WRITE_REG(hw, E1000_I210_LAUNCH_OS0, igb_tx_offset(dev));
}

之后在 igb_rxtx.c 发包的 transmission path 中设置发送描述符 (Tx descriptor)。其实就是把调度时间写每个包在内存的固定位置,这里要注意的是这个功能比较小众,并没有一个固定的 field,需要用 DPDK 的 dynamic descriptor 去写入。动态描述符 这个概念感兴趣可以了解下,资源性能优化的一个经典解决方案。

// 在 eth_igb_xmit_pkts 中通过 RTE_MBUF_DYNFIELD 拿到设置的调度时间
if (igb_tx_timestamp_dynflag > 0) {
  ts = *RTE_MBUF_DYNFIELD(tx_pkt,
  igb_tx_timestamp_dynfield_offset, uint64_t *);
  igbe_set_xmit_ctx(txq, ctx_txd, tx_ol_req, tx_offload, ts);
} else {
  igbe_set_xmit_ctx(txq, ctx_txd, tx_ol_req, tx_offload, 0);
}

然后

// 把调度时间在 igbe_set_xmit_ctx 中写到 mbuf 里面
if (txtime) {
  launch_time = (txtime - IGB_I210_TX_OFFSET_BASE) % NSEC_PER_SEC;
  ctx_txd->u.launch_time = rte_cpu_to_le_32(launch_time / 32);
} else {
  ctx_txd->u.launch_time = 0;
}

后面就是 NIC 去接管了,在读取 mbuf 后根据用户设定的时间去发送包。

遇到的困难

其中遇到的一个困难就是我发现实现以后网卡发包的时间跟我设置的调度时间不一致,在从设备的手册以及内核里面实现的类似功能代码里寻找以后并没有发现相关的描述与介绍。好在我手上有几张使用这个驱动的设备,在我进行了一系列测试之后,我发现这个发包时间与调度时间的差异在每次测试中是一个固定值,并且仅仅跟当前网口的 速度相关。基于这个测试结果,就选择通过判断当前网口速度在调度时间中补偿这个差异。

首先在 e1000_ethdev.h 里面 hardcode 测试出来的 delay 值。这些 delay 是我拿两种不同的 NIC 测试的,可以满足不同设备的兼容性。

/*
  * Macros to compensate the constant latency observed in i210 for launch time
  *
  * launch time = (offset_speed - offset_base + txtime) * 32
  * offset_speed is speed dependent, set in E1000_I210_LAUNCH_OS0
  */
 #define IGB_I210_TX_OFFSET_BASE        0xffe0
 #define IGB_I210_TX_OFFSET_SPEED_10      0xc7a0
 #define IGB_I210_TX_OFFSET_SPEED_100   0x86e0
 #define IGB_I210_TX_OFFSET_SPEED_1000    0xbe00

然后通过判断当前网络速度选择不同的补偿值


 static uint32_t igb_tx_offset(struct rte_eth_dev *dev)
 {
  struct e1000_hw *hw =
    E1000_DEV_PRIVATE_TO_HW(dev->data->dev_private);

  uint16_t duplex, speed;
  hw->mac.ops.get_link_up_info(hw, &speed, &duplex);

  uint32_t launch_os0 = E1000_READ_REG(hw, E1000_I210_LAUNCH_OS0);
  if (hw->mac.type != e1000_i210) {
    /* Set launch offset to base, no compensation */
    launch_os0 |= IGB_I210_TX_OFFSET_BASE;
  } else {
    /* Set launch offset depend on link speeds */
    switch (speed) {
    case SPEED_10:
      launch_os0 |= IGB_I210_TX_OFFSET_SPEED_10;
      break;
    case SPEED_100:
      launch_os0 |= IGB_I210_TX_OFFSET_SPEED_100;
      break;
    case SPEED_1000:
      launch_os0 |= IGB_I210_TX_OFFSET_SPEED_1000;
      break;
    default:
      launch_os0 |= IGB_I210_TX_OFFSET_BASE;
      break;
    }
  }

  return launch_os0;
 }

最后把补偿值的值写到 E1000_I210_LAUNCH_OS0 寄存器里面。其实这个地方也是有一个小小的提高性能的思考。在这里我并没有选择常用思路,即在发包的时候去获取当前的网口速率然后修改调度时间,而是在设备启动的时候去读取网卡速度然后把这个值写在寄存器里面。这利用了这个寄存器里面的数值也参与了计算最终的调度时间,从而避免了每次询问网口速度的开销。这个方案的缺陷是,如果在发包过程中网速发生了变化,补偿的值无法实时改变,但总体上在运行中改变网口速度情况相对少见。


代码审查 👀

相对于一般小的开源项目,DPDK 的代码审查是比较严格的,它分为自动化代码审查和人工审查。

自动化审查的结果在提交几个小时内就会通过邮件收到,虽然是我第一次提交 patch,但非常幸运并没有检查出来什么错误。

后面的人工审查阶段维护人员会针对 patch 的内容提出一些问题,这个阶段其实很像论文的 rebuttal。我这次遇到的问题主要关注以下几点:

  • 解释不同行代码的功能分别是什么
  • 解释为什么选择某种解决方案
  • 提供测试结果

只需要针对问题一一回复就好了,有一些小细节可以注意:

  • 回复的时候使用 --in-reply-to 参数明确回复的是哪一个邮件
git send-email \
    --in-reply-to=xxxxxxxx.namprd11.prod.outlook.com \
    --to=xxxx@xxx.com \
    --cc=xxxx@xxx.com \
    --subject="RE: [PATCH] net/e1000: support launchtime feature” \
    ./reply.txt
  • 注意格式,通过添加 > 来表示对之前邮件的引用。
  • subject 在 patch 的标题之前加入 RE:,注意无论回复多少次都只有一个,不要出现 RE: RE: RE:.

虽然过程比较繁琐,但是开源社区的氛围还是很好的。我从提交到最后 review 结束一共经过十多天,中间进行了三次回复以及一次代码更新。过程中遇到了一些技术上的问题,以及不确定的问题,最后通过询问维护者都得到了解决方案与建议。感兴趣的可以去翻一翻我的提交记录。


总结 🎉

通过这次提交给我留下最大的启发是,大型开源项目并没有那么神秘,想要在里面做出一点贡献也不是很难。只是各种繁琐的规定,以及对于开源开发者不友善的刻板印象把大家吓退了。虽然并不是我的主业,但尝试提交一个 patch 还是不错的经历 🤗。