# 2021q1 Homework1 (lab0)
contributed by < `ambersun1234` >
> [作業說明](https://hackmd.io/@sysprog/linux2021-lab0)
### Reviewed by `jserv`
* [commit 81f38](https://github.com/ambersun1234/lab0-c/commit/81f3819cba1de8c63678f6671cbd3abd33b39f34) 對程式碼變更的歷史不友善,難以看出過往程式碼的變更和你在合併的過程中,多做哪些修改。改用 [git rebase](https://git-scm.com/docs/git-rebase) 可消弭這樣的問題,並確保你所做的修改會在 git 變更歷程的最前方
* [commit f35422](https://github.com/ambersun1234/lab0-c/commit/f35422c0686bff78f9df03e2a4c353e16743c87a) 的訊息過於寒酸: "Remove natsort",外人實在不易理解你背後的考量,請閱讀 [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/),特別是這幾點:
* Use the imperative mood in the subject line
* Use the body to explain what and why vs. how
* [commit 15c022](https://github.com/ambersun1234/lab0-c/commit/15c02221292095cc604e7faa25ed85c014ab39ad) 宣稱 "Fix Address sanitizer error",但沒說問題發生原因,更沒談及這樣的解法是否充分,難保日後會不會重複發生。
* 共筆已列出 select 系統呼叫的參考資訊和 `lab0-c` 中對應的程式碼,但本程式實際用到 select 系統呼叫嗎?你看到的程式碼,真有符合預期地運作嗎?在沒有驗證前,務必大膽假設和小心求證
* 缺乏對 coroutine 的實作
## 開發環境
```shell
$ uname -a
Linux station 5.8.0-44-generic #50~20.04.1-Ubuntu SMP Wed Feb 10 21:07:30 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
$ gcc -v
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)
```
## 開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer),於 qtest 中輸入 help 會導致錯誤
+ 
+ 上圖為執行結果,白色區塊顯示的區塊為 traceback,錯誤的地方為 `console.c:307`
```c=305
report(1, "Options:");
while (plist) {
report(1, "\t%s\t%d\t%s", plist->name, *plist->valp,
plist->documentation);
plist = plist->next;
}
return true;
```
這個是輸出 help 裡面的 option 的程式碼,考慮新增 option 程式
```c=
void add_param(char *name,
int *valp,
char *documentation,
setter_function setter)
{
...
ele->valp = valp;
...
```
呼叫端
```c=
add_param("simulation", (int *) &simulation, "Start/Stop simulation mode", NULL);
add_param("verbose", &verblevel, "Verbosity level", NULL);
add_param("error", &err_limit, "Number of errors until exit", NULL);
add_param("echo", (int *) &echo, "Do/don't echo commands", NULL);
```
其中第二個參數定義
```c=
/* Parameters */
static int err_limit = 5;
static bool echo = 0;
bool simulation = false;
int level = 4;
```
`add_param` 將上述所有參數都強制轉型成 int pointer,因此 dereference 的時候大小會不一致,導致 Address Sanitizer 錯誤。
將 `bool` 更改為 `int` 即可
根據 [C 語言規格書 p.86 提到](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf#page=98)
> Each of the operators < (less than), > (greater than), <= (less than or equal to), and >= (greater than or equal to) shall yield 1 if the specified relation is true and 0 if it is false.92) The result has type int
我們可以以 `0` 代替 `false`
## 解釋 [select](http://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫在本程式的使用方式,並分析 [console.c](https://github.com/sysprog21/lab0-c/blob/master/console.c) 的實作,說明其中運用 CS:APP [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的原理和考量點。
根據 [man 2 select](https://man7.org/linux/man-pages/man2/select.2.html)
> select() allows a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for some class of I/O operation (e.g., input possible). A file descriptor is considered ready if it is possible to perform a corresponding I/O operation (e.g., read(2), or a sufficiently small write(2)) without blocking.
監控給定 file descriptor 是否處於 ready 狀態。
```c=
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_SET(infd, readfds);
if (infd == STDIN_FILENO && prompt_flag) {
printf("%s", prompt);
fflush(stdout);
prompt_flag = true;
}
if (infd >= nfds)
nfds = infd + 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);
}
}
return result;
}
```
函式 cmd_select 是用於同步檔案,更準確地來說是 internal buffer,逐步同步檔案,將 command 送到 interpret_cmd 進行運算。值得注意的是,sets 如果在迴圈裡面執行,需要手動重新初始化
> Note well: Upon return, each of the file descriptor sets is modified in place to indicate which file descriptors are currently "ready". Thus, if using select() within a loop, the sets must be reinitialized before each call. The implementation of the fd_set arguments as value-result arguments is a design error that is avoided in poll(2) and epoll(7).
所以才需要 `FD_CLR()`
<hr>
根據 [man 2 read](https://man7.org/linux/man-pages/man2/read.2.html) 的內容顯示
> It is not an error if this number is smaller than the number of bytes requested; this may happen for example because fewer bytes are actually available right now (maybe because we were close to end-of-file, or because we are reading from a pipe, or from a terminal), or because read() was interrupted by a signal. See also NOTES.
由此可見,qtest 使用 [RIO](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的原因是為了避免有 short count(即讀取到的資料短少)的問題
## 說明 [antirez/linenoise](https://github.com/antirez/linenoise) 的運作原理,注意到 [termios](http://man7.org/linux/man-pages/man3/termios.3.html) 的運用
基本上是當使用者按下 tab 的時候,呼叫 [linenoise/linenoise.c completeLine](https://github.com/antirez/linenoise/blob/master/linenoise.c#364),然後由 `completionCallback` 提供可能的結果而 call back 的註冊是呼叫使用者定義 function,以 qtest 為例,就是 [lab0-c/console.c completion](https://github.com/sysprog21/lab0-c/blob/master/console.c#620)
```c=
static bool cmd_maybe(char *target, const char *src)
{
for (int i = 0; i < strlen(src); i++) {
if (target[i] == '\0')
return false;
if (src[i] != target[i])
return false;
}
return true;
}
void completion(const char *buf, linenoiseCompletions *lc)
{
if (strncmp("option ", buf, 7) == 0) {
param_ptr plist = param_list;
while (plist) {
char str[128] = "";
// if parameter is too long, now we just ignore it
if (strlen(plist->name) > 120)
continue;
strcat(str, "option ");
strcat(str, plist->name);
if (cmd_maybe(str, buf))
linenoiseAddCompletion(lc, str);
plist = plist->next;
}
return;
}
cmd_ptr clist = cmd_list;
while (clist) {
if (cmd_maybe(clist->name, buf))
linenoiseAddCompletion(lc, clist->name);
clist = clist->next;
}
}
```
上述實作會尋找所有 `param_list` 以及 `cmd_list` 的名字進行相似度比對(使用 `cmd_maybe`) 然後交回 `completeLine` 執行。
使用者可以按 `tab` 或 `esc` 分別對應 `顯示下一個可能 command` 或 `顯示原本輸入字串`
<hr>
[man 2 termios](https://man7.org/linux/man-pages/man3/termios.3.html)
> The termios functions describe a general terminal interface that is provided to control asynchronous communications ports.
可以得知,termios 是一個介面提供非同步的操作,可是這樣還是很抽象阿。
根據 [Is there a way to detect if a key has been pressed?](https://stackoverflow.com/a/22166185) 的線索
> So tcgetattr and tcsetattr are used to get input from terminal without needing to press 'ENTER' key.
回想 `./qtest` 中的操作,確實按 tab 或 esc 的時候是及時反應的。對照程式碼來看
```c=
/* Raw mode: 1960 magic shit. */
static int enableRawMode(int fd) {
struct termios raw;
if (!isatty(STDIN_FILENO)) goto fatal;
if (!atexit_registered) {
atexit(linenoiseAtExit);
atexit_registered = 1;
}
if (tcgetattr(fd,&orig_termios) == -1) goto fatal;
raw = orig_termios; /* modify the original mode */
/* input modes: no break, no CR to NL, no parity check, no strip char,
* no start/stop output control. */
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
/* output modes - disable post processing */
raw.c_oflag &= ~(OPOST);
/* control modes - set 8 bit chars */
raw.c_cflag |= (CS8);
/* local modes - choing off, canonical off, no extended functions,
* no signal chars (^Z,^C) */
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
/* control chars - set return condition: min number of bytes and timer.
* We want read to return every single byte, without timeout. */
raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */
/* put terminal in raw mode after flushing */
if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal;
rawmode = 1;
return 0;
fatal:
errno = ENOTTY;
return -1;
}
static void disableRawMode(int fd) {
/* Don't even check the return value as it's too late. */
if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1)
rawmode = 0;
}
```
`raw mode` 根據 [man 2 termios](https://man7.org/linux/man-pages/man3/termios.3.html)
> cfmakeraw() sets the terminal to something like the "raw" mode of the old Version 7 terminal driver: input is available character by character, echoing is disabled, and all special processing of terminal input and output characters is disabled. The terminal attributes are set as follows:
簡單來說,disable echo,輸入輸出處理 disable。不過對照 man page 的 raw mode 有一點不一樣

> 為了方便比較,我將一些不影響結果的部分更改了
由上圖可知以下結果
||多出以下|少了以下|
|:--:|:--:|:--:|
|c_iflag||igncr<br>inlcr<br>ignbrk<br>parmrk|
|c_lflag|iexten|echonl|
|c_cflag||csize<br>parenb|
|c_cc|c_cc[VMIN]=1<br>c_cc[VTIME]=0||
參照註解 [linoise.c](https://github.com/antirez/linenoise/blob/master/linenoise.c#L243)
```c
/* input modes: no break, no CR to NL, no parity check, no strip char,
* no start/stop output control. */
/* output modes - disable post processing */
/* control modes - set 8 bit chars */
/* local modes - choing off, canonical off, no extended functions,
* no signal chars (^Z,^C) */
/* control chars - set return condition: min number of bytes and timer.
* We want read to return every single byte, without timeout. */
/* 1 byte, no timer */
```
就是對於輸入不做基本處理,然後不輸出以及限制一次讀一個 byte
可參考 [linux tty通過VTIME VMIN實現阻塞與非阻塞接收](https://www.twblogs.net/a/5b8dcabf2b7177188340b2c5)
## 指出現有程式的缺陷 (提示: 和 [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 有關),嘗試強化並提交 pull request
## 在 `qtest` 中實作 [coroutine](https://en.wikipedia.org/wiki/Coroutine),並提供新的命令 `web`,提供 web 伺服器功能,注意: web 伺服器運作過程中,`qtest` 仍可接受其他命令
> 可嘗試整合 [tiny-web-server](https://github.com/7890/tiny-web-server),將 `FORK_COUNT` 變更為 `0`,並以 [coroutine](https://en.wikipedia.org/wiki/Coroutine) 取代原本的 fork 系統呼叫
###### tags: `linux2021`