這是最後一個實驗,做完這個一個基本的內核就做完了。這章需要自己去看的東西特麼的多,所以大部分,我們就看看實現了什麼,不會專門一個個細節的看了。
Lab 6: Network Driver (default final project)
Introduction
Lab6
是最後一個實驗了,做完這個,一個簡單的內核就已經實現了,現在你可以自己做自己的內核。
現在,你有一個文件系統,操作系統沒有網絡堆棧。在這個實驗室裏你要編寫一個網絡接口卡的驅動程序。該卡將基於Intel 82540EM
芯片,也被稱爲E1000
上。
Getting Started
先切換個分支。
除了編寫驅動程序之外,您還需要創建一個系統調用接口來授予對驅動程序的訪問權限。您將實現缺少的網絡服務器代碼,以在網絡堆棧和驅動程序之間傳輸數據包。您還將通過完成Web
服務器將所有內容捆綁在一起。使用新的Web
服務器,您將能夠從文件系統提供文件。
您必須從頭開始編寫許多內核設備驅動程序代碼。與以前的實驗相比,本實驗提供的指導要少得多:沒有框架文件,沒有任何固定的系統調用接口,許多設計決策都由您自己決定。因此,我們建議您在開始任何練習之前,先閱讀整個作業記錄。許多學生髮現本實驗比以前的實驗困難得多,因此請相應地計劃您的時間。
最終你會發現這個主要難點就是看文檔寫驅動。
根據他的推薦讓我們先看看整個任務,直接用谷歌流浪器,翻譯整個頁面,然後大致看看。
看完之後發現並沒有什麼卵用,還是不懂,還是慢慢來。
QEMU’s virtual network
我們將使用QEMU
的用戶模式網絡堆棧,因爲它不需要運行任何管理權限。QEMU
的文檔在這裏有更多關於user-net
的信息。我們已經更新了makefile
,以啓用QEMU
的用戶模式網絡堆棧和虛擬E1000
網卡。
默認情況下,QEMU
提供運行在IP
10.0.2.2
上的虛擬路由器,並將爲JOS
分配IP
地址10.0.2.15
。爲了簡單起見,我們將這些默認值硬編碼到net/ns.h
中的網絡服務器中。
我們簡單看一下這個文件
#include <inc/ns.h>
#include <inc/lib.h>
#define IP "10.0.2.15" //IP
#define MASK "255.255.255.0" //ZIYANMA
#define DEFAULT "10.0.2.2" //這是個虛擬路由
#define TIMER_INTERVAL 250 //應該是時間中斷時間
// Virtual address at which to receive page mappings containing client requests.
//在這個虛擬地址接收 包含客戶端請求的 頁面映射。
#define QUEUE_SIZE 20
#define REQVA (0x0ffff000 - QUEUE_SIZE * PGSIZE)
/* timer.c */
void timer(envid_t ns_envid, uint32_t initial_to);
/* input.c */ /*這兩個函數是我們的目標,就是爲了實現這兩個函數*/
void input(envid_t ns_envid);
/* output.c */
void output(envid_t ns_envid);
儘管QEMU
的虛擬網絡允許JOS
進行到Internet
的任意連接,但JOS
的10.0.2.15
地址在QEMU
內部運行的虛擬網絡外部沒有任何意義(即QEMU
充當NAT
),因此我們無法直接連接到服務器即使在運行QEMU
的主機中,也可以在JOS
內部運行。爲了解決這個問題,我們將QEMU
配置爲在主機上某個端口上運行服務器,該服務器僅連接到JOS
中的某個端口,並在真實主機和虛擬網絡之間來回穿梭數據。
您將在端口7
(回顯)和80
(http)上運行JOS
服務器。爲避免在共享的Athena
機器上發生衝突,makefile
會根據您的用戶ID
爲這些機器生成轉發端口。要查找QEMU
將要轉發到您的開發主機上的端口,請運行make which-ports
。爲了方便起見,makefile
還提供make nc-7
和make nc-80
,使您可以直接與在終端中這些端口上運行的服務器進行交互。(這些目標僅連接到正在運行的QEMU
實例;您必須單獨啓動QEMU
本身。)
通俗點來講,就是 這個JOS服務器用的是 7 和80端口,但是你的虛擬機上面可能已經用了,所以幫你轉發到另一個端口了。
Packet Inspection
生成文件還配置QEMU
的網絡堆棧,以將所有傳入和傳出數據包記錄到您的實驗室目錄中的qemu.pcap
。
要獲取捕獲的數據包的hex/ASCII
,請使用tcpdump
,如下所示:
tcpdump -XXnr qemu.pcap
或者,您可以使用Wireshark
以圖形方式檢查pcap文
件。Wireshark
還知道如何解碼和檢查數百種網絡協議。如果您使用的是Athena
,則必須使用Wireshark
的前身ethereal
,它位於sipbnet locker
。
Debugging the E1000
我們很幸運能夠使用仿真硬件。由於E1000
在軟件中運行,因此仿真的E1000
可以以用戶可讀的格式向我們報告其內部狀態以及遇到的任何問題。通常,使用裸機編寫驅動程序的開發人員將無法獲得這種奢侈。
E1000
可以產生很多調試輸出,因此您必須啓用特定的日誌記錄通道。您可能會發現有用的一些渠道是:
Flag | Meaning |
---|---|
tx | 日誌包發送操作 |
txerr | 記錄傳輸環錯誤 |
rx | 將更改記錄到RCTL |
rxfilter | 傳入數據包的日誌過濾 |
rxerr | 日誌接收振鈴錯誤 |
unknown | 日誌讀取和寫入未知寄存器 |
eeprom | 從EEPROM讀取日誌 |
interrupt | 記錄中斷和更改到中斷寄存器。 |
例如,要啓用tx
和txerr
日誌記錄,請使用make E1000_DEBUG=tx,txerr ...
。
注意: E1000_DEBUG
標誌僅在6.828
版本的QEMU
中起作用。
您可以進一步使用軟件仿真的硬件進行調試。如果您陷入困境並且不瞭解E1000
爲什麼沒有按預期方式做出響應,則可以在hw/net/e1000.c
中查看QEMU
的E1000
實現。
The Network Server
從頭開始編寫網絡堆棧是一項艱鉅的工作。相反,我們將使用lwIP
,這是一個開源的輕量級TCP/IP
協議套件,其中包括一個網絡堆棧。您可以在此處找到有關lwIP
的更多信息 。就此而言,就我們而言,lwIP
是一個黑箱,它實現了BSD
套接字接口,並具有一個數據包輸入端口和一個數據包輸出端口。
網絡服務器實際上是四個環境的組合:
- 核心網絡服務器環境(包括套接字調用分派器和
lwIP
) - 輸入環境
- 輸出環境
- 計時器環境
下圖顯示了不同的環境及其關係。該圖顯示了包括設備驅動程序在內的整個系統,稍後將進行介紹。在本實驗中,您將實現以綠色突出顯示的部分。
這個地方已經告訴你我們要實現什麼了
- 實現
E1000
驅動裏面的TX
用於傳輸數據,RX
用於發送數據。 - 實現
發送環境
和輸出環境
,時鐘環境
已經幫我們實現好了,我們後面會去看看 http
服務器,這些事具體應用服務器了。
The Core Network Server Environment
核心網絡服務器環境由套接字調用分派器和lwIP
本身組成。套接字調用調度程序的工作方式與文件服務器完全相同。用戶環境使用存根(可在lib/nsipc.c
中找到)將IPC
消息發送到核心網絡環境。如果查看 lib/nsipc.c
,您會發現我們找到核心網絡服務器的方式與找到文件服務器的方式相同:i386_init
使用NS_TYPE_NS
創建NS
環境,因此我們掃描envs
,尋找這種特殊的環境類型。對於每個用戶環境IPC
,網絡服務器中的調度程序代表用戶調用lwIP
提供的相應BSD
套接字接口功能。
我們來簡單看看這些東西。大部分都是一樣的,我們就看看就行。
// Virtual address at which to receive page mappings containing client requests.
#define REQVA 0x0ffff000
union Nsipc nsipcbuf __attribute__((aligned(PGSIZE)));
// Send an IP request to the network server, and wait for a reply.
// The request body should be in nsipcbuf, and parts of the response
// may be written back to nsipcbuf.
// type: request code, passed as the simple integer IPC value.
// Returns 0 if successful, < 0 on failure.
static int
nsipc(unsigned type) //和 文件發送一模一樣,自己看看
{
static envid_t nsenv;
if (nsenv == 0)
nsenv = ipc_find_env(ENV_TYPE_NS);
static_assert(sizeof(nsipcbuf) == PGSIZE);
if (debug)
cprintf("[%08x] nsipc %d\n", thisenv->env_id, type);
ipc_send(nsenv, type, &nsipcbuf, PTE_P|PTE_W|PTE_U);
return ipc_recv(NULL, NULL, NULL);
}
/*
struct sockaddr {
u8_t sa_len;
u8_t sa_family;
char sa_data[14];
};
*/
int
nsipc_accept(int s, struct sockaddr *addr, socklen_t *addrlen) //接受函數
{
int r;
nsipcbuf.accept.req_s = s;
nsipcbuf.accept.req_addrlen = *addrlen;
if ((r = nsipc(NSREQ_ACCEPT)) >= 0) {
struct Nsret_accept *ret = &nsipcbuf.acceptRet;
memmove(addr, &ret->ret_addr, ret->ret_addrlen);
*addrlen = ret->ret_addrlen;
}
return r;
}
再看看init.c
,多了這麼幾行,看架勢是創建了一個網絡服務器。
#if !defined(TEST_NO_NS)
// Start ns.
ENV_CREATE(net_ns, ENV_TYPE_NS);
#endif
不出意外我們在net/serv.c
成功找到了umian
void
umain(int argc, char **argv)
{
envid_t ns_envid = sys_getenvid();
binaryname = "ns";
// fork off the timer thread which will send us periodic messages
timer_envid = fork();//創建定時器
if (timer_envid < 0)
panic("error forking");
else if (timer_envid == 0) {
timer(ns_envid, TIMER_INTERVAL);
return;
}
// fork off the input thread which will poll the NIC driver for input
// packets
input_envid = fork();//輸入環境
if (input_envid < 0)
panic("error forking");
else if (input_envid == 0) {
input(ns_envid);
return;
}
// fork off the output thread that will send the packets to the NIC
// driver
output_envid = fork();//輸出環境
if (output_envid < 0)
panic("error forking");
else if (output_envid == 0) {
output(ns_envid);
return;
}
// lwIP requires a user threading library; start the library and jump
// into a thread to continue initialization.
thread_init();//線程初始化 //做實現開始之前回來好好分析一下
thread_create(0, "main", tmain, 0);//線程創建
thread_yield();//線程調度???
// never coming here!
}
常規用戶環境不會nsipc_*
直接使用呼叫。相反,它們使用lib/ sockets.c
中的函數,該函數提供了基於文件描述符的套接字API
。因此,用戶環境通過文件描述符引用套接字,就像它們引用磁盤文件一樣。多個操作(connect
,accept
等)特定於插座,但是read
,write
和 close
經過在正常文件描述符設備分派代碼lib/fd.c
。就像文件服務器爲所有打開的文件維護內部唯一ID
的方式一樣,lwIP
還會爲所有打開的套接字生成唯一的ID
。在文件服務器和網絡服務器中,我們都使用存儲在其中的信息將struct Fd
每個環境的文件描述符映射到這些唯一的ID
空間。
我們去看看lib/sockets.c
和前面的文件服務調用的接口也是一樣的。
即使文件服務器和網絡服務器的IPC
調度程序看起來似乎相同,也存在關鍵區別。BSD
套接字調用like accept
和recv
可以無限期阻塞。如果調度程序要讓lwIP
執行這些阻塞調用之一,則調度程序也將阻塞,並且整個系統一次只能有一個未完成的網絡調用。由於這是不可接受的,因此網絡服務器使用用戶級線程來避免阻塞整個服務器環境。對於每個傳入的IPC
消息,調度程序都會創建一個線程並在新創建的線程中處理請求。如果線程阻塞,則只有該線程進入睡眠狀態,而其他線程繼續運行。
除了核心網絡環境外,還有三個幫助程序環境。除了接受來自用戶應用程序的消息外,核心網絡環境的調度程序還接受來自輸入和計時器環境的消息。
核心服務器環境,本質上就是一個文件服務器,他負責和高層的數據交換,比如說,http
要用socket.c
,就調用socket.c
裏面的一個操作,然後進行轉發傳到輸入/輸出
環境,他在在E1000
來進行硬件操作。
The Output Environment
爲用戶環境套接字調用提供服務時,lwIP
將生成數據包供網卡傳輸。LwIP
將使用NSREQ_OUTPUTIPC
消息將每個要發送的數據包發送到輸出幫助程序環境,並將該數據包附加在IPC
消息的page
參數中。輸出環境負責接受這些消息,並通過即將創建的系統調用接口將數據包轉發到設備驅動程序。
The Input Environment
網卡收到的數據包需要注入lwIP
。對於設備驅動程序收到的每個數據包,輸入環境(使用您將實現的內核系統調用)將數據包拉出內核空間,然後使用NSREQ_INPUTIPC
消息將數據包發送到核心服務器環境。
數據包輸入功能與核心網絡環境分開,因爲JOS
使其難以同時接受IPC
消息以及輪詢或等待來自設備驅動程序的數據包。我們select
在JOS
中沒有系統調用,該調用允許環境監視多個輸入源以標識準備好處理哪些輸入。
如果你看看net/input.c
和net/output.c
你會看到,都需要執行。這主要是因爲實現取決於您的系統調用接口。在實現驅動程序和系統調用接口之後,將爲兩個幫助程序環境編寫代碼。
The Timer Environment
計時器環境會定期向NSREQ_TIMER
核心網絡服務器發送消息類型,通知其計時器已過期。lwIP
使用此線程的計時器消息來實現各種網絡超時。
通過這些我們大致知道這個網絡的流程了,實際上核心服務器和文件服務器是一模一樣的,讓我們再做一次實際上也就是把上次的代碼在看一遍。至於輸出環境,輸入環境和時鐘環境,就是讓我們實現的東西。
前置代碼分析
到這個地方,我們已經知道了基本的結構,但是我們還是對代碼沒啥瞭解。所以我們來看看多的代碼做了什麼。
一如既往,一切的起點,肯定init
,前面我們已經看過一點了。
// Lab 6 hardware initialization functions//多了這些東西,看註釋事硬件初始化
time_init(); //這個後面第一個實驗就會講是什麼,是給內核添加時鐘的概念用的
pci_init(); //這個是 PCI初始化,也就是搜索所有 用PCI連接的硬件
#if !defined(TEST_NO_NS)
// Start ns.
ENV_CREATE(net_ns, ENV_TYPE_NS);//這個說過了就是核心環境啓動,而且通過這個fork 除了 輸入/輸出/時鐘環境
#endif
我們知道這些之後,我們再去看看net
裏面的東西.
我靠一進去看裏面的lwip
目錄,我靠那麼多東西,看個鬼,告辭。我們還是繼續看看serv.c
,這個input.c
和output.c
,是輸入輸出,後面主要要做的。
一開始我們已經看了一部分,我們直接看看這個線程
。
thread_init();
thread_create(0, "main", tmain, 0);
thread_yield();
//lwpic/jos/thread.c
void
thread_init(void) {
threadq_init(&thread_queue);//進去看這個函數
max_tid = 0;
}
//lwpic/jos/threadq.h
static inline void
threadq_init(struct thread_queue *tq)
{
tq->tq_first = 0;
tq->tq_last = 0;
}
struct thread_context;//一個這個表示一個進程
struct thread_queue //一個線程池,或許應該叫線程隊列
{
struct thread_context *tq_first;
struct thread_context *tq_last;
};
struct thread_context { //線程結構題 也就是TCB
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;
};
然後我們運行了線程創建
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));//分配一個空間
if (!tc)
return -E_NO_MEM;
memset(tc, 0, sizeof(struct thread_context));
thread_set_name(tc, name);//這個不用多說了
tc->tc_tid = alloc_tid();//自己看
tc->tc_stack_bottom = malloc(stack_size);//每個線程應該有獨立的棧,但是一個進程的線程內存是共享的,因爲共用一個頁表。 很明顯的能夠看出來,TCB沒有頁表,所以內存都是共享的,所以理論上來說,是可以跨線程訪問棧的。
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;//初始化棧頂
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)
return;
threadq_push(&thread_queue, cur_tc);//保存當前線程
}
cur_tc = next_tc;
jos_longjmp(&cur_tc->tc_jb, 1);//將下一個線程對應的thread_context結構的tc_jb字段恢復到CPU繼續執行
}
//所以從這個地方就跑去了運行線程main函數了。
static void
tmain(uint32_t arg) {
serve_init(inet_addr(IP),
inet_addr(MASK),
inet_addr(DEFAULT));//初始化了一點東西
serve();//然後就是這個服務了
}
serve()
裏面主要是和另外兩個環境通信。
void
serve(void) {
int32_t reqno;
uint32_t whom;
int i, perm;
void *va;
while (1) {
// ipc_recv will block the entire process, so we flush
// all pending work from other threads. We limit the
// number of yields in case there's a rogue thread.
for (i = 0; thread_wakeups_pending() && i < 32; ++i)
thread_yield();
perm = 0;
va = get_buffer();
reqno = ipc_recv((int32_t *) &whom, (void *) va, &perm);//在這個地方進行通信
if (debug) {
cprintf("ns req %d from %08x\n", reqno, whom);
}
// first take care of requests that do not contain an argument page
if (reqno == NSREQ_TIMER) {//這個就是如果通信來自時鐘
process_timer(whom);
put_buffer(va);
continue;
}
// All remaining requests must contain an argument page
if (!(perm & PTE_P)) {
cprintf("Invalid request from %08x: no argument page\n", whom);
continue; // just leave it hanging...
}
// Since some lwIP socket calls will block, create a thread and
// process the rest of the request in the thread.
struct st_args *args = malloc(sizeof(struct st_args));
if (!args)
panic("could not allocate thread args structure");
args->reqno = reqno;
args->whom = whom;
args->req = va;
thread_create(0, "serve_thread", serve_thread, (uint32_t)args);//給他創建一個線程去處理。
thread_yield(); // let the thread created run
}
}
在serve()
經歷了一大堆,最終處理事件的函數是serve_thread
了,可以在裏面明確的看出是啥。
static void
serve_thread(uint32_t a) {
struct st_args *args = (struct st_args *)a;
union Nsipc *req = args->req;
int r;
switch (args->reqno) {
case NSREQ_ACCEPT:
{
struct Nsret_accept ret;
ret.ret_addrlen = req->accept.req_addrlen;
r = lwip_accept(req->accept.req_s, &ret.ret_addr,
&ret.ret_addrlen);
memmove(req, &ret, sizeof ret);
break;
}
case NSREQ_BIND:
r = lwip_bind(req->bind.req_s, &req->bind.req_name,
req->bind.req_namelen);
break;
case NSREQ_SHUTDOWN:
r = lwip_shutdown(req->shutdown.req_s, req->shutdown.req_how);
break;
case NSREQ_CLOSE:
r = lwip_close(req->close.req_s);
break;
case NSREQ_CONNECT:
r = lwip_connect(req->connect.req_s, &req->connect.req_name,
req->connect.req_namelen);
break;
case NSREQ_LISTEN:
r = lwip_listen(req->listen.req_s, req->listen.req_backlog);
break;
case NSREQ_RECV:
// Note that we read the request fields before we
// overwrite it with the response data.
r = lwip_recv(req->recv.req_s, req->recvRet.ret_buf,
req->recv.req_len, req->recv.req_flags);
break;
case NSREQ_SEND:
r = lwip_send(req->send.req_s, &req->send.req_buf,
req->send.req_size, req->send.req_flags);
break;
case NSREQ_SOCKET:
r = lwip_socket(req->socket.req_domain, req->socket.req_type,
req->socket.req_protocol);
break;
case NSREQ_INPUT:
jif_input(&nif, (void *)&req->pkt);
r = 0;
break;
default:
cprintf("Invalid request code %d from %08x\n", args->whom, args->req);
r = -E_INVAL;
break;
}
if (r == -1) {
char buf[100];
snprintf(buf, sizeof buf, "ns req type %d", args->reqno);
perror(buf);
}
if (args->reqno != NSREQ_INPUT)
ipc_send(args->whom, r, 0, 0);
put_buffer(args->req);
sys_page_unmap(0, (void*) args->req);
free(args);
}
然後就從其中調用了lwip
的一些函數,這個裏面有一個socket.c
和lib/socket.c
有點不一樣,也不知道有啥區別,個人覺得是lib/socket.c
是系統裏面的調用給用戶用的這個文件裏面的應該是進行底層調用的。具體就不分析了,有興趣的自己去看看。
其他三個環境後面再看。
Part A: Initialization and transmitting packets
您的內核沒有時間概念,因此我們需要添加它。當前,硬件每10
毫秒產生一次時鐘中斷。在每個時鐘中斷處,我們都可以增加一個變量以指示時間提前了10
ms。這是在kern/ time.c
中實現的,但尚未完全集成到您的內核中。
不着急做實驗,我們先去看看kern/ time.c
#include <kern/time.h>
#include <inc/assert.h>
static unsigned int ticks;
void
time_init(void)//初始化時鐘
{
ticks = 0;
}
// This should be called once per timer interrupt. A timer interrupt
// fires every 10 ms.
void
time_tick(void)//時間增加
{
ticks++;
if (ticks * 10 < ticks)
panic("time_tick: time overflowed");
}
unsigned int
time_msec(void)
{
return ticks * 10;//返回時間
}
看了這個練習1
就簡單了。練習1
就是讓我們把他加入內核。我們已經在內核裏面初始化了,現在我們需要時鐘跳動。那麼什麼時候時鐘增加呢。我們已經實現了時鐘中斷,所以我們在這個時候調用就行了。另外一個添加一個系統調用獲取時鐘就行了。
case IRQ_OFFSET + IRQ_TIMER:{
lapic_eoi();
time_tick();//時鐘中斷 時鐘增加
sched_yield();
break;
}
// Return the current time.
static int
sys_time_msec(void)//獲取時鐘
{
// LAB 6: Your code here.
//panic("sys_time_msec not implemented");
return time_msec();
}
//這個絕對不要完了再syscall()裏面添加
case SYS_time_msec:
return sys_time_msec();
我們現在可以實現是時鐘環境,我們去看看net/time.c
#include "ns.h"
void
timer(envid_t ns_envid, uint32_t initial_to) {
int r;
uint32_t stop = sys_time_msec() + initial_to;
binaryname = "ns_timer";
while (1) {
while((r = sys_time_msec()) < stop && r >= 0) {//沒到到時間
sys_yield();
}
if (r < 0)
panic("sys_time_msec: %e", r);
ipc_send(ns_envid, NSREQ_TIMER, 0, 0);//到了時鐘就給核心服務程序發了一個信息
while (1) {
uint32_t to, whom;
to = ipc_recv((int32_t *) &whom, 0, 0);
if (whom != ns_envid) {
cprintf("NS TIMER: timer thread got IPC message from env %x not NS\n", whom);
continue;
}
stop = sys_time_msec() + to;//時鐘改變
break;
}
}
}
The Network Interface Card
編寫驅動程序需要深入瞭解硬件和提供給軟件的接口。該實驗文本將提供有關如何與E1000
進行交互的高級概述,但是您在編寫驅動程序時需要充分利用Intel
的手冊。
練習2
讓我門看看手冊。因爲是全英文的又不能翻譯所以沒看。後面告訴我們需要什麼我們去看什麼。
後面纔是真的魔鬼。
PCI Interface
E1000
是PCI
設備,這意味着它已插入主板上的PCI
總線。PCI
總線具有地址,數據和中斷線,並允許CPU
與PCI
設備進行通信,並且PCI
設備可以讀寫存儲器。在使用PCI
設備之前,需要先對其進行發現和初始化。發現是遍歷PCI
總線以查找連接的設備的過程。初始化是分配I/O
和內存空間以及協商設備要使用的IRQ
線的過程。
我們在kern/pci.c
中爲您提供了PCI代碼。要在引導過程中執行PCI
初始化,PCI
代碼將遍歷PCI
總線以查找設備。找到設備後,它將讀取其供應商ID
和設備ID
,並將這兩個值用作搜索pci_attach_vendor
陣列的鍵。該數組由以下struct pci_driver
條目組成 :
struct pci_driver {
uint32_t key1, key2;
int (*attachfn) (struct pci_func *pcif);
};
如果發現的設備的供應商ID
和設備ID
與陣列中的條目匹配,則PCI
代碼將調用該條目的attachfn
來執行設備初始化。(設備也可以通過類來標識,這是kern/pci.c
中其他驅動程序表的作用。)
Attach
函數通過PCI
函數進行初始化。儘管E1000
僅提供一種功能,但PCI
卡可以提供多種功能。這是我們在JOS
中表示PCI
功能的方式:
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;
};
以上結構反映了開發人員手冊第4.1
節表4-1
中的某些條目。(大家可以去看看)後三個條目 struct pci_func
對我們特別有意義,因爲它們記錄了設備的協商內存,I/O
和中斷資源。在reg_base
與reg_size
陣列包含多達六個基地址寄存器或條信息。reg_base
存儲用於內存映射的I/O
區域(或用於I/O
端口資源的基本I/O
端口)的基本內存地址, reg_size
包含來自的相應基本值的字節大小或I/O
端口數reg_base
,並irq_line
包含分配給設備的IRQ
線路用於中斷。E1000 BAR
的具體含義在表4-2
的後半部分給出。
調用設備的附加功能時,已找到該設備但尚未啓用。這意味着PCI
代碼尚未確定分配給設備的資源,例如地址空間和IRQ
線,因此該struct pci_func
結構的最後三個元素尚未填寫。attach
函數應調用 pci_func_enable
,將啓用設備,協商這些資源並填寫struct pci_func
。
看到這個時候應該和我一樣雲裏霧裏的,這他媽都在講些啥啊。
我們簡單來說,我們現在需要把設備啓動,然後把供應商ID和設備ID對上號,然後需要一個函數啓動這個設備。怎麼初始化,怎麼啓動,先不去管他。
我們來分析pci_init
怎麼執行的。
int
pci_init(void)
{
static struct pci_bus root_bus;//這是個總線結構體就是他提供的。
/*
struct pci_bus {
struct pci_func *parent_bridge;
uint32_t busno;//總線號,因爲可能存在多總線
};
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;
};
*/
memset(&root_bus, 0, sizeof(root_bus));
return pci_scan_bus(&root_bus);//然後開始掃描
}
static int
pci_scan_bus(struct pci_bus *bus)
{
int totaldev = 0;
struct pci_func df;
memset(&df, 0, sizeof(df));
df.bus = bus;
for (df.dev = 0; df.dev < 32; df.dev++) {
uint32_t bhlc = pci_conf_read(&df, PCI_BHLC_REG);//在df裏面找PCI_BHLC_REG ,具體就不用去關心了
if (PCI_HDRTYPE_TYPE(bhlc) > 1) // Unsupported or no device不支持設備或者沒有這個設備
continue;
totaldev++;//設備數+1
struct pci_func f = df;
for (f.func = 0; f.func < (PCI_HDRTYPE_MULTIFN(bhlc) ? 8 : 1);
f.func++) {
struct pci_func af = f;
af.dev_id = pci_conf_read(&f, PCI_ID_REG);//讀取ID
if (PCI_VENDOR(af.dev_id) == 0xffff)
continue;
uint32_t intr = pci_conf_read(&af, PCI_INTERRUPT_REG);//讀取中斷
af.irq_line = PCI_INTERRUPT_LINE(intr);
af.dev_class = pci_conf_read(&af, PCI_CLASS_REG);//讀取class
if (pci_show_devs)//打印獲取到的設備信息
pci_print_func(&af);
pci_attach(&af);//這個函數我們進去看看
}
}
return totaldev;
}
static int
pci_attach(struct pci_func *f)
{
return
pci_attach_match(PCI_CLASS(f->dev_class),
PCI_SUBCLASS(f->dev_class),
&pci_attach_class[0], f) ||
pci_attach_match(PCI_VENDOR(f->dev_id),
PCI_PRODUCT(f->dev_id),
&pci_attach_vendor[0], f);
}
pci_attach_match(uint32_t key1, uint32_t key2,
struct pci_driver *list, struct pci_func *pcif)
{
uint32_t i;
for (i = 0; list[i].attachfn; i++) {
if (list[i].key1 == key1 && list[i].key2 == key2) {//如果匹配上了
int r = list[i].attachfn(pcif);//這樣去運行了
if (r > 0)
return r;
if (r < 0)
cprintf("pci_attach_match: attaching "
"%x.%x (%p): e\n",
key1, key2, list[i].attachfn, r);
}
}
return 0;
}
簡單思考了下,pci_init
應該就是掃描了一下總線把總線裏面的所有設備,然後初始化了他們,然後返回了總共的設備數量。
在 pci_attach
我們調用了pci_attach_vendor
,我們看到這個東西,現在裏面啥都沒有。所以我們現在要做的就是把我們的網卡驅動
添進去初始化。
練習3
然我們添加他,並添加初始化函數。
我們運行內核很容易看出來網卡的信息。
同樣我們在文檔5.1節
的表裏找到了這個東西
那麼還有個問題,廠商號、設備號有了,怎麼初始化????實驗的要求是讓我寫在e1000.h
和e1000.c
先不管這些,我們先把函數定義好。
先在e1000.h
裏面定義
#include <kern/pci.h>
int e1000_init(struct pci_func *pcif);
//記得先把在 pic.c裏面添加頭文件 #include <kern/e1000.h>
//然後修改pci_driver
// pci_attach_vendor matches the vendor ID and device ID of a PCI device. key1
// and key2 should be the vendor ID and device ID respectively
#define PCI_E1000_VENDOR_ID 0x8086
#define PCI_E1000_DEVICE_ID 0x100E
struct pci_driver pci_attach_vendor[] = {
{ PCI_E1000_VENDOR_ID, PCI_E1000_DEVICE_ID, &e1000_init},
{ 0, 0, 0 },
};
在我萬般無奈的時候看到了一句練習裏面的提示For now, just enable the E1000 device via pci_func_enable. We'll add more initialization throughout the lab.
你他媽在逗我,告辭,兩行解決。
uint32_t *pci_e1000;
int
e1000_init(struct pci_func *pcif)
{
pci_func_enable(pcif);
return 1;
}
因爲會用到其他頭文件的的函數,所以先把頭文件加入好,最終會用到
#include <kern/e1000.h>
#include <kern/pmap.h>
#include <inc/string.h>
出現頭文件問題自己去看看少了啥。
軟件通過內存映射的I/O
(MMIO
)與E1000
通信。您在JOS中已經看過兩次了:CGA
控制檯和LAPIC
都是通過寫入和讀取“內存”來控制和查詢的設備。但是這些讀和寫操作不會存儲到DRAM
中。他們直接去這些設備。
pci_func_enable
與E1000
協商MMIO
區域,並將其基數和大小存儲在BAR 0
(即 reg_base[0]
和reg_size[0]
)中。這是分配給設備的一系列物理內存地址,這意味着您必須做一些事情才能通過虛擬地址訪問它。由於MMIO
區域分配了很高的物理地址(通常大於3GB
),KADDR
因此由於JOS
的256MB
限制,您不能使用它來訪問它。因此,您必須創建一個新的內存映射。我們將使用MMIOBASE
上方的區域(您 mmio_map_region
在實驗4中將確保我們不會覆蓋LAPIC
使用的映射)。由於PCI
設備初始化發生在JOS
創建用戶環境之前,因此您可以在其中創建映射,kern_pgdir
並且該映射將始終可用。
練習4
實現mmio_map_region
爲E1000
的BAR 0
創建虛擬內存映射,lapic = mmio_map_region(lapicaddr, 4096);
仿着這個寫一個。然後讓我們打印狀態,但是狀態在哪。後面給了提示
提示:您將需要很多常量,例如寄存器的位置和位掩碼的值。嘗試將這些內容從開發人員手冊中複製出來很容易出錯,而錯誤可能導致痛苦的調試會話。我們建議改用QEMU的e1000_hw.h
標頭作爲指導。我們不建議逐字複製它,因爲它定義的內容遠遠超出您的實際需要,並且可能無法按照您需要的方式進行定義,但這是一個很好的起點。
我們下載那個文件,然後ctrl+f
查找statu
找到了這個#define E1000_STATUS 0x00008 /* Device Status - RO */
,所以添進去就行了。
所以隨便添加一點就行了。
uint32_t *pci_e1000;
#define E1000_STATUS 0x00008 /* Device Status - RO 建議寫到頭文件裏面*/
int
e1000_init(struct pci_func *pcif)
{
pci_func_enable(pcif);
pci_e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]);
cprintf("the E1000 status register: [%08x]\n", *(pci_e1000+(E1000_STATUS>>2)));
return 1;
}
DMA
您可以想象通過寫入和讀取E1000
的寄存器來發送和接收數據包,但這會很慢,並且需要E1000
在內部緩衝數據包數據。相反,E1000
使用直接內存訪問或DMA
直接從內存讀取和寫入數據包數據,而無需使用CPU
。驅動程序負責爲發送和接收隊列分配內存,設置DMA
描述符,並使用這些隊列的位置配置E1000
,但之後的所有操作都是異步的。爲了發送數據包,驅動程序將其複製到發送隊列中的下一個DMA
描述符中,並通知E1000
另一個數據包可用。當有時間發送數據包時,E1000
會將數據從描述符中複製出來。同樣,當E1000
接收到一個數據包時,它會將其複製到接收隊列中的下一個DMA
描述符中,驅動程序可以在下一次機會讀取該描述符。
在高層,接收和發送隊列非常相似。兩者都由一系列描述符組成。儘管這些描述符的確切結構有所不同,但是每個描述符都包含一些標誌和包含數據包數據的緩衝區的物理地址(或者是要發送給卡的數據包數據,或者是OS
爲卡分配的緩衝區,用於將接收到的數據包寫入卡)。
隊列被實現爲圓形陣列,這意味着當卡或驅動程序到達陣列的末尾時,它會迴繞到開頭。兩者都有一個頭指針和一個尾指針隊列的內容是這兩個指針之間的描述符。硬件始終從頭消耗描述符並移動頭指針,而驅動程序總是向描述符添加描述符並移動尾指針。傳輸隊列中的描述符表示等待發送的數據包(因此,在穩定狀態下,傳輸隊列爲空)。對於接收隊列,隊列中的描述符是卡可以接收數據包的空閒描述符(因此,在穩定狀態下,接收隊列由所有可用的接收描述符組成)。在不混淆E1000
的情況下正確更新尾部寄存器非常棘手;小心!
這個隊列是個圈,也就是取個模
指向這些數組的指針以及描述符中的數據包緩衝區的地址都必須是物理地址, 因爲硬件無需通過MMU
即可直接在物理RAM
之間進行DMA
操作。
簡單來說就是給一塊內存用作緩衝區,讓硬件能夠直接訪問DMA
Transmitting Packets
E1000
的發送和接收功能基本上彼此獨立,因此我們可以一次完成一個工作。我們將首先攻擊發送數據包的原因僅僅是因爲我們無法在不發送“我在這裏!”的情況下測試接收。數據包優先。
首先,您必須按照14.5節
中所述的步驟初始化要傳輸的卡(不必擔心這些小節)。傳輸初始化的第一步是設置傳輸隊列。隊列的精確結構在3.4節
中描述,描述符的結構在3.3.3節
中描述。我們將不會使用E1000
的TCP
卸載功能,因此您可以專注於“舊版傳輸描述符格式”。您現在應該閱讀這些部分,並熟悉這些結構。
C Structures
您會發現使用C struct
來描述E1000
的結構很方便。如您所見struct Trapframe
,使用C struct
等結構可以 使您精確地在內存中佈置數據。C
可以在字段之間插入填充,但是E1000
的結構佈局使得這不成問題。如果確實遇到字段對齊問題,請查看GCC
的“打包”屬性。
例如,請考慮手冊表3-8
中給出並在此處複製的舊版傳輸描述符:
63 48 47 40 39 32 31 24 23 16 15 0 +---------------------------------------------------------------+ | Buffer address | +---------------+-------+-------+-------+-------+---------------+ | Special | CSS | Status| Cmd | CSO | Length | +---------------+-------+-------+-------+-------+---------------+
結構的第一個字節從右上角開始,因此要將其轉換爲C struct
,從右到左,從上到下讀取。如果佈局正確,您會發現所有字段甚至都非常適合標準大小的類型:
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
字節,這限制了這些緩衝區的大小。更復雜的驅動程序可以動態分配數據包緩衝區(例如,以在網絡使用率較低時減少內存開銷),甚至可以傳遞用戶空間直接提供的緩衝區(一種稱爲“零複製”的技術),但是最好還是從簡單開始。
練習5
執行第14.5節
(但不包括其小節)中描述的初始化步驟。使用第13節
作爲初始化過程所引用的寄存器的參考,並使用3.3.3
和3.4
節作爲發送描述符和發送描述符數組的參考。
請注意對發送描述符數組的對齊要求以及對該數組長度的限制。由於TDLEN
必須對齊128
字節,每個傳輸描述符爲16
字節,因此您的傳輸描述符數組將需要8
個傳輸描述符的某個倍數。但是,請勿使用超過64
個的描述符,否則我們的測試將無法測試傳輸環溢出。
對於TCTL.COLD
,您可以假定爲全雙工操作。對於TIPG
,請參閱第13.4.34節
的表13-77
中描述的IEEE 802.3
標準IPG
的默認值(不要使用第14.5節
的表中的值)。
。。。對於這個,我真看不懂是啥。
按照14.5節
的描述初始化。步驟如下:
- 分配一塊內存用作發送描述符隊列,起始地址要
16
字節對齊。用基地址填充(TDBAL/TDBAH
) 寄存器。 - 設置
(TDLEN)
寄存器,該寄存器保存發送描述符隊列長度,必須128
字節對齊。 - 設置
(TDH/TDT)
寄存器,這兩個寄存器都是發送描述符隊列的下標。分別指向頭部和尾部。應該初始化爲0
。 - 初始化
TCTL
寄存器。設置TCTL.EN
位爲1
,設置TCTL.PSP
位爲1
。設置TCTL.CT
爲10h
。設置TCTL.COLD
爲40h
。 - 設置
TIPG
寄存器。
我們先把這些東西加e1000.h
中,在把結構定義出來。
#define E1000_TCTL 0x00400 /* TX Control - RW */
#define E1000_TDBAL 0x03800 /* TX Descriptor Base Address Low - RW */
#define E1000_TDBAH 0x03804 /* TX Descriptor Base Address High - RW */
#define E1000_TDLEN 0x03808 /* TX Descriptor Length - RW */
#define E1000_TDH 0x03810 /* TX Descriptor Head - RW */
#define E1000_TDT 0x03818 /* TX Descripotr Tail - RW */
#define E1000_TIPG 0x00410 /* TX Inter-packet gap -RW */
#define E1000_TCTL_EN 0x00000002 /* enable tx */
#define E1000_TCTL_BCE 0x00000004 /* busy check enable */
#define E1000_TCTL_PSP 0x00000008 /* pad short packets */
#define E1000_TCTL_CT 0x00000ff0 /* collision threshold */
#define E1000_TCTL_COLD 0x003ff000 /* collision distance */
#define E1000_TXD_CMD_RS 0x08000000 /* Report Status */
#define E1000_TXD_STAT_DD 0x00000001 /* Descriptor Done */
#define E1000_TXD_CMD_EOP 0x01000000 /* End of Packet */
#define TX_MAX 64 //發送包的最大數量
#define BUFSIZE 2048
struct tx_desc
{
uint64_t addr;
uint16_t length;
uint8_t cso;
uint8_t cmd;
uint8_t status;
uint8_t css;
uint16_t special;
}__attribute__((packed));
struct tx_desc tx_list[TX_MAX];//描述符
struct packets{
char buffer[BUFSIZE];//16對齊
}__attribute__((packed));
struct packets tx_buf[TX_MAX];//緩衝區
具體實現,我只是看別人的看懂了。。。。
//這個初始化函數是要在前面那個初始化e1000_init裏面調用,不然不會運行
void
e1000_transmit_init(){
//初始化
memset(tx_list, 0, sizeof(struct tx_desc)*TX_MAX);
memset(tx_buf, 0, sizeof(struct packets)*TX_MAX);
for(int i=0; i<TX_MAX; i++){
tx_list[i].addr = PADDR(tx_buf[i].buffer);
tx_list[i].cmd = (E1000_TXD_CMD_EOP>>24) | (E1000_TXD_CMD_RS>>24);
tx_list[i].status = E1000_TXD_STAT_DD;
}
//填充E1000_TDBAL/E1000_TDBAH
pci_e1000[E1000_TDBAL>>2] = PADDR(tx_list);
pci_e1000[E1000_TDBAH>>2] = 0;
//設置長度
pci_e1000[E1000_TDLEN>>2] = TX_MAX*sizeof(struct tx_desc);
//初始化頭尾
pci_e1000[E1000_TDH>>2] = 0;
pci_e1000[E1000_TDT>>2] = 0;
//設置寄存器的值
pci_e1000[E1000_TCTL>>2] |= (E1000_TCTL_EN | E1000_TCTL_PSP |
(E1000_TCTL_CT & (0x10<<4)) |
(E1000_TCTL_COLD & (0x40<<12)));
pci_e1000[E1000_TIPG>>2] |= (10) | (4<<10) | (6<<20);
}
現在,傳輸已初始化,您將必須編寫代碼以傳輸數據包,並使其通過系統調用可在用戶空間訪問。要傳輸數據包,您必須將其添加到傳輸隊列的末尾,這意味着將數據包數據複製到下一個數據包緩衝區,然後更新TDT
(傳輸描述符末尾)寄存器以通知卡中存在另一個數據包。傳輸隊列。(請注意,TDT
是傳輸描述符數組的索引,而不是字節偏移量;文檔對此並不十分清楚。)
但是,發送隊列只有這麼大。如果卡落後於傳輸數據包並且傳輸隊列已滿怎麼辦?爲了檢測到這種情況,您需要E1000
的一些反饋。不幸的是,您不能只使用TDH
(發送描述符頭)寄存器。該文檔明確指出,從軟件讀取該寄存器是不可靠的。但是,如果您在發送描述符的命令字段中設置了RS
位,則當卡已在該描述符中發送了數據包時,卡將在描述符的狀態字段中將DD
位置爲1
。如果已將描述符的DD
位置1
,則可以安全地回收該描述符並使用它傳輸另一個數據包。
如果用戶呼叫您的傳輸系統調用,但未設置下一個描述符的DD
位,表明傳輸隊列已滿怎麼辦?您必須決定在這種情況下該怎麼做。您可以簡單地丟棄數據包。網絡協議對此具有一定的彈性,但是如果丟棄大量的數據包,則該協議可能無法恢復。您可以改爲告訴用戶環境必須重試,就像您對所做的一樣sys_ipc_try_send
。這樣做的好處是可以推遲生成數據的環境。
前面已經初始化了發送,現在就是要你實現發送功能。
練習6
通過檢查下一個描述符是否空閒,將包數據複製到下一個描述符並更新TDT
,編寫一個函數來發送數據包。確保處理傳輸隊列已滿。
int
fit_txd_for_E1000_transmit(void *addr, int length){
int tail = pci_e1000[E1000_TDT>>2];//取隊尾
struct tx_desc *tx_next = &tx_list[tail];//獲取結構體
if(length > sizeof(struct packets))//長度不能超過最大值
length = sizeof(struct packets);
if((tx_next->status & E1000_TXD_STAT_DD) == E1000_TXD_STAT_DD){//通過這個標誌位實現判斷
memmove(KADDR(tx_next->addr), addr, length);
tx_next->status &= !E1000_TXD_STAT_DD;
tx_next->length = (uint16_t)length;
pci_e1000[E1000_TDT>>2] = (tail + 1)%TX_MAX;
return 0;
}
return -1;
}
練習7
將他在系統調用裏面調用。這個就簡單了。
添加一個新的系統調用,自己命名就行。
static int
sys_packet_try_send(void *addr, uint32_t len){
user_mem_assert(curenv, addr, len, PTE_U);
return fit_txd_for_E1000_transmit(addr, len);
}
//添加case 注意這個SYS_packet_try_send 是沒有的 要在syscall.h 的頭文件裏面的enum 添加了。
case (SYS_packet_try_send):
return sys_packet_try_send((void *)a1,a2);
在這個地方添加之後要寫到lib/syscall.c
裏面
int sys_packet_try_send(void *data_va, int len){
return (int) syscall(SYS_packet_try_send, 0 , (uint32_t)data_va, len, 0, 0, 0);
}
//還要在 inc/lib.h裏面聲明
int sys_packet_try_send(void *data_va, int len);
到這裏就有系統調用發送東西了。
Transmitting Packets: Network Server
現在,您已經在設備驅動程序的發送端有了一個系統調用接口,是時候發送數據包了。輸出幫助程序環境的目標是循環執行以下操作:接受NSREQ_OUTPUT
來自核心網絡服務器的IPC
消息,並使用上面添加的系統調用將伴隨這些IPC
消息的數據包發送到網絡設備驅動程序。該NSREQ_OUTPUT
IPC
的由發送low_level_output
功能在 net/lwip/jos/jif/jif.c
,該膠合的LWIP
的堆書的網絡系統。每個IPC
都將包含一個頁面,該頁面由union Nsipc
其struct jif_pkt pkt
字段中包含數據包 (請參見inc / ns.h
)。 struct jif_pkt
struct jif_pkt {
int jp_len;
char jp_data [0];
};
jp_len
表示數據包的長度。IPC
頁面上的所有後續字節專用於數據包內容。jp_data
在結構的末尾使用零長度數組是一種常見的C
技巧,用於表示沒有預定長度的緩衝區。由於C
不會進行數組邊界檢查,因此只要您確保該結構後面有足夠的未使用內存,就可以將其jp_data
用作任何大小的數組。
當設備驅動程序的傳輸隊列中沒有更多空間時,請注意設備驅動程序,輸出環境和核心網絡服務器之間的交互。核心網絡服務器使用IPC
將數據包發送到輸出環境。如果由於發送數據包系統調用而導致輸出環境暫停,因爲驅動程序沒有更多的緩衝區可容納新數據包,則核心網絡服務器將阻止等待輸出服務器接受IPC
調用。
盜個圖
這就是整個的流程了。
最終實現也簡單。練習8
實現output.c
#include "ns.h"
extern union Nsipc nsipcbuf;
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
envid_t from_env;
int perm;
while(1){
if( ipc_recv(&from_env, &nsipcbuf, &perm) != NSREQ_OUTPUT)
continue;
while(sys_packet_try_send(nsipcbuf.pkt.jp_data, nsipcbuf.pkt.jp_len)<0)
sys_yield();
}
}
Part B: Receiving packets and the web server
我都不想說話了,整個和前面那個基本上一模一樣。我直接給代碼了
e100..h
#ifndef JOS_KERN_E1000_H
#define JOS_KERN_E1000_H
#include <kern/pci.h>
#define E1000_STATUS 0x00008 /* Device Status - RO */
int e1000_init(struct pci_func *pcif);
#define E1000_TCTL 0x00400 /* TX Control - RW */
#define E1000_TDBAL 0x03800 /* TX Descriptor Base Address Low - RW */
#define E1000_TDBAH 0x03804 /* TX Descriptor Base Address High - RW */
#define E1000_TDLEN 0x03808 /* TX Descriptor Length - RW */
#define E1000_TDH 0x03810 /* TX Descriptor Head - RW */
#define E1000_TDT 0x03818 /* TX Descripotr Tail - RW */
#define E1000_TIPG 0x00410 /* TX Inter-packet gap -RW */
#define E1000_TCTL_EN 0x00000002 /* enable tx */
#define E1000_TCTL_BCE 0x00000004 /* busy check enable */
#define E1000_TCTL_PSP 0x00000008 /* pad short packets */
#define E1000_TCTL_CT 0x00000ff0 /* collision threshold */
#define E1000_TCTL_COLD 0x003ff000 /* collision distance */
#define E1000_TXD_CMD_RS 0x08000000 /* Report Status */
#define E1000_TXD_STAT_DD 0x00000001 /* Descriptor Done */
#define E1000_TXD_CMD_EOP 0x01000000 /* End of Packet */
#define TX_MAX 64
#define BUFSIZE 2048
struct tx_desc
{
uint64_t addr;
uint16_t length;
uint8_t cso;
uint8_t cmd;
uint8_t status;
uint8_t css;
uint16_t special;
}__attribute__((packed));
struct tx_desc tx_list[TX_MAX];
struct packets{
char buffer[BUFSIZE];
}__attribute__((packed));
struct packets tx_buf[TX_MAX];
void e1000_transmit_init();
int
fit_txd_for_E1000_transmit(void *addr, int length);
#define RX_MAX 128
#define E1000_RCTL_EN 0x00000002 /* enable */
#define E1000_RCTL_SBP 0x00000004 /* store bad packet */
#define E1000_RCTL_UPE 0x00000008 /* unicast promiscuous enable */
#define E1000_RCTL_MPE 0x00000010 /* multicast promiscuous enab */
#define E1000_RCTL_LPE 0x00000020 /* long packet enable */
#define E1000_RCTL_LBM_NO 0x00000000 /* no loopback mode */
#define E1000_RCTL_BAM 0x00008000 /* broadcast enable */
#define E1000_RCTL_SZ_2048 0x00000000 /* rx buffer size 2048 */
#define E1000_RCTL_SECRC 0x04000000 /* Strip Ethernet CRC */
#define E1000_RXD_STAT_DD 0x01 /* Descriptor Done */
#define E1000_RXD_STAT_EOP 0x02 /* End of Packet */
#define E1000_RCTL 0x00100 /* RX Control - RW */
#define E1000_RDBAL 0x02800 /* RX Descriptor Base Address Low - RW */
#define E1000_RDBAH 0x02804 /* RX Descriptor Base Address High - RW */
#define E1000_RDLEN 0x02808 /* RX Descriptor Length - RW */
#define E1000_RDH 0x02810 /* RX Descriptor Head - RW */
#define E1000_RDT 0x02818 /* RX Descriptor Tail - RW */
#define E1000_MTA 0x05200 /* Multicast Table Array - RW Array */
#define E1000_RA 0x05400 /* Receive Address - RW Array */
#define E1000_RAH_AV 0x80000000 /* Receive descriptor valid */
struct rx_desc
{
uint64_t addr;
uint16_t length;
uint16_t pcs;
uint8_t status;
uint8_t errors;
uint16_t special;
}__attribute__((packed));
struct rx_desc rx_list[RX_MAX];
int read_rxd_after_E1000_receive(void *addr);
struct packets rx_buf[RX_MAX];
void e1000_receive_init();
int read_rxd_after_E1000_receive(void *addr);
#endif // SOL >= 6
最終的e1000.c
#include <kern/e1000.h>
#include <kern/pmap.h>
#include <inc/string.h>
// LAB 6: Your driver code here
uint32_t *pci_e1000;
int
e1000_init(struct pci_func *pcif)
{
pci_func_enable(pcif);
pci_e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]);
cprintf("the E1000 status register: [%08x]\n", *(pci_e1000+(E1000_STATUS>>2)));
e1000_transmit_init();
e1000_receive_init();
return 1;
}
void
e1000_transmit_init(){
memset(tx_list, 0, sizeof(struct tx_desc)*TX_MAX);
memset(tx_buf, 0, sizeof(struct packets)*TX_MAX);
for(int i=0; i<TX_MAX; i++){
tx_list[i].addr = PADDR(tx_buf[i].buffer);
tx_list[i].cmd = (E1000_TXD_CMD_EOP>>24) | (E1000_TXD_CMD_RS>>24);
tx_list[i].status = E1000_TXD_STAT_DD;
}
pci_e1000[E1000_TDBAL>>2] = PADDR(tx_list);
pci_e1000[E1000_TDBAH>>2] = 0;
pci_e1000[E1000_TDLEN>>2] = TX_MAX*sizeof(struct tx_desc);
pci_e1000[E1000_TDH>>2] = 0;
pci_e1000[E1000_TDT>>2] = 0;
pci_e1000[E1000_TCTL>>2] |= (E1000_TCTL_EN | E1000_TCTL_PSP |
(E1000_TCTL_CT & (0x10<<4)) |
(E1000_TCTL_COLD & (0x40<<12)));
pci_e1000[E1000_TIPG>>2] |= (10) | (4<<10) | (6<<20);
}
int
fit_txd_for_E1000_transmit(void *addr, int length){
int tail = pci_e1000[E1000_TDT>>2];
struct tx_desc *tx_next = &tx_list[tail];
if(length > sizeof(struct packets))
length = sizeof(struct packets);
if((tx_next->status & E1000_TXD_STAT_DD) == E1000_TXD_STAT_DD){
memmove(KADDR(tx_next->addr), addr, length);
tx_next->status &= !E1000_TXD_STAT_DD;
tx_next->length = (uint16_t)length;
pci_e1000[E1000_TDT>>2] = (tail + 1)%TX_MAX;
return 0;
}
return -1;
}
void
e1000_receive_init()
{
for(int i=0; i<RX_MAX; i++){
memset(&rx_list[i], 0, sizeof(struct rx_desc));
memset(&rx_buf[i], 0, sizeof(struct packets));
rx_list[i].addr = PADDR(rx_buf[i].buffer);
}
pci_e1000[E1000_MTA>>2] = 0;
pci_e1000[E1000_RDBAL>>2] = PADDR(rx_list);
pci_e1000[E1000_RDBAH>>2] = 0;
pci_e1000[E1000_RDLEN>>2] = RX_MAX*sizeof(struct rx_desc);
pci_e1000[E1000_RDH>>2] = 0;
pci_e1000[E1000_RDT>>2] = RX_MAX - 1;
pci_e1000[E1000_RCTL>>2] = (E1000_RCTL_EN | E1000_RCTL_BAM |
E1000_RCTL_SZ_2048 |
E1000_RCTL_SECRC);
pci_e1000[E1000_RA>>2] = 0x52 | (0x54<<8) | (0x00<<16) | (0x12<<24);
pci_e1000[(E1000_RA>>2) + 1] = (0x34) | (0x56<<8) | E1000_RAH_AV;
}
int
read_rxd_after_E1000_receive(void *addr)
{
int head = pci_e1000[E1000_RDH>>2];
int tail = pci_e1000[E1000_RDT>>2];
tail = (tail + 1) % RX_MAX;
struct rx_desc *rx_hold = &rx_list[tail];
if((rx_hold->status & E1000_TXD_STAT_DD) == E1000_TXD_STAT_DD){
int len = rx_hold->length;
memcpy(addr, rx_buf[tail].buffer, len);
pci_e1000[E1000_RDT>>2] = tail;
return len;
}
return -1;
}
添加 系統調用的就不貼了都一樣。
input.c
#include "ns.h"
extern union Nsipc nsipcbuf;
void
sleep(int msec)//簡單的延遲函數
{
unsigned now = sys_time_msec();
unsigned end = now + msec;
if ((int)now < 0 && (int)now > -MAXERROR)
panic("sys_time_msec: %e", (int)now);
while (sys_time_msec() < end)
sys_yield();
}
void
input(envid_t ns_envid)
{
binaryname = "ns_input";
// LAB 6: Your code here:
// - read a packet from the device driver
// - send it to the network server
// Hint: When you IPC a page to the network server, it will be
// reading from it for a while, so don't immediately receive
// another packet in to the same physical page.
char my_buf[2048];
int length;
while(1){
while((length = sys_packet_try_recv(my_buf))<0)
sys_yield();
nsipcbuf.pkt.jp_len=length;
memcpy(nsipcbuf.pkt.jp_data, my_buf, length);
ipc_send(ns_envid, NSREQ_INPUT, &nsipcbuf, PTE_U | PTE_P);
sleep(50);
}
}
到這個地方基本上已經全部結束了。最後讓你實現http
的部分代碼。我也直接給了,因爲如果要理解要看全部的http
源碼。
static int
send_data(struct http_request *req, int fd)
{
// LAB 6: Your code here.
int n;
char buf[BUFFSIZE];
while((n=read(fd,buf,(long)sizeof(buf)))>0){
if(write(req->sock,buf,n)!=n){
die("Failed to send file to client");
}
}
return n;
//panic("send_data not implemented");
}
static int
send_file(struct http_request *req)
{
int r;
off_t file_size = -1;
int fd;
// open the requested url for reading
// if the file does not exist, send a 404 error using send_error
// if the file is a directory, send a 404 error using send_error
// set file_size to the size of the file
// LAB 6: Your code here.
if ((fd = open(req->url, O_RDONLY)) < 0) {
send_error(req, 404);
goto end;
}
struct Stat stat;
fstat(fd, &stat);
if (stat.st_isdir) {
send_error(req, 404);
goto end;
}
//panic("send_file not implemented");
if ((r = send_header(req, 200)) < 0)
goto end;
if ((r = send_size(req, file_size)) < 0)
goto end;
if ((r = send_content_type(req)) < 0)
goto end;
if ((r = send_header_fin(req)) < 0)
goto end;
r = send_data(req, fd);
end:
close(fd);
return r;
}
至此all is over
。