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