執行人: SimonLee0316
解說影片
dockyu
可以將實驗結果畫成圖表,比較能表現出數據間的差異
已補上,謝謝。
steven523
網頁伺服器在應對大量請求的同時,如何確保安全性不受影響?例如防範常見的網路攻擊 DDoS、SQL injection、XSS攻擊等,確保資料完整性和保護使用者隱私。
藉由 coroutine 和 POSIX Thread,以 M:N 執行緒模型建構高性能的網頁伺服器。
留意其 M:N 執行緒模型如何在 GNU/Linux 落實,特別是
__thread
(thread-local storage; tls) 和 C11 Atomics 的使用。
測驗八使用 coroutine 並搭配任務排程器實作任務之 啟動/暫停/恢復/建立/分離/收合操作。
__thread
的使用讓每一個執行緒都有自己的變數,根據 TLS 說明其用法。
__thread
可以單獨是用也可以與 extern、static
一起使用,但不可以與其他 storage class specifier 一起使用。
不要急著對程式碼進行逐行解說,你可能會落得「舉燭」的下場。揣摩程式開發者的想法,對整個程式碼進行想法的解讀,解釋到底要解決什麼問題,及用什麼手法。
task_runners
:儲存正在等待使用 cpu 的 task。
task_yielders
:儲存等待排程的 task。
task_map 是一個 hash-map style 的結構使用多個 binary search tree 又稱(aa-tree) 在 hashed shards 裡面,這允許 map 可以均勻增長, 無須分配,並且性能高於使用 single binary search tree。
task_paused
: 儲存被暫停的 task。
task_detached
: 儲存被分離的 task。
這裡可以想成被從排程分離的 task。
透過 quick_start
建立並啟動 coroutine:
.entry
: 要 coroutine 執行的函式。
.udate
: 放入的引數。
.entry
: task_entry
為真正要執行我們要 coroutine 執行函式的函式。
切換不同 coroutine,呼叫 coro_switch1,如果是第一次呼叫 desc 不會是 0,所以在 coro_switch1 裡面如果是第一次呼叫的話他的 switch to 參數是 0 ,代表需要透過 coro_asmctx_make
建立一個新的 coroutine ,在切換過去,如果是不是第一次的話就透過 _coro_asm_switch
,切換過去。
_coro_asm_switch 是用 x86-64 組合語言撰寫,將目前暫存器內容存回 目前執行 coroutine 內,在載入要切換過去之 task 之 contex。
切換後執行 task_entry
:
queue 是「佇列」,不是「實作」
先將要啟動的 coroutine 還有目前在執行的 coroutine 放入先前提到的等待排程的實作 上面,並呼叫切換 task。
如果目前沒有正在等待使用 cpu 的 task 且,
(!resumed_from_main && task_npaused > 0)
如果其中一個條件滿足則會 task_return_to_main
回到 main。
如果都不滿足則會將在等待排程實作上的 task 都移動到等待 cpu 的實作。
以上為討論一個 corouine 建立並執行的過程。
一個 task 讓出 cpu :
將自己插到等待排程的實作最後端,呼叫這個函式可以想像成主動放棄 cpu 的 task。
將 task 暫停,從排程中分離:
我們在前面說明儲存被分離的 task 結構中,可以看到前面並沒有 __thread
說明這個結構在多執行緒環境下,會有同步的問題所以需要透過加入適當的互斥鎖機制來確保同步問題。
確認上述說法是否合理。TLS 要解決的問題,是否跟任務排程器相關。
TLS 允許執行緒擁有私自的資料。對於每個執行緒來說,TLS 是獨一無二,不會相互影響,假設任務實作搭配使用 TLS ,且不使用多執行緒,在每個 cpu 上面只有一個執行緒,也就是可以說可以達到每個 cpu 上可以有各自的任務實作,並搭配排程器可以達到並行。
注意用語。
atomic_compare_exchange_weak 為 C11 提供的 atomic 指令 函式可以確保我們在對共享變數存取操作是 atomic,week 是可以允許 fail spuriously。
atomic_store 一個 atomic 操作寫入共享變數。
以下說明 Quiz8 是如何對 coroutine 做排程的相關操作 :
執行 co_root_entry 了,在裡面會不斷的新建 child coroutine。
運行順序如下。
do_test(test_task_start)
,會建立多個 coroutine 。
不要急著說「總結來說」,你真的懂這個程式如何運作?若是,說得出裡頭的實作缺失、可改進之處嗎?倘若沒有徹底掌握,何以「總結」。
了解。
透過 quick_start 呼叫 co_sleep 函式執行,會在呼叫 task_sleep。
task_sleep 中的 task_yeild 功能主要是將當前 coroutine 放入 task_yielders 的串列當中,這可以表示成主動放棄 cpu 的 coroutine ,可以想成在 sleep 的狀態並等待排程,將自己放入 task_yielders 串列的末端後,在呼叫task_switch 呼叫在 task_nrunners 排隊的 coroutine 執行。
這裡會不斷的將 coroutine 加入 task_yielders 直到 100ms 時間到。
建立100個 co_pause_one
coroutine 還有 一個 co_resume_all
coroutine ,並輪流執行。
co_pause_one
coroutine,每次都會做一次順序暫停一次逆序暫停,再做一次順序再做一次逆序。將目前被暫停的 coroutine 標記為 true ,並做暫停切換到其他 coroutine (包含 test_task_pause),從暫停回來之後將標記改回 false ,如果還有人在被暫停的話,那就將自己排到task_yielders
,等待所有人都恢復。
程式碼是如何做到逆序暫停?
答: 將 id 較小的 coroutine sleep 久一點 這樣 id 較大的 coroutine 就會比較先被暫停。
co_pause_one
coroutine 和 1 個 co_resume_all
依序交換執行之下,在 co_pause_one
裡面每一個逆/順序暫停中,最後都會檢查是否所有人都被恢復執行了才會繼續往下一個逆/順序暫停,而負責恢復執行的 coroutine 就是 co_resume_all
,在程式碼中可以看到,在暫停的時候的順序是 1. 順序暫停 2. 逆序暫停 3. 順序暫停 4. 逆序暫停,而恢復執行的順序卻是 1. 順序恢復 2. 順序恢復 3. 順序恢復 4. 逆序恢復 ,這種情況會使就算 coroutine 是早暫停的將會晚恢復執行。注意 task_yield
是將自己插入 task_yielders
末端,這樣的操作是可以再度被執行的。
但是如果使用 task_pause
的話是將自己插入到 task_paused
是一個 task_map
的結構末端, 也就是直到它被 resume 之前都不會被返回 task_yielders
。
避免「舉燭」,揣摩程式開發者當初的想法和規劃。
co_one
在啟動co_two,co_three,co_four
後,回到 main 等待所有的 task 都執行完畢,並在最後確認結束的順序是否與規劃的相同。
程式碼運作流程:
exitvals[] : 用來紀錄執行順序。
1. 啟動 co_one
coroutine
2. co_one
紀錄 1,並啟動 co_two
3. co_two
將自己放入 task_yielders
,task_sleep(1e7 * 2),它會比 co_three
晚被恢復執行
4. 回到 co_one
,啟動 co_three
5. co_three
將自己放入 task_yielders
,task_sleep(1e7)
6. 回到 co_one
,啟動 co_four
7. co_four
紀錄 4 將自己放入 task_yielders
8. co_one
呼叫 task_exit
,回到 test_task_exit
9. test_task_exit
確認是否還有 coroutine 沒有執行完,如果有就去執行它
c void test_task_exit(void) { /*...*/ while (task_active()) { task_resume(0); } /*...*/ }
10. 恢復執行 co_three
,紀錄 3
11. 恢復執行 co_two
,紀錄 2
12. test_task_exit 紀錄-2,表示所有任務完成。
至此 exitvals 內的值為 [1,4,-1,3,2,-2]
測試 coroutine 執行順序。
quickstart(co_yield)
-> task_start
-> coro_start
-> coro_switch0
-> coro_switch1
-> task_entry
注意用語!
task_entry
task_switch()
task_switch()
現在沒有 runners ,所以將 task_nyielders 全部搬到 runnerstask_yield()
,將自己插到 task_nyielders。task_switch()
。task_entry
task_switch()
task_entry()
,並呼叫 task_switchtask_yield()
,將自己插到 task_nyielders。task_yield()
,將自己插到 task_nyielders。task_entry
,並呼叫 task_switchtask_entry
,並呼叫 task_switchtest_task_order
。建立兩個 pthread 分別執行 threda0
和 thread1
執行後透過 pthread_join
等待 pthread 完成並回到 main。
threda0
程式碼分析:
啟動 100 個 coroutine ,並當被暫停的 coroutine 數量為 100 的時後(意思就是 task_paused 串列數量為 100),就開始將 task_paused 的所有 coroutine 搬到 task_detached 串列上。
thread1
程式碼分析:
如果 所有 coroutine 還沒都被 detached 的會就會卡在迴圈裡面。
離開迴圈後將所有 coroutine 從 task_detached 串列移回到 task_paused 串列,並把他們重新加回去 task_yielders 讓他們可以繼續被執行。
co_thread_one
程式碼分析:
將自己加到 task_yielders 串列直到 時間到。
在將自己放到 task_paused 串列。
task_detach
程式碼分析:
給定 要 detach 的 coroutine id ,找到並加到 task_detached 結構。
task_attach
程式碼分析:
給定 要 attach 的 coroutine id ,找到並加到 task_paused 結構。
工程人員該避免說「差不多」,要是有人說林志玲跟令堂的 DNA 序列有 99% 以上相同,就說「差不多」是台灣最美麗的女人,這樣對嗎?
使用精準的說法,避免「舉燭」。
了解。
將上方超連結指向的內容整合到本頁面。
將第 8 週測驗題提及的任務排程器擴充為網頁伺服器,關於測試和效能分析,見 ktcp。
應比較 NGINX、cserv,和本實作的效能表現
透過 fork (cpu 可用數量)個監聽行程 ,並綁定在特定 cpu ,這樣可以達到同時處理多個連線請求。
監聽特定埠號 (port number)
透過 start , stop
,開啟跟關閉伺服器
先創建 socket 並將網路協定設定為 IPV4,類型為 TCP。
為了防止 TCP/IP 在關閉 socket 後會處於 TIME-WAIT
狀態,使用 setsockopt
設定 SO_REUSEADDR
選項,好讓我們可以重新綁定 socket。
設定 TCP_CORK ,提高傳輸效率。
使用 bind and listen
將 socket 綁定到特定 port 和 address ,使其成為一個監聽 socket ,準備接受連線請求。
建立 cpu 可用數量個行程並綁定在特定 cpu 上面,先取得目前可以使用的 cpu 數量。
sysconf 取得目前可用 cpu 數量。
cpu_set_t
初始化一個 CPU 集合。
將 0-15 號 CPU 添加到集合中。
使用 sched_setaffinity 函数 函式將行程的 CPU 親和力設定為指定的 CPU 集合。
務必使用本課程教材規範的詞彙!
不要捨近求遠,回去看教材。
建立 coroutine 去循環接受請求。
確認行程是否有綁定在特定 cpu 。
worker_process_cycle
負責接受請求,handle_client
處理請求並給予回應。
accept 是一個 Blocking I/O,它會一直在這裡阻塞直到收到請求。
收到請求之後會建立 coroutine 去處理請求並給予回應。
注意書寫規範:
收到
在開啟伺服器之後,會先將 fork 的子行程,紀錄在 tmp/tcp_server.pid
,這樣當要停止的時候才可以透過 read_pidfile
將所有子行程停止。
注意用語,務必使用本課程教材規範的術語!
在 create_worker
中如果是親代行程 (pid > 0),會將子行程的 pid 紀錄在 pids[]
,在使用 create_pidfile
將值存入目錄裡面。
注意書寫規範:
而親代行程會在 create_workers
裡面等到所有子行程都停止才會關閉 listenfd 並停止。
解讀實驗結果。
fork 完子行程之後,會去產生一個 coroutine 去接收請求,而在接受到請求之後會在產生一個 coroutine 去處理請求。
注意書寫規範:
工具
比較對象
htstress
100000 requests with 500 requests at a time(concurrency)
request/sec | seconds | |
---|---|---|
apache | 37318.529 | 2.680 |
nginx | 45460.600 | 2.200 |
cserv | 46730.129 | 2.140 |
tcp_server | 44194.207 | 2.263 |
解釋實驗結果並量化分析。
從實驗結果中可以得出,在使用並行程式去處理並行的連線請求可以達到不錯的效果。
使用精準描述,注意 I/O multiplexing
善用 perf, Ftrace 等工具,找出效能瓶頸,並持續提升效能。
使用 perf 可以看到在關閉連線佔據大量 cpu cycle
引入 keep-alive 機制,減少反覆建立和關閉連線的成本。