OSDI final project Network Driver

ss
14 min readMar 26, 2022

--

先前遇到一個很懂system kernel的人, 突然想要看一下我之前在OSDI的筆記

赫然發現medium上竟然沒有, 好險之前有存在hackmd過

圖檔以全部失效, 也沒有留了

已經是5年前的筆記了, 參考價值應該非常低

在此註記

原本是打算要做有關OS的排班演算法,或是同步問題等的相關題目,但是發覺這些東西並非短期之內可以做出個成果,自己本身跟網路比較有相關,於是突發奇想,乾脆來研究網路L1到L3OS是怎樣將header加上去的好了,結果發現這好像也非短期之內可以找到的結論,但在搜尋的過程中,發現MIT的LAB6就是在研究如何將封包收好後傳到主機裡,以及如何送出,看到這個我就決定把它做出來,並好好研究他是如何運作的

環境的話她有提供一個環境讓我們去下載,
這個環境跟我們這學期的NCTUOS很像,但細節上還是有著許多的差別,所以我重新花時間去理解,理解差異以及實作的方式不同之處,之後都懂了才開始
首先我們必須先去了解整個運作原理,才有辦法去知道我們需要跟要做的事情,首先先了解LAB將network server 分成四個部分去探討
分別是
1.core network server environment
2.input environment
3.output environment
4.timer environment

上面那張圖將我們的組織概念整個表現出來了,而綠色部分是我們要完成的地方(雖然我最後只有完成output的部分…….)

首先我們先看到 Core Network Server Environment

看圖我們可以很清楚地看到IPC Dispatch與lwIP network Stack,User 會向Nerworok server 發送IPC,IPC dispatch會通知LwIP提供的函式庫來處理這先需求並實現(lib/nsipc.c),但此LAB特地說明了,這個接口只能接受一個執行的指令,若dispatcher只能讓LwIP執行blocking calls(
例如accept或receive),那麼整個dispatcher會整個blocking住,但有提到解決方法就是用user level thread,當thread發生blocking的時候就可以先停掉換人做,這樣就能避免整個block

接下來介紹的是Output Environment

主要是當user 產生傳送的需求時,需要透過lwIP去實現,並且產生packet傳送,而這時會通過NSREQ_OUTPUT IPC去發送信息到這個output Environment並在這邊處理後發送到device去傳遞

Input Environment

在網卡拿到packet後會轉入這邊,我們需要再轉入給core server environment

Timer Environment

會定時的去提醒core server environment一個時間間隔過去了,而有些lwIP function 會需要這個提醒

我們先去了解環境,首先我們在QEMU會有一個network的stack以及一張E1000的網卡,並且QEMU會運行一個NAT網路讓我們的虛擬機有一個IP address,而我們的host端也可以與我們的虛擬機連線
## Timer的實作

這是第一個要我們完成的部分,其實這個對我們來說非常輕鬆因為我們這學期的某一個實驗就是要實作出timer,所以我這邊就不做多的介紹了,主要就是設好system call以及timer的function就行了

Network Interface Card

我們必須先去驅動這個E1000這個PCI(Peripheral Component Interconnect)的device,就是能插入主機板上PCI插槽的裝置,而這PCI的插槽允許CPU能直接與裝置去進行溝通,而我們也可以用I/O mapping的方式去控制我們的device,所以找到這個裝置並初始化它是我們現在要做的工作

PCI code會去尋找PCI bus找尋裝置,一旦找到了就會去讀取裝置的vender ID 與 device ID來確認裝置是否正確被讀取到,
而裝置的資料結構如下

struct pci_driver {
uint32_t key1, key2;
int (*attachfn) (struct pci_func *pcif);
};

key1就是vender ID,key2就是device ID,
pci_attach_vendor array存取的就會是我們需要安裝的device資訊,
如下圖

至於我們要怎麼去知道我們裝置的資訊,LAB有提供給我們一個SPEC要我們去讀
https://pdos.csail.mit.edu/6.828/2011/readings/hardware/8254x_GBe_SDM.pdf
而我們所需要的資料就放在Section 4.1的table裡
而我們在資料結構還看到一個PCI的function,而這function的結構如下

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與reg_size
reg_base存放memory_map I/O的記憶體位置,reg_size放的是memory區存放的大小,而我們會用pci_func_enalbe來去分配記憶體資源

當我們啟動這個fuction時,代表這個device已經偵測到,可我們仍須去初始化一些數據,讓我們去使用他

成功的話我們可以由測試畫面看到

裝置偵測就到這邊,接下來我們要寫如何去驅動裝置

— -
## Memory-mapped I/O
這邊其實我們再過LAPIC的時候就有過類似的經驗了,而我們接下來要在e1000.c去完成整個e1000_attach這個函式,pci_func_enable這個function在我們剛剛就已經完成了,由於上個function會分給裝置一個實體記憶體位置,我們需要去做一個虛擬記憶體位置映射到這上面的動作,這邊我們可以用之前用過的mmio_map_region來確保我們不會去蓋到別人

// memory map pci 
e1000 = mmio_map_region(pci->reg_base[0],pci->reg_size[0]);
cprintf(“E1000 status: %08x\n”,e1000[E1000_STATUS]);

DMA

由於通過E1000的register來收送封包會導致速度變得很慢(因為都是透過CPU去操作的),所以這邊e1000使用DMA,透過直接讀寫memory來加速,我們要做的就是做好分配接收陣列跟發送陣列在memory的分配以及建立DMA的descriptor,並且需讓e1000知道這些記憶體的位置,如此一來配置完成後,我們就能讓封包資料留在memory中,等到e1000可以送出的時候就直接去descriptor中直接取出並寄送,接收也是相同原理,將資料放在descriptor中等到要讀取時再取出。
聽起來很容易,但接收的真的太麻煩了…….我也還沒有弄出來

至於陣列的形式會建議用circuit array,來幫助自動重整閒置的descriptor

以下是建立descriptor array

struct tx_desc{ 
uint64_t addr;
uint16_t length;
uint8_t cso;
uint8_t cmd;
uint8_t status;
uint8_t css;
uint16_t special;
};
struct tx_desc tx_desc_array[64];//exercise5 it must 8 multi and can’t exceed 64

清空後就式接著一連串對這裝置的初始化工作,詳情需參考SPEC來慢慢配置

memset(tx_desc_array,0x0,sizeof(struct tx_desc)*64);
memset(tx_pkt_array,0x0,sizeof(struct tx_pkt)*64);
int i ;
for(i = 0;i < 64;i++)
{
tx_desc_array[i].addr = PADDR(tx_pkt_array[i].buf);
tx_desc_array[i].status |= E1000_TXD_STAT_DD;
}
// transnit init (from spec 14.5)
// the descriptor base addr reg
e1000[E1000_TDBAL] = PADDR(tx_desc_array);
e1000[E1000_TDBAH] = 0x0;
// head and tail reg
e1000[E1000_TDH] = 0x0;
e1000[E1000_TDT] = 0x0;
//e1000_tdt = &e1000[E1000_TDT];
//the len of TD SIZE
e1000[E1000_TDLEN] = sizeof(struct tx_desc)*64;

//init TCTL reg bit descript,spec at 13–76
e1000[E1000_TCTL] |= E1000_TCTL_EN;// Set the Enable (TCTL.EN) bit to 1b for normal operation.
e1000[E1000_TCTL] |= E1000_TCTL_PSP;//Set the Pad Short Packets (TCTL.PSP) bit to 1b.a
e1000[E1000_TCTL] &= 0x0000000f;//10h
e1000[E1000_TCTL] |= (0x10) << 4;//the ethernet stadard is 10h and the CT is at 4–11 bit
e1000[E1000_TCTL] &= ~E1000_TCTL_COLD;
e1000[E1000_TCTL] |= (0x40) << 12;//the cold is at 12–21 bit
// Program the Transmit IPG(Inter Packet Gap) Register,spec 13.4.34
e1000[E1000_TIPG] = 0x0;//init
e1000[E1000_TIPG] |= 0xA; // IPGR
e1000[E1000_TIPG] |= (0x6) << 20; // IPGR2 , a value of six should be added to the IPGR2 value
e1000[E1000_TIPG] |= (0x4) << 10; // IPGR1 the value of 2/3 IPGR2

Transmitting Packets

我們接著要做的是有關transmitting packets的發送,
初始化的部分已在上面完成,我們可以注意到descriptor的結構如下

63 48 47 40 39 32 31 24 23 16 15 0
+ — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -+
| Buffer address |
+ — — — — — — — -+ — — — -+ — — — -+ — — — -+ — — — -+ — — — — — — — -+
| Special | CSS | Status| Cmd | CSO | Length |
+ — — — — — — — -+ — — — -+ — — — -+ — — — -+ — — — -+ — — — — — — — -+

我們在上方已經有將descriptor去做一個map到packet buffer的動作,但是記得必須預先去預留buffer的位置,這邊做法有很多種我是直接去預留全部buffer的大小,就是一個封包*descriptor的大小

struct tx_pkt{
uint8_t buf[1518];//the max size of pkt
};
struct tx_pkt tx_pkt_array[64];

注意到這個descriptor包含了許多資訊,譬如status表示這個descriptor現在的狀態是否有封包尚未被處理之類的
而如果所有descriptor都被占用怎辦,這邊我試著先將封包丟棄,文中有提到或許可以讓他先停下等有descriptor被送出時再發中斷通知,但這對我來說難度有點高XD,就在這邊先做個懶人做法XD

int e1000_send_tx_desc(struct tx_desc *td){
//i = e1000[0x03818];
struct tx_desc *t = &tx_desc_array[i];
if(!(t->status & E1000_TXD_STAT_DD)){
cprintf(“td full\n”);
return -1;
}
//i =e1000[E1000_TDT];
cprintf(“i sent the pkg, priit E1000_tdt = %d\n”,i);
t = td;
t ->cmd |= (E1000_TXD_CMD_RS >>24);
i = (i+1)%64;
cprintf(“after the add e1000 TDT = %d\n”,i);
return 0;
}

最後最後,我們必須要去接收來自core network lwIP送過來的封包,判斷完類型,確定是要送出的封包就會透過我們設定好的system call送到裝置並送出
以下是其中的一小段

if (thisenv->env_ipc_value == NSREQ_OUTPUT) {
td.addr = (uint32_t)nsipcbuf.pkt.jp_data;
td.length = nsipcbuf.pkt.jp_len;
r = sys_tx_send(&td); }

其中接收的類型是NSREQ_OUTPUT的IPC msg,我LAB有提到我們可以在net/lwip/jos/jif/jif.c
裡面有提到

/*
* jif_output():
*
* This function is called by the TCP/IP stack when an IP packet
* should be sent. It calls the function called low_level_output() to
* do the actual transmission of the packet.
*
*/
static err_t
jif_output(struct netif *netif, struct pbuf *p,
struct ip_addr *ipaddr)
{
/* resolve hardware address, then send (or queue) packet */
return etharp_output(netif, p, ipaddr);
}

而每一個IPC msg,都會去包含這個jif_pkt

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

len就是指packet的長度,data就是packet內容,這樣的用法相當於 char * jp_data

結語

當這些都達成的時候(當然還有很多小細節要去調整,這邊就沒有再特別去說明了),我們可以試著用測試程式測看看自己的功能是否完整

雖然介紹起來好像非常清楚,但實際做起來真的要人命,我光PARTA就已經花了很多日夜的時間了,收封包的難度我覺得更高,加上還有其他的課程要做,所以就暫時先做到這邊了
抓到送出的封包

--

--

ss
ss

No responses yet