这块的东西我现在还没有用到,所以很不厚道的抄了背着笔记本流浪的原文下来。
简介
Suricata中使用队列来缓存数据包,包括缓存线程模块内部新产生数据包的线程内队列,以及线程之间用来传递数据包的线程间队列。
用于表示数据包队列的结构体为PacketQueue,其定义如下(省略了调试相关字段):
typedef struct PacketQueue_ {
Packet top; / 头指针 */
Packet bot; / 尾指针 */
uint32_t len; /* 队列长度 */
SCMutex mutex_q; /* 互斥锁 */
SCCondT cond_q; /* 条件变量 */
} PacketQueue;
线程内队列
线程内队列与线程内的每个slot相绑定,其又可细分为pre队列和post队列,定义如下:
typedef struct TmSlot_ {
…
/* queue filled by the SlotFunc with packets that will
* be processed futher before the current packet.
* The locks in the queue are NOT used */
PacketQueue slot_pre_pq;
/* queue filled by the SlotFunc with packets that will
* be processed futher after the current packet. The
* locks in the queue are NOT used */
PacketQueue slot_post_pq;
}
关于pre队列和post队列之间的用法区别,注释中已经说得很清楚了。具体使用方式可参考前面的关于数据包源的笔记。
线程间队列
Suricata中使用了一个全局数组作为所有的线程间队列的存储,定义为:
PacketQueue trans_q[256];
这里为什么使用全局静态数组,而不是更直观更省内存的在运行时按需动态分配呢?主要是出于性能考虑。按需动态分配,就要求将队列组织为链表(当然,动态数组也行,但实现相对复杂,且完全没有必要),而为了支持队列负载均衡,需要将数据包在各个数据包之间进行分配,像hash这种分配方式是需要能够按照队列索引进行随机访问的。并且,由于所需队列个数在编译时就已经确定,因此使用全局静态数据是最合适的了。
然而,由于PacketQueue结构只是单纯用来存储,Suricata中另外使用了一个Tmq结构体实现对队列的管理。
typedef struct Tmq_ {
char name; / 队列名字 */
uint16_t id; /* 对应的队列存储在trans_q中的索引 */
uint16_t reader_cnt; /* 读这个队列中数据的线程数 */
uint16_t writer_cnt; /* 向这个队列写数据的线程数 */
uint8_t q_type; /* 队列类型, 0为数据包队列,1为数据队列 */
} Tmq;
同样地,与trans_q对应,Suricata也使用了一个全局数组作为Tmq的存储,与之相关的定义包括:
define TMQ_MAX_QUEUES 256
static uint16_t tmq_id = 0;
static Tmq tmqs[TMQ_MAX_QUEUES];
相应地,创建队列的函数TmqCreateQueue流程为:
1. 获取tmqs[tmq_id]的地址,作为返回的Tmq指针。
2. 设置该Tmq的name、id、type,其中id设为tmq_id,然后递增tmq_id。
该函数在TmThreadCreate中被直接或间接调用。
● 对于线程的inq,首先查inq_name对应的队列是否已存在,若不存在则直接调用TmqCreateQueue创建。
● 对于线程的outq,则首先要看outqh的OutHandlerCtxSetup是否设置,若设置了则会调用该函数去间接创建队列,否则跟创建inq类似。间接创建目前只有一种情况:使用”flow”作为线程的outqh->TmqhOutputFlowSetupCtx->StoreQueueId解析队列名字符串->TmqCreateQueue。
下面的小节都是关于线程间队列的。
队列数量
Suricata中究竟会创造多少个线程间队列呢?这取决于系统的运行模式。
运行模式 | 线程数 | 队列数 | 说明 |
---|---|---|---|
single | IDS: 取决于接口配置; IPS: 取决于nqueue | 0 | 单线程模式,不需要队列 |
workers | 取决于网卡数量; IDS: 取决于接口配置; IPS: 取决于nqueue | 0 | 多线程工人模式,但每个线程做所有事(从抓包到输出日志),因此也不需要队列 |
autofp | IDS:多个包获取线程,多个其他线程; IPS:与IDS下类似,但裁决/响应为单独线程 | 多个pickup队列,其余各1个 | 多线程自动流绑定负载均衡模式(auto flow pinned load balancing),每个流的包只会被同一个检测线程处理(flow pinned)。其中pickup对立个数与其reader线程数量一致,因为每个线程只能有1个inq。 |
队列Handler
现在队列有了,线程也创建好了,那么线程与数据包队列之间是如何传递数据包的呢?
例如,前面的笔记中已经记录了包含ReceivePcap模块和DecodePcap模块的线程的执行流程,在这两个模块处理完数据包后,接下来这些数据包怎么送到pickup队列去呢?
这时候,队列Handler就起作用了。每个线程会在其ThreadVars中绑定两个Handler:
● tmqh_in:在初始化时绑定为inqh的InHander,用于从上一级队列中获取数据包。
● tmqh_out:在初始化时绑定为outqh的OutHander,用于将处理后的数据包送往下一级队列。
系统初始化时会调用TmqhSetup注册所有队列handler,类似包括:
类型 | 说明 |
---|---|
simple | 简单地从inq获取数据包,线程处理完后将包送往唯一的outq。 |
packetpool* | 从数据包池中获取数据包,将数据包放回数据包池。见下面注释。 |
flow | 用于autofp模式的handler,实现流的绑定和负载均衡。 |
注:目前Suricata中关于packet pool的部分比较乱。例如,虽然包获取线程的inq和inqh都设为”packetpool”,但首先来说,对于包获取线程inq是没意义的,因此这里这么设置给程序中带来很多特殊处理(TmThreadCreate),我认为设为NULL其实更好理解;其次,inqh这么设可能本意是想统一包获取过程,即不区分抓包或者从队列入包,都统一从tmqh_in来得到包,然而实际上Suricata中却做了特殊区分,对于包获取线程其slot类型为”pktacqloop”,相应的执行函数TmThreadsSlotPktAcqLoop并没有调用tmqh_in,而是调用其第一个slot中模块的PktAcqLoop。另外,TmqhOutputPacketpool函数作为”packetpool”这个handler的OutHandler,本应该只能由线程的tmqh_out来调用,但实际上程序中却到处都有调用(主要目的是跳过后续的线程处理,直接返回到数据包池中)。总之,这块是我目前感觉到Suricata中最ugly的。。。
由于autofp是目前几乎全部运行模式类型下的默认模式(参见RunModeDispatch),下面将重点介绍”flow”队列handler的实现。
“flow”
首先,TmqhFlowRegister完成”flow”的注册,包括InHandler、OutHandler、OutHandlerCtxSetup/Free等。
InHandler:由于每个线程只有一个inq,因此”flow”的输入函数TmqhInputFlow与”simple”是一致的,直接从inq取包就可以了。
OutHandler:”flow”有多个OutHandle,分别对应不同的负载均衡策略
Suricata支持用户通过”autofp-scheduler”参数对以上策略进行选择,默认为active-packets。
OutHandlerCtxSetup/Free:这个是”flow”模块目前独有的。使用”flow”作为outqh的线程都会有多个输出队列(如pickup1, pickup2, …),因此直接调用TmqCreateQueue创建ThreadVars的outq不太方便,因此Suricata就把这个任务下放到outqh的OutHandlerCtxSetup去做了。
“flow”对应的OutHandlerCtxSetup为TmqhOutputFlowSetupCtx,函数原型为:
/**
* \brief setup the queue handlers ctx
*
* Parses a comma separated string “queuename1,queuename2,etc”
* and sets the ctx up to devide flows over these queue’s.
*
* \param queue_str comma separated string with output queue names
*
* \retval ctx queues handlers ctx or NULL in error
*/
void *TmqhOutputFlowSetupCtx(char *queue_str)
函数的功能、参数及返回值在上面的注释已经说得比较清楚了。具体实现流程为:
1. 创建一个TmqhFlowCtx,该结构体将被存储在ThreadVars的outctx中,并在线程调用tmqh_out时被”flow”的OutHandler所利用。
2. 循环解析queue_str,将每个qname传给StoreQueueId,以创建相应名字队列并存储到ctx中。
下面介绍各个OutHandler的实现。首先,需要知道Flow中有一个字段是专门用于autofp的:
typedef struct Flow_ {
…
/* flow queue id, used with autofp /
SC_ATOMIC_DECLARE(int, autofp_tmqh_flow_qid);
}
这个字段用来记录这个流所被绑定到的输出队列的ID,在FLOW_INITIALIZE中被设为-1,表示还未被绑定。而这个绑定关系,就是由下面这些OutHandler所确定的:
TmqhOutputFlowRoundRobin
1. 首先需要确定一个该包将被送往的qid。
2. 若数据包有对应的flow,则首先获取该flow的autofp_tmqh_flow_qid,若为-1说明还没绑定,就将qid和autofp_tmqh_flow_qid都设为round robin的下一个值(ctx->round_robin_idx + 1 % ctx->size),并将。相应队列的total_flows计数器+1(用于统计负载分配情况)。
3. 若没有对应flow,则简单的使用(ctx->last + 1) % ctx->size,即将所有这种包单独进行round robin。其他OutHandler也都是这么处理的。注意,这种包实际上很少,比如ICMPv4的报错包,或者内存不够导致无法分配flow。
4. 确定好qid了,就把包enqueue到对应PacketQueue就行了,然后cond_signal这个队列的cond,以唤醒等待这个队列的线程。
下面两个Handler整体流程类似,只是第2步为flow绑定qid的方式不同。
TmqhOutputFlowActivePackets:qid设置为所有输出队列中len最小的队列。
TmqhOutputFlowHash:qid设置为 ((p->flow)>>7 % ctx->size)。个人感觉这样利用指针的hash分布会不均匀,毕竟连续分配的指针的高位很可能是相同的。
最后需要注意一点的是,若两个(组)线程通过数据包队列对接,则前面线程的outqh应该和后面线程的inqh应该保持一致。