linux內核netfilter連接跟蹤的hash算法
linux內核中的netfilter是一款強大的基於狀態的防火牆,具有連接跟蹤(conntrack)的實現。conntrack是netfilter的核心,許多增強的功能,例如,地址轉換(NAT),基於內容的業務識別(l7, layer-7 module)都是基於連接跟蹤。然而,netfilter的性能還有很多值得改進的地方。
netfilter的連接跟蹤的hash算法是在Bob Jenkins的lookup2.c基礎上的改進實現,Bob Jenkins已經推出lookup3.c的實現,見地址:http://burtleburtle.net/bob/hash/和http://burtleburtle.net/bob/c/lookup3.c
netfilter中的hash求值的代碼如下:
static u_int32_t __hash_conntrack(const struct nf_conntrack_tuple *tuple,
unsigned int size, unsigned int rnd)
{
unsigned int a, b;
a = jhash((void *)tuple->src.u3.all, sizeof(tuple->src.u3.all),
((tuple->src.l3num) << 16) | tuple->dst.protonum);
b = jhash((void *)tuple->dst.u3.all, sizeof(tuple->dst.u3.all),
(tuple->src.u.all << 16) | tuple->dst.u.all);
return jhash_2words(a, b, rnd) % size;
}
static inline u_int32_t hash_conntrack(const struct nf_conntrack_tuple *tuple)
{
return __hash_conntrack(tuple, nf_conntrack_htable_size,
nf_conntrack_hash_rnd);
}
這是一個對於ipv6和ipv4的hash求值的通用實現。struct nf_conntrack_tuple是一個通用的連接的四元組,同時用於ipv4和ipv6,tcp,udp,sctp,icmp協議,所以,其定義比較復雜。可以把它理解為源地址,源端口,目的地址,目的端口。
#define NF_CT_TUPLE_L3SIZE 4
union nf_conntrack_man_l3proto {
u_int32_t all[NF_CT_TUPLE_L3SIZE];
u_int32_t ip;
u_int32_t ip6[4];
};
其實這就是ip地址。
union nf_conntrack_man_proto
{
/* Add other protocols here. */
u_int16_t all;
struct {
u_int16_t port;
} tcp;
struct {
u_int16_t port;
} udp;
struct {
u_int16_t id;
} icmp;
struct {
u_int16_t port;
} sctp;
};
這就是端口。
struct nf_conntrack_man
{
union nf_conntrack_man_l3proto u3;
union nf_conntrack_man_proto u;
/* Layer 3 protocol */
u_int16_t l3num;
};
目的地址和端口,l3num不知道是什么東西?
struct nf_conntrack_tuple
{
struct nf_conntrack_man src;
/* These are the parts of the tuple which are fixed. */
struct {
union {
u_int32_t all[NF_CT_TUPLE_L3SIZE];
u_int32_t ip;
u_int32_t ip6[4];
} u3;
union {
/* Add other protocols here. */
u_int16_t all;
struct {
u_int16_t port;
} tcp;
struct {
u_int16_t port;
} udp;
struct {
u_int8_t type, code;
} icmp;
struct {
u_int16_t port;
} sctp;
} u;
/* The protocol. */
u_int8_t protonum;
/* The direction (for tuplehash) */
u_int8_t dir;
} dst;
};
有些混亂,就是源地址和目的地址,protonum和dir不知道為什么這么定義?
上面的hash算法在僅用於ipv4時,可以進行優化。jhash函數是通用的hash函數,上面的目的是把ipv6的長串字符hash為一個32位整數,而ipv4的情況下,可以不用。
最后,使用%運算,這是非常低效的,Bob Jenkins專門指出了這一點。由於table的大小都為2的次方,所以,可以使用&的算法。
另外,我認為Bob Jenkins的算法是對於通用的數字的hash算法,對於tcp連接這樣比較特殊的數字的hash,使用這么復雜的算法,是否有意義?簡單的加法運算是否更有效率?
lookup3.c與lookup2.c有很大的不同。lookup3.c中,使用了final宏,和mix宏分開。而lookup2.c中沒有使用final宏。
linux下的修改過的hash函數:
static inline u32 jhash(const void *key, u32 length, u32 initval)
通用的hash函數,對任意長度的key字符串進行hash運算,得到一個32位數字。
static inline u32 jhash2(u32 *k, u32 length, u32 initval)
優化的版本,對任意長度的32位整數進行hash運算,得到一個32位數字。
static inline u32 jhash_3words(u32 a, u32 b, u32 c, u32 initval)
{
a += JHASH_GOLDEN_RATIO;
b += JHASH_GOLDEN_RATIO;
c += initval;
__jhash_mix(a, b, c);
return c;
}
優化的版本,對3個32位整數進行hash運算,得到一個32位數字。
static inline u32 jhash_2words(u32 a, u32 b, u32 initval)
{
return jhash_3words(a, b, 0, initval);
}
對2個32位整數進行hash運算,得到一個32位數字。
static inline u32 jhash_1word(u32 a, u32 initval)
{
return jhash_3words(a, 0, 0, initval);
}
對1個32位整數進行hash運算,得到一個32位數字。
#define mix(a,b,c) /
{ /
a -= c; a ^= rot(c, 4); c += b; /
b -= a; b ^= rot(a, 6); a += c; /
c -= b; c ^= rot(b, 8); b += a; /
a -= c; a ^= rot(c,16); c += b; /
b -= a; b ^= rot(a,19); a += c; /
c -= b; c ^= rot(b, 4); b += a; /
}
#define final(a,b,c) /
{ /
c ^= b; c -= rot(b,14); /
a ^= c; a -= rot(c,11); /
b ^= a; b -= rot(a,25); /
c ^= b; c -= rot(b,16); /
a ^= c; a -= rot(c,4); /
b ^= a; b -= rot(a,14); /
c ^= b; c -= rot(b,24); /
}
上面的兩個宏這是lookup3.c的核心hash算法,hash的基礎。
uint32_t hashword(
const uint32_t *k, /* the key, an array of uint32_t values */
size_t length, /* the length of the key, in uint32_ts */
uint32_t initval) /* the previous hash, or an arbitrary value */
{
uint32_t a,b,c;
/* Set up the internal state */
a = b = c = 0xdeadbeef + (((uint32_t)length)<<2) + initval;
/*------------------------------------------------- handle most of the key */
while (length > 3)
{
a += k[0];
b += k[1];
c += k[2];
mix(a,b,c);
length -= 3;
k += 3;
}
/*------------------------------------------- handle the last 3 uint32_t's */
switch(length) /* all the case statements fall through */
{
case 3 : c+=k[2];
case 2 : b+=k[1];
case 1 : a+=k[0];
final(a,b,c);
case 0: /* case 0: nothing left to add */
break;
}
/*------------------------------------------------------ report the result */
return c;
}
hashword是通用的hash算法,用於計算任意cpu架構,任意長度的字符串的hash值。
不斷的把輸入的串k,每隔3位進行mix,直到完畢。返回final。
對於ipv4的話,可以直接把源地址,目的地址,(源端口<< 16)|目的端口,這三個整數進行final,得到hash值。
對於ip地址和端口號的特點,這種復雜的算法是否真的有更好的hash效果,我持懷疑態度。
