e1000收发包分析
前言
上次看了 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 位)。
解析出 addr 和 length。
发起 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
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐




所有评论(0)