# 2021q1 Homework1 (lab0) contributed by < `hankluo6` > > 延續 [2020q3 Homework1(lab0)](https://hackmd.io/@hankluo6/2020q3-lab0) 的開發 > [GitHub](https://github.com/hankluo6/lab0-c) ## 環境 ```shell $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 1 On-line CPU(s) list: 0 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 158 Model name: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz Stepping: 10 CPU MHz: 2304.002 BogoMIPS: 4608.00 Hypervisor vendor: KVM Virtualization type: full L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 8192K NUMA node0 CPU(s): 0 ``` ## 整合 [tiny-web-server](https://github.com/7890/tiny-web-server) 至 lab0 因為要同時處理命令列輸入與 web 輸入,先找出程式等待輸入時的主要迴圈,位於 `linenoise()->linenoiseRaw()->linenoiseEdit()` 內的 `while(1)`。但 linenoise 是用 `read` 等待使用者輸入,當 read 阻塞時,便無法接收 web 傳來的資訊。 * 嘗試用 [`select()`](https://man7.org/linux/man-pages/man2/select.2.html) 同時處理 stdin 及 socket: * 在 `linenoiseEdit` 中加入以下程式碼: ```cpp while (1) { char c; int nread; char seq[3]; fd_set set; FD_ZERO(&set); FD_SET(listenfd, &set); FD_SET(stdin_fd, &set); int rv = select(listenfd + 1, &set, NULL, NULL, NULL); struct sockaddr_in clientaddr; socklen_t clientlen = sizeof clientaddr; int connfd; switch (rv) { case -1: perror("select"); /* an error occurred */ continue; case 0: printf("timeout occurred\n"); /* a timeout occurred */ continue; default: if (FD_ISSET(listenfd, &set)) { connfd = accept(listenfd,(SA *) &clientaddr, &clientlen); char *p = process(connfd, &clientaddr); strncpy(buf, p, strlen(p) + 1); close(connfd); free(p); return strlen(p); } else if (FD_ISSET(stdin_fd, &set)) { nread = read(l.ifd, &c, 1); if (nread <= 0) return l.len; } break; } } ``` ~~但沒有成功,在輸入時產生亂碼。~~ 重新測試後發現能正常運作,推測當初會出錯是因為使用網址輸入而非 `curl` 指令,需探討兩種方法造成差異的原因,解決方法位在下方。 :::warning TODO: 解釋 select 這類 I/O multiplexor 系統呼叫的運作方式 :notes: jserv ::: * I/O Multiplexing Model ![](https://i.imgur.com/klmZhI9.png) `select` 及 `poll` 皆使用此種 Model,好處是可以同時檢查多個 descriptor,但缺點是需要兩次的 system call,從圖中也可以看到,先透過 `select` system call 呼叫到 kernel space 內等待資料,接著 kernel 接收到資料後通知 user space,再重新發送 `recvfrom` system call 至 kernel space,意味著需要多次 context switch,成本相對比其他 Model (如 Blocking I/O) 來得高。 * 嘗試修改 `console.c`,讓程式讀到 `web` 命令後手動按下 Enter 鍵繼續: * 先將 socket 改為 non-blocking,防止程式停止接收使用者輸入: ```cpp int flags = fcntl(listenfd, F_GETFL); fcntl(listenfd, F_SETFL, flags | O_NONBLOCK); ``` * 接著修改 `run_console()`: ```cpp if (!has_infile) { char *cmdline; while ((cmdline = linenoise(prompt)) != NULL) { int connfd = accept(*listenfd, (SA *)&clientaddr, &clientlen); if (connfd == -1) { interpret_cmd(cmdline); linenoiseHistoryAdd(cmdline); /* Add to the history. */ linenoiseHistorySave(HISTORY_FILE); /* Save the history on disk. */ linenoiseFree(cmdline); } else { cmdline = process(connfd, &clientaddr); interpret_cmd(cmdline); free(cmdline); close(connfd); } } } else { while (!cmd_done()) cmd_select(0, NULL, NULL, NULL, NULL); } ``` * 透過 HTTP 的 GET method 來傳送想要執行的 function,`process()` 處理 URL 字串並將 function name 與 parameter 以跟 `cmdline` 一樣的格式回傳 (`[function name][space][parameter]`): ```cpp char *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); int status = 200; handle_request(fd, req.function_name); char *p = req.function_name; /* Change '/' to ' ' */ while (*p && (*p) != '\0') { ++p; if (*p == '/') { *p = ' '; } } #ifdef LOG_ACCESS log_access(status, clientaddr, &req); #endif char *ret = malloc(strlen(req.function_name) + 1); strncpy(ret, req.function_name, strlen(req.function_name) + 1); return ret; } ``` * 在分析完 `console.c` 後,發現使用 `cmd_select()` 能夠滿足需求: * 因為我們需要同時控制 web 輸入及使用者輸入,必須關閉 `linenoise` API 才能達成,在輸入 `web` 命令後將 `linenoise` 關閉,並儲存監聽的 file descriptor: ```c static bool do_web_cmd(int argc, char *argv[]) { listenfd = socket_init(); noise = false; return true; } ``` * `run_console()` 依照 `linenoise` 開啟與否來選擇要使用 `linenoise()` 還是 `cmd_select()` ```c if (!has_infile) { char *cmdline; while (noise && (cmdline = linenoise(prompt)) != NULL) { interpret_cmd(cmdline); linenoiseHistoryAdd(cmdline); /* Add to the history. */ linenoiseHistorySave( HISTORY_FILE); /* Save the history on disk. */ linenoiseFree(cmdline); } if (!noise) { while (!cmd_done()) { cmd_select(0, NULL, NULL, NULL, NULL); } } } else { while (!cmd_done()) { cmd_select(0, NULL, NULL, NULL, NULL); } } ``` * `cmd_select()` 新增對應的處理 ```diff=549 int cmd_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) { int infd; fd_set local_readset; if (cmd_done()) return 0; if (!block_flag) { /* Process any commands in input buffer */ if (!readfds) readfds = &local_readset; /* Add input fd to readset for select */ infd = buf_stack->fd; + FD_ZERO(readfds); FD_SET(infd, readfds); + /* If web not ready listen */ + if (listenfd != -1) + FD_SET(listenfd, readfds); if (infd == STDIN_FILENO && prompt_flag) { printf("%s", prompt); fflush(stdout); prompt_flag = true; } if (infd >= nfds) nfds = infd + 1; + if (listenfd >= nfds) + nfds = listenfd + 1; } if (nfds == 0) return 0; int result = select(nfds, readfds, writefds, exceptfds, timeout) if (result <= 0) return result; infd = buf_stack->fd; if (readfds && FD_ISSET(infd, readfds)) { /* Commandline input available */ FD_CLR(infd, readfds); result--; - if (has_infile) { char *cmdline; cmdline = readline(); if (cmdline) interpret_cmd(cmdline); - } + } else if (readfds && FD_ISSET(listenfd, readfds)) { + FD_CLR(listenfd, readfds); + result--; + int connfd; + struct sockaddr_in clientaddr; + socklen_t clientlen = sizeof(clientaddr); + connfd = accept(listenfd,(SA *) &clientaddr, &clientlen); + char *p = process(connfd, &clientaddr); + if (p) + interpret_cmd(p); + free(p); + close(connfd); + } return result; } ``` * 在 `process()` 中處理 http 請求與對應的[狀態碼](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Status),與上方 `process()` 相同。 * 運行結果 ```c cmd> web listen on port 9999, fd is 3 cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/new 127.0.0.1:54346 200 - 'new' (text/plain) q = [] cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/ih/1 127.0.0.1:54348 200 - 'ih 1' (text/plain) q = [1] cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/ih/2 127.0.0.1:54350 200 - 'ih 2' (text/plain) q = [2 1] cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/ih/3 127.0.0.1:54352 200 - 'ih 3' (text/plain) q = [3 2 1] cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/sort 127.0.0.1:54356 200 - 'sort' (text/plain) q = [1 2 3] cmd> accept request, fd is 4, pid is 100592 # curl http://localhost:9999/quit 127.0.0.1:54358 200 - 'quit' (text/plain) Freeing queue ``` :::danger ~~直接從網址欄輸入 URL 可能會造成傳輸過程出現錯誤~~,已解決。 ::: * 解決方法 根據 [I'm getting favicon.ico error](https://stackoverflow.com/questions/31075893/im-getting-favicon-ico-error) 描述,在 `<head>` 欄位中增加可讀取圖示位址的程式碼 `<link rel="shortcut icon" href="#"` 即可。此原因是因為某些 browser (測試時使用 chrome 瀏覽器) 會要求給予網頁圖案的需求,而我提供的 head request 中沒有對應的資訊,故瀏覽器不會回傳正確的內容,而是一直提示要求給予 `favicon.ico` 檔案位置。 * Coroutine * TODO --- ## 分析 `console.c` * `run_console` `run_console()` 分成兩部份,判斷為檔案輸入或為 stdin,如果是 stdin 則解析並執行對應的命令處理器:如果為檔案的話則透過 `cmd_select()` 來完成。 * `cmd_select()` ```c=561 if (!block_flag) { /* Process any commands in input buffer */ if (!readfds) readfds = &local_readset; /* Add input fd to readset for select */ infd = buf_stack->fd; FD_SET(infd, readfds); if (infd == STDIN_FILENO && prompt_flag) { printf("%s", prompt); fflush(stdout); prompt_flag = true; } if (infd >= nfds) nfds = infd + 1; } ``` * `block_flag` 保持為 false。 * `readfds` 為 `select()` 中的 readset,如果 `readfds` 尚未定義,則將新增的 readset 給它。 * `infd` 為當前處理的 file descriptor,在目前實作中為啟動程式 `./qtest -f [file]` 時的 file。 * `line 569 ~ line 573` 為之前手動輸入時,`infd` 為 stdin(0) 時會進入,目前實做不會運行。 * 如果當前 file descriptor 比輸入的參數 `nfds` 還大時,更新 `nfds` 為最大值加 1,這是為了設置之後 `select()` 的 `readfds` 範圍。 ```c=581 int result = select(nfds, readfds, writefds, exceptfds, timeout); if (result <= 0) return result; infd = buf_stack->fd; if (readfds && FD_ISSET(infd, readfds)) { /* Commandline input available */ FD_CLR(infd, readfds); result--; if (has_infile) { char *cmdline; cmdline = readline(); if (cmdline) interpret_cmd(cmdline); } } ``` * 使用 `select()` 取得可以讀取的 descriptor 總數,目前實做中因只有一個 file,故永遠回傳 1。 * `line 591 ~ line 596` 將 file 從 readset 中移除,並作對應的處理。 * `readline()` ```c=480 static char *readline() { int cnt; char c; char *lptr = linebuf; if (!buf_stack) return NULL; for (cnt = 0; cnt < RIO_BUFSIZE - 2; cnt++) { if (buf_stack->cnt <= 0) { /* Need to read from input file */ buf_stack->cnt = read(buf_stack->fd, buf_stack->buf, RIO_BUFSIZE); buf_stack->bufptr = buf_stack->buf; if (buf_stack->cnt <= 0) { /* Encountered EOF */ pop_file(); if (cnt > 0) { /* Last line of file did not terminate with newline. */ /* Terminate line & return it */ *lptr++ = '\n'; *lptr++ = '\0'; if (echo) { report_noreturn(1, prompt); report_noreturn(1, linebuf); } return linebuf; } return NULL; } } /* Have text in buffer */ c = *buf_stack->bufptr++; *lptr++ = c; buf_stack->cnt--; if (c == '\n') break; } if (c != '\n') { /* Hit buffer limit. Artificially terminate line */ *lptr++ = '\n'; } *lptr++ = '\0'; if (echo) { report_noreturn(1, prompt); report_noreturn(1, linebuf); } return linebuf; } ``` * `line 489` 的迴圈會從 `buf_stack->buf` 中取出一個字元,當 `buf_stack->buf` 中的字元都被讀完後,在透過 `read()` 重新讀取 `RIO_BUFSIZE` 大小的 byte。 * `line 496` 是當 `buf->stack->cnt <= 0` 時,再 `read()` 一次也沒有讀出字元時會進入,表示該 file descriptor 內的內容以被讀取完畢,將結果印出並釋放空間。 * 額外處理檔案結尾有無空行的情形 ###### tags: `linux2021`