我们将使用QEMU的用户模式网络堆栈,因为它的运行不要求任何管理权限。我们已经更新了makefile,以启用QEMU的用户模式网络堆栈和虚拟E1000网卡。
默认情况下,QEMU提供一个在IP地址 10.0.2.2上运行的虚拟路由器,并将为JOS分配IP地址10.0.2.15。 为简单起见,我们将这些默认值硬编码到net / ns.h中的网络服务器中。
尽管QEMU的虚拟网络允许JOS进行到Internet的任意连接,但JOS的10.0.2.15地址在QEMU内部运行的虚拟网络外没有任何意义(即QEMU充当NAT,NAT应该是指网络地址转换),因此我们在运行JOS时,无法直接连接到服务器,即使是在运行QEMU的主机中。
为了解决这个问题,我们将QEMU配置为在主机的某个端口上运行服务器,该服务器仅连接到JOS中的某个端口,并在真实主机和虚拟网络之间来回穿梭数据。
你会运行JOS 服务器在ports 7(echo) 和 80 (http)。可以键入make which-ports来查找QEMU转发到你的开发主机上的那些端口。为了方便,makefile也提供make nc-7和make nc-80,它们允许你跟运行在你终端的这些端口的服务器直接交互。
我们能够使用模拟硬件其实很幸运,因为E1000是运行在软件中的,模拟的E1000能够以一种用户可读的格式告诉我们它的内部状态和它遇到的所有问题,正常情况下,这对一个驱动开发者来说很奢侈。
E1000能够产生很多调试输出,所以必须启用特定的logging channels,这些channels可能比较有用:
要使用tx,txerr等日志,可以键入make E1000_DEBUG=tx,txerr命令。
您可以进一步使用软件仿真的硬件进行调试。 如果您陷入困境而又不明白为什么E1000无法按预期方式做出响应,则可以在hw / net / e1000.c中查看QEMU的E1000实现。
从头开始写一个网络堆栈是很难的,相反我们将使用lwIP,一个开源的轻量级的TCP/IP协议套件,涵盖了很多东西其中也包含了一个网络堆栈,在这个实验中,lwIP对于我们来说是个黑箱,它实现了一个BSD socket接口并具有封包输入端口和封包输出端口。网络服务器实际上由四个进程组成:
下图显示了不同的环境及其关系。 该图显示了包括设备驱动程序在内的整个系统,稍后将进行介绍。 在本实验中,您将实现以绿色突出显示的部分。
核心网络服务器环境由the socket call dispatcher和lwIP本身组成。the socket call dispatcher的工作原理与文件服务器完全相同。 用户环境使用stubs(在lib / nsipc.c中找到)将IPC消息发送到核心网络环境。 如果查看lib / nsipc.c,您会发现我们找到核心网络服务器的方式与找到文件服务器的方式相同:i386_init使用NS_TYPE_NS创建了NS环境,因此我们扫描env,寻找这种特殊的环境类型。 对于每个用户环境IPC,网络服务器中的调度程序代表用户调用lwIP提供的相应BSD套接字接口功能。
普通用户进程不能直接使用nsipc_ *类函数,相反,它们使用lib/sockets.c中的函数,这些函数提供了一个基于文件描述符的sockets API,因此用户进程通过文件描述符引用sockets,正如使用文件描述符引用硬盘中的文件一样,有些操作是socket特有的,如connect,accept等等,有些是通用的如read,write,close等等,类似于文件服务器为每个打开文件维护一个内部唯一的id一样,lwIP同样会为打开的套接字维护一个唯一的id,在文件服务器和网络服务器中,我们使用存储在struct Fd中的信息将每个进程的文件描述符映射到这些惟一的ID空间。
文件服务器和网络服务器的IPC调度程序的行为相同,但还是有一个关键区别。BSD sockets calls 比如 accept和recv 可以无限期地阻塞。如果dispatcher(调度器)让lwIP执行这些阻塞调用中的一个,调度器也会阻塞,对于整个系统,一次只能有一个未完成的网络调用。对于每个传入的IPC消息,dispatcher创建一个线程,并在新创建的线程中处理请求。如果线程阻塞,则只有该线程处于休眠状态,而其他线程继续运行。
除了核心网络进程之外,还有三个辅助进程(helper environment)。除了接受来自用户应用程序的消息外,核心网络进程的调度器还接受来自input和timer进程的消息。
当服务于用户进程sockets calls时,lwIP将生成输出包供网卡传输。lwIP将使用NSREQ_OUTPUT IPC消息将每个要传输的包发送到输出辅助进程,这个输出包会装入到IPC消息的页面参数中。输出辅助进程负责接收这些IPC消息,并通过您即将创建的系统调用接口将包转发到设备驱动程序。
网卡收到的数据包需要注入lwIP。 对于设备驱动程序收到的每个数据包,输入环境(使用您将实现的内核系统调用)将数据包拉出内核空间,然后使用NSREQ_INPUT IPC消息将数据包发送到核心服务器环境。
数据包输入功能与核心网络环境分开,因为JOS使得很难同时接受IPC消息以及轮询或等待来自设备驱动程序的数据包。 在JOS中,我们没有一个select系统调用,该调用允许环境监视多个输入源以标识准备好处理哪些输入。
如果看一下net / input.c和net / output.c,您会发现两者都需要实现。 这主要是因为实现取决于您的系统调用接口。 在实现驱动程序和系统调用接口之后,将为两个帮助程序环境编写代码。
计时器环境会定期向核心网络服务器发送NSREQ_TIMER类型的消息,以通知其计时器已过期。 lwIP使用此线程的计时器消息来实现各种网络超时。
您的内核没有时间概念,因此我们需要添加它。 当前,硬件每10毫秒产生一次时钟中断。 在每个时钟中断处,我们都可以增加一个变量以指示时间过去了10ms。 这是在kern / time.c中实现的,但尚未完全集成到您的内核中。
Exercise 1:在kern/trap.c中为每次时钟中断添加一个对time_tick的调用,实现sys_time_msec并且在kern/syscall.c中加入该系统调用,这样用户空间就能访问时间。
键入make INIT_CFLAGS=-DTEST_NO_NS run-testtime来测试你的代码. 你应当看到进程在1秒内从5开始倒数,“-DTEST_NO_NS”会禁用启动网络服务器进程,因为它会引起panic。
分成三步:
1.trap.c:
if(tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER){
lapic_eoi();
time_tick();
sched_yield();
}
2.sys_time_msec
static int sys_time_msec(void)
{
// LAB 6: Your code here.
//panic("sys_time_msec not implemented");
return time_msec();
}
3.在syscall里添加:
case SYS_time_msec: return sys_time_msec();
编写驱动程序需要深入了解硬件和提供给软件的接口。 该实验文本将提供有关如何与E1000进行交互的高级概述,但是您在编写驱动程序时需要充分利用Intel的手册。
Exercise 2:浏览E1000的英特尔软件开发人员手册。本手册涵盖了几个紧密相关的以太网控制器。 QEMU模拟82540EM。
您现在应该浏览第二章,以了解该设备。 要编写驱动程序,您需要熟悉第3章和第14章以及4.1(尽管不是4.1的小节)。 您还需要使用第13章作为参考。 其他章节主要介绍您的驱动程序无需与之交互的E1000组件。 现在不用担心细节了; 只是了解一下文档的结构,以便以后查找。
阅读手册时,请记住E1000是具有许多高级功能的复杂设备。 有效的E1000驱动程序仅需要NIC提供的部分功能和接口。 请仔细考虑与卡接口的最简单方法。 强烈建议您先使用基本驱动程序,然后再使用高级功能。
82546GB/EB的外部接口:
TBI接口:(就是一种千兆以太网的接口)
以太网控制器提供卸载IP,TCP和UDP校验和以进行传输的功能。这些功能可以通过将功能的负担从驱动程序转移到硬件上,从而大大降低处理器利用率。
在一般情况下,数据包接收包括(1)识别线路上是否存在数据包,(2)执行地址过滤,(3)将数据包存储在接收数据FIFO中,然后将数据传输到接收主机内存中的缓冲区,并(4)更新接收描述符的状态。
本节列出了所有必要的初始化,并描述了PCI / PCI-X系列千兆以太网控制器的重置命令。
上电时,硬件不会自动将以太网控制器配置为正常运行。 在继续正常操作之前,需要进行软件初始化。 通常,在软件驱动程序成功加载和设置硬件之前,以太网控制器将被视为无法正常工作。 但是,如果通过EEPROM进行配置,则自动协商可以在加电时启动或在收到PCI重置声明后开始。
(回到lab6)
E1000是一个PCI设备,这意味着它能插入到主板上的PCI总线上,PCI总线有地址,数据和中断线,并且允许cpu与PCI设备进行通信,允许PCI设备读写内存。一个PCI设备在被使用前首先要被发现和初始化,发现指等待PCI总线查找附加的PCI设备的过程,初始化是分配I/O和内存空间,也是为PCI设备分配中断请求线(IRQ LINE)的过程。
PCI是外围设备互连(Peripheral Component Interconnect)的简称,是在目前计算机系统中得到广泛应用的通用总线接口标准:
在一个PCI系统中,最多可以有256根PCI总线,一般主机上只会用到其中很少的几条。
在一根PCI总线上可以连接多个物理设备,可以是一个网卡、显卡或者声卡等,最多不超过32个。
一个PCI物理设备可以有多个功能,比如同时提供视频解析和声音解析,最多可提供8个功能。 每个功能对应1个256字节的PCI配置空间。
课程中已经在kern/pci.c中给出了PCI的相关代码,为了在boot阶段执行PCI初始化,PCI代码会先等待PCI总线查找设备,当找到一个设备时,便会读取它的供应商ID和设备ID,然后使用这两个值作为键值来搜索pci_attach_vendor数组,该数组由pci_drive结构体组成:
struct pci_driver {
uint32_t key1, key2;
int (*attachfn) (struct pci_func *pcif);
};
如果在该数组中找到了供应商id与设备id都匹配的一项,PCI代码会调用表项的attachfn函数来执行设备的初始化(设备也可以通过class来识别,这是在kern/pci.c中的另一个驱动表的作用)
attachfn会被传递一个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节Table 4-1中的几项。struct pci_func的最后三项尤其让我们感兴趣,因为它们记录了设备的内存, I/O, 和中断源等相关信息。reg_base和reg_size数组包含最多六个基址寄存器的信息。reg_base存储内存映射I/O区域的基内存地址或基I/O端口,reg_size包含对应的reg_base基址的字节大小或I/O端口数量,irq_line包含分配给设备用于中断的IRQ线。
当一个设备的attach函数被调用时,该设备已经被找到了但还不能使用,这意味着PCI代码尚未决定把资源(如地址空间,IRQ线)分配给设备,因此,pci_func结构体的最后三项也还未被填充,attach函数应该调用pci_func_enable来分配相应的资源,填充pci_func结构体,以使能该设备。
exercise 3:实现附加功能以初始化E1000。 如果找到匹配的PCI设备,则在kern / pci.c中的pci_attach_vendor数组中添加一个条目以触发功能(请确保将其放在标记表末尾的{0,0,0}条目之前)。 您可以在5.2节中找到QEMU模拟的82540EM的供应商ID和设备ID。 当启动时JOS扫描PCI总线时,您也应该看到这些内容。
现在,只需通过pci_func_enable启用E1000设备。 在整个实验中,我们将添加更多初始化。
我们为您提供了kern / e1000.c和kern / e1000.h文件,这样您就无需弄乱构建系统。 它们目前为空; 您需要为此练习填写它们。 您可能还需要在内核的其他位置包含e1000.h文件。
测试:引导内核时,应该看到它显示E1000卡的PCI功能已启用。 您的代码现在应该通过了make级的pci附加测试。
代码:
1.e1000.c文件,PCI_E1000_attach函数调用pci_func_enable函数来分配相应的资源,填充pci_func结构体,以使能该设备。
#include
#include
#include
// LAB 6: Your driver code here
// The attach function should call pci_func_enable, which will enable the device, negotiate these resources, and fill in the struct pci_func.
int PCI_E1000_attach(struct pci_func *pcif)
{
pci_func_enable(pcif);
return 0;
}
2.e1000.h文件,定义了E1000的设备号和供应商号,还有attach方法。
#ifndef JOS_KERN_E1000_H
#define JOS_KERN_E1000_H
#define E1000_DEV_ID 0x100E
#define E1000_VENDOR_ID 0x8086
#include
int PCI_E1000_attach(struct pci_func *pcif);
#endif // SOL >= 6
3.pci.c文件里用到了e1000.h里定义的数据,因此要include包含e1000.h文件。
// 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[] = {
{E1000_VENDOR_ID,E1000_DEV_ID,&PCI_E1000_attach},
{ 0, 0, 0 },
};
**软件通过内存映射IO(MMIO-统一编址)和E1000网卡进行通信。**我们已经在JOS两次见到过它了:对于CGA和LAPIC都是通过直接读写“内存”来控制和访问的。但是这些读写操作并不经过DRAM的,而是直接对设备的IO空间进行操作。
pci_func_enable为E1000网卡分配了一个MMIO区域,并且将它的基地址和大小存储在了reg_base[0]和reg_size[0]中。这是一段为设备分配的物理地址,意味着需要通过虚拟内存访问它。因为MMIO区域通常都被放在非常高的物理地址上(通常高于3GB),因此我们不能直接使用KADDR去访问它,因为JOS 256MB的内存限制。所以我们需要建立一个新的内存映射。我们将会使用高于MMIOBASE的区域(lab4中的mmio_map_region将会保证我们不会复写LAPIC的映射)。因为PCI设备的初始化发生在JOS创建用户之前,所以我们可以在内核页表中建立映射,从而保证它总是可用的。
exercise 4:在attach函数中,通过调用mmio_map_region(您在实验4中编写的以支持LAPIC的内存映射)为E1000的BAR 0创建虚拟内存映射。
你需要使用1个变量记录下该映射地址以便之后可以访问映射的寄存器。查看在kern/lapic.c中的lapic变量,效仿它的做法。假如你使用1个指针指向设备寄存器映射地址,那么你必须声明它为volatile,否则编译器会运行缓存该值和重新排序内存访问序列。
测试:为了测试你的映射,可以尝试将网卡设备状态寄存器的内容打印出来,该寄存器大小为4个字节,地址偏移量为0x08,打印值应为0x80080783,表示全双工1000MB/S。
代码:
1.e1000.c
// LAB 6: Your driver code here
volatile uint32_t* E1000;
// The attach function should call pci_func_enable, which will enable the device, negotiate these resources, and fill in the struct pci_func.
int PCI_E1000_attach(struct pci_func *pcif){
pci_func_enable(pcif);
//You'll want to record the location of this mapping in a variable
E1000 = mmio_map_region(pcif->reg_base[0],pcif->reg_size[0]);
cprintf("E1000 STATUS: %x\n", E1000[E1000_DEVICE_STATUS /4]);
return 0;
}
2.e1000.h
#define E1000_DEVICE_STATUS 0x00008 /* Device Status - RO */
我们可以想象通过读写E1000网卡的寄存器来发送和接收packet,但这实在是太慢了,而且需要E1000暂存packets。因此E1000使用Direct Access Memory(DMA)来直接从内存中读写packets而不通过CPU。驱动的作用就是负责为发送和接收队列分配内存,建立DMA描述符,以及配置E1000网卡,让它知道这些队列的位置,不过之后的所有事情都是异步。
要发送数据包,驱动程序将其复制到传输队列中的下一个DMA描述符并通知E1000另一个数据包可用,当发送数据包时,E1000又会将数据从描述符中拷贝出来,同样地,当E1000接收一个数据包时,该数据包被复制到接收队列的下一个DMA描述符,下一次时机到来时,驱动程序可以从DMA描述符中读取该包。
接收和发送队列从高层次看非常相似,都有一系列的描述符组成,尽管这些描述符的确切结构有所不同,但每个描述符都包含一些标识位和存储数据包的缓冲区的物理地址(供网卡发送的数据包或者OS分配给网卡的用于写入接收数据包的缓冲区)。
指向这些数组的指针以及描述符中的数据包缓冲区的地址都必须是物理地址,因为硬件无需通过MMU即可直接执行与物理RAM之间的DMA操作。
E1000网卡的发送和接收函数是独立的,因此我们能一次处理其中一个。我们将首先实现发送packet的操作,因为没有发送就不能接收。
首先,我们要做的是初始化网卡的发包。根据14.5章节描述的步骤,发送操作初始化的第一步就是建立发送队列,具体队列结构的描述在3.4章节,描述符的结构在3.3.3章节。我们不会使用E1000网卡的TCP offload特性,所以我们使用legacy发送描述符格式。
发送队列的结构如下图:
你会发现用C语言的struct很好描述E1000的结构,就像以前见过的trapFrame一样,C语言的struct能让你准确地在内存上布局数据,C会在结构的各个元素间插入空白用于对齐,但是对于E1000里的结构这都不是问题。例如,传统的发送描述符如下图所示:
该结构的第一个字节在最右上方,将它转换成一个C语言的struct,从右往左读,从上往下读,你会看到该结构体刚好是对齐的,不会有空白填充。
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个byte,这就表明了这些缓冲区至少要多大。更加复杂的驱动可以动态地获取数据包缓冲区(为了降低网络使用率比较低的时候带来的浪费)或者直接提供由用户空间提供的缓冲区(zero copy),不过一开始简单点总是好的。
exercise 5:根据14.5章节的描述,实现发包初始化,同时借鉴13章节(寄存器初始化)、3.3.3章节((发送描述符)和3.4章节(发送描述符数组)。
记住发送描述数组的对齐要求和数组长度的限制。TDLEN必须是128字节对齐的,每个发送描述符是16字节的,你的发送描述符数组大小需要是8的倍数。在JOS中不要超过64个描述符,以防不好测试发送环形队列溢出情况。
对于TCTL.COLD,你可以认为是全双工的。对于TIPG,要参考13.4.34章节表13-77关于IEEE802.3标准IPG的默认值描述(不要使用14.5章节的默认值)
根据14.5节的描述,可以将发包初始化的过程分为以下几步:
1.为发送描述符数组分配一块内存,内存应当按16字节对齐
2.根据分配内存的地址设置TDBAL和TDBAH寄存器,TDBAL用于32位地址,而TDBAL和TDBAH用于64位地址
3.根据发送描述符数组的大小设置TDLEN寄存器,TDLEN寄存器的数值应当是128的倍数
4.发送描述符数组头/尾寄存器(TDH/TDT)应被初始化为0
5.设置发送控制(TCTL)寄存器:
5.1 设置使能位(TCTL.EN)为1
5.2 设置TCTL.PSP位为1
5.3 将TCTL.CT设置成合适的值,以太网标准为10h,该设置仅在半双工下有意义。
5.4 设置TCTL.COLD,全双工时,设置为40h,千兆级半双工时设置为200h,10/100半双工时,设置为40h,此处可以默认为是全双工的
5.5 设置TIPG寄存器来获得最小合法包间的间距(参考13.4.34章节而非14.5章节)
1.e1000.c,初始化发送包函数,记得需要将transmitInit()加入到PCI_E1000_attach()函数中。
//发送部分的初始化主要分为三部分工作:
//1.为设备分配一个发送描述符数组和用于缓冲发送包的缓冲区数组,并将二者绑定
//2.初始化发送描述符的一些值
//3.告诉设备发送部分的相关信息,并进行配置
void transmitInit()//定义发送初始化函数
{
if(!E1000)
{
cprintf("E1000 == NULL, return\n");
return;
}
//allocate the memory
memset(transmitRing,0,sizeof(struct tx_desc)*TX_MAX_LEN);
memset(tDescBuffer,0,sizeof(struct tBuffer)*TX_MAX_LEN);
//为每个发送描述符绑定一个发送包缓冲区,注意要绑定的是其物理地址
for(int i=0;i<TX_MAX_LEN;i++)
{
transmitRing[i].addr = PADDR(tDescBuffer[i].buffer);
//设置RS位允许网卡设置DD位
transmitRing[i].cmd = (E1000_TXD_CMD_RS>>24)|(E1000_TXD_CMD_EOP>>24);
//设置DD位标识该描述符空闲可用
transmitRing[i].status = E1000_TXD_STAT_DD;
}
//设置TDBAL为发送描述符数组的物理首地址
E1000[E1000_TDBAL/4] = PADDR(&transmitRing[0]);
//because our machine is 32 bites,so the high is 0
E1000[E1000_TDBAH/4] = 0;
//设置TDLEN为描述符数组的大小
E1000[E1000_TDLEN/4] = TX_MAX_LEN*sizeof(struct tx_desc);
//发送描述符数组头/尾寄存器(TDH/TDT)应被初始化为0
E1000[E1000_TDH/4] = 0;
E1000[E1000_TDT/4] = 0;
E1000[E1000_TCTL/4] |= (E1000_TCTL_EN|E1000_TCTL_PSP); //设置TCTL寄存器的EN和PSP位
E1000[E1000_TCTL/4] &= ~E1000_TCTL_CT; //设置TCTL寄存器的CT位
E1000[E1000_TCTL/4] |= 0x10<<4;
E1000[E1000_TCTL/4] &= ~E1000_TCTL_COLD; //设置TCTL寄存器的COLD位
E1000[E1000_TCTL/4] |= 0x40<<12;
E1000[E1000_TIPG/4] |= (10)|(4<<10)|(6<<20);
}
2.e1000.h中定义各种数值
#ifndef JOS_KERN_E1000_H
#define JOS_KERN_E1000_H
#define E1000_DEV_ID 0x100E
#define E1000_VENDOR_ID 0x8086
#define E1000_DEVICE_STATUS 0x00008 /* Device Status - RO */
#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_TIDV 0x03820 /* TX Interrupt Delay Value - RW */
#define E1000_TXDCTL 0x03828 /* TX Descriptor Control - RW */
//TCTL
#define E1000_TCTL 0x00400 /* TX Control - RW */
#define E1000_TIPG 0x00410 /* TX Inter-packet gap -RW */
#define E1000_TCTL_RST 0x00000001 /* software reset */
#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_CMD_EOP 0x01000000 /* End of Packet */
#define E1000_TXD_STAT_DD 0x00000001 /* Descriptor Done */
#define TX_MAX_LEN 64
#include
//define the transmit descriptor
struct tx_desc
{
uint64_t addr;
uint16_t length;
uint8_t cso;
uint8_t cmd;
uint8_t status;
uint8_t css;
uint16_t special;
}__attribute__((packed));
struct tBuffer
{
//why is 2048?
char buffer[2048];
}__attribute__((packed));
struct tx_desc transmitRing[64];
struct tBuffer tDescBuffer[64];
#include
int PCI_E1000_attach(struct pci_func *pcif);
void transmitInit();
#endif // SOL >= 6
尝试运行make E1000_DEBUG = TXERR,TX qemu。 如果您正在使用课程qemu,则在设置TDT寄存器时应该会看到“ e1000:tx disable”消息(因为这是在设置TCTL.EN之前发生的),而没有其他“ e1000”消息。
测试:
现在传输已经初始化,接下来需要编写代码来传输数据包,并通过系统调用让用户空间能够访问它。您必须将它添加到传输队列的尾部,这意味着将发送包复制到下一个发送包缓冲区,然后更新TDT(transmit descriptor tail)寄存器,以通知网卡在传输队列中有另一个包。(注意,TDT是传输描述符数组的索引,而不是字节偏移量;) 然而,传输队列只有这么大。如果网卡落后于要发送的包,且传输队列已满,会发生什么情况?为了检测这种情况,您需要从E1000得到一些反馈。不幸的是,您不能只使用TDH(transmit descriptor head)寄存器;文档明确声明从软件中读取寄存器是不可靠的。但是,如果您在发送描述符的cmd字段中设置RS位(Report Status),那么,当网卡发送了该描述符对应的数据包之后,网卡将在描述符的status字段中设置DD位(Descriptor Done)。如果设置了描述符的DD位,您就知道可以安全地回收该描述符并使用它来传输另一个包。
如果用户呼叫您的传输系统调用,但未设置下一个描述符的DD位,表示传输队列已满怎么办? 您必须决定在这种情况下该怎么做。 您可以简单地丢弃数据包。 网络协议对此具有弹性,但是如果您丢弃大量的数据包,则该协议可能无法恢复。 相反,您可以告诉用户环境必须重试,就像对sys_ipc_try_send所做的一样。 这样做的好处是可以推迟生成数据的环境。
exercise 6:写一个函数来实现传送数据包的功能,函数思路大致如下:检查下一个描述符是否空闲,若空闲则将数据包复制到该描述符对应的数据包缓冲区内,然后更新TDT,确保记得处理发送队列为满的情况。
可以通过在内核中调用完成的发送函数来发送一些数据来测试你的发包代码是否正确,发送的数据不必遵循任何特定的网络协议,可以键入make E1000_DEBUG=TXERR,TX qemu来测试,应当会看到类似下方的输出:
e1000: index 0: 0x271f00 : 9000002a 0
…
每行给出发送数组中的索引,该发送描述符的缓冲区地址,cmd / CSO / length字段以及special / CSS / status字段。如果没有显示你希望从发送描述符中获取的值,请检查是否填写了正确的描述符,以及是否正确配置了TDBAL和TDBAH。如果收到“e1000: TDH wraparound @0, TDT x, TDLEN y”消息,则表示E1000一直贯穿传输队列而没有停止(如果QEMU没有检查,它将进入无限循环),这可能意味着您没有正确处理TDT。如果收到很多“ e1000:tx disable”消息,则说明您没有正确设置发送控制寄存器。
QEMU运行后,您可以运行tcpdump -XXnr qemu.pcap来查看传输的数据包数据。如果您看到了来自QEMU的预期“ e1000:index”消息,但是您的数据包捕获为空,请再次检查您是否填写了每个必要的字段并在传输描述符中添加了一些位(E1000可能检查了你的传输描述符,但认为它不需要发送任何东西)。
代码:
//the send data function
int e1000_transmit_data(void* addr, int length)
{
length = MIN(length,BF_MAX_SIZE);
//find the tail descripter
int tail = E1000[E1000_TDT/4];
struct tx_desc* next_desc = &transmitRing[tail];
//if the descripter can be used
if(next_desc->status&E1000_TXD_STAT_DD)
{
//copy the data to the descripter(by address)
memmove(KADDR(next_desc->addr),addr,length);
//将DD位置0,标志该描述符正在使用
next_desc->status &= ~E1000_TXD_STAT_DD;
//设置标识符的length,表示该次操作要传送多少字节
next_desc->length = (uint16_t)length;
//尾指针向后移动一位,表明有包等待传输
E1000[E1000_TDT/4] = (tail+1)%TX_MAX_LEN;
return length;
}
else
return -1;
}
exercise 7:添加一个系统调用,使您可以从用户空间传输数据包。 确切的界面由您决定。 不要忘记检查从用户空间传递到内核的任何指针。
1.lib/syscall.c
int sys_e1000_transmit(void* buf, size_t len)
{
return syscall(SYS_e1000_transmit, 0,(uint32_t)buf, len, 0,0,0);
}
2.inc/syscall.h
/* system call numbers */
enum {
SYS_cputs = 0,
SYS_cgetc,
SYS_getenvid,
SYS_env_destroy,
SYS_page_alloc,
SYS_page_map,
SYS_page_unmap,
SYS_exofork,
SYS_env_set_status,
SYS_env_set_trapframe,
SYS_env_set_pgfault_upcall,
SYS_yield,
SYS_ipc_try_send,
SYS_ipc_recv,
SYS_time_msec,
SYS__e1000_transmit,
NSYSCALLS
};
3.kern/syscall.c
static int sys_e1000_transmit(void *buf, size_t len)
{
user_mem_assert(curenv,buf,len,PTE_U|PTE_P);
return e1000_transmit_data(buf,len);
}
case SYS_e1000_transmit:
return sys_e1000_transmit((char*)(a1),a2);
4.inc/lib.h
int sys_e1000_transmit(void* buf, size_t len);
现在我们有了能够访问设备驱动发送端的系统调用了,是时候试试发包了,输出辅助进程的目标是循环做以下事情:从核心网络服务器接收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];
};
jp_len代表数据包的长度,ipc上的所有后续字节都用于数据包内容,使用长度为0的jp_data数组是一个常用的c技巧,这表示一个不预先决定长度的缓冲区。 由于C不执行数组边界检查,因此只要确保在该结构之后有足够的未使用内存,就可以使用jp_data,就好像它是任意大小的数组一样。
当设备驱动程序的传输队列中没有更多空间时,请注意设备驱动程序,输出环境和核心网络服务器之间的交互。 核心网络服务器使用IPC将数据包发送到输出环境。 如果由于发送数据包系统调用而导致输出环境暂停,因为驱动程序没有更多的缓冲区可容纳新数据包,则核心网络服务器将阻止等待输出服务器接受IPC调用。
Exercise 8 :实现 net/output.c。
代码:
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
while(1)
{
//accept NSREQ_OUTPUT IPC messages from the core network server
if(ipc_recv(NULL,&nsipcbuf,NULL) == NSREQ_OUTPUT)
{
//发包调用返回-1时,说明发送描述符队列已满,此次发送失败,则采用循环尝试发包的方法处理该情况
//send the packets accompanying these IPC message to the network device driver using the system call you added above.
while(sys_e1000_transmit(nsipcbuf.pkt.jp_data,nsipcbuf.pkt.jp_len) == -1 )
//调用sys_yield先让出处理器
sys_yield();
}
}
}