HAProxy 代码阅读(一)
(2013-04-09 21:12:37)
标签:
haproxy代码阅读it |
分类: linux |
HAproxy是一个简单强大的http/tcp负载均衡工具,其代码量只有5w多行,以下是一些阅读代码的过程的记录,方便以后查看。
HAProxy是一个单进程事件驱动的模型。
首先看一个比较重要的结构体:
struct poller {
};
该结构体是事件循环的核心,各个字段都有比较详细的注释,就不解释了,注意一下函数指针前面的 REGPRM2和REGPRM1,这两个宏指示编译器,在函数调用时,参数传递采用寄存器传递。
__attribute__((constructor))
static void _do_register(void)
{
struct poller *p;
if (nbpollers >= MAX_POLLERS)
return;
epoll_fd = -1;
p = &pollers[nbpollers++];
p->name = "epoll";
p->pref = 300;
p->private = NULL;
p->test = _do_test;
p->init = _do_init;
p->term = _do_term;
p->poll = _do_poll;
p->fork = _do_fork;
p->is_set = __fd_is_set;
p->cond_s = p->set = __fd_set;
p->cond_c = p->clr = __fd_clr;
p->rem = __fd_rem;
p->clo = __fd_clo;
}
该函数很明显就是给结构体中的函数指针赋值,但是却没有地方调用这个函数,原来被__attribute__((constructor))修饰的函数是在main函数之前自动调用的,见我的前一篇博客。
使用readelf
查看可执行文件,发现在我的系统上,haproxy调用了4个_do_register,分别是select/poll/epoll/sepoll,这几个pref值分别是100/200/300/400,可以看到sepoll的优先级是最高的。
sepoll在底层还是采用的epoll的系统调用如epoll_wait等,只是在外面加了一下优化,这个以后有机会再分析。
haproxy的主循环是 run_poll_loop();
展开该函数:
cur_poller 是个全局变量,指向正在使用的poller结构。该函数中主要分析
process_runnable_tasks 函数。
process_runable_tasks主要是从运行队列中取出task,调用其process函数,process函数返回之后将task放入等待队列。
为了保证所有task都及时响应,每次调用运行队列取出的task最大为运行队列的1/4,同时个数不超过200个。
task结构体如下:
struct task {
};
上 面介绍了HAProxy的主循环 run_poll_loop(),在该函数的最后是调用
cur_poller.poll(&cur_poller, next); ,其中cur_poller是指当前被使用的poller结构,本篇
以linux的epoll为例来看一下poller的工作方式。
首先看其初始化函数:
__attribute__((constructor))
static void _do_register(void)
{
struct poller *p;
p = &pollers[nbpollers++];
p->name = "epoll";
p->private = NULL;
p->test = _do_test;
p->poll = _do_poll;
p->cond_s = p->set = __fd_set;
p->cond_c = p->clr = __fd_clr;
p->rem = __fd_rem;
}
其中poll 初始化为
_do_poll,在_do_poll中主做了两件事情:1、调用epoll_wait等待事件;2、调用相应fd的接收/发送函数。
调
用epoll_wait之前需要计算出最大允许事件个数,这里稍微有点奇怪的是,并没有检查epoll_wait的返回码,即当epoll_wait返回
-1时没有做处理,当然如果是因为信号中断而返回-1,run_poll_loop里面还是有处理的,但是其他错误,好像没有处理。
调用fd的接收/发送函数也是通过函数指针调用的,类似于下面的情形:
fdtab[fd].cb[DIR_RD].f(fd);
fdtab是一个结构体名称,也是一个全局变量名称,全局变量在main函数中进行初始化,fdtab结构体定义如下:
struct fdtab {
};
其中cb 有接收/发送的函数和缓冲区初始化,owner
在处理客户端连接时初始化为task,处理listen时初始化为listener。
fdtab初始化主要是进行了空间的分配,以及state的初始化,代码如下:
接下来查看cb 以及owner在哪里初始化,在client.c里面:
int event_accept(int fd) {
struct listener *l =
fdtab[fd].owner;
.......
struct task *t;
......
t = task_new()
.......
s->si[0].owner = t;
fdtab[cfd].owner = &s->si[0];
//owner 初始化为 task
fdtab[cfd].cb[DIR_RD].f = l->proto->read;
fdtab[cfd].cb[DIR_RD].b = s->req;
fdtab[cfd].cb[DIR_WR].f = l->proto->write;
fdtab[cfd].cb[DIR_WR].b = s->rep;
......
}
可见,初始化cb的是lintener里面的proto结构体。而该结构体是在哪里初始化呢?
在proto_tcp.c 中有如下代码:
void tcpv4_add_listener(struct listener
*listener)
{
if (listener->state != LI_INIT)
return;
listener->state =
LI_ASSIGNED;
listener->proto =
&proto_tcpv4;
LIST_ADDQ(&proto_tcpv4.listeners,
&listener->proto_list);
{