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