contributed by <shawn5141>
參照 Linux 核心模組掛載機制,解釋 $ sudo insmod khttpd.ko port=1999
這命令是如何讓 port=1999
傳遞到核心,作為核心模組初始化的參數呢?
過程中也會參照到 你所不知道的 C 語言:連結器和執行檔資訊
參照 CS:APP 第 11 章,給定的 kHTTPd 和書中的 web 伺服器有哪些流程是一致?又有什麼是你認為 kHTTPd 可改進的部分?
htstress.c
用到 epoll 系統呼叫,其作用為何?這樣的 HTTP 效能分析工具原理為何?
給定的 kecho
已使用 CMWQ,請陳述其優勢和用法
核心文件 Concurrency Managed Workqueue (cmwq) 提到 “The original create_*
workqueue() functions are deprecated and scheduled for removal”,請參閱 Linux 核心的 git log (不要用 Google 搜尋!),揣摩 Linux 核心開發者的考量
解釋 user-echo-server
運作原理,特別是 epoll 系統呼叫的使用
是否理解 bench
原理,能否比較 kecho
和 user-echo-server
表現?佐以製圖
解釋 drop-tcp-socket
核心模組運作原理。TIME-WAIT
sockets 又是什麼?
透過 insmod 這個程式(可執行檔)來將 kecho.ko 植入核心中。因為 insmod 會呼叫相關管理記憶體的 system call,將在 user space 中 kernel module 的資料複製到 kernel space 中。finit_module 就是所謂的system call。
以下為 執行 sudo strace insmod kecho.ko port=1999
的部分結果,可看到 port 1999 被當作參數傳入 finit_module
中。
但要如何讓這個參數(port=1999)實際被讀到呢? 參考 The Linux Kernel Module Programming Guide - 4.5 Passing Command Line Arguments to a Module,可以知道需要設定 module_param
與對應的參數。而我們的確在 kecho_mod.c
中找到 module_param
這個參數,而 port
就在其中。
而 module_param
定義在 include/linux/moduleparam.h。
__param_check
,會執行 compile-time type checking只要是產生 kernel_param
object
__section("__param")
透過 GCC extension 的 aribute((section(name))) 會把 data 放到 __param
的 位置。
Normally, the ARM compiler places the objects it generates in sections like .data and .bss. However, you might require additional data sections or you might want a variable to appear in a special section, for example, to map to special hardware.
If you use the section attribute, read-only variables are placed in RO data sections, read-write variables are placed in RW data sections unless you use the zero_init attribute. In this case, the variable is placed in a ZI section.
大部分的 work 都是通过 normal worker_pool 来执行的 ( 例如通过 schedule_work()、schedule_work_on() 压入到系统 workqueue(system_wq) 中的 work),最后都是通过 normal worker_pool 中的 worker 来执行的。这些 worker 是和某个 CPU 绑定的,work 一旦被 worker 开始执行,都是一直运行到某个 CPU 上的不会切换 CPU。
unbound worker_pool 相对应的意思,就是 worker 可以在多个 CPU 上调度的。但是他其实也是绑定的,只不过它绑定的单位不是 CPU 而是 node。所谓的 node 是对 NUMA(Non Uniform Memory Access Architecture) 系统来说的,NUMA 可能存在多个 node,每个 node 可能包含一个或者多个 CPU。
workqueue 和 pwq 是一对多的关系,pwq 和 worker_pool 是一对一的关系。
最终的目的还是把 work( 工作 ) 传递给 worker( 工人 ) 去执行,中间的数据结构和各种关系目的是把这件事组织的更加清晰高效。
worker_pool 怎么来动态增减 worker,这部分的算法是 CMWQ 的核心。其思想如下:
cmwq is a reimplementation of wq with focus on the following goals.
int socket(int family, int type, int protocol);
socket()打開一個網絡通訊連接埠,如果成功的話,就像open()一樣返回一個檔案描述符,應用程序可以像讀寫檔案一樣用read/write在網絡上收發數據,如果socket()調用出錯則返回-1。對於IPv4,family參數指定為AF_INET。對於TCP協議,type參數指定為SOCK_STREAM,表示面向流的傳輸協議。如果是UDP協議,則type參數指定為SOCK_DGRAM,表示面向數據報的傳輸協議。protocol參數的介紹從略,指定為0即可。\
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
伺服器程序所監聽的網絡地址和連接埠號通常是固定不變的,客戶端程序得知伺服器程序的地址和連接埠號後就可以向伺服器發起連接,因此伺服器需要調用bind綁定一個固定的網絡地址和連接埠號。bind()成功返回0,失敗返回-1。
bind()的作用是將參數sockfd和myaddr綁定在一起,使sockfd這個用於網絡通訊的檔案描述符監聽myaddr所描述的地址和連接埠號。前面講過,struct sockaddr *是一個通用指針類型,myaddr參數實際上可以接受多種協議的sockaddr結構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度。
myaddr
首先將整個結構體清零,然後設置地址類型為AF_INET,網絡地址為INADDR_ANY,這個宏表示本地的任意IP地址,因為伺服器可能有多個網卡,每個網卡也可能綁定多個IP地址,這樣設置可以在所有的IP地址上監聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址,連接埠號為SERV_PORT,我們定義為8000。
int listen(int sockfd, int backlog);
典型的伺服器程序可以同時服務于多個客戶端,當有客戶端發起連接時,伺服器調用的accept()返回並接受這個連接,如果有大量的客戶端發起連接而伺服器來不及處理,尚未accept的客戶端就處于連接等待狀態,listen()聲明sockfd處于監聽狀態,並且最多允許有backlog個客戶端處于連接待狀態,如果接收到更多的連接請求就忽略。listen()成功返回0,失敗返回-1。
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
三方握手完成後,伺服器調用accept()接受連接,如果伺服器調用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來。cliaddr是一個傳出參數,accept()返回時傳出客戶端的地址和連接埠號。addrlen參數是一個傳入傳出參數(value-result argument),傳入的是調用者提供的緩衝區cliaddr的長度以避免緩衝區溢出問題,傳出的是客戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩衝區)。如果給cliaddr參數傳NULL,表示不關心客戶端的地址。
int epoll_create(int size);
建立一個epoll instance 並且返回一個指代該例項的檔案描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
特定檔案描述符 (fd) 通過 epoll_ctl 註冊到 epoll instance (epfd)。
op: 通過指定op來新增(EPOLL_CTL_ADD)/修改(EPOLL_CTL_MOD)/刪除(EPOLL_CTL_DEL)需要偵聽的檔案描述符及其事件
fd 是想新增/刪減的某 socket 的 fd。
event 則是記錄著 fd 更多的資料
epoll_wait:
waits for I/O events(等待IO事件),如果沒有 event 回傳的話,會把 call thread 擋住。
epfd:
The interest list
The ready list: subset of interest list
event:
描述連結於 fd 的 object。 epoll_event 的結構如下
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
EPOLLIN 是監聽讀的事件,而 EPOLLET 是 edge-triggered,
水平觸發:就是只有高電平(1)或低電平(0)時才觸發通知,只要在這兩種狀態就能得到通知。邊緣觸發:只有電平發生變化(高電平到低電平,或者低電平到高電平)的時候才觸發通知。簡單理解,在水平觸發的時候,可以時刻監測IO的狀態,而邊緣觸發,只有下次IO活動到來的時候,才進行通知。因此,當監視數量描述符多的時候,邊緣觸發的epoll效率更高
延伸閱讀 Beej's Guide to Network Programming 不錯的C Socket 介紹
bench_worker
可以看到使用 pthread_mutex_lock
,pthread_mutex_unlock
,pthread_cond_broadcast
和 pthread_cond_wait
。thread 1 一開始會取得 lock。然後進到 line 2 判斷目前的 thread
數量是否為 MAX_THREAD 數量。如果不是的話就會進到 line6 的環圈,並且呼叫pthread_cond_wait(&worker_wait, &worker_lock)
後進入 wait
(這邊注意 pthread_cond_wait
會入下圖把前面鎖起來的 mutex
打開再進入阻塞)。
等到足夠的 thread 數量後,會呼叫 pthread_cond_broadcast
把所有的 thread 喚醒,開始建立 socket
。 可以注意到在廣播之前,先把 ready 設成 true,這是為了避免在廣播時還有人嘗試想睡在 condition variable 上(若 ready 為真,則不會呼叫 pthread_cond_wait)。既然廣播了,那我們又要回到 bench_worker 繼續做事。從 pthread_cond_wait 繼續往下做。
bench_worker
的後半段基本上就是在建立 socket。會去評估一次傳送和一次接受所需要的時間:kecho:
user-echo-server: