从零写 OS 内核-第三十三篇:e1000 网卡驱动 —— 让你的 OS 接入网络世界!
"没有网络的操作系统,只是信息孤岛。
今天,我们实现 Intel e1000 网卡驱动,让 MyOS 能 ping 通外部世界!"
在前面的篇章中,我们构建了完整的桌面系统:
✅ 窗口管理器
✅ LVGL GUI
✅ 文件系统
但所有操作都局限在本地——无法访问互联网,无法与其他机器通信。
真正的操作系统必须支持网络功能!
而 Intel 8254x 系列(e1000) 是 QEMU 完美模拟的经典网卡,
也是学习网络驱动的最佳起点。
今天,我们就来:
✅ 解析 e1000 硬件架构
✅ 实现 MMIO 寄存器访问
✅ 配置接收/发送描述符
✅ 处理网卡中断
✅ 发送第一个 ICMP Ping 包
让你的 OS 拥有真正的网络能力!
🌐 一、e1000 硬件基础
为什么选择 e1000?
- QEMU 原生支持:
-netdev user,id=n1 -device e1000,netdev=n1 - 文档齐全:Intel 官方《8254x Software Developer's Manual》
- 架构经典:DMA 描述符 + 中断驱动
e1000 关键组件:
| 组件 | 作用 | |——|——| | MMIO 寄存器 | 控制网卡(基地址由 PCI 配置空间提供)| | 接收描述符环 | 网卡存放接收到的帧 | | 发送描述符环 | CPU 提交待发送的帧 | | 中断系统 | 通知 CPU 帧接收/发送完成 |
💡 e1000 通过 DMA 直接读写物理内存,无需 CPU 拷贝数据!
🔌 二、PCI 设备探测
1. 扫描 PCI 总线
e1000 的 PCI Vendor ID = 0x8086(Intel),Device ID = 0x100E(QEMU 模拟)
// drivers/pci.c
bool pci_scan_for_e1000() {
for (int bus = 0; bus < 256; bus++) {
for (int slot = 0; slot < 32; slot++) {
for (int func = 0; func < 8; func++) {
uint16_t vendor = pci_read_word(bus, slot, func, 0x00);
if (vendor == 0xFFFF) continue; // 无效设备
uint16_t device = pci_read_word(bus, slot, func, 0x02);
if (vendor == 0x8086 && device == 0x100E) {
// 找到 e1000
e1000_bus = bus;
e1000_slot = slot;
e1000_func = func;
return true;
}
}
}
}
return false;
}
2. 获取 BAR(Base Address Register)
void e1000_init_mmio() {
// 读取 BAR0(MMIO 基地址)
uint32_t bar0 = pci_read_dword(e1000_bus, e1000_slot, e1000_func, 0x10);
uint32_t mmio_base = bar0 & 0xFFFFFFF0; // 清除标志位
// 映射到内核虚拟地址
e1000_mmio_vaddr = (void*)(mmio_base + KERNEL_VIRTUAL_BASE);
map_range(mmio_base, (uint32_t)e1000_mmio_vaddr, 128 * 1024,
PAGE_PRESENT | PAGE_RW | PAGE_GLOBAL);
}
🔑 MMIO 寄存器通过内存映射访问,无需 inb/outb!
📡 三、e1000 寄存器操作
1. 寄存器定义
// drivers/e1000.h
#define E1000_REG_CTRL 0x0000 // 控制寄存器
#define E1000_REG_STATUS 0x0008 // 状态寄存器
#define E1000_REG_ICR 0x00C0 // 中断原因
#define E1000_REG_IMS 0x00D0 // 中断屏蔽
#define E1000_REG_RCTL 0x0100 // 接收控制
#define E1000_REG_TCTL 0x0400 // 发送控制
#define E1000_REG_RDBAL 0x2800 // 接收描述符基地址低 32 位
#define E1000_REG_TDBAL 0x3800 // 发送描述符基地址低 32 位
2. 读写宏
static inline uint32_t e1000_read(uint32_t reg) {
return *(volatile uint32_t*)((char*)e1000_mmio_vaddr + reg);
}
static inline void e1000_write(uint32_t reg, uint32_t value) {
*(volatile uint32_t*)((char*)e1000_mmio_vaddr + reg) = value;
}
📦 四、接收/发送描述符
1. 描述符结构
// 接收描述符(64 字节)
struct e1000_rx_desc {
uint64_t buffer_addr; // 物理地址(2KB 缓冲区)
uint16_t length; // 接收长度
uint16_t checksum;
uint8_t status; // DD=1 表示有效
uint8_t errors;
uint16_t special;
};
// 发送描述符(16 字节)
struct e1000_tx_desc {
uint64_t buffer_addr; // 物理地址
uint16_t length;
uint8_t cso; // Checksum offset
uint8_t cmd; // RS=1 报告状态, EOP=1 结束包
uint8_t status; // DD=1 表示完成
uint8_t css;
uint16_t special;
};
2. 初始化描述符环
void e1000_init_rx_ring() {
// 1. 分配 32 个接收缓冲区(每个 2KB)
for (int i = 0; i < RX_DESC_COUNT; i++) {
rx_buffers[i] = buddy_alloc_pages(1); // 2KB = 半页
rx_descs[i].buffer_addr = (uint64_t)rx_buffers[i];
rx_descs[i].status = 0;
}
// 2. 设置接收描述符基地址
e1000_write(E1000_REG_RDBAL, (uint32_t)rx_descs);
e1000_write(E1000_REG_RDBAH, 0);
e1000_write(E1000_REG_RDLEN, RX_DESC_COUNT * sizeof(struct e1000_rx_desc));
e1000_write(E1000_REG_RDH, 0);
e1000_write(E1000_REG_RDT, RX_DESC_COUNT - 1); // 尾指针
// 3. 启用接收
e1000_write(E1000_REG_RCTL,
E1000_RCTL_EN | // 启用接收
E1000_RCTL_BAM | // 广播接收
E1000_RCTL_LBM_NONE |
E1000_RCTL_RDMTS_HALF |
E1000_RCTL_BSIZE_2048);
}
⚡ 五、中断处理
1. 启用中断
void e1000_enable_interrupts() {
// 屏蔽所有中断
e1000_write(E1000_REG_IMC, 0xFFFFFFFF);
// 仅启用接收/发送完成中断
e1000_write(E1000_REG_IMS,
E1000_IMS_RXT0 | // 接收超时
E1000_IMS_RXDMT0 | // 接收描述符最小阈值
E1000_IMS_TXDW); // 发送描述符写回
// 允许 PCI 中断
pci_write_word(e1000_bus, e1000_slot, e1000_func, 0x3C, 0x000A); // IRQ 10
pic_enable_irq(10);
}
2. 中断处理程序
void e1000_irq_handler() {
uint32_t icr = e1000_read(E1000_REG_ICR); // 读取中断原因(自动清零)
if (icr & (E1000_ICR_RXT0 | E1000_ICR_RXDMT0)) {
// 处理接收帧
e1000_handle_rx();
}
if (icr & E1000_ICR_TXDW) {
// 处理发送完成
e1000_handle_tx();
}
// 发送 EOI
outb(0xA0, 0x20);
outb(0x20, 0x20);
}
📤 六、发送第一个 Ping 包
1. 构建 ICMP Echo Request
void send_ping(uint32_t dst_ip) {
// 1. 分配发送缓冲区
uint8_t *packet = buddy_alloc_pages(1); // 2KB 足够
// 2. 构建以太网帧头
struct eth_header *eth = (void*)packet;
memcpy(eth->dst, "\xFF\xFF\xFF\xFF\xFF\xFF", 6); // 广播
memcpy(eth->src, e1000_mac, 6);
eth->type = 0x0800; // IPv4
// 3. 构建 IP 头
struct ip_header *ip = (void*)(packet + 14);
ip->version_ihl = 0x45;
ip->tos = 0;
ip->total_length = htons(28 + 20); // ICMP + IP
ip->id = htons(1);
ip->frag_off = 0;
ip->ttl = 64;
ip->protocol = 1; // ICMP
ip->src_ip = htonl(0x0A000001); // 10.0.0.1
ip->dst_ip = htonl(dst_ip);
ip->checksum = 0;
ip->checksum = ip_checksum(ip, 20);
// 4. 构建 ICMP 头
struct icmp_header *icmp = (void*)(packet + 14 + 20);
icmp->type = 8; // Echo Request
icmp->code = 0;
icmp->checksum = 0;
icmp->id = htons(0x1234);
icmp->seq = htons(1);
memcpy(icmp->data, "MyOS Ping", 10);
icmp->checksum = icmp_checksum(icmp, 18);
// 5. 提交到发送描述符
tx_descs[tx_tail].buffer_addr = (uint64_t)packet;
tx_descs[tx_tail].length = 14 + 20 + 18;
tx_descs[tx_tail].cmd = E1000_TXD_CMD_RS | E1000_TXD_CMD_EOP;
tx_descs[tx_tail].status = 0;
// 6. 更新尾指针
tx_tail = (tx_tail + 1) % TX_DESC_COUNT;
e1000_write(E1000_REG_TDT, tx_tail);
}
2. QEMU 测试命令
# 启用 e1000 网卡 + 用户模式网络(自动分配 10.0.2.15)
qemu-system-i386 -kernel kernel.bin -hda disk.img \
-netdev user,id=n1 -device e1000,netdev=n1
# 在 MyOS 中 ping 网关(QEMU 默认网关 10.0.2.2)
ping 10.0.2.2
🧪 七、测试:接收 Ping 回复
1. 处理接收帧
void e1000_handle_rx() {
while (rx_descs[rx_head].status & 0x01) { // DD=1
uint8_t *buffer = rx_buffers[rx_head];
uint16_t length = rx_descs[rx_head].length;
// 检查是否为 ICMP Echo Reply
if (is_icmp_echo_reply(buffer, length)) {
printk("Ping reply received!\n");
}
// 重新挂载缓冲区
rx_descs[rx_head].status = 0;
rx_head = (rx_head + 1) % RX_DESC_COUNT;
e1000_write(E1000_REG_RDT, rx_head - 1);
}
}
2. 运行效果
MyOS# ping 10.0.2.2
Sending ping to 10.0.2.2...
Ping reply received!
✅ e1000 驱动成功收发网络包!
⚠️ 八、关键注意事项
- 物理地址 vs 虚拟地址
- 描述符中的
buffer_addr必须是物理地址 - 通过
virt_to_phys()转换
- 描述符中的
- 缓存一致性
- x86 通常无需显式 flush cache(WC 内存类型)
- 但需确保描述符更新对设备可见
- 描述符环边界
- 头/尾指针需取模运算
- 避免写满整个环(留一个空描述符)
- 错误处理
- 检查
status中的错误位(如 CRC 错误) - 重置网卡(极端情况)
- 检查
💡 Linux 的 e1000 驱动(drivers/net/ethernet/intel/e1000)是绝佳参考!
💬 写在最后
e1000 驱动是网络栈的第一块基石。
它将你的 OS 从孤岛连接到广阔网络,
为后续的 TCP/IP、HTTP、SSH 铺平道路。
今天你发送的第一个 Ping 包,
正是无数网络通信的起点。
🌟 网络的意义,不在于技术,而在于连接。
📬 动手挑战:
实现 ARP 请求/应答,让 MyOS 能通过 MAC 地址通信。
欢迎在评论区分享你的网络抓包截图!
👇 下一篇你想看:ARP 协议实现,还是 UDP 与 DHCP 客户端?
#操作系统 #内核开发 #e1000 #网卡驱动 #网络 #PCI #从零开始
📢 彩蛋:关注后回复关键词 "e1000",获取:
- 完整 e1000 驱动代码(含 PCI 扫描、描述符管理)
- ICMP Ping 实现模板
- QEMU 网络测试配置指南