---
title: 2023 年 Linux 核心設計/實作課程作業 —— lab0 (C)
image: https://i.imgur.com/robmeiQ.png
description: 改寫自 CMU Introduction to Computer Systems 課程第一份作業,擴充 GNU/Linux 開發工具的使用並強化自動分析機制。
tags: linux2023
---
# L01: [lab0](https://hackmd.io/@sysprog/linux2023-lab0)
> 主講人: [jserv](http://wiki.csie.ncku.edu.tw/User/jserv) / 課程討論區: [2023 年系統軟體課程](https://www.facebook.com/groups/system.software2023/)
:mega: 返回「[Linux 核心設計/實作](http://wiki.csie.ncku.edu.tw/linux/schedule)」課程進度表
## 整合 [tiny-web-server](https://github.com/7890/tiny-web-server)
接下來,我們思考同時處理命令列輸入與網頁伺服器,先找出程式等待輸入時的主要迴圈,位於 `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;
}
}
```
* I/O Multiplexing Model

`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
```
根據 [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` 的問題

從瀏覽器發 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 了!

## 檢驗網頁伺服器行為是否正確
準備以下程式碼: (檔名: `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 中,如此一來即可回傳執行結果。