主講人: jserv / 課程討論區: 2025 年系統軟體課程
返回「Linux 核心設計」課程進度表Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
接下來,我們思考同時處理命令列輸入與網頁伺服器,先找出程式等待輸入時的主要迴圈,位於 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;
}
}
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_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 請求與對應的狀態碼,與上方 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
根據 I'm 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
命令進行連線,測試命令如下:
$ 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 有提供做法,就是將下面這行加進去 head 裡面即可:
<link rel="shortcut icon" href="#">
因此修改 send_response()
,將上面的程式加進去:
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
)
#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,傳送 HTTP request 給開著的 web server,並接收 HTTP response,查看是否與預期的結果相同。
在 testweb.c
中,傳送 new 的命令,但是回傳回來除了執行結果還多了換行和一個隨機的字元…
HTTP/1.1 200 OK
Content-Type: text/plain
l = []
v
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 中,如此一來即可回傳執行結果。