MIT6.828 LAB6: Network Driver

  抽了点空把LAB6重新整理一下,作为结束符~~。
Introduction
  我们已经实现了1个文件系统,当然OS还需要1个网络栈,在本次实验中我们将实现1个网卡驱动,这个网卡基于Intel 82540EM芯片,也就是熟知的E1000网卡。
  网卡驱动不足以使你的OS能连接上Internet。在LAB6新增加的代码中,我们提供了1个网络栈(network stack)和网络服务器(network server)在net/目录和kern/目录下。
  本次新增加的文件如下:
  net/lwip目录:开源轻量级TCP/IP协议组件包括1个网络栈
  net/timer.c:定时器功能测试程序
  net/ns.h:网卡驱动相关的参数宏定义和函数声明
  net/testinput.c:收包功能测试程序
  net/input.c:收包功能的用户态函数
  net/testoutput.c:发包功能测试程序
  net/output.c:发包功能的用户态函数
  net/serv.c:网络服务器的实现
  kern/e1000.c:网卡驱动的内核实现
  kern/e1000.h:网卡驱动实现相关的参数宏定义和函数声明

  除了实现网卡驱动,我们还要实现1个系统调用接口来访问驱动。我们需要实现网络服务器代码来传输网络数据包在网络栈和驱动之间。同时网络服务器也能使用文件系统中的文件。
  大部分内核驱动代码必须从零开始编写,这次实验比前面的实验提供更少的指导:没有骨架文件、没有系统调用接口等。总之一句话,要实现这次实验需要阅读很多提供的指导说明手册,才能完成实验。

QEMU’s virtual network
  我们将会使用QEMU用户态网络栈,因为它运行不需要管理员权限。关于QEMU用户态网络栈的说明在这里(QEMU用户态网络栈)。我们已经更新了makefile,从而能够使用QEMU用户态网络栈和虚拟E1000网卡。
  在默认情况下,QEMU会提供一个运行在IP为10.0.2.2的虚拟路由器并且分配给JOS一个10.0.2.15的IP地址。为了简单起见,我们把这些默认设置硬编码在了net/ns.h中。
  尽管QEMU的虚拟网络允许JOS和互联网做任意的连接,但是JOS的10.0.2.15 IP地址在QEMU运行的虚拟网络之外没有任何意义(QEMU就像一个NAT),所以我们不能直接和JOS中运行的se服务器连接,即使是运行QEMU的宿主机上也不行。为了解决这个问题,我们通过配置QEMU,让JOS的一些端口和宿主机的某些端口相连,让服务器运行在这些端口上,从而让数据在宿主机和虚拟网络之间进行交换。
  我们将在端口7(echo)和80(http)运行端口。为了避免端口冲突,makefile里实现了端口转发。可以通过运行make which-ports来找出QEMU转发的端口,也可以通过make nc-7和make nc-80来和运行在这些端口上的服务器交互。

Packet Inspection
  makefile也配置了QEMU的网络栈来记录各种进入和出去的数据包到qemu.pcap文件中。为了获得hex/ASCII的转换,我们可以使用tcpdump命令(Linux下非常有用的网络抓包分析工具,具体的参数说明可以用man tcpdump):
  tcpdump -XXnr qemu.pcap
  或者,也可以使用著名的Wireshark图形化工具来解析pcap文件。

Debugging the E1000  
  很幸运我们使用的是模拟硬件,E1000网卡运行为软件,模拟的E1000网卡能以用户可读的形式,向我们汇报有用的信息,比如内部状态和问题。
  模拟E1000网卡能产生一系列debug输出,通过打开特殊的日志通道,来捕获输出信息:
  MIT6.828 LAB6: Network Driver_第1张图片

注意: E1000_DEBUG标志只在mit6.828课程提供QEMU中有用。
  
The Network Server
  从零开始写1个网络栈是很难的。这里,我们使用lwIP开源TCP/IP协议组件来实现网络栈(具体可以查看lwIP)。在这个实验中,我们只需知道lwIP是一个黑盒,它实现了BSD的socket接口并且有一个数据包input port和数据包output port。
  网络服务器其实是由以下四个environments组成的
  (1) 核心网络服务 environment(包括socket调用分发和lwIP
  (2) 输入environment
  (3) 输出environment
  (4) 计时environment

  下图显示了各个environments以及它们之间的关系。图中展示了整个系统包括设备驱动。在本次实验中,我们将实现被标记为绿色的那些部分。
  MIT6.828 LAB6: Network Driver_第2张图片
  其实整个网络服务器实现与文件系统的实现类似,也是通过IPC机制来在各个environment之间进行数据交互。

The Core Network Server Environment  
  核心网络服务environment由socket调用分发器和lwIP组成。The socket调用分发和文件服务器的工作方式类似。用户 environment通过stubs(定义在lib/nsipc.c)向核心网络environment发送IPC消息。查看lib/nsipc.c可以发现,核心网络服务器的工作方式和文件服务器是类似的:i386_init创建了NS environment,类型为NS_TYPE_NS,因此我们遍历envs,找到这个特殊的environment type。对于每一个用户environment的IPC,网络服务器中的IPC分发器会调用由lwIP提供的BSD socket接口来实现。
  普通的用户environment不直接使用nsipc_*调用。通常它们都使用lib/sockets.c中提供的基于文件描述符的sockets API。因此,用户environment通过文件描述符来引用socket,就像引用普通的磁盘文件一样。虽然socket有许多特殊的操作(比如connect、accept等等),但是像read,write,close这样的操作也是通过lib/fd.c中正常的文件描述符device-dispatcher代码。就像文件服务器会为所有打开的文件维护一个内部独有的ID,lwIP也会为每个打开的socket维护一个独有的ID。在文件服务器或者网络服务器中,我们使用存储在struct Fd中的信息来映射每个environment的文件描述符到相应的ID空间中。
  虽然看起来文件服务器和网络服务器的IPC分发器工作方式相同,但是事实上有一个非常重要的区别。有些BSD socket的操作,例如accept和recv可能会永远阻塞。如果分发器让lwIP运行其中一个堵塞调用,那么很可能分发器会阻塞,因此整个系统在某一时刻只能有一个网络调用,显然,这是不能让人接收的。因此网络服务器使用用户级线程去避免整个服务器environment的阻塞。对于每一个到来的IPC,分发器都会创建一个线程,然后由它对请求进行处理。即使这个线程阻塞了,那么也仅仅只是它进入休眠状态,而其他的线程照样能继续运行。
  除了核心网络environment之外,还有其他三个辅助的environment。除了从用户程序中获取消息以外,核心网络 environment的分发器还从input environment和timer environment处获取信息。

The Output Environment 
  当处理用户environment的socket调用时,lwIP会产生packet用于网卡的传输。lwIP会将需要发送的packet通过NSREQ_OUTPUT IPC发送给output helper environment,packet的内容存放在IPC的共享页中。output environment负责接收这些信息并且通过系统调用接口将这些packet转发到相应的设备驱动(我们即将实现)。

The Input Environment
  网卡得到的packet需要注入到lwIP中。对于设备驱动获得的每一个packet,input environment需要通过相应的系统调用将它们从内核中抽取出来,然后通过NSREQ_INPUT IPC 发送给核心服务器environment。
  packet input的功能从核心网络environment中剥离出来了,因为接收IPC并且同时接收或等待来自设备驱动的packet对于JOS是非常困难的。因为JOS中没有select这样能够允许environment监听多个输入源并且判断出哪个源已经准备好了。
  net/input.c和net/output.c中就是我们要实现的2个用户态函数,当我们实现完网卡驱动和系统调用接口后。

The Timer Environment       
  timer environment会定期地向核心网络服务器发送NSREQ_TIMER IPC,通知它又过去了一个时间间隔,而lwIP会利用这些时间信息去实现各种的网络超时。

Part A: Initialization and transmitting packets
  我们的内核中还没有时间的概念,所以我们需要加上它。现在每隔10ms都有一个由硬件产生的时钟中断。每次出现一个时钟中断的时候,我们都对一个变量进行加操作,表示过去了10ms。这实现在kern/time.c中,但是并未归并到内核中。
  Exercise 1:
  在kern/trap.c中增加1个time_tick调用来处理每次时钟中断,实现sys_time_msec系统调用,使用户空间能读取时间。
  回答:
  首先在kern/trap.c的trap_dispatch函数中,对于IRQ_OFFSET + IRQ_TIMER中断添加time_tick调用:

//kern/trap.c
if (tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER) {
        lapic_eoi();
        time_tick();
        sched_yield();
        return;
}

//kern/time.c
void
time_tick(void)
{   
    ticks++;
    if (ticks * 10 < ticks)
        panic("time_tick: time overflowed");
}

  接下去就是添加获取时间的系统调用,具体流程和之前的一样,主要是在kern/syscall.c的中实现sys_time_msec函数,在该函数中调用time_msec函数来获得系统时间。

//kern/syscall.c
// Return the current time.
static int
sys_time_msec(void)
{
    return time_msec();
}   

//kern/time.c
unsigned int
time_msec(void)
{   
    return ticks * 10;
}

  通过运行make INIT_CFLAGS=-DTEST_NO_NS run-testtime来测试计时器共,将会看到从5到1的倒计时。其中”-DTEST_NO_NS”禁止启动网络服务器environment,因为我们暂时还没实现。

The Network Interface Card
  要写1个驱动必须要深入硬件和软件接口,在本次实验中我们将给1个高层次综述关于如何与E1000网卡交互,但是你需要去使用Intel的帮助手册来实现驱动。
  Exercise 2:
  浏览Intel关于E1000网卡的软件开发手册,该手册包括了多个相关的网卡控制器,而QEMU模拟的是82540EM。
  回答:
  主要是浏览第2张关于设备架构,为了编写驱动需要阅读第3章收发包描述符、第14章通用初始化和重置操作、第13章寄存器描述。

PCI Interface
  E1000网卡是一个PCI设备,这说明它是插入主板的PCI总线。PCI总线有地址总线、数据总线和中断总线,从而允许CPU能访问PCI设备,PCI设备也能读写内存。一个PCI设备在使用之前需要被发现并且初始化。发现的过程是指遍历PCI总线找到已经连接的设备。初始化是指为设备分配IO和内存空间并且指定IRQ线的过程。
  PCI是外围设备互连(Peripheral Component Interconnect)的简称,是在目前计算机系统中得到广泛应用的通用总线接口标准:  

  • 在一个PCI系统中,最多可以有256根PCI总线,一般主机上只会用到其中很少的几条。
  • 在一根PCI总线上可以连接多个物理设备,可以是一个网卡、显卡或者声卡等,最多不超过32个。
  • 一个PCI物理设备可以有多个功能,比如同时提供视频解析和声音解析,最多可提供8个功能。
  • 每个功能对应1个256字节的PCI配置空间。

      我们在kern/pci.c中已经提供了PCI相关的代码。为了在启动过程中实现PCI的初始化,相关的PCI代码遍历了PCI总线进行设备查找。当发现一个设备时,它会读取它的vendor ID和device ID,把这两个值作为key去查询pci_attach_vendor数组。该数组元素是struct pci_driver类型的,如下所示:

struct pci_driver {
  uint32_t key1, key2;
  int (*attachfn) (struct pci_func *pcif);
};

  如果被发现设备的vendor ID和device ID和数组中的某个表项是匹配的,那么接下来就会调用该表项的attachfn函数进行初始化工作。(设备也能被class识别,我们在kern/pci.c中也提供了其它驱动表)
  每一个PCI设备都有它映射的内存地址空间和I/O区域,除此之外,PCI设备还有配置空间,一共有256字节,其中前64字节是标准化的,提供了厂商号、设备号、版本号等信息,唯一标示1个PCI设备,同时提供最多6个的IO地址区域。
  MIT6.828 LAB6: Network Driver_第3张图片
  MIT6.828 LAB6: Network Driver_第4张图片
  MIT6.828 LAB6: Network Driver_第5张图片
  当我们向查询1个特定PCI设备的配置空间时,需要向I/O地址[0cf8,0cfb]写入1个4字节的查询码指定总线号:设备号:功能号以及其配置地址空间中的查询位置。PCI Host Bridge将监听对于这个I/O端口的写入,并将查询结果写入到[0cfc,0cff],我们可以从这个地址读出1个32位整数表示查询到的相应信息。
  attachfn函数的参数是一个PCI function。一个PCI card可以有多个function,虽然E1000只有一个。下面就是我们在JOS中呈现PCI function的方式:

你可能感兴趣的:(操作系统,麻省理工)