# 2025q1 homework 6 contributed by <`Andrushika`> ## RCU 同步機制及用法 借用教材中的一張圖片,來說明 RCU 的機制: ![image](https://hackmd.io/_uploads/ByBFsEGbxe.png) 圖片上方有一個 writer,下方有多個 reader。RCU 提供所謂的寬限期,每個 reader 要使用資源時,會進行 lock;當 writer 更新資料時,不會馬上把舊資料刪除,而是先將其保留,直到所有 reader lock 被釋放為止,這樣先前取用資料到一半的 reader 就不會受到影響。 而「寬限期」指的就是 writer update 到 reader 全部 unlock 的這段期間。這段期間內,不同的 reader 可能看到不同版本的資料;分界點取決於 reader 取用 lock 的時機點,如果 reader lock 在 writer update 之前,會取用到被保留下來的舊資料,反之則會取用到新資料。 ### linux 核心中的 RCU 函式 `rcu_read_lock()`, `rcu_read_unlock()`:reader 使用,宣告 RCU 讀取的開始與結束。當還有 reader 持有 lock、writer 嘗試 update 時,進入寬限期。 `synchronize_rcu()` 是一個 blocking 的函式,會等待所有正在 read lock 的 reader 結束,之後才可以釋放舊資料。 `call_rcu(rcu_head, callback)` 則是 non-blocking 版本的,可以用來註冊一個 callback 函式,當所有 reader unlock 時執行 callback。 `rcu_dereference()`,`rcu_assign_pointer()`:分別用於 pointer 的安全讀寫,帶有 `READ_ONCE`, `WRITE_ONCE` 的相同功能,且帶有 memory barrier。舉 `rcu_assign_pointer()` 的例子來說: ```c void foo_update(foo *new_fp) { spin_lock(&foo_mutex); foo *old_fp = global_foo; new_fp->a = 1; new_fp->b = 'b'; new_fp->c = 100; // global_foo = new_fp; // wrong rcu_assign_pointer(global_foo, new_fp); // right spin_unlock(&foo_mutex); synchronize_rcu(); kfree(old_fp); } ``` 如果沒有使用那個函式,因為不帶有 memory barrier,而可能會在 `new_fp` 更新到一半,就把 `new_fp` assign 給 `global_foo` 了。正確的方式是要使用 `rcu_assign_pointer()`,才能保證寫入順序的正確性。 :::info [教材](https://hackmd.io/@sysprog/linux-rcu#%E9%A1%A7%E5%8F%8A-memory-ordering-%E7%9A%84%E5%BD%B1%E9%9F%BF)裡面有一句話: > 我們可見其實作只是在數值指派之前,**加了 WRITE_ONCE 這樣的 memory barrier** 來確保程式碼的執行順序。 我對這裡的敘述有疑問,`WRITE_ONCE` 似乎並不能直接說是 memory barrier。我去找了 `rcu_assign_pointer()` 的定義: ```c #define rcu_assign_pointer(p, v) \ do { \ uintptr_t _r_a_p__v = (uintptr_t)(v); \ rcu_check_sparse(p, __rcu); \ \ if (__builtin_constant_p(v) && (_r_a_p__v) == (uintptr_t)NULL) \ WRITE_ONCE((p), (typeof(p))(_r_a_p__v)); \ else \ smp_store_release(&p, RCU_INITIALIZER((typeof(p))_r_a_p__v)); \ } while (0) ``` 上方程式碼可見,當 `v` (要 assign 的新值)是 constant 且 `NULL` 的時候,會直接用 WRITE_ONCE;反之會呼叫 `smp_store_release()`,於是我又看了該函式的定義 [linux/tools/include/asm/barrier.h](https://github.com/torvalds/linux/blob/9f35e33144ae5377d6a8de86dd3bd4d995c6ac65/tools/include/asm/barrier.h#L49): ```c # define smp_store_release(p, v) \ do { \ smp_mb(); \ WRITE_ONCE(*p, v); \ } while (0) ``` 這邊同時使用了 memory barrier 和 `WRITE_ONCE`,這也變相說明了兩者應該是不同的事情。`READ_ONCE`,`WRITE_ONCE` 的主要目的是阻止編譯器的優化行為,每次讀都一定從記憶體抓,每次寫都真的寫回記憶體,不能偷懶、不能延後。 ::: ## 測試網頁伺服器的效能,並針對多核處理器場景調整 後續實驗我將同時使用 `hstress` 以及 [wrk](https://github.com/wg/wrk) 作為我的 benchmarking tool,不同工具的比較使實驗更精準。使用 `wrk` 而非 `ab` 的好處是,其可以指定 client 使用多少 threads 進行壓力測試(`hstress` 當中也有該功能),比如: ```shell $ wrk -t8 -c200 -d10s http://localhost/ ``` 其中 `-t8` 代表同時使用 8 個 thread,使請求的發送可以平行化,當 concurrent 數量大的時候,能夠真正給予伺服器壓力,而不會因為只有一個 thread 在發送請求導致壓力推不上去;尤其在多核伺服器的測試中更可以看出差別。此外 `wrk` 可以輸出詳細的 metrics,其輸出格式如下(範例): ```bash Running 30s test @ http://127.0.0.1:8080/index.html 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 635.91us 0.89ms 12.92ms 93.69% Req/Sec 56.20k 8.07k 62.00k 86.54% 22464657 requests in 30.00s, 17.76GB read Requests/sec: 748868.53 Transfer/sec: 606.33MB ``` 其額外包含 tail latency 和標準差可供參考。 ## 使用 ftrace 追蹤 khttpd 效能瓶頸 首先,要啟用 `gcc` 的 code instrument 機制,需要在 `makefile` 中編譯時使用 `-pg` flag: ``` ccflags-y += -pg ``` 之後檢查 ftrace 可追蹤到的函式: ```shell sudo cat /sys/kernel/debug/tracing/available_filter_functions | grep khttpd ``` 但我發現,有部份函式沒有出現在可 tracing 的列表中。詢問 ChatGPT 後,發現是編譯器會自動優化掉一些較小且只有單點呼叫的函式;此時需要在消失的函式前方加上 `noinline` 防止優化: ```c static noinline struct work_struct *create_work(struct socket *socket) static noinline void free_work(void) ``` 之後就可以正確在 `available_filter_functions` 中看見想要 trace 的函式: ```shell andrew@andrew-Alienware-m15-R6:~/linux2025/khttpd$ sudo cat /sys/kernel/debug/tracing/available_filter_functions | grep khttpd parse_url_char [khttpd] http_message_needs_eof [khttpd] http_should_keep_alive [khttpd] http_parser_execute [khttpd] http_method_str [khttpd] http_status_str [khttpd] http_parser_init [khttpd] http_parser_settings_init [khttpd] http_errno_name [khttpd] http_errno_description [khttpd] http_parser_url_init [khttpd] http_parser_parse_url [khttpd] http_parser_pause [khttpd] http_body_is_final [khttpd] http_parser_version [khttpd] http_parser_set_max_header_size [khttpd] http_parser_callback_header_field [khttpd] http_parser_callback_headers_complete [khttpd] http_parser_callback_request_url [khttpd] http_parser_callback_message_begin [khttpd] create_work [khttpd] free_work [khttpd] http_parser_callback_header_value [khttpd] http_server_recv.constprop.0 [khttpd] http_server_worker [khttpd] http_server_send.isra.0 [khttpd] http_parser_callback_message_complete [khttpd] http_parser_callback_body [khttpd] http_server_daemon [khttpd] ``` 接下來需要設置 `ftrace_filter`,避免太多底層的呼叫混入 trace 紀錄中影響判讀。在 *Demystifying the Linux CPU Scheduler* 一書中,使用到了「僅保留特定 PID」的過濾方式,然而這在 khttpd 的測試中不管用:因為使用了 CMWQ 分配 kworker 執行任務,其 PID 會不斷生成,所以需要使用別的過濾方式。 我一開始把所有 khttpd 專案中的 function 全部寫入 `set_ftrace_filter`,於是就產生了下面這一長串: ```shell echo http_server_worker >> $TRACE_DIR/set_ftrace_filter echo create_work >> $TRACE_DIR/set_ftrace_filter echo http_parser_callback_message_begin >> $TRACE_DIR/set_ftrace_filter echo http_parser_callback_request_url >> $TRACE_DIR/set_ftrace_filter echo http_parser_callback_header_field >> $TRACE_DIR/set_ftrace_filter echo http_parser_callback_header_value >> $TRACE_DIR/set_ftrace_filter echo http_parser_callback_headers_complete >> $TRACE_DIR/set_ftrace_filter echo http_parser_callback_body >> $TRACE_DIR/set_ftrace_filter echo http_parser_callback_message_complete >> $TRACE_DIR/set_ftrace_filter echo http_parser_init >> $TRACE_DIR/set_ftrace_filter echo http_parser_execute >> $TRACE_DIR/set_ftrace_filter echo http_should_keep_alive >> $TRACE_DIR/set_ftrace_filter ``` 後來再看老師寫的作業說明,裡面原來提供了更簡潔的寫法: ```shell echo '*:mod:khttpd' >> $TRACE_DIR/set_ftrace_filter ``` 這個故事告訴我們要好好閱讀作業說明,不要偷懶。 其實 ftrace 的官方文件也有說明更詳細的 [filter commands](https://docs.kernel.org/trace/ftrace.html#filter-commands) 用法。 以下 script 設定好要觀測的函式後,執行 `curl` 向 server 發出請求並印出 trace 結果;記得執行 shell 前需要先掛載 kernel module。 ```shell #!/bin/bash TRACE_DIR=/sys/kernel/debug/tracing # clean up echo > $TRACE_DIR/trace echo > $TRACE_DIR/set_graph_function echo > $TRACE_DIR/set_ftrace_filter echo function_graph > $TRACE_DIR/current_tracer echo http_server_worker >> $TRACE_DIR/set_graph_function echo create_work >> $TRACE_DIR/set_graph_function echo '*:mod:khttpd' > $TRACE_DIR/set_ftrace_filter echo funcgraph-tail > $TRACE_DIR/trace_options echo 1 > $TRACE_DIR/tracing_on curl -s http://localhost:8081/ > /dev/null echo 0 > $TRACE_DIR/tracing_on cat $TRACE_DIR/trace ``` 執行後可以觀測到 ftrace 結果: ``` andrew@andrew-Alienware-m15-R6:~/linux2025/khttpd/scripts$ sudo ./trace_graph_func.sh # tracer: function_graph # # CPU DURATION FUNCTION CALLS # | | | | | | | 3) 3.413 us | create_work [khttpd](); 12) | http_server_worker [khttpd]() { 12) 1.543 us | http_parser_init [khttpd](); 12) 6.866 us | http_server_recv.constprop.0 [khttpd](); 12) | http_parser_execute [khttpd]() { 12) 0.500 us | http_parser_callback_message_begin [khttpd](); 12) 0.548 us | parse_url_char [khttpd](); 12) 0.397 us | http_parser_callback_request_url [khttpd](); 12) 0.276 us | http_parser_callback_header_field [khttpd](); 12) 0.372 us | http_parser_callback_header_value [khttpd](); 12) 0.277 us | http_parser_callback_header_field [khttpd](); 12) 0.233 us | http_parser_callback_header_value [khttpd](); 12) 0.262 us | http_parser_callback_header_field [khttpd](); 12) 0.262 us | http_parser_callback_header_value [khttpd](); 12) 0.372 us | http_parser_callback_headers_complete [khttpd](); 12) 0.345 us | http_message_needs_eof [khttpd](); 12) 0.327 us | http_should_keep_alive [khttpd](); 12) | http_parser_callback_message_complete [khttpd]() { 12) 0.268 us | http_should_keep_alive [khttpd](); 12) + 29.013 us | http_server_send.isra.0 [khttpd](); 12) + 38.932 us | } /* http_parser_callback_message_complete [khttpd] */ 12) + 50.521 us | } /* http_parser_execute [khttpd] */ 12) 0.286 us | http_should_keep_alive [khttpd](); 12) + 41.372 us | http_server_recv.constprop.0 [khttpd](); 12) ! 132.066 us | } /* http_server_worker [khttpd] */ ``` 其中發現 `http_server_send`, `http_server_recv` 等花費較多時間。 ## 引入 CMWQ 至 khttpd commit [ad0e8ad](https://github.com/Andrushika/khttpd/commit/ad0e8adf6c71684140795b26a2b1c9f63bd8ae93)