contributed by < YiChianLin
>
htstress.c
流程htstress.c
為 client,做為發送給 server 的測試,未傳入參數時可以得到參數的設定模式,如下
對應在 script/test.sh
中的敘述:
-n
: 表示對 server 請求連線的數量-c
: 表示總體對 server 的連線數量-t
: 表示使用多少執行緒main
中主要建立與 server 的連線
getopt_long()
獲得輸入的參數,再透過 swtich
設定對應的變數getaddrinfo
取得多個 addrinfo
結構,裡面含有 server 的 IP 位址start_time()
紀錄時間,使用 gettimeofday()
計算運行時間pthread_create
創立參數所設定的執行數數量,執行 worker()
函式對應到每一個創建 client,發送連線請求給 server再來看到 worker()
函式,與 server 進行連線過程,分別要建立與 server 連線的 client 與 epoll 程序監聽
epoll_event
結構陣列儲存監聽資料,變數名稱為 evts[MAX_EVENT]
(MAX_EVENT 為設定監聽事件數量的最大值)epoll_create
(變數為 efd
)建立總體對 server 的 concurrency(1) 連線不過自從 Linux2.6.8 後 epoll_create 中 size 的引數是被忽略的,建立好後占用一個 fd,使用後必須呼叫 close() 關閉,否則會導致資源的浪費
init_conn()
,並設定 epoll 程序
struct econn ecs[concurrency], *ec
中,進行初始化將 efd(epoll fd) 與 socket(ecs) 傳入 init_conn()
中socket()
建立與 server 的連線,並返回 fd,傳入 ec->fd
中fctrl(ec->fd, F_SETFL, O_NONBLOCK)
將 socket 的 fd 更改為非阻塞式,相比於阻塞式的方式,不會因為讀取不到資料就會停著ec->fd
) 與 server 的 IP 地址連線,因為是 nonblocking 的型式,所以不會等待連線成功的時候才會返回,因此在未連線時會回傳一巨集 EAGAIN
表示未連線,所以將 connect()
在迴圈中執行到連線成功EPOLL_CTL_ADD
巨集加入監聽事件,並將 efd 事件設定為可寫的狀態,使用 EPOLLOUT
連線的初始化完成後,繼續看 worker()
處理 I/O 事件的無限 for-loop
epoll 監聽 :
epoll_wait
輪詢的方式將可用的 fd 儲存至 evts
陣列中htstress.c
中 evts.event
表示事件狀態的巨集:
epoll 的錯誤處理,以 if (evts[n].events & EPOLLERR){ ... }
判斷事件是否為錯誤狀態
SO_ERROR
紀錄錯誤訊息(0 為沒有錯誤的產生),看到宣告方式 if (getsockopt(efd, SOL_SOCKET, SO_ERROR, (void *) &error, &errlen) == 0)
,讀取到 efd
的資料將檢查的結果寫入至 error
變數中ISO/IEC 9899:2011 (P.283) : atomic_fetch function
These operations are atomic read-modify-write operations.
client 傳送數據至 server :
send()
函式開啟要傳送資料的 fd,再來傳送資料(包含傳送的資料與長度,以檔案的 offset 表示),傳送成功後會返回傳送資料的長度write()
,注意到 write
的第一個引數為 fd,這裡使用 2
,參考文章解釋,0 表示 STDIN
標準輸入(鍵盤),1 表示 STDOUT
標準輸出(終端機視窗),2 表示 STDERR
標準錯誤輸出(將錯誤訊息輸出至終端機))EPOLLIN
可讀的狀態,等待 server 傳送資料server 傳送數據至 client :
EPOLLIN
,使用 recv()
得到從 server 傳送來的資料,從 socket 的 fd 獲得,將獲得的資料存入 buffer(inbuf)
中。關閉 client 與 server 連線:
ret = 0
) 時,使用 close()
關閉 client 的 fd(要關閉否則會占用資源),這裡要注意的是在建立 epoll 監聽與 socket 連線,同時都要有對應的 close()
關閉其 fd,不過在 htstress.c
中沒有看到對 epoll 的 fd 進行 close()
的敘述。來自 epoll_create man page 的敘述:
對應的 epoll_create() 要透過 close() 將 epoll fd 關閉,不過若 epoll 所監聽所有的 fd 已被關閉,kernel 就會直接釋放 epoll 的相關資源
When no longer required, the file descriptor returned by epoll_create() should be closed by using close(2). When all file descriptors referring to an epoll instance have been closed, the kernel destroys the instance and releases the associated resources for reuse.
編譯 kHTTPd 原始碼
可以得到 htstress
執行檔與 khttpd.ko
核心模組,進行測試:
對 google 網站進行測試
kHTTPd 掛載時可指定 port 號碼: (預設是 port=8081)
使用 wget 工具可得到 index.html 所得到的內容為: Hello World!
利用 dmesg
查看可發現在 kernel 中出現了錯誤的訊息
由 errno.h 查詢到 #define ECONNRESET 104
表示 Connection reset by peer 一端的 socket 被關閉造成(主動關閉或是異常斷開),而另一端繼續發送封包而導致,而這個問題來自於在連線時採取 keep-alive 機制,對 server 端不會造成影響,只會顯示出錯誤訊息
看到 Risheng1128 同學提出的 pull request 他發現在 http_server.c 中的 http_server_worker()
函式,在處理到每一個 client 的連線時,將由 client 端所傳送到的資料放入 buf
中,但是每經過一次的連線處理時並沒有把 buf
內的資料歸零,很可能會導致資料的錯誤,包含在第一次 buf
經過 kmalloc
後並未初始化資料
buf
經過了連線後獲得的資使用 dmesg
查看就會發現以下的結果,buf
的資料有錯誤並不是我們所預期的 buf: GET / HTTP/1.1
因此將增加初始化 buf
的程式碼如下:
重新執行後,可以得到正確的結果
在看到這則 commit 後,思考有沒有其他的方式改善,memset
使用在 http_server_recv()
之前,目的是要初始化 kmalloc
的資料,因此嘗試找有無其他的方式如 calloc()
的方式將一開始配置的記憶體就先做一次初始化的動作,而在 kernel 配置記憶體的函式中 kzalloc()
就有這樣的作用
kzalloc - allocate memory. The memory is set to zero.
更改為:
實測後的結果也能達到一樣的效果,避免到 buf
的資料錯誤。不過要思考到從 kmalloc()
轉變成使用 kzalloc()
後是否會影響到處理的速度,所以使用到 ktime
相關 API 測試花費的時間,分別測試 kmalloc()
、 kzalloc()
、memset()
所花費的時間,測出的時間為奈秒(ns),在原程式碼中加入:
編譯後,插入 khttpd.ko
核心模組,並使用 telnet localhost 8081
進行連線,測試兩次的連線,再用 dmesg
輸出結果得到:
可以發現 kzalloc()
所花費的時間比 kmalloc()
、 memset()
還要短很多,而且這樣的執行順序可以將 memset()
放在判斷式後執行,若中途 client 端結束了連線,在判斷式中就會跳出連線的迴圈,減少 buf
再執行一次 memset()
過程,而面對一次更多的 client 連線請求可以減少更多的處理時間
TODO: 查看 kmalloc、 kzalloc man pages 或是相關文件,直觀推測來看 kzalloc 比起 kmalloc 所花的時間應該要比較多,但實測上並沒有
參考了 kecho 的作法引用到 khttp 中
http_service
管理 workqueue
,http_server
管理 work
,連接的方式都是採取 list_head
方式,將 work
、 workqueue
連接一起宣告 workqueue
,並在使用 workqueue 相關 API 時 include workqueue.h
workqueue_struct
定義於 workqueue.c 中,裡面有 CMWQ 文件中所提及的 unbound_attrs
可以設定 workqueue
在 unbound
條件下的屬性;還有提到 struct worker *rescuer
為確保在釋放記憶體時不會產生 deadlock
的情形alloc_workqueue
定義於 workqueue.c 中初始化一個 workqueue
並在 flag 的設定為 WQ_UNBOUND
表示不會被特定的 CPU 所限制,使資源不會被閒置,可以透過切換的方式執行未完成的任務http_server_daemon()
為每一個連線的請求建立一個 work
進行處理(work = create_work(socket);
),而建立出來的 work
會由作業系統分配 worker
執行,配置後由 khttp_wq
將每一個 work
用 list_head
的 linked list
進行管理,使用到 queue_work()
將 work
放入 workqueue
中http_worker()
: 建立新的 worker
透過 container_of
找到結構中的 struct work_struct http_work
建立 socket
連線任務與對應 worker
處理create_work()
:為每一個連線請求進行 kernel space
(kmalloc
) 的動態記憶體配置,並進行初始化,再透過 list_add
加入到 workqueue
中free_work()
:用於釋放掉所建立連線的所分配的記憶體空間,使用 list_for_each_entry_safe
巨集走訪每一個在 workqueue
中所管理的 work
kernel_sock_shutdown()
: 斷開 socket 的連線(包含傳送與接收的功能),對應的巨集 SHUT_RDWR
關閉方式flush_work()
:等待當前的的 work
執行完畢sock_release()
: 根據文件的註解,將 socket
釋放在 stack
,也會斷開對應連接的 fd
kfree()
:釋放掉從 kmalloc
所配置出的記憶體空間make check
後比較引入 CMWQ 後處理 100000 筆的請求很明顯看出在處理的速度上加快了很多,提升約有 4 倍的速度
參照 Linux 核心模組掛載機制,在掛載核心時,對應到引數 port=1999
的設定方式
在執行掛載核心模組時,加入 strace
可以追蹤 insmod khttpd.ko port=1999
使用了甚麼系統呼叫
執行後的結果為下列所示:
execve()
為執行程式的系統呼叫,將輸入的命令執行#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
(argv is an array of pointers to strings passed to the new program as its command-line arguments.)
finit_module(3, "port=1999", 0)
,查看 finit_module man page finit_module()
實作方式,來源自 kernel/module.cload_module()
字串中的 "port=1999" 就是將 main.c
中的 port
引數 8081(default 值) 改為 1999,當中就是利用 strndup_user()
函式,把使用者的參數設定輸入進去finit_module()
int finit_module(int fd, const char *param_values, int flags);
我們注意對第二個引數 param_values 中的敘述:
The param_values argument is a string containing space-delimited specifications of the values for module parameters (defined inside the module using module_param() and module_param_array()).
為由字串所存下參數的在模組資訊,實際定義在module_param()
中
module_param()
巨集完成,詳細定義於 linux/moduleparam.hname
為傳入變數名稱,以專案舉例傳入 port
的變數作為設定type
為傳入變數之變數宣告,以專案舉例傳入 ushort
(typedef unsigned short int)perm
為存取 sysfs 的權限,以專案舉例設定參數為 S_IRUGO
(為 stat.h 中的巨集)S_IRUGO 巨集,表示可被所有人進行讀取(不能寫),巨集定義參考
#define S_IRUGO (S_IRUSR|S_IRGRP|S_IROTH)
其中有三個變數分別定義為 00400 00040 00004 ,其 4 表示權限為讀取
S_IRUSR 為允許擁有者、S_IRGRP 為允許擁有者群組、S_IROTH 為允許其他使用者
insmod
命令動態載入一個核心模組,會辨認到 module_init()
的系統呼叫,初始化函式,以本例的 main.c
中:static inline ...
,裡面的 __maybe_unused
巨集可展開為 __attribute__((unused))
表示該函式可能是未使用的,因此 GCC 不會為此函式產生警告initcall_t
宣告 typedef int (*initcall_t)(void)
為函式指標,透過 __inittest(void)
判斷傳入的函式是否合法(需要與 initcall_t
型態一樣)不然編譯器會報錯,得以回傳 initfn
(khttpd_init()
)GCC手冊
對於 __attribute__((alias(#initfn)))
解釋:alias ("target")
用於宣告不同名稱("別名"),以本例來說宣告#initfn
的函式名稱,而透過前置處理器的展開可以得知,initfn
就是khttpd_init()
注意到#
的使用為 Stringizing 字串化,於前置處理器篇有提到
The alias attribute causes the declaration to be emitted as an alias for another symbol, which must have been previously declared with the same type, and for variables, also the same size and alignment.
init_module()
,insmod
會呼叫到此系統呼叫的函式,藉此將自定義的函式載入至 kernel space
中init_module man page
init_module() loads an ELF image into kernel space, performs any necessary symbol relocations, initializes module parameters to values provided by the caller,…
getaddrinfo()
啟用程序,回傳值為 struct addrinfo
的結構,裡面就含有連線所需要的資料,如:IP 位址、 port (通訊埠)、 server 名稱…等等socket()
建立連接,回傳值為 file descriptor
,注意只有建立連結但不會操作系統,也不會往網路上傳送任何內容socket man page
socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint.
bind()
函式將 socket
與特定的 IP 位址和 port 連接起來(在 kernel space 中進行)listen()
,準備接受來自 client 的請求accept()
將 client 連接connect()
發送 Connection request 等待 server accept
此流程只能用於單一的 server/client 的連線,依照需求適用於小型連線系統,如:路由器內部系統設定
來源 : 課程錄影
fork
行程去處理到對應的 client ,所以在處理的過程中,子行程只要對應 client 就好,不會干涉到其他子行程的運作(Address space
獨立),能完成多 client 的連線需求。以 server 的角度就是持續的接受連線的請求
htstress.c
用到 epoll
系統呼叫首先看到 I/O 事件的模型為:
select() can monitor only file descriptors numbers that are less than FD_SETSIZE (1024)
epoll API : monitoring multiple file descriptors to see if I/O is possible on any of them.
user space
透過 epoll_wait
系統呼叫後,經過 mode transition,開始在核心中監聽,透過監聽的時間決定(參數 timeout
決定),注意到在 htstress.c
的設定為 -1,epoll_wait
會保持 blocking
的方式直到任一個 I/O 事件變為就緒,就緒後會切回 user space
中,而在回傳的數值為監聽到的事件數量(epoll_wait() returns the number of file descriptors ready for the requested I/O),根據數量來做對應的處理。EPOLL_CTL_ADD
、 EPOLL_CTL_MOD
、 EPOLL_CTL_DEL
參考: Http 文章
HTTP 協定(HyperText Transfer Protocol,超文本傳輸協定),其中在 1996 年發布了 Http 1.0 的版本,規定 client 和 server 保持短暫的連線,而 client 每次的請求都需要與 server 建立一個 TCP 的連線,但是在 server 處理完 client 一次的請求後即關閉 TCP 的連線,server 不會留下任何紀錄。
於 1999 年發布 HTTP 1.1 版本,改善了 HTTP 1.0 的效能問題,在 HTTP 1.1 中增加 Connection 欄位可設定 Keep-Alive 保持與 HTTP 連線不中斷,避免每次的請求要重複建立 TCP 的連線(1.0 版本預設沒有 Keep-Alive,1.1 版本預設有)
Keep-Alive 模式:
優點 : Keep-Alive 模式更加高效,因為避免了建立和釋放的過程
缺點 : 長時間的 TCP 連線容易導致系統資源無效佔用,浪費系統資源
使用 telnet
對 www.google.com
發送請求
回應:
參考文章 : Tcp-3-way-handshake-process
為 TCP 傳輸中建立連線傳送的方式,又稱三向交握
與 UDP 相比不需要這些步驟,沒有連線要求、連線終止或是流量控制的管理程序,優點在於傳輸速率較快,主要應用少量、即時性的傳輸,資料的正確性要求不高(語音或視訊),相對的在傳輸過程可能會有資料重覆、資料未依序到達、資料遺失等等問題
Ethernet(乙太網路) 是由 Intel 、 Xerox 和 Digital 所共同制定,遵循 IEEE 802.3 協定的網路硬體標準,也是大部分區域網路的標準,其中在有線連接的情況使用 CSMA/CD (Carrier Sense Multiple Access with Collision Detection,載波偵聽多路存取)的通訊協定,無線連接的情況使用 CSMA/CA (Collision Avoidance)通訊協定
CSMA/CD(以網路匯流排(BUS)架構舉例),包含期以下幾項特性
frame 的大小為 DA 到 FCS 間的位元組數目,而一個 frame 長度必須大於或等於 64 位元組,主要是要能夠再傳送完畢之前偵測衝撞(Collison),每一個工作站的監聽時間為 1 個時槽時間(51.2微秒,以實際上元件處理的時間為 46.38 微秒,考慮到 2 的冪次方處理上的方便)
網路的傳輸速度為 10 Mbps,所以在一個時槽的時間內可以傳送:
所以每一個 frame 必須大於或等於 64 位元組,否則在 frame 可能在衝撞尚未偵測出來之前被傳送完畢,傳送的結果是成功還是失敗無法確定,在 PAD 欄位是當 LLC 的長度不夠時用來增加長度,DA(6) + SA(6) + Length(2) + FCS(4) = 18 位元組,所以 LLC + PAD(LLC 長度不夠才需要補) 的長度要大於等於 64 - 18 = 46 位元組,而為了避免某一個工作站佔用傳輸媒介太久,每一筆 frame 的最大長度也受到限制,為 1518 位元組(LLC 1500 位元組 + 18 位元組),不含 Preamble 和 SFD 欄位
參照 TCP 協定 RFC 793 page.40
Because segments may be lost due to errors(checksum test failure), or network congestion, TCP uses retransmission (after a timeout) to ensure delivery of every segment.Duplicate segments may arrive due to network or TCP retransmission.
為資料連接層,介於 OSI 實體層(第一層)與網路層(第三層)之間,傳輸中的最小單位為 frame 可在同一個區域網路中的設備進行傳輸與接收,其中包含錯誤偵測、網路資料連線等等。可以使用橋接器(Network Bridge) 連接 LAN 的分段區域,創建單獨的廣播區域從而創建 VLAN,為一個獨立的邏輯網路,將設備獨立於 LAN 的物理位置,若無橋接和 VLAN,以乙太網路特性會廣播到所有的設備中,使所有的設備都要檢測 LAN 上的所有封包。
第二層中包含兩個子層:
Linux 中含有 Workqueue 的機制,使用 struct list_head
將要處理的每一個任務連接,依次取出處理,處理結束的在從 queue 中刪除,目的是要簡化執行續的建立,可以根據當前系統的 CPU 個數建立執行緒,使得並行化執行緒。
Workqueue 有兩種方式:
在 Multi Thread 方式下會造成系統資源上的浪費,更別說 Single Thread 的影響,而並行的效果也有限,所以進行對 Workqueue 的改善(CMWQ),有以下三個目標:
worker pool 管理每個 worker
workqueue 連接所有的 work 所形成的 queue
CMWQ 架構圖:
引入了 work item 的概念,為一個簡單的結構包含函式指標指向非同步任務運作的函式,執行時創建一個 work item 和(放入) workqueue。而處理這些任務的執行緒為 worker threads 用於一個接一個處理這些任務,而在任務結束後 thread 會變為 idle 狀態,而 worker-pools 就是用來管理這些 threads
兩種 worker-pools 類型:
引入 alloc_workqueue
來自這則 commit 中提到 workqueue 有很多的功能存在,皆是利用參數去調控,在調整上可利用 flag 各式巨集去新增功能(如:WQ_UNBOUND
、 WQ_DFL_ACTIVE
)
引用自原文:
Now that workqueue is more featureful, there should be a public workqueue creation function which takes paramters to control them. Rename _create_workqueue() to alloc_workqueue() and make 0 max_active mean WQ_DFL_ACTIVE. In the long run, all create_workqueue*() will be converted over to alloc_workqueue().
可以看到在這則之前的 commit 建立一個 real time 的 workqueue
在 __create_workqueue
巨集中要更改到整個函式的 prototype,而使用到此巨集的的函式皆要跟著改動所有的函式宣告配合巨集,在新增功能的時候會花費大量時間去改動相關的函式,舉例:
更改後相關的 workqueue
實作以新增巨集或是修改巨集為主要方向,在未來有更多的想法上都是透過這樣的方式,如:
workqueue: remove WQ_SINGLE_CPU and use WQ_UNBOUND instead
$ sudo insmod khttpd.ko port=1999
這命令是如何讓 port=1999
傳遞到核心,作為核心模組初始化的參數呢?
htstress.c
用到 epoll 系統呼叫,其作用為何?這樣的 HTTP 效能分析工具原理為何?kecho
已使用 CMWQ,請陳述其優勢和用法*
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 又是什麼?