MIT6.828学习之Lab6_Part B: Receiving packets and the web server

Receiving Packets

就像您在transmitting packets时所做的一样,您必须配置E1000来接收数据包,并提供receive descriptor queue and receive descriptors。第3.2节描述包接收的工作原理,包括接收队列结构和接收描述符,第14.4节详细介绍了初始化过程。

Exercise 9. 阅读3.2节。您可以忽略关于 interrupts and checksum offloading的任何内容(如果您决定稍后使用这些特性,您可以返回到这些部分),并且您不必关心details of thresholds(临界值)以及card的内部缓存如何工作。

receive queuetransmit queue非常相似,除了它由等待被传入的包填充的empty packet buffers组成。当网络空闲时,传输队列是空的(因为所有包都已发送),而接收队列是满的(of empty packet buffers)。

当E1000接收到一个数据包时,它首先检查它是否匹配card配置的过滤器(filters)(例如,查看数据包是否指向这个E1000的MAC地址),如果数据包不匹配任何过滤器,则忽略它。否则,E1000将尝试从接收队列的头部检索下一个接收描述符。如果头(RDH)赶上了尾(RDT),那么接收队列就没有可用的描述符,因此卡将丢弃数据包。如果有一个空闲的接收描述符,它将包数据复制到描述符指向的缓冲区中,设置描述符的DD(Descriptor Done)和EOP(End of Packet) status bits,并增加RDH。(感觉RDH、RDT跟TDH、TDT是反着的?)

如果E1000在一个接收描述符中接收到一个大于packet buffer的包,它将从接收队列中检索尽可能多的描述符来存储包的全部内容。为了表明这已经发生,它将在所有这些描述符上设置DD状态位,但只在最后一个描述符上设置EOP status bit 。您可以在驱动程序中处理这种可能性,或者简单地将卡配置为不接受“long packets”(也称为巨型帧(jumbo frames)),并确保接收缓冲区足够大,可以存储尽可能大的standard Ethernet packet (1518字节)。

Exercise 10.

  • 按照第14.4节中的流程设置接收队列配置E1000。您不需要支持"long packets" or multicast。目前,不要将该卡配置为使用中断;如果您决定使用receive中断,您可以稍后更改它。另外,配置E1000来剥离(strip)Ethernet CRC,因为grade脚本希望剥离它。
  • 默认情况下,card将过滤掉所有数据包。您必须使用card自己的MAC地址来配置Receive Address Registers (RAL和RAH),以便接收发送到card的数据包。您可以简单地硬编码QEMU的默认MAC地址52:54:00:12:34:56(我们已经在lwIP中硬编码了这个地址,所以在这里这样做不会使事情变得更糟)。注意字节顺序;MAC地址是从低序字节写到高序字节的,因此52:54:00:12是MAC地址的 low-order 32位,34:56是high-order 16位。
  • E1000只支持一组特定的接收缓冲区大小(在13.4.22中的RCTL.BSIZE的描述中给出) 。如果您使接收包缓冲区足够大并禁用 long packets,您就不必担心跨多个接收缓冲区的数据包。另外,请记住,就像传输一样,接收队列和包缓冲区必须在物理内存中是连续的
  • 您应该使用至少128个接收描述符

14.4节中的Receive Initialization过程

  • 用所需的Ethernet addresses对Receive Address Register(s) (RAL/RAH) 进行编程。应始终使用RAL[0]/RAH[0]存储Ethernet controller的Individual Ethernet MAC address 。
  • MTA((Multicast Table Array)初始化为0b。每个软件都可以根据需要将entries添加到这个表中。
  • (忽略此步)编写 Interrupt Mask Set/Read (IMS) register 程序,以启用软件驱动程序希望在事件发生时得到通知的任何中断。建议的位包括RXT, RXO, RXDMT,RXSEQ, LSC
  • (忽略此步)如果软件使用 Receive Descriptor Minimum Threshold Interrupt,则使用Receive Delay Timer(RDTR) register初始化所需的延迟时间。
  • receive descriptor list分配内存区域。软件应该确保该内存在(16字节)边界上对齐。编写 Receive Descriptor Base Address(RDBAL/RDBAH) register(s)记录该区域的地址
  • 将Receive Descriptor Length (RDLEN) register设置为descriptor ring的大小(以字节为单位)。这个寄存器必须是128字节对齐的。
  • Receive Descriptor Head and Tail registers(RDH/RDT)在通电(power-on)或软件启动以太网控制器复位后(由硬件)初始化为0b。RDH应该指向descriptor ring中第一个有效的接收描述符,尾应该指向descriptor ring中最后一个有效描述符之外的一个描述符。
  • 用适当的值对 Receive Control (RCTL) register进行编程。
    Enable (RCTL.EN) = 1b,
    Long Packet Enable (RCTL.LPE) = 1b(我觉得应该禁掉),
    Loopback Mode (RCTL.LBM) = 00b,
    Broadcast Accept Mode (RCTL.BAM) = 1b,
    Receive Buffer Size (RCTL.BSIZE) bits = receive buffers = 2048,
    设置Strip Ethernet CRC (RCTL.SECRC) bit to strip the CRC

您现在可以对receive功能进行基本测试,即使不编写接收包的代码。运行make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER Run -net_testinput。testinput将传输一个ARP(Address Resolution Protocol地址解析协议)通知包(使用您的包传输系统调用),QEMU将自动应答该通知包。即使您的驱动程序还不能接收到此回复,您应该看到一条“e1000: unicast match[0]: 52:54:00:12:34:56”消息,指示e1000接收了一个数据包,并且匹配了配置的receive filter。

如果您看到"e1000: unicast mismatch: 52:54:00:12:34:56" 消息,则e1000过滤掉了数据包,这意味着您可能没有正确配置RAL和RAH。确保字节顺序正确,不要忘记在RAH中设置“Address Valid”位。如果您没有收到任何“e1000”消息,您可能没有正确地启用receive。

//kern/e1000.h
#define RX_MAX          128
#define E1000_RCTL_EN             0x00000002    /* enable */
#define E1000_RCTL_SBP            0x00000004    /* store bad packet */
#define E1000_RCTL_UPE            0x00000008    /* unicast promiscuous enable */
#define E1000_RCTL_MPE            0x00000010    /* multicast promiscuous enab */
#define E1000_RCTL_LPE            0x00000020    /* long packet enable */
#define E1000_RCTL_LBM_NO         0x00000000    /* no loopback mode */
#define E1000_RCTL_BAM            0x00008000    /* broadcast enable */
#define E1000_RCTL_SZ_2048        0x00000000    /* rx buffer size 2048 */
#define E1000_RCTL_SECRC          0x04000000    /* Strip Ethernet CRC */
#define E1000_RXD_STAT_DD       0x01    /* Descriptor Done */
#define E1000_RXD_STAT_EOP      0x02    /* End of Packet */
#define E1000_RCTL     0x00100  /* RX Control - RW */
#define E1000_RDBAL    0x02800  /* RX Descriptor Base Address Low - RW */
#define E1000_RDBAH    0x02804  /* RX Descriptor Base Address High - RW */
#define E1000_RDLEN    0x02808  /* RX Descriptor Length - RW */
#define E1000_RDH      0x02810  /* RX Descriptor Head - RW */
#define E1000_RDT      0x02818  /* RX Descriptor Tail - RW */

#define E1000_MTA      0x05200  /* Multicast Table Array - RW Array */
#define E1000_RA       0x05400  /* Receive Address - RW Array */
#define E1000_RAH_AV  0x80000000        /* Receive descriptor valid */


//kern/e1000.c
void
e1000_receive_init()
{
        for(int i=0; i<RX_MAX; i++){
                memset(&rx_list[i], 0, sizeof(struct rx_desc));
                memset(&rx_buf[i], 0, sizeof(struct packets));
                rx_list[i].addr = PADDR(rx_buf[i].buffer); //不太懂为什么可以用PADDR
                //rx_list[i].cmd = (E1000_TXD_CMD_EOP>>24) | (E1000_TXD_CMD_RS>>24); 
                //rx_list[i].status = E1000_TXD_STAT_DD; 
        }
        pci_e1000[E1000_MTA>>2] = 0;
        pci_e1000[E1000_RDBAL>>2] = PADDR(rx_list);
        pci_e1000[E1000_RDBAH>>2] = 0;
        pci_e1000[E1000_RDLEN>>2] = RX_MAX*sizeof(struct rx_desc);
        pci_e1000[E1000_RDH>>2] = 0;
        pci_e1000[E1000_RDT>>2] = RX_MAX + 1;
        //pci_e1000[E1000_TCTL>>2] |= 0x4010A;
        pci_e1000[E1000_RCTL>>2] = (E1000_RCTL_EN | E1000_RCTL_BAM |
                                    E1000_RCTL_LBM_NO | E1000_RCTL_SZ_2048 |
                                     E1000_RCTL_SECRC);
        //一开始这里没加0x,可能默认10进制,所以导致地址错误了
        pci_e1000[E1000_RA>>2] = 0x52 | (0x54<<8) | (0x00<<16) | (0x12<<24);
        pci_e1000[(E1000_RA>>2) + 1] = (0x34) | (0x56<<8) | E1000_RAH_AV;
}
e1000: unicast match[0]: 52:54:00:12:34:56

现在可以实现接收包了。要接收数据包,驱动程序必须跟踪下一个准备接收数据包的描述符(提示:根据您的设计,E1000中可能已经有一个寄存器记录了这一点)。与传输类似,文档声明RDH寄存器不能可靠地从软件中读取,因此,为了确定包是否已被发送到描述符的包缓冲区,您必须读取描述符中的DD状态位。如果设置了DD位,您可以从描述符的包缓冲区复制包数据,然后通过更新队列的尾部索引RDT告诉card描述符是空闲的。

如果没有设置了DD位的描述符,则没有收到包。这是接收端等效于传输队列已满时的情况,在这种情况下可以做几件事。

您可以简单地返回一个“try again”错误,并要求caller(要从接收队列拿数据的环境)重试。虽然这种方法适用于满的transmit queues ,因为这是一种瞬态条件,但是不适用于 receive queues,因为接收队列可能会在很长一段时间内保持空。

第二种方法是挂起调用环境,直到接收队列中有要处理的包为止。这个策略非常类似于sys_ipc_recv。就像在IPC中一样,因为每个CPU只有一个内核堆栈,所以一旦我们离开内核,堆栈上的状态就会丢失。我们需要设置一个标志,指示receive queue underflow已挂起一个环境,并记录系统调用参数。这种方法的缺点是复杂性:必须指示E1000生成接收中断,驱动程序必须处理这些中断,以便恢复阻塞的等待数据包的环境。

Exercise 11. 编写一个函数来接收来自E1000的数据包,并通过添加系统调用将其公开给用户空间。确保处理了接收队列为空的情况。

//kern/e1000.h
struct rx_desc
{
        uint64_t addr;
        uint16_t length;
        uint16_t pcs; //Packet Checksum
        uint8_t status;
        uint8_t errors;
        uint16_t special;
}__attribute__((packed));
struct rx_desc rx_list[RX_MAX];

int read_rxd_after_E1000_receive(void *addr);


//kern/e1000.c
int
read_rxd_after_E1000_receive(void *addr)
{
// 由于一开始所有的rx_desc都是free的,所以tail只好指着127
// 但是tail应该指着not free的描述符(即status==DD那种),所以要读应该从tail+1开始读
        int head = pci_e1000[E1000_RDH>>2];
        int tail = pci_e1000[E1000_RDT>>2];
        tail = (tail + 1) % RX_MAX;
        // 有效描述符指的是可供E1000存放接收到数据的描述符或者是说数据已经被读取过了
        struct rx_desc *rx_hold = &rx_list[tail];
        if((rx_hold->status & E1000_TXD_STAT_DD) == E1000_TXD_STAT_DD){
                int len = rx_hold->length;
                memcpy(addr, rx_buf[tail].buffer, len);
                rx_hold->status &= ~E1000_TXD_STAT_DD;
                pci_e1000[E1000_RDT>>2] = tail;
                //cprintf("hello %s\n", addr);
                return len;

        }
        //cprintf("hello wrong\n");
        return -1;
}


//kern/syscall.c 系统调用的其他就省略了,别忘了在inc/lib.h中声明哦
static int
sys_packet_recv(void *addr)
{
        user_mem_assert(curenv, addr, 2048, PTE_U);
        return read_rxd_after_E1000_receive(addr);
}

Receiving Packets: Network Server

network server input environment中,您将需要使用新的接收系统调用来接收数据包,并使用NSREQ_INPUT IPC消息将它们传递到 core network server environment。这些IPC输入消息应该有一个带有union Nsipc的页面,该页面的struct jif_pkt pkt字段由从网络接收到的包填充。

Exercise 12. Implement net/input.c.

void
input(envid_t ns_envid) 
{
        binaryname = "ns_input";

        // LAB 6: Your code here:
        //      - read a packet from the device driver
        //      - send it to the network server
        // Hint: When you IPC a page to the network server, it will be
        // reading from it for a while, so don't immediately receive
        // another packet in to the same physical page.
        char my_buf[2048];
        int length;
        while(1){
                while((length = sys_packet_recv(my_buf))<0)
                        sys_yield();
                nsipcbuf.pkt.jp_len=length;
                memcpy(nsipcbuf.pkt.jp_data, my_buf, length);
                ipc_send(ns_envid, NSREQ_INPUT, &nsipcbuf, PTE_U | PTE_P);
                for(int i=0; i<50000; i++)
                        if(i%1000==0)
                                sys_yield();
        }
}

想要实现第二种方法,挂起调用环境,但是我又不知道怎么才能在驱动程序中接受中断,所以为了避免注释里说的情况(在network server读取前接收另一个包,导致内容被覆盖),只好学着同学的加一些延时操作(那个for循环)。我之前是写的for(int i=0; i<50000; i++)结果还是有些包被覆盖了,如下图所示,所以还是得sys_yield()让出几次cpu才行

在这里插入图片描述
Run testinput again with make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-net_testinput. You should see

Sending ARP announcement...
Waiting for packets...
e1000: index 0: 0x26dea0 : 900002a 0
e1000: unicast match[0]: 52:54:00:12:34:56
input: 0000   5254 0012 3456 5255  0a00 0202 0806 0001
input: 0010   0800 0604 0002 5255  0a00 0202 0a00 0202
input: 0020   5254 0012 3456 0a00  020f 0000 0000 0000
input: 0030   0000 0000 0000 0000  0000 0000 0000 0000

以“input:”开头的行表示QEMU的ARP reply的己转储。

您的代码应该通过make grade的testinput测试。注意,如果不发送至少一个ARP packet来通知QEMU这JOS的IP地址,就无法测试数据包的接收,因此transmitting code中的bugs可能导致测试失败。

为了更彻底地测试您的网络代码,我们提供了一个名为echosrv的daemon(后台程序),它设置了一个运行在端口7上的echo server,该服务器将echo通过TCP连接发送的任何内容。使用make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-net_testinput启动一个终端中的echo服务器,并在另一个终端中使nc-7连接到它。

您键入的每一行都应该由该服务器echo back。每次仿真的E1000接收到数据包时,QEMU应该在控制台打印如下内容:

e1000: unicast match[0]: 52:54:00:12:34:56
e1000: index 2: 0x26ea7c : 9000036 0
e1000: index 3: 0x26f06a : 9000039 0
e1000: unicast match[0]: 52:54:00:12:34:56

此时,您还应该能够通过echosrv测试。

Question How did you structure your receive implementation? In particular, what do you do if the receive queue is empty and a user environment requests the next incoming packet?

答:和transmit implementation基本一致,把从receive queue的尾部后一个描述符对于的buffer内容读取出来,返回buffer的长度,如果receive queue为空,则返回-1。因为不会在驱动程序中添加中断,所以我还是采用try again方法,接收到-1返回值就sys_yield()让出cpu,下次获得cpu后再重试,直到返回值大于0。并且一次读取成功后,会指向for循环50000次,每1000次就sys_yield()一下,保证在network server读取包前不会覆盖掉

The Web Server

web服务器以其最简单的形式将文件的内容发送给请求的client。我们在user/httpd.c中为一个非常简单的web服务器提供了框架代码。框架代码处理到来的连接并解析头文件。

Exercise 13. web服务器缺少处理将文件内容发送回客户机的代码。通过实现send_filesend_data来完成web服务器。

//user/httpd.c/send_file
		···
		struct Stat st;
        //cprintf("hello send file\n");
        /*if ((r = fd_lookup(req->sock, &sfd))<0) //我还以为open的模式应该是sfd->fd_omode
                return send_error(req, 404); */
        if ((fd = open(req->url, O_RDONLY)) < 0) //尼吗,就一个括号扩错地方了,让我找了两个小时bug???
                return send_error(req, 404);
        if ((r = fstat(fd, &st)) < 0){
                r = send_error(req, 404);
                cprintf("hello fstat\n");
                goto end;
        }
        //cprintf("hello isdir %d\n", st.st_isdir);
        if (st.st_isdir){
                r =  send_error(req, 404);
                goto end;
        }
	    file_size = st.st_size;

        if ((r = send_header(req, 200)) < 0)
                goto end;
		···

//user/httpd.c/send_data
static int
send_data(struct http_request *req, int fd)
{
        // LAB 6: Your code here.
        int n;
        char buf[BUFFSIZE]; //不懂为啥,写PGSIZE就会出现页面错误,太大了?
        while((n = read(fd, buf, (long)sizeof(buf)))>0){
                if(write(req->sock, buf, n) != n)
                        die("Failed to send file to client");
        }       
        return n;       
        panic("send_data not implemented");

}

老是因为括号括回去错了耽误很多时间,真的想哭。。。比如if ((fd = open(req->url, O_RDONLY) < 0)),还有while((n = read(fd, buf, (long)sizeof(buf))>0))都是第二层括号把<0括进去了,心态爆炸。

完成web服务器之后,启动web服务器(make run-httpd-nox),并在您最喜欢的浏览器输入http://host:port/index。host是运行QEMU的计算机的名称,port是make which-ports为web服务器报告的端口号。您应该看到一个由运行在JOS中的HTTP服务器提供服务的web页面。
在这里插入图片描述
At this point, you should score 105/105 on make grade.

Question

  1. What does the web page served by JOS’s web server say?
    答:cheesy web page
  2. How long approximately did it take you to do this lab?
    答:拒绝回答这个问题,傲(diu)娇(ren)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值