# 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`