# 2022q1 Homework5 (quiz6) contributed by < `Kevin-Shih` > > [測驗題目 quiz6](https://hackmd.io/@sysprog/linux2022-quiz6) > [final project github](https://github.com/Kevin-Shih/Linux2022q1-final-project) ## 問題描述 〈[UNIX 作業系統 fork/exec 系統呼叫的前世今生](https://hackmd.io/@sysprog/unix-fork-exec)〉提到 [clone](https://man7.org/linux/man-pages/man2/clone.2.html) 系統呼叫,搭配閱讀〈[Implementing a Thread Library on Linux](https://www.evanjones.ca/software/threading.html)〉,嘗試撰寫一套使用者層級的執行緒函式庫,程式碼可見: [readers.c](https://gist.github.com/jserv/f1c55b97ae6cb0ae8f4945203c2b12c9) ### 運作原理與解題 :::danger 逐行解釋程式碼運作前,應當描述 Linux NPTL 和相關系統呼叫的原理。 :notes: jserv ::: **儲存執行緒資料的節點及 TCB** ```c typedef struct __node { unsigned long int tid, tid_copy; void *ret_val; struct __node *next; funcargs_t *fa; } node_t; typedef struct { node_t *head, *tail; } list_t; ``` `node_t` 用來存該執行緒的 tid,回傳值 (`ret_val` ) 及所執行的函式 (`fa`),`fa` 包含函式的輸入參數。 TCB 則以單向的鏈結串列建構。 **鎖定指定的 spinlock object** ```c static inline int spin_acquire(spin_t *l) { int out; volatile int *lck = &(l->lock); asm("whileloop:" "xchg %%al, (%1);" // exchange src and dest val /* bitwise AND between src and dest. * If they're both zero, the zero flag is set */ "test %%al,%%al;" "jne whileloop;" // exit when zero flag is set : "=r"(out) // output : "r"(lck)); // input return 0; } ``` - `%%al` : 相當於 `rax` 的 lowest 8 bits - `%1` : input, 相當於 `rax` (見底下組合語言指令列表) 對應的組合語言: (AT&T 語法) ```c movq -8(%rbp), %rax //input, move 64bits from `lck` to rax whileloop: xchg %al, (%rax); //rax is addr to l->lock so swap l-lock val and al val test %al,%al; jne whileloop; movl %eax, -12(%rbp) //output, move 32bits from eax to `out` ``` 最後的結果 `xchg` 相當於 `l->lock = &(l->lock) << (64-8) >> (64-8)` 因為 `%al` 是 input lck 的低位 8 bits,如果 `l->lock` 是 0 (未被 lock 的狀態)換完一次就會跳出迴圈。 而 spin_release 則對應釋放 lock,將 `l->lock`, `l->locker` 設為 0。 `spin lock` 用來保護後續 thread create, join, exit 等 critical section,確保對 node 中的資料操作時不會被中斷。 `mutex_t` 類別的 init, acquire, release 大致與 `spin_t` 相同,但在 acquire, release 多了一道 `SYS_futex` 系統呼叫。 ```c // mutex_acquire function readers.c L109 syscall(SYS_futex, m, FUTEX_WAIT, 1, NULL, NULL, 0); // mutex_release function readers.c L125 syscall(SYS_futex, m, FUTEX_WAKE, 1, NULL, NULL, 0); ``` 根據 man page `FUTEX_WAIT` 對應的行為會檢驗 uaddr (即 `m`) 指向的 futex word 值是否與 val (即 `1`) 相同則進入 sleep 並等待相同 futex word 上的 `FUTEX_WAKE` 指令,當值不同則 system call fails immediately 並產生錯誤訊號 `EAGAIN` 。 `FUTEX_WAKE` 會喚醒 uaddr (即 `m`) 所指向的 futex word,val (即 `1`) 可以指定喚醒的 waiter 數量 (`1` 或 `INT_MAX`),需要注意的是喚醒單個 waiter 時並不保證喚醒的順序。 更詳細的 `FUTEX_WAIT`, `FUTEX_WAKE` 行為可以查閱下方引文中的 man page 連結。 > `SYS_futex`: > ```c > long syscall(SYS_futex, uint32_t *uaddr, int futex_op, uint32_t val, > const struct timespec *timeout, /* or: uint32_t val2 */ > uint32_t *uaddr2, uint32_t val3); > ``` > futex - fast user-space locking. The futex() system call provides a method for waiting until a > certain condition becomes true. > --man [futex](https://man7.org/linux/man-pages/man2/futex.2.html) **clone one-to-one 所用的 flags** ```c #define CLONE_FLAGS \ (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | \ CLONE_SYSVSEM | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID) ``` - 當 `CLONE_VM` 被設定時,呼叫端行程及子行程會共用記憶體空間,使得它們的記憶體存取對二者均為可見。 > If the CLONE_VM flag is specified and the CLONE_VFORK flag is not specified, then any alternate signal stack that was established by sigaltstack(2) is cleared in the child process. - 當 `CLONE_FS` 被設定時,呼叫端行程及子行程會共用檔案系統。 > This includes the root of the filesystem, the current working directory, and the umask. - 當 `CLONE_FILES` 被設定時,呼叫端行程及子行程會共用 fd 表格,當其中一者關閉 fd 時另一者也會受影響。 - 當 `CLONE_SIGHAND` 被設定時,呼叫端行程及子行程會共用 signal handler,兩者會共用對某個 signal 的 action,但 signal mask 及 signal 本身仍是獨立的。 - 當 `CLONE_THREAD` 被設定時,呼叫端行程及子行程放在同一個 thread group (所以有相同的 TGID),但也擁有獨立的 TID,以供區別。 - 當 `CLONE_SYSVSEM` 被設定時,呼叫端行程及子行程會共用 semadj list。 > A semaphore adjustment (semadj) value is a per-process, per-semaphore integer that is the negated sum of all operations performed on a semaphore specifying the SEM_UNDO flag. When a process specified the SEM_UNDO flag terminates, each of its per-semaphore semadj values is added to the corresponding semaphore, thus undoing the effect of that process's operations on the semaphore. - 當 `CLONE_PARENT_SETTID` 被設定時,child TID 會存入由 parent_tid 參數指向的位置 > Stored in the parent's memory. The store operation completes before the clone call returns control to user space. - 當 `CLONE_CHILD_CLEARTID` 被設定時,當子行程一旦執行 exit,則清除由 child_tid 參數指向的 child TID (in child memory),並接著喚醒該 address 對應的 futex。 更詳細的說明可見 [clone(2)](https://man7.org/linux/man-pages/man2/clone.2.html) **建立新的 thread** ```c /** * @brief Create a One One mapped thread * @param t Reference to the thread * @param routine Function associated with the thread * @param arg Arguments to the routine */ int thread_create(thread_t *t, void *routine, void *arg) { // lock it till thread created or failed to create spin_acquire(&global_lock); ... // insert the node to the TCB node_t *node = list_insert(&tid_list, 0); if (!node) {...} // arg for wrap funcargs_t *fa = malloc(sizeof(funcargs_t)); if(!fa) {...} fa->f = routine; fa->arg = arg; // stack for child void *thread_stack = alloc_stack(STACK_SZ, GUARD_SZ); if (!thread_stack) { perror("thread create"); spin_release(&global_lock); free(fa); return errno; } fa->stack = thread_stack; thread_t tid = clone(wrap, (char *) thread_stack + STACK_SZ + GUARD_SZ, CLONE_FLAGS, fa, &(node->tid), NULL, &(node->tid)); node->tid_copy = tid; node->fa = fa; // clone failed if (tid == -1) {...} *t = tid; // set *t to child tid spin_release(&global_lock); return 0; } ``` 該函式會將對應的 kernel level thread 的 tid 存入 thread_t,而該 kernel thread 負責執行傳入的 routine founction,routine 及其 arg 會先包入 `funcargs_t` 類別(即變數 fa) 後作為參數交由 clone 時的 wrap founction 執行。 這邊的 wrap 函式協助包裝 routine 並為該 thread 設定所需的 signal handler 並在 routine return 後結束該 thread (呼叫 `thread_exit`) 等功能。 `thread_stack + STACK_SZ + GUARD_SZ` 是指向分配給 child stack 的最上方的記憶體位置。 因為設定 `CLONE_VM` 使兩者共用 memory space 故 parent_tid 與 child_tid 均指向 `&(node->tid)` ,當 child 建立時會將其 tid 存入,結束時則會清除 (歸零),並喚醒對應的 futex。 (因為 `CLONE_PARENT_SETTID` 及 `CLONE_CHILD_CLEARTID` 的 flags)。 **thread join** ```c /** * @brief Function to wait for a specific thread to terminate * @param t TID of the thread to wait for */ int thread_join(thread_t t, void **retval) { spin_acquire(&global_lock); void *addr = get_tid_addr(&tid_list, t); if (!addr) { spin_release(&global_lock); return ESRCH; } if (*((pid_t *) addr) == 0) { spin_release(&global_lock); return EINVAL; } int ret = 0; while (*((pid_t *) addr) == t) { spin_release(&global_lock); ret = syscall(SYS_futex, addr, FUTEX_WAIT, t, NULL, NULL, 0); spin_acquire(&global_lock); } syscall(SYS_futex, addr, FUTEX_WAKE, INT_MAX, NULL, NULL, 0); if (retval) *retval = get_node_from_tid(&tid_list, t)->ret_val; spin_release(&global_lock); return ret; } ``` `while` 迴圈內的 FUTEX_WAIT 會在 addr 與 t 相等時進入睡眠,即該等待被 join 的 thread 仍在執行時 `thread_join` 就會睡眠直到該 thread 結束,`CLONE_CHILD_CLEARTID` flag 觸發清空 addr 及 FUTEX_WAKE,`thread_join` 才會繼續接收 return value 並結束。 **thread exit** ```c void thread_exit(void *ret) { spin_acquire(&global_lock); void *addr = get_tid_addr(&tid_list, gettid()); if (!addr) {...} if (ret) { node_t *node = get_node_from_tid(&tid_list, gettid()); node->ret_val = ret; } /** * after child thread killed, `CLONE_CHILD_CLEARTID` flag will * call another FUTEX_WAKE, won't it? */ syscall(SYS_futex, addr, FUTEX_WAKE, INT_MAX, NULL, NULL, 0); spin_release(&global_lock); kill(SIGINT, gettid()); } ``` 在 child thread 執行 wrap function 到最後時會呼叫 `thread_exit`,將回傳值放到 `thread_join` 可以存取的地方,並終止該 thread。 --- # final project: 改進 quiz6(A) ## 作業環境 **硬體** - CPU: Intel® Core™ i5-7300HQ CPU @ 2.50GHz × 4 :::spoiler lscpu ```shell $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 1 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 158 Model name: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz Stepping: 9 CPU MHz: 2500.000 CPU max MHz: 3500.0000 CPU min MHz: 800.0000 BogoMIPS: 4999.90 Virtualization: VT-x L1d cache: 128 KiB L1i cache: 128 KiB L2 cache: 1 MiB L3 cache: 6 MiB NUMA node0 CPU(s): 0-3 ``` ::: - RAM: 4GiB DDR4 @ 2133MHz x 2 (Dual channel) :::spoiler dmidecode -t memory ```shell $ sudo dmidecode -t memory # dmidecode 3.2 Handle 0x003F, DMI type 17, 40 bytes Memory Device Array Handle: 0x003E Error Information Handle: Not Provided Total Width: 64 bits Data Width: 64 bits Size: 4096 MB Form Factor: SODIMM Set: None Locator: ChannelA-DIMM0 Bank Locator: BANK 0 Type: DDR4 Type Detail: Synchronous Speed: 2133 MT/s Manufacturer: 859B Part Number: CT4G4SFS8213.C8FBR2 Rank: 1 Configured Memory Speed: 2133 MT/s Handle 0x0040, DMI type 17, 40 bytes Memory Device Array Handle: 0x003E Error Information Handle: Not Provided Total Width: 64 bits Data Width: 64 bits Size: 4096 MB Form Factor: SODIMM Set: None Locator: ChannelB-DIMM0 Bank Locator: BANK 2 Type: DDR4 Type Detail: Synchronous Speed: 2133 MT/s Manufacturer: SK Hynix Part Number: HMA451S6AFR8N-TF Rank: 1 Configured Memory Speed: 2133 MT/s ``` ::: **作業系統** - OS: Ubuntu 20.04.4 LTS (Focal Fossa) - Linux kernel: 5.13.0-44-generic ```shell $ uname -a Linux Shih-MSI 5.13.0-44-generic #49~20.04.1-Ubuntu SMP Wed May 18 18:44:28 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux ``` **編譯器** - gcc 9.4 - g++ 9.4 (目前因重現實驗時遭遇的問題改為 g++ 7.5) - GO 1.13.8 - nodejs v10.19.0 --- ## 重現 [uThreads](https://github.com/samanbarghi/uThreads) 的實驗 (throughput) 首先使用 [wrk](https://github.com/wg/wrk) 嘗試,過程中使用的命令依照 [httpbenchmarks](https://github.com/samanbarghi/httpbenchmarks)。 然而對於 uThreads webserver 卻產生了異常的結果,wrk 回報的 Requests/sec 為 0。 ```shell // first terminal $ sudo taskset -c 2,3 ./bin/webserver 2 // second terminal $ sudo taskset -c 0,1 ./wrk -d 15s -t 2 -c 200 https://127.0.0.1:8800 Running 15s test @ https://127.0.0.1:8800 2 threads and 200 connections Thread Stats Avg Stdev Max +/- Stdev Latency 0.00us 0.00us 0.00us -nan% Req/Sec 0.00 0.00 0.00 -nan% 0 requests in 15.03s, 0.00B read Requests/sec: 0.00 Transfer/sec: 0.00B ``` 與老師聯繫後嘗試改用 khttpd 中附的 [htstress](https://github.com/arut/htstress) 嘗試,仍然無法正常得到結果。 ```shell // first terminal $ sudo taskset -c 2,3 ./bin/webserver 2 // second terminal $ ./htstress -n 1000 -c 100 -t 1 http://localhost:8800 // over 1 minute no responce, so I press `Ctrl + C` ^Cepoll_wait: Interrupted system call ``` 而在試著使用 htstress 中的 debug 時,我注意到儘管 `uThreads webserver` 仍在運行,但從未得到 server 的回應。 ```shell ./htstress -d http://localhost:8800 [Press Ctrl-C to finish] GET / HTTP/1.0 Host: localhost ``` 除了嘗試對 `uThreads webserver` benchmark 外,也嘗試對`fasthttp`, `cppsp` 測試,意外發現 `cppsp` 也無法正常得到測試結果,僅有 `fasthttp` 可以在 htstress 上正確的測試。 (但當用 `./htstress -d` 對 `cppsp` 測試時是有回應 `Hello Wrold!` 的,但只有一次) :::danger htstress 無法正常測試 `uThreads webserver`, `cppsp`,僅能以 `Ctrl + C` 終止。 (目前改變 g++ 版本至 7.5 後測試正常) ::: **嘗試 gdb 以了解發生問題的環節在哪** first try: 第 226 行後,先 `./htstress -d` 再執行 227 行,可以 accept (`*cconn` 有被指派 `pd`, `fd`),但未收到回應()。 ``` (gdb) r 2 Starting program: /home/user/Senior/linux2022/project/uThreads/bin/webserver 2 ... Thread 1 "webserver" hit Breakpoint 1, main (argc=<optimized out>, argv=<optimized out>) at test/webserver.cpp:226 226 Connection* cconn = sconn->accept((struct sockaddr*)nullptr, nullptr); (gdb) display *sconn 1: *sconn = {pd = 0x5555555744b0, fd = 4} (gdb) display *cconn 2: *cconn = <error: value has been optimized out> // 這邊先執行 `./htstress -d http://localhost:8800` 可以 accept 到,但最後仍沒有收到回應 (gdb) n 227 uThread::create()->start(defaultCluster, (void*)handle_connection, (void*)cconn); 1: *sconn = {pd = 0x5555555744b0, fd = 4} 2: *cconn = {pd = 0x5555555744e0, fd = 5} (gdb) n Thread 1 "webserver" hit Breakpoint 1, main (argc=<optimized out>, argv=<optimized out>) at test/webserver.cpp:226 226 Connection* cconn = sconn->accept((struct sockaddr*)nullptr, nullptr); 1: *sconn = {pd = 0x5555555744b0, fd = 4} 2: *cconn = <error: value has been optimized out> // 已經進到下一次的迴圈了,但 htstress 仍沒有收到回應 ``` second try: 第 226 行後,先執行 227 行再嘗試 `./htstress -d`,無法正常 accept,也未收到回應。 ``` (gdb) r 2 Starting program: /home/user/Senior/linux2022/project/uThreads/bin/webserver 2 ... Thread 1 "webserver" hit Breakpoint 1, main (argc=<optimized out>, argv=<optimized out>) at test/webserver.cpp:226 226 Connection* cconn = sconn->accept((struct sockaddr*)nullptr, nullptr); (gdb) display *sconn 1: *sconn = {pd = 0x5555555744b0, fd = 4} (gdb) display *cconn 2: *cconn = <error: value has been optimized out> // 若這邊先不執行 `./htstress -d http://localhost:8800` 則會卡在 `n` (gdb) n // 此時執行 `./htstress -d http://localhost:8800` 但不會正確進到下一步 ^C Thread 1 "webserver" received signal SIGINT, Interrupt. 0x00007ffff7fa73d0 in kThread::postSwitchFunc (nextuThread=0x555555573e80, args=0x5555555744b0) at src/runtime/kThread.cpp:198 198 nextuThread->state = uThread::State::RUNNING; 1: *sconn = {pd = 0x5555555744b0, fd = 4} (gdb) n 197 ck->currentUT = nextuThread; 1: *sconn = {pd = 0x5555555744b0, fd = 4} ``` 也改用在其他同學共筆中見過的 ab 進行測試,測試結果同樣異常。 ```shell $ ab -n 100 -c 10 -k http://localhost:8800/ ... Benchmarking localhost (be patient)...apr_pollset_poll: The timeout specified has expired (70007) ``` 同樣是等到 timeout 仍無回應。 ### 改變 g++ 版本 經過在 ubuntu 18.04 以 g++ 7.5.0 編譯成功過得經驗,懷疑可能是 compiler 版本導致不能正常運行,嘗試降至 g++ 7.5.0 編譯。 ```shell $ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-7 100 $ g++ --version g++ (Ubuntu 7.5.0-6ubuntu2) 7.5.0 ``` 編譯完後的執行結果: ```shell $ wrk -d 15s -t 1 -c 200 http://localhost:8800 Running 15s test @ http://localhost:8800 1 threads and 200 connections Thread Stats Avg Stdev Max +/- Stdev Latency 849.23us 418.16us 9.88ms 90.49% Req/Sec 129.34k 8.38k 136.25k 88.67% 1929955 requests in 15.00s, 242.95MB read Requests/sec: 128626.03 Transfer/sec: 16.19MB ``` 看來確實是 g++ 版本導致的問題,但實際的原因仍待查證。 ### 抑制編譯器最佳化 修改 Makefile 抑制編譯器的最佳化,測試再此情況下以 g++ 9.4.0 編譯是否可以正確執行。 原先的 Makefile 有三處帶有 `-O` 選項,分別對應 uThreads、測試用 sample code 及 linker。 其中 linker 維持 `-O1` 不變,但將其他兩個改為 `-O0` 以抑制編譯器最佳化。 ```diff // uThreads 主體 @@ -18 +18 @@ -CXXFLAGS := -O3 -g -m64 -fpermissive -mtls-direct-seg-refs -fno-extern-tls-init -pthread -DNDEBUG -DNPOLLNONBLOCKING +CXXFLAGS := -O0 -g -m64 -fpermissive -mtls-direct-seg-refs -fno-extern-tls-init -pthread -DNDEBUG -DNPOLLNONBLOCKING // test (sample code) @@ -67 +67 @@ - $(CXX) -O3 -g -o $@ $(HTTP) $< -luThreads + $(CXX) -O0 -g -o $@ $(HTTP) $< -luThreads ``` 以這種設定編譯 uThreads webserver 是可以正常測試的,方法 `2` 的測試結果會與改用 g++ 7.5 的實驗結果並呈。 另外有嘗試將 uThreads、sample code 提至 `-O1`,若兩者均採 `-O1` 或 uThreads 採 `-O1`、sample code 採 `-O0`,則與修改前相同,均無法正常收到回應。 若 uThreads 採 `-O0`、sample code 採 `-O1` 則當 htstress 嘗試連入後便會產生以下錯誤: ```shell $ taskset -c 0-1 ./bin/webserver 2 Error reading from socket: No such file or directory fd 962199568 Segmentation fault (core dumped) ``` ### 盡可能排除干擾因素 根據 man [cpupower](http://manpages.ubuntu.com/manpages/focal/en/man1/cpupower-set.1.html),將其設為最大程度偏好效能。 > The range of valid numbers is 0-15, where 0 is maximum performance and 15 is maximum energy efficiency. > This policy hint does not supersede Processor Performance states (P-states) or CPU Idle power states (C-states), but allows software to have influence where it would otherwise be unable to express a preference. ```shell $ sudo cpupower -c 0-3 set -b 0 $ sudo cpupower -c all info -b analyzing CPU 0: perf-bias: 0 analyzing CPU 1: perf-bias: 0 analyzing CPU 2: perf-bias: 0 analyzing CPU 3: perf-bias: 0 ``` 接著由於該設定不會取代 P-states, C-states,故接下來試著調整 C-states 最大可能避免 CPU idle。 透過 cpupower idle-set 命令 disable 所有 idle states: ```shell $ sudo cpupower idle-set -D 0 Idlestate 0 disabled on CPU 0 Idlestate 1 disabled on CPU 0 Idlestate 2 disabled on CPU 0 Idlestate 3 disabled on CPU 0 Idlestate 4 disabled on CPU 0 Idlestate 5 disabled on CPU 0 Idlestate 6 disabled on CPU 0 Idlestate 7 disabled on CPU 0 Idlestate 8 disabled on CPU 0 Idlestate 0 disabled on CPU 1 ... ``` P-states 的部份則將 governor 設為 performance,來讓表現更穩定,此外也進一步修改 fibdrv 作業中為消除干擾因素所寫的 shell script,將先前調整 perf-bias 及 C-states 整合進來,讓設定變方便。 ```shell $ sudo bash perf_test_mode.sh cat /proc/sys/kernel/randomize_va_space 0 cat /sys/devices/system/cpu/intel_pstate/no_turbo 1 cat /sys/devices/system/cpu/cpu0/power/energy_perf_bias 0 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor performance sudo cpupower idle-info | grep "Available\|DISABLED" Available idle states: POLL C1 C1E C3 C6 C7s C8 C9 C10 POLL (DISABLED) : C1 (DISABLED) : C1E (DISABLED) : C3 (DISABLED) : C6 (DISABLED) : C7s (DISABLED) : C8 (DISABLED) : C9 (DISABLED) : C10 (DISABLED) : cat /sys/devices/system/cpu/isolated 0-1 ``` ### 實驗方法 `1` 及其結果 :::warning **注意:** 目前 wrk 的測試結果仍存在問題,詳見本段末尾,在此列出重現問題的步驟。 下段有改用方法 `2` 取得的正確(合理)實驗結果,後續分析均採用方法 `2`。 ::: - webserver 使用 taskset 將其綁定到已經隔離出來的 cpu 0,1。 實際執行的命令如下: ```shell // uThreads $ taskset -c 0-1 ./bin/webserver $T // nodejs (only for single thread) $ taskset -c 0-1 node server.js // fasthttp $ GOMAXPROCS=$T taskset -c 0-1 ./server // cppsp $ taskset -c 0-1 ./run_example -t $T ``` `$T`: 執行緒數量 根據 uThreads 原作者所述,uThreads webserver 總是使用 `$T - 1` 個執行緒。 > Note, that the number of threads here always mean number of worker threads + 1, which means if you pass 4, the number of worker threads will be 3 and there will be 1 poller thread. --[httpbenchmarks](https://github.com/samanbarghi/httpbenchmarks) - 測試工具: [wrk](https://github.com/wg/wrk) 為了方便彙整 wrk 測得的資訊及方便後續製圖,自行撰寫了基於 python 的測試程式來包裝 wrk 命令。 gist: [benchmark.py](https://gist.github.com/Kevin-Shih/7344e6227a9914ad813e96fa4d22f63c) 會在測試執行時以 htop 確保 wrk 所使用的核不會比 webserver 使用的核先達到滿負載,以確保測得 webserver 的完整效能。 - 測試 uThreads webserver 時遇到錯誤訊息 ```shell $ taskset -c 0-1 ./bin/webserver 2 Error: system:24 - Too many open files $ errno 24 EMFILE 24 Too many open files ``` `EMFILE` 根據 [errno(3)](https://man7.org/linux/man-pages/man3/errno.3.html) 及 [getrlimit(2)](https://man7.org/linux/man-pages/man2/getrlimit.2.html) 所述,是由於嘗試開啟超過 `RLIMIT_NOFILE` 所限制的最大 fd 數量所導致。 透過 `ulimit` 將該終端機的 Soft limit 由 1024 提升至 8192 (超過 connection 數) ```shell $ ulimit -Sn 8192 ``` - 測試結果 **1 thread**![](https://i.imgur.com/Zr6l5tm.png) **2 threads (有問題的)**![](https://i.imgur.com/DfqVy1l.png) :::warning 可以注意到使用 wrk 的測試,2 threads (以及沒放上來的 3 threads) 在並行程度低的時候,特別是在設為 `10` 時有特別顯著的效能降低,顯示使用 wrk 的測試方法在當前的環境下,其測試結果很可能有問題。 在下面會改用 `htsress` 測試。 ::: ### 實驗方法 `2` 及其結果 除了改變測試工具及 session soft limit 外其餘部份與方法 `1` 維持相同。 - 測試工具: [htstress](https://github.com/arut/htstress) 為了方便彙整 htstress 測得的資訊及方便後續製圖,同樣撰寫了基於 python 的測試程式來包裝 htstress 命令。 gist: [benchmark_htstress.py](https://gist.github.com/Kevin-Shih/0f8cd0867001f44bace21335c569afc2) 同樣會遇到 `EMFILE`,透過 `ulimit` 將該終端機的 Soft limit 由 1024(default) 直接提升至 999999 ```shell $ ulimit -Sn 999999 ``` 另外,由於測試環境限制達不到真正的 8k 並行度,致使測試結果失準,因此圖表中不會顯示 8k 的結果。 - 測試結果 **1 thread**![](https://i.imgur.com/CDCwF5Y.png) **2 threads**![](https://i.imgur.com/Pl5ehCA.png) **3 threads**![](https://i.imgur.com/YuCRx7o.png) :::info 後續測試將改為使用 [jserv/uThreads](https://github.com/jserv/uThreads)。 (此版本指定使用 g++ 7 編譯、不須安裝只須 `make all && make test`) 後續文中若無特別說明 `uThreads` 即代表此版本。 ::: 下方將會列出使用此版本以方法 `2` 測試的結果,作為後續進一步修改的參考基準。 **1 thread**![](https://i.imgur.com/EXSmeMa.png) **2 threads**![](https://i.imgur.com/n8i7Ti9.png) **3 threads**![](https://i.imgur.com/rc4gmJG.png) ## 找出並驗證 uThreads 勝過 cppsp 的原因 ### M:N mapping 在 uThreads 中提及對 M:N mapping 的支援,是透過將 N 個 kThreads 以 `Cluster` 的方式聚合在一起來服務 M 個 uThreads,在目前實驗的情境中就是每個連入的客戶端 (M connections),並具有非搶佔式的排程器。 根據以下程式碼可以確認 uThreads 中的 websever 確實是為每個連接的客戶端建立一個新的 user thread。 ```cpp sconn->listen(65535); while(true){ Connection* cconn = sconn->accept((struct sockaddr*)nullptr, nullptr); uThread::create()->start(defaultCluster, (void*)handle_connection, (void*)cconn); } sconn->close(); ``` 在本節希望透過限制 M:N mapping 的能力,回到 1:1 mapping 以檢驗 cluster 改進網頁伺服器效能的有效性。 故在試驗時將 server 設定為 1 個 kthread 並且同時只有一個 connection 以確保只有一個 uthread 來達到 1:1 mapping。 ![](https://i.imgur.com/56TknpN.png) :::warning TODO - Facebook's folly::fiber, folly - 若 uthreads 中的改進有效 試著移植到 quiz6 - 最前段補上 NPTL (Native POSIX Thread Library) 的介紹 - uThreads scheduler (#2) - [hackernew](https://news.ycombinator.com/item?id=6726357) - [google](https://www.theregister.com/2020/08/10/google_scheduling_code_reaches_linux/) 回去看 2013 的演講 ::: ## 將 uThreads 中的啟發移植到 quiz6 中 這個部份將會分為 2 個階段進行: - 建立一個 non-blocking socket, single thread 的伺服器 - 將 uThreads 中 M:N mapping 的 cluster, scheduler 等機制整合進來 ### 建立基於 non-blocking socket, single thread 的伺服器 過去以 c 語言寫的伺服器都是簡單的 single thread, blocking socket 的伺服器,在這個部份希望能先建立一個基於 non-blocking socket, single thread 的伺服器並確認其執行的正確性及效能。 完整程式碼請見:[GitHub master/singleT_nonblock](https://github.com/Kevin-Shih/Linux2022q1-final-project/tree/master/singleT_nonblock) Server 基本上是依據 uThreads 中的 webserver.cpp 並修改為 c 的版本 - 測試結果 ![](https://i.imgur.com/0PM6hjl.png) ### 建立 M:N mapping 的伺服器 先嘗試建立一個只有單個 Cluster 的伺服器,此時分配 user thread 的方式是指派給 local run queue 最短的 kernel thread。 完整程式碼請見:[GitHub master/m2n](https://github.com/Kevin-Shih/Linux2022q1-final-project/tree/master/m2n) uthread node: ```c typedef struct uthread_node_t{ thread_t current_kthread; funcargs_t *fa; // handle connections struct uthread_node_t *next; }uthread_node_t; typedef struct uthread_list_t{ unsigned long uthread_count; uthread_node_t *head, *tail; }uthread_list_t; ``` kthread node: ```c typedef struct __node { unsigned long int tid, tid_copy; uthread_list_t runqueue; // kthread local runqueue void *ret_val; struct __node *next; funcargs_t *fa; } node_t; ``` kthread 的 routine 改為持續試著由 local runqueue 取得等待的 uthread,當 local runqueue 長度不為 0 時 pop 出 head 並執行該 uthread 中的 function。 ```c= static void kthread_try_run(void *arg){ spin_acquire(&global_lock); uthread_list_t *runqueue = (uthread_list_t *)arg; spin_release(&global_lock); while(1){ if(__atomic_load_n(&runqueue->uthread_count, __ATOMIC_RELAXED) != 0){ // pop and run spin_acquire(&global_lock); uthread_node_t *ut = runqueue->head; runqueue->head = runqueue->head->next; runqueue->uthread_count--; spin_release(&global_lock); (ut->fa->f)(ut->fa->arg); } } } ``` - 測試結果 **1 thread**![](https://i.imgur.com/ksYDcqn.png) **2 threads**![](https://i.imgur.com/eoJ0mDF.png) **3 threads**![](https://i.imgur.com/3RJ2eIi.png) :::warning TODO 目前的 accept 並沒有加入 epoll_wait 來等待,後續應加入避免持續佔用 cpu 資源 試著在 accept_conn() 加入 epoll 後程式在 kthread_try_run() L10 出現 segmentation fault 但在 gdb 測試,有時會出現如下錯誤,原因正在查證 ``` Thread 1 "server" received signal SIGSEGV, Segmentation fault. tcache_get (tc_idx=<optimized out>) at malloc.c:2939 2939 malloc.c: No such file or directory. ``` 註: 當 request 數量較少(< 10000)時不一定會發生,但當數量增大則會發生(無法完成 benchmark) 把實用的 scheduler 搞定 :::