Try   HackMD

2021q1 Homework1 (lab0)

contributed by < hankluo6 >

延續 2020q3 Homework1(lab0) 的開發
GitHub

環境

$ 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 至 lab0

因為要同時處理命令列輸入與 web 輸入,先找出程式等待輸入時的主要迴圈,位於 linenoise()->linenoiseRaw()->linenoiseEdit() 內的 while(1)。但 linenoise 是用 read 等待使用者輸入,當 read 阻塞時,便無法接收 web 傳來的資訊。

  • 嘗試用 select() 同時處理 stdin 及 socket:

    • linenoiseEdit 中加入以下程式碼:
    ​​​​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 指令,需探討兩種方法造成差異的原因,解決方法位在下方。

    TODO: 解釋 select 這類 I/O multiplexor 系統呼叫的運作方式

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    jserv

    • I/O Multiplexing Model

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    selectpoll 皆使用此種 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,防止程式停止接收使用者輸入:
    ​​​​int flags = fcntl(listenfd, F_GETFL);
    ​​​​fcntl(listenfd, F_SETFL, flags | O_NONBLOCK);
    
    • 接著修改 run_console()
    ​​​​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]):
    ​​​​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:
    ​​​​static bool do_web_cmd(int argc, char *argv[])
    ​​​​{
    ​​​​    listenfd = socket_init();
    ​​​​    noise = false;
    ​​​​    return true;
    ​​​​}
    
    • run_console() 依照 linenoise 開啟與否來選擇要使用 linenoise() 還是 cmd_select()
    ​​​​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() 新增對應的處理
    ​​​​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 請求與對應的狀態碼,與上方 process() 相同。
  • 運行結果

    ​​​​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
    

    直接從網址欄輸入 URL 可能會造成傳輸過程出現錯誤,已解決。

    • 解決方法

    根據 I'm 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()

    ​​​​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。
    • readfdsselect() 中的 readset,如果 readfds 尚未定義,則將新增的 readset 給它。
    • infd 為當前處理的 file descriptor,在目前實作中為啟動程式 ./qtest -f [file] 時的 file。
    • line 569 ~ line 573 為之前手動輸入時,infd 為 stdin(0) 時會進入,目前實做不會運行。
    • 如果當前 file descriptor 比輸入的參數 nfds 還大時,更新 nfds 為最大值加 1,這是為了設置之後 select()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--; ​​​​ if (has_infile) { ​​​​ char *cmdline; ​​​​ cmdline = readline(); ​​​​ if (cmdline) ​​​​ interpret_cmd(cmdline); ​​​​ } ​​​​}
    • 使用 select() 取得可以讀取的 descriptor 總數,目前實做中因只有一個 file,故永遠回傳 1。
    • line 591 ~ line 596 將 file 從 readset 中移除,並作對應的處理。
  • readline()

    ​​​​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