Lab 6: Network Driver
Introduction
本实验室默认能自主完成的最后一个项目,现在我们已经有了一个文件系统,
本实验中我们将为网卡接口编写驱动程序,该卡基于Intel 82540EM芯片,也称为E1000。
Getting Started
但是,网卡驱动程序还不足以使JOS连接到Internet。 在新的lab6代码中,我们为您提供了网络堆栈和网络服务器。 与以前的实验一样,使用git获取该实验的代码,合并到您自己的代码中,并浏览新目录net中的内容以及kern /中的新文件。
除了编写驱动程序之外,还需要创建一个系统调用接口来提供对驱动程序的访问权限。 我们将实现缺少的网络服务器代码,以在网络堆栈和驱动程序之间传输数据包。 还将通过完成Web服务器将所有内容连接在一起。 使用新的Web服务器,我们将能够从文件系统提供文件。
许多内核设备驱动程序代码我们都得从头开始编写。 与以前的实验相比,本实验提供的指导要少得多:没有框架文件,没有任何固定的系统调用接口,许多设计决策都由自己决定。 因此,建议在开始任何练习之前,先阅读完整的实验内容。
我们将使用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应该是指网络地址转换,这里没意义应该是说10.0.2.15是一个专用网络中的本地ip,在外网无意义),因此我们在运行JOS时,无法直接连接到服务器 即使是在运行QEMU的主机中。 为了解决这个问题,我们将QEMU配置为在主机的某个端口上运行服务器,该服务器仅连接到JOS中的某个端口,并在真实主机和虚拟网络之间来回穿梭数据。
你会运行JOS 服务器在ports 7(echo) 和 80 (http)。可以键入make which-ports来查找QEMU转发到你的开发主机上的那些端口。为了方便,makefile也提供make nc-7和make nc-80,它们允许你跟运行在你终端的这些端口的服务器直接交互。
Packet Inspection
makefile同样配置了QEMU的网络堆栈以记录所有的输入/输出包到lab目录下的qemu.pcap.
想要得到一个hex/ASCII dump(转储)的被捕获数据包,可以键入如下指令:
tcpdump -xxnr qemu.pcap
Debugging the E1000
我们能够使用模拟硬件其实很幸运,因为E1000是运行在软件中的,模拟的E1000能够以一种用户可读的格式告诉我们它的内部状态和它遇到的所有问题,正常情况下,这对一个驱动开发者来说很奢侈。
E1000能够产生很多调试输出,所以必须启用特定的logging channels,一些channels可能比较有用:
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命令。
The Network Server
从头开始写一个网络堆栈是很难的,相反我们将使用lwIP,一个开源的轻量级的TCP/IP协议套件,其中包含了一个网络堆栈,你可以在此处查看更多关于lwIP的信息,在这个实验中,lwIP对于我们来说是个黑箱,它实现了一个BSD socket接口并具有封包输入端口和封包输出端口。网络服务器实际上由四个进程组成:
sore network server environment (includes socket call dispatcher and lwIP)
input environment
output environment
timer environment
下图展示了这些不同进程间的关系,和包括设备驱动在内的整个系统,稍后会对此介绍,本实验中,我们将实现下图中标记为绿色的部分:
The Core Network Server Environment
核心网络服务器进程由the socket call dispatcher和lwIP组成,socket call dispatcher的工作方式类似于文件服务器,用户进程使用stubs来向核心网络进程发送消息,阅读lib/nsipc.c,你会发现我们查找核心网络服务器的方式和我们查找文件服务器的方式是相同的,i386_init中以NS_TYPE_NS创建了NS进程,对每个用户进程IPC,网络服务器上的调度程序会代表用户调用合适的lwIP提供的BSD socket接口函数.
普通用户进程不能直接使用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创建一个线程,并在新创建的线程中处理请求。如果线程阻塞,则只有该线程处于休眠状态,而其他线程继续运行。
除了核心网络进程之外,还有三个辅助进程。除了接受来自用户应用程序的消息外,核心网络进程的调度器还接受来自input和timer进程的消息。
The Output Environment
当服务于用户进程sockets calls时,lwIP将生成输出包供网卡传输。LwIP将使用NSREQ_OUTPUT IPC消息将每个要传输的包发送到输出辅助进程,这个输出包会装入到IPC消息的页面参数中。输出辅助进程负责接收这些IPC消息,并通过您即将创建的系统调用接口将包转发到设备驱动程序。
The Input Environment
网卡接收到的数据包需要注入到lwIP中。对于设备驱动程序接收到的每个包,输入进程将包从内核空间中取出(使用您将实现的内核系统调用),并使用NSREQ_INPUT IPC消息将包发送到核心网络进程。 将输入包的功能与核心网络进程分离是因为JOS使得同时接受IPC消息和轮询或等待来自设备驱动程序的包变得困难。JOS中没有select系统调用,该调用允许环境监视多个输入源,以确定哪些输入已准备好被处理。
查看net/input.c和net/output.c,你会发现这两者都需要实现。该实现主要依赖你的系统调用接口。在实现驱动程序和系统调用接口之后,你将为这两个辅助进程编写代码。
The Timer Environment
timer进程定期向核心网络服务器发送NSREQ_TIMER IPC消息,通知它一个计时器已经过期。lwIP使用来自这个线程的计时器消息来实现各种网络超时。
Part A: Initialization and transmitting packets
我们首先要为jos内核添加时钟的概念,内核中每10ms都会由硬件产生一个时钟中断,每次时钟中断时我们可以增加一个变量的计数来指出时间已经过去了10ms,这部分的实现在kern/time.c中,但是尚未集成到内核