mit6.828-lab6 網絡 1 QEMU 虛擬網絡 2 網絡服務器 3 PCI接口、MMIO、DMA 4 傳輸數據包 5 接收數據包及input helper進程 6 WEB服務器

lab6是實現網絡部分,代碼見 這裏

1 QEMU 虛擬網絡

實驗中將使用到QEMU的用戶模式網絡棧,因爲它不需要管理員權限。JOS中通過更新makefile來啓用QEMU的用戶模式的網絡棧以及虛擬的E1000網卡。

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的虛擬網絡允許JOS與互聯網建立任意連接,但是JOS的IP地址10.0.2.15對於外部網絡來說並無意義(這是一個內網地址,而QEMU就充當了NAT的角色)。因此,我們無法直接連接到運行在JOS內部的網絡服務器,即便是從運行QEMU的宿主機連接。爲了解決該問題,我們將QEMU配置爲在主機上的某個端口上運行服務器,該端口只需連接到JOS中的某個端口,並在真實主機和虛擬網絡之間傳送數據。你將在端口7(echo)和80(http)上運行JOS服務器。要查找QEMU在開發主機上轉發的端口,請運行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的虛擬網絡棧會將進出的數據包記錄到 qemu.pcap 文件中,可以通過tcpdump來查看。

tcpdump -XXnr qemu.pcap

2 網絡服務器

從頭開始編寫網絡堆棧很難。爲此,我們將使用lwIP,一種開源輕量級包含了網絡棧的 TCP / IP協議套件。在這個實驗中,lwIP是一個黑盒子,它實現了一個BSD套接字接口,並有一個數據包輸入和輸出端口。

該網絡服務器實際上是下面四個進程組合,下圖展示了它們之間的關係。在本實驗中要完成綠色標記的四個部分。

  • core network server environment(包括socket 調用分發和lwIP)
  • input environment
  • output environment
  • timer environment

2.1 Core Network Server Environment

core network server 進程由socket調用和分發以及lwIP本身組成。其中調用和分發工作原理類似文件服務器。用戶進程使用stubs(lib/nsipc.c)發送IPC消息給core network server進程,對於每個用戶進程IPC,網絡服務器中的調度程序都會調用lwIP中提供的響應的BSD套接字接口函數。

常規用戶進程並不直接使用nsipc_* 這樣調用,它們使用 lib/sockets.c 中的函數。sockets.c中提供了基於文件描述符的套接字API,用戶環境通過文件描述符引用套接字,就像它們引用磁盤文件一樣。有許多操作(connect, accept)對於socket的文件描述符是特有的,不過像read,write,close則是跟文件服務器一樣。

儘管看起來文件服務器和網絡服務器的IPC調度很相似,但存在一個關鍵的區別:accept和recv這樣的BSD套接字調用可以無限阻塞。如果調度器執行一個阻塞式的調用,則調度器也會阻塞,並且整個系統一次只能有一個未完成的網絡調用,這是不可接受的,因此網絡服務器使用用戶級線程來避免阻塞整個服務器。對於每個傳入的IPC消息,調度器都會創建一個線程並在新創建的線程中處理該請求。如果線程阻塞,那麼只有那個線程進入休眠狀態,而其他線程繼續運行。此外,還有三個輔助進程,下面一一介紹。

2.2 Output Environment

當lwIP接收用戶進程的socket調用時,它會生成用於網卡傳輸的數據包(如TCP/ARP包等)。lwIP使用NSREQ_OUTPUT IPC消息發送數據包到output進程(數據包通過IPC的頁共享)。output進程接收IPC消息,通過我們要實現的系統調用 sys_pkt_send 將數據包發送至網卡驅動中。

2.3 Input Environment

網卡接收的數據包需要導入到lwIP中。對網卡接收到的每個數據包,input進程將從內核空間拉取數據包(通過我們實現的讀取數據包的系統調用 sys_pkt_receive),然後通過NSREQ_INPUT IPC消息將數據包發送到core network server進程中。

input進程的功能從core network server進程中分離出來是因爲同時接收IPC以及接收或等待來自設備驅動的數據包對於JOS是非常困難的,因爲JOS中沒有select這樣能夠允許進程監聽多個輸入源並判斷輸入源是否已經準備就緒。

2.4 Timer Environment

timer進程會定期向 core network server 進程發送 NSREQ_TIMER 的消息通知它某個計時器已經過時,它用於實現各種網絡超時。

3 PCI接口、MMIO、DMA

以太網卡中數據鏈路層的芯片一般簡稱爲MAC控制器,物理層的芯片簡稱爲PHY。此外還有DMA,DMA會用到FIFO buffer,DMA用於提高傳輸效率,不用CPU控制,直接在網卡和主存之間傳輸數據。

EEPROM 用於存儲產品配置信息。分爲幾個區域:

  • 硬件訪問區域 - 加電後被網卡控制器加載,D3->D0傳輸。
  • ASF訪問區域 - ASF模式啓動後加載。
  • 軟件訪問區域。

PCI接口

pci_init時掃描總線讀取外設信息,通過VENDER_ID和DEVICE_ID在pci_attach()查找設備,如果找到了設備,則會調用對應設備的attach函數初始化對應設備,然後在 struct pci_func中填充讀取到的配置信息。其中82450EM的 VENDER_ID 爲 0x8086,DEVICE ID爲0x100e,在5.2中可以找到。reg_base和reg_size數組存儲Base Address Register(BAR)的信息,BAR的作用就是用於說明該設備想在主存中映射多少內存空間和起始位置,一個網卡通常有6個32位的BAR或者3個64位的BAR。reg_base記錄了memory-mapped IO region的基內存地址或者基IO端口,reg_size則記錄了reg_base對應的內存區域的大小或者IO端口的數目,irq_line是分配給設備中斷用的IRQ線。

在pci_scan_bus中會設置好pic_func的dev_id,dev_class,dev,bus等值,而reg_base,reg_size,irq_line則是需要通過設備的attach函數調用pci_func_enable()中來初始化。如實驗中的網卡的函數我們定義在 kern/e1000.c 中,名爲 e1000_attach()

MMIO

其中初始化了設備外,還要設置好MMIO映射,這裏映射的物理地址是 reg_base[0](測試網卡的物理地址爲 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設備配置的操作通過兩個IO端口實現,一個是地址端口0xcf8,一個是數據端口0xcfc。具體通過 pci_conf_read 和 pci_conf_write 兩個函數實現,沒有探究細節了,大致原理就是在對應IO端口讀取寫入配置。

 static uint32_t pci_conf1_addr_ioport = 0x0cf8;
 static uint32_t pci_conf1_data_ioport = 0x0cfc;   

DMA

可以想象的是,從E1000的寄存器來接收和傳輸數據,效率會很低,而且要求E1000內部來緩存數據包。爲此,E1000採用了DMA來直接在網卡和主存之間傳輸數據,而不用CPU的參與。驅動程序負責爲發送隊列和接收隊列分配內存,設置DMA描述符,併爲E1000配置這些隊列的位置,之後的流程都是異步的。傳輸數據包時,驅動程序將數據包複製到傳輸隊列中的下一個DMA描述符中,並通知E1000另一個數據包可用,等到發送數據包的時候,E1000從DMA描述符複製出數據包。同樣,當E1000接收到一個數據包時,它將它複製到接收隊列中的下一個DMA描述符中,驅動程序可以在下一次讀取它。

接收和發送隊列從頂層看來非常相似,兩者都由一系列描述符組成。儘管這些描述符的確切結構各不相同,但每個描述符都包含一些標誌和包含分組數據的緩衝區的物理地址(要麼是網卡待發送的分組數據,要麼由操作系統分配的緩衝區以便網卡存入接收到的數據包)。

隊列實現爲循環數組,這意味着當網卡或驅動程序到達數組的末尾時,它會轉回到頭部。兩者都有一個頭指針header和一個尾指針tail,數組項是DMA描述符。網卡總是消耗來自頭部的描述符並移動頭指針,而驅動程序總是將DMA描述符添加到尾部並移動尾指針。傳輸隊列中的描述符表示等待發送的數據包(因此,在穩定狀態下,傳輸隊列爲空)。接收隊列中的描述符是網卡可以接收數據包的空閒描述符(因此,在穩定狀態下,接收隊列由所有可用的接收描述符組成)。

這些數組指針以及描述符中數據包緩衝區的地址都必須是物理地址,因爲硬件直接在物理內存上執行DMA,而不通過MMU,不經過分頁轉換。

4 傳輸數據包

4.1 傳輸描述符格式和初始化

E1000的發送和接收數據包的功能基本是獨立的,因此我們可以分開來實現。我們首先實現傳輸數據包功能,因爲如果不先實現傳輸功能我們無法測試接收數據包功能。

首先,我們要按照文檔14.5節中描述的步驟初始化要發送的網卡(不用過多關注細節)。傳輸初始化的第一步是設置傳輸隊列。隊列的結構在3.4節中描述,描述符的結構在3.3.3節中描述。我們不會使用E1000的TCP offload功能,因此關注legacy transform descriptor format即可。

爲描述E1000的結構,使用C語言中的結構體十分方便。比如對於文檔3.3.3節表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     |
  +---------------+-------+-------+-------+-------+---------------+

發送描述符可以用下面的結構體來描述:

struct tx_desc
{
    uint64_t addr;
    uint16_t length;
    uint8_t cso;
    uint8_t cmd;
    uint8_t status;
    uint8_t css;
    uint16_t special;
};

你的驅動程序必須爲發送描述符數組和發送描述符指向的數據包緩衝區保留內存。有幾種方法可以做到這一點,如動態分配頁面或者簡單地在全局變量中聲明。無論哪種方式,請記住E1000直接訪問物理內存,這意味着它訪問的任何緩衝區必須在物理內存中連續。

還有多種方法來處理數據包緩衝區。比較簡單的方式是在驅動程序初始化期間爲每個描述符保留數據包緩衝區的空間,並簡單地將數據包數據複製到這些預分配的緩衝區中。以太網數據包的最大爲1518字節,可以根據這個設置緩衝區的大小。更復雜的驅動程序可以動態地分配數據包緩衝區或者傳遞由用戶空間直接提供的緩衝區(稱爲“零拷貝”的技術)。

根據文檔14.5中描述完成網卡初始化。寄存器初始化參照文檔13章,傳輸描述符及其數組參照3.3.3和3.4節。注意傳輸描述符數組的對齊要求和數組長度限制。TDLEN必須是128字節對齊,每個傳輸描述符長度爲16字節,傳輸描述符數組的描述符數目必須是8的整數倍,不過不要超過64個,否則會影響ring overflow測試。對於TCTL.COLD,您可以認爲是全雙工操作。

查看文檔14.5節,可以看到網卡初始化步驟如下:

  • 爲發送描述符隊列分配一塊內存,並設置傳輸描述符基地址寄存器(Transmit Descriptor Base Address,TDBAL/TDBAH) 爲分配內存的地址。
  • 設置傳輸描述符長度寄存器(Transmit Descriptor Length,TDLEN)寄存器的值爲描述符隊列的大小,必須128字節對齊。
  • 設置發送描述符的header和tail指針爲0.
  • 根據需要初始化傳輸控制寄存器( Transmit Control Register, TCTL):
    • 設置TCTL.EN位爲1以支持常規操作。
    • 設置 Pad Short Packets(TCTL.PSP) 爲1.
    • 設置 Collision Threshold(TCTL.CT)位爲需要的值。以太網標準是設置爲0x10,這個設置在半雙工模式中有用。
    • 設置 Collision Distance (TCTL.COLD)爲期望的值。在全雙工模式設置爲0x40,在1000M半雙工網絡這個值設置爲0x200,在10/100M半雙工設置爲0x40,我們這裏設置爲0x40。
  • 設置 Transmit IPG(TIPG)寄存器的IPGT,IPGR1和IPGR2的值。TIPG用於設置legal Inter Packet Gap。TIPG設置參考13.4.34中的表13-77,分別將ipgt設置爲10,ipgr1設爲4(ipgr2的2/3),ipgr2設置爲6。

根據要求來設置傳輸描述符和描述符數組,採用簡單點的方式,描述符數組和packet buffer全部採用數組方式。當我們傳輸數據包時,如果設置了描述符的cmd參數爲RS,則當網卡發送完數據包時,會設置DD位,即設置描述符中的status對應位,我們可以根據DD位來判斷當前描述符是否可以重用,如果DD置位了,則表示可以回收並重新使用了。

傳輸數據包函數如果正確的話,make E1000_DEBUG=TXERR,TX run-net_testoutput會輸出如下,其中index是描述符數組索引,後面的0x302040是packet buffer地址,而9000009是cmd/CSO/length值(因爲我們設置了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寄存器沒有設置正確。

4.2 output helper進程

現在在網卡驅動傳輸端有了一個系統調用,輸出進程的目標就是循環執行下面操作:

  • 從網絡服務器進程接收NSREQ_OUTPUT IPC消息
  • 使用新添加的系統調用(sys_pkg_send)將IPC消息中附帶的數據包發送給網卡。

NSREQ_OUTPUT IPC消息是lwip的net/lwip/jos/jif/jif.c中的low_level_output發送的,IPC消息中會包含一個共享頁,這個頁內容是一個union Nsipc,其中有一個struct jif_pkt pkt,而 jif_pkt結構體定義如下,其中jp_len是數據包長度,而jp_data則是數據內容。這裏用到長度爲0的數組的技巧。

struct jif_pkt {
    int jp_len;
    char jp_data[0];
};

注意網卡驅動,輸出進程以及網絡服務器進程之間的交互。當網絡服務器進程通過IPC發送數據包給輸出進程時,如果此時因爲網卡驅動沒有更多buffer導致輸出進程掛起,則網絡服務器進程必須阻塞等待。這裏的流程是:

core network env -> output helper env -> e1000 driver

5 接收數據包及input helper進程

類似傳輸數據包,接下來完成接收數據包流程。這裏要設置接收描述符和接收描述符隊列,接收描述符和隊列結構在文檔3.2節描述,而初始化細節在 14.4節。

接收的描述符的隊列大小這裏設置的是128個,另外,而E1000_RA 這個設置MAC地址時要注意,比如我們測試的MAC地址是 52:54:00:12:34:56,則ral處要設置爲 0x12005452,而rah則要設置爲 0x5634| E1000_RAH_AV,E1000_RAH_AV標識地址有效。

RDH寄存器指向網卡可存放數據包的第一個描述符,當網卡接收到數據包時,會將數據包存入接收隊列,並將RDH寄存器的值加1,這個更新寄存器的值的操作是網卡硬件執行的。

RDT寄存器則是存放的是網卡可用用來存放數據包的最後一個描述符的下一個描述符,這裏我們設置爲127,即浪費一個描述符作爲標識,我們的隊列最多可以存放127個數據包。

而測試程序net/testinput.c主要是做了下面幾個事情:

  • 創建一個子進程運行 output(),一個子進程運行input(),然後通過lwip構建一個ARP報文併發送給output environment。ARP報文通過 ipc_send()發送NSREQ_OUTPUT類型的IPC消息給output 進程。
  • output 進程接收到IPC消息後,會讀取IPC映射頁中數據包內容,調用系統調用 sys_pkt_send() 將數據包發送到網卡的發送描述符隊列中,發送程序就是我們實現的 e1000_transmit()函數。
  • 而當網卡接收到ARP請求後,會響應請求並輸出響應到我們設置的接收描述符隊列中。
  • 網卡輸出完畢後,會設置接收描述符的DD標記,此時input進程從接收描述符接收到網卡的數據包,並將其發送給core network server 進程。注意這裏,每次接收後發送要間隔一段時間,因爲網絡服務器進程讀取數據需要一定時間。

6 WEB服務器

最後是實現web服務器,類似httpd,主要完成send_file和send_data函數。實現就是根據請求解析出文件名,然後調用 fstat 獲取文件大小類型等元數據,並調用readn讀取文件以及使用writen寫入文件數據到socket中。

注意這裏的accept,bind等函數都是 lib/sockets.c中定義的,最終都是通過IPC功能將請求發送至 core network server進程(ns/serv.c),然後 core network server進程再調用的lwip來實現相關功能。這裏用到了線程,線程實現在 net/lwip/jos/arch/thread.c中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章