tags: mit-6.828 osgit
本lab是6.828默认的最后一个实验,围绕网络展开。主要就作了一件事情。
从0实现网络驱动。
还提到一些比较重要的概念:github
从0开始写协议栈是很困难的,咱们将使用lwIP,轻量级的TCP/IP实现,更多lwIP信息能够参考lwIP官网。对于咱们来讲lwIP就像一个实现了BSD socket接口的黑盒,分别有一个包输入和输出端口。
JOS的网络网络服务由四个进程组成:
web
仔细看上图,绿颜色的部分是本lab须要实现的部分。分别是:数组
内核目前尚未时间的概念,硬件每隔10ms都会发送一个时钟中断。每次时钟中断,咱们能够给某个变量加一,来代表时间过去了10ms,具体实如今kern/time.c中。缓存
在kern/trap.c中添加对time_tick()调用。实现sys_time_msec()系统调用。sys_time_msec()能够配合sys_yield()实现sleep()(见user/testtime.c)。很简单,代码省略了。网络
编写驱动须要很深的硬件以及硬件接口知识,本lab会提供一些E1000比较表层的知识,你须要学会看E1000的开发者手册。数据结构
E1000是PCI设备,意味着E1000将插到主板上的PCI总线上。PCI总线有地址,数据,中断线容许CPU和PCI设备进行交互。PCI设备在被使用前须要被发现和初始化。发现的过程是遍历PCI总线寻找相应的设备。初始化的过程是分配I/O和内存空间,包括协商IRQ线。
咱们已经在kern/pic.c中提供了PCI代码。为了在启动阶段初始化PCI,PCI代码遍历PCI总线寻找设备,当它找到一个设备,便会读取该设备的厂商ID和设备ID,而后使用这两个值做为键搜索pci_attach_vendor数组,该数组由struct pci_driver结构组成。struct pci_driver结构以下:并发
struct pci_driver { uint32_t key1, key2; int (*attachfn) (struct pci_func *pcif); };
若是找到一个struct pci_driver结构,PCI代码将会执行struct pci_driver结构的attachfn函数指针指向的函数执行初始化。attachfn函数指针指向的函数传入一个struct pci_func结构指针。struct pci_func结构的结构以下:app
struct pci_func { struct pci_bus *bus; uint32_t dev; uint32_t func; uint32_t dev_id; uint32_t dev_class; uint32_t reg_base[6]; uint32_t reg_size[6]; uint8_t irq_line; };
其中reg_base数组保存了内存映射I/O的基地址, reg_size保存了以字节为单位的大小。 irq_line包含了IRQ线。
当attachfn函数指针指向的函数执行后,该设备就算被找到了,但尚未启用,attachfn函数指针指向的函数应该调用pci_func_enable(),该函数启动设备,协商资源,而且填充传入的struct pci_func结构。异步
实现attach函数来初始化E1000。在kern/pci.c的pci_attach_vendor数组中添加一个元素。82540EM的厂商ID和设备ID能够在手册5.2节找到。实验已经提供了kern/e1000.c和kern/e1000.h,补充这两个文件完成实验。添加一个函数,并将该函数地址添加到pci_attach_vendor这个数组中。
kern/e1000.c:
int e1000_attachfn(struct pci_func *pcif) { pci_func_enable(pcif); return 0; }
kern/pci.c:
struct pci_driver pci_attach_vendor[] = { { E1000_VENDER_ID_82540EM, E1000_DEV_ID_82540EM, &e1000_attachfn }, { 0, 0, 0 }, };
程序经过内存映射IO(MMIO)和E1000交互。经过MMIO这种方式,容许经过读写"memory"进行控制设备,这里的"memory"并不是DRAM,而是直接读写设备。pci_func_enable()协商MMIO范围,并将基地址和大小保存在基地址寄存器0(reg_base[0] and reg_size[0])中,这是一个物理地址范围,咱们须要经过虚拟地址来访问,因此须要建立一个新的内核内存映射。
使用mmio_map_region()创建内存映射。至此咱们能经过虚拟地址bar_va来访问E1000的寄存器。
volatile void *bar_va; #define E1000REG(offset) (void *)(bar_va + offset) int e1000_attachfn(struct pci_func *pcif) { pci_func_enable(pcif); bar_va = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]); //mmio_map_region()这个函数以前已经在kern/pmap.c中实现了。 //该函数从线性地址MMIOBASE开始映射物理地址pa开始的size大小的内存,并返回pa对应的线性地址。 uint32_t *status_reg = (uint32_t *)E1000REG(E1000_STATUS); assert(*status_reg == 0x80080783); return 0; }
lab3和lab4的结果是,咱们能够经过直接访问bar_va开始的内存区域来设置E1000的特性和工做方式。
什么是DMA?简单来讲就是容许外部设备直接访问内存,而不须要CPU参与。https://en.wikipedia.org/wiki/Direct_memory_access
咱们能够经过读写E1000的寄存器来发送和接收数据包,可是这种方式很是慢。E1000使用DMA直接读写内存,不须要CPU参与。驱动负责分配内存做为发送和接受队列,设置DMA描述符,配置E1000这些队列的位置,以后的操做都是异步的。
发送一个数据包:驱动将该数据包拷贝到发送队列中的一个DMA描述符中,通知E1000,E1000从发送队列的DMA描述符中拿到数据发送出去。
接收数据包:E1000将数据拷贝到接收队列的一个DMA描述符中,驱动能够从该DMA描述符中读取数据包。
发送和接收队列很是类似,都由DMA描述符组成,DMA描述符的确切结构不是固定的,可是都包含一些标志和包数据的物理地址。发送和接收队列能够由环形数组实现,都有一个头指针和一个尾指针。
这些数组的指针和描述符中的包缓冲地址都应该是物理地址,由于硬件操做DMA读写物理内存不须要经过MMU。
首先咱们须要初始化E1000来支持发送包。第一步是创建发送队列,队列的具体结构在3.4节,描述符的结构在3.3.3节。驱动必须为发送描述符数组和数据缓冲区域分配内存。有多种方式分配数据缓冲区。最简单的是在驱动初始化的时候就为每一个描述符分配一个对应的数据缓冲区。最大的包是1518字节。
发送队列和发送队列描述符以下:
更加详细的信息参见说明手册。
按照14.5节的描述初始化。步骤以下:
struct e1000_tdh *tdh; struct e1000_tdt *tdt; struct e1000_tx_desc tx_desc_array[TXDESCS]; char tx_buffer_array[TXDESCS][TX_PKT_SIZE]; static void e1000_transmit_init() { int i; for (i = 0; i < TXDESCS; i++) { tx_desc_array[i].addr = PADDR(tx_buffer_array[i]); tx_desc_array[i].cmd = 0; tx_desc_array[i].status |= E1000_TXD_STAT_DD; } //设置队列长度寄存器 struct e1000_tdlen *tdlen = (struct e1000_tdlen *)E1000REG(E1000_TDLEN); tdlen->len = TXDESCS; //设置队列基址低32位 uint32_t *tdbal = (uint32_t *)E1000REG(E1000_TDBAL); *tdbal = PADDR(tx_desc_array); //设置队列基址高32位 uint32_t *tdbah = (uint32_t *)E1000REG(E1000_TDBAH); *tdbah = 0; //设置头指针寄存器 tdh = (struct e1000_tdh *)E1000REG(E1000_TDH); tdh->tdh = 0; //设置尾指针寄存器 tdt = (struct e1000_tdt *)E1000REG(E1000_TDT); tdt->tdt = 0; //TCTL register struct e1000_tctl *tctl = (struct e1000_tctl *)E1000REG(E1000_TCTL); tctl->en = 1; tctl->psp = 1; tctl->ct = 0x10; tctl->cold = 0x40; //TIPG register struct e1000_tipg *tipg = (struct e1000_tipg *)E1000REG(E1000_TIPG); tipg->ipgt = 10; tipg->ipgr1 = 4; tipg->ipgr2 = 6; }
如今初始化已经完成,接着须要编写代码发送数据包,提供系统调用给用户代码使用。要发送一个数据包,须要将数据拷贝到数据下一个数据缓存区,而后更新TDT寄存器来通知网卡新的数据包已经就绪。
编写发送数据包的函数,处理好发送队列已满的状况。若是发送队列满了怎么办?
怎么检测发送队列已满:若是设置了发送描述符的RS位,那么当网卡发送了一个描述符指向的数据包后,会设置该描述符的DD位,经过这个标志位就能知道某个描述符是否能被回收。
检测到发送队列已满后怎么办:能够简单的丢弃准备发送的数据包。也能够告诉用户进程进程当前发送队列已满,请重试,就像sys_ipc_try_send()同样。咱们采用重试的方式。
int e1000_transmit(void *data, size_t len) { uint32_t current = tdt->tdt; //tail index in queue if(!(tx_desc_array[current].status & E1000_TXD_STAT_DD)) { return -E_TRANSMIT_RETRY; } tx_desc_array[current].length = len; tx_desc_array[current].status &= ~E1000_TXD_STAT_DD; tx_desc_array[current].cmd |= (E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS); memcpy(tx_buffer_array[current], data, len); uint32_t next = (current + 1) % TXDESCS; tdt->tdt = next; return 0; }
用一张图来总结下发送队列和接收队列,相信会清晰不少:
对于发送队列来讲是一个典型的生产者-消费者模型:
实现发送数据包的系统调用。很简单呀,不贴代码了。
输出协助进程的任务是,执行一个无限循环,在该循环中接收核心网络进程的IPC请求,解析该请求,而后使用系统调用发送数据。若是不理解,从新看看第一张图。
实现net/output.c.
void output(envid_t ns_envid) { binaryname = "ns_output"; // LAB 6: Your code here: // - read a packet from the network server // - send the packet to the device driver uint32_t whom; int perm; int32_t req; while (1) { req = ipc_recv((envid_t *)&whom, &nsipcbuf, &perm); //接收核心网络进程发来的请求 if (req != NSREQ_OUTPUT) { cprintf("not a nsreq output\n"); continue; } struct jif_pkt *pkt = &(nsipcbuf.pkt); while (sys_pkt_send(pkt->jp_data, pkt->jp_len) < 0) { //经过系统调用发送数据包 sys_yield(); } } }
有必要总结下发送数据包的流程,我画了个图,总的来讲仍是图一的细化:
总的来讲接收数据包和发送数据包很类似。直接看原文就行。
有必要总结下用户级线程实现。
具体实如今net/lwip/jos/arch/thread.c中。有几个重要的函数重点说下。
void thread_init(void) { threadq_init(&thread_queue); max_tid = 0; } static inline void threadq_init(struct thread_queue *tq) { tq->tq_first = 0; tq->tq_last = 0; }
初始化thread_queue全局变量。该变量维护两个thread_context结构指针。分别指向链表的头和尾。
线程相关数据结构:
struct thread_queue { struct thread_context *tq_first; struct thread_context *tq_last; }; struct thread_context { thread_id_t tc_tid; //线程id void *tc_stack_bottom; //线程栈 char tc_name[name_size]; //线程名 void (*tc_entry)(uint32_t); //线程指令地址 uint32_t tc_arg; //参数 struct jos_jmp_buf tc_jb; //CPU快照 volatile uint32_t *tc_wait_addr; volatile char tc_wakeup; void (*tc_onhalt[THREAD_NUM_ONHALT])(thread_id_t); int tc_nonhalt; struct thread_context *tc_queue_link; };
其中每一个thread_context结构对应一个线程,thread_queue结构维护两个thread_context指针,分别指向链表的头和尾。
int thread_create(thread_id_t *tid, const char *name, void (*entry)(uint32_t), uint32_t arg) { struct thread_context *tc = malloc(sizeof(struct thread_context)); //分配一个thread_context结构 if (!tc) return -E_NO_MEM; memset(tc, 0, sizeof(struct thread_context)); thread_set_name(tc, name); //设置线程名 tc->tc_tid = alloc_tid(); //线程id tc->tc_stack_bottom = malloc(stack_size); //每一个线程应该有独立的栈,可是一个进程的线程内存是共享的,由于共用一个页表。 if (!tc->tc_stack_bottom) { free(tc); return -E_NO_MEM; } void *stacktop = tc->tc_stack_bottom + stack_size; // Terminate stack unwinding stacktop = stacktop - 4; memset(stacktop, 0, 4); memset(&tc->tc_jb, 0, sizeof(tc->tc_jb)); tc->tc_jb.jb_esp = (uint32_t)stacktop; //eip快照 tc->tc_jb.jb_eip = (uint32_t)&thread_entry; //线程代码入口 tc->tc_entry = entry; tc->tc_arg = arg; //参数 threadq_push(&thread_queue, tc); //加入队列中 if (tid) *tid = tc->tc_tid; return 0; }
该函数很好理解,直接看注释就能看懂。
void thread_yield(void) { struct thread_context *next_tc = threadq_pop(&thread_queue); if (!next_tc) return; if (cur_tc) { if (jos_setjmp(&cur_tc->tc_jb) != 0) //保存当前线程的CPU状态到thread_context结构的tc_jb字段中。 return; threadq_push(&thread_queue, cur_tc); } cur_tc = next_tc; jos_longjmp(&cur_tc->tc_jb, 1); //将下一个线程对应的thread_context结构的tc_jb字段恢复到CPU继续执行 }
该函数保存当前进程的寄存器信息到thread_context结构的tc_jb字段中,而后从链表中取下一个thread_context结构,并将其tc_jb字段恢复到对应的寄存器中,继续执行。
jos_setjmp()和jos_longjmp()由汇编实现,由于要访问寄存器嘛。
ENTRY(jos_setjmp) movl 4(%esp), %ecx // jos_jmp_buf movl 0(%esp), %edx // %eip as pushed by call movl %edx, 0(%ecx) leal 4(%esp), %edx // where %esp will point when we return movl %edx, 4(%ecx) movl %ebp, 8(%ecx) movl %ebx, 12(%ecx) movl %esi, 16(%ecx) movl %edi, 20(%ecx) movl $0, %eax ret ENTRY(jos_longjmp) // %eax is the jos_jmp_buf* // %edx is the return value movl 0(%eax), %ecx // %eip movl 4(%eax), %esp movl 8(%eax), %ebp movl 12(%eax), %ebx movl 16(%eax), %esi movl 20(%eax), %edi movl %edx, %eax jmp *%ecx
最后老规矩
具体代码在:https://github.com/gatsbyd/mit_6.828_jos
若有错误,欢迎指正(*^_^*): 15313676365