一、e1000 网卡驱动
首先需要完成网卡驱动,才能使系统能够接收和发送数据包。e1000网卡也属于PCI设备。我们这里直接根据代码来看网卡的工作流程
typedef struct eth_t // 以太网帧
{
eth_addr_t dst; // 目标地址
eth_addr_t src; // 源地址
u16 type; // 类型
u8 payload[0]; // 载荷
} _packed eth_t;
#define ETH_ADDR_LEN 6
#define IP_ADDR_LEN 4
typedef u8 eth_addr_t[ETH_ADDR_LEN]; // MAC 地址
typedef u8 ip_addr_t[IP_ADDR_LEN]; // IPV4 地址
以上为以太网一个帧格式的定义,接下来看网卡驱动:
#define RX_DESC_NR 32 // 接收描述符数量
#define TX_DESC_NR 32 // 传输描述符数量
typedef struct rx_desc_t // 接收描述符
{
u64 addr; // 数据缓冲器地址
u16 length; // 长度
u16 checksum; // 校验和
u8 status; // 状态
u8 error; // 错误
u16 special; // 特殊
} _packed rx_desc_t;
typedef struct tx_desc_t // 传输描述符
{
u64 addr; // 缓冲区地址
u16 length; // 包长度
u8 cso; // Checksum Offset
u8 cmd; // 命令
u8 status; // 状态
u8 css; // Checksum Start Field
u16 special; // 特殊
} _packed tx_desc_t;
#define NAME_LEN 16
typedef struct e1000_t
{
char name[NAME_LEN]; // 名称
pci_device_t *device; // PCI 设备
u32 membase; // 映射内存基地址
u8 mac[6]; // MAC 地址
bool link; // 网络连接状态
bool eeprom; // 只读存储器可用
rx_desc_t *rx_desc; // 接收描述符
u16 rx_cur; // 接收描述符指针
tx_desc_t *tx_desc; // 传输描述符
u16 tx_cur; // 传输描述符指针
task_t *tx_waiter; // 传输等待进程
} e1000_t;
static e1000_t obj;
static void recv_packet(e1000_t *e1000) // 接收数据包
{
while (true)
{
rx_desc_t *rx = &e1000->rx_desc[e1000->rx_cur]; // 接收描述符
// TODO RECEIVE PACKET
eth_t *eth = (eth_t *)(u32)(rx->addr & 0xffffffff); // 获取以太网帧
LOGK("ETH R [0x%04X]: %m -> %m, %d\n",
ntohs(eth->type),
eth->src,
eth->dst,
rx->length);
rx->status = 0;
moutl(e1000->membase + E1000_RDT, e1000->rx_cur);
e1000->rx_cur = (e1000->rx_cur + 1) % RX_DESC_NR;
}
}
void send_packet(eth_t *eth, u16 len) // 发送数据包
{
e1000_t *e1000 = &obj;
tx_desc_t *tx = &e1000->tx_desc[e1000->tx_cur]; // 发送描述符
while (tx->status == 0)
{
assert(e1000->tx_waiter == NULL);
e1000->tx_waiter = running_task();
assert(task_block(e1000->tx_waiter, NULL, TASK_BLOCKED, TIMELESS) == EOK);
}
memcpy((void *)(u32)tx->addr, eth, len);
tx->length = len;
tx->cmd = TCMD_EOP | TCMD_RS | TCMD_RPS | TCMD_IFCS;
tx->status = 0;
e1000->tx_cur = (e1000->tx_cur + 1) % TX_DESC_NR;
moutl(e1000->membase + E1000_TDT, e1000->tx_cur);
LOGK("ETH S [0x%04X]: %m -> %m, %d\n",
ntohs(eth->type),
eth->src,
eth->dst,
len);
}
我们这里只粘贴了发送数据包和接收数据包的实现,省略了大部分辅助函数和网卡的初始化。在网卡初始化过程中,首先要查找PCI设备是否有网卡,找到后设置物理内存映射区域和网卡中断处理函数。
接收数据包我们这里写了死循环,它不断从网卡的接收描述符环(RX Descriptor Ring)里取出已经接收到的数据包,然后打印以太网帧的信息。它的职责是:不停地检查网卡的接收描述符队列,看看有没有新包进来。网卡每次接收到包,会触发中断,中断服务例程(ISR)调用 recv_packet(),让它把所有当前队列中已完成的包都取完。
在现代网络中,只有发给自己或广播/多播的帧才会被送到网卡端口,网卡芯片内部有一套地址过滤机制(Address Filtering),比如 Intel e1000 就有这样的硬件逻辑:它会检查每个接收到的以太网帧头部(frame header):接收广播帧和发给自己MAC地址的帧。如下为日志打印的包结果:
[kernel/e1000.c] [263] ETH R [0x0806]: 00:50:56:f2:39:f2 -> ff:ff:ff:ff:ff:ff, 60
[kernel/e1000.c] [263] ETH R [0x0806]: 5a:5a:5a:5a:5a:22 -> 00:50:56:f2:39:f2, 60
[kernel/e1000.c] [263] ETH R [0x0800]: 5a:5a:5a:5a:5a:22 -> 00:50:56:f2:39:f2, 74
[kernel/e1000.c] [263] ETH R [0x0800]: 5a:5a:5a:5a:5a:22 -> 00:50:56:f2:39:f2, 60
[kernel/e1000.c] [263] ETH R [0x0800]: 5a:5a:5a:5a:5a:22 -> 00:50:56:f2:39:f2, 142
[kernel/e1000.c] [263] ETH R [0x0800]: 5a:5a:5a:5a:5a:22 -> 00:50:56:f2:39:f2, 60
[kernel/e1000.c] [263] ETH R [0x0806]: 00:50:56:f2:39:f2 -> ff:ff:ff:ff:ff:ff, 60
[kernel/e1000.c] [263] ETH R [0x0806]: 5a:5a:5a:5a:5a:22 -> 00:50:56:f2:39:f2, 60
[kernel/e1000.c] [263] ETH R [0x0806]: 5a:5a:5a:5a:5a:22 -> 3e:22:fb:78:3d:65, 60
如图所示,mac地址22是网桥br0, 3e:22:fb:78:3d:65为宿主机的网卡MAC。我们的网卡可以接受到广播包和局域网内的包。最后我们理一下数据接收流程:
- 网线收到电/光信号 → 网卡 PHY 解码成比特流 → 网卡 MAC 层组装成以太网帧。
- 网卡检测到帧目标地址是自己(或者广播/多播) → 决定接收。
- 网卡通过 DMA把数据包写入 RX 描述符的 addr 指向的缓冲区。
- 网卡在 RX 描述符里更新: length:写入的数据长度 status:标记数据已经接收完成 checksum / error 等硬件信息
- CPU 驱动检查描述符的状态位,知道数据包已经到内存,可以处理。
二、发送数据包测试
首先需要再makefile中配置qemu网卡,
QEMU+= -netdev tap,id=eth0,ifname=tap0,script=no,downscript=no # 网络设备
QEMU+= -device e1000,netdev=eth0,mac=5A:5A:5A:5A:5A:33 # 网卡 e1000
指定使用 TAP 网络接口 作为虚拟机的网络后端。给这个网络后端分配一个 ID名字为 eth0。绑定宿主机上已有的 TAP 设备 tap0,也就是说,虚拟机网卡通过 tap0 与宿主机通信。 以下为发包的测试程序:
// 发送测试数据包
void test_e1000_send_packet()
{
e1000_t *e1000 = &obj; // 获取到网卡
eth_t *eth = (eth_t *)alloc_kpage(1); // 申请一页内存,存放以太网帧
memcpy(eth->src, e1000->mac, 6); // 将以太网卡的mac地址赋为源
memcpy(eth->dst, "\xff\xff\xff\xff\xff\xff", 6); // 将目的 MAC 设置为广播地址 FF:FF:FF:FF:FF:FF。发送到这个地址意味着局域网内所有主机都会接收到这个帧。
eth->type = 0x0090; // LOOP 0x9000
int len = 1500;
memset(eth->payload, 'A', len);
send_packet(eth, len + sizeof(eth_t)); // 发送数据包A
free_kpage((u32)eth, 1);
}
这个函数是发送一个以太网测试数据包,通过 e1000 网卡驱动发送。
我们在宿主机上进行抓包,可以看到,网卡发送的广播包会被发给局域网内的所有地址。
三、数据包高速缓冲
数据包高速缓冲是一种缓存机制,尽可能避免调用 memcpy 函数。如图所示:
缓存内容是以太网帧,这里有一个概念叫MTU,MTU为1500表示最大传输单元,单位是字节。如下为缓冲的实现:
typedef struct pbuf_t
{
list_node_t node; // 列表节点
size_t length; // 载荷长度
u32 count; // 引用计数
union
{
u8 payload[0]; // 载荷
eth_t eth[0]; // 以太网帧
};
} pbuf_t;
static list_t free_buf_list;
static size_t pbuf_count = 0;
pbuf_t *pbuf_get() // 获取空闲缓冲
{
pbuf_t *pbuf = NULL;
if (list_empty(&free_buf_list)) // 如果链表为空就申请一页内存
{
u32 page = alloc_kpage(1);
pbuf = (pbuf_t *)page;
list_push(&free_buf_list, &pbuf->node);
page += PAGE_SIZE / 2; // 一页内存分为两部分,各2048字节,加入到空闲链表
pbuf = (pbuf_t *)page;
list_push(&free_buf_list, &pbuf->node);
pbuf_count += 2;
LOGK("pbuf count %d\n", pbuf_count);
}
pbuf = element_entry(pbuf_t, node, list_popback(&free_buf_list));
assert(((u32)pbuf & 0x7ff) == 0); // 应该对齐到 2K
pbuf->count = 1;
return pbuf;
}
void pbuf_put(pbuf_t *pbuf) // 释放缓冲
{
assert(((u32)pbuf & 0x7ff) == 0); // 应该对齐到 2K
assert(pbuf->count > 0);
pbuf->count--;
if (pbuf->count > 0)
{
assert(pbuf->node.next && pbuf->node.prev);
return;
}
list_push(&free_buf_list, &pbuf->node);
}
void pbuf_init() // 初始化数据包缓冲, 即初始化链表
{
list_init(&free_buf_list);
}
如此,在网卡驱动的实现中就可以申请数据包缓冲区,这个具体实现我们不做过多分析。
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付