XV6 Network解析-1

启动流程分析

主要来看下当前内核中,相较于之前的部分,多了哪些东西,已经分析下他们的作用。

Makefile分析

首先来分析一下执行make qemu 后,对于网络部分,做了哪些工作。

在Makefile文件中,我们可以找到关于网络的一些特有qemu虚拟配置,代码如下:

ifeq ($(LAB),net)
QEMUOPTS += -netdev user,id=net0,hostfwd=udp::$(FWDPORT)-:2000 -object filter-dump,id=net0,netdev=net0,file=packets.pcap
QEMUOPTS += -device e1000,netdev=net0,bus=pcie.0
endif

查找qemu的手册,找到了关于其配置的讲解,其中qemu有设备前端和后端的概念,简单来说前端指的是模拟的硬件设备,后端指的是前端的数据应该被如何处理。前端通过-device来指定设备,后端通过-xxxdev来指定数据的处理方式。
在这里,

  • -device e1000 指定了前端设备是e1000网卡,它的后端设备是net0,在总线pcie.0上;

  • -netdev user 配置了不需要管理员权限即可运行的用户模式主机网络后端。

    • id 指定了可以在监视器命令中使用的符号名称,也就是net0。

    • hostfwd 表示将传入的 TCP 或 UDP 的数据从主机hostaddr的端口 hostport 转发到 访客 IP 地址为guestaddr的guestport 端口上的。如果未指定 guestaddr,则其值为 xxx15(内置 DHCP 服务器默认提供的第一个地址)。通过指定 hostaddr,可以将规则绑定到特定的主机接口。如果未设置连接类型,则使用 TCP。

      那么在这里,指定了将传入的UDP数据包从主机FWDPORT端口上转发到访客端口2000上。

  • -object filter-dump 表示将 netdev net0 上的网络流量转储到 packets.pcap文件中。每个数据包存储64k个字节(默认)。

main函数分析

在main函数中,添加了两个初始化的方法,如下所示

#ifdef LAB_NET
     pci_init();
     sockinit();
#endif

pci_init 方法是对pcie.0总线上的网卡设备的初始化,代码如下:

void
pci_init()
{
  // we'll place the e1000 registers at this address.
  // vm.c maps this range.
  uint64 e1000_regs = 0x40000000L;

  // qemu -machine virt puts PCIe config space here.
  // vm.c maps this range.
  uint32  *ecam = (uint32 *) 0x30000000L;

  // look at each possible PCI device on bus 0.
  for(int dev = 0; dev < 32; dev++){
    int bus = 0;
    int func = 0;
    int offset = 0;
    uint32 off = (bus << 16) | (dev << 11) | (func << 8) | (offset);
    volatile uint32 *base = ecam + off;
    uint32 id = base[0];

    // 100e:8086 is an e1000
    if(id == 0x100e8086){
      // command and status register.
      // bit 0 : I/O access enable
      // bit 1 : memory access enable
      // bit 2 : enable mastering
      base[1] = 7;
      __sync_synchronize();

      for(int i = 0; i < 6; i++){
        uint32 old = base[4+i];

        // writing all 1's to the BAR causes it to be
        // replaced with its size.
        base[4+i] = 0xffffffff;
        __sync_synchronize();

        base[4+i] = old;
      }

      // tell the e1000 to reveal its registers at
      // physical address 0x40000000.
      base[4+0] = e1000_regs;

      e1000_init((uint32*)e1000_regs);
    }
  }
}

在了解pci_init函数的功能之前,需要知道的储备知识是pci设备的配置空间,每个pic设备都有一个配置空间,大小为256B,实际上是一组连续的寄存器,其中头部64字节是PCI标准规定的,格式如下(图片来自维基百科)
在这里插入图片描述
剩余的部分是PCI设备自定义的。
其中DeviceID寄存器和VendorID寄存器标识设备ID和供应商ID;Status寄存器用于标识支持哪些功能以及是否发生了某些类型的错误。命令寄存器包含可以单独启用和禁用的功能的位掩码。

另外PCI配置空间头部有6个BAR(Base Address Registers),BAR记录了设备所需要的地址空间的类型(memory space或者I/O space),基址以及其他属性。PCI配置空间的初始值是由厂商预设在设备中的,于是设备需要哪些地址空间都是其自己定的,可能造成不同的PCI设备所映射的地址空间冲突,因此在PCI设备枚举(也叫总线枚举,由BIOS或者OS在启动时完成)的过程中,会重新为其分配地址空间,然后写入PCI配置空间中。

所以不难看出pci_init的前半部分功能是枚举总线上设备的配置空间,读取Device ID和Vendor ID从而确定e1000网卡设备,设置命令寄存器从而开启了设备可以响应I/O空间的访问,可以响应内存空间的访问,以及设备可以充当总线主控。
重点来讲一下对每个BAR设置0xffffffff的作用,这部分,查阅了很多,都没有讲,最后在维基百科中找到了答案,如下
在这里插入图片描述
也就是说,为了完成对BAR的配置,需要:

  • 对BAR[x]写入全1
  • 再从BAR[x]读出值,假设为y,通过~(0xffffff00 & y) + 1就是该BAR映射地址空间的大小
  • 确定完大小后写入地址空间的起始地址就完成了对BAR的配置

我们来验证一下这个过程,在确定每个BAR大小的for循环中加入对大小的输出语句
在这里插入图片描述
再次执行 make qemu 得到输出
在这里插入图片描述
其中0x20000就是BAR0的大小,0x100就是BAR1的大小,在这里我们只需要使用BAR0,也就是e1000_regs,就只在BAR0写入了起始地址,并且可以在vm.c的kvmmake中得到验证

// pci.c maps the e1000's registers here.
   kvmmap(kpgtbl, 0x40000000L, 0x40000000L, 0x20000, PTE_R | PTE_W);

这样,大概就可以理解这个函数的功能了,也就是设置了网卡中的一些寄存器值,并确定了BAR的大小,以及BAR0中的基址。这个BAR0存储的就是设备所需要空间的内存基址,在这里,也就是e1000_init函数中,我们可以看到,这段空间被用来设置网卡的各个控制位。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值