歡迎光臨
每天分享高質量文章

Caffeinated 6.828:實驗 6:網路驅動程式 | Linux 中國

現在你已經有了一個檔案系統,一個典型的作業系統都應該有一個網路棧。在本實驗中,你將繼續為一個網絡卡去寫一個驅動程式。
— Csail.mit
致謝
譯自 | pdos.csail.mit.edu 
作者 | Csail.mit
譯者 | LCTT / qhwdw

簡介

這個實驗是預設你能夠自己完成的最終專案。

現在你已經有了一個檔案系統,一個典型的作業系統都應該有一個網路棧。在本實驗中,你將繼續為一個網絡卡去寫一個驅動程式。這個網絡卡基於 Intel 82540EM 晶片,也就是眾所周知的 E1000 晶片。

預備知識

使用 Git 去提交你的實驗 5 的原始碼(如果還沒有提交的話),獲取課程倉庫的最新版本,然後建立一個名為 lab6 的本地分支,它跟蹤我們的遠端分支 origin/lab6

  1. athena% cd ~/6.828/lab
  2. athena% add git
  3. athena% git commit -am 'my solution to lab5'
  4. nothing to commit (working directory clean)
  5. athena% git pull
  6. Already up-to-date.
  7. athena% git checkout -b lab6 origin/lab6
  8. Branch lab6 set up to track remote branch refs/remotes/origin/lab6.
  9. Switched to a new branch "lab6"
  10. athena% git merge lab5
  11. Merge made by recursive.
  12. fs/fs.c |   42 +++++++++++++++++++
  13. 1 files changed, 42 insertions(+), 0 deletions(-)
  14. athena%

然後,僅有網絡卡驅動程式並不能夠讓你的作業系統接入網際網路。在新的實驗 6 的程式碼中,我們為你提供了網路棧和一個網路伺服器。與以前的實驗一樣,使用 git 去拉取這個實驗的程式碼,合併到你自己的程式碼中,並去瀏覽新的 net/ 目錄中的內容,以及在 kern/ 中的新檔案。

除了寫這個驅動程式以外,你還需要去建立一個訪問你的驅動程式的系統呼叫。你將要去實現那些在網路伺服器中缺失的程式碼,以便於在網路棧和你的驅動程式之間傳輸包。你還需要透過完成一個 web 伺服器來將所有的東西連線到一起。你的新 web 伺服器還需要你的檔案系統來提供所需要的檔案。

大部分的核心裝置驅動程式程式碼都需要你自己去從頭開始編寫。本實驗提供的指導比起前面的實驗要少一些:沒有框架檔案、沒有現成的系統呼叫介面、並且很多設計都由你自己決定。因此,我們建議你在開始任何單獨練習之前,閱讀全部的編寫任務。許多學生都反應這個實驗比前面的實驗都難,因此請根據你的實際情況計劃你的時間。

實驗要求

與以前一樣,你需要做實驗中全部的常規練習和至少一個挑戰問題。在實驗中寫出你的詳細答案,並將挑戰問題的方案描述寫入到 answers-lab6.txt 檔案中。

QEMU 的虛擬網路

我們將使用 QEMU 的使用者樣式網路棧,因為它不需要以管理員許可權執行。QEMU 的檔案的這裡[1]有更多關於使用者網路的內容。我們更新後的 makefile 啟用了 QEMU 的使用者樣式網路棧和虛擬的 E1000 網絡卡。

預設情況下,QEMU 提供一個執行在 IP 地址 10.2.2.2 上的虛擬路由器,它給 JOS 分配的 IP 地址是 10.0.2.15。為了簡單起見,我們在 net/ns.h 中將這些預設值硬編碼到網路伺服器上。

雖然 QEMU 的虛擬網路允許 JOS 隨意連線網際網路,但 JOS 的 10.0.2.15 的地址並不能在 QEMU 中的虛擬網路之外使用(也就是說,QEMU 還得做一個 NAT),因此我們並不能直接連線到 JOS 上執行的伺服器,即便是從執行 QEMU 的主機上連線也不行。為解決這個問題,我們配置 QEMU 在主機的某些埠上執行一個伺服器,這個伺服器簡單地連線到 JOS 中的一些埠上,併在你的真實主機和虛擬網路之間傳遞資料。

你將在埠 7(echo)和埠 80(http)上執行 JOS,為避免在共享的 Athena 機器上發生衝突,makefile 將為這些埠基於你的使用者 ID 來生成轉髮埠。你可以執行 make which-ports 去找出是哪個 QEMU 埠轉發到你的開發主機上。為方便起見,makefile 也提供 make nc-7 和 make nc-80,它允許你在終端上直接與執行這些埠的伺服器去互動。(這些標的僅能連線到一個執行中的 QEMU 實體上;你必須分別去啟動它自己的 QEMU)

包檢查

makefile 也可以配置 QEMU 的網路棧去記錄所有的入站和出站資料包,並將它儲存到你的實驗目錄中的 qemu.pcap 檔案中。

使用 tcpdump 命令去獲取一個捕獲的 hex/ASCII 包轉儲:

  1. tcpdump -XXnr qemu.pcap

或者,你可以使用 Wireshark[2] 以圖形化介面去檢查 pcap 檔案。Wireshark 也知道如何去解碼和檢查成百上千的網路協議。如果你在 Athena 上,你可以使用 Wireshark 的前輩:ethereal,它執行在加鎖的保密網際網路協議網路中。

除錯 E1000

我們非常幸運能夠去使用模擬硬體。由於 E1000 是在軟體中執行的,模擬的 E1000 能夠給我們提供一個人類可讀格式的報告、它的內部狀態以及它遇到的任何問題。通常情況下,對祼機上做驅動程式開發的人來說,這是非常難能可貴的。

E1000 能夠產生一些除錯輸出,因此你可以去開啟一個專門的日誌通道。其中一些對你有用的通道如下:

< 如顯示不全,請左右滑動 >
標誌 含義
tx 包傳送日誌
txerr 包傳送錯誤日誌
rx 到 RCTL 的日誌通道
rxfilter 入站包過濾日誌
rxerr 接收錯誤日誌
unknown 未知暫存器的讀寫日誌
eeprom 讀取 EEPROM 的日誌
interrupt 中斷和中斷暫存器變更日誌

例如,你可以使用 make E1000_DEBUG=tx,txerr 去開啟 “tx” 和 “txerr” 日誌功能。

註意:E1000_DEBUG 標誌僅能在打了 6.828 補丁的 QEMU 版本上工作。

你可以使用軟體去模擬硬體,來做進一步的除錯工作。如果你使用它時卡殼了,不明白為什麼 E1000 沒有如你預期那樣響應你,你可以檢視在 hw/e1000.c 中的 QEMU 的 E1000 實現。

網路伺服器

從頭開始寫一個網路棧是很困難的。因此我們將使用 lwIP,它是一個開源的、輕量級 TCP/IP 協議套件,它能做包括一個網路棧在內的很多事情。你能在 這裡[3] 找到很多關於 lwIP 的資訊。在這個任務中,對我們而言,lwIP 就是一個實現了一個 BSD 套接字介面和擁有一個包輸入埠和包輸出埠的黑盒子。

一個網路伺服器其實就是一個有以下四個環境的混合體:

◈ 核心網路伺服器環境(包括套接字呼叫派發器和 lwIP)
◈ 輸入環境
◈ 輸出環境
◈ 定時器環境

下圖展示了各個環境和它們之間的關係。下圖展示了包括裝置驅動的整個系統,我們將在後面詳細講到它。在本實驗中,你將去實現圖中綠色高亮的部分。

Network server architecture

核心網路伺服器環境

核心網路伺服器環境由套接字呼叫派發器和 lwIP 自身組成的。套接字呼叫派發器就像一個檔案伺服器一樣。使用者環境使用 stubs(可以在 lib/nsipc.c 中找到它)去傳送 IPC 訊息到核心網路伺服器環境。如果你看了 lib/nsipc.c,你就會發現核心網路伺服器與我們建立的檔案伺服器 i386_init 的工作方式是一樣的,i386_init 是使用 NSTYPENS 建立的 NS 環境,因此我們檢查 envs,去查詢這個特殊的環境型別。對於每個使用者環境的 IPC,網路伺服器中的派發器將呼叫相應的、由 lwIP 提供的、代表使用者的 BSD 套接字介面函式。

普通使用者環境不能直接使用 nsipc_* 呼叫。而是透過在 lib/sockets.c 中的函式來使用它們,這些函式提供了基於檔案描述符的套接字 API。以這種方式,使用者環境透過檔案描述符來取用套接字,就像它們取用磁碟上的檔案一樣。一些操作(connectaccept 等等)是特定於套接字的,但 readwrite 和 close 是透過 lib/fd.c 中一般的檔案描述符裝置派發程式碼的。就像檔案伺服器對所有的開啟的檔案維護唯一的內部 ID 一樣,lwIP 也為所有的開啟的套接字生成唯一的 ID。不論是檔案伺服器還是網路伺服器,我們都使用儲存在 struct Fd中的資訊去對映每個環境的檔案描述符到這些唯一的 ID 空間上。

儘管看起來檔案伺服器的網路伺服器的 IPC 派發器行為是一樣的,但它們之間還有很重要的差別。BSD 套接字呼叫(像 accept 和 recv)能夠無限期阻塞。如果派發器讓 lwIP 去執行其中一個呼叫阻塞,派發器也將被阻塞,並且在整個系統中,同一時間只能有一個未完成的網路呼叫。由於這種情況是無法接受的,所以網路伺服器使用使用者級執行緒以避免阻塞整個伺服器環境。對於每個入站 IPC 訊息,派發器將建立一個執行緒,然後在新建立的執行緒上來處理請求。如果執行緒被阻塞,那麼只有那個執行緒被置入休眠狀態,而其它執行緒仍然處於執行中。

除了核心網路環境外,還有三個輔助環境。核心網路伺服器環境除了接收來自使用者應用程式的訊息之外,它的派發器也接收來自輸入環境和定時器環境的訊息。

輸出環境

在為使用者環境套接字呼叫提供服務時,lwIP 將為網絡卡生成用於傳送的包。lwIP 將使用 NSREQ_OUTPUT 去傳送在 IPC 訊息頁引數中附加了包的 IPC 訊息。輸出環境負責接收這些訊息,並透過你稍後建立的系統呼叫介面來轉發這些包到裝置驅動程式上。

輸入環境

網絡卡接收到的包需要傳遞到 lwIP 中。輸入環境將每個由裝置驅動程式接收到的包拉進核心空間(使用你將要實現的核心系統呼叫),並使用 NSREQ_INPUT IPC 訊息將這些包傳送到核心網路伺服器環境。

包輸入功能是獨立於核心網路環境的,因為在 JOS 上同時實現接收 IPC 訊息並從裝置驅動程式中查詢或等待包有點困難。我們在 JOS 中沒有實現 select 系統呼叫,這是一個允許環境去監視多個輸入源以識別準備處理哪個輸入的系統呼叫。

如果你查看了 net/input.c 和 net/output.c,你將會看到在它們中都需要去實現那個系統呼叫。這主要是因為實現它要依賴你的系統呼叫介面。在你實現了驅動程式和系統呼叫介面之後,你將要為這兩個輔助環境寫這個程式碼。

定時器環境

定時器環境週期性傳送 NSREQ_TIMER 型別的訊息到核心伺服器,以提醒它那個定時器已過期。lwIP 使用來自執行緒的定時器訊息來實現各種網路超時。

Part A:初始化和傳送包

你的核心還沒有一個時間概念,因此我們需要去新增它。這裡有一個由硬體產生的每 10 ms 一次的時鐘中斷。每收到一個時鐘中斷,我們將增加一個變數值,以表示時間已過去 10 ms。它在 kern/time.c 中已實現,但還沒有完全整合到你的核心中。

練習 1、為 kern/trap.c 中的每個時鐘中斷增加一個到 time_tick 的呼叫。實現 sys_time_msec 並增加到 kern/syscall.c 中的 syscall,以便於使用者空間能夠訪問時間。

使用 make INIT_CFLAGS=-DTEST_NO_NS run-testtime 去測試你的程式碼。你應該會看到環境計數從 5 開始以 1 秒為間隔減少。-DTEST_NO_NS 引數禁止在網路伺服器環境上啟動,因為在當前它將導致 JOS 崩潰。

網絡卡

寫驅動程式要求你必須深入瞭解硬體和軟體中的介面。本實驗將給你提供一個如何使用 E1000 介面的高度概括的檔案,但是你在寫驅動程式時還需要大量去查詢 Intel 的手冊。

練習 2、為開發 E1000 驅動,去瀏覽 Intel 的 軟體開發者手冊[4]。這個手冊涵蓋了幾個與乙太網控制器緊密相關的東西。QEMU 模擬了 82540EM。

現在,你應該去瀏覽第 2 章,以對裝置獲得一個整體概念。寫驅動程式時,你需要熟悉第 3 到 14 章,以及 4.1(不包括 4.1 的子節)。你也應該去參考第 13 章。其它章涵蓋了 E1000 的元件,你的驅動程式並不與這些元件去互動。現在你不用擔心過多細節的東西;只需要瞭解檔案的整體結構,以便於你後面需要時容易查詢。

在閱讀手冊時,記住,E1000 是一個擁有很多高階特性的很複雜的裝置,一個能讓 E1000 工作的驅動程式僅需要它一小部分的特性和 NIC 提供的介面即可。仔細考慮一下,如何使用最簡單的方式去使用網絡卡的介面。我們強烈推薦你在使用高階特性之前,只去寫一個基本的、能夠讓網絡卡工作的驅動程式即可。

PCI 介面

E1000 是一個 PCI 裝置,也就是說它是插到主機板的 PCI 匯流排插槽上的。PCI 匯流排有地址、資料、和中斷線,並且 PCI 匯流排允許 CPU 與 PCI 裝置通訊,以及 PCI 裝置去讀取和寫入記憶體。一個 PCI 裝置在它能夠被使用之前,需要先發現它併進行初始化。發現 PCI 裝置是 PCI 匯流排查詢已安裝裝置的過程。初始化是分配 I/O 和記憶體空間、以及協商裝置所使用的 IRQ 線的過程。

我們在 kern/pci.c 中已經為你提供了使用 PCI 的程式碼。PCI 初始化是在引導期間執行的,PCI 程式碼遍歷PCI 匯流排來查詢裝置。當它找到一個裝置時,它讀取它的供應商 ID 和裝置 ID,然後使用這兩個值作為關鍵字去搜索 pci_attach_vendor 陣列。這個陣列是由像下麵這樣的 struct pci_driver 條目組成:

  1. struct pci_driver {
  2.    uint32_t key1, key2;
  3.    int (*attachfn) (struct pci_func *pcif);
  4. };

如果發現的裝置的供應商 ID 和裝置 ID 與陣列中條目匹配,那麼 PCI 程式碼將呼叫那個條目的 attachfn 去執行裝置初始化。(裝置也可以按類別識別,那是透過 kern/pci.c 中其它的驅動程式表來實現的。)

系結函式是傳遞一個 PCI 函式 去初始化。一個 PCI 卡能夠釋出多個函式,雖然這個 E1000 僅釋出了一個。下麵是在 JOS 中如何去表示一個 PCI 函式:

    1. struct pci_func {
    2.    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;
  • };


上面的結構反映了在 Intel 開發者手冊裡第 4.1 節的表 4-1 中找到的一些條目。struct pci_func 的最後三個條目我們特別感興趣的,因為它們將記錄這個裝置協商的記憶體、I/O、以及中斷資源。reg_base 和 reg_size 陣列包含最多六個基址暫存器或 BAR。reg_base 為對映到記憶體中的 I/O 區域(對於 I/O 埠而言是基 I/O 埠)儲存了記憶體的基地址,reg_size 包含了以位元組表示的大小或來自 reg_base 的相關基值的 I/O 埠號,而 irq_line 包含了為中斷分配給裝置的 IRQ 線。在表 4-2 的後半部分給出了 E1000 BAR 的具體涵義。

當裝置呼叫了系結函式後,裝置已經被髮現,但沒有被啟用。這意味著 PCI 程式碼還沒有確定分配給裝置的資源,比如地址空間和 IRQ 線,也就是說,struct pci_func 結構的最後三個元素還沒有被填入。系結函式將呼叫 pci_func_enable,它將去啟用裝置、協商這些資源、併在結構 struct pci_func 中填入它。

練習 3、實現一個系結函式去初始化 E1000。新增一個條目到 kern/pci.c 中的陣列 pci_attach_vendor 上,如果找到一個匹配的 PCI 裝置就去觸發你的函式(確保一定要把它放在表末尾的 {0, 0, 0} 條目之前)。你在 5.2 節中能找到 QEMU 模擬的 82540EM 的供應商 ID 和裝置 ID。在引導期間,當 JOS 掃描 PCI 匯流排時,你也可以看到列出來的這些資訊。

到目前為止,我們透過 pci_func_enable 啟用了 E1000 裝置。透過本實驗我們將新增更多的初始化。

我們已經為你提供了 kern/e1000.c 和 kern/e1000.h 檔案,這樣你就不會把構建系統搞糊塗了。不過它們現在都是空的;你需要在本練習中去填充它們。你還可能在內核的其它地方包含這個 e1000.h 檔案。

當你引導你的核心時,你應該會看到它輸出的資訊顯示 E1000 的 PCI 函式已經啟用。這時你的程式碼已經能夠透過 make grade 的 pci attach 測試了。

記憶體對映的 I/O

軟體與 E1000 透過記憶體對映的 I/O(MMIO)來溝通。你在 JOS 的前面部分可能看到過 MMIO 兩次:CGA 控制檯和 LAPIC 都是透過寫入和讀取“記憶體”來控制和查詢裝置的。但這些讀取和寫入不是去往記憶體晶片的,而是直接到這些裝置的。

pci_func_enable 為 E1000 協調一個 MMIO 區域,來儲存它在 BAR 0 的基址和大小(也就是 reg_base[0] 和 reg_size[0]),這是一個分配給裝置的一段物理記憶體地址,也就是說你可以透過虛擬地址訪問它來做一些事情。由於 MMIO 區域一般分配高位物理地址(一般是 3GB 以上的位置),因此你不能使用 KADDR 去訪問它們,因為 JOS 被限製為最大使用 256MB。因此,你可以去建立一個新的記憶體對映。我們將使用 MMIOBASE(從實驗 4 開始,你的 mmio_map_region 區域應該確保不能被 LAPIC 使用的對映所改寫)以上的部分。由於在 JOS 建立使用者環境之前,PCI 裝置就已經初始化了,因此你可以在 kern_pgdir 處建立對映,並且讓它始終可用。

練習 4、在你的系結函式中,透過呼叫 mmio_map_region(它就是你在實驗 4 中寫的,是為了支援 LAPIC 記憶體對映)為 E1000 的 BAR 0 建立一個虛擬地址對映。

你將希望在一個變數中記錄這個對映的位置,以便於後面訪問你對映的暫存器。去看一下 kern/lapic.c 中的 lapic 變數,它就是一個這樣的例子。如果你使用一個指標指向裝置暫存器對映,一定要宣告它為 volatile;否則,編譯器將允許快取它的值,並可以在記憶體中再次訪問它。

為測試你的對映,嘗試去輸出裝置狀態暫存器(第 12.4.2 節)。這是一個在暫存器空間中以位元組 8 開頭的 4 位元組暫存器。你應該會得到 0x80080783,它表示以 1000 MB/s 的速度啟用一個全雙工的鏈路,以及其它資訊。

提示:你將需要一些常數,像暫存器位置和掩碼位數。如果從開發者手冊中複製這些東西很容易出錯,並且導致除錯過程很痛苦。我們建議你使用 QEMU 的 e1000_hw.h[5] 頭檔案做為基準。我們不建議完全照抄它,因為它定義的值遠超過你所需要,並且定義的東西也不見得就是你所需要的,但它仍是一個很好的參考。

DMA

你可能會認為是從 E1000 的暫存器中透過寫入和讀取來傳送和接收資料包的,其實這樣做會非常慢,並且還要求 E1000 在其中去快取資料包。相反,E1000 使用直接記憶體訪問(DMA)從記憶體中直接讀取和寫入資料包,而且不需要 CPU 參與其中。驅動程式負責為傳送和接收佇列分配記憶體、設定 DMA 描述符、以及配置 E1000 使用的佇列位置,而在這些設定完成之後的其它工作都是非同步方式進行的。傳送包的時候,驅動程式複製它到傳送佇列的下一個 DMA 描述符中,並且通知 E1000 下一個傳送包已就緒;當輪到這個包傳送時,E1000 將從描述符中複製出資料。同樣,當 E1000 接收一個包時,它從接收佇列中將它複製到下一個 DMA 描述符中,驅動程式將能在下一次讀取到它。

總體來看,接收佇列和傳送佇列非常相似。它們都是由一系列的描述符組成。雖然這些描述符的結構細節有所不同,但每個描述符都包含一些標誌和包含了包資料的一個快取的物理地址(傳送到網絡卡的資料包,或網絡卡將接收到的資料包寫入到由作業系統分配的快取中)。

佇列被實現為一個環形陣列,意味著當網絡卡或驅動到達陣列末端時,它將重新回到開始位置。它有一個頭指標和尾指標,佇列的內容就是這兩個指標之間的描述符。硬體就是從頭開始移動頭指標去消費描述符,在這期間驅動程式不停地新增描述符到尾部,並移動尾指標到最後一個描述符上。傳送佇列中的描述符表示等待傳送的包(因此,在平靜狀態下,傳送佇列是空的)。對於接收佇列,佇列中的描述符是表示網絡卡能夠接收包的空描述符(因此,在平靜狀態下,接收佇列是由所有的可用接收描述符組成的)。正確的更新尾指標暫存器而不讓 E1000 產生混亂是很有難度的;要小心!

指向到這些陣列及描述符中的包快取地址的指標都必須是物理地址,因為硬體是直接在物理記憶體中且不透過 MMU 來執行 DMA 的讀寫操作的。

傳送包

E1000 中的傳送和接收功能本質上是獨立的,因此我們可以同時進行傳送接收。我們首先去攻剋簡單的資料包傳送,因為我們在沒有先去傳送一個 “I’m here!” 包之前是無法測試接收包功能的。

首先,你需要初始化網絡卡以準備傳送,詳細步驟檢視 14.5 節(不必著急看子節)。傳送初始化的第一步是設定傳送佇列。佇列的詳細結構在 3.4 節中,描述符的結構在 3.3.3 節中。我們先不要使用 E1000 的 TCP offload 特性,因此你只需專註於 “傳統的傳送描述符格式” 即可。你應該現在就去閱讀這些章節,並要熟悉這些結構。

C 結構

你可以用 C struct 很方便地描述 E1000 的結構。正如你在 struct Trapframe 中所看到的結構那樣,C struct 可以讓你很方便地在記憶體中描述準確的資料佈局。C 可以在欄位中插入資料,但是 E1000 的結構就是這樣佈局的,這樣就不會是個問題。如果你遇到欄位對齊問題,進入 GCC 檢視它的 “packed” 屬性。

檢視手冊中表 3-8 所給出的一個傳統的傳送描述符,將它複製到這裡作為一個示例:

  1.  63            48 47   40 39   32 31   24 23   16 15             0
  2.  +---------------------------------------------------------------+
  3.  |                         Buffer address                        |
  4.  +---------------|-------|-------|-------|-------|---------------+
  5.  |    Special    |  CSS  | Status|  Cmd  |  CSO  |    Length     |
  6.  +---------------|-------|-------|-------|-------|---------------+

從結構右上角第一個位元組開始,我們將它轉變成一個 C 結構,從上到下,從右到左讀取。如果你從右往左看,你將看到所有的欄位,都非常適合一個標準大小的型別:

  1. struct tx_desc
  2. {
  3.    uint64_t addr;
  4.    uint16_t length;
  5.    uint8_t cso;
  6.    uint8_t cmd;
  7.    uint8_t status;
  8.    uint8_t css;
  9.    uint16_t special;
  10. };

你的驅動程式將為傳送描述符陣列去保留記憶體,並由傳送描述符指向到包緩衝區。有幾種方式可以做到,從動態分配頁到在全域性變數中簡單地宣告它們。無論你如何選擇,記住,E1000 是直接訪問物理記憶體的,意味著它能訪問的任何快取區在物理記憶體中必須是連續的。

處理包快取也有幾種方式。我們推薦從最簡單的開始,那就是在驅動程式初始化期間,為每個描述符保留包快取空間,並簡單地將包資料複製進預留的緩衝區中或從其中複製出來。一個乙太網包最大的尺寸是 1518 位元組,這就限制了這些快取區的大小。主流的成熟驅動程式都能夠動態分配包快取區(即:當網路使用率很低時,減少記憶體使用量),或甚至跳過快取區,直接由使用者空間提供(就是“零複製”技術),但我們還是從簡單開始為好。

練習 5、執行一個 14.5 節中的初始化步驟(它的子節除外)。對於暫存器的初始化過程使用 13 節作為參考,對傳送描述符和傳送描述符陣列參考 3.3.3 節和 3.4 節。

要記住,在傳送描述符陣列中要求對齊,並且陣列長度上有限制。因為 TDLEN 必須是 128 位元組對齊的,而每個傳送描述符是 16 位元組,你的傳送描述符陣列必須是 8 個傳送描述符的倍數。並且不能使用超過 64 個描述符,以及不能在我們的傳送環形快取測試中上限溢位。

對於 TCTL.COLD,你可以假設為全雙工操作。對於 TIPG、IEEE 802.3 標準的 IPG(不要使用 14.5 節中表上的值),參考在 13.4.34 節中表 13-77 中描述的預設值。

嘗試執行 make E1000_DEBUG=TXERR,TX qemu。如果你使用的是打了 6.828 補丁的 QEMU,當你設定 TDT(傳送描述符尾部)暫存器時你應該會看到一個 “e1000: tx disabled” 的資訊,並且不會有更多 “e1000” 資訊了。

現在,傳送初始化已經完成,你可以寫一些程式碼去傳送一個資料包,並且透過一個系統呼叫使它可以訪問使用者空間。你可以將要傳送的資料包新增到傳送佇列的尾部,也就是說複製資料包到下一個包緩衝區中,然後更新 TDT 暫存器去通知網絡卡在傳送佇列中有另外的資料包。(註意,TDT 是一個進入傳送描述符陣列的索引,不是一個位元組偏移量;關於這一點檔案中說明的不是很清楚。)

但是,傳送佇列只有這麼大。如果網絡卡在傳送資料包時卡住或傳送佇列填滿時會發生什麼狀況?為了檢測這種情況,你需要一些來自 E1000 的反饋。不幸的是,你不能只使用 TDH(傳送描述符頭)暫存器;檔案上明確說明,從軟體上讀取這個暫存器是不可靠的。但是,如果你在傳送描述符的命令欄位中設定 RS 位,那麼,當網絡卡去傳送在那個描述符中的資料包時,網絡卡將設定描述符中狀態列位的 DD 位,如果一個描述符中的 DD 位被設定,你就應該知道那個描述符可以安全地回收,並且可以用它去傳送其它資料包。

如果使用者呼叫你的傳送系統呼叫,但是下一個描述符的 DD 位沒有設定,表示那個傳送佇列已滿,該怎麼辦?在這種情況下,你該去決定怎麼辦了。你可以簡單地丟棄資料包。網路協議對這種情況的處理很靈活,但如果你丟棄大量的突發資料包,協議可能不會去重新獲得它們。可能需要你替代網路協議告訴使用者環境讓它重傳,就像你在 sys_ipc_try_send 中做的那樣。在環境上回推產生的資料是有好處的。

練習 6、寫一個函式去傳送一個資料包,它需要檢查下一個描述符是否空閑、複製包資料到下一個描述符並更新 TDT。確保你處理的傳送佇列是滿的。

現在,應該去測試你的包傳送程式碼了。透過從核心中直接呼叫你的傳送函式來嘗試傳送幾個包。在測試時,你不需要去建立符合任何特定網路協議的資料包。執行 make E1000_DEBUG=TXERR,TX qemu 去測試你的程式碼。你應該看到類似下麵的資訊:

  1. e1000: index 0: 0x271f00 : 9000002a 0
  2. ...

在你傳送包時,每行都給出了在傳送陣列中的序號、那個傳送的描述符的快取地址、cmd/CSO/length 欄位、以及 special/CSS/status 欄位。如果 QEMU 沒有從你的傳送描述符中輸出你預期的值,檢查你的描述符中是否有合適的值和你配置的正確的 TDBAL 和 TDBAH。如果你收到的是 “e1000: TDH wraparound @0, TDT x, TDLEN y” 的資訊,意味著 E1000 的傳送佇列持續不斷地執行(如果 QEMU 不去檢查它,它將是一個無限迴圈),這意味著你沒有正確地維護 TDT。如果你收到了許多 “e1000: tx disabled” 的資訊,那麼意味著你沒有正確設定傳送控制暫存器。

一旦 QEMU 執行,你就可以執行 tcpdump -XXnr qemu.pcap 去檢視你傳送的包資料。如果從 QEMU 中看到預期的 “e1000: index” 資訊,但你捕獲的包是空的,再次檢查你傳送的描述符,是否填充了每個必需的欄位和位。(E1000 或許已經遍歷了你的傳送描述符,但它認為不需要去傳送)

練習 7、新增一個系統呼叫,讓你從使用者空間中傳送資料包。詳細的介面由你來決定。但是不要忘了檢查從使用者空間傳遞給內核的所有指標。

傳送包:網路伺服器

現在,你已經有一個系統呼叫介面可以傳送包到你的裝置驅動程式端了。輸出輔助環境的標的是在一個迴圈中做下麵的事情:從核心網路伺服器中接收 NSREQ_OUTPUT IPC 訊息,並使用你在上面增加的系統呼叫去傳送伴隨這些 IPC 訊息的資料包。這個 NSREQ_OUTPUT IPC 是透過 net/lwip/jos/jif/jif.c 中的 low_level_output 函式來傳送的。它整合 lwIP 棧到 JOS 的網路系統。每個 IPC 將包含一個頁,這個頁由一個 union Nsipc 和在 struct jif_pkt pkt 欄位中的一個包組成(檢視 inc/ns.h)。struct jif_pkt 看起來像下麵這樣:

  1. struct jif_pkt {
  2.    int jp_len;
  3.    char jp_data[0];
  4. };

jp_len 表示包的長度。在 IPC 頁上的所有後續位元組都是為了包內容。在結構的結尾處使用一個長度為 0 的陣列來表示快取沒有一個預先確定的長度(像 jp_data 一樣),這是一個常見的 C 技巧(也有人說這是一個令人討厭的做法)。因為 C 並不做陣列邊界的檢查,只要你確保結構後面有足夠的未使用記憶體即可,你可以把 jp_data 作為一個任意大小的陣列來使用。

當裝置驅動程式的傳送佇列中沒有足夠的空間時,一定要註意在裝置驅動程式、輸出環境和核心網路伺服器之間的互動。核心網路伺服器使用 IPC 傳送包到輸出環境。如果輸出環境在由於一個傳送包的系統呼叫而掛起,導致驅動程式沒有足夠的快取去容納新資料包,這時核心網路伺服器將阻塞以等待輸出伺服器去接收 IPC 呼叫。

練習 8、實現 net/output.c

你可以使用 net/testoutput.c 去測試你的輸出程式碼而無需整個網路伺服器參與。嘗試執行 make E1000_DEBUG=TXERR,TX run-net_testoutput。你將看到如下的輸出:

  1. Transmitting packet 0
  2. e1000: index 0: 0x271f00 : 9000009 0
  3. Transmitting packet 1
  4. e1000: index 1: 0x2724ee : 9000009 0
  5. ...

執行 tcpdump -XXnr qemu.pcap 將輸出:

  1. reading from file qemu.pcap, link-type EN10MB (Ethernet)
  2. -5:00:00.600186 [|ether]
  3.    0x0000:  5061 636b 6574 2030 30                   Packet.00
  4. -5:00:00.610080 [|ether]
  5.    0x0000:  5061 636b 6574 2030 31                   Packet.01
  6. ...

使用更多的資料包去測試,可以執行 make E1000_DEBUG=TXERR,TX NET_CFLAGS=-DTESTOUTPUT_COUNT=100 run-net_testoutput。如果它導致你的傳送佇列上限溢位,再次檢查你的 DD 狀態位是否正確,以及是否告訴硬體去設定 DD 狀態位(使用 RS 命令位)。

你的程式碼應該會透過 make grade 的 testoutput 測試。

問題 1、你是如何構造你的傳送實現的?在實踐中,如果傳送快取區滿了,你該如何處理?

Part B:接收包和 web 伺服器

接收包

就像你在傳送包中做的那樣,你將去配置 E1000 去接收資料包,並提供一個接收描述符佇列和接收描述符。在 3.2 節中描述了接收包的操作,包括接收佇列結構和接收描述符、以及在 14.4 節中描述的詳細的初始化過程。

練習 9、閱讀 3.2 節。你可以忽略關於中斷和 offload 校驗和方面的內容(如果在後面你想去使用這些特性,可以再傳回去閱讀),你現在不需要去考慮閾值的細節和網絡卡內部快取是如何工作的。

除了接收佇列是由一系列的等待入站資料包去填充的空快取包以外,接收佇列的其它部分與傳送佇列非常相似。所以,當網路空閑時,傳送佇列是空的(因為所有的包已經被髮送出去了),而接收佇列是滿的(全部都是空快取包)。

當 E1000 接收一個包時,它首先與網絡卡的過濾器進行匹配檢查(例如,去檢查這個包的標的地址是否為這個 E1000 的 MAC 地址),如果這個包不匹配任何過濾器,它將忽略這個包。否則,E1000 嘗試從接收佇列頭部去檢索下一個接收描述符。如果頭(RDH)追上了尾(RDT),那麼說明接收佇列已經沒有空閑的描述符了,所以網絡卡將丟棄這個包。如果有空閑的接收描述符,它將複製這個包的資料到描述符指向的快取中,設定這個描述符的 DD 和 EOP 狀態位,並遞增 RDH。

如果 E1000 在一個接收描述符中接收到了一個比包快取還要大的資料包,它將按需從接收佇列中檢索盡可能多的描述符以儲存資料包的全部內容。為表示發生了這種情況,它將在所有的這些描述符上設定 DD 狀態位,但僅在這些描述符的最後一個上設定 EOP 狀態位。在你的驅動程式上,你可以去處理這種情況,也可以簡單地配置網絡卡拒絕接收這種”長包“(這種包也被稱為”巨幀“),你要確保接收快取有足夠的空間盡可能地去儲存最大的標準乙太網資料包(1518 位元組)。

練習 10、設定接收佇列並按 14.4 節中的流程去配置 E1000。你可以不用支援 ”長包“ 或多播。到目前為止,我們不用去配置網絡卡使用中斷;如果你在後面決定去使用接收中斷時可以再去改。另外,配置 E1000 去除乙太網的 CRC 校驗,因為我們的評級指令碼要求必須去掉校驗。

預設情況下,網絡卡將過濾掉所有的資料包。你必須使用網絡卡的 MAC 地址去配置接收地址暫存器(RAL 和 RAH)以接收傳送到這個網絡卡的資料包。你可以簡單地硬編碼 QEMU 的預設 MAC 地址 52:54:00:12:34:56(我們已經在 lwIP 中硬編碼了這個地址,因此這樣做不會有問題)。使用位元組順序時要註意;MAC 地址是從低位位元組到高位位元組的方式來寫的,因此 52:54:00:12 是 MAC 地址的低 32 位,而 34:56 是它的高 16 位。

E1000 的接收快取區大小僅支援幾個指定的設定值(在 13.4.22 節中描述的 RCTL.BSIZE 值)。如果你的接收包快取夠大,並且拒絕長包,那你就不用擔心跨越多個快取區的包。另外,要記住的是,和傳送一樣,接收佇列和包快取必須是連線的物理記憶體。

你應該使用至少 128 個接收描述符。

現在,你可以做接收功能的基本測試了,甚至都無需寫程式碼去接收包了。執行 make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-net_testinputtestinput 將傳送一個 ARP(地址解析協議)通告包(使用你的包傳送的系統呼叫),而 QEMU 將自動回覆它,即便是你的驅動尚不能接收這個回覆,你也應該會看到一個 “e1000: unicast match[0]: 52:54:00:12:34:56” 的訊息,表示 E1000 接收到一個包,並且匹配了配置的接收過濾器。如果你看到的是一個 “e1000: unicast mismatch: 52:54:00:12:34:56” 訊息,表示 E1000 過濾掉了這個包,意味著你的 RAL 和 RAH 的配置不正確。確保你按正確的順序收到了位元組,並不要忘記設定 RAH 中的 “Address Valid” 位。如果你沒有收到任何 “e1000” 訊息,或許是你沒有正確地啟用接收功能。

現在,你準備去實現接收資料包。為了接收資料包,你的驅動程式必須持續跟蹤希望去儲存下一下接收到的包的描述符(提示:按你的設計,這個功能或許已經在 E1000 中的一個暫存器來實現了)。與傳送類似,官方檔案上表示,RDH 暫存器狀態並不能從軟體中可靠地讀取,因為確定一個包是否被髮送到描述符的包快取中,你需要去讀取描述符中的 DD 狀態位。如果 DD 位被設定,你就可以從那個描述符的快取中複製出這個資料包,然後透過更新佇列的尾索引 RDT 來告訴網絡卡那個描述符是空閑的。

如果 DD 位沒有被設定,表明沒有接收到包。這就與傳送佇列滿的情況一樣,這時你可以有幾種做法。你可以簡單地傳回一個 ”重傳“ 錯誤來要求對端重發一次。對於滿的傳送佇列,由於那是個臨時狀況,這種做法還是很好的,但對於空的接收佇列來說就不太合理了,因為接收佇列可能會保持好長一段時間的空的狀態。第二個方法是掛起呼叫環境,直到在接收佇列中處理了這個包為止。這個策略非常類似於 sys_ipc_recv。就像在 IPC 的案例中,因為我們每個 CPU 僅有一個核心棧,一旦我們離開核心,棧上的狀態就會被丟棄。我們需要設定一個標誌去表示那個環境由於接收佇列下溢被掛起並記錄系統呼叫引數。這種方法的缺點是過於複雜:E1000 必須被指示去產生接收中斷,並且驅動程式為了恢復被阻塞等待一個包的環境,必須處理這個中斷。

練習 11、寫一個函式從 E1000 中接收一個包,然後透過一個系統呼叫將它釋出到使用者空間。確保你將接收佇列處理成空的。

小挑戰!如果傳送佇列是滿的或接收佇列是空的,環境和你的驅動程式可能會花費大量的 CPU 週期是輪詢、等待一個描述符。一旦完成傳送或接收描述符,E1000 能夠產生一個中斷,以避免輪詢。修改你的驅動程式,處理髮送和接收佇列是以中斷而不是輪詢的方式進行。

註意,一旦確定為中斷,它將一直處於中斷狀態,直到你的驅動程式明確處理完中斷為止。在你的中斷服務程式中,一旦處理完成要確保清除掉中斷狀態。如果你不那樣做,從你的中斷服務程式中傳回後,CPU 將再次跳轉到你的中斷服務程式中。除了在 E1000 網絡卡上清除中斷外,也需要使用 lapic_eoi 在 LAPIC 上清除中斷。

接收包:網路伺服器

在網路伺服器輸入環境中,你需要去使用你的新的接收系統呼叫以接收資料包,並使用 NSREQ_INPUT IPC 訊息將它傳遞到核心網路伺服器環境。這些 IPC 輸入訊息應該會有一個頁,這個頁上系結了一個 union Nsipc,它的 struct jif_pkt pkt 欄位中有從網路上接收到的包。

練習 12、實現 net/input.c

使用 make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-net_testinput再次執行 testinput,你應該會看到:

  1. Sending ARP announcement...
  2. Waiting for packets...
  3. e1000: index 0: 0x26dea0 : 900002a 0
  4. e1000: unicast match[0]: 52:54:00:12:34:56
  5. input: 0000 5254 0012 3456 5255 0a00 0202 0806 0001
  6. input: 0010 0800 0604 0002 5255 0a00 0202 0a00 0202
  7. input: 0020 5254 0012 3456 0a00 020f 0000 0000 0000
  8. input: 0030 0000 0000 0000 0000 0000 0000 0000 0000

“input:” 打頭的行是一個 QEMU 的 ARP 回覆的十六進位制轉儲。

你的程式碼應該會透過 make grade 的 testinput 測試。註意,在沒有傳送至少一個包去通知 QEMU 中的 JOS 的 IP 地址上時,是沒法去測試包接收的,因此在你的傳送程式碼中的 bug 可能會導致測試失敗。

為徹底地測試你的網路程式碼,我們提供了一個稱為 echosrv 的守護程式,它在埠 7 上設定執行 echo 的伺服器,它將回顯透過 TCP 連線傳送給它的任何內容。使用 make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-echosrv 在一個終端中啟動 echo 伺服器,然後在另一個終端中透過 make nc-7 去連線它。你輸入的每一行都被這個伺服器回顯出來。每次在模擬的 E1000 上接收到一個包,QEMU 將在控制臺上輸出像下麵這樣的內容:

  1. e1000: unicast match[0]: 52:54:00:12:34:56
  2. e1000: index 2: 0x26ea7c : 9000036 0
  3. e1000: index 3: 0x26f06a : 9000039 0
  4. e1000: unicast match[0]: 52:54:00:12:34:56

做到這一點後,你應該也就能透過 echosrv 的測試了。

問題 2、你如何構造你的接收實現?在實踐中,如果接收佇列是空的並且一個使用者環境要求下一個入站包,你怎麼辦?

小挑戰!在開發者手冊中閱讀關於 EEPROM 的內容,並寫出從 EEPROM 中載入 E1000 的 MAC 地址的程式碼。目前,QEMU 的預設 MAC 地址是硬編碼到你的接收初始化程式碼和 lwIP 中的。修複你的初始化程式碼,讓它能夠從 EEPROM 中讀取 MAC 地址,和增加一個系統呼叫去傳遞 MAC 地址到 lwIP 中,並修改 lwIP 去從網絡卡上讀取 MAC 地址。透過配置 QEMU 使用一個不同的 MAC 地址去測試你的變更。

.

小挑戰!修改你的 E1000 驅動程式去使用 零複製 技術。目前,資料包是從使用者空間快取中複製到傳送包快取中,和從接收包快取中複製回到使用者空間快取中。一個使用 ”零複製“ 技術的驅動程式可以透過直接讓使用者空間和 E1000 共享包快取記憶體來實現。還有許多不同的方法去實現 ”零複製“,包括對映內容分配的結構到使用者空間或直接傳遞使用者提供的快取到 E1000。不論你選擇哪種方法,都要註意你如何利用快取的問題,因為你不能在使用者空間程式碼和 E1000 之間產生爭用。

小挑戰!把 “零複製” 的概念用到 lwIP 中。

一個典型的包是由許多頭構成的。使用者傳送的資料被髮送到 lwIP 中的一個快取中。TCP 層要新增一個 TCP 包頭,IP 層要新增一個 IP 包頭,而 MAC 層有一個乙太網頭。甚至還有更多的部分增加到包上,這些部分要正確地連線到一起,以便於裝置驅動程式能夠傳送最終的包。

E1000 的傳送描述符設計是非常適合收集分散在記憶體中的包片段的,像在 lwIP 中建立的包的幀。如果你排隊多個傳送描述符,但僅設定最後一個描述符的 EOP 命令位,那麼 E1000 將在內部把這些描述符串成包快取,併在它們標記完 EOP 後僅傳送串起來的快取。因此,獨立的包片段不需要在記憶體中把它們連線到一起。

修改你的驅動程式,以使它能夠傳送由多個快取且無需複製的片段組成的包,並且修改 lwIP 去避免它合併包片段,因為它現在能夠正確處理了。

小挑戰!增加你的系統呼叫介面,以便於它能夠為多於一個的使用者環境提供服務。如果有多個網路棧(和多個網路伺服器)並且它們各自都有自己的 IP 地址執行在使用者樣式中,這將是非常有用的。接收系統呼叫將決定它需要哪個環境來轉發每個入站的包。

註意,當前的介面並不知道兩個包之間有何不同,並且如果多個環境去呼叫包接收的系統呼叫,各個環境將得到一個入站包的子集,而那個子集可能並不包含呼叫環境指定的那個包。

在 這篇[6] 外核心論文的 2.2 節和 3 節中對這個問題做了深度解釋,並解釋了在核心中(如 JOS)處理它的一個方法。用這個論文中的方法去解決這個問題,你不需要一個像論文中那麼複雜的方案。

Web 伺服器

一個最簡單的 web 伺服器型別是傳送一個檔案的內容到請求的客戶端。我們在 user/httpd.c 中提供了一個非常簡單的 web 伺服器的框架程式碼。這個框架內碼處理入站連線並解析請求頭。

練習 13、這個 web 伺服器中缺失了傳送一個檔案的內容到客戶端的處理程式碼。透過實現 send_file 和 send_data 完成這個 web 伺服器。

在你完成了這個 web 伺服器後,啟動這個 web 伺服器(make run-httpd-nox),使用你喜歡的瀏覽器去瀏覽 http://host:port/index.html 地址。其中 host 是執行 QEMU 的計算機的名字(如果你在 athena 上執行 QEMU,使用 hostname.mit.edu(其中 hostname 是在 athena 上執行 hostname 命令的輸出,或者如果你在執行 QEMU 的機器上執行 web 瀏覽器的話,直接使用 localhost),而 port 是 web 伺服器執行 make which-ports 命令報告的埠號。你應該會看到一個由執行在 JOS 中的 HTTP 伺服器提供的一個 web 頁面。

到目前為止,你的評級測試得分應該是 105 分(滿分為 105)。

小挑戰!在 JOS 中新增一個簡單的聊天伺服器,多個人可以連線到這個伺服器上,並且任何使用者輸入的內容都被髮送到其它使用者。為實現它,你需要找到一個一次與多個套接字通訊的方法,並且在同一時間能夠在同一個套接字上同時實現傳送和接收。有多個方法可以達到這個目的。lwIP 為 recv(檢視 net/lwip/api/sockets.c 中的 lwip_recvfrom)提供了一個 MSG_DONTWAIT 標誌,以便於你不斷地輪詢所有開啟的套接字。註意,雖然網路伺服器的 IPC 支援 recv 標誌,但是透過普通的 read 函式並不能訪問它們,因此你需要一個方法來傳遞這個標誌。一個更高效的方法是為每個連線去啟動一個或多個環境,並且使用 IPC 去協調它們。而且碰巧的是,對於一個套接字,在結構 Fd 中找到的 lwIP 套接字 ID 是全域性的(不是每個環境私有的),因此,比如一個 fork 的子環境繼承了它的父環境的套接字。或者,一個環境透過構建一個包含了正確套接字 ID 的 Fd 就能夠傳送到另一個環境的套接字上。

問題 3、由 JOS 的 web 伺服器提供的 web 頁面顯示了什麼?

問題 4、你做這個實驗大約花了多長的時間?

本實驗到此結束了。一如既往,不要忘了執行 make grade 並去寫下你的答案和挑戰問題的解決方案的描述。在你動手之前,使用 git status 和 git diff 去檢查你的變更,並不要忘了去 git add answers-lab6.txt。當你完成之後,使用 git commit -am 'my solutions to lab 6’ 去提交你的變更,然後 make handin 並關註它的動向。


via: https://pdos.csail.mit.edu/6.828/2018/labs/lab6/

作者:csail.mit[8] 選題:lujun9972 譯者:qhwdw 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

贊(0)

分享創造快樂