Linux内核连接跟踪锁的优化分析(2)

20020阅读 1评论2015-01-29 GFree_Wind
分类:LINUX

Linux内核连接跟踪锁的优化分析(2)

作者:gfree.wind@gmail.com
博客:linuxfocus.blog.chinaunix.net 
微博:weibo.com/glinuxer
QQ技术群:4367710
路漫漫其修远兮,吾将上下而求索

前文简介

前面一篇文章Linux内核连接跟踪锁的优化分析1中,介绍了新的内核优化了全局的连接跟踪锁nf_conntrack_lock。对于连接跟踪表来说,由一个全局锁扩展为多个锁,这样就使锁的粒度变细,减少了锁竞争,提高了并发性能。

连接跟踪锁的使用

以前的连接跟踪锁因为就是一个全局的自旋锁,所以使用起来很简单,直接调用spinlockbh即可,spin_lock_bh(&nf_conntrack_lock)。

现在连接跟踪锁使用起来就要比以前繁琐多了。下面以nf_ct_delete_from_lists为例说明:

struct net *net = nf_ct_net(ct);
unsigned int hash, reply_hash;
u16 zone = nf_ct_zone(ct);
unsigned int sequence;  

nf_ct_helper_destroy(ct);
/* 
因为nf_conntrack_double_lock使用的是spin_lock,没有禁止软中断,所以首先要使用local_bh_disable禁止软中断。
 */
local_bh_disable();
/* 使用seqlock机制封装nf_conntrack_double_lock */
do {
    /* seqlock机制:得到当前连接跟踪的序号 */
    sequence = read_seqcount_begin(&net->ct.generation);
    /* 得到fwd和rev两个方向tuple的hash值 */
    hash = hash_conntrack(net, zone,
                        &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
    reply_hash = hash_conntrack(net, zone,
                            &ct->tuplehash[IP_CT_DIR_REPLY].tuple);
    /* 使用两个hash值进行加速,并使用seqlock机制对连接跟踪序号进行校验 */
} while (nf_conntrack_double_lock(net, hash, reply_hash, sequence)); 

通过前面的文章,我想大家对使用两个hash值来上锁,应该没有什么疑问。这是利用连接的两个tuple都是唯一的,因此可以利用两个tuple得到两个唯一的hash值,并按照固定的顺序获得相应的锁。这样就保证了对一个连接的唯一访问。

这里可能会产生疑问的是为什么nf_conntrack_double_lock要使用seqlock机制封装起来。回答这个问题,我们要先看一下计算tuple的hash值函数hash_conntrack。

static inline u_int32_t hash_conntrack(const struct net *net, u16 zone,
                   const struct nf_conntrack_tuple *tuple)
{
    return __hash_conntrack(tuple, zone, net->ct.htable_size);
} 

从上面的代码是可以看到,hash值的计算是依赖于连接跟踪表的大小htable_size相关的。而连接跟踪表的大小是可动态调整的。如果不讲这个因素考虑进去,这意味着在一个cpu通过nf_conntrack_double_lock获得某个连接的访问权限后,这时修改了跟踪表的大小,这会导致另外一个cpu也可以得到这个连接的访问权限。因为该连接两个tuple的hash发生了变化,因此另外一个cpu可以成功上锁。 这样就导致了资源冲突。

因为连接跟踪表的大小很少发生变化,因此内核选择了使用了seqlock作为保障。这里不对seqlock进行额外的介绍了——seqlock也是一个设计比较巧妙的实现。这样,连接锁的使用,就需要使用seqlock机制。在nf_conntrack_double_lock中,如果通过sequence发现表的大小有变化,则对两个hash值对应的锁进行解锁,并返回真值。这样就会重新计算两个hash值,并再次尝试调用nf_conntrack_double_lock,直到其返回假值。

连接跟踪表大小变化

上一节说nf_conntrack_double_lock会在上锁的时候,对连接跟踪表的序号进行检查。只有在上锁的过程中,序号没有变化,才算上锁成功。但是内核如何保证在上锁以后,连接跟踪表的大小不会发生变化呢?

这就需要查看nf_conntrack_set_hashsize,该函数是用于改变连接跟踪表大小。下面是其主要实现代码。

/* 因为与软中断竞争,需要禁止软中断 */
local_bh_disable();
/* 
对所有nf_conntrack_locks进行上锁,这样就做到了两点:
1. 对所有锁进行上锁,相当于获得了seqlock的写锁;
2. 在nf_conntrack_double_lock成功以后,不能更新表的大小,直到其解锁;
 */
nf_conntrack_all_lock();
write_seqcount_begin(&init_net.ct.generation); 

(距离上篇文章已经有好几个月,先跟大家说一声抱歉。同时声明这系列文章还没有结束,后面还会持续更新)

上一篇:Linux内核连接跟踪锁的优化分析(1)
下一篇:多年系统架构师大会感受

文章评论