# 2020q1 Homework1 (lab0) contributed by < `rwe0214` > ###### tags: `Linux`, `rwe0214`, `NCKU` ## 作業說明 [Homework1 (lab0)](https://hackmd.io/@sysprog/linux2020-lab0) ## 開發過程 在實作過程發現,如果想在 `q_insert_tail` 達到 $O(1)$ 的時間,不在 `queue_t` structure 中新增其他的 `field` 去紀錄額外資訊是很難達到的,所以新增了一個 `list_ele_t *tail` 指標記錄尾部的位址。 ### q_size 在 `queue` 為空的時候回傳 zero ,其他狀況回傳 queue size,使用 `!!` 可以確保 `queue` 不為 `NULL` 的時候,條件判斷式的值為 1。 ``` cpp int q_size(queue_t *q) { return !!q ? q->size : 0; } ``` ### q_new 需要處理 malloc 失敗的情形。 ``` c= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (!q) return NULL; q->head = NULL; q->tail = NULL; q->size = 0; return q; } ``` ### q_free `queue` 為空無動作。 ``` cpp void q_free(queue_t *q) { if (!q) return; for (list_ele_t *del = q->head; q->head; del = q->head) { q->head = del->next; free(del->value); free(del); } free(q); } ``` ### q_insert_head 一開始在 `strncpy` 複製字串時使用 `sizeof(s)` 作為字串大小,後來發現這僅僅會回傳 `char *` 的大小,而不是字串真正的記憶體大小,另外為了防止字串無法終止,所以額外多一個位元組 (byte) 存放 `null terminator`。 ``` cpp bool q_insert_head(queue_t *q, char *s) { if (!q) return false; list_ele_t *newh; newh = malloc(sizeof(list_ele_t)); if (!newh) return false; newh->value = malloc(strlen(s) * sizeof(char) + 1); if (!newh->value) { free(newh); return false; } newh->next = q->head; strncpy(newh->value, s, strlen(s) * sizeof(char)); (newh->value)[strlen(s) * sizeof(char)] = '\0'; q->head = newh; if (q->size++ == 0) q->tail = newh; return true; } ``` ### q_insert_tail 在沒有在 `queue_t` 中新增 `tail` pointer 前,一直無法在時間複雜度上有所提升,皆為 $O(n)$,新增後可達 $O(1)$ ``` c= bool q_insert_tail(queue_t *q, char *s) { if (!q) return false; list_ele_t *newt = malloc(sizeof(list_ele_t)); if (!newt) return false; newt->value = malloc(strlen(s) * sizeof(char) + 1); if (!newt->value) { free(newt); return false; } newt->next = NULL; strncpy(newt->value, s, strlen(s) * sizeof(char) + 1); (newt->value)[strlen(s) * sizeof(char)] = '\0'; /* wrong section is below*/ q->tail->next = newt; q->tail = newt; if (q->size++ == 0) q->head = newt; return true; } ``` 但是上述在跑 complexity test 時會不正常結束,使用 valgrind 分析後會發生 `Invalid write of size 8`,發現是因為原本的程式碼在新增流程中出錯, ```cpp=19 q->tail->next = newt; q->tail = newt; if (q->size++ == 0) q->head = newt; return true; ``` 更改為以下: ```cpp=19 if (q->size++ == 0) q->head = newt; else q->tail->next = newt; q->tail = newt; return true; ``` ### q_remove_head 依照註解完成,目前還沒搞懂為什麼需要 `char *sp` 和 `size_t bufsize`,推測是和實作 `q_reverse` 有關。 :::warning 程式碼是提供給你修改用,而非鑑賞的,有疑慮就去做實驗 :notes: jserv ::: ``` cpp bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (!q || !q->head) return false; list_ele_t *tmp = q->head; q->head = q->head->next; if (!!sp) { strncpy(sp, tmp->value, bufsize - 1); sp[bufsize - 1] = '\0'; } q->size--; free(tmp->value); free(tmp); return true; } ``` ### q_reverse 在實作這個的時候,因為提示的註解暗示可以使用 `q_insert_head`, `q_insert_tail`, 和`q_remove_head` 來完成,但是目前我對如何使用他們還沒有頭緒,所以先暫時實作一個利用 `pointer` 暫存前後 `elements` 的方式來完成 ``` cpp void q_reverse(queue_t *q) { if (!q || !q->head) return; list_ele_t *iter, *nexti, *prei = NULL; q->tail = q->head; for (iter = q->head; iter->next; iter = nexti) { nexti = iter->next; iter->next = prei; prei = iter; } iter->next = prei; q->head = iter; } ``` ### q_sort 一開始的想法是單純使用隨堂考 [quiz1](https://hackmd.io/@sysprog/linux2020-quiz1) 的排序方法,並把它改成 iterative 的版本,但發現執行時間一樣逾時。 :::danger 不難發現 [quiz1](https://hackmd.io/@sysprog/linux2020-quiz1) 提供的程式碼存在嚴重缺陷,並非典型的 merge sort,特別在時間複雜度的表現上 :notes: jserv ::: 分析時間複雜度發現,原本以為 [quiz1](https://hackmd.io/@sysprog/linux2020-quiz1) 是一般的 merge sort,但他的 divide and conquer 並不是切成對等的兩個子問題,說明如下: * merge sort: $T(n) = 2T(\frac{n}{2}) + n = O(nlog{n})$ * [quiz1](https://hackmd.io/@sysprog/linux2020-quiz1): $T(n) = T(n-1) + n = O(n^2)$ 所以重新撰寫一個針對 linked list 的 merge sort 實作: [q_sort](https://github.com/rwe0214/lab0-c/blob/master/queue.c#L166)。 ``` cpp void q_sort(queue_t *q) { if (!q || q->size <= 1) return; sort(&q->head, q->size, &q->tail); } void merge(list_ele_t **llist_head, list_ele_t **llist_tail, list_ele_t **rlist_head, list_ele_t **rlist_tail) { list_ele_t *tmp = NULL; if (strcmp((*llist_head)->value, (*rlist_head)->value) > 0) { tmp = *llist_head; *llist_head = *rlist_head; *rlist_head = tmp; tmp = *llist_tail; *llist_tail = *rlist_tail; *rlist_tail = tmp; } list_ele_t *l_head = *llist_head, *l_tail = *llist_tail; list_ele_t *r_head = *rlist_head; list_ele_t *r_end_next = (*rlist_tail)->next; while (l_head != l_tail && r_head != r_end_next) { if (strcmp(l_head->next->value, r_head->value) > 0) { tmp = r_head->next; r_head->next = l_head->next; l_head->next = r_head; r_head = tmp; } l_head = l_head->next; } /* if rlist is empty but llist in not, all of rlist nodes are alreay inserted into llist */ if (l_head == l_tail) // In this case the tail of sorted list is rlist_tail l_head->next = r_head; else // In this case the tail of sorted list is llist_tail *rlist_tail = *llist_tail; } void sort(list_ele_t **start, int n, list_ele_t **end) { list_ele_t *prev_tail = NULL; for (int cmp_t = 1; cmp_t < n; cmp_t *= 2) { list_ele_t *iter; for (iter = *start; iter;) { list_ele_t *llist_head = iter, *llist_tail = iter; list_ele_t *rlist_head, *rlist_tail; int is_first = (llist_head == *start) ? 1 : 0; for (int i = 1; i < cmp_t && llist_tail->next; i++, llist_tail = llist_tail->next) ; rlist_head = llist_tail->next; if (!rlist_head) break; rlist_tail = rlist_head; for (int i = 1; i < cmp_t && rlist_tail->next; i++, rlist_tail = rlist_tail->next) ; list_ele_t *tmp = rlist_tail->next; merge(&llist_head, &llist_tail, &rlist_head, &rlist_tail); if (is_first) *start = llist_head; else prev_tail->next = llist_head; prev_tail = rlist_tail; iter = tmp; } /*NEED TO INSURE THAT prev_tail ALWAYS POINT TO THE TAIL OF WHOLE LIST!*/ prev_tail->next = iter; } } ``` :::warning TODO: 1. 思考更精簡的 merge sort 實作; 2. 用 perf 一類的工具找出執行時間的熱點 (hot spot, 往往是效能的瓶頸); :notes: jserv ::: ## 解釋 [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) 的原理和考量點 我把 CSAPP 中有談到 RIO 的部分整理在 [CSAPP Ch10 筆記](https://hackmd.io/@willychiu/Hktn-eWV8#CS:APP-CH10-System-level-IO) * 在 `console.c/cmd_select` 中利用 `select` 去監視 readfd set 中是否有哪個 `readfd` 是已準備好資料進行讀取的,程式碼片段如下, ```cpp= int cmd_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) { ... /* Add input fd to readset for select */ infd = buf_stack->fd; FD_SET(infd, readfds); 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--; cmdline = readline(); //cmdline = linenoise(""); if (cmdline) interpret_cmd(cmdline); } return result; } ``` 可以看到在 ```c=12 int result = select(nfds, readfds, writefds, exceptfds, timeout); ``` 去對已經加入 readset 的 fd 去做監視,並在有準備好資料輸入的情況下 ( after typing `<ENTER>`),進入下面的迴圈進行 ```c=21 cmdline = readline(); ``` 而在 `readline` 的實作中,就運用了 `RIO package` 的原理和方法, 可參考 [CSAPP Ch10 筆記 buffered I/O function](https://hackmd.io/@willychiu/Hktn-eWV8#Buffered-IO-Function) :::warning TODO: 除了研讀程式碼,能否修改程式碼並搭配 GDB 進行實驗? :notes: jserv ::: ## 挑戰題 ### 說明 [antirez/linenoise](https://github.com/antirez/linenoise) 運作原理 此 api 透過 ``` c char *linenoise(const char *prompt); ``` 作為呼叫入口,之後依照 `terminal` 輸入環境區分成三個運作方式 1. tty 呼叫流程為 :::warning TODO: HackMD 支援 GraphViz 語法,請改寫以下示意圖。 :notes: jserv ::: ``` linenoiseRaw → enableRawMode → linenoiseEdit ^ ^ | | Setting termios of STDIN | | | 在此執行額外功能( e.g. History, Completion, Hints... ) ``` 在 `enalbeRawMode` 中修改 STDIN 的設定,使其能在 [raw mode](https://www.gnu.org/software/mit-scheme/documentation/mit-scheme-ref/Terminal-Mode.html) 下輸入, 終端機介面又稱 tty 介面,分成兩種模式,一種為正規模式 ( cooked mode ) ,另一種是非正規模式 ( raw ) * cooked mode * 在這種模式中,終端設備會處理特殊字元,且會以一次一列的方式將輸入傳給應用程式。例如 `Linux` 的 `shell` 指令。 * raw mode * 在這種模式中,終端設備不會處理特殊字元,且會以一次一個字元的方式將輸入傳給應用程式。例如在 `Linux` 使用 `vim` 編輯程式。 termio 可以透過 [termios](http://man7.org/linux/man-pages/man3/termios.3.html) 來設定 ( 如下 ) ```c int tcgetattr(int fd, struct termios *termios_p); int tcsetattr(int fd, int optional_actions, const struct termios *termios_p); ``` 透過 bits 的調整來達到設定 > 這部分讓我聯想到之前在 Microchips 課堂中也是透過 bit 來控制 Microchip 的行為 ```c= /* 省略在 linenoise 中的例外錯誤處理 */ tcgetattr(fd,&orig_termios); raw = orig_termios; raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); raw.c_oflag &= ~(OPOST); raw.c_cflag |= (CS8); raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; tcsetattr(fd,TCSAFLUSH,&raw); ``` 在 ```c=9 tcsetattr(fd,TCSAFLUSH,&raw); ``` 使用 `TCSAFLUSH` option 將 `fd` `flush` 後設定剛剛的 `termio` 3. not a tty 此模式 read from file / pipe,和前者的差別為不需要限制輸入的 line size 4. Unsupported tty ### 在 lab0-c 中整合 [antirez/linenoise](https://github.com/antirez/linenoise) 實作的想法如下 - [x] 在 `lab0-c` 直接使用修改後的 `linenoise` ( i.e., 捨棄 `readline`,將 `readline` 替換成 ` linenoise ` ,並修改 `linenoise.c` 使其符合 `lab0-c` 的環境 ) > 上面的想法來自於 linenoise 的 [README.md](https://github.com/antirez/linenoise/blob/master/README.markdown#linenoise) 將他的使用方法講的非常簡當,而 [example.c](https://github.com/antirez/linenoise/blob/master/example.c) 也是...,但是目前卡關中xD > [solved] > [time=Thu, Feb 27, 2020 3:53 PM] #### 命令自動補完 ( Completion ) 首先了解在 lab-0c/qtest.c 中是如何處理輸入的, ``` qtest → ...some init... → run_console → push_file → cmd_done → cmd_select |<--------------qtest.c-------------... |<------------console.c-----------... ``` 在進入 `console.c` 之前,會先對需要的設定進行 initailize ( e.g. 處理 option, `queue_init`, `init_cmd`, `console_init`... ),並在 run_console 將 `input_file` 傳入, ``` run_console → push_file → cmd_done → cmd_select ^ ^ | | 將 fd 設定成 STDIN 或是 file | | 在這裡進行 cmd 的讀取 ``` 在 `cmd_select` 中利用 `select` 去監視 readfd set 中是否有哪個 `readfd` 是已準備好資料進行讀取的,程式碼片段如下, ```c= int cmd_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) { ... /* Add input fd to readset for select */ infd = buf_stack->fd; FD_SET(infd, readfds); 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--; cmdline = readline(); //cmdline = linenoise(""); if (cmdline) interpret_cmd(cmdline); } return result; } ``` 嘗試將20行替換成以下,目標是先在 STDIN 的輸入下,單純將讀取方式更改為 `linenoise` ```c=20 cmdline = linenoise(""); ``` 但卻需要重複兩次輸入才能在將 `STDIN` 寫進 `linenoise` ```console.log= cmd> new /* 第一次輸入 */ new /* 第二次輸入 */ q = [] cmd> ``` 進入 GDB 後發現, ```console.log (gdb) n 592 if (infd == STDIN_FILENO && prompt_flag) { (gdb) n 593 printf("%s", prompt); (gdb) n 594 fflush(stdout); (gdb) n cmd> 595 prompt_flag = true; (gdb) n 599 nfds = infd + 1; (gdb) n 601 if (nfds == 0) (gdb) n 604 int result = select(nfds, readfds, writefds, exceptfds, timeout); (gdb) n /* waiting for user input */ ... (gdb) n 612 result--; (gdb) n 614 ln_cmdline = linenoise("", buf_stack->fd); (gdb) n /* waiting for user input again*/ ``` 在執行到 `select` 時,`select` 會如預期的監視 `STDIN` 有沒有資料輸入,但是輸入後又會再 `linenoise` 處再停等一次,而不是將 `STDIN` 上已有的資料讀取。 >目前在這裡卡了一陣子,目前的想法是跟 `linenoise` 會去設定 `STDIN` 的模式的影響,但是我把 `raw mode` 關掉後卻只會立即輸出指令 >```console.log= >cmd> new /*first input*/ >new /*followed up output*/ >new /*second input*/ >new /*followed up output*/ >``` 重新思考 `linenoise` 的運作模式,是直接對 `STDIN` 做編輯和修改 ( i.e., 在還沒輸入 `<ENTER>` 之前即在進行 ),而 `select` 則是會在輸入 `<ENTER>` 之後才會認為 `STDIN` 已經準備好 ( 因為 `STDIN` 是 line buffered ),而 `linenoise.c/enableRawMode` 中的 ```c=9 tcsetattr(fd,TCSAFLUSH,&raw); ``` 會將 `STDIN` flush 掉,所以才會需要重新輸入一次。 但在只使用 `STDIN` 的情況下,做好在讀取之前的`buffer flush` ( 這部分 `linenoise.c/enableRawMode.c` 會幫我們做 ),就可以拿掉 `select` 後就直接使用 `linenoise` 了。 但是在讀檔的情況下也能拿掉 `select` 嗎?如果檔案是一個正在編寫中的文件就無法得知檔案目前是否已經資料準備好,所以仍然需要 `select`。 ```c= int cmd_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) { ... if (infd != STDIN_FILENO) { /* Not using STDIN as readfd */ 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--; cmdline = readline(); if (cmdline) interpret_cmd(cmdline); } return result; } else { cmdline = linenoise("cmd> "); if (cmdline) { if (cmdline[0] != '\n') { linenoiseHistoryAdd(cmdline); /* Add to the history. */ } interpret_cmd(cmdline); } free(cmdline); return 0; } } ``` 另外在執行時發現 `linenoise` 不會將 `"\n\0"` 回傳,但是在 cmd interpret 的時候需要以 `"\n\0"` 做判斷,再加入 ```c=25 } else { cmdline = linenoise("cmd> "); if (cmdline) { if (cmdline[0] != '\n') { linenoiseHistoryAdd(cmdline); /* Add to the history. */ linenoiseHistorySave("history.txt"); } char *cmdline_terminate = malloc(strlen(cmdline) + 2); strncpy(cmdline_terminate, cmdline, strlen(cmdline)); cmdline_terminate[strlen(cmdline)] = '\n'; cmdline_terminate[strlen(cmdline) + 1] = '\0'; interpret_cmd(cmdline_terminate); free(cmdline_terminate); } free(cmdline); return 0; } ``` 接完後就剩把 `linenoise` 中的 `complete` 功能在執行 `linenoise` 初始化。 先看看 `linenoise` 怎麼實作 `complete` 的,可以看到在 `completeLine` 的實作方式是用一個新的結構體 `linenoiseCompletions` ```c= typedef struct linenoiseCompletions { size_t len; char **cvec; } linenoiseCompletions; ``` `len` 指的是有著同樣開頭的 `cmd` 數量,而 `cvec` 則把同樣開頭的 `cmd` 以 linked list 串起來。所以我們只需要設定這個結構體即可,而在 `linenoise` 是用 callback 的方式去定義這個設定 function,需要先自行寫好再去 callback ```c= /* 自行撰寫 */ void completion(const char *buf, linenoiseCompletions *lc) { if (buf[0] == 'n') { linenoiseAddCompletion(lc,"new"); } else if (buf[0] == 's') { linenoiseAddCompletion(lc,"sort"); linenoiseAddCompletion(lc,"size"); linenoiseAddCompletion(lc,"show"); } ... } ``` ```c= void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) { completionCallback = fn; } ``` 並在初始化 console 的時候呼叫 ```c= linenoiseSetCompletionCallback(completion); ``` 如此一來,就可以在 `completeLine` 中直接使用 ```c= static int completeLine(struct linenoiseState *ls) { linenoiseCompletions lc = { 0, NULL }; int nread, nwritten; char c = 0; completionCallback(ls->buf,&lc); if (lc.len == 0) { linenoiseBeep(); } else { size_t stop = 0, i = 0; while(!stop) { /* Show completion or original buffer */ if (i < lc.len) { struct linenoiseState saved = *ls; ls->len = ls->pos = strlen(lc.cvec[i]); ls->buf = lc.cvec[i]; refreshLine(ls); ls->len = saved.len; ls->pos = saved.pos; ls->buf = saved.buf; } else { refreshLine(ls); } nread = read(ls->ifd,&c,1); if (nread <= 0) { freeCompletions(&lc); return -1; } switch(c) { case 9: /* tab */ i = (i+1) % (lc.len+1); if (i == lc.len) linenoiseBeep(); break; case 27: /* escape */ /* Re-show original buffer */ if (i < lc.len) refreshLine(ls); stop = 1; break; default: /* Update buffer and return */ if (i < lc.len) { nwritten = snprintf(ls->buf,ls->buflen,"%s",lc.cvec[i]); ls->len = ls->pos = nwritten; } stop = 1; break; } } } freeCompletions(&lc); return c; /* Return last read character */ } ``` 如此便完成了。而 history 則沒那麼複雜,直接使用 `char` 陣列紀錄。 ### 指出現有程式的缺陷 (提示: 和 RIO 套件 有關),嘗試強化並提交 pull request 以下的程式碼為 `console.c` 中片段, ```c= 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; } ``` 上面程式碼的 for-loop 中有關使用 `read` 的控制是在第 11 行 `if` ```c=10 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); ... if (buf_stack->cnt <= 0) { ... if (cnt > 0) { ... } } } /* Have text in buffer */ c = *buf_stack->bufptr++; *lptr++ = c; buf_stack->cnt--; if (c == '\n') break; } ``` 而 `buf_stack->cnt` 在最初 init 時會是 `0` ,毫無疑問的可以進入 `read` 的步驟,並將 `buf_stack->fd` 讀進 `buf_stack->buf`,之後再透過判斷 `read` 回傳值更新 `stack_buf->cnt`,根據 linux manual,`read` 的 return value <= 0 會有兩種情況: 1. `EOF` and `return 0` 2. `ERROR` and `return -1` 但在上面的程式碼片段中,只對遇到 `EOF` 的情況做處理,也就是說當 `read` 發生未預期的錯誤時,只把它當成 `EOF` 的狀況處理。 根據 CSAPP Ch10 提到 RIO 應該是在 `read` 的時候如果沒能完成任務 ( short has been happened ) ,應該繼續的嘗試 `read`,所以在程式中加入判斷是何種情形以及將十一行的 `if` 替換成 `while` 。 ```c= if (stack_buf->cnt < 0){ /* Handle with ERROR */ if (errno != EINTR) /* interrupted by sig handler return */ return NULL; /* read again */ } ``` 如下, ```c= static char *readline() { int cnt; char c; char *lptr = linebuf; if (!buf_stack) return NULL; for (cnt = 0; cnt < RIO_BUFSIZE - 2; cnt++) { while (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) { /* Check errno to check which error was happened and read it * again */ } else 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; } ``` :::warning TODO: 解釋如此修改的 side effect 以及你如何驗證 :notes: jserv :::