---
tags: linux kernel, linux2022
---
# [rhashtable](https://elixir.bootlin.com/linux/latest/source/include/linux/rhashtable.h) 研究
contributed by < [linjohnss](https://github.com/linjohnss) >
> [GitHub: rcuhashbash](https://github.com/linjohnss/rcuhashbash)
## 目標
Linux 核心提供一套 Resizable, Scalable, Concurrent Hash Table,名為 `rhashtable` (見 [include/linux/rhashtable.h](https://elixir.bootlin.com/linux/latest/source/include/linux/rhashtable.h) 及 [lib/rhashtable.c](https://elixir.bootlin.com/linux/latest/source/lib/rhashtable.c)),描述於論文〈[Resizable, Scalable, Concurrent Hash Tables via Relativistic Programming](https://www.usenix.org/legacy/event/atc11/tech/final_files/Triplett.pdf)〉。摘譯 LWN 文章,並佐以原論文,描述 rhashtable 何以落實高效的並行雜湊表,搭配撰寫 Linux 核心模組作為試驗
## [Relativistic hash tables, part 1: Algorithms](https://lwn.net/Articles/612021/)
### hash table 在 Linux 的應用場景
在 Linux 的 file system 中,利用 hash table 儲存 mount 的 root dentry ,因此 `lookup_mount` 可以利用 `hash_get` 取得 mount 的 root dentry
```c
/*
* find the first mount at @dentry on vfsmount @mnt.
* call under rcu_read_lock()
*/
struct mount *__lookup_mnt(struct vfsmount *mnt, struct dentry *dentry)
{
struct hlist_head *head = m_hash(mnt, dentry);
struct mount *p;
hlist_for_each_entry_rcu(p, head, mnt_hash)
if (&p->mnt_parent->mnt == mnt && p->mnt_mountpoint == dentry)
return p;
return NULL;
}
```
#### data compress
Linux 核心支援多種資料壓縮的方法,而 hash table 在其中扮演重要的角色。hash table 儲存 encode 過後的片段資料,在進行資料壓縮過程中,可以用快速的找出重複的資料
在 `linux/lib/842/842_compress.c` 中,利用 hash table 找尋是否有重複的 `index`
```c
#define find_index(p, b, n) ({ \
struct sw842_hlist_node##b *_n; \
p->index##b[n] = INDEX_NOT_FOUND; \
hash_for_each_possible(p->htable##b, _n, node, p->data##b[n]) { \
if (p->data##b[n] == _n->data) { \
p->index##b[n] = _n->index; \
break; \
} \
} \
p->index##b[n] >= 0; \
})
```
### rhashtable 解決的問題
hash table 雖然相較於 linked list 能更快速的搜索目標,但需要避免碰撞發生造成效能損失。為了應付此問題,除了改進 hash function,對於 hash table 的大小估計同樣重要,太小的會成性能損失;太大會造成空間的浪費。為 hash table 選擇正確的大小並不容易,在 Linux 核心中有許多 hash table 其大小是在系統初始化時進行簡單的估計。然而,儘管猜測是完美的,工作負載也會隨時間而變化。此時,一個 resizable 的 hash table 即可解決此問題。
但是若要同時達到 concurrent 與 resizable,不對 hash table 進行 blocking 是很難達成的。在 2010 年,Josh Triplett、Paul McKenney 和 Jonathan Walpole 在 USENIX ATC 發表一篇名為〈[Resizable, Scalable, Concurrent Hash Tables via Relativistic Programming](https://www.usenix.org/legacy/event/atc11/tech/final_files/Triplett.pdf)〉 的論文,描述了該問題解決方案。
### rhashtable 在 Linux 核心的應用場景
在 Linux 核心的網路模組中,RDS 使用 rhashtable 儲存 ip 地址與 port
`linux/net/rds/bind.c`
```c
static inline void __rds_create_bind_key(u8 *key, const struct in6_addr *addr,
__be16 port, __u32 scope_id)
{
memcpy(key, addr, sizeof(*addr));
key += sizeof(*addr);
memcpy(key, &port, sizeof(port));
key += sizeof(port);
memcpy(key, &scope_id, sizeof(scope_id));
}
```
### 論文〈Resizable, Scalable, Concurrent Hash Tables via Relativistic Programming〉
Relativistic hash table 與普通 hash table 類似,同樣都是由包含 link list 的 bucket 組成。
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">Bucket</TD></TR>
<TR><TD PORT="c">Bucket</TD></TR>
<TR><TD PORT="d">...</TD></TR>
</TABLE>>];
{
subgraph cluster_treenode{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=green];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=green];
ge[shape=none, label="..."];
gr->g1->g2->g3->ge;
br[label="", style=filled,fillcolor=blue];
b1[label="", style=filled,fillcolor=blue];
b2[label="", style=filled,fillcolor=blue];
b3[label="", style=filled,fillcolor=blue];
be[shape=none, label="..."];
br->b1->b2->b3->be;
}
}
table->a:a
a:c:e->br;
a:b:e->gr;
}
```
#### Table shrinking
若發現目前的 hash table 的空間太大,希望能縮小 hash table
1. Initial state:
將初始狀態分為 odd 與 even 兩種 bucket,odd 為奇數 reader 可以存取,even 為偶數 reader 可以存取。
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">Bucket</TD></TR>
<TR><TD PORT="c">Bucket</TD></TR>
<TR><TD PORT="d">...</TD></TR>
</TABLE>>];
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=green];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=green];
ge[shape=none, label="..."];
gr->g1->g2->g3->ge;
br[label="", style=filled,fillcolor=blue];
b1[label="", style=filled,fillcolor=blue];
b2[label="", style=filled,fillcolor=blue];
b3[label="", style=filled,fillcolor=blue];
be[shape=none, label="..."];
br->b1->b2->b3->be;
}
}
table->a:a
a:c:e->br;
a:b:e->gr;
}
```
2. Initialize new buckets:
分配一個新的 shrinking hash table,連接到對應的舊 node(指向具有匹配 index 的第一項),並將其儲存在 writer 的 local memory 中,其它 reader 無法存取
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
// new_table [shape=none, label="new table"]
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
<TR><TD PORT="d">...</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=green];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=green];
ge[shape=none, label="..."];
gr->g1->g2->g3->ge;
br[label="", style=filled,fillcolor=blue];
b1[label="", style=filled,fillcolor=blue];
b2[label="", style=filled,fillcolor=blue];
b3[label="", style=filled,fillcolor=blue];
be[shape=none, label="..."];
br->b1->b2->b3->be;
}
}
table->a:a;
// new_table->b:a;
b:b:e->gr:n;
a:c:e->br;
a:b:e->gr;
}
```
3. Link old chains:
將奇數 bucket 的 tail 接到偶數 bucket,因此在 reader 存取奇數 bucket 時,也會存取到偶數的部份。查找的結果仍然會是正確的,但這會導致需要走訪更長的時間
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
// new_table [shape=none, label="new table"]
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
<TR><TD PORT="d">...</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=green];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=green];
// ge[shape=none, label="..."];
gr->g1->g2->g3;
br[label="", style=filled,fillcolor=blue];
b1[label="", style=filled,fillcolor=blue];
b2[label="", style=filled,fillcolor=blue];
b3[label="", style=filled,fillcolor=blue];
be[shape=none, label="..."];
br->b1->b2->b3->be;
}
subgraph node_same{
rank="same";
edge[style="invisible",dir="none"];
gr->br;
}
}
table->a:a;
b:b:e->gr:n;
a:c:e->br;
a:b:e->gr;
g3->br:s
}
```
:::warning
此演算法放寬每個 bucket 內的 link list 必須為同一個 index 的限制,使 link list 可以臨時包含不同 index 的 node,確保在 resizing 時不會找不到 node,並且可以避免複製 link list,論文稱其為 "hash chains imprecise"
:::
4. Publish new buckets:
將 table 指標指向新的 hash table,此時後續的 reader 將會從新的 table 進行操作
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
<TR><TD PORT="d">...</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=green];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=green];
gr->g1->g2->g3;
br[label="", style=filled,fillcolor=blue];
b1[label="", style=filled,fillcolor=blue];
b2[label="", style=filled,fillcolor=blue];
b3[label="", style=filled,fillcolor=blue];
be[shape=none, label="..."];
br->b1->b2->b3->be;
}
}
table->b:a;
b:b:e->gr;
a:c:e->br;
a:b:e->gr;
g3->br;
}
```
5. Wait for readers:
此時,仍可能有 reader 正在走訪舊的 bucket,所以必須等待一個 [RCU](https://hackmd.io/@sysprog/linux-rcu?type=view) 的寬限期過去
若新的 hash table 有兩個以上的 bucket,則需像 expansion 一樣 unzip
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
<TR><TD PORT="d">...</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=green];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=green];
gr->g1->g2->g3;
br[label="", style=filled,fillcolor=blue];
b1[label="", style=filled,fillcolor=blue];
b2[label="", style=filled,fillcolor=blue];
b3[label="", style=filled,fillcolor=blue];
be[shape=none, label="..."];
br->b1->b2->b3->be;
}
}
table->b:a;
b:b:e->gr;
a:c:e->br[style=dashed];
a:b:e->gr[style=dashed];
g3->br;
}
```
6. Reclaim:
釋放舊的 hash table,完成 hash table shrinking
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=green];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=green];
gr->g1->g2->g3;
br[label="", style=filled,fillcolor=blue];
b1[label="", style=filled,fillcolor=blue];
b2[label="", style=filled,fillcolor=blue];
b3[label="", style=filled,fillcolor=blue];
be[shape=none, label="..."];
br->b1->b2->b3->be;
}
}
table->b:a;
b:b:e->gr;
g3->br;
}
```
:::warning
為簡單起見,rhashtable 透過整數因子限制 resizing 改變桶的數量。例如,將 bucket 的數量加倍或減半。這保證了兩個約束:
1. 當收縮 table 時,新 table 的每個 bucket 將包含多個來自舊 table 的bucket
2. 當擴張 table 時,新 table 的每個 bucket 將包含最多一個來自舊 table 的 bucket
:::
#### Table expansion
隨著 hash table 中的 node 數量增加,每個 bucket 的平均深度會增加,造成查找效率降低,因此需要擴張大小
1. Initial state:
假設初始狀態為只有一個 bucket 的 hash table
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=blue];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=blue];
ge[shape=none, label="..."];
gr->g1->g2->g3->ge;
}
}
table->b:a;
b:b:e->gr;
}
```
2. Initialize new buckets:
分配一個新的包含兩個 bucket 的 expansion hash table,連接到對應的舊 node(指向具有匹配 index 的第一項),並將其儲存在 writer 的 local memory 中,其它 reader 無法存取
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=blue];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=blue];
ge[shape=none, label="..."];
gr->g1->g2->g3->ge;
}
}
table->b:a;
b:b:e->gr;
a:c:e->g1:s[style=dashed];
a:b:e->gr[style=dashed];
}
```
3. Publish new buckets:
將 table 指標指向新的 hash table,此時後續的 reader 將會從新的 table 進行操作,舊有的 hash table 可能有 reader 正在存取,還不可以釋放
:::warning
注意,此時新的兩個 bucket 只有連接的第一個 node 為正確的,但由於舊的 link list 仍將 node 接在一起,所以此時新的 hash table 查找成果仍為正確,只是需要花費較多走訪時間
:::
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=blue];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=blue];
ge[shape=none, label="..."];
gr->g1->g2->g3->ge;
}
}
table->a:a;
b:b:e->gr;
a:c:e->g1:s[color=red];
a:b:e->gr[color=red];
}
```
4. Wait for readers:
需要等待一個 RCU 寬限期過去,才能開始舊的 hash table 進行 unzip
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=blue];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=blue];
ge[shape=none, label="..."];
gr->g1->g2->g3->ge;
}
}
table->a:a;
b:b:e->gr:n[style=dashed];
a:c:e->g1:s[color=red];
a:b:e->gr[color=red];
}
```
5. Unzip one step:
進行 unzip 的過程使利用存取舊的 hash table 來工作,其演算法如下:
1.) 確定 link list 中的第一個 node 屬於新 hash table 的哪個 bucket
2.) 持續走訪 link list,直到遇到不屬於同一個 bucket 的 node
3.) 此時,舊的 bucket 將會指向該 node,成果如下:
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
{
rank = same
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
}
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=blue];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=blue];
ge[shape=none, label="..."];
gr->g1->g2->g3->ge;
// gr:n->g2:n[color=red]
}
}
table->a:a;
b:b:e->g1:n[style=dashed];
a:c:e->g1:s[color=red];
a:b:e->gr[color=red];
}
```
   4.) 更改前一項中的 next 指標,指向下一個匹配的 node:
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
{
rank = same
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
}
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=blue];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=blue];
ge[shape=none, label="..."];
gr->g1[style="invisible",dir="none"];
g1->g2->g3->ge;
gr:n->g2:n[color=red]
}
}
table->a:a;
b:b:e->g1:n[style=dashed];
a:c:e->g1:s[color=red];
a:b:e->gr[color=red];
}
```
:::warning
在另一個 RCU 寬限期過去之前,不能對此 list 進行任何更改,否則在 list 中的 reader 可能最終會出現在錯誤的位置。
> 假設尋找綠色 node 的 reader 可能正在查看從列表中修補出來的藍色 node,但是,由於該 node 的 next 指標保持不變,因此 reader 不會 derailed。而尋找藍色 node 的 reader 從一開始就不會看到更改的 node,因此它們也是安全的。
該演算法通過更改不尋找將 "unzip" 的 node 的 reader 的可見指標來工作。只要每個 RCU 寬限期只更改列表中的一個指標,這個屬性就成立,並且當 reader 通過它時,list 可以安全地 unzip。
:::
6. Wait for readers:
修改完成後,在一次等待一個 RCU 寬限期
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
{
rank = same
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
}
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=blue];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=blue];
ge[shape=none, label="..."];
gr->g1[style="invisible",dir="none"];
g1->g2->g3->ge;
gr:n->g2:n[color=red]
}
}
table->a:a;
b:b:e->g1:n[style=dashed];
a:c:e->g1:s[color=red];
a:b:e->gr[color=red];
}
```
7. Unzip again:
重複步驟 5,此時舊 hash table 的 bucket 指向 even node
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
{
rank = same
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVNE</TD></TR>
</TABLE>>];
b[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ALL</TD></TR>
</TABLE>>];
}
{
subgraph link_list{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=blue];
g2[label="", style=filled,fillcolor=green];
g3[label="", style=filled,fillcolor=blue];
ge[shape=none, label="..."];
gr->g1[style="invisible",dir="none"];
g1->g2[style="invisible",dir="none"];
g2->g3->ge;
gr:n->g2:n[color=red]
g1:s->g3:s[color=red]
}
}
table->a:a;
b:b:e->g2:n[style=dashed];
a:c:e->g1:s[color=red];
a:b:e->gr[color=red];
}
```
8. Final state:
重複等待 reader 與 unzip,直到處理到每個 bucket 的尾端。此時每個 node 僅出現在其相段應的 bucket 中,即可釋放舊的 hash table,完成 hash table expansion
```graphviz
digraph g{
rankdir=LR;
node [shape=record,width=01,height=.1];
table [shape=none, label="table"]
a[shape=plaintext,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD PORT="a" BGCOLOR="gray">Hash Table</TD></TR>
<TR><TD PORT="b">ODD</TD></TR>
<TR><TD PORT="c">EVEN</TD></TR>
</TABLE>>];
{
subgraph cluster_treenode{
penwidth=0;
node[shape=oval,width=.5,height=.3];
gr[label="", style=filled,fillcolor=green];
g1[label="", style=filled,fillcolor=green];
ge[shape=none, label="..."];
gr->g1->ge;
br[label="", style=filled,fillcolor=blue];
b1[label="", style=filled,fillcolor=blue];
be[shape=none, label="..."];
br->b1->be;
}
}
table->a:a
a:c:e->br;
a:b:e->gr;
}
```
:::info
如果 hash table 很長,則此過程可能需要相當長的時間。但是調整大小操作應該比較少,而 reader 存取 table 的頻率很高。所以,對於一個存取量足夠大的 table,額外的努力最終是值得的,在經常有 reader 存取的路徑中將得到了很多倍的回報。
:::
#### Handling Insertion and Removal
現階段我們可以利用以 RCU 為基礎的 hash table 使插入、刪除與查找並行執行。而引入額外的 resizing 操作,也同樣需要做到與其他操作同步。
原先若要進行 resize 操作,需阻止 table 的更新直到 resize 完成。但是,我們可以處理在 resizing 產生的中間狀態,從而提前運行並行更新 table,這可以避免 concurrent 時的過度延遲。
> In the simplest case, concurrent updates can continue to block until the resize completes;however, concurrent updates can potentially run earlier if they carefully handle the intermediate states produced by the resizer. For a sufficiently large hash table, this may prove necessary to avoid excessive delays on concurrent updates.
關於在 resizing 同時插入與刪除主要可以分為兩種情形:
1. Table shrinking
收縮演算法可以選擇在完成初始化後立即為 updaters 發布已初始化的 table,從而允許在進行 cross-linking 的同時進行並行更新(*Table shrinking step 3*)。收縮演算法也會在它完成與該 bucket 的 cross-linking 後立即刪除該 bucket 的每個鎖,從而允許對該 bucket 的並行插入和刪除。
2. Table expansion
在擴展演算法中,需要比收縮演算法更小心處理,對 zipped link list 進行單個 unzip 操作時,必須獲取該 bucket 的鎖。在執行插入時,只須在沒有被 lock 的 bucket 開頭插入,就不會影響 resizing。但在執行刪除時,可能會發生在 list 中的任何節點,包括標記下一個 unzip 目標的指標位置(舊 table 的 bucket pointer,論文稱 "aux pointer"),所以需要檢查與 aux pointer 的衝突,如果發生衝突,刪除節點後需要更新 aux pointer。鑑於與查找和插入相比,刪除的頻率相對較低,調整大小的頻率甚至更低,因此在刪除演算法中要求這種額外的檢查是可以接受的。
:::info
論文中有更多的實做細節,包含針對記憶體分配的 "Resizing in Place" 與優化 hash 分配的 "Keeping Buckets Sorted" 本節就暫不描述
:::
#### [rcuhashbash-resize](https://git.kernel.org/pub/scm/linux/kernel/git/josh/rcuhashbash.git)
rcuhashbash-resize 為論文創建一個為了可調整大小的 hash table 的測試工具和基準測試框架。首先創建一個具有指定 bucket 數量的 hash table,並向其中添加包含從 0 到指定上限的整數值的元素。然後它啟動 reader 執行緒和調整大小執行緒,這些執行緒記錄 thread-local 統計資料,以避免需要額外的同步。當測試完成時,rcuhashbash-resize 停止所有執行緒,匯總它們記錄的統計數據,並通過 kernel message buffer 顯示結果。
## [Relativistic hash tables, part 2: Implementation](https://lwn.net/Articles/612100/)
rhashtable 的實現在 Linux 核心 3.17 版開始加入,這使得 hashtable 可以調整大小,並同時允需 reader 與 writer 同時存取。此篇將描述在 3.17 版中所看到的 "rhashtable" 的 kernel API。
rhashtable 最初是為了在網路子系統中使用而被創建的,但當時的開發者 Thomas Graf 明白 rhashtable 將會獲得更廣泛的使用。因此,他們設計一系列的前置參數,來擴張 rhashtable 的 API 的使用可能,只要完成設置,後續對 table 的操作就會相對簡單了。
### rhashtable structure
首先,我們可以看到 `rhashtable` 的結構包含 `rhashtable_params`,因此需要先定義參數結構
```c
/**
* struct rhashtable - Hash table handle
* @tbl: Bucket table
* @nelems: Number of elements in table
* @shift: Current size (1 << shift)
* @p: Configuration parameters
*/
struct rhashtable {
struct bucket_table __rcu *tbl;
size_t nelems;
size_t shift;
struct rhashtable_params p;
};
```
以下為 `rhashtable_params` 的定義:
- `nelem_hint` 是對錶中將存儲多少元素的猜測(它用於計算初始的 table 大小)。
- `key_len` 和 `key_offset` 描述了 key 的字節大小和它在結構中的 offset(應該用 offsetof() 獲得)。
- `head_offset` 是 hashed object 中 rhash_head 結構的 offset。
- `hash_rnd` 是 hash function 中使用的隨機種子;如果為零,則 rhashtable 將生成一個隨機種子。
- `max_shift` 是用於設置 table 的最大大小;它的值是 table 大小以 2 為底的對數。
- `hashfn` 是執行的 hash function;通常它可以設置為 `arch_fast_hash()`,在`<linux/hash.h>` 中有定義。
- `obj_hashfn` 是 hash 整個 hash object 的函式。如果已提供 `key_len` 和 `key_offset`,則不需要 `obj_hashfn`。如果計算 hash 需要比查看 object 更為複雜,則可以提供這個函式來完成這項工作(並且 `key_len` 應該設置為零)。
- `grow_decision` 和 `shrink_decision` 提供自動調整 table 大小的函式。如果這些函式返回真值,則表的大小將適當的加倍或減半。另外,`rhashtable` 提供了兩個可用的函式:
`rht_grow_above_75()` 和 `rht_shrink_below_30()`。這些函式會力求保持 table 30% 到 75% 的滿度。
- `mutex_is_held` 如果當前持有 protecting mutex,則返回 true。用於除錯和追蹤,確保在呼叫修改 table 的函式時保持鎖定。
```c
/**
* struct rhashtable_params - Hash table construction parameters
* @nelem_hint: Hint on number of elements, should be 75% of desired size
* @key_len: Length of key
* @key_offset: Offset of key in struct to be hashed
* @head_offset: Offset of rhash_head in struct to be hashed
* @hash_rnd: Seed to use while hashing
* @max_shift: Maximum number of shifts while expanding
* @hashfn: Function to hash key
* @obj_hashfn: Function to hash object
* @grow_decision: If defined, may return true if table should expand
* @shrink_decision: If defined, may return true if table should shrink
* @mutex_is_held: Must return true if protecting mutex is held
*/
struct rhashtable_params {
size_t nelem_hint;
size_t key_len;
size_t key_offset;
size_t head_offset;
u32 hash_rnd;
size_t max_shift;
rht_hashfn_t hashfn;
rht_obj_hashfn_t obj_hashfn;
bool (*grow_decision)(const struct rhashtable *ht,
size_t new_size);
bool (*shrink_decision)(const struct rhashtable *ht,
size_t new_size);
int (*mutex_is_held)(void);
};
```
後續就可以使用 `rhashtable_init(struct rhashtable *ht, struct rhashtable_params *params)` 來分配一個 `rhashtable`,另外也有,`rhashtable_destroy(const struct rhashtable *ht)` 來釋放 rhashtable
### rhashtable operations
#### Insertion and Removal
concurrent inserts 與 removes 必須進行 blocking,直到收縮演算法完成發布新 hash table 並等待 reader 結束存取舊 table
```c
void rhashtable_insert(struct rhashtable *ht, struct rhash_head *node, gfp_t);
bool rhashtable_remove(struct rhashtable *ht, struct rhash_head *node, gfp_t);
void rhashtable_remove_pprev(struct rhashtable *ht, struct rhash_head *obj,
struct rhash_head __rcu **pprev, gfp_t flags);
```
#### Expand and Shrink
```c
int rhashtable_expand(struct rhashtable *ht, gfp_t gfp_flags);
int rhashtable_shrink(struct rhashtable *ht, gfp_t gfp_flags);
```
#### Lookup
```c
void *rhashtable_lookup(const struct rhashtable *ht, const void *key);
void *rhashtable_lookup_compare(const struct rhashtable *ht, u32 hash,
bool (*compare)(void *, void *), void *arg);
```
:::warning
文章提到,後需提交的 patch 將會把調整大小的操作移到單獨的執行序中,所以將不再需要在上述函式中分配記憶體空間。因此,`gfp_flags` 參數將被刪除。而我們也可以看到在 [6eba82](https://github.com/torvalds/linux/commit/6eba82248ef47fd478f940a418429e3ec95cb3db) 已刪除此參數
:::
#### [Per bucket locks & deferred expansion/shrinking](https://github.com/torvalds/linux/commit/97defe1ecf868b8127f8e62395499d6a06e4c4b1)
原先沒有像論文描述的對每個 bucket 用 lock 保護 ,加入對每個 bucket 的 spinlock,可以增加並行效率。並且將 expansion 與 shrinking 加到允許從 atomic context 插入和刪除的 warker queue,達到延遲 resizing 使插入和刪除可能與它並行發生,僅在 bucket unzip 或被 link 才短暫暫停
```diff
struct bucket_table {
size_t size;
+ unsigned int locks_mask;
+ spinlock_t *locks;
struct rhash_head __rcu *buckets[];
};
```
## 撰寫 Linux 核心模組作為試驗
我在 Linux 核心發現從 v4 開始有一個用於測試 `rhashtable` 的 module [test_rhashtable.c](https://elixir.bootlin.com/linux/v5.13/source/lib/test_rhashtable.c)
我嘗試將 rhashtable.c 複製下來,並將其掛載到核心
### 實驗環境
首先,確認 Linux 核心版本為 5.13
```shell
$ uname -r
5.13.0-51-generic
```
### 建構核心模組
從 test_rhashtable.c 的程式碼來看,他是使用 `__init` and `__exit` macros 去實做。因此我參考 [Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux2021-summer/https%3A%2F%2Fhackmd.io%2F%40sysprog%2Flinux-kernel-module#%E7%A8%8B%E5%BC%8F%E7%A2%BC%E6%BA%96%E5%82%99) 中掛載 hello 的方式
`Makefile`
```shell
obj-m := test_rhashtable.o
clean:
rm -rf *.o *.ko *.mod.* *.symvers *.order *.mod.cmd *.mod
```
### 編譯核心模組
```shell
$ make -C /lib/modules/`uname -r`/build M=`pwd` modules
```
### 掛載核心模組
產生 test_rhashtable.ko 之後,可將其掛載:
```shell
$ sudo insmod test_rhashtable.ko
```
dmesg 命令可顯示核心訊息
```c
[54613.602222] Running rhashtable test nelem=8, max_size=0, shrinking=0
[54613.602227] Test 00:
[54613.602278] Adding 50000 keys
[54613.628007] Info: encountered resize
[54613.629866] Traversal complete: counted=57342, nelems=50000, entries=50000, table-jumps=1
[54613.629869] Test failed: Total count mismatch ^^^
[54613.634864] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[54613.634867] Deleting 50000 keys
[54613.642286] Duration of test: 40009203 ns
[54613.642293] Test 01:
[54613.642341] Adding 50000 keys
[54613.652447] Info: encountered resize
[54613.653142] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=1
[54613.661613] Info: encountered resize
[54613.663122] Traversal complete: counted=59447, nelems=50000, entries=50000, table-jumps=1
[54613.663125] Test failed: Total count mismatch ^^^
[54613.663126] Deleting 50000 keys
[54613.669292] Duration of test: 26952688 ns
[54613.669302] Test 02:
[54613.669342] Adding 50000 keys
[54613.680276] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[54613.685170] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=0
[54613.685172] Deleting 50000 keys
[54613.692572] Duration of test: 23231530 ns
[54613.692580] Test 03:
[54613.692631] Adding 50000 keys
[54613.703299] Info: encountered resize
[54613.703959] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=1
[54613.711343] Info: encountered resize
[54613.712014] Traversal complete: counted=50000, nelems=50000, entries=50000, table-jumps=1
[54613.712017] Deleting 50000 keys
[54613.720477] Duration of test: 27847456 ns
[54613.722645] test if its possible to exceed max_size 8192: no, ok
[54613.722698] Average test time: 29510219
[54613.722699] test inserting duplicates
[54613.722707]
---- ht: ----
bucket[1] -> [[ val 21 (tid=1) ]] -> [[ val 1 (tid=0) ]]
-------------
[54613.722713]
---- ht: ----
bucket[1] -> [[ val 21 (tid=1) ]] -> [[ val 1 (tid=2), val 1 (tid=0) ]]
-------------
[54613.722717]
---- ht: ----
bucket[1] -> [[ val 21 (tid=1) ]] -> [[ val 1 (tid=0) ]]
-------------
[54613.722720]
---- ht: ----
bucket[1] -> [[ val 21 (tid=1) ]] -> [[ val 1 (tid=2), val 1 (tid=0) ]]
-------------
[54613.722722] Testing concurrent rhashtable access from 10 threads
[54614.010355] test 3125 add/delete pairs into rhlist
[54614.039495] test 3125 random rhlist add/delete operations
[54614.061349] Started 10 threads, 0 failed, rhltable test returns 0
```
### 程式碼運作原理
`test_rhashtable.c` 的目的是測量操作的時間與正確性,主要分為三個部份:
1. 插入、走訪與刪除測試以及 table 的最大大小測試
2. 測試插入會 map 到同一個 bucket 的不同值
3. 在多線程同時存取 table
而 resizing 的部份會在 `rhashtable_init` 時利用 `rht_deferred_worker` 使 table 在測試時自動調整大小,其作法是用 `rht_grow_above_75()` 與 `rht_shrink_below_30()` 判斷要擴張還是縮小
```c
static void rht_deferred_worker(struct work_struct *work)
{
struct rhashtable *ht;
struct bucket_table *tbl;
int err = 0;
ht = container_of(work, struct rhashtable, run_work);
mutex_lock(&ht->mutex);
tbl = rht_dereference(ht->tbl, ht);
tbl = rhashtable_last_table(ht, tbl);
if (rht_grow_above_75(ht, tbl))
err = rhashtable_rehash_alloc(ht, tbl, tbl->size * 2);
else if (ht->p.automatic_shrinking && rht_shrink_below_30(ht, tbl))
err = rhashtable_shrink(ht);
else if (tbl->nest)
err = rhashtable_rehash_alloc(ht, tbl, tbl->size);
if (!err || err == -EEXIST) {
int nerr;
nerr = rhashtable_rehash_table(ht);
err = err ?: nerr;
}
mutex_unlock(&ht->mutex);
if (err)
schedule_work(&ht->run_work);
}
```
### v5.13 與 v3.17 比較
> `rhashtable.h` 自 v3.17 到 v5.13,已從 200 多行增加至 1200 多行程式碼,有眾多的 API 擴增與改進,以下針對幾項 `test_rhashtable.c` 有使用到的進行說明。
#### rhashtable-types.h
由於 `rhashtable.h` 自 2014 年實現在 Linux 核心以後被大量使用,因此微小的更改可能需要大量的重新編譯。所以 [0eb71a](https://github.com/torvalds/linux/commit/0eb71a9da5796851fa87ddc1a534066c0fe54055) 將 [`rhashtable-types.h`](https://elixir.bootlin.com/linux/v5.13/source/include/linux/rhashtable-types.h) 拆分出來,內容只包含主要的型態宣告,後續只須引入 `rhashtable-types.h` 即可,進而減少大型的重新編譯。
#### rhashtable_params
v5.13 的 rhashtable 參數相較 v3.17 的更加簡單易用,多了可以自動對 table 進行收縮的參數 `automatic_shrinking`
```c
/**
* struct rhashtable_params - Hash table construction parameters
* @nelem_hint: Hint on number of elements, should be 75% of desired size
* @key_len: Length of key
* @key_offset: Offset of key in struct to be hashed
* @head_offset: Offset of rhash_head in struct to be hashed
* @max_size: Maximum size while expanding
* @min_size: Minimum size while shrinking
* @automatic_shrinking: Enable automatic shrinking of tables
* @hashfn: Hash function (default: jhash2 if !(key_len % 4), or jhash)
* @obj_hashfn: Function to hash object
* @obj_cmpfn: Function to compare key with object
*/
struct rhashtable_params {
u16 nelem_hint;
u16 key_len;
u16 key_offset;
u16 head_offset;
unsigned int max_size;
u16 min_size;
bool automatic_shrinking;
rht_hashfn_t hashfn;
rht_obj_hashfn_t obj_hashfn;
rht_obj_cmpfn_t obj_cmpfn;
};
```
#### rhashtable_lookup_fast
原先的 Lookup,需要有 RCU 確保並行的正確性,但是若有其他機制保證目標的存在,則可以使用 `rhashtable_lookup_fast` 加快運行時間
```c
/**
* rhashtable_lookup_fast - search hash table, without RCU read lock
* @ht: hash table
* @key: the pointer to the key
* @params: hash table parameters
*
* Computes the hash value for the key and traverses the bucket chain looking
* for a entry with an identical key. The first matching entry is returned.
*
* Only use this function when you have other mechanisms guaranteeing
* that the object won't go away after the RCU read lock is released.
*
* Returns the first entry on which the compare function returned true.
*/
static inline void *rhashtable_lookup_fast(
struct rhashtable *ht, const void *key,
const struct rhashtable_params params)
{
void *obj;
rcu_read_lock();
obj = rhashtable_lookup(ht, key, params);
rcu_read_unlock();
return obj;
}
```
## 執行論文提供的 [rcuhashbash-resize](https://git.kernel.org/pub/scm/linux/kernel/git/josh/rcuhashbash.git)
### 下載程式碼
```shell
git clone git://git.kernel.org/pub/scm/linux/kernel/git/josh/rcuhashbash.git
```
### 編譯核心模組
程式碼提供 `rcuhashbash.c` 與 `rcuhashbash-resize.c`,這裡主要先測試 `rcuhashbash-resize.c`
```shell
make modules
```
編譯後會出現很錯誤,接下來就是一個一個 debug
1. 出現 cpu_clock 錯誤
```c
error: implicit declaration of function ‘cpu_clock’ [-Werror=implicit-function-declaration]
```
從 linux kernel 程式碼中找到 `cpu_clock()` 是定義在 `linux/sched/clock.h`,因此引入該函式庫
```c
#include <linux/sched/clock.h>
```
2. hlist_for_each_entry_rcu、hlist_for_each_entry
程式碼中的 hlist_for_each_entry_rcu 用法怪怪的,原先 [v2.6.37](https://elixir.bootlin.com/linux/v2.6.37/source/include/linux/rculist.h#L449) 的定義為 `hlist_for_each_entry_rcu(tpos, pos, head, member)`,而根據 [v5.13](https://elixir.bootlin.com/linux/v5.13/source/include/linux/rculist.h#L705) 的 `rculist.h` 已改為 `hlist_for_each_entry_srcu(pos, head, member, cond)`,故將程式碼根據新的定義進行修改
```diff
- hlist_for_each_entry_rcu(entry, node, &t->buckets[value & t->mask], node)
+ hlist_for_each_entry_rcu(entry, &t->buckets[value & t->mask], node)
- hlist_for_each_entry(entry, node, &table->buckets[i & table->mask], node)
+ hlist_for_each_entry(entry, &table->buckets[i & table->mask], node)
```
### 掛載核心模組
產生 rcuhashbash.ko 之後,可將其掛載:
```shell
$ sudo insmod rcuhashbash.ko
```
dmesg 命令可顯示核心訊息
```c
[ 六 27 03:52] rcuhashbash starting threads
[ +6.727131] rcuhashbash summary: test=rcu readers=8 resize=true
rcuhashbash summary: entries=65536 shift1=13 (8192 buckets) shift2=14 (16384 buckets)
rcuhashbash summary: reads: 582710953 primary hits, 0 slowpath primary hits, 0 secondary hits, 0 misses
rcuhashbash summary: resizes: 13
rcuhashbash summary: PASS
[ +0.000016] rcuhashbash done
```
編譯成功後執行,若將 resize 開啟仍出現問題
```c
[ 六 27 02:00] rcuhashbash starting threads
[ +0.006406] BUG: kernel NULL pointer dereference, address: 0000000000000008
[ +0.000017] #PF: supervisor write access in kernel mode
[ +0.000008] #PF: error_code(0x0002) - not-present page
```
發現是 resize 在 grow 時誤用了 `node`,應該改為 `entry->node`,就沒問題了
```diff=184
- temp_table->buckets[i].first = node;
- node->pprev = &temp_table->buckets[i].first;
+ temp_table->buckets[i].first = &entry->node;
+ entry->node.pprev = &temp_table->buckets[i].first;
```
```diff=208
- old_table->buckets[i].first = node;
- if (!node)
+ old_table->buckets[i].first = &entry->node;
+ if (!&entry->node)
```
```diff=215
- entry_prev->node.next = node;
- if (node)
- node->pprev = &entry_prev->node.next;
+ entry_prev->node.next = &entry->node;
+ if (&entry->node)
+ entry->node.pprev = &entry_prev->node.next;
```
### 量測時間
利用 `ktime_get_ns()` 紀錄分配 `kthread_run()` 與 `kthread_stop()` 的時間,計算 `end - start` 即為執行時間,並將其寫入 `rcuhashbash_print_stats` 作輸出
```diff
[ 六 27 04:04] rcuhashbash starting threads
[ +6.052081] rcuhashbash summary: test=rcu readers=8 resize=true
rcuhashbash summary: entries=65536 shift1=13 (8192 buckets) shift2=14 (16384 buckets)
rcuhashbash summary: reads: 552065527 primary hits, 0 slowpath primary hits, 0 secondary hits, 0 misses
rcuhashbash summary: resizes: 17
rcuhashbash summary: PASS
+ rcuhashbash summary: totak time: 6005639149 ns
[ +0.000027] rcuhashbash done
```
### 程式碼運作原理
論文提供的測試平台主要是想測試 rhashtable 在發生 resizing 時,多個 reader read hit 的次數,用於比較 scalability。
Module 的參數包含:
- `test`:測試的演算法(rcu, ddds, rwlock)
- `readers`:reader 線程的數量
- `resize`:是否開啟 resizing
- `shift1`:原始 table 大小
- `shift2`:調整的 table 大小
- `entries`:table 內條目的數量
```c
module_param(test, charp, 0444);
MODULE_PARM_DESC(test, "Hash table implementation");
module_param(readers, int, 0444);
MODULE_PARM_DESC(readers, "Number of reader threads (default: number of online CPUs)");
module_param(resize, bool, 0444);
MODULE_PARM_DESC(resize, "Whether to run a resize thread (default: true)");
module_param(shift1, byte, 0444);
MODULE_PARM_DESC(shift1, "Initial number of hash buckets, log 2");
module_param(shift2, byte, 0444);
MODULE_PARM_DESC(shift2, "Number of hash buckets after resize, log 2");
module_param(entries, ulong, 0444);
MODULE_PARM_DESC(entries, "Number of hash table entries");
```
`rcuhashbash_table` 的結構包含用於計算 index 的 `mask` 與由 `hlist_head` 組成的 `buckets` 陣列。而 `stats` 結構為每個 reader 執行緒需要紀錄的統計變數
```c
struct rcuhashbash_table {
unsigned long mask;
struct hlist_head buckets[];
};
struct stats {
u64 read_hits; /* Primary table hits */
u64 read_hits_fallback; /* Fallback (secondary table) hits */
u64 read_hits_slowpath; /* Slowpath primary table hits (if applicable) */
u64 read_misses;
u64 resizes;
};
```
接著,程式會根據設定的參數先建立好 hashtable,並利用 `hlist_add_head` 插入 `entries` 個條目,然後分配 reader thread 與 resize thread 分別執行 `rcuhashbash_read_thread` 與 `rcuhashbash_resize_thread`
```c
static int __init rcuhashbash_init(void)
{
int ret;
unsigned long i;
for (i = 0; i < ARRAY_SIZE(all_ops); i++)
if (strcmp(test, all_ops[i].test) == 0) {
ops = &all_ops[i];
}
if (!ops) {
printk(KERN_ALERT "rcuhashbash: No implementation with test=%s\n",
test);
return -EINVAL;
}
if (readers < 0)
readers = num_online_cpus();
if (!ops->read) {
printk(KERN_ALERT "rcuhashbash: Internal error: read function NULL\n");
return -EINVAL;
}
if (!ops->resize) {
printk(KERN_ALERT "rcuhashbash: Internal error: resize function NULL\n");
return -EINVAL;
}
entry_cache = KMEM_CACHE(rcuhashbash_entry, 0);
if (!entry_cache)
goto enomem;
table = kzalloc(sizeof(*table) + (1UL << shift1) * sizeof(table->buckets[0]), GFP_KERNEL);
if (!table)
goto enomem;
table->mask = (1UL << shift1) - 1;
for (i = 0; i < entries; i++) {
struct rcuhashbash_entry *entry;
entry = kmem_cache_zalloc(entry_cache, GFP_KERNEL);
if(!entry)
goto enomem;
entry->value = i;
hlist_add_head(&entry->node, &table->buckets[i & table->mask]);
}
thread_stats = kcalloc(readers + resize, sizeof(thread_stats[0]), GFP_KERNEL);
if (!thread_stats)
goto enomem;
tasks = kcalloc(readers + resize, sizeof(tasks[0]), GFP_KERNEL);
if (!tasks)
goto enomem;
printk(KERN_ALERT "rcuhashbash starting threads\n");
for (i = 0; i < readers + resize; i++) {
struct task_struct *task;
if (i < readers)
task = kthread_run(rcuhashbash_read_thread,
&thread_stats[i],
"rcuhashbash_read");
else
task = kthread_run(rcuhashbash_resize_thread,
&thread_stats[i],
"rcuhashbash_resize");
if (IS_ERR(task)) {
ret = PTR_ERR(task);
goto error;
}
tasks[i] = task;
}
return 0;
enomem:
ret = -ENOMEM;
error:
rcuhashbash_exit();
return ret;
}
```
當 module exit 時,會停止所有執行緒,統計每個 reader thread 的 `stats`,並釋放 `table` 與 `thread_stats`
```c
static void __exit rcuhashbash_exit(void)
{
unsigned long i;
int ret;
printk(KERN_ALERT "exit\n");
if (tasks) {
for (i = 0; i < readers + resize; i++)
if (tasks[i]) {
ret = kthread_stop(tasks[i]);
if(ret)
printk(KERN_ALERT "rcuhashbash task returned error %d\n", ret);
}
kfree(tasks);
}
/* Wait for all RCU callbacks to complete. */
rcu_barrier();
if (table2) {
printk(KERN_ALERT "rcuhashbash FAIL: secondary table still exists during cleanup.\n");
kfree(table2);
}
if (table) {
for (i = 0; i <= table->mask; i++) {
struct hlist_head *head = &table->buckets[i];
while (!hlist_empty(head)) {
struct rcuhashbash_entry *entry;
entry = hlist_entry(head->first, struct rcuhashbash_entry, node);
head->first = entry->node.next;
kmem_cache_free(entry_cache, entry);
}
}
kfree(table);
}
if (entry_cache)
kmem_cache_destroy(entry_cache);
rcuhashbash_print_stats();
kfree(thread_stats);
printk(KERN_ALERT "rcuhashbash done\n");
}
```
## 論文 Banchmark 比較
### 實驗環境
```c
$ 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): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 142
Model name: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
Stepping: 10
CPU MHz: 1800.000
CPU max MHz: 3400.0000
CPU min MHz: 400.0000
BogoMIPS: 3600.00
Virtualization: VT-x
L1d cache: 128 KiB
L1i cache: 128 KiB
L2 cache: 1 MiB
L3 cache: 6 MiB
NUMA node0 CPU(s): 0-7
```
### 實驗參數
分別調整三中不同演算法(rcu, ddds, rwlock)與 `reader` 的數量,比較每秒 lookup hit 的次數,而 resizer 在過程中會不斷調整 table 的大小,從 2^13^ 擴張到 2^14^ 再收縮回去(2 倍)
```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 固定每次測試時間為 30 秒
- RP 為論文的演算法
- DDDS 為另一個基於 RCU hashtable 的調整大小演算法,該演算法通過檢查多個 hashtable 達成 reaizing 時的查找
- rwlock 為利用 read, write lock 來達成查找
#### 固定 table 大小
固定大小在 2^14^, 比較不同演算法的查找次數
![](https://i.imgur.com/reDEwbA.png)
#### 開啟 resize
resize thread 會不斷在 2^13^ 到 2^14^ 之間調整 table 大小,比較在 resizing 時不同演算法的查找次數
![](https://i.imgur.com/UvQTqoR.png)
#### 不同大小的 RP
比較固定大小在 2^13^ 與 2^14^ 還有 resize 時,使用 RP 演算法的查找次數
![](https://i.imgur.com/kvR5lKS.png)
:::warning
論文所使用的實驗環境為 24 個 hardware-supported threads,所以它選擇使用 1, 2, 4, 8, 16 作為測試的 reader 數量,使運行的線程都比支持的硬體還少。
> Our test system had two Intel “Westmere” Xeon DP
processors at 2.4GHz, each of which had 6 hardware
cores of two logical threads each, for a total of 24
hardware-supported threads.
另外,論文觀察到 16 個 readers 與線性增長的預期偏差,可能是由於超過了 12 個處理器核的限制,而論文測試 16 readers 的性能似乎比 8 readers 的性能高出大約 50%,這與充分利用 12 個 cores 的預期線性增長一致
而本研究的實驗環境為 8 核 16 個硬體執行緒,但在 8 個 readers 就產生偏差,16 個 readers 發生瓶頸,可能原因是我在測試時仍有其他 process 在運作
:::
## 結論
本研究從最初的論文出發,說明 rhashtable 的動機、原理,再來介紹 Thomas Graf 在 Linux 核心的首次實作,以及至今 `rhashtable.h` 的演化,最後撰寫 Linux 核心模組作為試驗,得出以下結論:
- 比較論文與 Linux 核心測試的差異:
- 論文者要是希望驗證演算法在 resizing 時對查找影響
- Linux 核心將 resizing 自動化,主要測試插入、刪除與查找所花費的時間
- 論文的演算法比較:
- rhashtable 在測試中都提供了線性可擴展的查找性能。
- 在固定大小時,論文演算法與 DDDS 差不多,這兩種實現都大大地使 rwlock 相形見絀
- 在 resizing 時,論文演算法明顯優於 DDDS,但沒有如論文所述的幅度高達 56%
- 比較不同大小的 RP,可知論文的調整大小演算法幾乎不會增加並行查找的開銷