---
tags: linux2023
---
# 第 10, 11, 12 週課堂問答簡記
## Chiwawachiwawa
[quiz4](https://hackmd.io/@CWWPPB/H1IyltGW2) 紅黑樹
> 對照 [hamt](https://github.com/mkirchner/hamt), [hamt-bench](https://github.com/mkirchner/hamt-bench)
rbtree 的應用場景: mm, sched (CFS, $3.1)
rb_gen
設計實驗 ==> 檢驗 rbtree 的 locality
規模?
128 op -> 1024 op ->
## chun61205
> [2023q1 第 11 週測驗題](https://hackmd.io/@sysprog/linux2023-quiz11)
### `barrier_cross` 是怎麼執行的?
看到 `barrier_cross`
```c
// test
/* before starting the test, we insert a number of elements in the data
* structure.
* we do this at each thread to avoid the situation where the entire data
* structure resides in the same memory node.
*/
for (int i = 0; i < (int)d->n_add; ++i) {
the_value =
(val_t) my_random(&seeds[0], &seeds[1], &seeds[2]) & rand_max;
/* we make sure the insert was effective (as opposed to just updating an
* existing entry).
*/
// if(push(the_list,the_value))
if (list_insert(the_list, the_value) == 0)
i--;
}
/* Wait on barrier */
barrier_cross(d->barrier);
```
```c
void barrier_cross(barrier_t *b)
{
pthread_mutex_lock(&b->mutex);
b->crossing++; /* One more thread through */
if (b->crossing < b->count) { /* If not all here, wait */
pthread_cond_wait(&b->complete, &b->mutex);
} else {
pthread_cond_broadcast(&b->complete);
b->crossing = 0; /* Reset for next time */
}
pthread_mutex_unlock(&b->mutex);
}
```
看到 `test` 的說明,在實際測試時,會交錯進行插入和刪除,因此需要在測試之前進行測試前插入,以確保在刪除時 list 內部有節點可以刪除。
只要 thread 執行完測試前的插入,就會執行 `barrier_cross` ,並執行 `b->crossing++` 代表多一個 thread ,直到 `b->crossing < b->count` 時,就會值行 `p_thread_cond_broadcase` 將其他 thread 叫起來,代表所有的 thread 都已經做完測試前插入。
1. 這段程式碼的 condition variable 的使用很像 counting semaphore ,差別在於這裡有使用到 broadcase 發送信號給其他 thread 。
2. 另外這個 barrier_cross 在 linux 有相對應的 API 實作,因此可以用 API 來改寫。
這裡我有個問題,就是為什麼在測試前插入時,會需要使用 barrier 來等待所有 thread 的測試前插入都執行完成?
仔細觀察程式碼可以發現,
```c
barrier_init(&barrier, n_threads + 1);
void barrier_init(barrier_t *b, int n)
{
pthread_cond_init(&b->complete, NULL);
pthread_mutex_init(&b->mutex, NULL);
b->count = n;
b->crossing = 0;
}
void barrier_cross(barrier_t *b)
{
pthread_mutex_lock(&b->mutex);
b->crossing++; /* One more thread through */
if (b->crossing < b->count) { /* If not all here, wait */
pthread_cond_wait(&b->complete, &b->mutex);
} else {
pthread_cond_broadcast(&b->complete);
b->crossing = 0; /* Reset for next time */
}
pthread_mutex_unlock(&b->mutex);
}
```
`barrier_init` 會以 `nthreads + 1` 作為參數,並設定 `b->count = n` ,換句話說在 `barrier_cross` 的時候會等待 `pthread_create` 執行的數量再加上 $1$ 個 thread 才會執行 `pthread_cond_broadcast` 。
繼續往下看到 `main` 可以發現
```c
/* Start threads */
barrier_cross(&barrier);
gettimeofday(&start, NULL);
if (duration > 0) {
/* sleep for the duration of the experiment */
nanosleep(&timeout, NULL);
} else {
sigemptyset(&block_set);
sigsuspend(&block_set);
}
/* signal the threads to stop */
*running = 0;
gettimeofday(&end, NULL);
```
在所有 thread 都創建完後,才會再執行 `barrier_cross` 這時進入 `barrier_cross` 數量才會足夠,並把所有的 thread 叫起來開始測試,`main` 也會在這個時候開始計時。因此,使用 barrier 等待所有 thread 的測試前插入都執行完的原因是,為了能夠讓所有 thread 在同一個時間「起跑」。
然而,如此一來又會產生另外一個問題,因為這些 thread 事實上並不會同時執行,而是由 CPU scheduler 決定當前的資源要分給哪個 thread 執行。
### Insert 和 Delete 的執行順序
```c
if (op < read_thresh) { /* do a find operation */
top(the_list);
if (list_insert(the_list, the_value)) {
d->n_insert++;
last = 1;
}
if (list_delete(the_list, the_value)) {
d->n_remove++;
last = -1;
}
d->n_ops++;
```
在插入一定的資料到鏈結串列後,會測試 insert 和 delete 以及「某個其他的動作」 (這裡是 `top(the_list)`) 的運作效能。
實際運作方式如下:
1. 先進行一定數量的 `top(the_list)`
2. `list_insert`
3. `list_delete`
4. 反覆進行步驟 3 和步驟 4
然而,這麼做會有一個問題,因為 insert 和 delete 是交錯進行,會造成能夠測試到的範圍固定的問題。
接下來,我希望能夠設計出一種方法,利用隨機數產生接下來執行的 operation 。
大略想法如下,
1. 隨機數生成出的 operation 和目前鏈結串列中的資料數有關,資料越多,則生成出 delete 的機率越高。
2. 因為當鏈結串列的資料數量為 $0$ 時,不能執行 delete ,因此如果發生這樣的情況生成出 delete 的機率應該要為零。
設計這樣的方法有一個前題,也就是鏈結串列的容量固定。如果能夠事先估算鏈結串列的容量,則代表在接近容量滿的情況下,會再需要插入資料的可能性較低。
#### 參考線性同餘方法 LCG
> [線性同餘方法- 维基百科,自由的百科全书](https://zh.wikipedia.org/zh-tw/%E7%B7%9A%E6%80%A7%E5%90%8C%E9%A4%98%E6%96%B9%E6%B3%95)
$$
N_{j+1}=(A\times N_j B)\ mod\ M
$$
其中
1. $N_{j+1}$ 和 $N_j$ 分別是下個要產生出的隨機數和當前的隨機數。
2. $A$, $B$, $M$ 為常數
1. $B$, $M$ 互質
2. $M$ 的所有質因數能夠整除 $A-1$
3. 若 $M$ 是 $4$ 的倍數,則 $A-1$ 也是
4. $A$, $B$, $N_0$ 都小於 $M$
5. $A$, $B$ 屬於正整數
事實上, glibc 的 `rand` 正是使用此方法來實作,其中,
1. $A=1103515245$
2. $B=12345$
3. $M=2^{31}$
#### 設計 operation 生成函式
### barrier 的改寫
## fennecJ
### `uintptr_t` 是從什麼型態的衍生型態
### 為何需要使用 (`uintptr_t`) 轉型
在 `try_flag` 這個函式中,第 31 行的操作如下:
```c
const uintptr_t new_val = (uintptr_t) n | F_FLAG;
```
為何需要在 n 前面加上 `(uintptr_t)`,如果不加的話會有什麼問題?
> 會有 warning ?
好,我們把它拿掉直接編譯看看
```bash
...
error: invalid operands to binary | (have ‘struct nblist_node *’ and ‘int’)
```
這是因為 C 語言中的 bitwise 運算只能給整數型態使用,而 n 不是整數型態,所以會出錯。
### list_search 的用途是找兩個連續的相鄰節點,這件事為何重要
> 我想是為了確保它 insert 時不會讓資料出錯...?
你有看過 [並行程式設計: Lock-Free Programming](https://hackmd.io/@sysprog/concurrency/https%3A%2F%2Fhackmd.io%2F%40sysprog%2Fconcurrency-lockfree) 這篇文章了嗎?
> 還沒。
沒關係,那我們現在來看:
![](https://hackmd.io/_uploads/SJky5AP72.png)
現在有一個鏈結串列,我要把 10 移走,把 20 插進來,如果先發生 remove 才發生 insert 結果就會出錯:
![](https://hackmd.io/_uploads/Skbt90PXh.png)
> 所以找兩個相鄰節點目的就是找出可能發生這種錯誤的地方?
沒錯,在程式中我們會用 F_FLAG 和 F_MARK 來標出即將被移除的節點和已經被移除的節點。這裡有一件值得注意的事情,看到 `list_delete` 這個函式,你有在裡面看到任何釋放記憶體的程式碼嗎?
> 沒有。
對,這個函式真正做的事情只有更新該節點的 flag ,而真正移除節點的操作則是在
## mimalloc
[include/mimalloc/internal.h](https://github.com/microsoft/mimalloc/blob/master/include/mimalloc/internal.h)
> Overflow detecting multiply
mmap(2)
> If addr is NULL, then the kernel chooses the (page-aligned) address at which to create the mapping; this is the most portable method of creating a new mapping.
## Korin777
```c
struct page_header {
size_t pages;
size_t page_size;
char page_data[]; // the rest of the page(s) of memory
};
```
```c
struct page_header {
size_t pages;
size_t page_size;
char *page_data; // the rest of the page(s) of memory
};
```
> 為何將 `struct page_header` 的 `page_data` 改為 `char *page_data` 會引發錯誤 => a.out: test.c:138: test_alloc_small: Assertion `result == TEST_SUCCESS' failed.`
> [name= fennecJ]
是不是和 [Flexible array members](https://www.ibm.com/docs/en/i/7.2?topic=declarations-flexible-array-members) 有關
在 `simple_malloc` 我們要回傳記憶體地址給使用者,若將 `page_data[]` 改為 `*page_data`,此時 `result->page_data` 會是記憶體地址中的值
* 當 `page_data` 為 `page_data[]` 此時 `result->page_data` 相當於 `&result->page_data[0]`
* 當 `page_data` 為 `*page_data` 此時 `result->page_data` 相當於 `result->page_data[0]`
因此把 `page_data[]` 改為 `*page_data` 就必須將所有 `result->page_data` 改為 `&result->page_data` 才會是我們要的記憶體地址,就能避免上述錯誤,這時會發現在 `free` 產生錯誤
用 `sizeof` 去看上面兩個結構體所佔的空間,可以發現 `page_data[]` 空間並沒有被算進去,所以 `void_to_page_header` 可以直接減去結構體大小來獲取 `page_header` , 也因此當我們將 `page_data[]` 改為 `*page_data` , 就會額外去扣掉 `*page_data` 所佔的空間,可以在 `simple_free_internal` 補回多扣掉的空間來解決這個問題
## YSRossi
> [alloc.c](https://gist.github.com/jserv/6ba3bacbbde6ccbf1f0a2b26f00b92a3) 目前的 `alloc` 實作能否在多執行緒的環境運作?
不行,因為沒有保護
> 哪些部份需要被保護?
第 15 行之前都是區域變數,不需要保護。[mmap](https://man7.org/linux/man-pages/man2/mmap.2.html) 為 `MT-Safe`,因此也不需要保護。應該保護的地方為第 23~24 行,也就是更新 page header 資訊的地方。
```c=
static struct page_header *alloc_internal(size_t size)
{
if (!size)
return NULL;
/* size in bytes of a page of memory */
size_t page_size = get_page_size();
/* size in bytes that needs to be allocated */
size_t size_with_header = size + sizeof(AAAA);
/* number of pages to allocate */
/* TODO: fix case where size_with_header == page_size */
size_t pages = size_with_header / page_size + BBBB;
size_t mmap_size = pages * page_size;
void *ptr = mmap(0, mmap_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED)
return NULL;
struct page_header *page = ptr;
/* copy values to page header */
page->pages = pages;
page->page_size = page_size;
return page;
}
```
:::info
我仔細思考這一題,我覺得這題不必特別去保護資料就可以在多執行緒運作。
`realloc` 和 `free` 以功能來說,在傳入指標相同的狀況下一定不可能在二個以上執行緒運作。因為一旦一個執行緒為先進行 `free` ,另一個執行緒在傳入相同指標下執行 `free` 就變成 double-free , `realloc` 重新配置空間可能會改變指標,故一個執行緒成功,另一個執行緒就會對無效的指標進行 `realloc` 。故考慮多執行緒的狀況下,應只需要考慮 `realloc` 和 `free` 在不同指標的情況下同時執行的狀況。
老師在檢討時提到的 `alloc_internal` 函式來說,在多執行緒同時執行時,`mmap` 同時被執行,但回傳的指標應會不同。故下面的幾行指派 `struct page_header` 裡的 `pages` 和 `page_size` 部份應不會造成問題,不需要保護。
不過老師有提到,若加入 free list 來充份利用空間,而不是每此配置只能以 page 為單位來配置空間,維護 free list 就需要特別保護。
> [name= yanjiew1]
:::
> 如果為了使用較小的空間而配置一個 page,這樣很浪費資源,我們可以配置一個 page 重複使用這個空間,那你會用什麼較不複雜的資料結構來管理?
雖然使用單向鏈結串列較省空間,但通常會使用雙向 linked list,相較於單向鏈結串列有較快的搜尋速度。
我們需要搜尋 free list 尋找符合請求的空間,所以搜尋複雜度為一個考量點。選擇使用 red-black tree 來管理 free list ,雖然搜尋速度不是最快的,重點是 worst case 的時間複雜度為 O(log n)。
## [concurrent hashtable](https://hackmd.io/@sysprog/linux2023-quiz12)
- [ ] concurrent hashtable
* deep copy
* 運用 callback 處理數值傳遞和資源釋放
* 呼叫 `sched_yield`
#### `ht_get_list` 的作用
本次程式碼使用 [seperate chaining](https://en.wikipedia.org/wiki/Hash_table#Separate_chaining) 的機制 (示意圖如下)以處理 hash table 中發生 collision 的問題, 其中 `ht_get_list` 的作用如其名稱,將回傳作為 bucket 所使用的 linked list。
![](https://hackmd.io/_uploads/r1-t6QGNn.png)
#### deep copy 的存在必要是在並行的環境中,避免 dangling pointer
Copy 主要可以分為 shallow copy 和與其相對的 deep copy,其中 shallow copy 並不完全 "複製" 出一個物件,而是複製出一個物件的參照 (如: 指標)。 例如 [python](https://github.com/python/cpython/blob/3.11/Lib/copy.py) 中的 `copy` 作法是創造一個物件並將原物件內含的物件裝入,示意圖如下。
> - A shallow copy constructs a new compound object and then (to the
extent possible) inserts *the same objects* into it that the
original contains.
![](https://hackmd.io/_uploads/HJPRYEME3.png)
而 deep copy 相對於 shallow copy,除了複製物件本身之外也會一併複製內含的物件,新物件和就物件可以看成相同屬性的量個獨立個體,示意圖如下。 由於內容物也需要配置記憶體,所消耗資源也比 shallow copy 多上許多。
> - A deep copy constructs a new compound object and then, recursively,
inserts *copies* into it of the objects found in the original.
![](https://hackmd.io/_uploads/HyCtTVz42.png)
在 cocurrent 的需求下,物件何時會被哪個執行序進行修改是無法得知的,使用 deep copy 可以避免其他執行序若持有該物件的參照進而在執行過程中產生 dangling pointer。
iterator 在走訪個別元素時,可能會有其他執行緒變更