前言

上次看了 lan9118 收发包过程,PIO 模式
现代网卡基本千兆,cpu直接搬运,性能不太行了,所以需要DMA
开始看带dma的pci千兆网卡了,能用qemu跑起来的,重点看dma搬运,用e1000
经过查询,发现有个 MIT 的著名操作系统课程 6.828 / 6.S081 的 Lab 就是让学生为 E1000 编写驱动,网上有大量可以参考的学习资源
就用这个吧,vx6 网络实验

目标

DMA (Direct Memory Access)描述符环 (Descriptor Ring)

vx6 网络实验

概念

6.828 操作系统课
xv6 操作系统

做什么

在 xv6 教学操作系统上,亲手为一个 E1000 网卡 实现设备驱动。需要完成的核心任务有两个:
实现发送 (e1000_transmit)
实现接收 (e1000_recv) 函数

过程

https://pdos.csail.mit.edu/6.828/2020/labs/net.html
网页是英文,但已有不少优秀的前辈做过这个实验,还翻译成了中文,可以直接搜索参考
按照这个网页的步骤,里面提供了一些参考资料,如vx6的手册,e1000的手册,可以参考其中的哪些章节
我看了,然后发现自己还是不会写接收和发送函数
于是,决定给自己降低难度,查找做过这个实验的前辈的代码,我的任务是理解代码

环境

VMware+Ubuntu 20.04_64(尽量用较新的版本,不然risv gcc工具链不太好搞)
安装库

sudo apt install git
sudo apt install make
sudo apt install gcc
sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
sudo apt install vim

下载 riscv64-elf-ubuntu-20.04-gcc-nightly-2023.09.27-nightly.tar.gz 这是编译好的二进制,解压可以了。(太早的版本只能下载源码编译,编译尚未成功,已放弃)
解压,bin路径加入PATH

tar -zxvf  riscv64-elf-ubuntu-20.04-gcc-nightly-2023.09.27-nightly.tar.gz
export PATH=/....../riscv/bin:$PATH

下载 vx6源码,切到 net 分支

git clone git://g.csail.mit.edu/xv6-labs-2020
git checkout net

开始编译

make clean
make qemu

然后就报错了

user/sh.c:58:1: error: infinite recursion detected [-Werror=infinite-recursion]
   58 | runcmd(struct cmd *cmd)
      | ^~~~~~

修改

 vim Makefile

CFLAGS = ... -Wno-infinite-recursion

再次 make qumu,可以啦

xv6 kernel is booting

hart 2 starting
hart 1 starting
init: starting sh
$ 

已进入 qemu
退出 qemu,ctrl+a 松开再按x

发包测试

在一个终端,cd xv6-labs-2020 目录,运行 make server 命令,这启动了一个服务器程序,监听来自 xv6 的 UDP 数据包
另一个终端,运行 make qemu 命令来启动 xv6 操作系统,进到$后,执行 nettests 命令
这会向 IP 地址为 10.0.2.2 的“主机”发送一个 UDP 数据包。

  • 在 QEMU 的虚拟网络里,10.0.2.2 这个 IP 地址被映射到了真实主机(Host)
  • 因此,xv6 协议栈会调用 e1000_transmit() 函数来发送这个数据包
    直接执行是不可能收到回复的,因为现在还没有实现发送函数 e1000_transmit(),也没有实现 接收函数

过程分析

相关概念

在实际看代码之前,先来看些基本概念
DMA
DMA(直接内存访问)的核心原理:

CPU 不搬运数据,

而是将“内存缓冲区地址”和“描述符环”的物理地址告诉网卡,

网卡内置的 DMA 控制器会通过 PCIe 总线自行读写这些物理内存

mbuf 数据包仓库

tx/rx ring 地址簿 描述符
发送描述符

e1000 手册 3.3.3 表 3.9

vx6里的简化(相比 linux 内核)定义

【kernel/e1000_dev.h】
struct tx_desc
{
	uint64 addr;
	uint16 length;
	uint8 cso;
	uint8 cmd;
	uint8 status;
	uint8 css;
	uint16 special;
}

// [E1000 3.2.3]
struct rx_desc
{
  uint64 addr;       /* Address of the descriptor's data buffer */
  uint16 length;     /* Length of data DMAed into data buffer */
  uint16 csum;       /* Packet checksum */
  uint8 status;      /* Descriptor status */
  uint8 errors;      /* Descriptor Errors */
  uint16 special;
};

mbuf

【kernel/net.h】
struct mbuf {
  struct mbuf  *next; // the next mbuf in the chain
  char         *head; // the current start position of the buffer
  unsigned int len;   // the length of the buffer
  char         buf[MBUF_SIZE]; // the backing store
};
初始化

vx6的代码里提供了初始化函数 e1000_init()

e1000_init
		把tx_ring数组首元素的地址写入 TDBAL
​		数组长度写入 TDLEN
​		TDH写0
​		TDT写0

这里我就不贴代码了,网上能直接搜索到,只记录过程。

发包过程

网卡硬件提供:e1000手册3.4

​ 发送描述符基地址寄存器 TDBAL

​ 发送描述符长度寄存器 TDLEN

​ 发送描述符头存器 TDH

​ 发送描述符尾存器 TDT

软件

​ 定义 tx_ring[]

​ e1000_init

​ 把tx_ring数组首元素的地址写入 TDBAL

​ 数组长度写入 TDLEN

​ TDH写0

​ TDT写0

​ 发送函数

​ 接收协议栈组装好的 mbuf

​ 读TDT,知道可用的描述位置(即tx_ring数组下标,发第1个包是0,一次加1)

​ 用下标,在tx_ring数组里取到 描述符 desc

​ 检查 desc 里 DD位,1表示(desc对应的上一个数据包已处理完成)完成

​ 释放旧的 mbuf

​ 用mbuf里的信息,填充 desc :数据包的内存地址,长度

​ mbufer地址保存到 tx_mbufs数组(用于下次使用前释放内存)

​ 更新 TDT

硬件

​ **网卡硬件内部的状态机(FSM)**轮询发现 TDT != TDH

​ 从当前 TDH 指向的位置,读取描述符(Descriptor) 的内容(地址、长度、CMD 位)。

​ 解析出 addrlength

​ 发起 PCIe 读事务(DMA 读),去内存中读取实际的 mbuf 数据。

​ 数据读回来,放入网卡内部的 FIFO,再通过网线发出去

回写描述符的 status 字段: 置位 E1000_TXD_STAT_DD(Descriptor Done)位。

收包过程

网卡硬件提供

​ RDH 硬件下一个要写的位置

接收尾指针寄存器 RDT,指向的是“驱动已经处理并回填好的最后一个描述符的索引”

软件

​ 定义 rx_ring[]

​ 定义 rx_mbufs[]

​ e1000_init

​ 把rx_ring数组首元素的地址写入 RDBAL

​ 数组长度写入 RDLEN

​ RDH写0

​ RDT写 RX_RING_SIZE - 1

​ 接收函数

e1000_intr
	e1000_recv

e1000_recv
	while(1)
		根据 RDT 得到 rx_index
		用 rx_index 得到 rx_ring[rx_index] 描述符
		判断描述符的 DD //网卡硬件在将数据包 DMA 搬运到内存完成后,由网卡硬件自动置 1 的标志位
			0 break
			1 继续向下执行
		用描述符->length 更新 rx_mbufs[rx_index]->len
		net_rx 把数据 rx_mbufs[rx_index] 给到协议栈
		
		分配新 mbuf
		更新 desc->addr
		清除 desc->status = 0
		
		更新 RDT

总结

接收包完整过程

e1000驱动初始化:基址+预填所有空仓库

​ rx_ring数组首元素的地址写入 RDBAL

​ 用 mbufalloc() 分配好所有空 mbuf,把它们的物理地址(head一个一个填进 rx_ring 里每个描述符的 addr 字段。

当数据包从网线进入网卡

​ 网卡硬件DMA,根据RDH,找到用rx_ring[]里哪个描述符

​ 从描述符里读出物理地址,把数据搬运到对应物理地址

​ 描述符DD写1,中断通知cpu

cpu用注册好的中断函数处理e1000_intr,调用接收函数

接收函数

​ 传递 **mbuf 结构体的指针(虚拟地址)**给到了协议栈(net.c),而不是物理地址

​ 分配新的mbuf

​ 把新 mbuf 的物理地址填回同一个描述符

​ 清空 status(去掉DD)

更新RDT

协议栈

​ 通过**mbuf 结构体的指针(虚拟地址)**去读取 mbuf->head(里面存的才是物理地址,但协议栈不需要知道,因为 xv6 做了直接映射,虚拟地址可以直接加减偏移得到物理地址)

参考

vx6 网络实验 https://pdos.csail.mit.edu/6.828/2020/labs/net.html

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐