## Linux 核心專題: rhashtable 及核心記憶體管理研究
> 執行人: SHChang-Anderson
> [GitHub](https://github.com/SHChang-Anderson/rcuhashbash)
> [專題解說錄影](https://youtu.be/FX0I8hzsizc)
> [專題解說簡報](https://drive.google.com/file/d/1gY2m_WhPAvyRSgV5SIvRD8dnTLTHtdHZ/view)
### Reviewed by `kevinzxc1217`
當 hash table 進行 resize 過程中,會有新的 future table,即同時存在兩個 hash table,請問這樣會不會造成記憶體空間不足的問題?
> 新增 Bucket lock 和 Future Table 功能後,會增加記憶體使用量。其中,Future Table 本身會佔用一部分記憶體,而每個 Bucket lock 也會個別佔用記憶體空間。
> [name=SH]
### Reviewed by `fennecJ`
* 針對 Table shrinking 的手法,可否分析該操作中 bucket 的數量對時間複雜度的影響為何?
* 在介紹 Table shrinking 的手法時,解說影片提及為了避免還有人在對 hash table 進行存取,要等待一段時間才把指標刪除,這裡的 「等待一段時間」在影片中提及是透過 RCU 實作。顯然,隨著存取 hash table 的 thread 數量變多,要等待的時間也會相應變長。可否提供分析計算出存取 hash table 的 thread 數量和等待時間的數學關係為何 ?
* 根據你提供的實驗結果,可見 Lookups/sec 大致和 reader threads 呈固定比例,直到 reader threads 數量超過 8 個後,斜率有顯著下降,請問該現象的成因可能為何?
>根據〈[Resizable, Scalable, Concurrent Hash Tables via Relativistic Programming](https://www.usenix.org/legacy/event/atc11/tech/final_files/Triplett.pdf)〉所提供的實驗環境,擁有 24 個硬體支援的執行緒,因此當讀取器(readers)數量低於硬體支援的執行緒數量時,效能呈現線性成長。然而,本實驗環境僅有 6 核心 12 執行緒,當讀取器數量達到 8 個時,效能就顯著下降。這是因為 16 個讀取器超過了可用執行緒的數量,導致效能瓶頸。
>[name=SH]
### Reviewed by `ssheep773`
在 table expansion 時若從 bucket 中串列的最後一個節點開始,是否可以減少資料競爭的情況。
>在 unzip 過程中,即使從 bucket 串列的最後一個節點開始走訪,仍需持續走訪鏈結串列,直到遇到不屬於同一個 bucket 的節點,並重複此過程。由於這整個過程都需要持有該 bucket lock,因此無法有效減少資料競爭的情況。
此外,根據最新的 [lib/rhashtable.c](https://github.com/torvalds/linux/blob/master/lib/rhashtable.c) 實作,unzip 操作已被取消,改為逐一將舊的 bucket 節點透過 rehash 函式重新分配到新的 hash table。因此,無論從鏈結串列的開頭或結尾開始,資料競爭的情況皆相同,因為需要走訪整個鏈結串列才能完成該 bucket 的 rehash。
[name=SH]
## 任務說明
閱讀 [rhashtable](https://hackmd.io/@linjohn/rhashtable) 及 Linux 核心記憶體管理研究、紀錄問題,確保對應的程式碼可運作於 Ubuntu Linux 24.04 (搭配 Linux v6.8)。
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 48 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 12
On-line CPU(s) list: 0-11
Vendor ID: AuthenticAMD
Model name: AMD Ryzen 5 5500U with Radeon Graphics
CPU family: 23
Model: 104
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 1
CPU(s) scaling MHz: 26%
CPU max MHz: 4056.0000
CPU min MHz: 400.0000
BogoMIPS: 4192.08
$ uname -r
6.8.0-35-generic
```
## TODO: 閱讀 [rhashtable](https://hackmd.io/@linjohn/rhashtable),紀錄核心記憶體管理相關問題
> 可重用之前的材料,但需要針對 Linux v6.8+ 更新
### [2023 開發紀錄](https://hackmd.io/@sysprog/HkKKE5eB3) 回顧及文獻探討
> [Relativistic hash tables, part 1: Algorithms](https://lwn.net/Articles/612021/)
事先理解 rhashtable ,參考過去共筆以及 [Relativistic hash tables, part 1: Algorithms](https://lwn.net/Articles/612021/) 了解 hashtable 的 resize 方式。
#### Table shrinking
若雜湊表中 buckets 數量過多,希望降低 buckets 數量使用 Table shrinking 的方式重新分配雜湊表 。
* 第一步驟建立一個新的雜湊表並指向第一個 bucket 中串列的第一個節點。
* 接著將第一個 bucket 鏈結串列結尾指向下一個串列的開頭,值得注意的是這樣的作法避免在 resize 的過程中避免還在讀取的 readers 找不到節點的情況發生。
* 最後將舊的雜湊表指標指向新的雜湊表,這個時候舊的雜湊表尚不能回收,需要等待一個 [grace period](https://hackmd.io/@sysprog/linux-rcu#%E5%AF%AC%E9%99%90%E6%9C%9F) 避免尚為讀取完成的 readers 發生錯誤。
#### Table expansion
若雜湊表內 buckets 數量過少導致鏈結串列過長,搜尋效率低,需要將 buckets 內的節點分配到更多新的 buckets 上,確保負載更均勻地分散,這樣可以提升雜湊表操作的效率。
* 首先建立一個新的雜湊表,並加入更多的 buckets 。使新的雜湊表中每一個 buckets 連接到對應的舊節點。
* 將雜湊表指標指向新的雜湊表 ,保留舊有的雜湊表避免本來的 reader 發生錯誤。
* 等待一個 [grace period](https://hackmd.io/@sysprog/linux-rcu#%E5%AF%AC%E9%99%90%E6%9C%9F) 過後執行 Unzip 操作。
#### 閱讀論文〈[Resizable, Scalable, Concurrent Hash Tables via Relativistic Programming](https://www.usenix.org/legacy/event/atc11/tech/final_files/Triplett.pdf)〉
雜湊表為作業系統提供便利性,使得**平均存取與修改資料的時間為常數**。在並行處理中使用的雜湊表需要同步機制來維持內部資料的一致性。
現有的雜湊表主要利用**互斥鎖**(mutex lock)實作並行處理。多個執行緒同時嘗試取得同一個互斥鎖時,就會發生爭搶。這會導致執行緒等待,降低程式的執行效率。
**隨著處理器數量增加**,問題會更嚴重,因為會有更多執行緒同時競爭互斥鎖。這使得程式無法有效利用多個處理器,難以擴展。
現有一種可擴展的並行雜湊表解決方案是**讀取-複製-更新**(RCU,Read-Copy Update)。RCU 為並行程式提供了一種同步機制,對讀取器的開銷非常低。因此,RCU 特別適用於**讀取比寫入顯著多的資料結構**。
現有的基於 RCU 的雜湊表存在一個主要缺陷:**無法動態調整大小**。雜湊表 的效能與 hash buckets 的數量相關。**太少會導致效能低落,太多又會佔用過多記憶體**。系統需要一個**能根據需求自動調整大小**的雜湊表,以維持最佳效能。
**核心問題**:
* 動態調整大小的困難: 在不阻塞讀取操作的前提下,實作 RCU 雜湊表的動態調整大小是一個難題。
* 現有方案的不足: 過去的解決方案,如 DDDS,會在調整大小期間導致查找速度變慢,影響效能。
**論文目標**:
本論文介紹一種演算法,能夠在不阻塞或減慢並行查找的情況下,動態調整基於 RCU 的雜湊表大小。
利用 RCU 的 wait-for-readers 操作來實作動態調整大小,同時不影響讀取操作。
#### Relativistic Hash Tables
Relativistic Hash Table 是一種高**並行性**的雜湊表實作,它允許在調整大小的同時進行讀寫操作。
**核心概念:**
* **Imprecise hash chains:** 在調整大小過程中,雜湊鏈結串列可能暫時包含不屬於該 bucket 的節點,以避免資料複製和浪費記憶體。
* **整數倍調整:** 雜湊表的大小調整限制為 bucket 數量的整數倍,簡化了調整演算法。
* **RCU 同步:** 使用 RCU(Read-Copy Update)機制實作讀寫並行,避免讀取操作被調整大小阻塞。
**實作步驟:**
1. **分配新雜湊表:** 配置一個容量較小的新雜湊表。
2. **連結新舊 bucket:** 將新表中的每個 bucket 連結到舊表中對應的第一個桶。
3. **串接舊 bucket 的鏈結串列:** 將每個舊 bucket 的鏈結串列末端連接到下一個舊 bucket 的開頭。
5. **設定雜湊表大小:** 更新雜湊表的大小資訊,以反映新的 bucket 數量。
5. **發布新雜湊表:** 讓讀取操作可以開始使用新的雜湊表。
6. **等待 readers:** 使用 `wait-for-readers` 操作等待所有在舊雜湊表上進行的讀取操作完成。確保沒有新的 reader 會再存取舊雜湊表。
7. **回收舊雜湊表 :** 釋放舊雜湊表所佔用的記憶體空間。
### Relativistic hash tables Implementation
> 此部份將探究 rhashtable 的在 Linux 核心的實作。
#### rhashtable structure
首先看到 `rhashtable` 的結構:
```c
/**
* struct rhashtable - Hash table handle
* @tbl: Bucket table
* @key_len: Key length for hashfn
* @max_elems: Maximum number of elements in table
* @p: Configuration parameters
* @rhlist: True if this is an rhltable
* @run_work: Deferred worker to expand/shrink asynchronously
* @mutex: Mutex to protect current/future table swapping
* @lock: Spin lock to protect walker list
* @nelems: Number of elements in table
*/
struct rhashtable {
struct bucket_table __rcu *tbl;
unsigned int key_len;
unsigned int max_elems;
struct rhashtable_params p;
bool rhlist;
struct work_struct run_work;
struct mutex mutex;
spinlock_t lock;
atomic_t nelems;
};
```
* `tbl`:這是一個指向 `bucket_table` 結構的指標。`bucket_table` 儲存了雜湊表中的所有 buckets。
* `key_len`:是一個整數,表示雜湊函數所使用的鍵值的長度。
* `max_elems`:是一個整數,表示雜湊表中最大允許的元素數量。
* `p`:這是一個 `rhashtable_params` 結構,表示雜湊表所使用的參數。
* `rhlist`:這是一個布林值,表示是否為一個 `rhltable`。
:::info
rhltable:允許多個元素擁有**相同鍵值**的雜湊表
struct rhltable 是 Linux 核心用來實作雜湊表鏈結串列(relativistic hash list table)的資料結構。它繼承了雜湊表的所有功能,但允許在同一個 bucket 中儲存多個具有相同鍵值的元素。
```c
/**
* struct rhltable - Hash table with duplicate objects in a list
* @ht: Underlying rhtable
*/
struct rhltable {
struct rhashtable ht;
};
```
:::
* `run_work`:這是一個 `work_struct` 結構,用於執行擴張 (expansion) 或縮小 (shrink) 的 worker。
* `mutex`:互斥鎖,用於保護雜湊表避免發生同步存取導致資料不一致的問題。
* `lock`:這是一個 spinlock ,用於保護 walker list。
:::info
rhashtable_walker 結構體:雜湊表中,讀取操作可能會與 resize 同時發生。為了確保讀取操作的正確性,雜湊表引入了 walker 。
* list:一個 `list_head` 結構,這個鏈結串列記錄了所有正在進行讀取操作的 walkers。
* tbl:指向 `bucket_table` 結構的指標。 tbl 記錄了這個 walker 正在走訪的雜湊表。
```c
/**
* struct rhashtable_walker - Hash table walker
* @list: List entry on list of walkers
* @tbl: The table that we were walking over
*/
struct rhashtable_walker {
struct list_head list;
struct bucket_table *tbl;
};
```
:::
* `nelems`:表示雜湊表中目前元素的數量。
## TODO: 確保 [rhashtable](https://hackmd.io/@linjohn/rhashtable) 對應的程式碼可運作於 Linux v6.8
### 嘗試執行程式測試程式
#### 前期準備
* 檢查 Linux 核心版本
```shell
$ uname -r
6.8.0-31-generic
```
* 安裝核心標頭檔 linux-headers 套件
```shell
$ sudo apt install linux-headers-`uname -r`
```
* 更新套件清單
```shell
$ sudo apt update
```
* 確認 `linux-headers` 套件已正確安裝於開發環境
```shell
$ dpkg -L linux-headers-`uname -r` | grep "/lib/modules/.*/build"
```
* 得到以下輸出
```shell
/lib/modules/6.8.0-31-generic/build
```
### 測試程式碼
找到 Linux v6.8+ 版本的測試程式碼 [test_rhashtable.c](https://elixir.bootlin.com/linux/v6.8.12/source/lib/test_rhashtable.c) 並嘗試執行。
* 建立 `Makefile` 以編譯測試程式碼
```
obj-m := test_rhashtable.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
rm -rf *.o *.ko *.mod.* *.symvers *.order *.mod.cmd *.mod
```
* 掛載測試核心模組
```shell
$ sudo insmod test_rhashtable.ko
```
* 顯示核心資訊
```shell
$ sudo dmesg
```
這是一段針對 rhashtable 進行的測試程式,主要測試其效能和功能。測試內容如下:
**基本操作測試:**
- 測試新增、刪除和走訪鍵值對的效能。
- 重複測試四次,每次新增 50000 個鍵值,然後刪除,並記錄測試時間。
**最大容量測試:**
- 測試 rhashtable 是否能正確處理超過最大容量的情況,確保不會發生錯誤或資料遺失。
**重複插入測試:**
- 測試 rhashtable 如何處理插入重複的鍵值,驗證是否能正確管理具有相同鍵的多個值。
**多執行緒並行測試:**
- 模擬 10 個執行緒同時存取 rhashtable,測試 rhashtable 在並行環境下的效能和正確性。
**rhltable 鏈結串列測試:**
- 測試與 rhashtable 相關的 rhltable 鏈結串列功能,包括新增、刪除和隨機操作,確保其功能正常運作。
**測試結果:**
- 平均測試時間:每次新增 50000 個鍵值對並刪除的平均時間約為 49.15 毫秒。
- 最大容量測試:rhashtable 能正確處理超過最大容量的情況。
- 重複插入測試:rhashtable 能正確處理重複的鍵,將新值新增至鏈結串列中。
- 多執行緒並行測試:10 個執行緒並行測試成功,rhltable 鏈結串列測試回傳 0,表示未發生錯誤。
```c
[ 327.908633] Running rhashtable test nelem=8, max_size=0, shrinking=0
[ 327.908640] Test 00:
[ 327.908712] Adding 50000 keys
[ 327.928050] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[ 327.939587] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[ 327.939596] Deleting 50000 keys
[ 327.955622] Duration of test: 46904488 ns
[ 327.955641] Test 01:
[ 327.955729] Adding 50000 keys
[ 327.976176] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[ 327.986102] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[ 327.986109] Deleting 50000 keys
[ 328.002335] Duration of test: 46601311 ns
[ 328.002353] Test 02:
[ 328.002470] Adding 50000 keys
[ 328.024717] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[ 328.035320] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[ 328.035331] Deleting 50000 keys
[ 328.050550] Duration of test: 48074468 ns
[ 328.050568] Test 03:
[ 328.050668] Adding 50000 keys
[ 328.072286] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[ 328.082694] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[ 328.082703] Deleting 50000 keys
[ 328.098314] Duration of test: 47642464 ns
[ 328.106790] test if its possible to exceed max_size 8192: no, ok
[ 328.106876] Average test time: 47305682
[ 328.106879] test inserting duplicates
[ 328.106888]
---- ht: ----
bucket[1] -> [[ val 21 (tid=1) ]] -> [[ val 1 (tid=0) ]]
-------------
[ 328.106900]
---- ht: ----
bucket[1] -> [[ val 21 (tid=1) ]] -> [[ val 1 (tid=2), val 1 (tid=0) ]]
-------------
[ 328.106908]
---- ht: ----
bucket[1] -> [[ val 21 (tid=1) ]] -> [[ val 1 (tid=0) ]]
-------------
[ 328.106917]
---- ht: ----
bucket[1] -> [[ val 21 (tid=1) ]] -> [[ val 1 (tid=2), val 1 (tid=0) ]]
-------------
[ 328.106921] Testing concurrent rhashtable access from 10 threads
[ 328.350688] test 3125 add/delete pairs into rhlist
[ 328.381459] test 3125 random rhlist add/delete operations
[ 328.410140] Started 10 threads, 0 failed, rhltable test returns 0
```
* 卸載核心模組
```shell
$ sudo rmmod test_rhashtable
```
### v6.8+ 與 v5.15+ 比較
#### `workqueue_types.h`
將 `workqueue_types.h` 這個標頭檔從 `workqueue.h` 中獨立出來。減少 `sched.h`(Linux 核心的排程相關標頭檔)的依賴性,進一步減少` rhashtable-types.h` 對 `workqueue.h` 的依賴。
> commit [b2fa844](https://github.com/torvalds/linux/commit/b2fa8443db320c4873feca2588b957439e350890)
#### Allow rhashtable to be used from irq-safe contexts
在 commit [e47877c](https://github.com/torvalds/linux/commit/e47877c7aa821c413b45e05f804819579bdfa1a3) 中,主要目的是允許 rhashtable 在 IRQ-safe contexts 中使用。原始程式碼中使用 `local_bh_disable()` 和 `local_bh_enable()` 來保護臨界區間的資料,這表示 rhashtable 原本的使用情境只能防止被 Bottom Half 中斷。
以下是修改內容:
```diff
- static inline void rht_lock(struct bucket_table *tbl,
- struct rhash_lock_head __rcu **bkt)
+ static inline unsigned long rht_lock(struct bucket_table *tbl,
+ struct rhash_lock_head __rcu **bkt)
{
- local_bh_disable();
+ unsigned long flags;
+ local_irq_save(flags);
bit_spin_lock(0, (unsigned long *)bkt);
lock_map_acquire(&tbl->dep_map);
+ return flags;
}
```
```diff
static inline void rht_unlock(struct bucket_table *tbl,
- struct rhash_lock_head __rcu **bkt)
+ struct rhash_lock_head __rcu **bkt,
+ unsigned long flags)
{
lock_map_release(&tbl->dep_map); // 釋放鎖映射的互斥鎖
bit_spin_unlock(0, (unsigned long *)bkt); // 釋放桶的位元自旋鎖
- local_bh_enable();
+ local_irq_restore(flags);
}
```
**改動解釋:**
* **使用 `local_irq_save(flags)` 和 `local_irq_restore(flags)`:**
* 取代了原本的 `local_bh_disable()` 和 `local_bh_enable()`。
* `local_bh_disable()` 只關閉 bottom half 的中斷,而 `local_irq_save(flags)` 能夠保存當前的中斷狀態,並關閉所有中斷,確保在 `rht_lock` 和 `rht_unlock` 之間的臨界區段不會被任何中斷打斷,從而實作 IRQ-safe 的同步機制。
* **傳遞中斷狀態旗標 `flags`:**
* `rht_lock` 返回保存的中斷狀態 `flags`,`rht_unlock` 接收這個旗標,以確保在 `rht_unlock` 中能夠正確恢復 `rht_lock` 之前的中斷狀態,包括中斷的開啟或關閉狀態。
### 在 Ubuntu Linux 24.04 執行 [rcuhashbash-resize](https://github.com/SHChang-Anderson/rcuhashbash/)
:::danger
解釋 rcuhashbash 的原理,特別是其 RCU 使用的考量因素。
:::
#### 編譯並執行
下載程式碼
```shell
$ git clone https://github.com/chiangkd/rcuhashbash.git
```
編譯程式碼
```shell
make
```
加入 [Cppcheck](https://cppcheck.sourceforge.net/) 之後提交時出現以下錯誤:
```c
rcuhashbash-resize.c:213:21: style: Condition '!&entry->node' is always false [knownConditionTrueFalse]
if (!&entry->node)
^
rcuhashbash-resize.c:220:21: style: Condition '&entry->node' is always true [knownConditionTrueFalse]
if (&entry->node)
^
rcuhashbash-resize.c:150:35: style: Variable 'new_table' can be declared as pointer to const [constVariablePointer]
struct rcuhashbash_table *new_table;
^
rcuhashbash-resize.c:208:33: error: Uninitialized variable: entry->value [uninitvar]
if ((entry->value & mask2) != (entry_prev->value & mask2))
```
首先檢查程式碼, `if (!&entry->node)` , `&entry->node` 取得 `entry` 結構體中 `node` 成員的記憶體地址。由於若`entry` 存在,則 `node` 成員的記憶體地址永遠不可能為空。
除此之外,`old_table->buckets[i].first = &entry->node;`,若 `entry == NULL` 對 `entry->node` 進行取成員操作,可能造成未定義的行為。
```c
hlist_for_each_entry(entry, &old_table->buckets[i], node) {
if ((entry->value & mask2) != (entry_prev->value & mask2))
break;
entry_prev = entry;
}
old_table->buckets[i].first = &entry->node;
if (!&entry->node)
continue;
```
因此我對程式碼進行以下修正:
```diff
- old_table->buckets[i].first = &entry->node;
- if (!&entry->node)
+ if (!entry) {
+ old_table->buckets[i].first = NULL;
+ continue;
+ }
+ old_table->buckets[i].first = &entry->node;
moved_one = true;
hlist_for_each_entry(entry, &old_table->buckets[i], node)
if ((entry->value & mask2) == (entry_prev->value & mask2))
break;
+ if (!entry) {
+ entry_prev->node.next = NULL;
+ continue;
+ }
entry_prev->node.next = &entry->node;
- if (&entry->node)
```
-----
```c
rcuhashbash-resize.c:150:35: style: Variable 'new_table' can be declared as pointer to const [constVariablePointer]
```
這個訊息是一個程式碼風格建議,建議將 `new_table` 變數宣告為指向 `const` 的指標。
這麼做的原因是,在 rcuhashbash_resize 函式中,new_table 指標所指向的 hashtable 內容在建立後就不會再被修改。使用 const 可以明確表示這一點,提高程式碼的可讀性和安全性。
----
```c
rcuhashbash-resize.c:208:33: error: Uninitialized variable: entry->value [uninitvar]
```
這是一個錯誤訊息,表示在使用 `entry->value` 時,該變數尚未被初始化。這可能會導致未定義的行為,需要修正。
因此在 `hlist_for_each_entry` 迴圈之前,應該對 `entry_prev` 指標進行初始化。
檢查程式碼發現 `entry->value` 的初始話被定義在 `rcuhashbash_init` 當中,不會出現未定義的問題,因此我使用 `--suppress=uninitvar:rcuhashbash-resize.c` 抑制錯誤訊息。
確認程式的正確性,能夠正確的進行量測。
```c
[ 1347.255348] rcuhashbash summary: test=rcu readers=5 resize=true
rcuhashbash summary: entries=65536 shift1=13 (8192 buckets) shift2=14 (16384 buckets)
rcuhashbash summary: reads: 92811811 primary hits, 0 slowpath primary hits, 0 secondary hits, 0 misses
rcuhashbash summary: resizes: 78
rcuhashbash summary: PASS
rcuhashbash summary: total time: 3035726612 ns
[ 1347.255368] rcuhashbash done
```
> commit [8e2e509](https://github.com/SHChang-Anderson/rcuhashbash/commit/8e2e509dee46f0f0aed2b909a086f1ad16205d09)
### 重現實驗結果
#### 實驗參數
參照 [rhashtable](https://hackmd.io/@linjohn/rhashtable) 的實驗方式分別調整三中不同演算法(rcu, ddds, rwlock)與 reader 的數量,比較每秒 lookup hit 的次數,而 resizer 在過程中會不斷調整 table 的大小,從 $2^{13}$ 擴張到 $2^{14}$ 再縮小回去。
```c
static char *test = "rcu"; /* Hash table implementation to benchmark */
static int readers = 1;
static bool resize = true;
static u8 shift1 = 13;
static u8 shift2 = 14;
static unsigned long entries = 65536;
```
#### 實驗結果
三中不同演算法,依序執行 1,2,4, 8, 16 個 readers
利用 shell script 固定每次測試時間為 10 秒
* **固定 table 大小**
固定大小在 $2^{13}$, 比較不同演算法的查找次數
![](https://hackmd.io/_uploads/HJq6B2UU0.png)
* **開啟 resize**
resize thread 會不斷在 $2^{13}$ 到 $2^{14}$ 之間調整雜湊表大小,比較在 resizing 時不同演算法的查找次數
![](https://hackmd.io/_uploads/HyIUvZvI0.png)
實驗結果與 [rhashtable](https://hackmd.io/@linjohn/rhashtable) 相仿 rhashtable 在測試中都提供了線性可擴展的查找性能。
在固定大小時,論文演算法與 DDDS 差不多。
在 resizing 時,論文演算法明顯優於 DDDS。
### 現行版本 rhashtable 與測試程式碼不同之處
閱讀現行版本的 [rashtable.h](https://elixir.bootlin.com/linux/v6.8-rc1/source/include/linux/rhashtable.h) 程式碼時,我注意到在 rhashtble 的 bucket 結構體中存在 `future_tbl` ,從註解中可以得知, `future_tbl` 是在 resize 期間額外的 hashtable ,而在 [rcuhashbash](https://github.com/SHChang-Anderson/rcuhashbash/) 中並沒有出現這樣的結構。
```diff
/**
* struct bucket_table - Table of hash buckets
* @size: Number of hash buckets
* @nest: Number of bits of first-level nested table.
* @rehash: Current bucket being rehashed
* @hash_rnd: Random seed to fold into hash
* @walkers: List of active walkers
* @rcu: RCU structure for freeing the table
+* @future_tbl: Table under construction during rehashing
* @ntbl: Nested table used when out of memory.
* @buckets: size * hash buckets
*/
struct bucket_table {
unsigned int size;
unsigned int nest;
u32 hash_rnd;
struct list_head walkers;
struct rcu_head rcu;
+ struct bucket_table __rcu *future_tbl;
struct lockdep_map dep_map;
struct rhash_lock_head __rcu *buckets[] ____cacheline_aligned_in_smp;
};
```
使用 git log 找到最初使用到這樣結構的版本,在 commit [97def1](https://github.com/torvalds/linux/commit/97defe1ecf868b8127f8e62395499d6a06e4c4b1) 可以找到相關的更動以及說明。
這個 patch 主要介紹了針對 RCU hash table 的幾項改進:
**1. Spinlock 陣列保護 Bucket 操作:**
```diff
/**
* struct bucket_table - Table of hash buckets
* @size: Number of hash buckets
* @locks_mask: Mask to apply before accessing locks[]
* @locks: Array of spinlocks protecting individual buckets
* @buckets: size * hash buckets
*/
struct bucket_table {
struct bucket_table {
size_t size;
size_t size;
unsigned int locks_mask;
+ spinlock_t *locks;
struct rhash_head __rcu *buckets[];
struct rhash_head __rcu *buckets[];
};
};
```
* 引入 spinlock 陣列來保護 bucket 的修改。
* 允許插入和刪除與 resize 並行。
**2. 擴展/縮小時的 Future Table:**
* 在擴展或縮小時,分配的新 bucket 表在調整大小過程開始時立即公開為 **future table** 。
* 查找、刪除和插入操作將短暫使用兩個 table。
* 在 RCU 寬限期結束且舊表到新表的初始鏈接完成後,future table 成為主要的 table。
```c
void *rhashtable_lookup(struct rhashtable *ht, const void *key)
{
const struct bucket_table *tbl, *old_tbl;
struct rhash_head *he;
u32 hash;
BUG_ON(!ht->p.key_len);
rcu_read_lock();
old_tbl = rht_dereference_rcu(ht->tbl, ht);
tbl = rht_dereference_rcu(ht->future_tbl, ht);
hash = key_hashfn(ht, key, ht->p.key_len);
restart:
rht_for_each_rcu(he, tbl, rht_bucket_index(tbl, hash)) {
if (memcmp(rht_obj(ht, he) + ht->p.key_offset, key,
ht->p.key_len))
continue;
rcu_read_unlock();
return rht_obj(ht, he);
}
if (unlikely(tbl != old_tbl)) {
tbl = old_tbl;
goto restart;
}
rcu_read_unlock();
return NULL;
}
```
在查找的程式碼中可以觀察到,會對兩個雜湊表進行查找,因此可得知,此舉的目的使得查找操作可能與雜湊表修改和大小調整同時發生。
若我們對照這兩個版本之間的程式碼:
在更新前的插入程式碼中,從註解我們可以得知:
> The caller must ensure that no concurrent table mutations occur. It is however valid to have concurrent lookups if they are RCU protected.
使用者在進行插入操作時,必須確保沒有同時發生的雜湊表大小調整操作。然而,如果查找操作受到 RCU 保護,則允許並行查找。
```c
void rhashtable_insert(struct rhashtable *ht, struct rhash_head *obj)
{
//...
rcu_read_lock();
tbl = rht_dereference_rcu(ht->future_tbl, ht);
hash = head_hashfn(ht, tbl, obj);
lock = bucket_lock(tbl, hash);
spin_lock_bh(lock);
RCU_INIT_POINTER(obj->next, tbl->buckets[hash]);
rcu_assign_pointer(tbl->buckets[hash], obj);
spin_unlock_bh(lock);
//...
}
```
對照更新後的 `rhashtable_insert` 以及 `rhashtable_expand` 程式碼,可以發現每個 bucket 都受到 spinlock 的保護,使得針對不同 bucket 的插入和刪除操作可以並行。
> Make insertions go into the new, empty table right away. Deletions and lookups will be attempted in both tables until we synchronize.
The synchronize_rcu() guarantees for the new table to be picked up
插入操作時會直接插入 `future table` 當中, `synchronize_rcu()` 能確保 `future table` 被採用,因此在進行 `unzip` 操作時,不會有新的插入操作進入原始的 `hashtable`。
```c
int rhashtable_expand(struct rhashtable *ht)
{
//...
/* Make insertions go into the new, empty table right away. Deletions
* and lookups will be attempted in both tables until we synchronize.
* The synchronize_rcu() guarantees for the new table to be picked up
* so no new additions go into the old table while we relink.
*/
rcu_assign_pointer(ht->future_tbl, new_tbl);
synchronize_rcu();
/* For each new bucket, search the corresponding old bucket for the
* first entry that hashes to the new bucket, and link the end of
* newly formed bucket chain (containing entries added to future
* table) to that entry. Since all the entries which will end up in
* the new bucket appear in the same old bucket, this constructs an
* entirely valid new hash table, but with multiple buckets
* "zipped" together into a single imprecise chain.
*/
for (new_hash = 0; new_hash < new_tbl->size; new_hash++) {
old_hash = rht_bucket_index(old_tbl, new_hash);
old_bucket_lock = bucket_lock(old_tbl, old_hash);
spin_lock_bh(old_bucket_lock);
rht_for_each(he, old_tbl, old_hash) {
if (head_hashfn(ht, new_tbl, he) == new_hash) {
link_old_to_new(new_tbl, new_hash, he);
break;
}
}
spin_unlock_bh(old_bucket_lock);
}
//...
}
```
#### 在 Linux v6.8 中的 bucket lock 實作
**`rhash_lock_head`**
**`struct rhash_lock_head {}` 的作用**
:::danger
注意用語:
* access 是「存取」,而非「訪問」(visit)
避免使用 ChatGPT 來機器翻譯。
:::
這個結構體,沒有任何成員變量,由於這個結構體沒有內容,開發者不能直接使用它來<s>訪問</s> 存取 bucket 的內容,必須先將其轉換為正確的<s>指針</s> 指標 類型,這樣可以避免誤操作。
```c
/*
* Objects in an rhashtable have an embedded struct rhash_head
* which is linked into as hash chain from the hash table - or one
* of two or more hash tables when the rhashtable is being resized.
* The end of the chain is marked with a special nulls marks which has
* the least significant bit set but otherwise stores the address of
* the hash bucket. This allows us to be sure we've found the end
* of the right list.
* The value stored in the hash bucket has BIT(0) used as a lock bit.
* This bit must be atomically set before any changes are made to
* the chain. To avoid dereferencing this pointer without clearing
* the bit first, we use an opaque 'struct rhash_lock_head *' for the
* pointer stored in the bucket. This struct needs to be defined so
* that rcu_dereference() works on it, but it has no content so a
* cast is needed for it to be useful. This ensures it isn't
* used by mistake with clearing the lock bit first.
*/
struct rhash_lock_head {};
```
在 rhashtable 中,每個物件都內嵌了一個 `struct rhash_head`。
hash chain 的末端使用一個特殊的`nulls` 標記來表示。這個標記的 least significant bit 被設置為 1,但其餘儲存 bucket 的地址。
:::danger
注意用語:
* thread 是「執行緒」
* bit 是「位元」,而非「位」,後者是量詞
:::
bucket 中的最低有效位(BIT(0))被用作鎖定位(lock bit)。在對 hash table 進行任何更改之前,必須先 atomic 設置這個<s>位</s> 位元 。這是為了確保在多<s>線程</s> 執行緒 環境下,對 hash table 的修改操作不會相互干擾。
---
**`rht_lock`**
```c
static inline unsigned long rht_lock(struct bucket_table *tbl,
struct rhash_lock_head __rcu **bkt)
{
unsigned long flags;
local_irq_save(flags);
bit_spin_lock(0, (unsigned long *)bkt);
lock_map_acquire(&tbl->dep_map);
return flags;
}
```
**`rht_unlock`**
```c
static inline void rht_unlock(struct bucket_table *tbl,
struct rhash_lock_head __rcu **bkt,
unsigned long flags)
{
lock_map_release(&tbl->dep_map);
bit_spin_unlock(0, (unsigned long *)bkt);
local_irq_restore(flags);
}
```
**鎖定原理**
1. **利用最低有效位 (LSB)**:rhashtable 的設計利用指標的 least significant bit,LSB 作為鎖標記。
- 在正常的指標中,LSB 總是 0。
- 當一個 bucket 被鎖定時,其對應指針的 LSB 會被設置為 1。
2. **`bit_spin_lock` 函式**:程式碼中的 `bit_spin_lock` 函式用於指定<s>指針</s> 指標的 LSB 設置為 1,從而實作 bucket 的鎖定。
:::danger
注意用語
:::
### resize 和插入操作並行
#### 加入插入操作
> commit [d3be3bd](https://github.com/SHChang-Anderson/rcuhashbash/commit/d3be3bd0c438c01091131deb1f513140e672ce86)
在過去的的 [rcuhashbash](https://github.com/chiangkd/rcuhashbash) 專案測試程式中,並沒有加入插入功能。因此我修改去年的測試程式,加入插入操作,並使用 mutex lock 避免插入操作與 resize 操作並行出現問題,也作為接下來的並行測試作為基準。
在 commit [d3be3bd](https://github.com/SHChang-Anderson/rcuhashbash/commit/d3be3bd0c438c01091131deb1f513140e672ce86) 中,我實作了 `rhashtable_insert` 函式,並將其整合到 rcuhashbash 中。為了方便控制插入操作,我新增了 `insert` 切換選項和 `insertct` 數量限制。此外,為了確保在多執行緒環境下的安全插入,引入了 `table_mutex` 互斥鎖,以防止並行修改表格。最後,建立了一個 `rcuhashbash_insert_thread` 插入執行緒,以便進行插入效能測試。並在插入之後進行查找,確認插入是否成功。
#### 新增 Bucket lock 以及 Future Table
> commit [d2f18d5](https://github.com/SHChang-Anderson/rcuhashbash/commit/d2f18d5490a779c491168d50e3952efa36308342)
在 [rcuhashbash](https://github.com/SHChang-Anderson/rcuhashbash) 中,我們將參考 Linux 核心 [lib/rhashtable.c](https://github.com/torvalds/linux/blob/master/lib/rhashtable.c) 中 commit [97def1](https://github.com/torvalds/linux/commit/97defe1ecf868b8127f8e62395499d6a06e4c4b1) 的方法,為 rhashtable 增加 Bucket lock 和 Future Table 功能,以便能夠並行處理不同 bucket 的修改和插入操作。
首先,我們需要在原始的 `rcuhashbash_table` 結構中加入一個 `spinlock_t` 型態的指標。
```diff
struct rcuhashbash_table {
unsigned long mask;
+ spinlock_t *locks;
struct hlist_head buckets[];
};
```
接下來,加入一個 `bucket_lock` 函式。這個函式會接受一個 `rcuhashbash_table` 和一個 `hash` 作為參數,並回傳該 bucket 的鎖。這樣,在對特定 bucket 進行操作時,就能夠取得該 bucket 的鎖,以確保插入或修改操作的安全性。
```c
static spinlock_t *bucket_lock(const struct rcuhashbash_table *tbl, u32 hash)
{
return &tbl->locks[hash];
}
```
在 [rcuhashbash](https://github.com/SHChang-Anderson/rcuhashbash) 中加入 `rcuhashbash_try_lookup2` 函式。此函式會優先在 `future_table` 中查找資料,若未找到,則會在舊的 hash table 中再次查找。此外,插入操作也會優先在 `future_table` 中進行,以確保最新的資料能被快速查找到。
```c
static bool rcuhashbash_try_lookup2(struct rcuhashbash_table *t,
struct rcuhashbash_table *newt,
u32 value)
{
struct rcuhashbash_entry *entry;
struct hlist_node *node __attribute__((unused));
restart:
hlist_for_each_entry_rcu(entry, &newt->buckets[value & newt->mask], node) {
if (entry->value == value) {
return true;
}
}
if (unlikely(newt != t)) {
newt = t;
goto restart;
}
return false;
}
```
在這個函式中,`rcuhashbash_try_lookup2` 會首先在 `newt`(即 `future_table`)中進行查找。如果未找到,則會切換到舊的雜湊表 (`t`) 再次查找
在擴展/縮小的過程中需要加入 bucket lock。值得注意的是,在原始的實作當中,縮小操作是 in-place,而在 Linux 核心的實作當中,在 resize 的過程會短暫地使用到兩個雜湊表。因此,我必須修改此部分的程式碼,以適用於 bucket lock。
```c
if (mask2 < table->mask) {
/* Shrink. */
//..
/* Assume (and assert) that __krealloc shrinks in place. */
new_table = krealloc(table, sizeof(*table) + len2 * sizeof(table->buckets[0]), GFP_KERNEL);
//..
}
```
最後,在 unzip 的過程中,針對 future table 正在執行 unzip 的 bucket 進行上鎖,確保 resize 的安全性。
### rhashtable 並行插入實驗
> 本實驗旨在評估 rhashtable 在 resize 並行情況下,不同插入策略對其性能的影響。
比較以下兩種插入策略在 rhashtable 中的效能:
1. **hash table mutex lock**:在插入操作時,對整個哈希表加鎖,確保所有插入操作串行執行。
2. **bucket lock**:在插入操作時,僅對相關的 bucket 加鎖,允許不同 bucket 的插入操作並行執行,提高並行度。
**實驗方法**
1. **實驗環境**:
- **固定 readers 數量**:設定固定數量的 readers 。
- **固定測試時間**:設定固定的測試時間,比較兩種方法在相同時間內能完成的插入次數。
2. **實驗流程**:
- 將實驗分為兩組,每組執行相同的測試時間。
- 每組實驗中,固定數量的讀者線程持續對 rhashtable 進行讀取操作。
- 在兩組實驗中,各自使用不同的插入策略,並以固定的時間間隔 (10 ms) 持續進行插入操作,直到測試時間結束。
3. **實驗指標**:
- **成功插入次數**:統計在測試時間內成功插入到 rhashtable 中的元素數量,作為評估不同插入策略的並行效率的主要指標。
使用 git checkout 找到過去版本使用 hash table mutex lock 的程式碼,加入目前專案當中,命名為 `rcuhashbash-resize_wob_lock.c` 為接下來的測試做準備。
**測試腳本**
`do_insert_measurement.sh`
```bash
sudo dmesg -C
for((sleept = 0; sleept < 5; sleept = sleept + 1))
do
sudo insmod ${TARGET_MODULE[0]}.ko \
shift1=13 \
resize=true \
insert=true
sleep ${sleep_time[sleept]}
sudo rmmod ${TARGET_MODULE[0]}
done
mapfile -t inserts < <(sudo dmesg | grep "rcuhashbash summary: inserts" | awk '{print $4}')
for i in "${!inserts[@]}"; do
echo "${sleep_time[i]} ${inserts[i]}"
done > output_bucket_lock.txt
```
**產生的數據圖腳本**
`script.gp`
```bash
set title "Insertion Performance: Bucket Lock vs Table Mutex"
set xlabel "Time (seconds)"
set ylabel "Insertions"
set terminal png font "Times_New_Roman, 12"
set key right
set output "insertion_performance_comparison.png"
plot \
"output_bucket_lock.txt" using 1:2 with linespoints linewidth 2 title "Bucket Lock", \
"output_table_mutex.txt" using 1:2 with linespoints linewidth 2 title "Table Mutex" \
```
**實驗參數**
分別編譯 rcuhashbash-resize 和 rcuhashbash-resize_wob_lock 模組。
執行測試:
* 針對每個模組,依序設定測試時間為 5, 10, 15, 20, 25 秒。
* 在每個測試時間下,執行測試腳本,同時開啟插入和 resize 操作。
* 記錄每個測試時間下,兩個模組各自完成的插入次數。
```bash!
TARGET_MODULE=(rcuhashbash-resize rcuhashbash-resize_wob_lock)
sleep_time=(5 10 15 20 25)
tests="rcu"
readers=1
insert=true
resize=true
```
**執行 `do_insert_measurement.sh`**
```shell
bash do_insert_measurement.sh
```
### 實驗結果
> 比較兩種方法在不同時間的插入次數
* **使用 bucket lock**
|時間 (秒) |插入次數|
|-|-|
|5| 414|
|10| 828|
15| 1230
20 |1650
25| 2055
* **使用 table mutex**
|時間 (秒) |插入次數|
|-|-|
5| 83
10| 160
15 |240
20 |348
25 |441
**實驗圖表**
![image](https://hackmd.io/_uploads/HkiFQos8C.png)
**Resize 次數比較**
> 觀察並行插入是否對 resize 造成影響
* 使用 **bucket lock** 但不使用插入功能
|時間 (秒) |resize 次數|
|-|-|
5| 91
10| 200
15| 287
20| 411
25| 499
* 使用 **bucket lock** 以固定的時間間隔 (10 ms) 持續進行插入操作
|時間 (秒) |resize 次數|
|-|-|
5| 92
10| 199
15 |308
20| 410
25| 508
**實驗圖表**
![image](https://hackmd.io/_uploads/HyL2PjoLC.png)
* 使用 **table mutex** 但不使用插入功能
|時間 (秒) |resize 次數|
|-|-|
5| 91
10| 179
15| 281
20| 360
25| 488
* 使用 **table mutex** 以固定的時間間隔 (10 ms) 持續進行插入操作
|時間 (秒) |resize 次數|
|-|-|
5 |93
10 |181
15 |281
20 |377
25 |469
**實驗圖表**
![image](https://hackmd.io/_uploads/BklB7AiIC.png)
:::danger
如何解讀上方實驗結果?與最初論文描述的行為相符嗎?
:::
實驗結果顯示,在各種測試時間下,使用 bucket lock 對特定的 bucket 進行臨界區間的控制,能在單位時間內完成更多次插入操作。進一步結合 resize 次數的比較實驗,我們可以得出以下結論:
1. **Bucket lock 提升插入效率:** 相比於 mutex lock 鎖定整個雜湊表,bucket lock 能夠實作更細緻的鎖定,允許不同 bucket 的插入操作並行進行。這有效降低了鎖競爭,從而提高了插入操作的吞吐量。
2. **Bucket lock 有助於 resize 並行:** 在 hash table 需要擴展或縮小時,bucket lock 允許插入操作與 resize 操作並行進行,在 resize 過程中,新的插入操作不會被完全阻塞,從而提高了整體的效能。
**總結:**
綜合實驗結果,bucket lock 在 rhashtable 的插入操作中表現出明顯的優勢。它不僅能提高插入效率,還能降低 resize 成本。