就像您在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 queue
与transmit 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
过程
RAL/RAH
) 进行编程。应始终使用RAL[0]/RAH[0]存储Ethernet controller的Individual Ethernet MAC address 。MTA
((Multicast Table Array)初始化为0b。每个软件都可以根据需要将entries添加到这个表中。IMS
) register 程序,以启用软件驱动程序希望在事件发生时得到通知的任何中断。建议的位包括RXT, RXO, RXDMT,RXSEQ, LSCreceive descriptor list
分配内存区域。软件应该确保该内存在(16字节)边界上对齐。编写 Receive Descriptor Base Address(RDBAL/RDBAH
) register(s)记录该区域的地址RDLEN
) register设置为descriptor ring的大小(以字节为单位
)。这个寄存器必须是128字节对齐的。RDH/RDT
)在通电(power-on)或软件启动以太网控制器复位后(由硬件)初始化为0b。RDH应该指向descriptor ring中第一个有效的接收描述符,尾应该指向descriptor ring中最后一个有效描述符之外的一个描述符。RCTL
) register进行编程。您现在可以对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);
}
在 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读取包前不会覆盖掉
web服务器以其最简单的形式将文件的内容发送给请求的client。我们在user/httpd.c中为一个非常简单的web服务器提供了框架代码。框架代码处理到来的连接并解析头文件。
Exercise 13. web服务器缺少处理将文件内容发送回客户机的代码。通过实现
send_file
和send_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
- What does the web page served by JOS’s web server say?
答:cheesy web page- How long approximately did it take you to do this lab?
答:拒绝回答这个问题,傲(diu)娇(ren)