网卡驱动程序还不足以使操作系统连接到Internet。在新的lab6代码中,已经提供了一个网络堆栈和一个网络服务器。探索net目录下的内容和kern/中的新文件 。
除了编写驱动程序之外,还需要创建一个系统调用接口来访问驱动程序。你将实现缺失的网络服务器代码在网络堆栈和驱动程序之间传输数据包,还将通过完成web服务器将所有内容联系在一起。使用新的web服务器,你将能够从文件系统中服务文件。
大部分的内核设备驱动程序代码都必须自己从头编写。
我们将使用QEMU的用户模式网络堆栈,因为它不需要管理权限就可以运行。QEMU的文档中有很多关于user-net的内容。我们已经更新了makefile以启用QEMU的用户模式网络堆栈和虚拟E1000网卡。
默认情况下,QEMU提供一个运行在IP 10.0.2.2上的虚拟路由器并且分配给JOS一个10.0.2.15的IP地址。为了简单起见,我们把这些默认设置硬编码到net/ns.h中的network server.
虽然QEMU的虚拟网络允许JOS和互联网做任意的连接,但是JOS的10.0.2.15 IP地址在QEMU运行的虚拟网络之外没有任何意义(QEMU就像一个NAT),所以我们不能直接和JOS中运行的服务器连接,即便是运行QEMU的宿主机也不行。为了解决这个问题,我们通过配置QEMU,让JOS的一些端口和宿主机的某些端口相连,让服务器运行在这些端口上,从而让数据在宿主机和虚拟网络之间进行交换。
我们将在端口7(echo)和80(http)运行JOS服务器。为了避免端口冲突,makefile里实现了端口转发。可以通过运行make which-ports来找出QEMU转发的端口。为了方便起见,makefile还提供了make nc-7和make nc-80,它们允许你直接与运行在终端中的这些端口上的服务器交互(这些目标只连接到正在运行的QEMU示例;必须单独启动QEMU本身。)
makefile还配置QEMU的网络堆栈以记录QEMU的所有传入和传出数据包到lab目录中的qemu.pcap。
为了得到一个hex/ASCII dump的被捕获数据包,可以这样使用tcpdump:tcpdump -XXnr qemu.pcap。
或者,你也可以使用Wireshark图形化地检查pcap文件。Wireshark也知道如何解码和检查数以百计的网络协议。
我们很幸运能使用仿真硬件。由于E1000现在是运行在软件中的,所以模拟的E1000可以以一种用户可读的格式报告它的内部状态和它遇到的任何问题。这样是很奢侈的,所以无法提供给驱动程序开发人员编写在bare metal上。
E1000可以生成很多调试输出,因此必须启用特定的logging channels。一些有用的channel如下:
Flag | Meaning |
---|---|
tx | Log packet transmit operations |
txerr | Log transmit ring errors |
rx | Log changes to RCTL |
rxfilter | Log filtering of incoming packets |
rxerr | Log receive ring errors |
unknown | Log reads and writes of unknown registers |
eeprom | Log reads from the EEPROM |
interrupt | Log interrupts and changes to interrupt registers. |
举个例子,要启用“tx”和“txerr”日志记录,请使用make E1000_DEBUG=tx,txerr.
注:E1000_DEBUG标志仅在QEMU的6.828版本中有效。
您可以进一步使用软件仿真硬件进行调试。如果您遇到了困难,并且不理解E1000为什么没有按照预期的方式响应,那么可以查看hw/net/e1000.c中的QEMU E1000实现。
从头开始编写网络堆栈是一项艰苦的工作。相反,我们将使用lwIP,这是一个开源的轻量级TCP/IP协议套件,其中包括一个网络栈。在这个任务中,就我们而言,lwIP是一个实现BSD套接字接口的黑盒,它有一个包输入端口和一个包输出端口。
网络服务器实际上由四个环境组成:
下图显示了这些环境及其关系。该图显示了包括设备驱动程序在内的整个系统,稍后将对此进行介绍。在这个lab中,你将实现用绿色突出显示的部分。
核心网络服务器环境(core network server environment)由socket call dispatcher和lwIP本身组成。socket call dispatcher的工作原理与文件服务器完全相同。用户环境使用stubs(在lib/nsipc.c中)向核心网络环境发送IPC消息。如果查看lib/nsipc.c,就会发现我们找到核心网络服务器的方式与找到文件服务器的方式相同:i386_init用NS_TYPE_NS创建NS(network server)环境,因此我们扫描envs,寻找这种特殊类型的环境。对于每个用户环境IPC,网络服务器中的dispatcher代表用户调用由lwIP提供的适当的BSD套接字接口函数。
常规用户环境不直接使用nsipc_*调用。相反,它们使用lib/socket中的函数。它提供了一个基于文件描述符的套接字API。因此用户环境通过文件描述符引用套接字,就像它们引用磁盘文件一样。许多操作(connect,accept等)是特定于套接字的,但是读、写和关闭操作要通过lib/fd.c中的常规文件描述符device-dispatch code。就像文件服务器为所有打开的文件维护内部唯一ID一样,lwIP也为所有打开的套接字生成唯一ID。在文件服务器和网络服务器中,我们使用存储在struct Fd中的信息将每个环境的文件描述符映射到这些唯一的ID空间。
尽管文件服务器和网络服务器的IPC调度程序看起来是相同的,但有一个关键的区别。BSD套接字调用(如accept和recv)可以无限期阻塞。如果调度程序让lwIP执行这些阻塞调用中的一个,那么该调度程序也会阻塞,并且对于在整个系统,每次只能有一个未完成的网络调用。因为这是不可接受的,所以网络服务器使用用户级线程来避免阻塞整个服务器环境。对于每个传入的IPC消息,dispatcher将创建一个线程,并在新创建的线程中处理请求。如果线程阻塞,那么只有该线程被置于睡眠状态,其他线程继续运行。
除了核心网络环境外,还有三个helper environments。除了接受来自用户应用程序的消息外,核心网络环境的dispatcher还接受来自input和timer环境的消息。
当为用户环境套接字调用提供服务时,lwIP将为网卡生成要传输的数据包。lwIP将使用NSREQ_OUTPUT IPC消息将每个包发送到output helper environment,该包附加在IPC消息的页面参数中。输出环境负责接收这些消息,并通过即将创建的系统调用接口将数据包转发到设备驱动程序。
网卡接收到的数据包需要注入lwIP。对于设备驱动程序接收到的每个包,输入环境将该包从内核空间中取出(使用即将实现的内核系统调用),并使用NSREQ_INPUT IPC消息将该包发送到核心服务器环境。
数据包输入功能与核心网络环境分离,因为JOS使同时接收IPC消息和轮询或等待来自设备驱动程序的数据包变得困难。我们在JOS中没有一个select系统调用,这个调用允许环境监视多个输入源,以确定哪些输入已准备好被处理。
如果查看一下net/input.c和net/output.c,就会发现两者都需要实现。这主要是因为实现取决于你的系统调用接口。在实现驱动程序和系统调用接口之后,你将为这两个辅助环境编写代码。
The timer environment定期向核心服务器发送NSREQ_TIMER类型的消息,通知它一个定时器已经过期。lwIP使用来自这个线程的计时器消息来实现各种网络超时。
内核没有时间概念,所以我们需要添加它。目前,硬件每10ms就会产生一个时钟中断。在每一个时钟中断,我们可以增加一个变量来表示时间已经前进了10ms。这是在kern/time.c中实现的,但是还没完全集成到内核中。
Exercise 1
为kern/trap.c中的每个时钟中断添加对time_tick的调用,实现sys_time_msec并将其添加到kern/syscall.c中的syscall中,以便用户空间能够访问时间。
在lab4中也对时钟中断进行过dispatch,要记得把它注释掉(我就是没看见所以出了bug...)
//kern/trap.c:trap_dispatch()
if(tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER){
lapic_eoi();
time_tick();
sched_yield();
return;
}
//kern/syscall.c
static int
sys_time_msec(void)
{
// LAB 6: Your code here.
//panic("sys_time_msec not implemented");
return time_msec();
}
case SYS_time_msec:
return sys_time_msec();
使用make INIT_CFLAGS=-DTEST_NO_NS run-testtime测试代码。
编写一个驱动程序需要深入了解硬件和提供给软件的接口。实验文档将提供如何与E1000接口的高级概述,但在编写驱动程序时,你需要广泛使用Intel的手册。
Exercise 2
浏览E1000 Software Developer's Manual。这个手册涵盖了几个密切相关的以太网控制器。QEMU模拟82540EM。
现在你应该浏览第二章来了解一下这个设备。要编写你的驱动程序,还需要熟悉第三章和第14章,以及4.1章。你还需要用第13章作为参考。其他章节主要介绍了E1000中驱动程序不需要与之交互的组件。现在不需要担心细节,只需了解一下文档的结构,以便稍后查找。
在阅读手册时,请记住E1000是一个拥有许多高级功能的复杂设备。一个工作的E1000驱动程序只需要NIC提供的功能和接口的一小部分。仔细考虑最简单的方式与卡接口。我们强烈建议您在使用高级特性之前先使用基本驱动程序。
E1000是一个PCI设备,这意味着它插入到主板上的PCI总线。PCI总线有地址,数据和中断线,并允许中央处理器与PCI设备通信,也允许PCI设备读和写内存。在使用PCI设备之前,需要发现并初始化它。发现是遍历PCI总线寻找附加设备的过程。初始化是分配I/O和内存空间的过程,以及为使用的设备协商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);
};
如果被发现的设备的vendor ID和device ID匹配数组中的一个项,则PCI代码调用该项中的attachfn来执行设备初始化(设备也可以通过类来识别,这就是kern/pci.c中的另一个驱动表的作用)。
attach函数被传递一个PCI函数来初始化。PCI卡可以公开多个函数,但E1000只能公开一种函数。以下是我们如何在JOS中表示PCI函数:
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;
};
上面的结构反映了开发人员手册4.1节的表4-1中的一些项。struct pci_func的最后三个项对我们特别有意义,因为它们记录了设备的协商内存、I/O和中断资源。reg_base和reg_size数组最多包含6个基址寄存器或BARS的信息。reg_base存储内存映射I/O区域的基本内存地址(或I/O端口资源的基本I/O端口),reg_size包含reg_base中对应基值的I/O端口的字节大小或数量,irq_line包含为中断分配给设备IRQ line。E1000条的具体含义见表4-2的后半部分。
当设备的attach函数被调用时,该设备已经找到,但还没有启用。这意味着PCI代码还没有决定分配给设备的资源,如地址空间和一个IRQ线,并且,因此,struct pci_func结构的最后三个元素还没有被填充。attach函数应该调用pci_func_enable,它将启用设备,协商这些资源并填充struct pci_func。
Execise 3. 实现attach函数来初始化E1000。在kern/pci.c中的pci_attach_vendor数组中添加一个项,以便在找到匹配的PCI设备时触发函数(确保将其放在标记表结束的{0,0,0}项之前)。你可以在5.2节中找到QEMU模拟的82540EM的ventor ID和device ID。当JOS在引导时扫描PCI总线时,你应该可以看到这些内容。
目前,只需通过pci_func_enable启用E1000设备。我们将在lab中添加更多的初始化。
我们已经提供了kern/e1000.c和kern/e1000.h文件,这样就不需要干扰构建系统。它们目前是空白的,在这个练习中,你需要把它们填满,你可能还需要把e100.h文件包含在内核的其他位置。
当引导内核时,你应该看到它打印E1000卡的PCI函数已启用。你的代码现在应该通过pci attach测试。
关于PCI,参考了https://blog.csdn.net/bysui/article/details/75088596这篇博客。
PCI是外围设备互连(Peripheral Component Interconnect)的简称,是在目前计算机系统中得到广泛应用的通用总线接口标准:
- 在一个PCI系统中,最多可以有256根PCI总线,一般主机上只会用到其中很少的几条。
- 在一根PCI总线上可以连接多个物理设备,可以是一个网卡、显卡或者声卡等,最多不超过32个。
- 一个PCI物理设备可以有多个功能,比如同时提供视频解析和声音解析,最多可提供8个功能。
每个功能对应1个256字节的PCI配置空间。
另外参考手册中的4-1,可以获知
00h-3Ch这64个字节是标准化的,提供了厂商号,设备号、版本号等信息,唯一标示1个PCI设备,同时提供最多6个的IO地址区域。
然后,在5.2节中找到了82540EM的VENDOR_ID和DEVICE_ID。使用的是Desktop的ID。
PCI是如何进行初始化的呢?可以查看kern/pci.c。在kern/init.c中进行系统初始化时,会调用pci_init进行设备初始化。在pci_init函数中,调用pci_scan_bus顺次查找0号总线上的32个设备,查找到该设备之后,顺次扫描这个设备每个功能对应的配置地址空间,将一些关键的控制参数读入到pci_func中进行保存。
得到pci_func函数后,将其传入到pci_e1000_attach函数对设备进行初始化。
完成代码如下
//kern/e1000.c
int
pci_e1000_attach(struct pci_func *pcif){
pci_func_enable(pcif);
return 1;
}
//kern/e1000.h
#ifndef JOS_KERN_E1000_H
#define JOS_KERN_E1000_H
#include
int pci_e1000_attach(struct pci_func *pcif);
#endif // SOL >= 6
//kern/pci.c
struct pci_driver pci_attach_vendor[] = {
{PCI_VENDOR_ID, PCI_DEVICE_ID, &pci_e1000_attach},
{ 0, 0, 0 },
};
//kern/pcireg.h
#define PCI_VENDOR_ID 0x8086
#define PCI_DEVICE_ID 0x100E
结果
软件通过内存映射I/O(MMIO)与E1000通信。在JOS中已经见过两次:CGA控制台和LAPIC都是通过写入和读取内存来控制和查询设备。但是这些读和写不会进入DRAM;它们直接进入这些设备。
pci_func_enable与E1000协商MMIO区域,并将其base和size存储在BAR 0中(即reg_base[0]和reg_size[0])。这是一个分配给设备的物理内存地址范围,这意味着必须做一些事情来通过虚拟地址访问它由于MMIO区域被分配了非常高的物理地址(通常超过3GB),由于JOS的256MB限制,所以不能使用KADDR访问它。因此必须创建一个新的内存映射。我们将使用MMIOBASE上面的区域(lab4中的mmio_map_region将确保我们不会覆盖LAPIC使用的映射)。由于PCI设备初始化发生在JOS创建用户环境之前,所以你可以在kern/pgdir中创建映射,并且映射始终可用。
Exercise 4
在attach函数中,通过调用mmio_map_region(在lab4中编写以支持LAPIC的内存映射)为E1000的BAR 0创建虚拟内存映射。
你将希望在一个变量中记录此映射的位置,以便稍后访问刚刚映射的寄存器。查看kern/lapic变量,以获得实现此目的的一个示例。如果你确实使用一个指向设备寄存器映射的指针,一定要声明它为volatile;否则,允许编译器缓存值并对该内存的访问重排序。
要测试映射,请尝试打印设备状态寄存器(13.4.2节)。这是一个4字节的寄存器,从寄存器空间的第8字节开始,应该得到0x80080783,这表明一个全双工链路的速度达到了1000MB/s。
提示:你将需要很多常量,比如寄存器的位置和位掩码的值。试图从开发人员手册中复制这些代码是很容易出错的,而且错误可能会导致痛苦的调试会话。建议使用QEMU的e1000_hw.h作为指导原则。不建议逐字复制它,因为它定义的内容远远超过实际需要,而且可能不能按照你需要的方式定义内容,但这是一个很好的起点。
//kern/e1000.c
volatile uint32_t *e1000;
// LAB 6: Your driver code here
int
pci_e1000_attach(struct pci_func *pcif){
pci_func_enable(pcif);
e1000 = mmio_map_region(pcif->reg_base[0],pcif->reg_size[0]);
e1000_transmit_init();
cprintf("E1000 status register: %08x\n",*(e1000+E1000_LOCATE(E1000_STATUS)));
return 0;
}
//kern/e1000.h
#define E1000_STATUS 0x00008 /*Device Status - RW */
#define E1000_LOCATE(offset) (offset>>2)
你可以想象通过写入和读取E1000的寄存器来传输和接受数据包,但这样会很慢,并且需要E1000在内部缓冲数据包数据。相反,E1000使用DMA(Direct Memory Access)直接从内存读写数据包数据,而不涉及CPU。驱动程序负责为传输和接收队列分配内存,设置DMA描述符,并使用这些队列的位置配置E1000,但之后的一切都是异步的。为了传输一个包,驱动程序将它复制到传输队列的下一个DMA描述符,并通知E1000另一个包可用;当有时间发送数据包时,E1000将从描述符中复制数据。同样地,当E1000接收到一个数据包时,它将其复制到接收队列中的下一个DMA描述符中,驱动程序可以在下一个机会中读取该描述符。
接受和传输队列在高级别上非常相似。两者都由一系列描述符组成。虽然这些描述符的确结构各不相同,但每个描述符都包含一些标志和包含包数据的缓冲区的物理地址(要么是card要发送的包数据,要么是操作系统为card分配的缓冲区,以便card将接受的包写入其中)。
队列被实现为循环数组,这意味着当card或驱动程序到达数组的末尾时,它将绕回开始。两者都有一个头指针和一个尾指针,队列的内容是这两个指针之间的描述符。硬件重视消耗头部的描述符并移动头部指针,而驱动程序总是向尾部添加描述符并移动尾部指针。传输队列中的描述符表示等待发送的数据包(因此,在稳定状态下,传输队列为空)。对于接收队列,队列中的描述符是card可以接受数据包的自由描述符(因此,在稳定状态下,接收队列由所有可用的接受描述符组成)。正确地更新尾寄存器而不混淆E1000是很棘手的。
这些数组的指针以及描述符中的包缓冲区的地址都必须是物理地址,因为硬件直接从物理RAM执行DMA,而不经过MMU。
E1000的发射和接收功能基本上是相互独立的,所以我们可以一次处理一个。我们将首先实现发送数据包。因为我们无法在不先发送“I am here”数据包的情况下测试接收。
首先,你必须初始化card来传输。按照14.5节中描述的步骤。传输初始化的第一步是设置传输队列,第3.4节中描述了队列的精确结构,第3.3.3节描述了描述符的结构。我们不会使用E1000的TCP卸载特性,所以你可以关注“legacy transmit descriptor format”。你现在应该阅读这些章节,熟悉这些结构。
你会发现使用C结构来描述E1000的结构很方便。正如你在struct Trapframe结构中看到的那样,C结构允许你在内存中精确地布局数据。C可以在字段之间插入填充,但E1000的结构布局使这不会成为问题。如果遇到字段对齐问题,请查看GCC的“packed”属性。
一个例子,考虑手册表3-8中给出的legacy transmit descriptor format,并在这里描述:
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;
uint16_t length;
uint8_t cso;
uint8_t cmd;
uint8_t status;
uint8_t css;
uint16_t special;
};
你的驱动程序将不得不为传输描述符数组和传输描述符所指向的数据包缓冲区保留内存。有几种方法可以做到这一点,从动态分配页面到简单地在全局变量中声明页面。无论选择什么,请记住E1000直接访问物理内存,这意味着它访问的任何缓冲区必须在物理内存中相邻。
也有多种方法来处理数据包缓冲区。最简单的方法是在驱动程序初始化期间为每个描述符保留数据包缓冲区的空间,并简单地将数据包数据复制到这些预先分配的缓冲区中。以太网数据包的最大大小为1518字节,这限制了这些缓冲区的大小。更复杂的驱动程序可以动态地分配包缓冲区(例如,在网络使用率较低时减少内存开销),甚至可以直接通过用户空间提供的缓冲区(一种称为“零拷贝”的技术)。
Exercise 5
执行14.5节中描述的初始化步骤(不包括它的字节)。使用第13节作为初始化过程所引用的寄存器的参考,使用第3.3.3节和第3.4节作为传输描述符和传输描述符数组的参考。
请注意传输描述符阵列的对齐要求和该阵列的长度限制。因为TDLEN必须是128字节对齐的,并且每个传输符是16字节,所以传输描述符数组将需要8个传输描述符的若干倍。但是不要使用超过64个描述符,否则我们的测试将法务测试传输环溢出。
对于TCTL.COLD,你可以承担全双工操作。对于TIPG,请参考IEEE 802.3标准IPG第13.4.34节表13-77中描述的默认值(不要使用第14.5节表中的值)。
尝试run make E1000_DEBUG=TXERR,TX qemu。如果您正在使用course qemu,那么在设置TDT寄存器时(因为这发生在设置TCTL.EN之前)应该会看到“e1000: tx disabled”消息,而不会看到更多的“e1000”消息。
查看开发手册14.5章节关于发送初始化的描述,步骤如下:
1. 为发送描述符队列分配一块连续空间,设置TDBAL和TDBAH寄存器的值指向起始地址,起始地址要16字节对齐。其中TDBAL为32位地址,TDBAL和TDBAH表示64位地址。
2. 设置TDLEN寄存器的值为描述符队列的大小,128字节对齐。
3. 设置发送队列的头指针(TDH)和尾指针(TDT)寄存器的值为0。
4. 初始化发送控制TCTL寄存器的值,包括设置Enable位为1(TCTL.EN)、TCTL.PSP位为1、TCTL.CT位为10h、TCTL.COLD位为40h。
5. 设置TIPG寄存器为期望值。
接收描述符包含了接收数据缓冲区地址和硬件用于存储数据包信息的字段。阴影区域表示该字段在数据包接收时由硬件修改。
通过查找手册和参考e1000_hw.h,初始化Transmit相关寄存器。
//kern/e1000.h
#define E1000_CTRL 0x00000 /* Device Control - RW */
#define E1000_CTRL_DUP 0x00004 /* Device Control Duplicate (Shadow) - RW */
#define E1000_RCTL 0x00100 /* RX Control - RW*/
#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 */
/* Transmit Control */
#define E1000_TCTL_RST 0x00000001 /* Reserved */
#define E1000_TCTL_EN 0x00000002 /* enable tx */
#define E1000_TCTL_BCE 0x00000004 /* Reserved */
#define E1000_TCTL_PSP 0x00000008 /* pad short packets */
#define E1000_TCTL_CT 0x00000ff0 /* collision threshold */
#define E1000_TCTL_COLD 0x003ff000 /* collision distance */
#define E1000_TCTL_SWXOFF 0x00400000 /* SW Xoff transmission */
#define E1000_TCTL_PBE 0x00800000 /* Reserved */
#define E1000_TCTL_RTLC 0x01000000 /* Re-transmit on late collision */
#define E1000_TCTL_NRTU 0x02000000 /* No Re-transmit on underrun */
#define E1000_TCTL_MULR 0x10000000 /* Reserved */
#define E1000_TXD_CMD_EOP 0x01 /* End of Packet */
#define E1000_TXD_CMD_RS 0x08 /* Report Status */
#define E1000_TXD_STAT_DD 0x00000001 /* Descriptor Done */
#define E1000_TXD_STAT_EC 0x00000002 /* Excess Collisions */
#define E1000_TXD_STAT_LC 0x00000004 /* Late Collisions */
#define E1000_TXD_STAT_TU 0x00000008 /* Transmit underrun */
#define E1000_TXD_STAT_TC 0x00000004 /* Tx Underrun */
#define TX_LEN 64
#define TX_BUFF_SIZE 2048
struct tx_desc tx_list[TX_LEN]__attribute__((aligned(PGSIZE)));
void e1000_transmit_init(){
int i;
memset(tx_list,0,sizeof(struct tx_desc)*TX_LEN);
for(i=0;i
现在已经初始化了传输,您将必须编写代码以传输数据包,并使其可以通过系统调用在用户空间访问。要传输数据包,您必须将其添加到传输队列的尾部,这意味着将数据包数据复制到下一个数据包缓冲区,然后更新TDT(传输描述符尾部),告知card传输队列中存在另一个数据包(注意TDT是传输描述符数组的索引,而不是字节偏移量)
但是,传输队列只有这么大,如果card落后于传输数据包并且传输队列已满怎么办?为了检测到这种情况,需要E1000的一些反馈。不幸的是,您不能只使用TDH(发送描述符头)寄存器。该文档明确指出,从软件读取该寄存器是不可靠的。但是如果你在发送描述符的命令字段中设置了RS位,则当card在描述符中传输了数据包时,card将在描述符的状态字段中将DD位置为1。如果描述符的DD位置1,则可以安全地回收该描述符并使用它传输另一个数据包。
如果用户调用您的传输系统调用,但未设置下一个描述符的DD位,表示传输队列已满怎么办?你必须决定在这种情况下该怎么做,你可以简单地丢弃数据包。网络协议对此具有一定的弹性,但是如果丢弃大量的数据包,则该协议可能无法恢复。你可以改为告诉用户环境必须重试,就像对sys_ipc_try_send所做的一样。这样做的好处是可以推迟生成数据的环境。
Exercise 6
通过检查下一个描述符是否空闲,将包数据复制到下一个描述符并更新TDT,编写一个函数来发送数据包,确保处理了传输队列已满这种情况。
现在是测试您的数据包传输代码的好时机。通过直接从内核调用传输函数,尝试传输仅几个数据包。您无需创建符合任何特定网络协议的数据包即可对其进行测试。运行make E1000_DEBUG=TXERR,TX qemu以运行您的测试。你应该看到类似
e1000: index 0: 0x271f00 : 9000002a 0
//kern/e1000.c
int
handle_e1000_transmit(void *addr, size_t len){
//获取尾指针索引
int tdt = e1000[E1000_LOCATE(E1000_TDT)];
struct tx_desc *next_tx = &tx_list[tdt];
//如果传输长度大于packets,丢弃多余部分
if(len > sizeof(struct packets))
len = sizeof(struct packets);
//判断DD位是否置1
if((next_tx->status & E1000_TXD_STAT_DD) == 0)
return -1;
//把包数据传输给描述符
memmove(&tx_buf[tdt],addr,len);
next_tx->length = len;
//DD位置0
next_tx->status &= (~E1000_TXD_STAT_DD);
//尾指针+1
e1000[E1000_LOCATE(E1000_TDT)] = (tdt+1) % TX_LEN;
return 0;
}
在内核中添加调用。记得添加头文件,并且在kern/e1000.h中添加函数定义。
//kern/monitor.c:monitor
handle_e1000_transmit("I am here",10);
make qemu
tcpdump -XXnr qemu.pcap
Exercise 7
添加一个系统调用,使您可以从用户空间传输数据包。确切的界面有您决定。不要忘记检查从用户空间传递到内核的任何指针。
一定要记得注册系统调用!!!还有在一些c文件里添加头文件。
//kern/syscall.c
static int
sys_packet_try_send(void *buf_addr, size_t len){
user_mem_assert(curenv, buf_addr, len, PTE_U);
return handle_e1000_transmit(buf_addr, len);
}
case SYS_packet_try_send:
return sys_packet_try_send((void *)a1,a2);
//inc/lib.h
int sys_packet_try_send(void *buf_addr, size_t len);
//lib/syscall.c
int
sys_packet_try_send(void *buf_addr, size_t len){
return syscall(SYS_packet_try_send,1,(uint32_t) buf_addr,len,0,0,0);
}
别忘记在kern/syscall.h中添加SYS_packet_try_send这个系统调用号。
现在,您已经具有到设备驱动程序的发送端的系统调用接口,是时候发送数据包了。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就像这样:
struct jif_pkt {
int jp_len;
char jp_data[0];
};
jp_len表示packet的长度。IPC页面上的所有后续字节专用于数据包内容。jp_data在结构的末尾使用零长度数组是一种常见的C技巧,用于表示没有预定长度的缓冲区。由于C不会进行数组边界检查(?),因此只要确保该结构后面有足够的未使用的内存,就可以将其jp_data当做任何大小的数组使用。
当设备驱动程序的传输队列中没有更多空间时,请注意设备驱动程序,the output environment和core network server之间的交互。core network server使用IPC将数据包发送到output environment。如果由于发送数据包系统调用而导致output environment环境挂起,因为驱动程序没有更多的缓冲区可容纳新数据包,则核心网络服务器将阻塞,等待输出服务器接受IPC调用。
Exercise 8
实现net/output.c.
你可以使用user/testoutput.c测试你的output代码 ,无需涉及整个网络服务器。尝试run 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
#include "ns.h"
extern union Nsipc nsipcbuf;
void
output(envid_t ns_envid)
{
binaryname = "ns_output";
// LAB 6: Your code here:
// - read a packet from the network server
// - send the packet to the device driver
int perm;
envid_t envid;
while(1){
if(ipc_recv(&envid, &nsipcbuf, &perm)!= NSREQ_OUTPUT)
continue;
while(sys_packet_try_send(nsipcbuf.pkt.jp_data, nsipcbuf.pkt.jp_len)<0)
sys_yield();
}
}
make E1000_DEBUG=TXERR,TX NET_CFLAGS=-DTESTOUTPUT_COUNT=100 run-net_testoutput
问题
1. 您如何构造传输实现,如果发送队列已满该怎么办?
当发送队列满时,output environment就会重发,但是会sleep一段时间。如果由于发送数据包系统调用而导致output environment环境挂起,因为驱动程序没有更多的缓冲区可容纳新数据包,则核心网络服务器将阻塞,等待输出服务器接受IPC调用