主講人: jserv / 課程討論區: 2023 年系統軟體課程
返回「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
cserv 是個高效的網頁伺服器,採用非阻塞式 I/O 的事件驅動架構。單執行緒、支援多核並善用 CPU affinity。
/conf/cserv.conf
,可以設定 log 路徑、等級、worker process 數量、每個 worker 最大連線數、coroutine stack 大小、監聽連接埠以及 HTTP request line 跟 header 的大小市占率來源: w3techs, 25 May 2022
100000 requests with 500 requests at a time(concurrency)
ab -n 100000 -c 500 -k http://127.0.0.1:8081/
Requests per second | Time per request (mean, across all concurrent requests) | |
---|---|---|
cserv | 17763.63 #/sec | 0.056 ms |
lwan | 15960.17 #/sec | 0.063 ms |
nginx | 53021.65 #/sec | 0.019 ms |
apache2 | 23558.77 #/sec | 0.042 ms |
100000 requests with 500 requests at a time(concurrency)
./htstress -n 100000 -c 500 127.0.0.1:8081/
requests/sec | total time | |
---|---|---|
cserv | 21457.851 #/sec | 4.660 s |
lwan | 18758.664 #/sec | 5.331 s |
nginx | 17322.661 #/sec | 5.773 s |
apache2 | 15122.679 #/sec | 6.613 s |
total 100000 requests with 500 connections
200 * 500 = 100000
httperf --server 127.0.0.1 --port 8081 --num-conn 500 --num-call 200 --http-version 1.0
requests/sec | connection rate | |
---|---|---|
cserv | 21872.7 #/sec | 10936.3 conn/s |
lwan | 27230.0 #/sec | 136.1 conn/s |
nginx | 16056.9 #/sec | 159.0 conn/s |
apache2 | 10087.8 #/sec | 98.9 conn/s |
non-blocking: 利用 timer 達成,攔截(hook)阻塞式的 socket
系統呼叫,使其在 timer 終止時結束
I/O multiplexing: 利用強大的 epoll,透過監聽 events 來決定處理的 I/O
timeout 時間設定(單位:毫秒)
#include <dlfcn.h>
之前,需要先 #define _GNU_SOURCE
,否則會遇到編譯錯誤編譯時記得加入 -ldl
執行結果如下
其中 HOOK_SYSCALL 參考 cserv 中的寫法
這邊 real_sys_… 就是該 system call 的原始備份,用在每個 hooked system call 中以及像 logger 等不需要 non-blocking 版本的情境
執行 ./cserv start
啟動伺服器後使用 ps
命令可以得到以下結果
從上面可得知,預設情況下 cserv 在 8 核的機器上建立 8 個 worker process,pid 分別是 799 到 806,從 ppid 都是 793 可以證明每個 worker process 都是由 master process fork 出來。
接著撰寫一個簡單的 shell script 來檢視各 worker process 的 cpu affinity
執行後得到以下結果
可以發現每個 worker process 各使用 1 個不同的 cpu core,出份運用運算資源。
在 Unix, Unix like, 及其他 POSIX 相容的作業系統中,signal 是一種常用的 IPC(Inter-Process Communication) 方式,可以看作應用程式間的 interrupt,用來通知 process 某個事件的發生,並且中斷該 process 的正常流程。
signal 可藉由多種方式觸發,比如使用 kill systemcall 可以對任意 process 發送任意訊號。
常見的訊號有
man 7 signal 中有給出詳細的訊號列表
process 接收到訊號後會藉由自身的 signal handler 進行處理,以下是 signal handler 的大致流程,可以發現 signal 與 interrupt 不同的點是,前者會轉發回 process 並在 user mode 中處理,後者則是全部在 kernel mode 中交由核心處理
因此我們可以藉由 signal handler 來處理某些特定的訊號,比如 SIGINT,預設的行為是終止 process,以下例子使用 signal system call 來註冊當程式接收到 SIGINT 訊號後的行為
signal handler 中使用
write
而非 printf 是後者為 non-async-signal-safe,用於訊號處理程式時可能會出錯
執行結果如下
如果將第 11 行拿掉,則按下 ctrl + c 後程式不會停止
而因為 signal 系統的作用在不同的系統/版本中可能會有不同的定義,考慮到可攜性,Linux 建議用 sigaction 這個較為通用的系統呼叫
參見: signal(2) 中的 warning
下面是使用 sigaction 改寫的版本,輸出結果如上
在 cserv 中就利用 sigaction 來處理幾種不同的 signal
並且對部分 signal 作了特殊的定義,如下方程式碼中,將 SIGHUP 當作重新設定的訊號
SIGHUP: hangup,掛斷,預設行為為終止,出現在當某個使用者登出時,系統會發送此訊號給同一 session 中的所有 process
例如當執行 ./cserv stop
時,會對 master process 發送 SIGQUIT 訊號
其中 read_pidfile 會去讀取 ./conf/cserv.pid,來得到 master process 的 pid
pid file: 存放 pid 的檔案,通常用來給其他 service 或 daemon 讀取 pid 以便對其發送 signal
而在 signal.c
中,利用 sigaction 將各 signal 與對應的 signal handler 綁定
當 master process 收到 SIGQUIT 訊號時,會將全域變數 g_shall_stop
設為 1 來告知 master process 要停止,並向所有 worker process 發送 SIGQUIT 訊號
而當 worker process 收到 SIGQUIT 時,也會將其 g_shall_stop
設為 1,以便在 process 迴圈內接收到停止的命令,然後斷線並停止。
這邊 g_shall_stop
變數也被用來檢查 worker prosses 的終止是否正常,若 g_shall_stop
或 g_shall_exit
均為 0,但 worker 依然要停止時就是發生異常
最後 master process 會等到所有 worker process 結束才終止
總而言之就是利用 signal 來進行 master/worker process 間的通訊。
log_object[]
的空間,讓各個 logger (processes) 均能夠存取shm.*
MAP_PRIVATE
或著 MAP_SHARED
來決定這段記憶體是否能讓別的 process 存取MAP_ANONYMOUS
來達到匿名映射,讓 mmap 不對 fd 做記憶體映射(通常將 fd 填入 -1)。MAP_SHARED
加上 MAP_ANONYMOUS
就可以輕鬆地達成親子 process 間的共享記憶體執行結果
可以發現本來共享空間中的 A A A A 被子行程改成了 B B B B 後,親代行程也讀到了 B B B B,以此證明該空間是親子行程的共享記憶體空間
首先宣告一個共享記憶體的結構,裡面自帶了一個 spinlock,用來在配置記憶體空間時鎖上,以防止其他 process 同時存取
先看 shm_pages_alloc
,顧名思義這個函式就是配製一個 page 的共享記憶體空間,其中 page size 取決於每個機器
這邊可以看到 mmap 的使用,大致上跟前面的範例一樣
而 shm_alloc
這個函式則是在已經配置的 page 中分配空間出來
假設下圖是一個 shm_page
offset 指向目前的裝到哪邊的位置,size 則是 page size
因此要配置一個 size_bytes 大小的空間,會先將 size_bytes 進位,做記憶體對齊
然後檢查當下的空間夠不夠裝,最後才調整 offset 並返回 addr + offset 的指標,指向這塊空間的起始位置
改繪自 Life of a HTTP request, as seen by my toy web server 中的 Diagram of main loop plus two coroutines
每個 worker 運作時會先派發一個 coroutine 來處理 tcp accept (等待 connection 進來並且處理) 的工作,然後進入 coroutine 排程的循環
而在 worker_accept_cycle 中,如果 worker 有新的連線進入,則分派一個 coroutine 來處理,如果無法立即處理則呼叫 schedule_timeout()
將該 coroutine 放進 inactive list 中等待, timeout 時再行處理
每個排程循環都會先檢查有沒有等待超過 timeout 的 coroutine
如果有的話,就將它從 inactive list 中轉移到 active list 的尾端
並且執行 active list 頭部的 coroutine (替換調目前的工作,讓整個排程持續前進)
sched.policy(get_recent_timespan())
這邊會對應到 event_cycle
(
sched.policy = event_cycle;
)
在 event cycle 中利用 epoll 來監聽各個事件,並在觸發事件時通知。
這邊 epoll_wait()
會佔用 get_recent_timespan()
回傳的值,也就是距離目前的 timer_node timeout 還剩下的時間
man epoll_wait(2)
The timeout argument specifies the number of milliseconds that epoll_wait() will block.
這段時間中若有事件觸發,則結束等待,切換處理下一個 coroutine (透過 run_active_coroutine()
)
src/coro/switch.*
src/coro/sched.*
參見 Site Cache vs Browser Cache vs Server Cache: What’s the Difference?
/src/util/cirbuf.h
/tmp/cserv.log
檔案,更改 /conf/cserv.log
中的 log_path
來設定CRIT
, ERR
, WARN
, INFO
四種等級
/conf/cserv.conf
中的 log_level
為要寫入 log 檔案中的臨界點 (threshold)log_object[]
,以便作業