• 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
    
    • 1
    • 2
    • 3
    • 4

    查找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
    
    • 1
    • 2
    • 3
    • 4

    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);
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    在了解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);
    
    • 1
    • 2

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

  • 相关阅读:
    Python大数据之linux学习总结——day10_hive调优
    拉美国家货币汇率走强 其后继上涨形势将持续?
    mac os 渗透测试常用命令
    Python中的循环与可迭代对象
    重回童年4399| 【黄金矿工】游戏制作+解析
    Scrapy基本概念——Item Pipeline
    使用docker快速搭建jenkins
    微信小程序分包和预加载分包
    springboot+大学生就业规划系统 毕业设计-附源码191451
    Scalable Exact Inference in Multi-Output Gaussian Processes
  • 原文地址:https://blog.csdn.net/sscout/article/details/127854418