owned this note
owned this note
Published
Linked with GitHub
# 2021q1 Homework3 (quiz3)
contributed by < [`toastcheng`](https://github.com/toastcheng) >
###### tags: `linux2021`
## 進度
- [x] 1. 程式碼運作原理
- [x] 2. 改進方案
- [x] 3. 字串複製 (CoW) 的函式實作
- [ ] 4. 分析字串處理最佳化的空間效益
- [x] 5. 整合 string interning
- [x] 6. Linux 中的 SSO、CoW 案例
---
## Q1: Small String Optimization
### 1. 程式碼運作原理
```cpp
typedef union {
char data[16];
struct {
uint8_t filler[15],
space_left : 4,
is_ptr : 1,
is_large_string : 1,
flag2 : 1,
flag3 : 1;
};
struct {
char *ptr;
size_t size : 54,
capacity : 6;
};
} xs;
```
透過 `xs` 使用 `union` 的方式可以看出 `xs` 總共佔用 16 bytes (128 bits) 的記憶體空間,因為不同情境可能為 `char data[16]` 或另外兩種 `struct`。
#### `xs_data` 回傳字串的開頭
```cpp
static inline char *xs_data(const xs *x)
{
if (!xs_is_ptr(x))
return (char *) x->data;
if (xs_is_large_string(x))
return (char *) (x->ptr + /* OFF */ 4);
return (char *) x->ptr;
}
```
從程式的分支邏輯對應 `xs` 的三種 layout,可以看出依照不同情境,字串的開頭可能分別在:
1. short string: `x->data` 指向的位置
字串長度小於 16,直接存放在 stack。
2. large string: `x->ptr` 後 4 個 byte 的位置
字串長度大於 `LARGE_STRING_LEN` (256),會利用前 `x->ptr` 指向位置的前 4 個 byte 存放 reference count,因此真正存放資料的位置在 `x->ptr + 4`。與 `xs_allocate_data` 的邏輯相呼應,其依照字串長度做不同記憶體配置。
3. middle string: `x->ptr` 指向的位置
若是大於或等於 16,並且小於 `LARGE_STRING_LEN`,則一樣分配在 heap,並由 `x->ptr` 指向其位址,與 large string 差別在於不另外紀錄 reference count。
有趣的是 `x->data` 及 `x->ptr` 應是在相同的位置,這個部分應該可以改善,可見 [改進方案](https://hackmd.io/twR8uo8KT7G0rIXbiBfDiQ#2-%E6%94%B9%E9%80%B2%E6%96%B9%E6%A1%88)。
#### `xs_size` 回傳字串的長度
```cpp
static inline size_t xs_size(const xs *x)
{
return xs_is_ptr(x) ? x->size : 15 - x->space_left;
}
```
從這個函式可以看出字串的長度如何儲存在 `xs`,如果是 short string,其內容完全儲存在 stack,可以由 `space_left` 知道還有多少空間,因此將 `15 - x->space_left` 就能得到大小;而 medium string、large string 因為是動態配置,所以其大小另外存在 `x->size` 中。
#### `xs_capacity`
```cpp
static inline size_t xs_capacity(const xs *x)
{
return xs_is_ptr(x) ? ((size_t) 1 << x->capacity) - 1 : 15;
}
```
如果為 short string,容量固定為 15,而 medium string 及 large string 則透過 `capacity` 紀錄,而 `capacity` 則是在 `xs_new` 中決定的,會分配一個比字串長度還大的 power of 2 為其值。
#### `xs_*_refcnt`
```cpp
static inline void xs_set_refcnt(const xs *x, int val)
{
*((int *) ((size_t) x->ptr)) = val;
}
static inline void xs_inc_refcnt(const xs *x)
{
if (xs_is_large_string(x))
++(*(int *) ((size_t) x->ptr));
}
static inline int xs_dec_refcnt(const xs *x)
{
if (!xs_is_large_string(x))
return 0;
return --(*(int *) ((size_t) x->ptr));
}
static inline int xs_get_refcnt(const xs *x)
{
if (!xs_is_large_string(x))
return 0;
return *(int *) ((size_t) x->ptr);
}
```
對長字串的 reference count 做操作的函式,可以發現唯獨 `xs_set_refcnt` 並沒有確保字串為長字串,可見[改進方案](https://hackmd.io/twR8uo8KT7G0rIXbiBfDiQ#2-%E6%94%B9%E9%80%B2%E6%96%B9%E6%A1%88)。
#### `ilog2` 取對數
```cpp
static inline int ilog2(uint32_t n) { return /* LLL */ 32 - __builtin_clz(n) - 1; }
```
透過 `__builtin_clz` 的使用,找到最高位非 0 bit 的位置(從右算起),根據對數的定義該位置 - 1 便是 `n` 的對數的整數部分。
#### `xs_allocate_data` 配置記憶體
```cpp
static void xs_allocate_data(xs *x, size_t len, bool reallocate)
{
/* Medium string */
if (len < LARGE_STRING_LEN) {
x->ptr = reallocate ? realloc(x->ptr, (size_t) 1 << x->capacity)
: malloc((size_t) 1 << x->capacity);
return;
}
/* Large string */
x->is_large_string = 1;
/* The extra 4 bytes are used to store the reference count */
x->ptr = reallocate ? realloc(x->ptr, (size_t)(1 << x->capacity) + 4)
: malloc((size_t)(1 << x->capacity) + 4);
xs_set_refcnt(x, 1);
}
```
`xs_allocate_data` 針對 medium、large string 做記憶體配置,在此判斷如果長度大於 `LARGE_STRING_LEN` 就要多配置 4 個 bytes 以供紀錄 reference count。
#### `xs_new`
```cpp
xs *xs_new(xs *x, const void *p)
{
*x = xs_literal_empty();
size_t len = strlen(p) + 1;
if (len > /* NNN */ 16) {
x->capacity = ilog2(len) + 1;
x->size = len - 1;
x->is_ptr = true;
xs_allocate_data(x, x->size, 0);
memcpy(xs_data(x), p, len);
} else {
memcpy(x->data, p, len);
x->space_left = 15 - (len - 1);
}
return x;
}
```
`xs_new` 初始化一個 `xs`,針對是否是 short string 決定是否要利用 `xs_allocate_data` 動態配置記憶體,在 `xs_allocate_data` 中會再針對 medium string 或 large string 做不同的配置。
#### `xs_grow`
```cpp
xs *xs_grow(xs *x, size_t len)
{
char buf[16];
if (len <= xs_capacity(x))
return x;
/* Backup first */
if (!xs_is_ptr(x))
memcpy(buf, x->data, 16);
x->is_ptr = true;
x->capacity = ilog2(len) + 1;
if (xs_is_ptr(x)) {
xs_allocate_data(x, len, 1);
} else {
xs_allocate_data(x, len, 0);
memcpy(xs_data(x), buf, 16);
}
return x;
}
```
增加 `xs` 所配置的記憶體空間至 `len`,並更新 `capacity`,並且處理字串種類的變化,但只涵蓋從 short string 成長為 medium string 的情況,仔細觀察也會發現若是成長為 large string,並沒有相對應的處理,改進可見[改進方案](https://hackmd.io/twR8uo8KT7G0rIXbiBfDiQ#2-%E6%94%B9%E9%80%B2%E6%96%B9%E6%A1%88)。
#### `xs_cow_lazy_copy`
```cpp
static bool xs_cow_lazy_copy(xs *x, char **data)
{
if (xs_get_refcnt(x) <= 1)
return false;
/* Lazy copy */
xs_dec_refcnt(x);
xs_allocate_data(x, x->size, 0);
if (data) {
memcpy(xs_data(x), *data, x->size);
/* Update the newly allocated pointer */
*data = xs_data(x);
}
return true;
}
```
這個函式意義上應是將字串 `data` 的內容複製到 `x`,先考慮 reference count > 1 的情況:
1. 相同內容的長字串因為共用同一段記憶體空間,因此當要將 `data` 的內容更改為 `x`,勢必無法繼續共用原本的記憶體空間,因此在更改之前呼叫 `xs_dec_refcnt`(`x` 不再引用到這個記憶體空間)
2. 呼叫 `xs_allocate_data` 重新分配記憶體空間
3. 利用 `memcpy` 進行複製
而如果 reference count <= 1,
在最初的檢查 `x` 會直接回傳 false 並且完全沒有執行到 `memcpy`,亦即沒有真的進行複製。對於這樣的行為思考許久,為什麼不必複製?
搜尋使用的情境發現呼叫 `xs_cow_lazy_copy` 的方式都是對相同的內容進行複製,共有兩處。
`xs_concat`:
```cpp
char *data = xs_data(string);
xs_cow_lazy_copy(string, &data);
```
以及 `xs_trim`:
```cpp
char *dataptr = xs_data(x), *orig = dataptr;
if (xs_cow_lazy_copy(x, &dataptr))
orig = dataptr;
```
或許這兩處使用 `xs_cow_lazy_copy` 是因為它們將要改動原本的內容,因此先複製到別的位置。但 `xs_cow_lazy_copy` 較符合預期的行為應該還是要複製 reference count <= 1 的內容。
#### `xs_concat`
```cpp
xs *xs_concat(xs *string, const xs *prefix, const xs *suffix)
{
size_t pres = xs_size(prefix), sufs = xs_size(suffix),
size = xs_size(string), capacity = xs_capacity(string);
char *pre = xs_data(prefix), *suf = xs_data(suffix),
*data = xs_data(string);
xs_cow_lazy_copy(string, &data);
if (size + pres + sufs <= capacity) {
memmove(data + pres, data, size);
memcpy(data, pre, pres);
memcpy(data + pres + size, suf, sufs + 1);
if (xs_is_ptr(string))
string->size = size + pres + sufs;
else
string->space_left = 15 - (size + pres + sufs);
} else {
xs tmps = xs_literal_empty();
xs_grow(&tmps, size + pres + sufs);
char *tmpdata = xs_data(&tmps);
memcpy(tmpdata + pres, data, size);
memcpy(tmpdata, pre, pres);
memcpy(tmpdata + pres + size, suf, sufs + 1);
xs_free(string);
*string = tmps;
string->size = size + pres + sufs;
}
return string;
}
```
回傳一個 `prefix + string + suffix` 的字串。
#### `xs_trim`
```cpp
xs *xs_trim(xs *x, const char *trimset)
{
if (!trimset[0])
return x;
char *dataptr = xs_data(x), *orig = dataptr;
if (xs_cow_lazy_copy(x, &dataptr))
orig = dataptr;
/* similar to strspn/strpbrk but it operates on binary data */
uint8_t mask[32] = {0};
#define check_bit(byte) (/* CCC */ mask[(uint8_t) byte / 8] & 1 << (uint8_t) byte % 8)
#define set_bit(byte) (/* SSS */ mask[(uint8_t) byte / 8] |= 1 << (uint8_t) byte % 8)
size_t i, slen = xs_size(x), trimlen = strlen(trimset);
for (i = 0; i < trimlen; i++)
set_bit(trimset[i]);
for (i = 0; i < slen; i++)
if (!check_bit(dataptr[i]))
break;
for (; slen > 0; slen--)
if (!check_bit(dataptr[slen - 1]))
break;
dataptr += i;
slen -= i;
/* reserved space as a buffer on the heap.
* Do not reallocate immediately. Instead, reuse it as possible.
* Do not shrink to in place if < 16 bytes.
*/
memmove(orig, dataptr, slen);
/* do not dirty memory unless it is needed */
if (orig[slen])
orig[slen] = 0;
if (xs_is_ptr(x))
x->size = slen;
else
x->space_left = 15 - slen;
return x;
#undef check_bit
#undef set_bit
}
```
將 `x` 開頭及結尾含有 `trimset` 內的字元的部分截去。
### 2. 改進方案
#### union 中多餘的 data type
程式碼中可以看到 `xs` 所代表的字串有分為短中長三種,但這三種並不是一一對應 `xs` 的三種排列,短字串對應的是:
```cpp
struct {
uint8_t filler[15],
space_left : 4,
is_ptr : 1,
is_large_string : 1,
flag2 : 1,
flag3 : 1;
};
```
長字串則是:
```cpp
struct {
char *ptr;
size_t size : 54,
capacity : 6;
};
```
可以發現 `char data[16];` 是多餘的,其功能可以由 `filler` 代替,因為它們指向相同的記憶體位址,雖然 `filler` 的大小為 15 個 bytes,但當字串長度包含 `\0` 為 16 時,`space_left` 為 0、後面 4 個 flag 也因為是 short string 而為 0,因此 `filler` 後的最後一個 byte 也會是 `\0`。
但這樣設計要成立需要確保編譯器沒有為了避免 unaligned memory access 做 padding。
根據 ISO/IEC 9899:201x 6.7.2.1 p.113
> If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit
以 1 word = 4 byte 來說,`filler` 佔據了 3 個 word 又 3 bytes,最後一個 word 恰好可以讓剩下的 bit fields 補齊,因此沒有 padding 發生。
```cpp
struct {
uint8_t filler[15],
space_left : 4,
is_ptr : 1,
is_large_string : 1,
flag2 : 1,
flag3 : 1;
};
```
做一個實驗來看看每個 bitfield 的位置是否不必加額外的 pack 也不會有多餘的 padding:
```cpp
typedef struct {
uint8_t filler[15],
space_left : 4,
is_ptr : 1, is_large_string : 1, flag2 : 1, flag3 : 1;
} s_t;
int main() {
s_t s;
printf("filler: %p\n", &s.filler);
printf("space_left: %p\n", s.space_left);
printf("is_ptr: %p\n", &s.is_ptr);
printf("is_large_string: %p\n", &s.is_large_string);
printf("flag2: %p\n", &s.flag2);
printf("flag3: %p\n", &s.flag3);
}
```
但很顯然 bit field 無法被 address of:
```shell
padding_test.c:17:42: error: cannot take address of bit-field 'space_left'
17 | printf("space_left: %p\n", ((char *) &s.space_left));
| ^
```
因為 bit field 的單位是 bit,比指標指向的單位 (byte) 還要細。
在 ISO/IEC 9899:201x 6.7.2.1 p.112 提到:
> The unary & (address-of) operator cannot be applied to a bit-field object; thus, there are no pointers to
or arrays of bit-field objects.
所以改由觀察整個 struct 所佔據的記憶體空間,使用 `sizeof` 來看是否大於 16:
```cpp
int main() {
printf("size: %lu\n", sizeof(s_t));
}
```
> 預期輸出:
> ```
> size: 16
> ```
結果顯示確實沒有額外的 padding。
參考資料
[Unaligned Memory Accesses](https://www.kernel.org/doc/Documentation/unaligned-memory-access.txt#:~:text=Unaligned%20memory%20accesses%20occur%20when,be%20an%20unaligned%20memory%20access.)
#### `xs_set_refcnt` checking
`xs_set_refcnt` 並沒有防止使用者對 short string、middle string 指派 reference count。
```cpp
static inline void xs_set_refcnt(const xs *x, int val)
{
if (xs_is_large_string(x))
*((int *) ((size_t) x->ptr)) = val;
}
```
#### `xs_allocate_data`
考慮將已分配為 large string 重新分配成 medium string 的長度,應把 `is_large_string` 這個 flag 設為 0。
```cpp
static void xs_allocate_data(xs *x, size_t len, bool reallocate)
{
/* Medium string */
if (len < LARGE_STRING_LEN) {
x->ptr = reallocate ? realloc(x->ptr, (size_t) 1 << x->capacity)
: malloc((size_t) 1 << x->capacity);
x->is_large_string = 0;
return;
}
```
#### `xs_grow` 邏輯錯誤
`xs_grow` 有許多明顯的問題:
1. 在 short string 成長為 medium/large string 的情況,先更改了 `is_ptr` 才在後續判斷原本是否為 short string (利用 `xs_is_ptr`)
2. 沒有考慮增長為 large string 的情況
用一個簡單的測試就能看到程式的問題:
```cpp
int main(int argc, char *argv[])
{
xs *string = xs_new(&xs_literal_empty(), "12345");
printf("type should be short (0): %d, capcity: %ld\n", xs_type(string), xs_capacity(string));
xs_grow(string, 16);
printf("type should be medium (1): %d, capcity: %ld\n", xs_type(string), xs_capacity(string));
xs_grow(string, 256);
printf("type should be large (2): %d, capcity: %ld\n", xs_type(string), xs_capacity(string));
return 0;
}
```
> 預期輸出:
> ```shell
> type should be short (0): 0, capcity: 15
> Segmentation fault
> ```
修改程式碼如下:
```cpp
xs *xs_grow(xs *x, size_t len)
{
// 將成長前的 is_ptr 保存起來
bool is_ptr = x->is_ptr;
x->is_ptr = true;
if (len < LARGE_STRING_LEN) {
if (is_ptr) {
xs_allocate_data(x, len, 1);
} else {
xs_allocate_data(x, len, 0);
memcpy(xs_data(x), buf, 16);
}
} else {
// 增長為 large string 的情況
x->is_large_string = true;
if (is_ptr) {
xs_allocate_data(x, len, 1);
} else {
xs_allocate_data(x, len, 0);
memcpy(xs_data(x) + 4, buf, 16);
}
}
return x;
}
```
> 預期輸出:
> ```shell
> type should be short (0): 0, capcity: 15
> type should be medium (1): 1, capcity: 31
> type should be large (2): 2, capcity: 511
> ```
### 3. 字串複製 (CoW) 的函式實作
參照 [5. 整合 string interning](https://hackmd.io/twR8uo8KT7G0rIXbiBfDiQ#5-%E6%95%B4%E5%90%88-string-interning) 的部分,將 `xs_cow_lazy_copy` 改寫,主要需要考量若 `x` 為 large string,那複製 `data` 到 `x` 時,必須一併處理 reference count,在複製前,應將 reference count 減一,然後再為 `x` 配置新的記憶體空間。
```cpp
static void xs_cow_lazy_copy(xs *x, char **data)
{
if (!x->is_large_string) {
memcpy(xs_data(x), *data, strlen(*data));
return;
} else if (xs_get_refcnt(x) > 1) {
/* Lazy copy */
xs_dec_refcnt(x);
xs_interning_nontrack(x, *data, x->size, 0);
}
if (data) {
memcpy(xs_data(x), *data, x->size);
}
/* Update the newly allocated pointer */
*data = xs_data(x);
return;
}
```
考量到在 string interning 中的實作,會對比字串的 hash 是否跟 pool 中存在的 string 相同,若有則進一步比對,若確定與現存的 string 重複,則使用同一段記憶體空間。
但在 `xs_cow_lazy_copy` 的使用情境不同,即便字串相同,但可能即將被 `concat` 或 `trim` 等方式修改內容,所以會希望它被存在不同的記憶體空間,即使內容重複,因此實作 `xs_interning_nontrack`,將記憶體配置,而不透過 string interning 紀錄其值,也不會計算 hash。
```cpp
static void xs_interning_nontrack(xs *x, const void *p, size_t len, bool reallocate)
{
x->ptr = reallocate ? realloc(x->ptr, (size_t) 1 << x->capacity + 4)
: malloc((size_t) 1 << x->capacity + 4);
memcpy(xs_data(x), p, len);
xs_set_refcnt(x, 1);
}
```
而在 `concat` 及 `trim` 完成對字串的更動之後,加上這一個操作,將該記憶體位置加入 string interning 的紀錄。
```cpp
if (string->is_large_string)
add_interning_address(string->ptr);
```
`add_interning_address` 的實作如下,跟 `add_interning` 雷同,差別在於它直接接受一個位於 `heap` 的字串位址,不必再分配記憶體,而 `add_interning` 則是需要自行配置記憶體,再將該記憶體位址回傳給呼叫的函式,該函式需要負責將位址指派到 `xs_data(xs)`。
```cpp
void add_interning_address(char *data)
{
XS_LOCK();
int len = strlen(data);
uint32_t hash = hash_blob(data, len);
if (!__intern_ctx.pool) {
__intern_ctx.pool = malloc(sizeof(struct __xs_pool));
}
struct xs_node **n = &__intern_ctx.pool->node[hash];
while (*n) {
if (strcmp((*n)->data + 4, data) == 0) {
++(*(int *) ((size_t) (*n)->data));
XS_UNLOCK();
return;
}
n = &((*n)->next);
}
*n = malloc(sizeof(struct xs_node));
(*n)->data = data;
XS_UNLOCK();
return;
}
```
### 4. 分析字串處理最佳化的空間效益
:::success
應考慮到 locality of reference,善用 GDB 追蹤和分析,確認字串的確符合預期地配置於 stack 或 heap;
:::
`// TODO 補完分析和實驗`
#### `xs` 空間效率
針對 1000 個 short+medium 的字串做初始化:
`xs`
```
KB
46.84^ #
| @@#
| @@::@@#
| @@@@ : @@#
| @@@@@ @ : @@#
| :@@ @@@ @ : @@#
| :::@:@@ @@@ @ : @@#
| @@@: :@:@@ @@@ @ : @@#
| @@@@@@ : :@:@@ @@@ @ : @@#
| @@@ @@@@ : :@:@@ @@@ @ : @@#
| @@@@ @ @@@@ : :@:@@ @@@ @ : @@#
| :@@@@ @ @ @@@@ : :@:@@ @@@ @ : @@#
| @@:@ @@ @ @ @@@@ : :@:@@ @@@ @ : @@#
| @@::@ :@ @@ @ @ @@@@ : :@:@@ @@@ @ : @@#
| @@:@ : @ :@ @@ @ @ @@@@ : :@:@@ @@@ @ : @@#
| @@@@@:@ : @ :@ @@ @ @ @@@@ : :@:@@ @@@ @ : @@#
| @@@@@ @@:@ : @ :@ @@ @ @ @@@@ : :@:@@ @@@ @ : @@#
| :: @@@@@@@ @@:@ : @ :@ @@ @ @ @@@@ : :@:@@ @@@ @ : @@#
| : ::@ @@@@@ @@:@ : @ :@ @@ @ @ @@@@ : :@:@@ @@@ @ : @@#
| : ::@ @@@@@ @@:@ : @ :@ @@ @ @ @@@@ : :@:@@ @@@ @ : @@#
0 +----------------------------------------------------------------------->ki
0 486.2
```
`std::string`
```
KB
77.54^ #
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
| #:::::::@::
|@ #:::::::@::
0 +----------------------------------------------------------------------->Mi
0 2.430
```
### 5. 整合 string interning
嘗試用簡易的方式來追蹤程式中的 large string,加入 `intern.[ch]` 來為 `xs` 擴充。
大部分的 struct 為 `cstr` 的簡化:
```cpp
struct xs_node {
char *data;
struct xs_node *next;
};
struct __xs_interning {
int lock;
struct __xs_pool *pool;
};
struct __xs_pool {
struct xs_node *node[INTERNING_POOL_SIZE];
};
static struct __xs_interning __intern_ctx;
```
實作 `add_interning`,該函式主要會被 `xs_new` 呼叫,若是使用者新增了一個 large string,便會透過此函式分配記憶體,並計算 hash,放進 pool 中。
```cpp
struct xs_node *add_interning(const char *str)
{
XS_LOCK();
int len = strlen(str);
uint32_t hash = hash_blob(str, len);
if (!__intern_ctx.pool) {
__intern_ctx.pool = malloc(sizeof(struct __xs_pool));
}
struct xs_node **n = &__intern_ctx.pool->node[hash];
while (*n) {
if (strcmp((*n)->data + 4, str) == 0) {
++(*(int *) ((size_t) (*n)->data));
XS_UNLOCK();
return *n;
}
n = &((*n)->next);
}
*n = malloc(sizeof(struct xs_node));
(*n)->data = malloc(sizeof(char) * len + 4);
memcpy(((*n)->data + 4), str, len);
XS_UNLOCK();
return (*n);
}
```
修改過後的 `xs_new`,直接負責處理不同長度字串的邏輯,而 `xs_allocate_data` 也簡化為處理 middle string 的記憶體分配:
```cpp
xs *xs_new(xs *x, const void *p)
{
*x = xs_literal_empty();
size_t len = strlen(p) + 1;
if (len > LARGE_STRING_LEN) {
x->capacity = ilog2(len) + 1;
x->size = len - 1;
x->is_ptr = true;
x->is_large_string = true;
xs_interning(x, p, x->size, 0);
} else if (len > /* NNN */ MIDDLE_STRING_LEN) {
x->capacity = ilog2(len) + 1;
x->size = len - 1;
x->is_ptr = true;
xs_allocate_data(x, x->size, 0);
memcpy(xs_data(x), p, len);
} else {
memcpy(x->data, p, len);
x->space_left = 15 - (len - 1);
}
return x;
}
```
large string 的記憶體分配以及 string interning 紀錄則改由 `xs_interning` 來負責:
```cpp
static void xs_interning(xs *x, const void *p, size_t len, bool reallocate)
{
// the allocation and set refcnt is handled at interning
struct xs_node *n = add_interning(p);
x->ptr = n->data;
}
```
作為測試,新增三個長度超過 `LARGE_STRING_LEN` 的相同字串,並用 `xs_data` (效果同 `string->ptr + 4`)並用 `%p` 觀察是否有共用同一段記憶體位址。
```cpp
#include "../xs.h"
int main(int argc, char *argv[])
{
xs *string1 = xs_new(&xs_literal_empty(), "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890");
xs *string2 = xs_new(&xs_literal_empty(), "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890");
xs *string3 = xs_new(&xs_literal_empty(), "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890");
printf("string1: %p\n", xs_data(string1));
printf("string2: %p\n", xs_data(string2));
printf("string3: %p\n", xs_data(string3));
}
```
> 預期輸出:
> ```
> string1: 0x55a42c2332d4
> string2: 0x55a42c2332d4
> string3: 0x55a42c2332d4
> ```
詳細實作可見 [GitHub](https://github.com/ToastCheng/linux-2021q1/blob/main/week3/q3/intern.c)(TODO: 優化實作)。
### 6. Linux 中的 SSO、CoW 案例
CoW 的設計應該會有兩個情境:
1. 不修改內容時,**不複製**使用共享的資料
2. 預期會修改內容時,複製資料至新分配的記憶體空間
觀察 `sk_buff`,為代表 socket buffer 的 struct,有對應的函式 `skb_copy`、`skb_clone`:
#### `skb_copy`
```cpp
/**
* skb_copy - create private copy of an sk_buff
* @skb: buffer to copy
* @gfp_mask: allocation priority
*
* Make a copy of both an &sk_buff and its data. This is used when the
* caller wishes to modify the data and needs a private copy of the
* data to alter. Returns %NULL on failure or the pointer to the buffer
* on success. The returned buffer has a reference count of 1.
*
* As by-product this function converts non-linear &sk_buff to linear
* one, so that &sk_buff becomes completely private and caller is allowed
* to modify all the data of returned buffer. This means that this
* function is not recommended for use in circumstances when only
* header is going to be modified. Use pskb_copy() instead.
*/
struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t gfp_mask)
{
int headerlen = skb_headroom(skb);
unsigned int size = skb_end_offset(skb) + skb->data_len;
struct sk_buff *n = __alloc_skb(size, gfp_mask,
skb_alloc_rx_flag(skb), NUMA_NO_NODE);
if (!n)
return NULL;
/* Set the data pointer */
skb_reserve(n, headerlen);
/* Set the tail pointer and length */
skb_put(n, skb->len);
BUG_ON(skb_copy_bits(skb, -headerlen, n->head, headerlen + skb->len));
skb_copy_header(n, skb);
return n;
}
```
註解提到:
> Make a copy of both an &sk_buff and its data. This is used when the caller wishes to modify the data and needs a private copy of the data to alter.
很明顯地這是 CoW 的情境,在預期會修改內容的情況複製,並將 reference count 設為 1。
#### `skb_clone`
```cpp
/**
* skb_clone - duplicate an sk_buff
* @skb: buffer to clone
* @gfp_mask: allocation priority
*
* Duplicate an &sk_buff. The new one is not owned by a socket. Both
* copies share the same packet data but not structure. The new
* buffer has a reference count of 1. If the allocation fails the
* function returns %NULL otherwise the new buffer is returned.
*
* If this function is called from an interrupt gfp_mask() must be
* %GFP_ATOMIC.
*/
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)
{
struct sk_buff_fclones *fclones = container_of(skb,
struct sk_buff_fclones,
skb1);
struct sk_buff *n;
if (skb_orphan_frags(skb, gfp_mask))
return NULL;
if (skb->fclone == SKB_FCLONE_ORIG &&
refcount_read(&fclones->fclone_ref) == 1) {
n = &fclones->skb2;
refcount_set(&fclones->fclone_ref, 2);
} else {
if (skb_pfmemalloc(skb))
gfp_mask |= __GFP_MEMALLOC;
n = kmem_cache_alloc(skbuff_head_cache, gfp_mask);
if (!n)
return NULL;
n->fclone = SKB_FCLONE_UNAVAILABLE;
}
return __skb_clone(n, skb);
}
```
註解提到:
> The new one is not owned by a socket. Both copies share the same packet data but not structure. The new buffer has a reference count of 1.
透過 `skb_clone` 複製的 socket buffer 會跟原本的 `sk_buffer` 共享 packet data (`unsigned char *data`),並將 reference count 設為 1,可見 `__skb_clone`,在 `sk_buff` 中 reference count 紀錄在 `refcount_t users` 中:
```cpp
static struct sk_buff *__skb_clone(struct sk_buff *n, struct sk_buff *skb)
{
// ...
refcount_set(&n->users, 1);
// ...
}
```
這和純粹的共享資料有點不同,可以注意到 `skb_clone` 依然將 reference count 設為 1,照 string interning 的邏輯,如果要重複使用相同的字串,應該要將 reference count 加一。
那增加 reference count 的情境應該是本來預期的共享資料,因此用 `refcount_inc\(&[a-zA-Z]+->users\)` 搜尋,但並沒有特定的函式負責管理,而是每個函式在接收到 `sk_buff` 的指標副本後負責增減 reference count,各自管理。
#### `skb_unshare`
```cpp
/*
* Copy shared buffers into a new sk_buff. We effectively do COW on
* packets to handle cases where we have a local reader and forward
* and a couple of other messy ones. The normal one is tcpdumping
* a packet thats being forwarded.
*/
/**
* skb_unshare - make a copy of a shared buffer
* @skb: buffer to check
* @pri: priority for memory allocation
*
* If the socket buffer is a clone then this function creates a new
* copy of the data, drops a reference count on the old copy and returns
* the new copy with the reference count at 1. If the buffer is not a clone
* the original buffer is returned. When called with a spinlock held or
* from interrupt state @pri must be %GFP_ATOMIC
*
* %NULL is returned on a memory allocation failure.
*/
static inline struct sk_buff *skb_unshare(struct sk_buff *skb,
gfp_t pri)
{
might_sleep_if(gfpflags_allow_blocking(pri));
if (skb_cloned(skb)) {
struct sk_buff *nskb = skb_copy(skb, pri);
/* Free our shared copy */
if (likely(nskb))
consume_skb(skb);
else
kfree_skb(skb);
skb = nskb;
}
return skb;
}
```
另外還有 `skb_unshare` 這樣的函式,先用 `skb_cloned` 來判斷拿到的 `skb` 是否為透過 `skb_clone` 得到的,如果是就重新呼叫 `skb_copy` 斷開兩者共享的 packet data。