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流程为:
该函数在TmThreadCreate中被直接或间接调用。
下面的小节都是关于线程间队列的。
Suricata中究竟会创造多少个线程间队列呢?这取决于系统的运行模式。
运行模式 | 线程数 | 队列数 | 说明 |
single | 1 | 0 | 单线程模式,不需要队列 |
workers | IDS: 取决于接口配置 IPS: 取决于nqueue |
0 | 多线程工人模式,但每个线程做所有事(从抓包到输出日志),因此也不需要队列 |
auto | IDS:多个检测线程,其它各类线程均一个 IPS:一个解码/重组线程,其它各类线程都是多个 |
各类线程间均1个 | 多线程自动模式,同一个流的数据包可能被多个检测线程处理。 |
autofp | IDS:多个包获取线程,多个其他线程 IPS:与IDS下类似,但裁决/响应为单独线程 |
多个pickup队列,其余各1个 | 多线程自动流绑定负载均衡模式(auto flow pinned load balancing),每个流的包只会被同一个检测线程处理(flow pinned)。其中pickup对立个数与其reader线程数量一致,因为每个线程只能有1个inq。 |
现在队列有了,线程也创建好了,那么线程与数据包队列之间是如何传递数据包的呢?
例如,前面的笔记中已经记录了包含ReceivePcap模块和DecodePcap模块的线程的执行流程,在这两个模块处理完数据包后,接下来这些数据包怎么送到pickup队列去呢?
这时候,队列Handler就起作用了。每个线程会在其ThreadVars中绑定两个Handler:
系统初始化时会调用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的实现。
首先,TmqhFlowRegister完成"flow"的注册,包括InHandler、OutHandler、OutHandlerCtxSetup/Free等。
InHandler:由于每个线程只有一个inq,因此"flow"的输入函数TmqhInputFlow与"simple"是一致的,直接从inq取包就可以了。
OutHandler:"flow"有多个OutHandle,分别对应不同的负载均衡策略,目前支持的策略如下:
策略 | 对应函数 | 说明 |
round-robin | TmqhOutputFlowRoundRobin | 轮转调度:按照轮转方式依次选择下一个队列 |
active-packets | TmqhOutputFlowActivePackets | 最短匹配 :选择当前最短(数据包最少)的队列 |
hash | TmqhOutputFlowHash | 随机哈希:按照流指针的Hash值选择队列 |
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)
函数的功能、参数及返回值在上面的注释已经说得比较清楚了。具体实现流程为:
下面介绍各个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
下面两个Handler整体流程类似,只是第2步为flow绑定qid的方式不同。
TmqhOutputFlowActivePackets:qid设置为所有输出队列中len最小的队列。
TmqhOutputFlowHash:qid设置为 ((p->flow)>>7 % ctx->size)。个人感觉这样利用指针的hash分布会不均匀,毕竟连续分配的指针的高位很可能是相同的。
最后需要注意一点的是,若两个(组)线程通过数据包队列对接,则前面线程的outqh应该和后面线程的inqh应该保持一致。