suricata 源码分析之行锁

本文探讨了Suricata在多线程环境下采用细粒度行级锁和节点锁优化链式哈希表访问效率的方法。通过对suricata源码的分析,详细介绍了行级锁在连接管理模块的应用及其实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     此博客讨论的是在多线程下链式哈希表的行锁的使用,主要是针对对哈希表的表锁,二者各有优缺点。

     表锁哈希表,只有一把锁,内存少,实现简单,但是在并发访问时,效率极其低下;

     行级锁哈希表则每一行都有一把锁,内存开销大,实现复杂,但是在大并发,高效率的后台服务程序中使用非常广泛。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中使用行锁的一点浅见,请大家批评指正

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值