--- tags: NCKU Linux Kernel Internals --- # quiz2 [2020q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2020-quiz2) 完整的程式內容請參考: [Github](https://github.com/RinHizakura/quiz2) ## Trace code: xs.c ### `struct xs` ```c= typedef union { char data[16]; struct { uint8_t filler[15], /* how many free bytes in this stack allocated string * same idea as fbstring */ space_left : 4, /* if it is on heap, set to 1 */ is_ptr : 1, flag1 : 1, flag2 : 1, flag3 : 1; }; /* heap allocated */ struct { char *ptr; /* supports strings up to 2^54 - 1 bytes */ size_t size : 54, /* capacity is always a power of 2 (unsigned)-1 */ capacity : 6; /* the last 4 bits are important flags */ }; } xs; ``` * `char data[16]` 用來存放短字串。 * 第一個 struct 用來記錄此資料結構是放置長字串或短字串。其中 `filter` 是用來填滿前面的15 bytes,`space left` 記錄**最大短字串長度減去現在長度**,`is_ptr` 則記錄此結構是放長字串或短字串。 * 第二個struct `*ptr` 用來放長字串的 pointer,`size` 記錄該字串的長度。而 `capacity` 則是長度取log2 + 1 後的結果,紀錄malloc多少空間(之後如果要把字串加長,檢查 capacity 就可以知道需不需要多要)。保留最後 4-bit 讓 is_ptr, flag 的值不會被動到。 ### `xs_tmp` `main`中的 ```c= xs string = *xs_tmp("\n foobarbar \n\n\n"); ``` 展開會變成 ```c= xs string = *((void) ((struct { _Static_assert(sizeof("\n foobarbar \n\n\n") <= 16, "it is too big"); int dummy; }){1}), xs_new(&(xs) { .space_left = 15 }, "" "\n foobarbar \n\n\n")); ``` ```c= #define xs_tmp(x) \ ((void) ((struct { \ _Static_assert(sizeof(x) <= 16, "it is too big"); \ int dummy; \ }){1}), \ xs_new(&xs_literal_empty(), "" x)) ``` xs_tmp首先檢查x長度是否<=16,然後使用xs_new初始化字串 :::danger :question: 有點搞不清這語法QQ 先擱置著回頭再研究 ::: ### `xs_new` * `xs_literal_empty()` 先把 x 當成空的短字串初始化(把 `space_left` 設為 15,其他設為 0),接著去判斷x的長度。(因此題目中的`AAA = 16`) :::warning :question: xs_literal_empty 在 xs_tmp 時已經呼叫過,這樣是不是多用了一次? ::: * 如果 >16 為長字串,則調整紀錄長字串的 member,並透過指標紀錄,malloc 把字串放在 heap。 * 否則為短字串,放在 `data`,長度則透過 `space_left` 紀錄。 * 短字串理論上應該也要設定`is_ptr`,但因為一開始已經初始成0,除非短字串長度是16否則不會更改到。而如果剛好字串長度為16,`\0`會使 space_left 跟 is_ptr 也會正好是0。 ### `xs_trim` * `xs_trim` 是用來刪除字串前後含的`trimset`。 * `set_bit` 根據trimset中的char對應的ASCII設定mask。ASCII的表達範圍是0~255,因此mask需要256個bits來紀錄,所以`BBB`應該是 `256 / 8 = 32`。 * `check_bit` 是與 mask 對應看x中的是否有要修剪的char,因此 `CCC = &` ### `xs_concat` * `xs_concat` 把 `prefix` 和 `suffix` 串在 x 的前後。 * 檢查 capacity,看看如果直接把 prefix 和 suffix 加進去是否足夠容納,如果足夠,直接把memory copy進去即可。 * 如果不夠,則初始一個新的 xs 物件,並當成短字串初始化,再將其透過 `xs_grow` 使新的 xs 可以容納 prefix + suffix + 原字串的長度 * memmove 可以處理 source 跟 destination 的重疊 :::warning :question: concat 的時候如果 capacity 不夠,是直接產生新 xs,把舊的 free 掉。但字串中其實有重複的內容,是否有更好的作法? ::: ### `xs_grow` * 如果x的capacity不夠,則透過 `xs_grow` 增加可容納的字元數量。 * 如果已經是長字串,透過 `realloc` 增加空間 * 如果是短->長,則透過 malloc 改成把字串放在 heap * 觀察 `capacity` 的更新就知道,字串可容納的大小是 2^N 次方成長的。 ## Copy on Write 參考 [folly::fbstring](https://github.com/facebook/folly/blob/master/folly/FBString.h) 為了實現 Copy on write 機制,因此我們需要讓 ptr 使用的空間容納 reference count + 字串本身,需要對原本的函式做一些調整。 ### `xs_new` ```c= #define OFFSET sizeof(size_t) ``` * `OFFSET` 是擺放 reference count 需要的空間,也是從 `ptr` 推回 reference count 的 offset ```c= x->ptr = malloc((size_t)1 << x->capacity + OFFSET) + OFFSET; ``` ``` XS_INIT_REFCNT(x); ``` * 因此我們在要空間時,要多要一個offset的大小,並且把 ptr 指到 offset 擺放字串,以及初始化reference count。 ### `xs_grow` 跟 xs_new 類似,在malloc / realloc 時需要調整空間大小。 ### `xs_cpy` * `xs_cpy` 把 src 複製到 dest,首先要先透過 XS_GET_REFCNT(dest)確認 dest 原先是否reference 其他。如果是,XS_DECR_REFCNT(dest)將reference count減一。 * 接著判斷 src 是長字串或者短字串 * 如果是短字串,調整 dest 的資料結構,然後直接複製 src 的字串過去即可。 * 如果是長字串,增加 src 的 reference count,並且使 dest 和 src 指到相同位置。 ### `xs_cow` ```c= void xs_cow(xs *x) { size_t ref = XS_GET_REFCNT(x); if (ref > 1) { XS_DECR_REFCNT(x); xs_new(x, xs_data(x)); } } ``` * 檢查x是否為copy on write,如果是,將reference count減一,並且複製另一份出來。 :::warning if 敘述不能寫成 `if (XS_GET_REFCNT(x) > 1)` ! 仔細思考 macro 展開就會明白原因 ::: ### `xs_concat` and `xs_trim` * 在處理 string 之前,先透過 `xs_cow()` 檢查。 ### `xs_free` ```c= static inline xs *xs_free(xs *x) { int ref = XS_GET_REFCNT(x); if (ref > 1){ XS_DECR_REFCNT(x); } else if (xs_is_ptr(x)) { free(xs_data(x) - OFFSET); } return xs_newempty(x); } ``` * 先判斷是否是 reference,如果還有其他人 reference,僅清空 x 並將 reference count 減一,但不釋放。 * 如果是長字串且為唯一一份,則調整地址回開頭,並且釋放。 ## 實作 xs_tok 參考[glibc](https://code.woboq.org/userspace/glibc/string/strtok_r.c.html#__strtok_r)的 source code來實作,使 tok 的行為一致。 * 在操作 x 前,先檢查 x 是否是 reference string ,如果先透過 `xs_cow()` 並做相應的處理。因為 `xs_tok` 會調整到 x 的內容。 * 利用和 `xs_trim` 相同的概念,透過建立的 mask 後找到第一個 delimiter,並把delimiter 設成 '\0'。 * 因為會把 delimiter 設成 '\0',輸入的 x 長度需要被調整。 * 和strtok相同的概念,把 `save_ptr` 指向要處理的字串。 :::warning 發現老師在其他同學的筆記中有提到多執行緒的 tok 議題,需要再思考自己的實作在多執行緒下的正確性,並且加以修改。 ::: ## COW 的效益 為了可以測試使用 COW 與否的差別,使用 `NOCOW` 的 flag 來編譯程式。並透過使用 [perf](https://perf.wiki.kernel.org/index.php/Main_Page) 來觀察對於 cache 的行為差異。測試的函式請參考 Github 中的 `test_perf()` > perf stat -e cache-misses,cache-references ./xs 使用 COW 的版本: ``` Mode COW, using time 0.002744 Performance counter stats for './xs': 146,001 cache-misses # 45.669 % of all cache refs 319,692 cache-references 0.006344529 seconds time elapsed 0.006343000 seconds user 0.000000000 seconds sys ``` 無 COW 的版本: ``` Mode NO COW, using time 0.025074 Performance counter stats for './xs': 1,153,296 cache-misses # 84.331 % of all cache refs 1,367,575 cache-references 0.029098245 seconds time elapsed 0.020737000 seconds user 0.008294000 seconds sys ``` 可以看到,COW的使用大幅的減少 cache miss,因為沒有額外的 copy,花費的時間也相較之下也更少。 ## Linux 核心中的 SSO / CoW 待補