Caffeinated 6.828:实验 6:网络驱动程序
Caffeinated 6.828:利用的东西Caffeinated 6.828:尝试东西指南 Caffeinated 6.828:尝试 1:PC 的引导过程Caffeinated 6.828:尝试 2:内存办理 Caffeinated 6.828:尝试 3:用户情况 Caffeinated 6.828:尝试 4:抢占式多使命处置 Caffeinated 6.828:尝试 5:文件系统、Spawn 和 Shell Caffeinated 6.828:尝试 6:收集驱动法式 Caffeinated 6.828:尝试 7:最末的 JOS 项目 简介
那个尝试是默认你可以本身完成的最末项目。
如今你已经有了一个文件系统,一个典型的操做系统都应该有一个收集栈。在本尝试中,你将继续为一个网卡去写一个驱动法式。那个网卡基于 Intel 82540EM 芯片,也就是寡所周知的 E1000 芯片。
预备常识利用 Git 去提交你的尝试 5 的源代码(若是还没有提交的话),获取课程仓库的最新版本,然后创建一个名为 lab6 的当地分收,它跟踪我们的长途分收 origin/lab6:
athena% cd ~/6.828/lab athena% add git athena% git commit -am my solution to lab5 nothing to commit (working directory clean) athena% git pull Already up-to-date. athena% git checkout -b lab6 origin/lab6 Branch lab6 set up to track remote branch refs/remotes/origin/lab6. Switched to a new branch "lab6" athena% git merge lab5 Merge made by recursive. fs/fs.c | 42 +++++++++++++++++++ 1 files changed, 42 insertions(+), 0 deletions(-) athena%然后,仅有网卡驱动法式其实不可以让你的操做系统接入互联网。在新的尝试 6 的代码中,我们为你供给了收集栈和一个收集办事器。与以前的尝试一样,利用 git 去拉取那个尝试的代码,合并到你本身的代码中,并去阅读新的 net/ 目次中的内容,以及在 kern/ 中的新文件。
除了写那个驱动法式以外,你还需要去创建一个拜候你的驱动法式的系统挪用。你将要去实现那些在收集办事器中缺失的代码,以便于在收集栈和你的驱动法式之间传输包。你还需要通过完成一个 web 办事器来将所有的工具毗连到一路。你的新 web 办事器还需要你的文件系统来供给所需要的文件。
大部门的内核设备驱动法式代码都需要你本身去从头起头编写。本尝试供给的指点比起前面的尝试要少一些:没有框架文件、没有现成的系统挪用接口、而且良多设想都由你本身决定。因而,我们建议你在起头任何零丁操练之前,阅读全数的编写使命。许多学生都反响那个尝试比前面的尝试都难,因而请按照你的现实情况方案你的时间。
尝试要求与以前一样,你需要做尝试中全数的常规操练和至少一个挑战问题。在尝试中写出你的详细谜底,并将挑战问题的计划描述写入到 answers-lab6.txt 文件中。
QEMU 的虚拟收集我们将利用 QEMU 的用户形式收集栈,因为它不需要以办理员权限运行。QEMU 的文档的那里有更多关于用户收集的内容。我们更新后的 makefile 启用了 QEMU 的用户形式收集栈和虚拟的 E1000 网卡。
缺省情况下,QEMU 供给一个运行在 IP 地址 10.2.2.2 上的虚拟路由器,它给 JOS 分配的 IP 地址是 10.0.2.15。为了简单起见,我们在 net/ns.h 中将那些缺省值硬编码到收集办事器上。
固然 QEMU 的虚拟收集允许 JOS 随意毗连互联网,但 JOS 的 10.0.2.15 的地址其实不能在 QEMU 中的虚拟收集之外利用(也就是说,QEMU 还得做一个 NAT),因而我们其实不能间接毗连到 JOS 上运行的办事器,即使是从运行 QEMU 的主机上毗连也不可。为处理那个问题,我们设置装备摆设 QEMU 在主机的某些端口上运行一个办事器,那个办事器简单地毗连到 JOS 中的一些端口上,并在你的实在主机和虚拟收集之间传递数据。
你将在端口 7(echo)和端口 80(http)上运行 JOS,为制止在共享的 Athena 机器上发作抵触,makefile 将为那些端口基于你的用户 ID 来生成转发端口。你能够运行 make which-ports 去找出是哪个 QEMU 端口转发到你的开发主机上。为便利起见,makefile 也供给 make nc-7 和 make nc-80,它允许你在末端上间接与运行那些端口的办事器去交互。(那些目的仅能毗连到一个运行中的 QEMU 实例上;你必需别离去启动它本身的 QEMU)
包查抄makefile 也能够设置装备摆设 QEMU 的收集栈去记录所有的入站和出站数据包,并将它保留到你的尝试目次中的 qemu.pcap 文件中。
利用 tcpdump 号令去获取一个捕捉的 hex/ASCII 包转储:
tcpdump -XXnr qemu.pcap或者,你能够利用 Wireshark 以图形化界面去查抄 pcap 文件。Wireshark 也晓得若何去解码和查抄成百上千的收集协议。若是你在 Athena 上,你能够利用 Wireshark 的前辈:ethereal,它运行在加锁的保密互联网协议收集中。
调试 E1000我们十分幸运可以去利用仿实硬件。因为 E1000 是在软件中运行的,仿实的 E1000 可以给我们供给一小我类可读格局的陈述、它的内部形态以及它碰到的任何问题。凡是情况下,对祼机上做驱动法式开发的人来说,那长短常难能宝贵的。
E1000 可以产生一些调试输出,因而你能够去翻开一个专门的日记通道。此中一些对你有用的通道如下:
标记含义tx包发送日记txerr包发送错误日记rx到 RCTL 的日记通道rxfilter入站包过滤日记rxerr领受错误日记unknown未知存放器的读写日记eeprom读取 EEPROM 的日记interrupt中断和中断存放器变动日记
例如,你能够利用 make E1000_DEBUG=tx,txerr 去翻开 “tx” 和 “txerr” 日记功用。
留意:E1000_DEBUG 标记仅能在打了 6.828 补钉的 QEMU 版本上工做。
你能够利用软件去仿实硬件,来做进一步的调试工做。若是你利用它时卡壳了,不大白为什么 E1000 没有如你预期那样响应你,你能够查看在 hw/e1000.c 中的 QEMU 的 E1000 实现。
收集办事器从头起头写一个收集栈是很困难的。因而我们将利用 lwIP,它是一个开源的、轻量级 TCP/IP 协议套件,它能做包罗一个收集栈在内的良多工作。你能在 那里 找到良多关于 lwIP 的信息。在那个使命中,对我们而言,lwIP 就是一个实现了一个 BSD 套接字接口和拥有一个包输入端口和包输出端口的黑盒子。
一个收集办事器其实就是一个有以下四个情况的混合体:
核心收集办事器情况(包罗套接字挪用派发器和 lwIP)输入情况输出情况按时器情况下图展现了各个情况和它们之间的关系。下图展现了包罗设备驱动的整个系统,我们将在后面详细讲到它。在本尝试中,你将去实现图中绿色高亮的部门。
Network server architecture
核心收集办事器情况核心收集办事器情况由套接字挪用派发器和 lwIP 本身构成的。套接字挪用派发器就像一个文件办事器一样。用户情况利用 stubs(能够在 lib/nsipc.c 中找到它)去发送 IPC 动静到核心收集办事器情况。若是你看了 lib/nsipc.c,你就会发现核心收集办事器与我们创建的文件办事器 i386_init 的工做体例是一样的,i386_init 是利用 NSTYPENS 创建的 NS 情况,因而我们查抄 envs,去查找那个特殊的情况类型。关于每个用户情况的 IPC,收集办事器中的派发器将挪用响应的、由 lwIP 供给的、代表用户的 BSD 套接字接口函数。
通俗用户情况不克不及间接利用 nsipc_* 挪用。而是通过在 lib/sockets.c 中的函数来利用它们,那些函数供给了基于文件描述符的套接字 API。以那种体例,用户情况通过文件描述符来引用套接字,就像它们引用磁盘上的文件一样。一些操做(connect、accept 等等)是特定于套接字的,但 read、write和 close 是通过 lib/fd.c 中一般的文件描述符设备派发代码的。就像文件办事器对所有的翻开的文件维护独一的内部 ID 一样,lwIP 也为所有的翻开的套接字生成独一的 ID。不管是文件办事器仍是收集办事器,我们都利用存储在 struct Fd 中的信息去映射每个情况的文件描述符到那些独一的 ID 空间上。
虽然看起来文件办事器的收集办事器的 IPC 派发器行为是一样的,但它们之间还有很重要的不同。BSD 套接字挪用(像 accept 和 recv)可以无期限阻塞。若是派发器让 lwIP 去施行此中一个挪用阻塞,派发器也将被阻塞,而且在整个系统中,统一时间只能有一个未完成的收集挪用。因为那种情况是无法承受的,所以收集办事器利用用户级线程以制止阻塞整个办事器情况。关于每个入站 IPC 动静,派发器将创建一个线程,然后在新创建的线程上来处置恳求。若是线程被阻塞,那么只要阿谁线程被置入休眠形态,而其它线程仍然处于运行中。
除了核心收集情况外,还有三个辅助情况。核心收集办事器情况除了领受来自用户应用法式的动静之外,它的派发器也领受来自输入情况和按时器情况的动静。
输出情况在为用户情况套接字挪用供给办事时,lwIP 将为网卡生成用于发送的包。lwIP 将利用 NSREQ_OUTPUT 去发送在 IPC 动静页参数中附加了包的 IPC 动静。输出情况负责领受那些动静,并通过你稍后创建的系统挪用接口来转发那些包到设备驱动法式上。
输入情况网卡领受到的包需要传递到 lwIP 中。输入情况将每个由设备驱动法式领受到的包拉进内核空间(利用你将要实现的内核系统挪用),并利用 NSREQ_INPUT IPC 动静将那些包发送到核心收集办事器情况。
包输入功用是独立于核心收集情况的,因为在 JOS 上同时实现领受 IPC 动静并从设备驱动法式中查询或期待包有点困难。我们在 JOS 中没有实现 select 系统挪用,那是一个允许情况去监视多个输入源以识别筹办处置哪个输入的系统挪用。
若是你查看了 net/input.c 和 net/output.c,你将会看到在它们中都需要去实现阿谁系统挪用。那次要是因为实现它要依赖你的系统挪用接口。在你实现了驱动法式和系统挪用接口之后,你将要为那两个辅助情况写那个代码。
按时器情况按时器情况周期性发送 NSREQ_TIMER 类型的动静到核心办事器,以提醒它阿谁按时器已过时。lwIP 利用来自线程的按时器动静来实现各类收集超时。
Part A:初始化和发送包你的内核还没有一个时间概念,因而我们需要去添加它。那里有一个由硬件产生的每 10 ms 一次的时钟中断。每收到一个时钟中断,我们将增加一个变量值,以暗示时间已过去 10 ms。它在 kern/time.c 中已实现,但还没有完全集成到你的内核中。
操练 1、为 kern/trap.c 中的每个时钟中断增加一个到 time_tick 的挪用。实现 sys_time_msec 并增加到 kern/syscall.c 中的 syscall,以便于用户空间可以拜候时间。利用 make INIT_CFLAGS=-DTEST_NO_NS run-testtime 去测试你的代码。你应该会看到情况计数从 5 起头以 1 秒为间隔削减。-DTEST_NO_NS 参数制止在收集办事器情况上启动,因为在当前它将招致 JOS 瓦解。
网卡写驱动法式要求你必需深切领会硬件和软件中的接口。本尝试将给你供给一个若何利用 E1000 接口的高度归纳综合的文档,但是你在写驱动法式时还需要大量去查询 Intel 的手册。
操练 2、为开发 E1000 驱动,去阅读 Intel 的 软件开发者手册。那个手册涵盖了几个与以太网控造器慎密相关的工具。QEMU 仿实了 82540EM。
如今,你应该去阅读第 2 章,以对设备获得一个整体概念。写驱动法式时,你需要熟悉第 3 到 14 章,以及 4.1(不包罗 4.1 的子节)。你也应该去参考第 13 章。其它章涵盖了 E1000 的组件,你的驱动法式其实不与那些组件去交互。如今你不消担忧过多细节的工具;只需要领会文档的整体构造,以便于你后面需要时容易查找。
在阅读手册时,记住,E1000 是一个拥有良多高级特征的很复杂的设备,一个能让 E1000 工做的驱动法式仅需要它一小部门的特征和 NIC 供给的接口即可。认真考虑一下,若何利用最简单的体例去利用网卡的接口。我们强烈保举你在利用高级特征之前,只去写一个根本的、可以让网卡工做的驱动法式即可。PCI 接口
E1000 是一个 PCI 设备,也就是说它是插到主板的 PCI 总线插槽上的。PCI 总线有地址、数据、和中断线,而且 PCI 总线允许 CPU 与 PCI 设备通信,以及 PCI 设备去读取和写入内存。一个 PCI 设备在它可以被利用之前,需要先发现它并停止初始化。发现 PCI 设备是 PCI 总线查找已安拆设备的过程。初始化是分配 I/O 和内存空间、以及协商设备所利用的 IRQ 线的过程。
我们在 kern/pci.c 中已经为你供给了利用 PCI 的代码。PCI 初始化是在引导期间施行的,PCI 代码遍历PCI 总线来查找设备。当它找到一个设备时,它读取它的供给商 ID 和设备 ID,然后利用那两个值做为关键字去搜刮 pci_attach_vendor 数组。那个数组是由像下面如许的 struct pci_driver 条目构成:
struct pci_driver { uint32_t key1, key2; int (*attachfn) (struct pci_func *pcif); };若是发现的设备的供给商 ID 和设备 ID 与数组中条目婚配,那么 PCI 代码将挪用阿谁条目标 attachfn 去施行设备初始化。(设备也能够按类别识别,那是通过 kern/pci.c 中其它的驱动法式表来实现的。)
绑定函数是传递一个 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; };上面的构造反映了在 Intel 开发者手册里第 4.1 节的表 4-1 中找到的一些条目。struct pci_func 的最初三个条目我们出格感兴趣的,因为它们将记录那个设备协商的内存、I/O、以及中断资本。reg_base 和 reg_size 数组包罗最多六个基址存放器或 BAR。reg_base 为映射到内存中的 I/O 区域(关于 I/O 端口而言是基 I/O 端口)保留了内存的基地址,reg_size 包罗了以字节暗示的大小或来自 reg_base 的相关基值的 I/O 端标语,而 irq_line 包罗了为中断分配给设备的 IRQ 线。在表 4-2 的后半部门给出了 E1000 BAR 的详细涵义。
当设备挪用了绑定函数后,设备已经被发现,但没有被启用。那意味着 PCI 代码还没有确定分配给设备的资本,好比地址空间和 IRQ 线,也就是说,struct pci_func 构造的最初三个元素还没有被填入。绑定函数将挪用 pci_func_enable,它将去启用设备、协商那些资本、并在构造 struct pci_func 中填入它。
操练 3、实现一个绑定函数去初始化 E1000。添加一个条目到 kern/pci.c 中的数组 pci_attach_vendor上,若是找到一个婚配的 PCI 设备就去触发你的函数(确保必然要把它放在表末尾的 {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 grade 的 pci attach 测试了。内存映射的 I/O
软件与 E1000 通过内存映射的 I/O(MMIO)来沟通。你在 JOS 的前面部门可能看到过 MMIO 两次:CGA 控造台和 LAPIC 都是通过写入和读取“内存”来控造和查询设备的。但那些读取和写入不是去往内存芯片的,而是间接到那些设备的。
pci_func_enable 为 E1000 协调一个 MMIO 区域,来存储它在 BAR 0 的基址和大小(也就是 reg_base[0] 和 reg_size[0]),那是一个分配给设备的一段物理内存地址,也就是说你能够通过虚拟地址拜候它来做一些工作。因为 MMIO 区域一般分配高位物理地址(一般是 3GB 以上的位置),因而你不克不及利用 KADDR 去拜候它们,因为 JOS 被限造为更大利用 256MB。因而,你能够去创建一个新的内存映射。我们将利用 MMIOBASE(从尝试 4 起头,你的 mmio_map_region 区域应该确保不克不及被 LAPIC 利用的映射所笼盖)以上的部门。因为在 JOS 创建用户情况之前,PCI 设备就已经初始化了,因而你能够在 kern_pgdir 处创建映射,而且让它始末可用。
操练 4、在你的绑定函数中,通过挪用 mmio_map_region(它就是你在尝试 4 中写的,是为了撑持 LAPIC 内存映射)为 E1000 的 BAR 0 创建一个虚拟地址映射。
你将希望在一个变量中记录那个映射的位置,以便于后面拜候你映射的存放器。去看一下 kern/lapic.c 中的 lapic 变量,它就是一个如许的例子。若是你利用一个指针指向设备存放器映射,必然要声明它为 volatile;不然,编译器将允许缓存它的值,并能够在内存中再次拜候它。
为测试你的映射,测验考试去输出设备形态存放器(第 12.4.2 节)。那是一个在存放器空间中以字节 8 开头的 4 字节存放器。你应该会得到 0x80080783,它暗示以 1000 MB/s 的速度启用一个全双工的链路,以及其它信息。提醒:你将需要一些常数,像存放器位置和掩码位数。若是从开发者手册中复造那些工具很容易出错,而且招致调试过程很痛苦。我们建议你利用 QEMU 的 e1000_hw.h 头文件做为基准。我们不建议完全照抄它,因为它定义的值远超越你所需要,而且定义的工具也不见得就是你所需要的,但它仍是一个很好的参考。
DMA
你可能会认为是从 E1000 的存放器中通过写入和读取来传送和领受数据包的,其实如许做会十分慢,而且还要求 E1000 在此中去缓存数据包。相反,E1000 利用间接内存拜候(DMA)从内存中间接读取和写入数据包,并且不需要 CPU 参与此中。驱动法式负责为发送和领受队列分配内存、设置 DMA 描述符、以及设置装备摆设 E1000 利用的队列位置,而在那些设置完成之后的其它工做都是异步体例停止的。发送包的时候,驱动法式复造它到发送队列的下一个 DMA 描述符中,而且通知 E1000 下一个发送包已停当;当轮到那个包发送时,E1000 将从描述符中复造出数据。同样,当 E1000 领受一个包时,它从领受队列中将它复造到下一个 DMA 描述符中,驱动法式将能鄙人一次读取到它。
总体来看,领受队列和发送队列十分类似。它们都是由一系列的描述符构成。固然那些描述符的构造细节有所差别,但每个描述符都包罗一些标记和包罗了包数据的一个缓存的物理地址(发送到网卡的数据包,或网卡将领受到的数据包写入到由操做系统分配的缓存中)。
队列被实现为一个环形数组,意味着当网卡或驱动抵达数组末端时,它将从头回到起头位置。它有一个头指针和尾指针,队列的内容就是那两个指针之间的描述符。硬件就是从头起头挪动头指针去消费描述符,在那期间驱动法式不断地添加描述符到尾部,并挪动尾指针到最初一个描述符上。发送队列中的描述符暗示期待发送的包(因而,在安静形态下,发送队列是空的)。关于领受队列,队列中的描述符是暗示网卡可以领受包的空描述符(因而,在安静形态下,领受队列是由所有的可用领受描述符构成的)。准确的更新尾指针存放器而不让 E1000 产生紊乱是很有难度的;要小心!
指向到那些数组及描述符中的包缓存地址的指针都必需是物理地址,因为硬件是间接在物理内存中且欠亨过 MMU 来施行 DMA 的读写操做的。
发送包E1000 中的发送和领受功用素质上是独立的,因而我们能够同时停止发送领受。我们起首去霸占简单的数据包发送,因为我们在没有先去发送一个 “I’m here!” 包之前是无法测试领受包功用的。
起首,你需要初始化网卡以筹办发送,详细步调查看 14.5 节(没必要焦急看子节)。发送初始化的第一步是设置发送队列。队列的详细构造在 3.4 节中,描述符的构造在 3.3.3 节中。我们先不要利用 E1000 的 TCP offload 特征,因而你只需专注于 “传统的发送描述符格局” 即可。你应该如今就去阅读那些章节,并要熟悉那些构造。
C 构造
你能够用 C struct 很便利地描述 E1000 的构造。正如你在 struct Trapframe 中所看到的构造那样,C struct 能够让你很便利地在内存中描述准确的数据规划。C 能够在字段中插入数据,但是 E1000 的构造就是如许规划的,如许就不会是个问题。若是你碰到字段对齐问题,进入 GCC 查看它的 “packed” 属性。
查看手册中表 3-8 所给出的一个传统的发送描述符,将它复造到那里做为一个示例:
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 字节,那就限造了那些缓存区的大小。支流的成熟驱动法式都可以动态分配包缓存区(即:当收集利用率很低时,削减内存利用量),或以至跳过缓存区,间接由用户空间供给(就是“零复造”手艺),但我们仍是从简单起头为好。
操练 5、施行一个 14.5 节中的初始化步调(它的子节除外)。关于存放器的初始化过程利用 13 节做为参考,对发送描述符和发送描述符数组参考 3.3.3 节和 3.4 节。
要记住,在发送描述符数组中要求对齐,而且数组长度上有限造。因为 TDLEN 必需是 128 字节对齐的,而每个发送描述符是 16 字节,你的发送描述符数组必需是 8 个发送描述符的倍数。而且不克不及利用超越 64 个描述符,以及不克不及在我们的发送环形缓存测试中溢出。
关于 TCTL.COLD,你能够假设为全双工操做。关于 TIPG、IEEE 802.3 尺度的 IPG(不要利用 14.5 节中表上的值),参考在 13.4.34 节中表 13-77 中描述的缺省值。测验考试运行 make E1000_DEBUG=TXERR,TX qemu。若是你利用的是打了 6.828 补钉的 QEMU,当你设置 TDT(发送描述符尾部)存放器时你应该会看到一个 “e1000: tx disabled” 的信息,而且不会有更多 “e1000” 信息了。
如今,发送初始化已经完成,你能够写一些代码去发送一个数据包,而且通过一个系统挪用使它能够拜候用户空间。你能够将要发送的数据包添加到发送队列的尾部,也就是说复造数据包到下一个包缓冲区中,然后更新 TDT 存放器去通知网卡在发送队列中有别的的数据包。(留意,TDT 是一个进入发送描述符数组的索引,不是一个字节偏移量;关于那一点文档中申明的不是很清晰。)
但是,发送队列只要那么大。若是网卡在发送数据包时卡住或发送队列填满时会发作什么情况?为了检测那种情况,你需要一些来自 E1000 的反应。不幸的是,你不克不及只利用 TDH(发送描述符头)存放器;文档上明白申明,从软件上读取那个存放器是不成靠的。但是,若是你在发送描述符的号令字段中设置 RS 位,那么,当网卡去发送在阿谁描述符中的数据包时,网卡将设置描述符中形态字段的 DD 位,若是一个描述符中的 DD 位被设置,你就应该晓得阿谁描述符能够平安地收受接管,而且能够用它去发送其它数据包。
若是用户挪用你的发送系统挪用,但是下一个描述符的 DD 位没有设置,暗示阿谁发送队列已满,该怎么办?在那种情况下,你该去决定怎么办了。你能够简单地丢弃数据包。收集协议对那种情况的处置很灵敏,但若是你丢弃大量的突发数据包,协议可能不会去从头获得它们。可能需要你替代收集协议告诉用户情况让它重传,就像你在 sys_ipc_try_send 中做的那样。在情况上回推产生的数据是有益处的。
操练 6、写一个函数去发送一个数据包,它需要查抄下一个描述符能否空闲、复造包数据到下一个描述符并更新 TDT。确保你处置的发送队列是满的。如今,应该去测试你的包发送代码了。通过从内核中间接挪用你的发送函数来测验考试发送几个包。在测试时,你不需要去创建契合任何特定收集协议的数据包。运行 make E1000_DEBUG=TXERR,TX qemu 去测试你的代码。你应该看到类似下面的信息:
e1000: index 0: 0x271f00 : 9000002a 0 ...在你发送包时,每行都给出了在发送数组中的序号、阿谁发送的描述符的缓存地址、cmd/CSO/length 字段、以及 special/CSS/status 字段。若是 QEMU 没有从你的发送描述符中输出你预期的值,查抄你的描述符中能否有适宜的值和你设置装备摆设的准确的 TDBAL 和 TDBAH。若是你收到的是 “e1000: TDH wraparound @0, TDT x, TDLEN y” 的信息,意味着 E1000 的发送队列持续不竭地运行(若是 QEMU 不去查抄它,它将是一个无限轮回),那意味着你没有准确地维护 TDT。若是你收到了许多 “e1000: tx disabled” 的信息,那么意味着你没有准确设置发送控造存放器。
一旦 QEMU 运行,你就能够运行 tcpdump -XXnr qemu.pcap 去查看你发送的包数据。若是从 QEMU 中看到预期的 “e1000: index” 信息,但你捕捉的包是空的,再次查抄你发送的描述符,能否填充了每个必须的字段和位。(E1000 或许已经遍历了你的发送描述符,但它认为不需要去发送)
操练 7、添加一个系统挪用,让你从用户空间中发送数据包。详细的接口由你来决定。但是不要忘了查抄从用户空间传递给内核的所有指针。发送包:收集办事器如今,你已经有一个系统挪用接口能够发送包到你的设备驱动法式端了。输出辅助情况的目的是在一个轮回中做下面的工作:从核心收集办事器中领受 NSREQ_OUTPUT IPC 动静,并利用你在上面增加的系统挪用去发送陪伴那些 IPC 动静的数据包。那个 NSREQ_OUTPUT IPC 是通过 net/lwip/jos/jif/jif.c 中的 low_level_output 函数来发送的。它集成 lwIP 栈到 JOS 的收集系统。每个 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 暗示包的长度。在 IPC 页上的所有后续字节都是为了包内容。在构造的结尾处利用一个长度为 0 的数组来暗示缓存没有一个预先确定的长度(像 jp_data 一样),那是一个常见的 C 技巧(也有人说那是一个令人厌恶的做法)。因为 C 其实不做数组鸿沟的查抄,只要你确保构造后面有足够的未利用内存即可,你能够把 jp_data 做为一个肆意大小的数组来利用。
当设备驱动法式的发送队列中没有足够的空间时,必然要留意在设备驱动法式、输出情况和核心收集办事器之间的交互。核心收集办事器利用 IPC 发送包到输出情况。若是输出情况在因为一个发送包的系统挪用而挂起,招致驱动法式没有足够的缓存去包容新数据包,那时核心收集办事器将阻塞以期待输出办事器去领受 IPC 挪用。
操练 8、实现 net/output.c。你能够利用 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 ...利用更多的数据包去测试,能够运行 make E1000_DEBUG=TXERR,TX NET_CFLAGS=-DTESTOUTPUT_COUNT=100 run-net_testoutput。若是它招致你的发送队列溢出,再次查抄你的 DD 形态位能否准确,以及能否告诉硬件去设置 DD 形态位(利用 RS 号令位)。
你的代码应该会通过 make grade 的 testoutput 测试。
问题 1、你是若何构造你的发送实现的?在理论中,若是发送缓存区满了,你该若何处置?Part B:领受包和 web 办事器 领受包就像你在发送包中做的那样,你将去设置装备摆设 E1000 去领受数据包,并供给一个领受描述符队列和领受描述符。在 3.2 节中描述了领受包的操做,包罗领受队列构造和领受描述符、以及在 14.4 节中描述的详细的初始化过程。
操练 9、阅读 3.2 节。你能够忽略关于中断和 offload 校验和方面的内容(若是在后面你想去利用那些特征,能够再返归去阅读),你如今不需要去考虑阈值的细节和网卡内部缓存是若何工做的。除了领受队列是由一系列的期待入站数据包去填充的空缓存包以外,领受队列的其它部门与发送队列十分类似。所以,当收集空闲时,发送队列是空的(因为所有的包已经被发送进来了),而领受队列是满的(全数都是空缓存包)。
当 E1000 领受一个包时,它起首与网卡的过滤器停止婚配查抄(例如,去查抄那个包的目的地址能否为那个 E1000 的 MAC 地址),若是那个包不婚配任何过滤器,它将忽略那个包。不然,E1000 测验考试从领受队列头部去检索下一个领受描述符。若是头(RDH)逃上了尾(RDT),那么申明领受队列已经没有空闲的描述符了,所以网卡将丢弃那个包。若是有空闲的领受描述符,它将复造那个包的数据到描述符指向的缓存中,设置那个描述符的 DD 和 EOP 形态位,并递增 RDH。
若是 E1000 在一个领受描述符中领受到了一个比包缓存还要大的数据包,它将按需从领受队列中检索尽可能多的描述符以保留数据包的全数内容。为暗示发作了那种情况,它将在所有的那些描述符上设置 DD 形态位,但仅在那些描述符的最初一个上设置 EOP 形态位。在你的驱动法式上,你能够去向理那种情况,也能够简单地设置装备摆设网卡回绝领受那种”长包“(那种包也被称为”巨帧“),你要确保领受缓存有足够的空间尽可能地去存储更大的尺度以太网数据包(1518 字节)。
操练 10、设置领受队列并按 14.4 节中的流程去设置装备摆设 E1000。你能够不消撑持 ”长包“ 或多播。到目前为行,我们不消去设置装备摆设网卡利用中断;若是你在后面决定去利用领受中断时能够再去改。别的,设置装备摆设 E1000 去除以太网的 CRC 校验,因为我们的评级脚本要求必需去掉校验。
默认情况下,网卡将过滤掉所有的数据包。你必需利用网卡的 MAC 地址去设置装备摆设领受地址存放器(RAL 和 RAH)以领受发送到那个网卡的数据包。你能够简单地硬编码 QEMU 的默认 MAC 地址 52:54:00:12:34:56(我们已经在 lwIP 中硬编码了那个地址,因而如许做不会有问题)。利用字节挨次时要留意;MAC 地址是从低位字节到高位字节的体例来写的,因而 52:54:00:12 是 MAC 地址的低 32 位,而 34:56 是它的高 16 位。
E1000 的领受缓存区大小仅撑持几个指定的设置值(在 13.4.22 节中描述的 RCTL.BSIZE 值)。若是你的领受包缓存够大,而且回绝长包,那你就不消担忧逾越多个缓存区的包。别的,要记住的是,和发送一样,领受队列和包缓存必需是毗连的物理内存。
你应该利用至少 128 个领受描述符。如今,你能够做领受功用的根本测试了,以至都无需写代码去领受包了。运行 make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-net_testinput。testinput 将发送一个 ARP(地址解析协议)布告包(利用你的包发送的系统挪用),而 QEMU 将主动回复它,即使是你的驱动尚不克不及领受那个回复,你也应该会看到一个 “e1000: unicast match[0]: 52:54:00:12:34:56” 的动静,暗示 E1000 领受到一个包,而且婚配了设置装备摆设的领受过滤器。若是你看到的是一个 “e1000: unicast mismatch: 52:54:00:12:34:56” 动静,暗示 E1000 过滤掉了那个包,意味着你的 RAL 和 RAH 的设置装备摆设不准确。确保你按准确的挨次收到了字节,其实不要忘记设置 RAH 中的 “Address Valid” 位。若是你没有收到任何 “e1000” 动静,或许是你没有准确地启用领受功用。
如今,你筹办去实现领受数据包。为了领受数据包,你的驱动法式必需持续跟踪希望去保留下一下领受到的包的描述符(提醒:按你的设想,那个功用或许已经在 E1000 中的一个存放器来实现了)。与发送类似,官方文档上暗示,RDH 存放器形态其实不能从软件中可靠地读取,因为确定一个包能否被发送到描述符的包缓存中,你需要去读取描述符中的 DD 形态位。若是 DD 位被设置,你就能够从阿谁描述符的缓存中复造出那个数据包,然后通过更新队列的尾索引 RDT 来告诉网卡阿谁描述符是空闲的。
若是 DD 位没有被设置,表白没有领受到包。那就与发送队列满的情况一样,那时你能够有几种做法。你能够简单地返回一个 ”重传“ 错误来要求对端重发一次。关于满的发送队列,因为那是个临时情况,那种做法仍是很好的,但关于空的领受队列来说就不太合理了,因为领受队列可能会连结好长一段时间的空的形态。第二个办法是挂起挪用情况,曲到在领受队列中处置了那个包为行。那个战略十分类似于 sys_ipc_recv。就像在 IPC 的案例中,因为我们每个 CPU 仅有一个内核栈,一旦我们分开内核,栈上的形态就会被丢弃。我们需要设置一个标记去暗示阿谁情况因为领受队列下溢被挂起并记录系统挪用参数。那种办法的缺点是过于复杂:E1000 必需被指示去产生领受中断,而且驱动法式为了恢复被阻塞期待一个包的情况,必需处置那个中断。
操练 11、写一个函数从 E1000 中领受一个包,然后通过一个系统挪用将它发布到用户空间。确保你将领受队列处置成空的。.
小挑战!若是发送队列是满的或领受队列是空的,情况和你的驱动法式可能会破费大量的 CPU 周期是轮询、期待一个描述符。一旦完成发送或领受描述符,E1000 可以产生一个中断,以制止轮询。修改你的驱动法式,处置发送和领受队列是以中断而不是轮询的体例停止。
留意,一旦确定为中断,它将不断处于中断形态,曲到你的驱动法式明白处置完中断为行。在你的中断办事法式中,一旦处置完成要确保肃清掉中断形态。若是你不那样做,从你的中断办事法式中返回后,CPU 将再次跳转到你的中断办事法式中。除了在 E1000 网卡上肃清中断外,也需要利用 lapic_eoi 在 LAPIC 上肃清中断。领受包:收集办事器在收集办事器输入情况中,你需要去利用你的新的领受系统挪用以领受数据包,并利用 NSREQ_INPUT IPC 动静将它传递到核心收集办事器情况。那些 IPC 输入动静应该会有一个页,那个页上绑定了一个 union Nsipc,它的 struct jif_pkt pkt 字段中有从收集上领受到的包。
操练 12、实现 net/input.c。利用 make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-net_testinput 再次运行 testinput,你应该会看到:
Sending ARP announcement... Waiting for packets... e1000: index 0: 0x26dea0 : 900002a 0 e1000: unicast match[0]: 52:54:00:12:34:56 input: 0000 5254 0012 3456 5255 0a00 0202 0806 0001 input: 0010 0800 0604 0002 5255 0a00 0202 0a00 0202 input: 0020 5254 0012 3456 0a00 020f 0000 0000 0000 input: 0030 0000 0000 0000 0000 0000 0000 0000 0000“input:” 打头的行是一个 QEMU 的 ARP 回复的十六进造转储。
你的代码应该会通过 make grade 的 testinput 测试。留意,在没有发送至少一个包去通知 QEMU 中的 JOS 的 IP 地址上时,是没法去测试包领受的,因而在你的发送代码中的 bug 可能会招致测试失败。
为彻底地测试你的收集代码,我们供给了一个称为 echosrv 的守护法式,它在端口 7 上设置运行 echo的办事器,它将回显通过 TCP 毗连发送给它的任何内容。利用 make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-echosrv 在一个末端中启动 echo 办事器,然后在另一个末端中通过 make nc-7 去毗连它。你输入的每一行都被那个办事器回显出来。每次在仿实的 E1000 上领受到一个包,QEMU 将在控造台上输出像下面如许的内容:
e1000: unicast match[0]: 52:54:00:12:34:56 e1000: index 2: 0x26ea7c : 9000036 0 e1000: index 3: 0x26f06a : 9000039 0 e1000: unicast match[0]: 52:54:00:12:34:56做到那一点后,你应该也就能通过 echosrv 的测试了。
问题 2、你若何构造你的领受实现?在理论中,若是领受队列是空的而且一个用户情况要求下一个入站包,你怎么办?.
小挑战!在开发者手册中阅读关于 EEPROM 的内容,并写出从 EEPROM 中加载 E1000 的 MAC 地址的代码。目前,QEMU 的默认 MAC 地址是硬编码到你的领受初始化代码和 lwIP 中的。修复你的初始化代码,让它可以从 EEPROM 中读取 MAC 地址,和增加一个系统挪用去传递 MAC 地址到 lwIP 中,并修改 lwIP 去从网卡上读取 MAC 地址。通过设置装备摆设 QEMU 利用一个差别的 MAC 地址去测试你的变动。.
小挑战!修改你的 E1000 驱动法式去利用 零复造 手艺。目前,数据包是从用户空间缓存中复造到发送包缓存中,和从领受包缓存中复造回到用户空间缓存中。一个利用 ”零复造“ 手艺的驱动法式能够通过间接让用户空间和 E1000 共享包缓存内存来实现。还有许多差别的办法去实现 ”零复造“,包罗映射内容分配的构造到用户空间或间接传递用户供给的缓存到 E1000。不管你选择哪种办法,都要留意你若何操纵缓存的问题,因为你不克不及在用户空间代码和 E1000 之间产生争用。.
小挑战!把 “零复造” 的概念用到 lwIP 中。
一个典型的包是由许多头构成的。用户发送的数据被发送到 lwIP 中的一个缓存中。TCP 层要添加一个 TCP 包头,IP 层要添加一个 IP 包头,而 MAC 层有一个以太网头。以至还有更多的部门增加到包上,那些部门要准确地毗连到一路,以便于设备驱动法式可以发送最末的包。
E1000 的发送描述符设想长短常合适搜集分离在内存中的包片段的,像在 lwIP 中创建的包的帧。若是你列队多个发送描述符,但仅设置最初一个描述符的 EOP 号令位,那么 E1000 将在内部把那些描述符串成包缓存,并在它们标识表记标帜完 EOP 后仅发送串起来的缓存。因而,独立的包片段不需要在内存中把它们毗连到一路。
修改你的驱动法式,以使它可以发送由多个缓存且无需复造的片段构成的包,而且修改 lwIP 去制止它合并包片段,因为它如今可以准确处置了。.
小挑战!增加你的系统挪用接口,以便于它可以为多于一个的用户情况供给办事。若是有多个收集栈(和多个收集办事器)而且它们各自都有本身的 IP 地址运行在用户形式中,那将长短常有用的。领受系统挪用将决定它需要哪个情况来转发每个入站的包。
留意,当前的接口其实不晓得两个包之间有何差别,而且若是多个情况去挪用包领受的系统挪用,各个情况将得到一个入站包的子集,而阿谁子集可能其实不包罗挪用情况指定的阿谁包。
在 那篇 外内核论文的 2.2 节和 3 节中对那个问题做了深度解释,并解释了在内核中(如 JOS)处置它的一个办法。用那个论文中的办法去处理那个问题,你不需要一个像论文中那么复杂的计划。Web 办事器一个最简单的 web 办事器类型是发送一个文件的内容到恳求的客户端。我们在 user/httpd.c 中供给了一个十分简单的 web 办事器的框架代码。那个框架内码处置入站毗连并解析恳求头。
操练 13、那个 web 办事器中缺失了发送一个文件的内容到客户端的处置代码。通过实现 send_file 和 send_data 完成那个 web 办事器。在你完成了那个 web 办事器后,启动那个 web 办事器(make run-httpd-nox),利用你喜好的阅读器去阅读 http://host:port/index.html 地址。此中 host 是运行 QEMU 的计算机的名字(若是你在 athena 上运行 QEMU,利用 hostname.mit.edu(此中 hostname 是在 athena 上运行 hostname号令的输出,或者若是你在运行 QEMU 的机器上运行 web 阅读器的话,间接利用 localhost),而 port 是 web 办事器运行 make which-ports 号令陈述的端标语。你应该会看到一个由运行在 JOS 中的 HTTP 办事器供给的一个 web 页面。
到目前为行,你的评级测试得分应该是 105 分(满分为 105)。
小挑战!在 JOS 中添加一个简单的聊天办事器,多小我能够毗连到那个办事器上,而且任何用户输入的内容都被发送到其它用户。为实现它,你需要找到一个一次与多个套接字通信的办法,而且在统一时间可以在统一个套接字上同时实现发送和领受。有多个办法能够到达那个目标。lwIP 为 recv(查看 net/lwip/api/sockets.c 中的 lwip_recvfrom)供给了一个 MSG_DONTWAIT 标记,以便于你不竭地轮询所有翻开的套接字。留意,固然收集办事器的 IPC 撑持 recv 标记,但是通过通俗的 read 函数其实不能拜候它们,因而你需要一个办法来传递那个标记。一个更高效的办法是为每个毗连去启动一个或多个情况,而且利用 IPC 去协调它们。并且碰巧的是,关于一个套接字,在构造 Fd 中找到的 lwIP 套接字 ID 是全局的(不是每个情况私有的),因而,好比一个 fork 的子情况继承了它的父情况的套接字。或者,一个情况通过构建一个包罗了准确套接字 ID 的 Fd 就可以发送到另一个情况的套接字上。
问题 3、由 JOS 的 web 办事器供给的 web 页面显示了什么?.
问题 4、你做那个尝试大约花了多长的时间?本尝试到此完毕了。一如既往,不要忘了运行 make grade 并去写下你的谜底和挑战问题的处理计划的描述。在你脱手之前,利用 git status 和 git diff 去查抄你的变动,其实不要忘了去 git add answers-lab6.txt。当你完成之后,利用 git commit -am my solutions to lab 6’ 去提交你的变动,然后 make handin 并存眷它的意向。
via: https://pdos.csail.mit.edu/6.828/2018/labs/lab6/
做者:csail.mit 选题:lujun9972 译者:qhwdw 校对:wxy
本文由 LCTT 原创编译,Linux中国 荣誉推出