lab6是實現(xiàn)網(wǎng)絡(luò)部分,代碼見 這里
1 QEMU 虛擬網(wǎng)絡(luò)
實驗中將使用到QEMU的用戶模式網(wǎng)絡(luò)棧,因為它不需要管理員權(quán)限。JOS中通過更新makefile來啟用QEMU的用戶模式的網(wǎng)絡(luò)棧以及虛擬的E1000網(wǎng)卡。
QEMU默認提供了一個在IP地址10.0.2.2上運行的虛擬路由器,它會為JOS分配一個IP地址10.0.2.15。為簡單起見,我們將這些默認值硬編碼到了 net/ns.h。
// net/ns.h
#define IP "10.0.2.15"
#define MASK "255.255.255.0"
#define DEFAULT "10.0.2.2"
雖然QEMU的虛擬網(wǎng)絡(luò)允許JOS與互聯(lián)網(wǎng)建立任意連接,但是JOS的IP地址10.0.2.15對于外部網(wǎng)絡(luò)來說并無意義(這是一個內(nèi)網(wǎng)地址,而QEMU就充當(dāng)了NAT的角色)。因此,我們無法直接連接到運行在JOS內(nèi)部的網(wǎng)絡(luò)服務(wù)器,即便是從運行QEMU的宿主機連接。為了解決該問題,我們將QEMU配置為在主機上的某個端口上運行服務(wù)器,該端口只需連接到JOS中的某個端口,并在真實主機和虛擬網(wǎng)絡(luò)之間傳送數(shù)據(jù)。你將在端口7(echo)和80(http)上運行JOS服務(wù)器。要查找QEMU在開發(fā)主機上轉(zhuǎn)發(fā)的端口,請運行make which-ports。
# make which-ports
Local port 26001 forwards to JOS port 7 (echo server)
Local port 26002 forwards to JOS port 80 (web server)
抓包
QEMU的虛擬網(wǎng)絡(luò)棧會將進出的數(shù)據(jù)包記錄到 qemu.pcap 文件中,可以通過tcpdump來查看。
tcpdump -XXnr qemu.pcap
2 網(wǎng)絡(luò)服務(wù)器
從頭開始編寫網(wǎng)絡(luò)堆棧很難。為此,我們將使用lwIP,一種開源輕量級包含了網(wǎng)絡(luò)棧的 TCP / IP協(xié)議套件。在這個實驗中,lwIP是一個黑盒子,它實現(xiàn)了一個BSD套接字接口,并有一個數(shù)據(jù)包輸入和輸出端口。
該網(wǎng)絡(luò)服務(wù)器實際上是下面四個進程組合,下圖展示了它們之間的關(guān)系。在本實驗中要完成綠色標記的四個部分。
- core network server environment(包括socket 調(diào)用分發(fā)和lwIP)
- input environment
- output environment
- timer environment

2.1 Core Network Server Environment
core network server 進程由socket調(diào)用和分發(fā)以及l(fā)wIP本身組成。其中調(diào)用和分發(fā)工作原理類似文件服務(wù)器。用戶進程使用stubs(lib/nsipc.c)發(fā)送IPC消息給core network server進程,對于每個用戶進程IPC,網(wǎng)絡(luò)服務(wù)器中的調(diào)度程序都會調(diào)用lwIP中提供的響應(yīng)的BSD套接字接口函數(shù)。
常規(guī)用戶進程并不直接使用nsipc_* 這樣調(diào)用,它們使用 lib/sockets.c 中的函數(shù)。sockets.c中提供了基于文件描述符的套接字API,用戶環(huán)境通過文件描述符引用套接字,就像它們引用磁盤文件一樣。有許多操作(connect, accept)對于socket的文件描述符是特有的,不過像read,write,close則是跟文件服務(wù)器一樣。
盡管看起來文件服務(wù)器和網(wǎng)絡(luò)服務(wù)器的IPC調(diào)度很相似,但存在一個關(guān)鍵的區(qū)別:accept和recv這樣的BSD套接字調(diào)用可以無限阻塞。如果調(diào)度器執(zhí)行一個阻塞式的調(diào)用,則調(diào)度器也會阻塞,并且整個系統(tǒng)一次只能有一個未完成的網(wǎng)絡(luò)調(diào)用,這是不可接受的,因此網(wǎng)絡(luò)服務(wù)器使用用戶級線程來避免阻塞整個服務(wù)器。對于每個傳入的IPC消息,調(diào)度器都會創(chuàng)建一個線程并在新創(chuàng)建的線程中處理該請求。如果線程阻塞,那么只有那個線程進入休眠狀態(tài),而其他線程繼續(xù)運行。此外,還有三個輔助進程,下面一一介紹。
2.2 Output Environment
當(dāng)lwIP接收用戶進程的socket調(diào)用時,它會生成用于網(wǎng)卡傳輸?shù)臄?shù)據(jù)包(如TCP/ARP包等)。lwIP使用NSREQ_OUTPUT IPC消息發(fā)送數(shù)據(jù)包到output進程(數(shù)據(jù)包通過IPC的頁共享)。output進程接收IPC消息,通過我們要實現(xiàn)的系統(tǒng)調(diào)用 sys_pkt_send 將數(shù)據(jù)包發(fā)送至網(wǎng)卡驅(qū)動中。
2.3 Input Environment
網(wǎng)卡接收的數(shù)據(jù)包需要導(dǎo)入到lwIP中。對網(wǎng)卡接收到的每個數(shù)據(jù)包,input進程將從內(nèi)核空間拉取數(shù)據(jù)包(通過我們實現(xiàn)的讀取數(shù)據(jù)包的系統(tǒng)調(diào)用 sys_pkt_receive),然后通過NSREQ_INPUT IPC消息將數(shù)據(jù)包發(fā)送到core network server進程中。
input進程的功能從core network server進程中分離出來是因為同時接收IPC以及接收或等待來自設(shè)備驅(qū)動的數(shù)據(jù)包對于JOS是非常困難的,因為JOS中沒有select這樣能夠允許進程監(jiān)聽多個輸入源并判斷輸入源是否已經(jīng)準備就緒。
2.4 Timer Environment
timer進程會定期向 core network server 進程發(fā)送 NSREQ_TIMER 的消息通知它某個計時器已經(jīng)過時,它用于實現(xiàn)各種網(wǎng)絡(luò)超時。
3 PCI接口、MMIO、DMA
以太網(wǎng)卡中數(shù)據(jù)鏈路層的芯片一般簡稱為MAC控制器,物理層的芯片簡稱為PHY。此外還有DMA,DMA會用到FIFO buffer,DMA用于提高傳輸效率,不用CPU控制,直接在網(wǎng)卡和主存之間傳輸數(shù)據(jù)。
EEPROM 用于存儲產(chǎn)品配置信息。分為幾個區(qū)域:
- 硬件訪問區(qū)域 - 加電后被網(wǎng)卡控制器加載,D3->D0傳輸。
- ASF訪問區(qū)域 - ASF模式啟動后加載。
- 軟件訪問區(qū)域。
PCI接口
pci_init時掃描總線讀取外設(shè)信息,通過VENDER_ID和DEVICE_ID在pci_attach()查找設(shè)備,如果找到了設(shè)備,則會調(diào)用對應(yīng)設(shè)備的attach函數(shù)初始化對應(yīng)設(shè)備,然后在 struct pci_func中填充讀取到的配置信息。其中82450EM的 VENDER_ID 為 0x8086,DEVICE ID為0x100e,在5.2中可以找到。reg_base和reg_size數(shù)組存儲Base Address Register(BAR)的信息,BAR的作用就是用于說明該設(shè)備想在主存中映射多少內(nèi)存空間和起始位置,一個網(wǎng)卡通常有6個32位的BAR或者3個64位的BAR。reg_base記錄了memory-mapped IO region的基內(nèi)存地址或者基IO端口,reg_size則記錄了reg_base對應(yīng)的內(nèi)存區(qū)域的大小或者IO端口的數(shù)目,irq_line是分配給設(shè)備中斷用的IRQ線。
在pci_scan_bus中會設(shè)置好pic_func的dev_id,dev_class,dev,bus等值,而reg_base,reg_size,irq_line則是需要通過設(shè)備的attach函數(shù)調(diào)用pci_func_enable()中來初始化。如實驗中的網(wǎng)卡的函數(shù)我們定義在 kern/e1000.c 中,名為 e1000_attach()。
MMIO
其中初始化了設(shè)備外,還要設(shè)置好MMIO映射,這里映射的物理地址是 reg_base[0](測試網(wǎng)卡的物理地址為 0xfebc0000),大小為 reg_size0,即我們映射了 BAR[0],第0個基地址寄存器,然后將MMIO映射的虛擬地址保存到一個全局變量中(映射虛擬地址是 0xef804000)。
struct pci_func {
struct pci_bus *bus; // Primary bus for bridges
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;
};
pci讀取總線獲取PCI設(shè)備配置的操作通過兩個IO端口實現(xiàn),一個是地址端口0xcf8,一個是數(shù)據(jù)端口0xcfc。具體通過 pci_conf_read 和 pci_conf_write 兩個函數(shù)實現(xiàn),沒有探究細節(jié)了,大致原理就是在對應(yīng)IO端口讀取寫入配置。
static uint32_t pci_conf1_addr_ioport = 0x0cf8;
static uint32_t pci_conf1_data_ioport = 0x0cfc;
DMA
可以想象的是,從E1000的寄存器來接收和傳輸數(shù)據(jù),效率會很低,而且要求E1000內(nèi)部來緩存數(shù)據(jù)包。為此,E1000采用了DMA來直接在網(wǎng)卡和主存之間傳輸數(shù)據(jù),而不用CPU的參與。驅(qū)動程序負責(zé)為發(fā)送隊列和接收隊列分配內(nèi)存,設(shè)置DMA描述符,并為E1000配置這些隊列的位置,之后的流程都是異步的。傳輸數(shù)據(jù)包時,驅(qū)動程序?qū)?shù)據(jù)包復(fù)制到傳輸隊列中的下一個DMA描述符中,并通知E1000另一個數(shù)據(jù)包可用,等到發(fā)送數(shù)據(jù)包的時候,E1000從DMA描述符復(fù)制出數(shù)據(jù)包。同樣,當(dāng)E1000接收到一個數(shù)據(jù)包時,它將它復(fù)制到接收隊列中的下一個DMA描述符中,驅(qū)動程序可以在下一次讀取它。
接收和發(fā)送隊列從頂層看來非常相似,兩者都由一系列描述符組成。盡管這些描述符的確切結(jié)構(gòu)各不相同,但每個描述符都包含一些標志和包含分組數(shù)據(jù)的緩沖區(qū)的物理地址(要么是網(wǎng)卡待發(fā)送的分組數(shù)據(jù),要么由操作系統(tǒng)分配的緩沖區(qū)以便網(wǎng)卡存入接收到的數(shù)據(jù)包)。
隊列實現(xiàn)為循環(huán)數(shù)組,這意味著當(dāng)網(wǎng)卡或驅(qū)動程序到達數(shù)組的末尾時,它會轉(zhuǎn)回到頭部。兩者都有一個頭指針header和一個尾指針tail,數(shù)組項是DMA描述符。網(wǎng)卡總是消耗來自頭部的描述符并移動頭指針,而驅(qū)動程序總是將DMA描述符添加到尾部并移動尾指針。傳輸隊列中的描述符表示等待發(fā)送的數(shù)據(jù)包(因此,在穩(wěn)定狀態(tài)下,傳輸隊列為空)。接收隊列中的描述符是網(wǎng)卡可以接收數(shù)據(jù)包的空閑描述符(因此,在穩(wěn)定狀態(tài)下,接收隊列由所有可用的接收描述符組成)。
這些數(shù)組指針以及描述符中數(shù)據(jù)包緩沖區(qū)的地址都必須是物理地址,因為硬件直接在物理內(nèi)存上執(zhí)行DMA,而不通過MMU,不經(jīng)過分頁轉(zhuǎn)換。
4 傳輸數(shù)據(jù)包
4.1 傳輸描述符格式和初始化
E1000的發(fā)送和接收數(shù)據(jù)包的功能基本是獨立的,因此我們可以分開來實現(xiàn)。我們首先實現(xiàn)傳輸數(shù)據(jù)包功能,因為如果不先實現(xiàn)傳輸功能我們無法測試接收數(shù)據(jù)包功能。
首先,我們要按照文檔14.5節(jié)中描述的步驟初始化要發(fā)送的網(wǎng)卡(不用過多關(guān)注細節(jié))。傳輸初始化的第一步是設(shè)置傳輸隊列。隊列的結(jié)構(gòu)在3.4節(jié)中描述,描述符的結(jié)構(gòu)在3.3.3節(jié)中描述。我們不會使用E1000的TCP offload功能,因此關(guān)注legacy transform descriptor format即可。
為描述E1000的結(jié)構(gòu),使用C語言中的結(jié)構(gòu)體十分方便。比如對于文檔3.3.3節(jié)表3-8中描述的legacy transform descriptor format:
63 48 47 40 39 32 31 24 23 16 15 0
+---------------------------------------------------------------+
| Buffer address |
+---------------+-------+-------+-------+-------+---------------+
| Special | CSS | Status| Cmd | CSO | Length |
+---------------+-------+-------+-------+-------+---------------+
發(fā)送描述符可以用下面的結(jié)構(gòu)體來描述:
struct tx_desc
{
uint64_t addr;
uint16_t length;
uint8_t cso;
uint8_t cmd;
uint8_t status;
uint8_t css;
uint16_t special;
};
你的驅(qū)動程序必須為發(fā)送描述符數(shù)組和發(fā)送描述符指向的數(shù)據(jù)包緩沖區(qū)保留內(nèi)存。有幾種方法可以做到這一點,如動態(tài)分配頁面或者簡單地在全局變量中聲明。無論哪種方式,請記住E1000直接訪問物理內(nèi)存,這意味著它訪問的任何緩沖區(qū)必須在物理內(nèi)存中連續(xù)。
還有多種方法來處理數(shù)據(jù)包緩沖區(qū)。比較簡單的方式是在驅(qū)動程序初始化期間為每個描述符保留數(shù)據(jù)包緩沖區(qū)的空間,并簡單地將數(shù)據(jù)包數(shù)據(jù)復(fù)制到這些預(yù)分配的緩沖區(qū)中。以太網(wǎng)數(shù)據(jù)包的最大為1518字節(jié),可以根據(jù)這個設(shè)置緩沖區(qū)的大小。更復(fù)雜的驅(qū)動程序可以動態(tài)地分配數(shù)據(jù)包緩沖區(qū)或者傳遞由用戶空間直接提供的緩沖區(qū)(稱為“零拷貝”的技術(shù))。
根據(jù)文檔14.5中描述完成網(wǎng)卡初始化。寄存器初始化參照文檔13章,傳輸描述符及其數(shù)組參照3.3.3和3.4節(jié)。注意傳輸描述符數(shù)組的對齊要求和數(shù)組長度限制。TDLEN必須是128字節(jié)對齊,每個傳輸描述符長度為16字節(jié),傳輸描述符數(shù)組的描述符數(shù)目必須是8的整數(shù)倍,不過不要超過64個,否則會影響ring overflow測試。對于TCTL.COLD,您可以認為是全雙工操作。
查看文檔14.5節(jié),可以看到網(wǎng)卡初始化步驟如下:
- 為發(fā)送描述符隊列分配一塊內(nèi)存,并設(shè)置傳輸描述符基地址寄存器(Transmit Descriptor Base Address,TDBAL/TDBAH) 為分配內(nèi)存的地址。
- 設(shè)置傳輸描述符長度寄存器(Transmit Descriptor Length,TDLEN)寄存器的值為描述符隊列的大小,必須128字節(jié)對齊。
- 設(shè)置發(fā)送描述符的header和tail指針為0.
- 根據(jù)需要初始化傳輸控制寄存器( Transmit Control Register, TCTL):
- 設(shè)置TCTL.EN位為1以支持常規(guī)操作。
- 設(shè)置 Pad Short Packets(TCTL.PSP) 為1.
- 設(shè)置 Collision Threshold(TCTL.CT)位為需要的值。以太網(wǎng)標準是設(shè)置為0x10,這個設(shè)置在半雙工模式中有用。
- 設(shè)置 Collision Distance (TCTL.COLD)為期望的值。在全雙工模式設(shè)置為0x40,在1000M半雙工網(wǎng)絡(luò)這個值設(shè)置為0x200,在10/100M半雙工設(shè)置為0x40,我們這里設(shè)置為0x40。
- 設(shè)置 Transmit IPG(TIPG)寄存器的IPGT,IPGR1和IPGR2的值。TIPG用于設(shè)置
legal Inter Packet Gap。TIPG設(shè)置參考13.4.34中的表13-77,分別將ipgt設(shè)置為10,ipgr1設(shè)為4(ipgr2的2/3),ipgr2設(shè)置為6。
根據(jù)要求來設(shè)置傳輸描述符和描述符數(shù)組,采用簡單點的方式,描述符數(shù)組和packet buffer全部采用數(shù)組方式。當(dāng)我們傳輸數(shù)據(jù)包時,如果設(shè)置了描述符的cmd參數(shù)為RS,則當(dāng)網(wǎng)卡發(fā)送完數(shù)據(jù)包時,會設(shè)置DD位,即設(shè)置描述符中的status對應(yīng)位,我們可以根據(jù)DD位來判斷當(dāng)前描述符是否可以重用,如果DD置位了,則表示可以回收并重新使用了。
傳輸數(shù)據(jù)包函數(shù)如果正確的話,make E1000_DEBUG=TXERR,TX run-net_testoutput會輸出如下,其中index是描述符數(shù)組索引,后面的0x302040是packet buffer地址,而9000009是cmd/CSO/length值(因為我們設(shè)置了RS和EOP位,所以cmd為8位0x09,CSO為8位0x00,length為16為0x0009,表示長度為9),0是special/CSS/status值。
Transmitting packet 1
e1000: index 0: 0x302040 : 9000009 0
Transmitting packet 2
e1000: index 1: 0x30262e : 9000009 0
Transmitting packet 3
e1000: index 2: 0x302c1c : 9000009 0
如果遇到很多"e1000: tx disabled"提示信息,則說明你的TCTL寄存器沒有設(shè)置正確。
4.2 output helper進程
現(xiàn)在在網(wǎng)卡驅(qū)動傳輸端有了一個系統(tǒng)調(diào)用,輸出進程的目標就是循環(huán)執(zhí)行下面操作:
- 從網(wǎng)絡(luò)服務(wù)器進程接收NSREQ_OUTPUT IPC消息
- 使用新添加的系統(tǒng)調(diào)用(sys_pkg_send)將IPC消息中附帶的數(shù)據(jù)包發(fā)送給網(wǎng)卡。
NSREQ_OUTPUT IPC消息是lwip的net/lwip/jos/jif/jif.c中的low_level_output發(fā)送的,IPC消息中會包含一個共享頁,這個頁內(nèi)容是一個union Nsipc,其中有一個struct jif_pkt pkt,而 jif_pkt結(jié)構(gòu)體定義如下,其中jp_len是數(shù)據(jù)包長度,而jp_data則是數(shù)據(jù)內(nèi)容。這里用到長度為0的數(shù)組的技巧。
struct jif_pkt {
int jp_len;
char jp_data[0];
};
注意網(wǎng)卡驅(qū)動,輸出進程以及網(wǎng)絡(luò)服務(wù)器進程之間的交互。當(dāng)網(wǎng)絡(luò)服務(wù)器進程通過IPC發(fā)送數(shù)據(jù)包給輸出進程時,如果此時因為網(wǎng)卡驅(qū)動沒有更多buffer導(dǎo)致輸出進程掛起,則網(wǎng)絡(luò)服務(wù)器進程必須阻塞等待。這里的流程是:
core network env -> output helper env -> e1000 driver
5 接收數(shù)據(jù)包及input helper進程
類似傳輸數(shù)據(jù)包,接下來完成接收數(shù)據(jù)包流程。這里要設(shè)置接收描述符和接收描述符隊列,接收描述符和隊列結(jié)構(gòu)在文檔3.2節(jié)描述,而初始化細節(jié)在 14.4節(jié)。
接收的描述符的隊列大小這里設(shè)置的是128個,另外,而E1000_RA 這個設(shè)置MAC地址時要注意,比如我們測試的MAC地址是 52:54:00:12:34:56,則ral處要設(shè)置為 0x12005452,而rah則要設(shè)置為 0x5634| E1000_RAH_AV,E1000_RAH_AV標識地址有效。
RDH寄存器指向網(wǎng)卡可存放數(shù)據(jù)包的第一個描述符,當(dāng)網(wǎng)卡接收到數(shù)據(jù)包時,會將數(shù)據(jù)包存入接收隊列,并將RDH寄存器的值加1,這個更新寄存器的值的操作是網(wǎng)卡硬件執(zhí)行的。
RDT寄存器則是存放的是網(wǎng)卡可用用來存放數(shù)據(jù)包的最后一個描述符的下一個描述符,這里我們設(shè)置為127,即浪費一個描述符作為標識,我們的隊列最多可以存放127個數(shù)據(jù)包。
而測試程序net/testinput.c主要是做了下面幾個事情:
- 創(chuàng)建一個子進程運行 output(),一個子進程運行input(),然后通過lwip構(gòu)建一個ARP報文并發(fā)送給output environment。ARP報文通過 ipc_send()發(fā)送NSREQ_OUTPUT類型的IPC消息給output 進程。
- output 進程接收到IPC消息后,會讀取IPC映射頁中數(shù)據(jù)包內(nèi)容,調(diào)用系統(tǒng)調(diào)用 sys_pkt_send() 將數(shù)據(jù)包發(fā)送到網(wǎng)卡的發(fā)送描述符隊列中,發(fā)送程序就是我們實現(xiàn)的 e1000_transmit()函數(shù)。
- 而當(dāng)網(wǎng)卡接收到ARP請求后,會響應(yīng)請求并輸出響應(yīng)到我們設(shè)置的接收描述符隊列中。
- 網(wǎng)卡輸出完畢后,會設(shè)置接收描述符的DD標記,此時input進程從接收描述符接收到網(wǎng)卡的數(shù)據(jù)包,并將其發(fā)送給core network server 進程。注意這里,每次接收后發(fā)送要間隔一段時間,因為網(wǎng)絡(luò)服務(wù)器進程讀取數(shù)據(jù)需要一定時間。
6 WEB服務(wù)器
最后是實現(xiàn)web服務(wù)器,類似httpd,主要完成send_file和send_data函數(shù)。實現(xiàn)就是根據(jù)請求解析出文件名,然后調(diào)用 fstat 獲取文件大小類型等元數(shù)據(jù),并調(diào)用readn讀取文件以及使用writen寫入文件數(shù)據(jù)到socket中。
注意這里的accept,bind等函數(shù)都是 lib/sockets.c中定義的,最終都是通過IPC功能將請求發(fā)送至 core network server進程(ns/serv.c),然后 core network server進程再調(diào)用的lwip來實現(xiàn)相關(guān)功能。這里用到了線程,線程實現(xiàn)在 net/lwip/jos/arch/thread.c中。