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
因為要同時處理命令列輸入與 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 系統呼叫的運作方式
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 鍵繼續:
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);
}
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()
能夠滿足需求:
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
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。readfds
為 select()
中的 readset,如果 readfds
尚未定義,則將新增的 readset 給它。infd
為當前處理的 file descriptor,在目前實作中為啟動程式 ./qtest -f [file]
時的 file。line 569 ~ line 573
為之前手動輸入時,infd
為 stdin(0) 時會進入,目前實做不會運行。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 內的內容以被讀取完畢,將結果印出並釋放空間。linux2021