--- title: 2025 年 Linux 核心設計課程作業 —— lab0 (C) image: https://i.imgur.com/robmeiQ.png description: 改寫自 CMU Introduction to Computer Systems 課程第一份作業,擴充 GNU/Linux 開發工具的使用並強化自動分析機制。 tags: linux2025 --- # N01: [lab0](https://hackmd.io/@sysprog/linux2025-lab0) > 主講人: [jserv](https://wiki.csie.ncku.edu.tw/User/jserv) / 課程討論區: [2025 年系統軟體課程](https://www.facebook.com/groups/system.software2025/) :mega: 返回「[Linux 核心設計](https://wiki.csie.ncku.edu.tw/linux/schedule)」課程進度表 ## 整合 [tiny-web-server](https://github.com/7890/tiny-web-server) 接下來,我們思考同時處理命令列輸入與網頁伺服器,先找出程式等待輸入時的主要迴圈,位於 `linenoise()->line_raw()->line_edit()` 內的 `while(1)`。但 linenoise 是用 `read` 等待使用者輸入,當 read 阻塞時,便無法接收 web 傳來的資訊。 * 嘗試用 [`select()`](https://man7.org/linux/man-pages/man2/select.2.html) 同時處理 stdin 及 socket: * 在 `line_edit` 中加入以下程式碼: ```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; } } ``` * I/O Multiplexing Model ![](https://hackmd.io/_uploads/HJ0_t8Kf9.jpg) `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, listenfd = -1; 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_fd is available, add to readfds */ + 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 ``` 根據 [I'm getting favicon.ico error](https://stackoverflow.com/questions/31075893/im-getting-favicon-ico-error) 描述,在 `<head>` 欄位中增加可讀取圖示位址的程式碼 `<link rel="shortcut icon" href="#"` 即可。此原因是因為某些網頁瀏覽器 (如 Chrome 瀏覽器) 會要求給予網頁圖案的需求,而原本提供的 head request 中沒有對應的資訊,故瀏覽器不會回傳正確的內容,而是一直提示要求給予 `favicon.ico` 檔案位置。 上述程式碼已整合進 `qtest`,可在終端機執行以下命令: ``` $ ./qtest cmd> web listen on port 9999, fd is 3 cmd> ``` 如訊息所示,這個小型的網頁伺服器正在等待埠號 `9999` 的網路連線。 接著開啟另一個終端機,用 `curl` 命令進行連線,測試命令如下: ```shell $ curl http://localhost:9999/new $ curl http://localhost:9999/ih/1 $ curl http://localhost:9999/ih/2 $ curl http://localhost:9999/ih/3 $ curl http://localhost:9999/sort $ curl http://localhost:9999/quit ``` ## 解決 `favicon.ico` 的問題 ![](https://hackmd.io/_uploads/HyXBLFPTo.png) 從瀏覽器發 request 後,可以看到上圖 favicon.ico 的 status 是 404。因為部分瀏覽器會要求 request 中要提供網頁圖案的對應資訊,而我上面的 response 沒有提供。 在 [I'm getting favicon.ico error](https://stackoverflow.com/questions/31075893/im-getting-favicon-ico-error) 有提供做法,就是將下面這行加進去 head 裡面即可: ```c <link rel="shortcut icon" href="#"> ``` 因此修改 `send_response()`,將上面的程式加進去: ```c void send_response(int out_fd) { char *buf = "HTTP/1.1 200 OK\r\n%s%s%s%s%s%s" "Content-Type: text/html\r\n\r\n" "<html><head><style>" "body{font-family: monospace; font-size: 13px;}" "td {padding: 1.5px 6px;}" "</style><link rel=\"shortcut icon\" href=\"#\">" "</head><body><table>\n"; writen(out_fd, buf, strlen(buf)); } ``` 再次嘗試,就都是 200 OK 了! ![](https://hackmd.io/_uploads/r1f88KDpj.png) ## 檢驗網頁伺服器行為是否正確 準備以下程式碼: (檔名: `testweb.c`) ```c #include <arpa/inet.h> #include <netinet/in.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #define TARGET_HOST "127.0.0.1" #define TARGET_PORT 9999 /* length of unique message (TODO below) should shorter than this */ #define MAX_MSG_LEN 1024 static const char *msg_dum = "GET /new HTTP/1.1\n\n"; int main(void) { int sock_fd; char dummy[MAX_MSG_LEN]; //struct timeval start, end; sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd == -1) { perror("socket"); exit(-1); } struct sockaddr_in info = { .sin_family = PF_INET, .sin_addr.s_addr = inet_addr(TARGET_HOST), .sin_port = htons(TARGET_PORT), }; if (connect(sock_fd, (struct sockaddr *) &info, sizeof(info)) == -1) { perror("connect"); exit(-1); } send(sock_fd, msg_dum, strlen(msg_dum), 0); recv(sock_fd, dummy, MAX_MSG_LEN, 0); shutdown(sock_fd, SHUT_RDWR); close(sock_fd); printf("%s\n", dummy); int count = 0, last_pos = 0; for (int i = 0; i < strlen(dummy); i++) { if (dummy[i] == '\n') { count++; } if (count == 3) { last_pos = i + 1; break; } } char *answer = "l = []"; printf("%ld %ld\n", strlen(answer), strlen(dummy + last_pos)); return 0; } ``` 更改 [bench.c](https://github.com/sysprog21/kecho/blob/master/bench.c),傳送 HTTP request 給開著的 web server,並接收 HTTP response,查看是否與預期的結果相同。 在 `testweb.c` 中,傳送 new 的命令,但是回傳回來除了執行結果還多了換行和一個隨機的字元... ``` HTTP/1.1 200 OK Content-Type: text/plain l = [] v ``` :::spoiler report 和 report_noreturn ```c extern int connfd; void report(int level, char *fmt, ...) { if (!verbfile) init_files(stdout, stdout); int bufferSize = 4096; char buffer[bufferSize]; if (level <= verblevel) { va_list ap; va_start(ap, fmt); vfprintf(verbfile, fmt, ap); fprintf(verbfile, "\n"); fflush(verbfile); va_end(ap); if (logfile) { va_start(ap, fmt); vfprintf(logfile, fmt, ap); fprintf(logfile, "\n"); fflush(logfile); va_end(ap); } va_start(ap, fmt); vsnprintf(buffer, bufferSize, fmt, ap); va_end(ap); } if (connfd) { int len = strlen(buffer); buffer[len] = '\n'; buffer[len + 1] = '\0'; send_response(connfd, buffer); } } void report_noreturn(int level, char *fmt, ...) { if (!verbfile) init_files(stdout, stdout); int bufferSize = 4096; char buffer[bufferSize]; if (level <= verblevel) { va_list ap; va_start(ap, fmt); vfprintf(verbfile, fmt, ap); fflush(verbfile); va_end(ap); if (logfile) { va_start(ap, fmt); vfprintf(logfile, fmt, ap); fflush(logfile); va_end(ap); } va_start(ap, fmt); vsnprintf(buffer, bufferSize, fmt, ap); va_end(ap); } if (connfd) { send_response(connfd, buffer); } } ``` ::: 因為原本的 web 實作中,HTTP 的 body 只會回傳 ok,現在要將原本輸出到標準輸出的結果也當作 HTTP response 傳回給 client。 在 `qtest.c` 的 do_operation 系列的函式,常常呼叫 report.c 的 `report` 和 `report_noreturn`,為了將執行結果輸出到標準輸出,其中兩者差別在是否有輸出換行,輸出後有需要換行就會呼叫 `report`,不需要會呼叫 `report_noreturn`。 在 `report` 和 `report_noreturn` 檢查是否有連線,如果有連線,就將結果一起寫進 response 中,如此一來即可回傳執行結果。