MIT 6.828 (六) Lab 6: Network Driver (default final project)

這是最後一個實驗,做完這個一個基本的內核就做完了。這章需要自己去看的東西特麼的多,所以大部分,我們就看看實現了什麼,不會專門一個個細節的看了。

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的任意連接,但JOS10.0.2.15地址在QEMU內部運行的虛擬網絡外部沒有任何意義(即QEMU充當NAT),因此我們無法直接連接到服務器即使在運行QEMU的主機中,也可以在JOS內部運行。爲了解決這個問題,我們將QEMU配置爲在主機上某個端口上運行服務器,該服務器僅連接到JOS中的某個端口,並在真實主機和虛擬網絡之間來回穿梭數據。

您將在端口7(回顯)和80(http)上運行JOS服務器。爲避免在共享的Athena機器上發生衝突,makefile會根據您的用戶ID爲這些機器生成轉發端口。要查找QEMU將要轉發到您的開發主機上的端口,請運行make which-ports。爲了方便起見,makefile還提供make nc-7make 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可以產生很多調試輸出,因此您必須啓用特定的日誌記錄通道。您可能會發現有用的一些渠道是:

FlagMeaning
tx 日誌包發送操作
txerr記錄傳輸環錯誤
rx將更改記錄到RCTL
rxfilter傳入數據包的日誌過濾
rxerr日誌接收振鈴錯誤
unknown日誌讀取和寫入未知寄存器
eeprom從EEPROM讀取日誌
interrupt記錄中斷和更改到中斷寄存器。

例如,要啓用txtxerr日誌記錄,請使用make E1000_DEBUG=tx,txerr ...

注意: E1000_DEBUG標誌僅在6.828版本的QEMU中起作用。

您可以進一步使用軟件仿真的硬件進行調試。如果您陷入困境並且不瞭解E1000爲什麼沒有按預期方式做出響應,則可以在hw/net/e1000.c中查看QEMUE1000實現。

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。因此,用戶環境通過文件描述符引用套接字,就像它們引用磁盤文件一樣。多個操作(connectaccept等)特定於插座,但是readwriteclose經過在正常文件描述符設備分派代碼lib/fd.c。就像文件服務器爲所有打開的文件維護內部唯一ID的方式一樣,lwIP還會爲所有打開的套接字生成唯一的ID。在文件服務器和網絡服務器中,我們都使用存儲在其中的信息將struct Fd每個環境的文件描述符映射到這些唯一的ID空間。

我們去看看lib/sockets.c和前面的文件服務調用的接口也是一樣的。

即使文件服務器和網絡服務器的IPC調度程序看起來似乎相同,也存在關鍵區別。BSD套接字調用like acceptrecv可以無限期阻塞。如果調度程序要讓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消息以及輪詢或等待來自設備驅動程序的數據包。我們selectJOS中沒有系統調用,該調用允許環境監視多個輸入源以標識準備好處理哪些輸入。

如果你看看net/input.cnet/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.coutput.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.clib/socket.c有點不一樣,也不知道有啥區別,個人覺得是lib/socket.c是系統裏面的調用給用戶用的這個文件裏面的應該是進行底層調用的。具體就不分析了,有興趣的自己去看看。

其他三個環境後面再看。

Part A: Initialization and transmitting packets

您的內核沒有時間概念,因此我們需要添加它。當前,硬件每10毫秒產生一次時鐘中斷。在每個時鐘中斷處,我們都可以增加一個變量以指示時間提前了10ms。這是在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

E1000PCI設備,這意味着它已插入主板上的PCI總線。PCI總線具有地址,數據和中斷線,並允許CPUPCI設備進行通信,並且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_basereg_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.he1000.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/OMMIO)與E1000通信。您在JOS中已經看過兩次了:CGA控制檯和LAPIC都是通過寫入和讀取“內存”來控制和查詢的設備。但是這些讀和寫操作不會存儲到DRAM中。他們直接去這些設備。

pci_func_enableE1000協商MMIO區域,並將其基數和大小存儲在BAR 0(即 reg_base[0]reg_size[0])中。這是分配給設備的一系列物理內存地址,這意味着您必須做一些事情才能通過虛擬地址訪問它。由於MMIO區域分配了很高的物理地址(通常大於3GB),KADDR因此由於JOS256MB限制,您不能使用它來訪問它。因此,您必須創建一個新的內存映射。我們將使用MMIOBASE上方的區域(您 mmio_map_region在實驗4中將確保我們不會覆蓋LAPIC使用的映射)。由於PCI設備初始化發生在JOS創建用戶環境之前,因此您可以在其中創建映射,kern_pgdir並且該映射將始終可用。

練習4 實現mmio_map_regionE1000BAR 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節中描述。我們將不會使用E1000TCP卸載功能,因此您可以專注於“舊版傳輸描述符格式”。您現在應該閱讀這些部分,並熟悉這些結構。

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.33.4節作爲發送描述符和發送描述符數組的參考。
請注意對發送描述符數組的對齊要求以及對該數組長度的限制。由於TDLEN必須對齊128字節,每個傳輸描述符爲16字節,因此您的傳輸描述符數組將需要8個傳輸描述符的某個倍數。但是,請勿使用超過64個的描述符,否則我們的測試將無法測試傳輸環溢出。
對於TCTL.COLD,您可以假定爲全雙工操作。對於TIPG,請參閱第13.4.34節表13-77中描述的IEEE 802.3標準IPG的默認值(不要使用第14.5節的表中的值)。

。。。對於這個,我真看不懂是啥。
按照14.5節的描述初始化。步驟如下:

  1. 分配一塊內存用作發送描述符隊列,起始地址要16字節對齊。用基地址填充(TDBAL/TDBAH) 寄存器。
  2. 設置(TDLEN)寄存器,該寄存器保存發送描述符隊列長度,必須128字節對齊。
  3. 設置(TDH/TDT)寄存器,這兩個寄存器都是發送描述符隊列的下標。分別指向頭部和尾部。應該初始化爲0
  4. 初始化TCTL寄存器。設置TCTL.EN位爲1,設置TCTL.PSP位爲1。設置TCTL.CT10h。設置TCTL.COLD40h
  5. 設置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 Nsipcstruct 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

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