您的内核没有时间概念,所以我们需要添加它。目前有一个时钟中断
,每10ms由硬件产生
一次。在每个时钟中断时,我们可以增加一个变量来表示时间已经提前了10ms。这是在kern/time.c中实现的,但尚未完全集成到内核中。
Exercise 1. 为kern/trap.c中的每个时钟中断添加对
time_tick
的调用。实现sys_time_msec
并将其添加到kern/syscall.c中的syscall中,以便用户空间能够访问时间。
//kern/trap.c/trap_dispatch()
else if(tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER){
lapic_eoi(); //??承认中断?这个是真想不到
time_tick(); //我暂时还不懂为啥要注意多核处理器中时钟中断可被每个CPU触发?
sched_yield();
}
// kern/syscall.c
static int sys_time_msec(void){
// LAB 6: Your code here.
//kern/time.c里有这个函数,一共有ticks个10ms,所以当前时间是ticks*10 (单位:毫秒)
return time_msec();
}
//可以看一下kern/time.c里的几个函数,基本是围绕静态变量ticks的
void time_init(void){
ticks = 0;
}
// This should be called once per timer interrupt. A timer interrupt
// fires every 10 ms.
void time_tick(void){
ticks++;
if (ticks * 10 < ticks)
panic("time_tick: time overflowed");
}
unsigned int time_msec(void){
return ticks * 10;
}
使用make INIT_CFLAGS=-DTEST_NO_NS run-testtime
来测试您的时间代码。您应该看到环境在1秒内从5开始倒数(count down)。“-DTEST_NO_NS”禁用启动网络服务器环境,否则此时会引起panic。
starting count down: 5 4 3 2 1 0
Welcome to the JOS kernel monitor!
The Network Interface Card
编写驱动程序需要深入了解硬件和展示给软件的接口。实验文本将提供如何与E1000交互
的高级概述,但您需要在编写驱动程序时广泛使用Intel的手册。
Exercise 2. 浏览 Intel’s Software Developer’s Manual 的E1000。本手册涵盖了几个密切相关的以太网
(Ethernet)控制器
。QEMU模拟82540EM。
现在你应该浏览一下第2章
来感受一下这个设备。要编写驱动程序,您需要熟悉第3章
和第14章
,以及4.1章
(虽然不是4.1小节)。你也需要使用第13章
作为参考。其他章节主要介绍E1000的组件,您的驱动程序将不需要与之交互。现在不要担心细节;您只需要了解一下文档结构
,以便以后查找。
在阅读手册时,请记住E1000是一个具有许多高级功能的复杂设备
。
PCI Interface
E1000是一个PCI设备
,这意味着它可以插入主板上的PCI bus
(总线)。PCI总线具有地址、数据和中断线路,允许CPU与PCI设备通信
,并允许PCI设备读写内存
。PCI设备在使用之前需要被发现和初始化。discovery
是在PCI总线上寻找附加设备的过程。initialization
是分配I/O和内存空间,以及协商(negotiating)给设备使用的IRQ线
的过程。
我们已经在kern/PCI.c
中为您提供了PCI代码。为了在引导期间执行PCI初始化,PCI代码将遍历PCI总线寻找设备。当它找到一个设备时,它读取其vendor ID和device ID,并使用这两个值作为键来搜索pci_attach_vendor
数组。数组由struct pci_driver
条目组成,如下所示:
struct pci_driver {
uint32_t key1, key2;
int (*attachfn) (struct pci_func *pcif);
};
如果匹配成功,PCI代码将调用该条目的attachfn
来执行设备初始化。(设备也可以通过class来标识,这是kern/pci.c中的另一个驱动表的作用。)
attach函数被传递一个PCI函数来初始化。PCI卡可以公开(expose)多个功能,但E1000只公开一个功能。下面是我们JOS中的一个PCI function:
struct pci_func {
struct pci_bus *bus;
uint32_t dev;
uint32_t func;
uint32_t dev_id;
uint32_t dev_class;
uint32_t reg_base[6];
uint32_t reg_size[6];
uint8_t irq_line;
};
struct pci_bus {
struct pci_func *parent_bridge;
uint32_t busno;
};
上面的结构反映了开发人员手册4.1节Table 4-1中的一些条目。struct pci_func的最后三个条目尤其让我们感兴趣,因为它们记录了设备的negotiated memory, I/O, and interrupt resources
。reg_base和reg_size数组包含最多六个Base Address Registers or BARs的信息。reg_base
存储内存映射I/O区域的base memory addresses(or base I/O ports for I/O port resources),reg_size
包含对应的reg_base基值的字节大小或I/O端口数量,irq_line
包含分配给设备用于中断的IRQ lines。表4-2的后半部分给出了E1000条的具体含义。
调用设备的attach函数
时,已找到该设备,但尚未启用该设备。这意味着PCI代码还没有确定分配给设备的资源,例如地址空间和IRQ lines,因此,struct pci_func结构的最后三个元素还没有填充。attach函数应该调用pci_func_enable
(在attach函数里调用,而不是直接作为attach函数),它将启用设备,协商这些资源,并填充struct pci_func
。
Exercise 3. 实现一个
attach函数
来初始化E1000。在kern/ PCI.c中的pci_attach_vendor数组
中添加一个条目,以便在找到匹配的PCI设备时触发函数(请确保将它放在表示 末尾的{0,0,0}条目
之前)。您可以在第5.2节中找到QEMU模拟的82540EM的 vendor ID and device ID。您还应该在启动时JOS扫描PCI总线时看到列出的这些。
现在,只需通过pci_func_enable
启用E1000设备。我们将在整个实验室中添加更多的初始化。
我们已经为您提供了kern/e1000.c
和kern/e1000.h
文件,这样您就不需要破坏构建系统。它们目前是空白的;这次exercise你需要把它们填进去。您可能还需要在内核的其他位置包含e1000.h头文件
。
当您启动内核时,您应该看到它打印启用了E1000卡的PCI功能。您的代码现在应该通过了make grade的pci attach
测试。
先来简单看看PCI:
PCI
是外围设备互连(Peripheral Component Interconnect
)的简称,是在目前计算机系统中得到广泛应用的通用总线接口标准:
- 在一个PCI系统中,最多可以有256根PCI总线,一般主机上只会用到其中很少的几条。
- 在一根PCI总线上可以连接多个物理设备,可以是一个网卡、显卡或者声卡等,最多不超过32个。
- 一个PCI物理设备可以有多个功能,比如同时提供视频解析和声音解析,最多可提供8个功能。
- 每个功能对应1个256字节的PCI配置空间。
谢谢 bysui
对这个标准最直观的感受就是下面这个函数:
static void
pci_conf1_set_addr(uint32_t bus,
uint32_t dev,
uint32_t func,
uint32_t offset)
{
assert(bus < 256); //8位 最多可以有256根PCI总线,一般主机上只会用到其中很少的几根
assert(dev < 32); //5位 一根PCI总线可以连接多个物理设备,可以是一个网卡、显卡或声卡等,最多不超过32个
assert(func < 8); //3位 一个PCI物理设备可以有多个功能,比如同时提供视频解析和声音解析,最多可提供8个功能。
assert(offset < 256); //8位 每个功能对应1个256字节的PCI配置空间。
assert((offset & 0x3) == 0);//最后两位必须为00?
uint32_t v = (1 << 31) | // config-space
(bus << 16) | (dev << 11) | (func << 8) | (offset);
outl(pci_conf1_addr_ioport, v);
}
下面是PCI的configuration space
,同样值得好好看看
00h-3Ch
这64个字节是标准化的,提供了厂商号、设备号、版本号等信息,唯一标示1个PCI设备,同时提供最多6个的IO地址区域。
除了这些,想完成exercise还得在5.2节的表中找到82540EM对应的VENDOR_ID跟DEVICE_ID
82540EM-A 8086h 100E Desktop(台式机)
然后让我们来看看这个exercise该怎么完成:
//kern/e1000.h
#ifndef JOS_KERN_E1000_H
#define JOS_KERN_E1000_H
#include <kern/pci.h>
int e1000_init(struct pci_func *pcif);
#endif // SOL >= 6
// kern/e1000.c
int
e1000_init(struct pci_func *pcif)
{
pci_func_enable(pcif);
return 1;
}
//kern/pci.c
// pci_attach_vendor matches the vendor ID and device ID of a PCI device. key1
// and key2 should be the vendor ID and device ID respectively
struct pci_driver pci_attach_vendor[] = {
{ PCI_VENDOR_ID, PCI_DEVICE_ID, &e1000_init },
{ 0, 0, 0 },
};
//kern/pcireg.h
#define PCI_VENDOR_ID 0x8086
#define PCI_DEVICE_ID 0x100E
结果如下:
PCI function 00:03.0 (8086:100e) enabled
Memory-mapped I/O
软件通过memory-mapped I/O (MMIO)
与E1000通信。您以前在JOS已经见过两次了:CGA控制台
和LAPIC
都是通过写入和读取“内存”来控制和查询(query)的设备。但是这些读和写不会进入DRAM
;它们直接进入这些设备。不太明白。
pci_func_enable与E1000协商一个MMIO区域,并将其base和size存储在BAR 0中(即reg_base[0]
和reg_size[0]
)。这是分配给设备的物理内存地址
的范围,这意味着您必须做一些事情来通过虚拟地址访问
它。由于MMIO区域被分配了非常高
的物理地址(通常在3GB以上),由于JOS的256MB限制
,您不能使用KADDR访问它。因此,您必须创建
一个新内存映射
。
我们将使用MMIOBASE
上面的区域(Lab 4中的mmio_map_region将确保我们不会覆盖LAPIC使用的映射)。由于PCI设备初始化发生在JOS创建用户环境之前
,所以您可以在kern_pgdir中创建映射,并且它总是可用的。
Exercise 4. 在
attach函数
中,通过调用mmio_map_region
为E1000的BAR 0
创建一个虚拟内存映射(你在lab 4中写的用来支持内存映射LAPIC)。
您将希望在a variable
中记录这个映射的位置,以便稍后访问刚才映射的寄存器。以kern/lapic.c中的lapic变量为例说明一种方法。如果确实使用指向设备寄存器映射的指针,请确保声明它为volatile
;否则,编译器将被允许缓存值并重新排序对该内存的访问。
要测试映射,请尝试打印设备状态寄存器
(第13.4.2节)。这是一个从寄存器空间的字节8开始的4字节寄存器。您应该得到0x80080783,这表示一个完整的双工链路(full duplex link
)的大小为1000MB/s。
提示:您需要很多常量(constants
),比如寄存器的位置
和bit masks
的值。试图从开发人员手册中复制这些内容很容易出错,错误可能导致痛苦的调试过程。我们建议使用QEMU的e1000_hw.h头作为指导原则。我们不建议逐字复制它,因为它定义的内容比您实际需要的多得多,而且可能不会以您需要的方式定义内容,但这是一个很好的起点。
//kern/e1000.h
#define E1000_STATUS 0x00008 /* Device Status - RO */
#define e1000_print_status(offset) \
cprintf("the E1000 status register: [%08x]\n", *(pci_e1000+(offset>>2)));
// 由于pci_e1000是uint32_t的,如果直接加offset,就相当于加了offset*sizeof(uint32_t)
// 例如the E1000:[ef804000] offset:[00000008] sum:[ef804020]
//kern/e1000.c
pci_e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]);
e1000_print_status(E1000_STATUS);
the E1000 status register: [80080783]
DMA(Direct Memory Access)
您可以想象通过从E1000的寄存器写和读来发送和接收数据包,但是这会很慢,并且需要E1000在内部缓冲数据包数据。相反,E1000使用Direct Memory Access
(DMA)直接从内存读写数据包数据,而不涉及CPU。驱动程序负责为发送和接收队列
分配内存,设置DMA描述符
,并使用这些队列的位置配置E1000
,但之后的一切都是异步的
。
要传输数据包,驱动程序将其复制到传输队列中的下一个DMA描述符中,并通知E1000另一个数据包可用;嗯哼?当有时间发送数据包时,E1000将从描述符中复制数据。同样,当E1000接收到一个数据包时,它将它复制到接收队列中的下一个DMA描述符中,驱动程序可以在下次机会时从该描述符中读取。
在高层次上,接收队列和传输队列非常相似。两者都由一系列描述符组成。虽然这些描述符的确切结构各不相同,但是每个描述符都包含一些标志
和包含包数据的缓冲区的物理地址
(要么是要发送的数据包数据,要么是操作系统分配给card的缓冲区,以便将接收到的数据包写入其中)。
队列被实现为循环数组
,这意味着当card或驱动程序到达数组的末尾时,它会绕回开始处。它们都有一个头指针和一个尾指针,队列的内容是这两个指针之间的描述符
。硬件总是从头部拿出描述符并移动头部指针,而驱动程序总是向尾部添加描述符并移动尾部指针。(所以头指针指向有内容的,尾指针指向空的)。传输队列中的描述符表示等待发送的包
(因此,在稳定状态(steady state
)下,传输队列是空的)。正确地更新tail register
而不使E1000混淆是很棘手的;小心!
指向这些数组的指针以及描述符中包缓冲区的地址都必须是物理地址
,因为硬件直接在物理RAM之间执行DMA,而不需要经过MMU。
Transmitting Packets
E1000的transmit和receive功能基本上是相互独立
的,所以我们一次可以处理一个。我们将首先测试transmitting packets,因为如果不首先发送一个“I’m here!”包,我们就无法测试receive。
首先,您必须按照第14.5节中描述的步骤初始化card(您不必关心子章节)。传输初始化的第一步是设置传输队列。队列的确切structure
见第3.4节,描述符的structure见第3.3.3节。我们不会使用E1000的TCP卸载特性,所以您可以专注于“legacy(遗留) transmit descriptor format”。您现在应该阅读这些部分,熟悉这些结构。
C Structures
您会发现使用C结构体来描述E1000的structure非常方便。正如您在struct Trapframe等结构中看到的,C struct允许您在内存中精确布局数据
。C可以在字段之间插入填充( padding ),但是E1000的结构是这样安排的,所以这不应该是一个问题。如果您确实遇到字段对齐问题,请查看GCC的“packed”属性。
例如,考虑手册table 3-8中给出的并重现在下面的遗留(legacy) transmit descriptor
:
63 48 47 40 39 32 31 24 23 16 15 0
+---------------------------------------------------------------+
| Buffer address |
+---------------+-------+-------+-------+-------+---------------+
| Special | CSS | Status| Cmd | CSO | Length |
+---------------+-------+-------+-------+-------+---------------+
结构的第一个字节从右上角开始,因此要将其转换为C结构体,请从右向左、从上到下读取。如果你斜视它,你会发现所有的字段都很符合标准大小的类型:
struct tx_desc
{
uint64_t addr; //Address of the transmit descriptor in the host memory(缓冲区物理地址?)
uint16_t length; // the total length of the packet
uint8_t cso;
uint8_t cmd;
uint8_t status;
uint8_t css; //The Checksum start field (TDESC.CSS) indicates where to begin computing the checksum.
uint16_t special;
};
驱动程序必须为transmit descriptor数组
和由传输描述符指向的packet buffers
预留内存。有几种方法可以做到这一点,从动态分配页面到简单地在全局变量中声明页面。无论您选择什么,请记住E1000直接访问物理内存
,这意味着它访问的任何缓冲区必须在物理内存中是连续的
。
还有多种方法可以处理packet buffers
。最简单的方法(我们建议从这里开始)是在驱动程序初始化期间为每个描述符预留
一个包缓冲区,并简单地将包数据复制到这些预先分配的缓冲区中并从中取出。以太网数据包的最大大小为1518字节
,这限制了这些缓冲区的大小。更复杂的驱动程序可以动态分配包缓冲区(例如,当网络使用率较低时,可以减少内存开销),甚至可以传递用户空间直接提供的缓冲区(一种称为“zero copy”的技术),但是最好从简单的开始。
Exercise 5. 执行14.5节中描述的
初始化步骤
(但不是它的子节)。使用第13节作为初始化过程涉及寄存器的参考,使用第3.3.3和3.4节作为传输描述符和传输描述符数组的参考。
注意传输描述符数组的对齐要求
和对该数组长度
的限制。由于TDLEN
( Transmit Descriptor Length)必须是128字节对齐的,并且每个传输描述符都是16字节,所以您的传输描述符数组需要8个传输描述符的若干倍。但是,不要使用超过64个描述符,否则我们的测试将无法测试传输环路溢出。
TCTL.COLD
(Transmit Control Register),可假设采用全双工操作。对于TIPG,请参考IEEE 802.3标准IPG第13.4.34节表13-77中描述的默认值(不要使用14.5节中的表中的值)。
14.5节中描述的初始化步骤
如下:
- 为transmit descriptor list(
循环数组
)分配一块内存区域。(TDBAL/TDBAH
)指向这段区域的物理地址
- 设置 Transmit Descriptor Length (
TDLEN
) register为这个描述符循环数组的大小,以字节为单位 - 初始化 Transmit Control Register(
TCTL
) 。设置TCTL.EN=1b,TCTL.PSP=1b,TCTL.CT=10h,TCTL.COLD假设全双工下=40h - 编写 Transmit IPG (
TIPG
) register
//kern/e1000.h
#define E1000_TCTL 0x00400 /* TX Control - RW */
#define E1000_TDBAL 0x03800 /* TX Descriptor Base Address Low - RW */
#define E1000_TDBAH 0x03804 /* TX Descriptor Base Address High - RW */
#define E1000_TDLEN 0x03808 /* TX Descriptor Length - RW */
#define E1000_TDH 0x03810 /* TX Descriptor Head - RW */
#define E1000_TDT 0x03818 /* TX Descripotr Tail - RW */
#define E1000_TIPG 0x00410 /* TX Inter-packet gap -RW */
#define E1000_TCTL_EN 0x00000002 /* enable tx */
#define E1000_TCTL_BCE 0x00000004 /* busy check enable */
#define E1000_TCTL_PSP 0x00000008 /* pad short packets */
#define E1000_TCTL_CT 0x00000ff0 /* collision threshold */
#define E1000_TCTL_COLD 0x003ff000 /* collision distance */
#define E1000_TXD_CMD_RS 0x08000000 /* Report Status */
#define E1000_TXD_STAT_DD 0x00000001 /* Descriptor Done */
#define TX_MAX 64
struct tx_desc
{
uint64_t addr; //Address of the transmit descriptor in the host memory
uint16_t length; //The Checksum offset field indicates where to insert a TCP checksum if this mode is enabled.
uint8_t cso;
uint8_t cmd;
uint8_t status;
uint8_t css; //The Checksum start field (TDESC.CSS) indicates where to begin computing the checksum.
uint16_t special;
}__attribute__((packed));
struct tx_desc tx_list[TX_MAX];
struct packets{
char buffer[2048];
}__attribute__((packed));
struct packets tx_buf[TX_MAX];
void e1000_transmit_init();
//kern/e1000.c
void
e1000_transmit_init(){
//别忘了要加到e1000_init()里面才起作用
memset(tx_list, 0, sizeof(struct tx_desc)*TX_MAX);
memset(tx_buf, 0, sizeof(struct packets)*TX_MAX);
for(int i=0; i<TX_MAX; i++){
tx_list[i].addr = PADDR(tx_buf[i].buffer); //不太懂为什么可以用PADDR
tx_list[i].cmd = (E1000_TXD_CMD_EOP>>24) | (E1000_TXD_CMD_RS>>24);
tx_list[i].status = E1000_TXD_STAT_DD;
}
pci_e1000[E1000_TDBAL>>2] = PADDR(tx_list);
pci_e1000[E1000_TDBAH>>2] = 0;
pci_e1000[E1000_TDLEN>>2] = TX_MAX*sizeof(struct tx_desc);
pci_e1000[E1000_TDH>>2] = 0;
pci_e1000[E1000_TDT>>2] = 0;
//pci_e1000[E1000_TCTL>>2] |= 0x4010A;
pci_e1000[E1000_TCTL>>2] |= (E1000_TCTL_EN | E1000_TCTL_PSP |
(E1000_TCTL_CT & (0x10<<4)) |
(E1000_TCTL_COLD & (0x40<<12)));
pci_e1000[E1000_TIPG>>2] |= (10) | (4<<10) | (6<<20);
}
试着运行make E1000_DEBUG=TXERR,TX qemu
。如果您正在使用qemu,那么在设置TDT register
时,应该会看到一条“e1000: tx disabled”消息(因为这是在设置TCTL.EN之前发生的),并且不再有“e1000”消息。
现在传输已经初始化,您必须编写代码来传输数据包
,并通过系统调用
让用户空间能够访问它。您必须将它添加到传输队列的尾部,这意味着将包数据复制到下一个包缓冲区,然后更新TDT
(transmit descriptor tail)寄存器,以通知card
在传输队列中有另一个包。(注意,TDT是传输描述符数组的索引
,而不是字节偏移量;文档对此不是很清楚。)
然而,传输队列只有这么大。如果card落后于传输包,且传输队列已满
,会发生什么情况?为了检测这种情况,您需要从E1000得到一些反馈。不幸的是,您不能只使用TDH(transmit descriptor head
)寄存器;文档明确声明从软件中读取寄存器是不可靠的。但是,如果您在传输描述符的cmd字段
中设置RS位
(Report Status),那么,当card在该描述符中传输了数据包之后,card将在描述符的status字段
中设置DD位
(Descriptor Done)。如果设置了描述符的DD位,您就知道可以安全地回收该描述符并使用
它来传输另一个包。
如果用户调用您的传输系统调用,但是下一个描述符的DD位没有被设置,表示传输队列已满
,该怎么办?你必须决定在这种情况下该怎么办。你可以直接把packet扔了。Network protocols对此是有弹性的,但是如果您丢弃大量的数据包,协议可能无法恢复。您也可以告诉用户环境去重试
,就像您对sys_ipc_try_send所做的那样。这样做的好处是将问题回推给生成数据的环境。
Exercise 6. 编写一个函数,通过检查下一个描述符是否空闲、将包数据复制到下一个描述符并更新TDT来传输包。确保您处理了传输队列已满这种情况。
//kern/e1000.c
int
fit_txd_for_E1000_transmit(void *addr, int length){
int tail = pci_e1000[E1000_TDT>>2];
/* 这样写很明显是错的,因为对tx_next的修改并没有写回tx_list[tail],
因为tx_next是个新对象,并不是对tx_list[tail]的引用
struct tx_desc tx_next = tx_list[tail];
if(length > sizeof(struct packets))
length = sizeof(struct packets); //最大也只能2048 bytes
if(tx_next.status & E1000_TXD_STAT_DD){
memmove(KADDR(tx_next.addr), addr, length);
tx_next.status = 0; //表示现在该描述符还没被处理
tx_next.length = length;
pci_e1000[E1000_TDT>>2] = (tail + 1)%TX_MAX;
memcpy(&tx_list[tail], &tx_next, sizeof(struct tx_desc));//就算我在这复制回去了,也还是不行,真的不懂了
return 0;
}*/
//正确写法
struct tx_desc *tx_next = &tx_list[tail];
if(length > sizeof(struct packets))
length = sizeof(struct packets); //最大也只能2048 bytes
if((tx_next->status & E1000_TXD_STAT_DD) == E1000_TXD_STAT_DD){
//memmove(tx_buf[tail].buffer, addr, length);
memmove(KADDR(tx_next->addr), addr, length);
tx_next->status &= !E1000_TXD_STAT_DD; //表示现在该描述符还没被处理
tx_next->length = (uint16_t)length;
pci_e1000[E1000_TDT>>2] = (tail + 1)%TX_MAX;
//memcpy(&tx_list[tail], &tx_next, sizeof(struct tx_desc));
cprintf("my message:%s, %d, %02x\n", tx_buf[tail].buffer, tx_list[tail].length, tx_list[tail].status);
return 0;
}
return -1;
}
现在可以测试数据包传输代码了。尝试通过直接从内核调用传输函数来传输几个包。您没必要创建符合任何特定网络协议的数据包来测试。运行make E1000_DEBUG=TXERR,TX qemu
来运行您的测试。你会看到:
e1000: index 0: 0x271f00 : 9000002a 0
//根据大佬的提示,直接在monitor里调用了该函数
fit_txd_for_E1000_transmit("I'm here", 10);
//我的结果是这样的,很明显length字段还有status字段是错的
e1000: index 0: 0x2ad2c0 : 9000000 1
//把transmit_packets函数改对后
e1000: index 10: 0x2b22c0 : 900000a 0
就像你传输的包。每一行给出传输数组中的索引
、该传输描述符的缓冲区地址
、cmd/CSO/length字段
和special/CSS/status字段
。
如果QEMU没有打印您希望从传输描述符中得到的值,请检查是否填充了正确的描述符,以及是否正确配置了TDBAL和TDBAH
(Transmit Descriptor Base Address)。
如果您得到“e1000: TDH wraparound @0, TDT x, TDLEN y”
消息,这意味着e1000在传输队列中一直运行,没有停止(如果QEMU不检查这个,它将进入一个无限循环),这可能意味着您没有正确地操作TDT。
如果您收到许多“e1000: tx disabled”
消息,那么您没有正确设置传输控制寄存器。
一旦QEMU运行,就可以运行 tcpdump -XXnr qemu.pcap
查看您传输的数据包数据。如果您看到了来自QEMU的预期“e1000: index”消息,但您的包捕获是空的,请再次检查是否填入了传输描述符中的每个必要字段和位(E1000可能通过了您的传输描述符,但不认为它必须发送任何东西)。
Exercise 7. 添加一个
系统调用
,让您可以从用户空间传输数据包。确切的接口由您决定。不要忘记检查
从用户空间传递到内核的任何指针
。
//inc/lib.h 要在这里声明,千万别忘了
int sys_packet_try_send(void *data_va, int len);
//kern/syscall.c
static int
sys_packet_try_send(void *addr, uint32_t len){
user_mem_assert(curenv, addr, len, PTE_U); //考虑没这么周全
return fit_txd_for_E1000_transmit(addr, len);
}
case (SYS_packet_try_send):
return sys_packet_try_send((void *)a1,a2);
Transmitting Packets: Network Server
现在您的设备驱动程序的传输端有了一个系统调用接口,是时候发送数据包了。output helper environment
的目标是在循环中执行以下操作:接受来自core network server的NSREQ_OUTPUT
IPC消息,并使用上面添加的系统调用将伴随这些IPC消息的数据包发送到网络设备驱动程序。NSREQ_OUTPUT IPC由net/lwip/jos/jif/jif.c中的low_level_output
函数发送,它将lwIP堆栈绑定到JOS的network system。每个IPC将包含一个页面,该页面由一个union Nsipc
和它的struct jif_pkt pkt字段中的包组成(参见inc/ns.h)。
struct jif_pkt {
int jp_len;
char jp_data[0]; //对哦,这里数组长度为0
};
jp_len表示数据包的长度。IPC页面上的所有后续字节都专用于包内容(就是除了jp_len外的都是data)。结构的末尾使用像jp_data这样的零长度数组
是一种常见的C技巧(有些人会说讨厌),用于表示缓冲区没有预先确定(pre-determined
)长度。由于C不做数组边界检查
,只要确保结构后面有足够的未使用内存,就可以像使用任何大小的数组一样使用jp_data(哇哦,amazing!)。
当设备驱动程序的传输队列中没有更多空间时,要注意the device driver, the output environment and the core network server之间的交互。core network server使用IPC向output environment发送数据包。如果由于send packet系统调用而导致output environment挂起
(suspended),因为驱动程序没有更多的缓冲空间来容纳新包,那么core network server将阻塞
,等待output environmnet接受IPC调用。
Exercise 8. Implement
net/output.c
.
void
output(envid_t ns_envid)
{
binaryname = "ns_output";
int r;
int perm;
// LAB 6: Your code here:
// - read a packet from the network server
// - send the packet to the device driver
envid_t from_env;
while(1){
if( ipc_recv(&from_env, &nsipcbuf, &perm) != NSREQ_OUTPUT)
continue;
while((r = sys_packet_try_send(nsipcbuf.pkt.jp_data, nsipcbuf.pkt.jp_len)<0))
sys_yield();
}
}
您可以使用net/testoutput.c来测试输出代码,而不需要涉及整个网络服务器。试着运行make E1000_DEBUG=TXERR,TX run-net_testoutput
。你应该看到:
Transmitting packet 0
e1000: index 0: 0x271f00 : 9000009 0
Transmitting packet 1
e1000: index 1: 0x2724ee : 9000009 0
...
tcpdump -XXnr qemu.pcap
应该输出
reading from file qemu.pcap, link-type EN10MB (Ethernet)
-5:00:00.600186 [|ether]
0x0000: 5061 636b 6574 2030 30 Packet.00
-5:00:00.610080 [|ether]
0x0000: 5061 636b 6574 2030 31 Packet.01
...
使用更大的包计数进行测试,请尝试E1000_DEBUG=TXERR,TX NET_CFLAGS=-DTESTOUTPUT_COUNT=100 run-net_testoutput
。如果这个溢出了您的传输循环数组,请再次检查您是否正确地处理了DD状态位,并且您已经告诉硬件设置了DD状态位(使用RS命令位)。
您的代码应该通过make grade的testoutput测试。
Question
How did you structure yourtransmit implementation
? In particular, what do you do if thetransmit ring is full
?
答:主要是fit_txd_for_E1000_transmit
函数,他把传过来的数据存到传输描述符对应的buffer里,如果传输描述符ring满了,该函数就会返回-1,那么output函数就会接收到这个返回值,不停调用sys_yield(),一直到fit_txd_for_E1000_transmit函数成功返回0。