前言:DPDK的LPM模塊實現了一種最長前綴匹配,其中的KEY是32位的,可以說是為查找路由量身定做的,為了實現快速查找,實現上使用了用空間換時間的思路。同時為了最大限度的減少查詢次數,把32位的KEY值划分為24位和8位兩張表中。這樣的設計思路可以用於以后的前綴查找。本篇分析以16.07版本為例。
一. LPM的設計概覽
對於路由的查找,有多種方法,前綴匹配,紅黑樹,各有優劣。這都在昭示着上帝創世界是均衡的。從統計上看,只有少數的路由網段深度大於24,因此使用了DIR-24-8表,大部分匹配在第一次查找時即可命中。
對於二級表tbl8,理論上有224個,但是我們不能存儲這個多個表,否則總共的表項不就是232了嘛,相當於每個ip地址都有一個條目對應。所以,可以通過限定tbl8表中的組的數目,減少空間占用。每個組也可以再次限定條目的數目,最大自然就是256了。直接使用下面的圖來表示:
在16.04版本后,比如16.07版本中,就出現了LPM操作的兩套接口,如
rte_lpm_add_v20()
和rte_lpm_add_v1604()
,區別在於下一跳的值范圍,v20版本是8位的,v1604是32位的。這里的下一跳並不是地址的下一跳,僅僅是表示一個索引,可以用來指示自己的表項。
二.LPM的代碼實現
在代碼分析上,我們以v20版本做分析,v1604的版本基本一樣。
2.1 LPM路由的存儲
路由的存儲是從rte_lpm_add_v20()
開始的:
首先,獲得網段,
ip_masked = ip & depth_to_mask(depth);
然后是把路由信息添進規則表--rule_add_v20()
,
規則表是按照掩碼深度排的,共有32個組,分別對應不同的掩碼深度。
既然是添加rule,首先要看是否已經有了這個rule。這里使用了lpm->rule_info
結構來存儲每個組的第一個rule在rule table的index。所以,查找過程就是:先看對應深度的組是否有元素,如果沒有,直接下一步插入,如果有,就找到對應組的第一個元素,然后遍歷,找到就返回index,沒找到就下一步插入。
主要來說一下這個插入位置,由於各個組的rule存儲是連着的,沒有空余位置,因此當要添加一個20位深度的rule時,如果已經添加了大於20的rule,就要挪出位置來。
rule_index = 0;
for (i = depth - 1; i > 0; i--) {
if (lpm->rule_info[i - 1].used_rules > 0) {
rule_index = lpm->rule_info[i - 1].first_rule
+ lpm->rule_info[i - 1].used_rules;
break;
}
}
if (rule_index == lpm->max_rules)
return -ENOSPC;
lpm->rule_info[depth - 1].first_rule = rule_index;
這是計算出新的rule要在表中的位置。然后接下來就是為這個rule挪出位置來:
/* Make room for the new rule in the array. */
for (i = RTE_LPM_MAX_DEPTH; i > depth; i--) {
if (lpm->rule_info[i - 1].first_rule
+ lpm->rule_info[i - 1].used_rules == lpm->max_rules)
return -ENOSPC;
if (lpm->rule_info[i - 1].used_rules > 0) {
lpm->rules_tbl[lpm->rule_info[i - 1].first_rule
+ lpm->rule_info[i - 1].used_rules]
= lpm->rules_tbl[lpm->rule_info[i - 1].first_rule];
lpm->rule_info[i - 1].first_rule++;
}
}
從32位深度的組開始,大於depth的組往后依次挪一個位置。
最后把填充規則信息:
lpm->rules_tbl[rule_index].ip = ip_masked;
lpm->rules_tbl[rule_index].next_hop = next_hop;
/* Increment the used rules counter for this rule group. */
lpm->rule_info[depth - 1].used_rules++;
這樣,規則表就添加完成了!
接下來就是重要的往DIR-24-8表中添加條目了。
對於掩碼深度<=24位的,只需要添加tbl24表,而掩碼深度大於24位的,還需要添加tbl8表。因此,這里有兩個入口函數,先看掩碼深度<=24的處理:add_depth_small_v20()
首先要知道的是,對於tbl24表,存的是地址前24位的排列,最大共224個條目。此時想象一下,如果配置了一個20位深度的路由,那么前24位就有2(24-20)個可能,這些條目也都要填充為這個條目。
因此,先算出來對於這樣深度的路由,其前24位可能的范圍。
tbl24_index = ip >> 8;
tbl24_range = depth_to_range(depth);
然后對這個范圍內的條目操作:
for (i = tbl24_index; i < (tbl24_index + tbl24_range); i++)
更新或者新增新的tbl24條目。如果有tbl8表的,找到對應的表,更新或者添加tbl8表。
接下來我們看一下當深度>24時,對於這兩張表是怎么操作來插入路由的:add_depth_big_v20()
首先也是確定他的高24位的段和后8位的范圍,
tbl24_index = (ip_masked >> 8);
tbl8_range = depth_to_range(depth);
然后主要分了3類情況處理,
第一種,連對應的tbl24條目都不存在的,自然是要先增添tbl24條目
if (!lpm->tbl24[tbl24_index].valid) {
/* Search for a free tbl8 group. */
tbl8_group_index = tbl8_alloc_v20(lpm->tbl8);
/* Check tbl8 allocation was successful. */
if (tbl8_group_index < 0) {
return tbl8_group_index;
}
/* Find index into tbl8 and range. */
tbl8_index = (tbl8_group_index *
RTE_LPM_TBL8_GROUP_NUM_ENTRIES) +
(ip_masked & 0xFF);
/* Set tbl8 entry. */
for (i = tbl8_index; i < (tbl8_index + tbl8_range); i++) {
lpm->tbl8[i].depth = depth;
lpm->tbl8[i].next_hop = next_hop;
lpm->tbl8[i].valid = VALID;
}
/*
* Update tbl24 entry to point to new tbl8 entry. Note: The
* ext_flag and tbl8_index need to be updated simultaneously,
* so assign whole structure in one go
*/
struct rte_lpm_tbl_entry_v20 new_tbl24_entry = {
{ .group_idx = (uint8_t)tbl8_group_index, },
.valid = VALID,
.valid_group = 1,
.depth = 0,
};
lpm->tbl24[tbl24_index] = new_tbl24_entry;
}
這里的邏輯是比較容易理解的,同時創建tbl24和tbl8的條目,這里注意到tbl8表組索引的分配,就是在最大限定的組數量范圍內,找出沒使用的組,默認最大為256個組,所以,默認只能存256個大於32位深度的路由,在16.04版本中,這個組數量是在初始化可配置的。
第二種情況,tbl24表存在,但是沒有tbl8的標識,比如先配了192.168.1.0/24,此時,tbl24已經創建了一個條目,如果再配置192.168.1.0/28,那么,先檢查tbl24時,已經創建,但是沒有tbl8標識,現在要做的就是更新tbl24條目,同時創建tbl8條目。
這里就很有意思了,既然更新tbl24,那么主要就是nexthop,valid標識,此時,因為查詢大於24位深度的路由時要使用tbl8,而如果查詢小於24位的又不查tbl8,這樣怎么定nexthop呢?所以,就把先把tbl8條目都設置成tbl24的內容,也就是把對應網斷的tbl24條目搬到tbl8中,然后再根據tbl8的范圍,刷新tbl8,這樣,老的tbl24的條目和新添加tbl8的條目都存在這個tbl8中了。
for (i = tbl8_group_start; i < tbl8_group_end; i++) {
lpm->tbl8[i].valid = VALID;
lpm->tbl8[i].depth = lpm->tbl24[tbl24_index].depth;
lpm->tbl8[i].next_hop =
lpm->tbl24[tbl24_index].next_hop;
}
tbl8_index = tbl8_group_start + (ip_masked & 0xFF);
/* Insert new rule into the tbl8 entry. */
for (i = tbl8_index; i < tbl8_index + tbl8_range; i++) {
lpm->tbl8[i].valid = VALID;
lpm->tbl8[i].depth = depth;
lpm->tbl8[i].next_hop = next_hop;
}
最后更新tbl24表
struct rte_lpm_tbl_entry_v20 new_tbl24_entry = {
{ .group_idx = (uint8_t)tbl8_group_index, },
.valid = VALID,
.valid_group = 1,
.depth = 0,
};
lpm->tbl24[tbl24_index] = new_tbl24_entry;
第三種情況是如果已經有對應的tbl8組了,那自然就是更新啦。如果理解了第二種情況,這種也很好理解
tbl8_group_index = lpm->tbl24[tbl24_index].group_idx;
tbl8_group_start = tbl8_group_index *
RTE_LPM_TBL8_GROUP_NUM_ENTRIES;
tbl8_index = tbl8_group_start + (ip_masked & 0xFF);
for (i = tbl8_index; i < (tbl8_index + tbl8_range); i++) {
if (!lpm->tbl8[i].valid ||
lpm->tbl8[i].depth <= depth) {
struct rte_lpm_tbl_entry_v20 new_tbl8_entry = {
.valid = VALID,
.depth = depth,
.valid_group = lpm->tbl8[i].valid_group,
};
new_tbl8_entry.next_hop = next_hop;
/*
* Setting tbl8 entry in one go to avoid race
* condition
*/
lpm->tbl8[i] = new_tbl8_entry;
就是把最新添加的深度的條目更新一下,如老的是192.168.1.0/28的,新添加的是192.168.1.0/30的,根據最長匹配,那么就把最長網段(30掩碼的)中的4個路由更新一下。
這樣,路由添加就完成了!
2.2 LPM路由的查找
對應條目的查找就很簡單了,只要找到nexthop就行了,但要注意的是,這里的nexthop可不是最后的下一跳,因為看結構定義,只有8位
struct rte_lpm_tbl_entry_v20 {
uint8_t depth :6;
uint8_t valid_group :1;
uint8_t valid :1;
union {
uint8_t group_idx;
uint8_t next_hop;
};
};
因此,這個nexthop實際上也只是個索引,根據nexthop的索引,我們可以自己進行后續處理。
后記:
LPM模塊是DPDK的重要組成部分,主要用於查找路由,其使用的以空間換時間的方式,同時為了盡量減少空間消耗,采用DIR-24-8的二級表,再加上可配置的條目總數限制,在保證速度的前提下,最大可能降低了資源消耗,是一種很值得借鑒的設計方式。