Try   HackMD

2021q1 Homework1 (lab0)

contributed by < grb72t3yde >

tags: sysprog2020

:penguin: 作業要求

lab0 題目連結

實驗環境

  • Ubuntu 20.04 LTS
  • Linux 核心版本 : 5.4.0-58-generic

queue.[ch] 之實作日誌

queue.h

  • 於定義的結構queue_t中加入下列屬性, 其用以實現具有O(1)時間複雜度的q_size()q_insert_tail()函式
typedef struct {
    ...
    list_ele_t *tail;
    int size;
    ...
} queue_t;

queue.c

  • queue_t *q_new()
    • 功能 : 建立一條新的 queue 且進行內部結構的初始化, 並回傳其指標
    • 根據註解提示, 我先檢查 malloc() 的回傳值確認是否成功, 否則回傳 NULL
    • 做初始化時, 除了 head 與 tail 指標指向 NULL, 新加的 size 也要 assigned 0
  • void q_free(queue_t *q)
    • 功能 : 刪除一條 queue 並釋放其所有使用的記憶體空間
    • 使用 while loop 逐一將 list node 使用 free() 釋放
    • 其中用以儲存 value 所額外 malloc 的空間也要記得釋放
  • bool q_insert_head(queue_t *q, char *s)
    • 功能: 從 list 頭部插入新的 list node, 並將參數 s 所指向的資料複製入此 list node 的 value, 最後回傳 true/false 以告知是否成功
    • 此 function 我呼叫了兩次 malloc(), 分別為了 list node 的空間以及 value 的空間; 因此, 需注意第二次的 malloc()若 return error 時, 需將第一次成功分配的空間釋放
    • 使用 strlen()取得 s 之長度時, 不包括其原本 string terminator (i.e. '\0') 的長度, 因此替 list node 分配存儲字串的空間時需要+1
  • bool q_insert_tail(queue_t *q, char *s)
    • 功能: 從 list 尾部插入新的 list node, 並將參數 s 所指向的資料複製入此 list node 的 value, 最後回傳 true/false 以告知是否成功
    • 注意事項與 q_insert_head 十分類似
  • bool q_remove_head(queue_t *q, char *sp, size_t bufsize)
    • 功能: 從 list 頭部移除一個 list node, 並將 node 內的資料複製入指定位址sp, 最後回傳true/false以告知是否成功; 同時, 我們只允許bufsize(含'\0')長度的資料被存入sp
    • 要看清題目所述, bufsize-1 是不含 terminator 的, 因此做 clear 和 copy 時需注意長度
    • 需要注意 NULL 檢查的順序, 若考慮以下順序, 會造成的不合法的 dereference 存取
if (!q->head || !q) {
    ...
}
  • int q_size(queue_t q)

    • 功能: 回傳 queue q 內的元素個數, 若 q==NULL 則也回傳0
    • 因為有了 size 屬性, 我們直接回傳即可達到 O(1) 的複雜度
  • void q_reverse(queue_t *q)

    • 功能: 將 queue q 的元素順序倒轉
    • 使用常見的三個指標之方法非遞迴方法
void q_reverse(queue_t *q)
{
    if (!q || !q->head) {
        return;
    }
    if (q->size == 1) {
        return;
    }
    list_ele_t *prev, *cur, *foward;
    prev = q->head;
    cur = prev->next;
    foward = cur->next;

    while (foward) {
        cur->next = prev;
        prev = cur;
        cur = foward;
        foward = foward->next;
    }
    cur->next = prev;
    q->tail = q->head;
    q->head->next = NULL;
    q->head = cur;
}

其他實驗以及分析

qtest 實作 coroutine,嘗試整合 tiny web server 進 qtest

  • tiny-web-server 為一個 concurrent echo server based on processes 的實作 (可搭配 CS:APP 第十二章節閱讀)。其可於一個目錄下服務,接收來自多個 clients 的檔案請求 (長度,格式,內容)。
  • 程式碼追蹤
  1. 進入 main function 後,先進行 arguments 的拆解
int main(int argc, char** argv) {

	if(argc > 1 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")))
	{
		print_help();
		return 0;
	}

	struct sockaddr_in clientaddr;
	int default_port = DEFAULT_PORT,
		listenfd,
		connfd;
	char buf[256];
	char *path = getcwd(buf, 256);
	socklen_t clientlen = sizeof clientaddr;
	if(argc == 2) { // port number is argv1
		if(argv[1][0] >= '0' && argv[1][0] <= '9') {
			default_port = atoi(argv[1]);
		} else {
			path = argv[1];
			if(chdir(path) != 0) {
				perror(path);
				exit(1);
			}
		}
	} else if (argc == 3) { // port number is argv2
		default_port = atoi(argv[2]);
		path = argv[1];
		if(chdir(path) != 0) {
			perror(path);
			exit(1);
		}
	}
    ...
  1. 接著呼叫 open_listenfd,包含 server 端創建 socket descripter,bind,listen 等呼叫。bind 請求 kernel 將 listenfd 和 serveraddr 連結; listen 則將 listenfd 從 active socket 轉換成 listen socket ,一個可從 client 接收請求狀態。
int open_listenfd(int port) {
	int listenfd, optval=1;
	struct sockaddr_in serveraddr;

	/* Create a socket descriptor */
	if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		return -1;
	}

	/* Eliminates "Address already in use" error from bind. */
	if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
				   (const void *)&optval , sizeof(int)) < 0) {
		return -1;
	}

	// 6 is TCP's protocol number
	// enable this, much faster : 4000 req/s -> 17000 req/s
	if (setsockopt(listenfd, 6, TCP_CORK,
				   (const void *)&optval , sizeof(int)) < 0) {
		return -1;
	}

	/* Listenfd will be an endpoint for all requests to port
	   on any IP address for this host */
	memset(&serveraddr, 0, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons((unsigned short)port);
	if (bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0) {
		return -1;
	}

	/* Make it a listening socket ready to accept connection requests */
	if (listen(listenfd, LISTENQ) < 0) {
		return -1;
	}
	return listenfd;
}
  1. 以下為一個 concurrent server 的實作。需要注意到 listening 以及 connecting descirpter 的不同,有助於理解以下使用 fork 的實作 concurrent server 技巧。(請參考 CS:APP p.973)
...
	int i=0;
	for(; i < FORK_COUNT; i++) {
		int pid = fork();
		if (pid == 0) {		 //  child
			while(1) {
				connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);
				process(connfd, &clientaddr);
				close(connfd);
			}
		} else if (pid > 0) {   //  parent
			printf("child pid is %d\n", pid);
		} else {
			perror("fork");
		}
	}
...
  1. process 等待一個並隨後拆解 request 的檔案名稱並使用 rio_readlineb 讀取檔案。
void process(int fd, struct sockaddr_in *clientaddr) {
#ifdef LOG_ACCESS
	printf("accept request, fd is %d, pid is %d\n", fd, getpid());
#endif
	http_request req;
	parse_request(fd, &req);

	struct stat sbuf;
	int status = 200, ffd = open(req.filename, O_RDONLY, 0);
	if(ffd <= 0) {
		status = 404;
		char *msg = "File not found";
-  		   client_error(fd, status, "Not found", msg);
+                 client_error(fd, status, "Not found\n", msg);
	} else {
		fstat(ffd, &sbuf);
		if(S_ISREG(sbuf.st_mode)) {
			if (req.end == 0) {
				req.end = sbuf.st_size;
			}
			if (req.offset > 0) {
				status = 206;
			}
			serve_static(fd, ffd, &req, sbuf.st_size);
		} else if(S_ISDIR(sbuf.st_mode)) {
			status = 200;
			handle_directory_request(fd, ffd, req.filename);
		} else {
			status = 400;
			char *msg = "Unknow Error";
			client_error(fd, status, "Error", msg);
		}
		close(ffd);
	}
#ifdef LOG_ACCESS
	log_access(status, clientaddr, &req);
#endif
}

如下圖輸出結果,目前的程式碼預設會建立四個子程序,加上原本的程序(2450689)共有五個程序可同時服務 clients,若有後續連線請求將暫時得不到回應。

serve dicrectory '/home/bear/Documents/Github/linux2020/tiny-web-server'
listen on port 9999, fd is 3
child pid is 2450690
child pid is 2450691
child pid is 2450692
child pid is 2450693
accept request, fd is 6, pid is 2450690
accept request, fd is 6, pid is 2450691
accept request, fd is 6, pid is 2450692
accept request, fd is 6, pid is 2450689
accept request, fd is 6, pid is 2450693
...

文字導向的訊息不要用圖片顯示,否則不利於日後資料檢索,更對視覺障礙者不友善。
:notes: jserv

已修正


  • 作業目標 : 將 FORK_COUNT 改成 0,以 coroutine.h 實作 coroutine。
  • 參考: Coroutines in C 以及 coroutines and sockets
  • 想法 : 只使用單一程序,其在等待 client 連線 (accept) 或是 client 資料時 (read) 不使用 blocking 方法,而是將執行權 yield 出去,讓其他 routine 繼續執行工作。
  1. 首先將 FORK_CPUNT 改為 0,則目前只能同時服務一個 client。
- #define FORK_COUNT 4 + #define FORK_COUNT 0
  1. 設計一個 coroutine acceptor 負責接收來自 client 的連線領求,以及一個 scheduler 負責建立 coroutines 來處理已經建立連線的 socket 資料讀取。
while(1) {
+       acceptor(listenfd, &clientaddr, &clientlen, &pool);
+       scehduler(&pool, &clientaddr, &clientlen);
-       connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);
-       process(connfd, &clientaddr);
-       close(connfd);
    }

首先我將先前創建的 listenfd 改為 non-blocking,則程序在 accept 時並不會 block。

int fileflags = 0;
if (fileflags = fcntl(listenfd, F_GETFL) == -1) {
	perror("fcntl F_GETFL");
	exit(1);
}
if (fcntl(listenfd, F_SETFL, fileflags | O_NONBLOCK) == -1){
	perror("fcntl");
	exit(1);
}

acceptor 的程式碼如下 :

void acceptor(int listenfd, struct sockaddr_in *clientaddr, socklen_t *len, pool *p)
{
	int connfd = 1;

	scrBegin;
	while (1) {
		connfd = accept(listenfd, (SA*)clientaddr, len);

		if (connfd < 0)
		{
			scrReturnV;
		}
		else
		{
#ifdef LOG_ACCESS
	printf("accept request, fd is %d, pid is %d\n", listenfd, getpid());
#endif
			add_client(connfd, p);
			/* code */
		}
	}

	scrFinishV;
}

其會在 accept 到連線的時候將 client 加進 client pool (關於 client pool 的實作請參考 CS:APP 第十二章節),其餘時候,會 yield 出執行權。

  1. 接下來就是設計一個 scheduler 不斷輪流服務每個已經連線的 client (沒有使用 select 等 event-driven 的設計)。
void scehduler(pool *p, struct sockaddr_in* addr, socklen_t *len)
{
	int i, connfd;
	rio_t rio;

	for (i = 0; i <= p->maxi; i++) {
		connfd = p->clientfd[i];
		rio = p->clientrio[i];

		/* If the descriptor is ready, echo a text line from it */
		if ((connfd > 0) /* && (FD_ISSET(connfd, &p->ready_set)) */) {
			process(connfd, addr, &p->context[i], &rio);
		}
	}
}

scheduler 為每個已經連線的 client 建立一個 coroutine,以協調處理不同 client 之間的 socketfd 讀寫操作。coroutine 的特性為每次重新進入時可以從上次的返回點之下一行繼續執行。

  1. 最後為 process 內部的實作,我盡量維持此 function 的功用 (處理對 socket fd 的讀寫操作),但是改為一個 client 若是未讀到資料時,就返回以讓 scehduler 服務下個 client 的讀寫操作。