Part A
exercise 1:
为kern/trap.c中的每个时钟中断添加对time_tick的调用。实现sys_time_msec并将其添加到kern/syscall.c中的syscall中,以便用户空间能够访问时间。
在kern/trap.c中添加分支
注意不要傻fufu的重新添加一个分支,在lab4中我们已经添加了时钟中断,此处只要修改一下即可
else if(tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER){ lapic_eoi(); time_tick(); sched_yield(); return; }
在kern/syscall.c中给syscall添加分支:
case(SYS_time_msec): return sys_time_msec();
实现sys_time_msec
在此之前,首先看一下kern/time.c中的几个函数:
static unsigned int ticks; void time_init(void) { ticks = 0; } // This should be called once per timer interrupt. A timer interrupt // fires every 10 ms. void time_tick(void) { ticks++; if (ticks * 10 < ticks) panic("time_tick: time overflowed"); } unsigned int time_msec(void) { return ticks * 10; }
ticks在time.c进行了定义。
所以不能在sys_time_msec中直接 ticks * 10,而是要调用time_msec(),否则会报错ticks未定义。
// Return the current time. static int sys_time_msec(void) { // LAB 6: Your code here. return time_msec(); //panic("sys_time_msec not implemented"); }
键入make INIT_CFLAGS=-DTEST_NO_NS run-testtime
来测试你的代码. 你应当看到进程在1秒内从5开始倒数,“-DTEST_NO_NS”会禁用启动网络服务器进程,因为它会引起panic。
exercise 2:
阅读文档
exercise 3:
首先看看kern/pci.c中的几个函数:
// pci_attach_class matches the class and subclass of a PCI device struct pci_driver pci_attach_class[] = { { PCI_CLASS_BRIDGE, PCI_SUBCLASS_BRIDGE_PCI, &pci_bridge_attach }, { 0, 0, 0 }, }; // pci_attach_vendor matches the vendor ID and device ID of a PCI device. key1 // and key2 should be the vendor ID and device ID respectively
//第一参数应该是venderID,第二个参数是deviseID
struct pci_driver pci_attach_vendor[] = { { PCI_E1000_VENDOR, PCI_E1000_DEVICE, &pci_e1000_attach }, { 0, 0, 0 }, };
pci_attach_class和pci_attach_vendor2个数组就是设备数组
对于pci设备而言
PCI是外围设备互连(Peripheral Component Interconnect)的简称,是在目前计算机系统中得到广泛应用的通用总线接口标准:
--- 在一个PCI系统中,最多可以有256根PCI总线,一般主机上只会用到其中很少的几条。
--- 在一根PCI总线上可以连接多个物理设备,可以是一个网卡、显卡或者声卡等,最多不超过32个。
--- 一个PCI物理设备可以有多个功能,比如同时提供视频解析和声音解析,最多可提供8个功能。
--- 每个功能对应1个256字节的PCI配置空间。
//负责设置需要读写的具体设备
static void pci_conf1_set_addr(uint32_t bus, uint32_t dev, uint32_t func, uint32_t offset) { assert(bus < 256); //8位 最多可以有256根PCI总线,一般主机上只会用到其中很少的几根 assert(dev < 32); //5位 一根PCI总线可以连接多个物理设备,可以是一个网卡、显卡或声卡等,最多不超过32个 assert(func < 8); //3位 一个PCI物理设备可以有多个功能,比如同时提供视频解析和声音解析,最多可提供8个功能。 assert(offset < 256); //8位 每个功能对应1个256字节的PCI配置空间。 assert((offset & 0x3) == 0);//最后两位必须为00? uint32_t v = (1 << 31) | // config-space (bus << 16) | (dev << 11) | (func << 8) | (offset); outl(pci_conf1_addr_ioport, v); }
//读取PCI配置空间中特定位置的配置值
static uint32_t pci_conf_read(struct pci_func *f, uint32_t off) { pci_conf1_set_addr(f->bus->busno, f->dev, f->func, off); return inl(pci_conf1_data_ioport); } //设置PCI配置空间中特定位置的配置值 static void pci_conf_write(struct pci_func *f, uint32_t off, uint32_t v) { pci_conf1_set_addr(f->bus->busno, f->dev, f->func, off); outl(pci_conf1_data_ioport, v); }
初始化PCI的大致流程:
在pci_init函数中,root_bus被全部清0,然后交给pci_scan_bus函数来扫描这条总线上的所有设备,说明在JOS中E1000网卡是连接在0号总线上的。pci_scan_bus函数来顺次查找0号总线上的32个设备,如果发现其存在,那么顺次扫描它们每个功能对应的配置地址空间,将一些关键的控制参数读入到pci_func中进行保存。得到pci_func函数后,被传入pci_attach函数去查找是否为已存在的设备,并用相应的初始化函数来初始化设备。
在手册的5.2节中可以找到venderID和deviceID:
82540EM-A 8086h 100E Desktop(台式机)
在kern/ PCI.c中的pci_attach_vendor数组中添加一个条目,以便在找到匹配的PCI设备时触发函数(请确保将它放在表示 末尾的{0,0,0}条目之前)
struct pci_driver pci_attach_vendor[] = { { PCI_VENDOR_ID, PCI_DEVICE_ID, &e1000_init }, { 0, 0, 0 }, };
前两个参数PCI_VENDOR_ID, PCI_DEVICE_ID的定义放在kern/pcireg.h文件中,这个文件是用来存放PCI的配置信息的
#define PCI_VENDOR_ID 0x8086 #define PCI_DEVICE_ID 0x100E
第三个参数应该是他初始化的时候应该调用的函数,可以定义在kern/e1000.c中
int e1000_init(struct pci_func *pcif) { pci_func_enable(pcif); return 1; }
然后在kern/e1000.h中声明这个函数,让pic.c进行调用
#include <kern/pci.h> int e1000_init(struct pci_func *pcif);
exercise 4 :
在kern/e1000.c中的函数添加:
int e1000_init(struct pci_func *pcif) { pci_func_enable(pcif); pci_e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]); return 1; }
我们使用变量pci_e1000来记录映射的位置,以便稍后访问刚才映射的寄存器。
如果要尝试打印设备状态寄存器
在 QEMU's e1000_hw.h 中有关于状态寄存器的定义。
#define E1000_STATUS 0x00008 /* Device Status - RO */
在kern/e1000.h中添加
#define E1000_STATUS 0x00008 /* Device Status - RO */ #define e1000_print_status(offset) cprintf("the E1000 status register: [%08x] ", *(pci_e1000+(offset>>2))); // 由于pci_e1000是uint32_t的,如果直接加offset,就相当于加了offset*sizeof(uint32_t) // 例如the E1000:[ef804000] offset:[00000008] sum:[ef804020]
// 并且定义 pci_e1000
uint32_t *pci_e1000;
修改
int e1000_init(struct pci_func *pcif) { pci_func_enable(pcif); pci_e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]); e1000_print_status(E1000_STATUS); return 1; }
exercise5:
在kern/e1000.c中:
void e1000_transmit_init(){ memset(tx_list, 0, sizeof(struct tx_desc) * TX_MAX); memset(tx_buf, 0, sizeof(struct packets) * TX_MAX); //每一个传输描述符都对应着一个packet for (int i = 0; i < TX_MAX; i++) { tx_list[i].addr = PADDR(tx_buf[i].buffer); //不太懂为什么可以用PADDR //24=4(16进制相对于2进制)*6 tx_list[i].cmd = (E1000_TXD_CMD_RS >> 24) | (E1000_TXD_CMD_EOP >> 24); //因为RSV除了82544GC/EI之外,所有以太网控制器都应该保留这个位,并将其编程为0b。 //而LC,EC在全双工没有意义, tx_list[i].status = E1000_TXD_STAT_DD; } //(TDBAL/TDBAH)指向传输描述符队列的base和high //将pci_e1000视为数组的话,他存放的元素应该是32位的, //以太网控制器中的寄存器都是32位的, pci_e1000[E1000_TDBAL >> 2] = PADDR(tx_list); pci_e1000[E1000_TDBAH >> 2] = 0; pci_e1000[E1000_TDLEN >> 2] = TX_MAX * sizeof(struct tx_desc); pci_e1000[E1000_TDH >> 2] = 0; pci_e1000[E1000_TDT >> 2] = 0; pci_e1000[E1000_TCTL >> 2] |= (E1000_TCTL_EN | E1000_TCTL_PSP | (E1000_TCTL_CT & (0x10 << 4)) | (E1000_TCTL_COLD & (0x40 << 12))); pci_e1000[E1000_TIPG >> 2] |= (10) | (4 << 10) | (6 << 20); }
int e1000_init(struct pci_func* pcif) { pci_func_enable(pcif); //pci_e1000 是1个指针,指向映射地址, uint32_t* pci_e1000; //他创建了一个虚拟内存映射 pci_e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]); e1000_print_status(E1000_STATUS); e1000_transmit_init();return 1; }
在kern/e1000.h中加入:
#include <inc/string.h> //以下定义来自 QEMU's e1000_hw.h #define E1000_TCTL 0x00400 /* TX Control - RW */ #define E1000_TDBAL 0x03800 /* TX Descriptor Base Address Low - RW */ #define E1000_TDBAH 0x03804 /* TX Descriptor Base Address High - RW */ #define E1000_TDLEN 0x03808 /* TX Descriptor Length - RW */ #define E1000_TDH 0x03810 /* TX Descriptor Head - RW */ #define E1000_TDT 0x03818 /* TX Descripotr Tail - RW */ #define E1000_TIPG 0x00410 /* TX Inter-packet gap -RW */ #define E1000_TCTL_EN 0x00000002 /* enable tx */ #define E1000_TCTL_BCE 0x00000004 /* busy check enable */ #define E1000_TCTL_PSP 0x00000008 /* pad short packets */ #define E1000_TCTL_CT 0x00000ff0 /* collision threshold */ #define E1000_TCTL_COLD 0x003ff000 /* collision distance */ #define E1000_TXD_CMD_RS 0x08000000 /* Report Status */ #define E1000_TXD_CMD_EOP 0x01000000 /* End of Packet */ #define E1000_TXD_STAT_DD 0x00000001 /* Descriptor Done */ #define TX_MAX 64 //legacy transmit descriptor format struct tx_desc { uint64_t addr; //Address of the transmit descriptor in the host memory uint16_t length; //The Checksum offset field indicates where to insert a TCP checksum if this mode is enabled. uint8_t cso; uint8_t cmd; uint8_t status; uint8_t css; //The Checksum start field (TDESC.CSS) indicates where to begin computing the checksum. uint16_t special; }__attribute__((packed));//告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐, //传输描述符数组 struct tx_desc tx_list[TX_MAX]; //以太网数据包的最大大小为1518字节,将其限制在2048方便对齐 struct packets { char buffer[2048]; }__attribute__((packed)); //缓冲区数组 struct packets tx_buf[TX_MAX];
void e1000_transmit_init();
运行make E1000_DEBUG=TXERR,TX qemu
在设置TDT register
时,应该会看到一条“e1000: tx disabled”消息(因为这是在设置TCTL.EN之前发生的),并且不再有“e1000”消息
exercise 6:
在kern/e1000.c中加入
int e1000_transmit(void* addr, int length) { //TDT是传输描述符数组的索引 int tail = pci_e1000[E1000_TDT >> 2]; //得到下一个描述符 struct tx_desc* tx_next = &tx_list[tail]; //如果包长度超出了,就直接截住 if (length > sizeof(struct packets)) length = sizeof(struct packets); //如果DD位被设置了,就说明这个传输描述符安全回收,可以传输下一个包,也就是队列还没有满 if ((tx_next->status & E1000_TXD_STAT_DD) == E1000_TXD_STAT_DD) { //没有满,把包复制到缓冲区里面去 memmove(KADDR(tx_next->addr), addr, length); tx_next->status &= !E1000_TXD_STAT_DD; //表示现在该描述符还没被处理 tx_next->length = (uint16_t)length; //更新TDT,注意是循环队列 pci_e1000[E1000_TDT >> 2] = (tail + 1) % TX_MAX; cprintf("my message:%s, %d, %02x ", tx_buf[tail].buffer, tx_list[tail].length, tx_list[tail].status); return 0; } //DD位没有被设置,传输队列满了 return -1; }
在kern/e1000.h中声明这个函数
在kern/monitor.c中添加kern/e1000.h头文件
然后:
void monitor(struct Trapframe *tf) { char *buf; cprintf("Welcome to the JOS kernel monitor! "); cprintf("Type 'help' for a list of commands. "); e1000_transmit("I'm here", 10); if (tf != NULL) print_trapframe(tf); while (1) { buf = readline("K> "); if (buf != NULL) if (runcmd(buf, tf) < 0) break; } }
在qemu中测试:
make E1000_DEBUG=TXERR,TX qemu
能得到以下结果:
e1000: index 0: 0x2b02c0 : 900000a 0 my message:I'm here, 10, 00
在qemu中测试:
tcpdump -XXnr qemu.pcap
得到以下结果:
reading from file qemu.pcap, link-type EN10MB (Ethernet) 16:09:40.990305 [|ether] 0x0000: 4927 6d20 6865 7265 004b I'm.here.K
exercise 7:
在kern/syscall.c中添加系统调用函数
//network packet send static int sys_packet_try_send(void *addr, size_t len){ user_mem_assert(curenv, addr, len, PTE_U); //考虑没这么周全 return e1000_transmit(addr, len); }
在diapstach中添加分支:
case (SYS_packet_try_send): return sys_packet_try_send((void *)a1,a2);
添加声明和定义:
//在kern/syscall.c中 #include<kern/e1000.h> //lib/syscall.c中 int sys_packet_try_send(void* buf, size_t len) { return syscall(SYS_packet_try_send, 0,(uint32_t)buf, len, 0,0,0); } //inc/syscall.h中要声明 SYS_packet_try_send, //inc/lib.h中
int sys_packet_try_send(void *data_va,size_t len);
exercise 8:
# include <inc/lib.h> void output(envid_t ns_envid) { binaryname = "ns_output"; int r; int perm; // LAB 6: Your code here: // - read a packet from the network server // - send the packet to the device driver envid_t from_env; while(1){ if( ipc_recv(&from_env, &nsipcbuf, &perm) != NSREQ_OUTPUT) continue; while((r = sys_packet_try_send(nsipcbuf.pkt.jp_data, nsipcbuf.pkt.jp_len)<0)) sys_yield(); } }
运行 make grade 可以看到part a的测试全部通过