美文网首页
Lab 7 net: Network driver

Lab 7 net: Network driver

作者: 西部小笼包 | 来源:发表于2023-12-23 16:12 被阅读0次

    e1000 bottom part

    PLIC注册了网卡中断,分为2个步骤。
    步骤1. 设置PLIC 可以响应PCIE 的 IRQ

    #define E1000_IRQ 33
    
    for(int irq = 1; irq < 0x35; irq++){
        *(uint32*)(PLIC + irq*4) = 1;
      }
    

    步骤2. 让每个CPU 都对后32个IRQ感兴趣
    *(uint32*)(PLIC_SENABLE(hart)+4) = 0xffffffff;
    随后我们可以接受到中断,并且通过plic_claim 知道这是个来自PCIE的中断。

    else if(irq == E1000_IRQ){
          e1000_intr();
        }
    

    e1000_intr 会调用 e1000_recv 去接受网络包。

    随后开始走到net_rx(m),里面会判断这是一个IP 还是ARP的包。如果是IP则会调用net_rx_ip(m).

    这里面会处理IP层的HEADER,然后调用net_rx_udp(m, len, iphdr);

    这里解析UDP层的东西,调用sockrecvudp(m, sip, dport, sport);

    这里面就会唤醒,之前在sockread(struct sock *si, uint64 addr, int n) sleep的进程,告诉他们数据来了。不过不会等待,他会把数据放在mbufq中,mbufq_pushtail(&si->rxq, m);

    e1000 top part

    1. 当用户想向网卡发送数据,会调用 sockwrite
    2. 随后会把数据放进mbuf
    3. 调用void net_tx_udp(struct mbuf *m, uint32 dip,uint16 sport, uint16 dport)
    4. 加上udp的header,随后发给IP层 net_tx_ip(struct mbuf *m, uint8 proto, uint32 dip)
    5. 加上IP的header, 随后发给以太层net_tx_eth(m, ETHTYPE_IP);
    6. 把eth 的header 也放进mbuf里通过mbufpushhdr, 随后调用e1000_transmit(m)
    7. 然后我们需要把eth header 放进网卡的tx 描述符ring中,这样网卡就会发送这块数据了。

    网卡上的数据结构

    在上述过程里其实有2个缓存。

    1. socket_read sleep 醒来后,会用mbufq_pophead, 从buf里读出网络数据。 同时 sockrecvudp 会mbufq_pushtail 把数据放进缓存,然后wake。
    2. 当有数据要发送的时候,我们会把数据放到e1000网卡的缓存*tx_mbufs[TX_RING_SIZE]里。然后读的时候从*rx_mbufs[RX_RING_SIZE]缓存里取。

    qemu 模拟了2样东西。E1000 和 LAN。

    • E1000通常指的是Intel的一系列千兆位以太网控制器,它们被广泛用于网络通信设备。这些控制器支持高速数据传输,并且具有多种高级网络特性,比如DMA、中断处理和高级过滤功能。

    • LAN(局域网)是一个覆盖小范围区域(如家庭、学校、办公室或一组建筑物)的计算机网络。局域网允许连接的设备之间共享数据和资源,如打印机和文件服务器。它通常是通过有线(如以太网)或无线(如Wi-Fi)技术实现的。

    1701183712637.png

    这张图是E1000以太网控制器的内部架构块图。它展示了控制器如何组织其不同的功能模块。下面是各个部分的简要说明:

    • PCI Interface / PCI-X Core: 这是与PCI或PCI-X总线相连的接口部分,负责与主机的通信。
    • Host Arbiter: 主机仲裁器,负责管理不同组件之间的数据流动。
    • DMA Engine: 直接内存访问引擎,允许设备直接在RAM与设备间传输数据,无需CPU介入。
    • Packet Buffer: 数据包缓存区,临时存储从网络接收或待发送的数据包。
    • TX/RX MAC (Media Access Control): 分别处理发送和接收数据包的MAC地址识别和过滤。
    • Packet/Manageability Filter: 数据包和可管理性过滤器,用于提高网络数据的处理效率。
    • ASF Manageability: ASF(Alert Standard Format)可管理性支持,用于远程管理和控制。
    • SM Bus (System Management Bus): 系统管理总线,一个用于监控和控制的通信协议。
    • EEPROM / Flash: 存储设备的固件和配置设置。
    • RMON Statistics: 远程监控统计,用于网络监控和分析。
    • Link Interface: 链路接口,管理网络物理连接。
    • MDIO (Management Data Input/Output): 管理数据输入/输出接口,用于远程监控和配置网络设备。
    • TX Switch: 发送开关,负责管理和路由内部数据流,以确定数据包是发送到外部网络还是在设备内部处理。
    • GMII/MII (Gigabit Media Independent Interface / Media Independent Interface): 这些接口是介于以太网MAC层与物理层之间的标准接口。MII用于100Mbps或更低速率的连接,而GMII是MII的千兆位以太网版本。它们负责将数字数据流转换为物理层信号,反之亦然。

    网卡上的寄存器

    整个E1000网卡文档,分为3块。架构一块,了解即可。实际使用时,我们需要知道怎么再初始化时,去通过设置网卡指定的寄存器,来开关某些功能。最后一部分则是,通过怎么再发送数据时,设置对应的包头的位开关,来控制发送接受行为。
    这里我们讲寄存器部分:
    这里我们关注e1000_init函数:

    // Reset the device
      regs[E1000_IMS] = 0; // disable interrupts
      regs[E1000_CTL] |= E1000_CTL_RST;
      regs[E1000_IMS] = 0; // redisable interrupts
      __sync_synchronize();
    

    比如这块,我们看到它再设置这些字段,具体是什么意思,我们可以通过关键词去查阅文档。
    比如

    13.4.20 Interrupt Mask Set/Read Register
    如果相应的掩码位设置为1b,则使中断启用,如果设置为0b,则禁用。每当在此寄存器中的某个位被设置并且相应的中断条件发生时,都会生成一个PCI中断。中断条件的发生通过在“中断原因读取寄存器”(参见第13.4.17节)中设置一个位来反映。
    可以通过在此寄存器中的相应掩码位写入1b来启用特定的中断。用0b写入的任何位都不会改变。因此,如果软件希望禁用先前启用的特定中断条件,必须写入“中断屏蔽清除寄存器”(参见第13.4.21节),而不是在此寄存器中的位写入0b。

    依此类推,我们可以逐个查阅初始化函数里的每个寄存器和寄存器里的位的含义,读懂初始化代码的行为。

    网卡上的描述符

    描述符分为2种,rx_desctx_desc; 每一个描述符是1个128位的地址空间。可以从struct 这个类得到,每个地址空间代表的含义。当然文档上也会说明。
    比如说这段代码的含义:

    for (i = 0; i < TX_RING_SIZE; i++) {
        tx_ring[i].status = E1000_TXD_STAT_DD;
        tx_mbufs[i] = 0;
      }
    

    我们可以去查阅文档

    Table 3-11. Transmit Status Layout

    1703399698466.png

    代表这个描述符,已经完成,可以被下次使用。

    Lab7 Optional Challenge

    1.

    In this lab, the networking stack uses interrupts to handle ingress packet processing, but not egress packet processing. A more sophisticated strategy would be to queue egress packets in software and only provide a limited number to the NIC at any one time. You can then rely on TX interrupts to refill the transmit ring. Using this technique, it becomes possible to prioritize different types of egress traffic. (easy)

    查阅文档,找到E1000_TXD_CMD_IDE, interrupt delay 这个功能,可以做到缓存一波之后一起发送,那么为了做到缓存,我们需要额外维护一个待发送的包裹。
    在寄存器段,为了ENABLE 这个功能,我们需要配置打开E1000_TXDW 和 设置E1000_TADV

    1703400047370.png 1703400071251.png 1703400096814.png

    再在bottm的接受中断响应处,判断是发送完成中断和接受完成中断。

    void
    e1000_intr(void)
    {
      // tell the e1000 we've seen this interrupt;
      // without this the e1000 won't raise any
      // further interrupts.
      int reg = regs[E1000_ICR];
      // only reset the cause being triggered
      regs[E1000_ICR] = reg;
      // only provide a limited number to the NIC at any one time
      if (reg & E1000_TXDW)
        e1000_send();
      if (reg & E1000_RXDW)
        e1000_recv();
    }
    

    测试方案

    在发送端,增加打印输出,确保我们能够成功缓存并一次发送多个数据包

    void e1000_send()
    {
      int i = 0;
      acquire(&e1000_lock);
      while (!mbufq_empty(&txq)) {
        struct mbuf *cur = mbufq_pophead(&txq);
        if (e1000_transmit(cur))
          mbuffree(cur);
        i++;  
      }
      release(&e1000_lock);
      if (i > 1) printf("test pass: send multiple: %d\n", i);
    }
    
    1703400343815.png

    2

    The provided networking code only partially supports ARP. Implement a full ARP cache and wire it in to <tt>net_tx_eth()</tt>. (moderate)

    要完成这个挑战,主要就是熟读ARP的RFC,然后根据RFC的内容去实现代码。


    1703400467905.png

    增加ARP相关的函数:

    // arp ip 2 mac mapping
    struct arp_entry arp_cache[ARP_MAX_ENTRIES];
    int free_arp_idx = 0;
    
    // save request in queue which has no mapping in arp_cache
    static struct arpq *arpqs;
    
    // protect arp_cache and free_arp_idx
    struct spinlock arplock;
    // protect arpq
    struct spinlock arpqlock;
    
    static void net_tx_eth(struct mbuf *, uint16 , uint32 , uint16);
    
    void
    netinit(void)
    {
      initlock(&arplock, "arp_cache");
      initlock(&arpqlock, "arpq");
    }
    
    void add_arpq(struct mbuf *m, uint32 dip)
    {
      struct arpq *pos, *newarpq;
      acquire(&arpqlock);
      pos = arpqs;
      while (pos) {
        if (pos->ip == dip) {
          mbufq_pushtail(&pos->txq, m);
          release(&arpqlock);
          return;
        }
        pos = pos->next;
      }
      release(&arpqlock);
      if ((newarpq = (struct arpq*)kalloc()) == 0)
        return;
      newarpq->ip = dip;
      mbufq_init(&newarpq->txq);  
      newarpq->next = arpqs;
      mbufq_pushtail(&newarpq->txq, m);
      acquire(&arpqlock);
      arpqs = newarpq;
      release(&arpqlock);
    }
    
    void send_remove_arpq(uint32 dip)
    {
      struct arpq *pos, *pre = 0;
      acquire(&arpqlock);
      pos = arpqs;
      while (pos) {
        if (pos->ip == dip) {
          while(!mbufq_empty(&pos->txq))
          {
            struct mbuf *cur = mbufq_pophead(&pos->txq);
            net_tx_eth(cur, ETHTYPE_IP, dip, 0);
          }
          if (!pre) {
            arpqs = arpqs->next;
          } else {
            pre->next = pos->next;
          }
          kfree((void *)pos);
          break;
        }
        pre = pos;
        pos = pos->next;
      }
      release(&arpqlock);
    }
    
    uint8* arp_lookup(uint32 ip) {
      uint8 *res = 0;
      acquire(&arplock);
      int k = min(free_arp_idx, ARP_MAX_ENTRIES);
      for (int i = 0; i < k; i++) {
        if (arp_cache[i].ip == ip) {
          res = (uint8*)arp_cache[i].mac;
          break;
        }
      }
      release(&arplock);
      return res;
    }
    
    int update_arp(uint32 ip, uint8 *mac) {
      acquire(&arplock);
      int res = 0, k = min(free_arp_idx, ARP_MAX_ENTRIES);
      for (int i = 0; i < k; i++) {
        if (arp_cache[i].ip == ip) {
          memmove(arp_cache[i].mac, mac, sizeof(arp_cache[i].mac));
          res = 1;
          break;
        }
      }
      release(&arplock);
      return res;
    }
    
    void add_arp(uint32 ip, uint8* mac) {
      acquire(&arplock);
      free_arp_idx++;
      int k = (free_arp_idx-1) % ARP_MAX_ENTRIES;
      arp_cache[k].ip = ip;
      memmove(arp_cache[k].mac, mac, sizeof(arp_cache[k].mac));
      
      release(&arplock);
    }
    
    

    补全net_tx_eth,以实现完整的ARP

    arp的核心就是维护一个IP到MAC地址的映射关系,如果当前这个映射关系不存在,就没法知道要去的MAC地址,那么他会广播一个ARP消息去问谁有这个IP的MAC地址,当有一个端口发现自己的IP和这个ARP里的IP地址吻合,就会返回一个ARP RESPONSE,告诉他MAC地址。然后接受方,会把这个映射关系维护到自己的表里。

    当然有时候,我们发送的IP地址是外网的,这个时候,我们需要通过网关去和外网交互,这个时候会用IP路由表去确定,这个包的下一跳的IP是什么。所以这里实际查找的并不一定是最终IP地址的MAC地址;而是下一跳IP地址的MAC地址,所以我们这里模拟一个简单的IP路由。

    1703400786285.png 1703400819072.png

    当我们没有MAC地址,我们会暂时缓存本来要发的网络包进arpq里,等我们收到arp reply之后,我们有了实际的MAC地址,我们再把这些缓存住的包重新发送。

    1703401162952.png

    测试

    这块测试,比较简单,只要抓包,看一下能不能有ARP的交互即可。


    1703401233009.png

    3

    The E1000 supports multiple RX and TX rings. Configure the E1000 to provide a ring pair for each core and modify your networking stack to support multiple rings. Doing so has the potential to increase the throughput that your networking stack can support as well as reduce lock contention. (moderate), but difficult to test/measure

    我用multiple关键词搜索了下MIT提供的E1000的文档和e1000 multiqueue搜了下互联网的资料。并没有发现有关E1000如何enable 这个功能的说明。如果有小伙伴知道对应资料,可以留言告知。我会之后补上这块的实现。

    4

    uses a singly-linked list to find the destination socket, which is inefficient. Try using a hash table and RCU instead to increase performance. (easy), but a serious implementation would difficult to test/measure

    这块本质是一个数据结构的问题,用HASHTABLE去加速。在sysnet.c里增加下面4个方法。

    inline int hash(uint16 i) {
      return (i % SOCK_HASHTABLE_SIZE);
    }
    
    int sock_hashtable_add(uint16 lport, uint32 raddr, uint16 rport, struct sock *si)
    {
      int key = hash(si->lport);
      struct spinlock lock = locks[key];
      acquire(&lock);
      struct sock *pos = sockets[key];
      while (pos) {
        if (pos->lport == lport && si->raddr == raddr && si->rport == rport) {
          int is_tcp_accept_sock = (!raddr && !rport && si->tcpcb.parent); 
          if (!is_tcp_accept_sock) {
            release(&lock);
            return -1;
          }
        }
        pos = pos->next;
      }
      si->next = sockets[key];
      sockets[key] = si;
      release(&lock);
      return 0;
    }
    
    int sock_hashtable_remove(struct sock *si)
    {
      int key = hash(si->lport);
      struct spinlock lock = locks[key];
      acquire(&lock);
      struct sock **pos = &sockets[key];
      int res = -1;
      while (*pos) {
        if (*pos == si){
          *pos = si->next;
          res = 0;
          break;
        }
        pos = &(*pos)->next;
      }
      release(&lock);
      return res;
    }
    
    int sock_hashtable_update(struct sock *si, uint32 raddr, uint16 rport)
    {
      int key = hash(si->lport);
      struct spinlock lock = locks[key];
      acquire(&lock);
      struct sock *pos = sockets[key];
      int res = -1;
      while (pos) {
        if (pos == si){
          si->raddr = raddr;
          si->rport = rport;
          res = 0;
          break;
        }
        pos = pos->next;
      }
      release(&lock);
      return res;
    }
    
    int sock_hashtable_get(uint16 lport, uint32 raddr, uint16 rport, struct sock **ssi)
    {
      int key = hash(lport);
      
      struct spinlock lock = locks[key];
      acquire(&lock);
      struct sock *si = sockets[key];
      int backup = 0;
      while (si) {
        if (si->lport == lport) {
          if (si->raddr == raddr && si->rport == rport) {
            *ssi = si;
            release(&lock);
            return 0;
          } else if (!backup && si->raddr == 0 && si->rport == 0) {
            // in tcp, we assume accept sock should insert before its parent
            backup = 1;
            *ssi = si;
          }
        }
        si = si->next;
      }
      release(&lock);
      return backup ? 0 : -1;
    }
    

    修改sockrecvudp(), sockalloc, sockclose 使用上面的函数改写的逻辑:

    1703401721167.png

    5

    ICMP can provide notifications of failed networking flows. Detect these notifications and propagate them as errors through the socket system call interface.

    qemu user-mode network 不支持ICMP,如果非要实现这个功能,可以使用tap的网络模式。


    1703401922196.png

    不过user mode 进行一些修改,可以发送ICMP ECHO消息,我们可以实现下PING的功能。
    要解锁PING外部网络,需要查询自己的user group id:
    具体查询方式是:

    id
    

    我这里查到我的user group id 是1000
    我只需要这条命令

    sudo sh -c 'echo 1000 1000 > /proc/sys/net/ipv4/ping_group_range'
    

    即可解锁特权PING 的权限。
    有了这个之后,我们可以实现下PING这个功能。

    首先是客户端代码,主要就是用IP 去构建ICMP消息;我们这边没有实现标准sock create的系统调用,所有SOCK的创建是复用connect完成的。
    那我们为了支持ICMP,我们还需要定义SOCK_RAW

    #include "kernel/types.h"
    #include "kernel/net.h"
    #include "kernel/stat.h"
    #include "kernel/icmp.h"
    #include "user/user.h"
    
    int is_digit(char s) {
      return s >= '0' && s <= '9';
    }
    
    int to_int(char *s, int *num) {
      int result = 0;
      while (*s) {
        if (!is_digit(*s)) return -1; 
        result = result * 10 + (*s - '0');
        s++;
      }
      *num = result;
      return 0;
    }
    
    uint is_valid_ip(char *ip) {
      int segs = 0; 
      int seg_val = 0;
      char *temp = ip;
      if (!ip || *ip == '\0') {
          return 0; 
      }
      uint res = 0;
      int base = 24;
      while (*temp) {
        char *seg_start = temp;
        while (*temp && *temp != '.') {
            temp++;
        }
        char backup = *temp;
        *temp = '\0'; 
        if (to_int(seg_start, &seg_val) != 0 || seg_val < 0 || seg_val > 255) {
          *temp = backup;
          return 0;
        }
        res |= (seg_val) << base;
        base -= 8;
        *temp = backup; 
        if (*temp) {
            temp++;
        }
        segs++;
        if (segs > 4) break;
      }
      return (segs == 4) ? res : 0; 
    }
    
    uint16 checksum(void *b, int len) {    
        uint16 *buf = b;
        uint sum = 0;
        uint16 result;
    
        for (sum = 0; len > 1; len -= 2)
            sum += *buf++;
        if (len == 1)
            sum += *(unsigned char *)buf;
        sum = (sum >> 16) + (sum & 0xFFFF);
        sum += (sum >> 16);
        result = ~sum;
        return result;
    }
    
    int
    main(int argc, char *argv[])
    {
      int fd;
      uint32 dst;
      char obuf[8];
      char uri[128], path[60];
      
    
      if (argc == 1) {
        dst = (10 << 24) | (0 << 16) | (2 << 8) | (2 << 0);
      } else if (argc == 2) {
        if (!(dst = is_valid_ip(argv[1]))) {
          parseURL(argv[1], uri, path, 128, 60);
          dst = gethostbyname(uri);
          if (dst < 0) {
            printf("input addr invalid\n");
            exit(1);
          }
        }
      } else {
        printf("usage: %s <URL>\n", argv[0]);
        exit(1);
      }
      
      // build a raw sock
      if((fd = connect(dst, 0, 0, SOCK_RAW, SOCK_CLIENT)) < 0){
        fprintf(2, "ping: connect() failed\n");
        exit(1);
      }
    
      // build icmp header
      struct icmp *icmp = (struct icmp *)obuf;
      icmp->type = ICMP_ECHO;
      icmp->code = 0;
      icmp->checksum = 0;
      icmp->ih_id = htons(1);
      icmp->checksum = checksum(icmp, sizeof(struct icmp));
    
      struct {
        uint16 len;
        uint8 ttl;
      } echo_reply;
    
      // send icmp echo to dst
      for (int i = 1; i <= 3; i++) {
        icmp->ih_seq = htons(i);
        icmp->checksum = 0;
        icmp->checksum = checksum(icmp, sizeof(struct icmp));
        uint st = uptime();
        if(write(fd, obuf, sizeof(struct icmp)) < 0){
          fprintf(2, "ping: send() failed\n");
          exit(1);
        }
        int cc = read(fd, &echo_reply, 3);
        uint ed = uptime();
        if(cc != 3){
          fprintf(2, "ping: recv() failed\n");
          exit(1);
        }
        printf("%d bytes from %d.%d.%d.%d : icmp_seq=%d ttl=%d time=%d ticks\n", 
        echo_reply.len, 
        dst >> 24, (dst & 0x00ffffff) >> 16, (dst & 0x0000ffff) >> 8, (dst & 0x000000ff),
        ntohs(icmp->ih_seq) , echo_reply.ttl, ed-st);
      }
    
      close(fd);
      return 0;
    }
    

    接下来,实现下ICMP的几个方法

    #define ICMP_ECHOREPLY   0 // Echo Reply
    #define ICMP_ECHO 8 // Echo Request
    
    struct icmp {
        uint8 type; // Type of message
        uint8 code; // Type sub code
        uint16 checksum; // Ones complement checksum of the struct
        union {
            struct {
                uint16 id;
                uint16 seq;
            } echo; // For Echo Request and Echo Reply messages
            uint32 unused;
        } un;
    #define ih_id      un.echo.id
    #define ih_seq     un.echo.seq
    #define ih_unused  un.unused
    };
    
    #include "types.h"
    #include "param.h"
    #include "memlayout.h"
    #include "riscv.h"
    #include "spinlock.h"
    #include "sleeplock.h"
    #include "proc.h"
    #include "net.h"
    #include "tcp.h"
    #include "defs.h"
    #include "icmp.h"
    
    void
    net_tx_icmp(struct mbuf *m, int rip)
    {
        // SOCK_RAW only consider ICMP now
      net_tx_ip(m, IPPROTO_ICMP, rip);
    }
    
    void
    net_rx_icmp(struct mbuf *m, uint16 len, struct ip *iphdr)
    {
      struct icmp *icmphdr;
      uint8 type;
        
      icmphdr = mbufpullhdr(m, *icmphdr);
      if (!icmphdr)
        goto fail;
        len -= sizeof(*icmphdr);
        if (len > m->len)
        goto fail;
        uint16 padding = m->len - len;  
        mbuftrim(m, padding);
      // parse the necessary fields
      type = ntohs(icmphdr->type);
    
      if (type == ICMP_ECHOREPLY) {
            uint16 *recv_len = (uint16 *)mbufput(m, 2);
            *recv_len = (uint16)(padding + 42); // 20 ip, 14 eth, 8 icmp
            uint8 *ttl = (uint8 *)mbufput(m, 1);
            *ttl = iphdr->ip_ttl;
            sockrecvicmp(m, ntohl(iphdr->ip_src));
            return;
      } else {
            panic("net_rx_icmp");    
        }
    
    fail:
      mbuffree(m);
    }
    
    
    void
    sockrecvicmp(struct mbuf *m, uint32 raddr)
    {
      struct sock *si = 0;
      if (sock_hashtable_get(0, raddr, 0, &si) == 0)
        goto found;
      mbuffree(m);
      return;
    found:
      acquire(&si->lock);
      mbufq_pushtail(&si->rxq, m);
      wakeup(&si->rxq);
      release(&si->lock);
    }
    

    然后一些小改动:


    1708851146333.png
    1708851170968.png

    测试:

    1708851295787.png

    如何实现原要求

    首先我们需要在宿主机上配置TAP网络;然后把我们QEMU的USER MODE 切换到TAP MODE
    这里有一个代码例子,是搭配TAP网络来实现 从外部对内部的PING的
    https://github.com/pandax381/xv6-net/tree/net

    下面当我们收到一个类似"destination unreachable的ICMP错误消息之后;我们需要解析Original Data Datagram`

     0                   1                   2                   3
        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |     Type      |     Code      |          Checksum             |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                             unused                            |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |      Internet Header + 64 bits of Original Data Datagram      |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    
    1708851538347.png
    Original Data Datagram 我们可以取到 source port, 那么我们就可以知道是哪个socket出错了。
    然后我们就可以设置对应的errno, 然后唤醒用户程序read的阻塞等待。并返回-1.

    6

    The E1000 supports several stateless hardware offloads, including checksum calculation, RSC, and GRO. Use one or more of these offloads to increase the throughput of your networking stack. (moderate), but hard to test/measure

    这块比较坑爹,一开始我实现了想在收到包的时候自动计算checksum的功能,但是发现并没有效果。
    具体做法在e1000文档中的3.2.9 Receive Packet Checksum Offloading
    核心就是设置寄存器的RXCSUM.IPOFLDRXCSUM.TUOFLD

    For supported packet/frame types, the entire checksum calculation may be offloaded to the
    Ethernet controller. If RXCSUM.IPOFLD is set to 1b, the controller calculates the IP checksum
    and indicates a pass/fail condition to software by means of the IP Checksum Error bit
    (RDESC.IPE) in the ERROR field of the receive descriptor. Similarly, if the RXCSUM.TUOFLD
    is set to 1b, the Ethernet controller calculates the TCP or UDP checksum and indicates a pass/fail
    condition to software by means of the TCP/UDP Checksum Error bit (RDESC.TCPE). These error
    bits are valid when the respective status bits indicate the checksum was calculated for the packet
    (RDESC.IPCS and RDESC.TCPCS)

    然后通过检查,接受包的 RDESC.IPCS and RDESC.TCPCS是否被打开,如果打开了可以通过读取RDESC.TCPERDESC.IPE, 就可以知道返回包的CHECKSUM 是否是正确的做快速判断。

    然后TCPCSIPCS, 都要先看IXSM 是否为0. 如果为1,就代表,这个包无法自动计算checksum。

    1703402324934.png

    因为我们是标准的IPV4的包,应该是可以计算checksum的,但是我实际测试下来,这个IXSM 始终为1.

    1703402420116.png

    最后想到,这里的e1000是通过qemu e1000的软件代码去模拟的,如果软件没有完全还原硬件特性,这个值确实就被一直设置成了1.

    经过阅读https://github.com/qemu/qemu/blob/master/hw/net/e1000.c的代码,求证里面并未使用寄存器的RXCSUM.IPOFLDRXCSUM.TUOFLD 去handle ** Receive Packet Checksum Offloading**

    不过发现代码handle 了3.6 IP/TCP/UDP Transmit Checksum Offloading
    就实现一下吧。

    增加用到desc 常量

    1703402667331.png

    根据3.6 IP/TCP/UDP Transmit Checksum Offloading的描述, 改写e1000.c

    这里我们需要先发送一个context descriptor,去打开checksum offload, 以及告诉网卡硬件,对应的CHECKSUM OFFLOAD的参数;
    再发送一系列data descriptor 去传递数据的模式。
    这块知识对应了文档这2节:


    1703402809870.png

    对应发送context descriptor的函数为:

    int
    e1000_context_desc_transmit(struct mbuf *m)
    {
      int tdt = regs[E1000_TDT], tdh = regs[E1000_TDH];
      if ( (tdt + 1) % TX_RING_SIZE == tdh ||
        (tx_ring[tdt].status & E1000_TXD_STAT_DD) == 0) {
        release(&e1000_lock);
        return -1;
      }
      uint16 ipcse = 33; 
      uint32 ipcso = 24, ipcss = 14;
      tx_ring[tdt].addr = (ipcse << 16) | (ipcso << 8) | ipcss; //TUCSE, TUCSO, TUCSS, IPCSE, IPCSO, IPCSS
    
      int enableIp = 2;
      tx_ring[tdt].cmd = E1000_TXD_CMD_DEXT | E1000_TXD_CMD_RS | E1000_TXD_CMD_IDE | enableIp; //TUCMD
    
      if (m->checksum_offload & MBUF_CSUM_OFLD_TCP) {
        // Setting TUCSE field to 0b indicates that the checksum covers from TUCCS to the end of the packet.
        uint64 tucse = 0;
        uint64 tucso = 50, tucss = 34;
        tx_ring[tdt].addr |= ((tucse << 48) | (tucso << 40) | (tucss << 32));
    
        tx_ring[tdt].length = m->len; 
        int enabletcp = 1;
        tx_ring[tdt].cmd |= (enabletcp | E1000_TXD_CMD_TSE);
        tx_ring[tdt].special = TCP_CB_WND_SIZE; // MSS
        tx_ring[tdt].css = tucso + 4; // HDRLEN
      }
      
      regs[E1000_TDT] = (tdt + 1) % TX_RING_SIZE;
      return 0;
    }
    

    随后修改发送data desc的函数:

    int
    e1000_transmit(struct mbuf *m)
    {
    
      // the mbuf contains an ethernet frame; program it into
      // the TX descriptor ring so that the e1000 sends it. Stash
      // a pointer so that it can be freed after sending.
      //
    
      // struct tx_desc tx_ring[TX_RING_SIZE]
      
      if (m->checksum_offload & MBUF_CSUM_OFLD_ENABLE) {
        if (e1000_context_desc_transmit(m) < 0) return -1;
      }
    
      int tdt = regs[E1000_TDT], tdh = regs[E1000_TDH];
      // 然后检查环是否溢出。如果E1000_TXD_STAT_DD未在E1000_TDT索引的描述符中设置,
      // 则E1000尚未完成先前相应的传输请求,因此返回错误。
      if ( (tdt + 1) % TX_RING_SIZE == tdh ||
        (tx_ring[tdt].status & E1000_TXD_STAT_DD) == 0) {
        release(&e1000_lock);
        return -1;
      }
      // 使用 mbuffree()释放从该描述符传输的最后一个mbuf(如果有)
      if (tx_mbufs[tdt] != 0)
        mbuffree(tx_mbufs[tdt]);
      // 填写描述符
      tx_ring[tdt].addr = (uint64)m->head;
      tx_ring[tdt].length = m->len;
      tx_ring[tdt].cmd = E1000_TXD_CMD_RS | E1000_TXD_CMD_EOP | E1000_TXD_CMD_IDE;
      if (m->checksum_offload & MBUF_CSUM_OFLD_ENABLE) {
        tx_ring[tdt].cmd |= E1000_TXD_CMD_DEXT;
        tx_ring[tdt].cso = E1000_TXD_DTYP_D;
        tx_ring[tdt].css = E1000_TXD_POPTS_IXSM;
        if (m->checksum_offload & MBUF_CSUM_OFLD_TCP) {
          tx_ring[tdt].cmd |= E1000_TXD_CMD_TSE;
          tx_ring[tdt].css |= E1000_TXD_POPTS_TXSM;
        }
      }
      
      tx_mbufs[tdt] = m;
      regs[E1000_TDT] = (tdt + 1) % TX_RING_SIZE;
      return 0;
    }
    

    测试方案

    然后就是注释掉原本IP段的checksum代码,然后看硬件是否会自动帮我们算checksum


    1703402992841.png

    如果之前的测试依然能跑通,就说明你的代码正确了。因为IP CHECKSUM没算对,QEMU 是不会把你这个包成功发到你起的udp server的。

    当然你也可以通过wireshack 里的功能去验证IP CHECKSUM是否算对。

    1703403095559.png

    7

    The networking stack in this lab is susceptible to receive livelock. Using the material in lecture and the reading assignment, devise and implement a solution to fix it. (moderate), but hard to test.

    这个活锁问题,就是CPU大量时间都在处理网卡的中断,而没有时间取处理到来的数据。
    课上的解决方案就是,当我们接受到一个网络包,中断触发后,我们先关闭接受中断。然后循环读,直到没有新的数据进来后,我们再打开中断。

    这段代码比较容易:


    1708852234331.png

    8

    Implement a UDP server for xv6. (moderate)

    这个也不算太难,核心就是UDP SERVER 是根据发来的包的IP,确定发回的地址,所以需要扩展一些系统调用。为不能向原来那样一开始指定好端口,就只要复用 read, write 系统调用的做法了。

    1703403264052.png 1703403327136.png

    然后改写sockwrite, sockread , 前者可以支持传参ip,port进去。后者可以支持多返回ip,port 给user.

    写一个udp server 程序:

    #include "kernel/types.h"
    #include "kernel/net.h"
    #include "kernel/stat.h"
    #include "user/user.h"
    
    
    // UDP server
    int
    main(int argc, char *argv[])
    {
      if(argc <= 1){
        fprintf(2, "usage: udpserver pattern [port]\n");
        exit(1);
      }
    
      int fd, port;
      uint16 client_port;
      uint32 client_ip;
    
      port = atoi(argv[1]);
    
      printf("listening on port %d \n", port);
      if((fd = connect(0, port, 0, SOCK_DGRAM, SOCK_SERVER)) < 0){
        fprintf(2, "bind() failed\n");
        exit(1);
      }
      char ibuf[256];
      while (1) {
        int cc = recvfrom(fd, ibuf, sizeof(ibuf)-1, (uint32 *)&client_ip, (uint16 *)&client_port);
        if(cc < 0){
          fprintf(2, "recv() failed\n");
          exit(1);
        }
        ibuf[cc] = '\0';
        fprintf(1, "%s \n", ibuf);
    
        char *obuf = "this is the host!";
        if(sendto(fd, obuf, strlen(obuf), client_ip, client_port) < 0){
          fprintf(2, "send() failed\n");
          exit(1);
        }
      }
      close(fd);
      exit(0);
    }
    
    

    测试

    先在qemu里启动udpserver, 随后开一个新的窗口,朝localhost 发送udp 请求。
    nc -u localhost 26999

    1703403714964.png
    之所以2000,对应localhost 26999 是因为,在qemu usermode network启动参数中,我们设置了
    ifeq ($(LAB),net)
    QEMUOPTS += -netdev user,id=net0,hostfwd=udp::$(FWDPORT)-:2000...
    

    而这个FWDPORT,就是26999, 因为我的机器上id -u 是1000

    FWDPORT = $(shell expr `id -u` % 5000 + 25999)
    
    1703403846364.png

    9

    Implement a minimal TCP stack and download a web page. (hard)

    最后这个挑战,代码量非常大,要实现TCP的基本协议框架,包括3次握手,4次挥手,发送数据。才可以做到这个效果。
    同时也要借鉴nettests 里的 dns 方法先把域名转换为ip,然后再向目标ip地址发送tcp 请求。
    我们先看下tcpclient端的代码:

    #include "kernel/types.h"
    #include "kernel/net.h"
    #include "kernel/stat.h"
    #include "user/user.h"
    
    #define BUFFER_SIZE 1024
    
    int
    main(int argc, char *argv[])
    {
      int port = 2000;
      if (argc >= 2) port = atoi(argv[1]);
      int sock;
      uint32 dst = (10 << 24) | (0 << 16) | (2 << 8) | (2 << 0);
      //172.17.209.209
      // uint32 dst = (172 << 24) | (17 << 16) | (209 << 8) | (208 << 0);
      // Connect to the server
      if((sock = connect(dst, port, 26099, SOCK_STREAM, SOCK_CLIENT)) < 0){
        fprintf(2, "connect() failed\n");
        exit(1);
      }
      fprintf(2, "connect() succeed\n");
    
      char *obuf = "a message from xv6!";
      if(write(sock, obuf, strlen(obuf)) < 0){
        fprintf(2, "tcp ping: send() failed\n");
        exit(1);
      }
    
      char ibuf[128];
      int cc = read(sock, ibuf, sizeof(ibuf)-1);
      if(cc < 0){
        fprintf(2, "tcp ping: recv() failed\n");
        exit(1);
      }
      ibuf[cc] = '\0';
      printf("receive:%s\n", ibuf);
      close(sock);
      return 0;
    }
    

    然后我们用python 写一个tcp server,首先确保三次握手可以成功。这是一个非常有用的起点,只有3次握手成功,才可以继续开发后面的功能。

    import socket
    import sys
    
    
    def start_tcp_server(port):
        # 创建 socket 对象
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # addr = ('172.16.100.1', port)
        addr = ('localhost', port)
        # addr = ('172.17.209.209', port)
        # 绑定 IP 地址和端口
        server_socket.bind(addr)
    
        # 开始监听端口,设置最大连接数
        server_socket.listen(5)
        print(f"listening on {addr}")
    
        # 接受客户端连接
        while True:
            client_socket, addr = server_socket.accept()
            print(f"Connection from {addr} has been established.")
    
            # 接收客户端发送的数据
            while True:
                data = client_socket.recv(1024)
                if not data:
                    break
                print("Received from client:", data.decode())
                client_socket.send(data)
    
            # 关闭连接
            client_socket.close()
        server_socket.close()
        print("Connection closed.")
    
    # 运行服务器
    start_tcp_server(int(sys.argv[1]))
    

    那么当你成功完成了tcp 的基本操作后,我们实现的效果为:


    1703404555146.png 1703404638283.png 1703404699525.png

    一些hint

    1. 先从第一次握手开始实现,根据 TCP RFC,https://datatracker.ietf.org/doc/html/rfc793; 严格根据文档实现,极大减少出错概率。
    2. checksum 计算是必须的,不能跳过,不然 qemu 会选择直接丢弃checksum错误的tcp数据包; server端无法感应到任何数据
    3. 注意正确的wakeup的时机,得等到tcp状态转换到 establish 的时候
    4. 注意并发问题,使用正确的锁保护 会同时修改的数据。比如BOTTOM 端收到TCP网络包切换状态。发送端正在sleep; 确保唤醒后,依然持有正确的锁,防止race condition

    最终效果

    hart 2 starting
    hart 1 starting
    init: starting sh
    $ wget www.example.com
    path /, uri: www.example.com
    DNS arecord for www.example.com. is 93.184.216.34
    93 184 216 34
    connect success
    GET / HTTP/1.1
    Host: www.example.com
    Connection: close
    
    
    HTTP/1.1 200 OK
    Accept-Ranges: bytes
    Age: 427660
    Cache-Control: max-age=604800
    Content-Type: text/html; charset=UTF-8
    Date: Sun, 24 Dec 2023 08:05:49 GMT
    Etag: "3147526947"
    Expires: Sun, 31 Dec 2023 08:05:49 GMT
    Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
    Server: ECS (sac/2549)
    Vary: Accept-Encoding
    X-Cache: HIT
    Content-Length: 1256
    Connection: close
    
    <!doctype html>
    <html>
    <head>
        <title>Example Domain</title>
    
        <meta charset="utf-8" />
        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <style type="text/css">
        body {
            background-color: #f0f0f2;
            margin: 0;
            padding: 0;
            font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
    
        }
        div {
            width: 600px;
            margin: 5em auto;
            padding: 2em;
            background-color: #fdfdff;
            border-radius: 0.5em;
            box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
        }
        a:link, a:visited {
            color: #38488f;
            text-decoration: none;
        }
        @media (max-width: 700px) {
            div {
                margin: 0 auto;
                width: auto;
            }
        }
        </style>
    </head>
    
    <body>
    <div>
        <h1>Example Domain</h1>
        <p>This domain is for use in illustrative examples in documents. You may use this
        domain in literature without prior coordination or asking for permission.</p>
        <p><a href="https://www.iana.org/domains/example">More information...</a></p>
    </div>
    </body>
    </html>
    $
    
    1703405222578.png

    实现下TCP ECHO SERVER

    1703405294650.png

    这块代码量比较大,我就不一一展示;遇到困难的小伙伴,可以从我的github上获取,并对照着debug.
    https://github.com/yixuaz/6.1810-2023/tree/main

    相关文章

      网友评论

          本文标题:Lab 7 net: Network driver

          本文链接:https://www.haomeiwen.com/subject/icetgdtx.html