驱动程序是操作系统的重要组成部分,负责沟通操作系统和实际工作的硬件。在本次实 验中,我们会在 xv6 中为一个现实中广泛使用的硬件:Intel E1000 网卡,编写驱动程序,从而 体会编写驱动程序的一般步骤。
首先我们切换到 net 分支
Intel E1000 网卡是一类常见的千兆以太网卡,广泛用于各类个人电脑和服务器中。由于 其支持较为完善,且文档齐全,故而在我们的 qemu 中也有软件模拟的 E1000 设备,可供 xv6实验使用。
在开始编写驱动程序前,我们需要获取 Intel 提供的关于 E1000 网卡的驱动的开发者文档 Intel E1000 Software Developer’s Manual 1 ,其中包含了关于该网卡硬件特性和工作机制的说明。根据 xv6 的实验手册,我们主要需要关注以下内容:
大致浏览过上面的内容后,我们主要需要实现 kernel/e1000.c 中的两个函数:用于发送 数据包的 e1000_transmit() 和用于接收数据包的 e1000_recv() 。用于初始化设备的 e1000_init() 已经被实现好了,我们主要关注的是其涉及到的一些数据结构:
#define TX_RING_SIZE 16
static struct tx_desc tx_ring[TX_RING_SIZE] __attribute__((aligned(16)));
static struct mbuf *tx_mbufs[TX_RING_SIZE];
#define RX_RING_SIZE 16
static struct rx_desc rx_ring[RX_RING_SIZE] __attribute__((aligned(16)));
static struct mbuf *rx_mbufs[RX_RING_SIZE];
// remember where the e1000's registers live.
static volatile uint32 *regs;
struct spinlock e1000_lock;
其中最重要的是两个环形缓冲区:tx_ring 和 rx_ring 。根据 Intel E1000 Software Developer’s Manual 上的描述,我们只需要将需要发送的数据包放入环形缓冲区中,设置好对应的参数并更新管理缓冲区的寄存器,即可视为完成了数据包的发送。此后网卡的硬件会自动在合适的时间将我们放入的数据包按照配置发送出去,在 kernel/e1000.c 中的实现如下:
int
e1000_transmit(struct mbuf *m)
{
//
// Your code here.
acquire(&e1000_lock);
//printf("e1000_transmit: called mbuf=%p\\n",m);
uint32 idx = regs[E1000_TDT];
if (tx_ring[idx].status != E1000_TXD_STAT_DD)
{
printf("e1000_transmit: tx queue full\\n");
// __sync_synchronize();
release(&e1000_lock);
return -1;
} else {
if (tx_mbufs[idx] != 0)
{
mbuffree(tx_mbufs[idx]);
}
tx_ring[idx].addr = (uint64) m->head;
tx_ring[idx].length = (uint16) m->len;
tx_ring[idx].cso = 0;
tx_ring[idx].css = 0;
tx_ring[idx].cmd = E1000_TXD_CMD_RS | E1000_TXD_CMD_EOP;
tx_mbufs[idx] = m;
regs[E1000_TDT] = (regs[E1000_TDT] + 1) % TX_RING_SIZE;
}
// __sync_synchronize();
release(&e1000_lock);
return 0;
}
该函数的具体实现流程如下: