此博客讨论的是在多线程下链式哈希表的行锁的使用,主要是针对对哈希表的表锁,二者各有优缺点。
表锁哈希表,只有一把锁,内存少,实现简单,但是在并发访问时,效率极其低下;
行级锁哈希表则每一行都有一把锁,内存开销大,实现复杂,但是在大并发,高效率的后台服务程序中使用非常广泛。suricata就声称自己可以处理万兆网络,这和多线程架构和更细粒度的锁的机制是分不开的。
在上大学的时候了解过数据库,听过老师讲过行级锁,头脑中有这个概念,没有看过源码,也没有实现过。在去年阅读suricata源码时,一次偶然的机会看到the hash row lock,心中有种莫名冲动,觉得这个应该就是大名鼎鼎的行锁。怀着激动的心情去看代码实现。
先解释一下为什么suricata为什么会使用到行锁,suricata针对snort单线程处理数据包,无法很好利用多核cpu的劣势,开发了多线程架构方式并发处理数据包,而很多数据是线程间共享,所以在很多地方使用行级锁哈希表等其他高效数据结构。第一个行锁使用的地方就是连接管理模块(哈希表,检索速度O(1))
此源码版本为3.0.1,官网:https://suricata-ids.org/
连接管理的哈希表:FlowBucket *flow_hash。其为链式哈希表,其定义如下
typedef struct FlowBucket_ {
Flow *head; /* 链表头*/
Flow *tail;/* 链表尾*/
/* 行锁类型*/
#ifdef FBLOCK_MUTEX
SCMutex m;
#elif defined FBLOCK_SPIN
SCSpinlock s;
#else
#error Enable FBLOCK_SPIN or FBLOCK_MUTEX
#endif
} __attribute__((aligned(CLS))) FlowBucket;
typedef struct Flow_
{
...
/* 节点锁,提高并发*/
#ifdef FLOWLOCK_RWLOCK
SCRWLock r;
#elif defined FLOWLOCK_MUTEX
SCMutex m;
#else
#error Enable FLOWLOCK_RWLOCK or FLOWLOCK_MUTEX
#endif
...
/* 由hnext hprev 构成双向链表 */
/** hash list pointers, protected by fb->s */
struct Flow_ *hnext; /* hash list */
struct Flow_ *hprev;
...
} Flow;
flow_hash是一个数组,每个FlowBucket 元素由 head 、tail和flow的hnext、hprev 构成一个双向链表,也就是所谓的行。该哈希的行数由配置文件或者默认值决定(flow_config.hash_size)
代码如下:
flow_hash = SCCalloc(flow_config.hash_size, sizeof(FlowBucket));
使用接口如下:
FlowGetFlowFromHash:通过数据包的信息获取连接,如果哈希表中没有,则创建flow;
FlowLookupFlowFromHash:通过数据包的信息查找连接,不会创建;
FlowGetFlowFromHash函数其主要流程是:
先计算出该数据包对应的flow的哈希值,然后通过哈希值去定位到某一行,再对该行加锁,此时,flow_hash的其他行可以访问的,
如果这一行不为空,再去遍历者这个链表(行),如果已经存在,对该链表节点加锁、解锁行锁返回,如果没有创建节点插入链表,对该链表节点加锁、解锁行锁返回,
如果这一行为空,创建节点插入链表,对该链表节点加锁、解锁行锁返回。
FlowLookupFlowFromHash函数其主要流程是:
先计算出该数据包对应的flow的哈希值,然后通过哈希值去定位到某一行,再对该行加锁,此时,flow_hash的其他行可以访问的,
如果这一行为空,解锁行锁返回,
如果这一行不为空,再去遍历者这个链表(行),如果已经存在,对该链表节点加锁、解锁行锁返回,如果没有,解锁行锁返回
以上两个函数的返回成功的话,会对该节点加锁保护,也是用来并发访问;并且都会将当前的这个节点放在该行的第一个节点,
作为缓存假定其为最活跃的、近期最可能被访问的内容。
源码如下:
Flow *FlowGetFlowFromHash(ThreadVars *tv, DecodeThreadVars *dtv, const Packet *p)
{
Flow *f = NULL;
FlowHashCountInit;
/* get the key to our bucket */
uint32_t hash = FlowGetHash(p); /* 连接的哈希值*/
/* get our hash bucket and lock it */
FlowBucket *fb = &flow_hash[hash % flow_config.hash_size]; /* 根据连接的哈希值去定位对应的行*/
/* 这个就是关键所在, 这就是对这一行(链表)加锁代码,而不是对整个哈希表进行加锁,可以提高并发 */
FBLOCK_LOCK(fb);
SCLogDebug("fb %p fb->head %p", fb, fb->head);
FlowHashCountIncr;
/* see if the bucket already has a flow */
if (fb->head == NULL) {
/* 如果哈希表的这一行为空表明这个链表还没有元素,则获取一个flow节点,如果该节点返回成功,则该节点被节点锁加锁,然后初始化,返回前将行锁解锁*/
f = FlowGetNew(tv, dtv, p);
if (f == NULL) {
FBLOCK_UNLOCK(fb);
FlowHashCountUpdate;
return NULL;
}
/* flow is locked */
fb->head = f;
fb->tail = f;
/* got one, now lock, initialize and return */
FlowInit(f, p);
f->flow_hash = hash;
f->fb = fb;
/* update the last seen timestamp of this flow */
COPY_TIMESTAMP(&p->ts,&f->lastts);
/* 行锁解锁 */
FBLOCK_UNLOCK(fb);
FlowHashCountUpdate;
return f;
}
/* ok, we have a flow in the bucket. Let's find out if it is our flow */
/* 该行有元素 则遍历该链表查找对应的flow*/
f = fb->head;
/* see if this is the flow we are looking for */
/* 该行的第一个元素是否是要查找的flow,0表示不是,1表示是*/
if (FlowCompare(f, p) == 0) {
Flow *pf = NULL; /* previous flow */
/* 遍历该链表,进行查找packet 对应的flow。若未有,则调用FlowGetNew创建,剩余部分与上面的代码基本一致*/
while (f) {
FlowHashCountIncr;
pf = f;
f = f->hnext;
if (f == NULL) {
/*若调用成功, 节点加锁 */
f = pf->hnext = FlowGetNew(tv, dtv, p);
if (f == NULL) {
FBLOCK_UNLOCK(fb);
FlowHashCountUpdate;
return NULL;
}
fb->tail = f;
/* flow is locked */
f->hprev = pf;
/* initialize and return */
FlowInit(f, p);
f->flow_hash = hash;
f->fb = fb;
/* update the last seen timestamp of this flow */
COPY_TIMESTAMP(&p->ts,&f->lastts);
/* 行解锁 */
FBLOCK_UNLOCK(fb);
FlowHashCountUpdate;
return f;
}
if (FlowCompare(f, p) != 0) {
/* we found our flow, lets put it on top of the
* hash list -- this rewards active flows */
/* 找到,链表插入到链表头操作,暗示该节点很有可能在短时间内是活跃的,会被经常查找,用来提高效率 */
if (f->hnext) {
f->hnext->hprev = f->hprev;
}
if (f->hprev) {
f->hprev->hnext = f->hnext;
}
if (f == fb->tail) {
fb->tail = f->hprev;
}
f->hnext = fb->head;
f->hprev = NULL;
fb->head->hprev = f;
fb->head = f;
/* found our flow, lock & return */
/* 节点加锁 */
FLOWLOCK_WRLOCK(f);
/* update the last seen timestamp of this flow */
COPY_TIMESTAMP(&p->ts,&f->lastts);
/* 行解锁 */
FBLOCK_UNLOCK(fb);
FlowHashCountUpdate;
return f;
}
}
}
/* lock & return */
/* 节点加锁 */
FLOWLOCK_WRLOCK(f);
/* update the last seen timestamp of this flow */
COPY_TIMESTAMP(&p->ts,&f->lastts);
/* 行解锁 */
FBLOCK_UNLOCK(fb);
FlowHashCountUpdate;
return f;
}
查找接口比查找并插入接口(也就是上面的函数)简单,具体如下:
Flow *FlowLookupFlowFromHash(const Packet *p)
{
Flow *f = NULL;
/* get the key to our bucket */
uint32_t hash = FlowGetHash(p); /* 连接的哈希值*/
/* get our hash bucket and lock it */
FlowBucket *fb = &flow_hash[hash % flow_config.hash_size];/* 根据连接的哈希值去定位对应的行*/
/* 行锁 */
FBLOCK_LOCK(fb);
SCLogDebug("fb %p fb->head %p", fb, fb->head);
/* see if the bucket already has a flow */
if (fb->head == NULL) {
/* 未找到, 行解锁 */
FBLOCK_UNLOCK(fb);
return NULL;
}
/* ok, we have a flow in the bucket. Let's find out if it is our flow */
/* 该行有元素 则遍历该链表查找对应的flow*/
f = fb->head;
/* see if this is the flow we are looking for */
/* 该行的第一个元素是否是要查找的flow,0表示不是,1表示是*/
if (FlowCompare(f, p) == 0) {
while (f) {
FlowHashCountIncr;
f = f->hnext;
if (f == NULL) {
/* 行解锁 */
FBLOCK_UNLOCK(fb);
return NULL;
}
if (FlowCompare(f, p) != 0) {
/* we found our flow, lets put it on top of the
* hash list -- this rewards active flows */
/* 找到,链表插入到链表头操作,暗示该节点很有可能在短时间内是活跃的,会被经常查找,用来提高效率 */
if (f->hnext) {
f->hnext->hprev = f->hprev;
}
if (f->hprev) {
f->hprev->hnext = f->hnext;
}
if (f == fb->tail) {
fb->tail = f->hprev;
}
f->hnext = fb->head;
f->hprev = NULL;
fb->head->hprev = f;
fb->head = f;
/* found our flow, lock & return */
/* 节点加锁 */
FLOWLOCK_WRLOCK(f);
/* update the last seen timestamp of this flow */
COPY_TIMESTAMP(&p->ts,&f->lastts);
/* 行解锁 */
FBLOCK_UNLOCK(fb);
return f;
}
}
}
/* lock & return */
/* 节点加锁 */
FLOWLOCK_WRLOCK(f);
/* update the last seen timestamp of this flow */
COPY_TIMESTAMP(&p->ts,&f->lastts);
/* 找到, 行解锁 */
FBLOCK_UNLOCK(fb);
return f;
}
因此本文讨论的是针对多线程架构下面,在并发访问共享链式哈希表时,应该使用更细粒度的行级锁和节点锁,而不是粗暴型的一把表锁,
但是如果该哈希表如果元素达到千万级别的话,需要使用其他机制来替换这些细粒度的锁,例如innodb的页锁来升级行锁等等机制。
这是本人对suricata中使用行锁的一点浅见,请大家批评指正