zabbix是采用Automake方式构建的开源项目,服务和工具是通过C语言实现,实现来跨平台的能力,目前zabbix_server还不支持Windows系统。
主要的目录是:
src为C代码的源码目录,include为C代码的头文件,frontends为前端php代码。
src目录libs,modules和各功能主程序的目录,zabbix_agent就是本文需要分析的主目录。
zabbix的代码中为了跨平台和开发的简洁采用了大量的宏定义,其中很多对于阅读代码而言又是一种煎熬:)
int MAIN_ZABBIX_ENTRY(int flags)
MAIN_ZABBIX_ENTRY定义来功能程序的开始入口,在这个入口之前,由真正的main函数调用,并且在调用之前需要检查参数,并对无需进入程序主功能的参数直接执行并处理。所以定义了MAIN_ZABBIX_ENTRY的主程序文件(例如:zabbix_agent.c),就是实现zabbix代理功能的主入口,命令行参数的检测,help信息的输出,服务的安装,等等都由具体的程序模块实现。
通过这个宏,就相当于定义了实现业务功能的main,无论是通过服务(Windows),还是通过deamon(*nix下的守护进程)启动业务功能,都可以直接调用MAIN_ZABBIX_ENTRY(0)启动业务功能。
#if defined(_WINDOWS)
#define ZBX_THREAD_ENTRY(entry_name, arg_name) \
unsigned __stdcall entry_name(void *arg_name)
#else /* not _WINDOWS */
#define ZBX_THREAD_ENTRY(entry_name, arg_name) \
unsigned entry_name(void *arg_name)
#endif
ZBX_THREAD_ENTRY定义了功能线程的开始入口,如果需要调用一个开始线程,调用如下:
zbx_thread_start(collector_thread, thread_args);
而collector_thread的函数声明如下:
ZBX_THREAD_ENTRY(collector_thread, args)
这样通过宏,实际就是定义了函数collector_thread。直接定义函数不好吗,非要通过宏来定义一个函数的名称吗?对于跨平台系统而言,是必须的,如果不通过宏定义,那么对于windows上的stdcall的声明就需要特殊定义了,或者定义一个STDAPI的宏,实现Windows和*nix系统调用方式不同的生命方式(Windows系统定义为__stdcall,而*nix系统定义为空宏),相比较而言,zabbix采取的方法更好。
zaibbx的内存实现代码在libs/common/misc.c
内存函数共4个:
#define zbx_calloc(old, nmemb, size) zbx_calloc2(__FILE__, __LINE__, old, nmemb, size)
#define zbx_malloc(old, size) zbx_malloc2(__FILE__, __LINE__, old, size)
#define zbx_realloc(src, size) zbx_realloc2(__FILE__, __LINE__, src, size)
#define zbx_strdup(old, str) zbx_strdup2(__FILE__, __LINE__, old, str)
实际的实现函数后面有数字2(利用宏实现对用户调用文件和行号进行跟踪,具体实现在misc.c中。
这里,咋们通过看一段代码就知道zabbix设计得有意思的地方:
void *zbx_malloc2(const char *filename, int line, void *old, size_t size)
{
int max_attempts;
void *ptr = NULL;
/* old pointer must be NULL */
if (NULL != old)
{
zabbix_log(LOG_LEVEL_CRIT, "[file:%s,line:%d] zbx_malloc: allocating already allocated memory. "
"Please report this to Zabbix developers.",
filename, line);
}
for (
max_attempts = 10, size = MAX(size, 1);
0 < max_attempts && NULL == ptr;
ptr = malloc(size), max_attempts--
);
if (NULL != ptr)
return ptr;
zabbix_log(LOG_LEVEL_CRIT, "[file:%s,line:%d] zbx_malloc: out of memory. Requested " ZBX_FS_SIZE_T " bytes.",
filename, line, (zbx_fs_size_t)size);
exit(EXIT_FAILURE);
}
首先,他需要提供申请空间的指针,用于检测是否重复分配内存。调用函数是通过zbx_malloc调用实现内存分配,例如下面代码:
static ZBX_METRIC *commands = NULL;
//...
commands = zbx_malloc(commands, sizeof(ZBX_METRIC));
commands是一个内存变量指针。
并且,在申请内存时尝试调用malloc函数10次,这样是否有实际意义?不得而知!
如果分配失败,直接退出进程,结束程序运行。
在代码中,还可以看到很多zbx_realloc的调用,用于动态扩展需要分配的空间。
例如下面代码添加一个需要采集的指标到运行时采集列表:
int add_metric(ZBX_METRIC *metric, char *error, size_t max_error_len)
{
int i = 0;
while (NULL != commands[i].key)
{
if (0 == strcmp(commands[i].key, metric->key))
{
zbx_snprintf(error, max_error_len, "key \"%s\" already exists", metric->key);
return FAIL; /* metric already exists */
}
i++;
}
commands[i].key = zbx_strdup(NULL, metric->key);
commands[i].flags = metric->flags;
commands[i].function = metric->function;
commands[i].test_param = (NULL == metric->test_param ? NULL : zbx_strdup(NULL, metric->test_param));
commands = zbx_realloc(commands, (i + 2) * sizeof(ZBX_METRIC));
memset(&commands[i + 1], 0, sizeof(ZBX_METRIC));
return SUCCEED;
}
这里用到了zbx_realloc来实现内存扩展,也就是,内存不是一次性分配好的,而是不断申请和分配的。内存分配开始是总是保留一个空位,数据开始填充在空位,然后重新分配一个空间(使用realloc实现内存复制),并设置最后一个空位为0值。
这种分配内存的模式在zabbix源码中很常见,熟悉后,阅读起来就会省不少时间。
这里再说一下zbx_free的实现
#define zbx_free(ptr) \
\
do \
{ \
if (ptr) \
{ \
free(ptr); \
ptr = NULL; \
} \
} \
while (0)
zbx_free就是通过C语言的free实现的,这里为什么需要采用一个do-while的写法呢?
这是在C中实现多行宏定义的一个通用方法,防止语句在宏扩张是产生语义错误,例如如果宏用在如下代码时:
#define zbx_free(p) \
free(p); \
p = NULL;
int status = do_somthing();
if (status == 0) zbx_free(ptr);
else{
//using p pointer to do somthing...
}
如果zbx_free没有采用do-while的写法,那么就会出现错误的宏扩展:
if (status == 0) free(p);
ptr = NULL;
else{
// using p pointer to do somthing...
}
以上代码就会产生编译错误,最不好的情况是,直接出现运行时错误。原因就是宏使用时没有采用{}来包裹导致的问题,而采用do-while(0)时,该问题就可以避免。
zabbix处理字符串也是采用封装的方式进行,主要包括如下几个函数:
#define zbx_strdup(old, str) zbx_strdup2(__FILE__, __LINE__, old, str)
#define ZBX_STRDUP(var, str) (var = zbx_strdup(var, str))
一般采用zbx_strdup函数实现字符串的复制。
例如前面的指标名称复制就是调用该函数实现复制的:
commands[i].key = zbx_strdup(NULL, metric->key);
zbx_strdup的实现任采用C库的strdup实现,不过处理的原指针的释放。
char *zbx_strdup2(const char *filename, int line, char *old, const char *str)
{
int retry;
char *ptr = NULL;
zbx_free(old);
for (retry = 10; 0 < retry && NULL == ptr; ptr = strdup(str), retry--)
;
if (NULL != ptr)
return ptr;
zabbix_log(LOG_LEVEL_CRIT, "[file:%s,line:%d] zbx_strdup: out of memory. Requested " ZBX_FS_SIZE_T " bytes.",
filename, line, (zbx_fs_size_t)(strlen(str) + 1));
exit(EXIT_FAILURE);
}
并且,仍然采用多次尝试的方法复制字符串。
首先声明,这个地方有点复杂(^_^)
函数的声明是这样:
void zbx_strarr_init(char ***arr)
{
*arr = zbx_malloc(*arr, sizeof(char *));
**arr = NULL;
}
三个星号,你看看,太过复杂吧!一般人不用他!谁叫他是zabbix呢?
代码创建一个二维指针空间:
p -> [x1, x2, x3, ...]
| | | |
v v v v
char* char* null
这里参数为三个*,是为了把两个的变量通过指针传递给zbx_strarr_init,以便初始化。
为更好的理解二维数组,我编写了以下简要的代码:
#include
#include
void test_p(int **p)
{
// 外部实际是一个指针(一维)
// 通过**传递,是希望由该函数实现内存分配,并输出到外部的指针变量
// 这里为了方便,使用了临时变量a,如果直接使用p实现赋值,
// 就需要通过数组方式访问咯,例如*p[0], *p[1] *p[2]
int *a = *p = malloc(sizeof(int)*3);
*a++=1;
*a++=2;
*a++=3;
}
void test_pp(int ***p)
{
// 二维指针的变量
// 首先分配一维的指针变量
// 然后对一维指针变量初始化数据内存
int **a = *p = malloc(sizeof(int*) * 3);
*a++ = malloc(sizeof(int) * 3);
*a++ = malloc(sizeof(int) * 3);
*a++ = malloc(sizeof(int) * 3);
int **b = *p;
printf("0x%lx\r\n", *b++);
printf("0x%lx\r\n", *b++);
printf("0x%lx\r\n", *b++);
int **c = *p;
int *c1 = *c++;
int *c2 = *c++;
int *c3 = *c++;
*c1++ = 1; *c1++ = 2; *c1++ = 3;
*c2++ = 4; *c2++ = 5; *c2++ = 6;
*c3++ = 7; *c3++ = 8; *c3++ = 9;
}
void test_x(int *p){
*p++=1;
*p++=2;
*p++=3;
}
int main(){
printf("(1)*\r\n");
int a[3] = {0};
test_x(a);
printf("%d %d %d\r\n", a[0], a[1], a[2]);
printf("(2)**\r\n");
int *b = 0;
test_p(&b);
printf("%d %d %d\r\n", b[0], b[1], b[2]);
free(b);
printf("(3) ***\r\n");
int **c = 0;
test_pp(&c);
int *p;
for (int i = 0; i < 3; i++){
p = c[i];
printf("addr is %lx\r\n", p);
for (int j = 0; j < 3; j++){
printf("%d ", p[j]);
}
printf("\r\n");
free(p);
}
free(c);
return 0;
}
上面程序执行的结果是:
(1)*
1 2 3
(2)**
1 2 3
(3) ***
0x7fda91c025d0
0x7fda91c02600
0x7fda91c02610
addr is 7fda91c025d0
1 2 3
addr is 7fda91c02600
4 5 6
addr is 7fda91c02610
7 8 9
现在看zabbix的二维数组的初始化代码,实际上,只是分配咯一个元素的一位数组指针,并设置为空。
下面来看添加字符串实现:
void zbx_strarr_add(char ***arr, const char *entry)
{
int i;
assert(entry);
for (i = 0; NULL != (*arr)[i]; i++)
;
*arr = zbx_realloc(*arr, sizeof(char *) * (i + 2));
(*arr)[i] = zbx_strdup((*arr)[i], entry);
(*arr)[++i] = NULL;
}
其实和前面的示例一样,不过这里通过zbx_realloc实现内存扩张。首先,寻找最后一个为null元素,i 表示元素个数-1,所以为了扩张一个元素,需要i+2个指针变量的区域。
然后,复制字符串到倒数一个指针位置,同时设置最后一个位置为null。