目前的HTTP代理大部分都是基于Windows或是Linux用户态的应用程序,而作为Linux内核模块的HTTP PROXY并不多见.在性能方面,Apache、SQUID和基于Windows代理的表现无法令人满意.文中以国家十五863软件重大专项“面向宽带多媒体网络通信设备的嵌入式软件平台”为背景,为了满足多客户大容量并发的网络请求,提出了如何在Linux内核2.4下设计实现一个HTTP Proxy.为了叙述方便,文中用KHttp Proxyd表示该代理,介绍如何进行Linux内核编程[1]以及KHttp的拷贝[.1因,3]此,性能得到大幅度的提[2]高.3 3ΟProxyd的设计和实现.最后通过测试,证实KHttp ProxydSQU ID比Apache提高了3~5倍.
1 Linux内核网络编程
H T TP代理调用最频繁的操作为网络数据的收发函数的操作.一个处于用户态Linux应用程序收发网络包的函数流程大致如下:
asmlinkage long sys_send(int fd,void buff,size
_t len,unsigned flags)
{return sys_sendto(fd,buff,len,flags,NULL,0);
}
Asmlinkage long sys_ recv(int fd,void ubuff,size_t size,unsigned flags)
{
return sys _ receivefrom(fd,ubuff,size,flags,
NULL,NULL);
}
以sys_sendto为例,它会通过调用move_addr _to_kernel把用户态的数据拷贝到内核态,再通过调用内核态的函数sock_sendmsg把数据包继续处理下去.同理,sys _ recvfrom会通过调用sock _ recvmsg读取数据包,再调用move_addr_ to _ user把核心态所读取到的数据包拷贝到用户态空间的内存中.由此可见,一个用户态的H T TP代理在处理网络数据时会不停地在用户态和内核态之间切换.但是如果H T TP代理本身就是运行在内核态的一个模块,则可以省去出发80号中断来做用户态和内核态的切换,以及内核态和用户态数据之间
2 KHttp Proxyd系统构架
KHttp Proxyd系统结构见图1.图1从启动到关闭KHttp Proxyd,可以分为5个流程:
①载入KHttp Proxyd模块;②启动KHttp Proxyd;③运行状态;④关闭KHttp Proxyd;⑤卸载KHttp Proxyd模块.首先,通过insmod命令来装载,然后通过echo命令将配置参数写到/proc/KHttp Proxyd.比如开启端口,允许最大连接数等参数,然后再将启动命
令echo start>/proc/KHttp Proxyd,此时,KHttp Proxyd就已经运行.输入echo stop>/proc/KHttp Proxyd,即可停止KHttp Proxyd运行,而
Rmmod就卸载了KHttp Proxyd模块.
图1 KHttpProxyd系统构架
Fig.1 KHttpProxyd architecture
一般的服务程序对多个客户端的处理有2种模式:模式1,为每个客户端开启1个线程,该线程对该客户端进行处理;模式2,把每个客户端添加
到客户端列表里,对该列表进行轮循.模式1在小容量时运行较好,流程也较清晰,但是当客户容量增大时,随着系统开启线程数目的增加,系统性能将会明显下降;而模式2在小容量模式下,轮询相对模式1比较消耗资源,因为要不断地检查每个客户端是否有数据.但在大容量模式下,会表现出明显的性能优势,因为它没有增加新的线程,只是客户端列表增大,轮询浪费的资源也越来越少.另外还可以通过把Socket放到等待列表里睡眠,以解决轮询浪费资源的问题.当做完1个轮询没有作任何有用的处理时,便会使主流程进入休眠状态,直到超时或是通过Socket触发唤醒.因此,
KHttp Proxyd采用了模式2.[4Ο5]
3 KHttp Proxyd初始化
KHttp Proxyd初始化包括5个步骤.
1)初始化配置文件系统;
2)从配置文件系统读取参数的值,包括代理服务内核线程数目,代理监听端口,允许最大连接数目等;
3)一次性申请用来保存客户端链表所需的内核空间;
第6期张黄瞩等:基于Linux内核2.4的高性能H TTP代理647
4)启动监听端口;
5)启动代理服务线程.
3.1内存申请因为客户端不断地进行连接与断开操作,所以采用一次性申请内核空间.如果每次客户端连接上来采用kmalloc为该客户端分配1个数据结构,每次客户端断开采用kfree释放该客户端的数据,会产生2个缺点:①要频繁地进行kmalloc/kfree操作,造成性能损失;②最终造成内存碎片.
所以在初始化时,一次性按照最大容量的客户端申请好内存,然后每次客户端连接时从该内存中获取1块,用完释放到该内存池中即可,不会进行频繁的内存申请与释放操作,也不会带来内存碎片的问题[6].初始化代码如下:
int init_sock_node_free_list(int size)//一次性申请最大容量的内存,串成链表
{
⋯
node->next=NULL;sock_node_free_list_ head=prev=node;for(i=1;i<size;i++){node=(struct sock _ node 3)kmalloc(sizeof
(struct sock_node),GFP_ KERN EL);
⋯
}⋯
return 0;
}int free_sock_node_free_list()//最后释放内存
{
⋯
while(tmp){
tmphead=tmp->next;kfree(tmp);tmp=tmphead;
}
}
struct sock_node get _ sock_node_free_list()//从内存池中申请一块内存
{
struct sock_nodetmp;
tmp=sock_node_free_list_head;
if(tmp){
sock_node_free_list_head=tmp->next;
init_node(tmp);
}
return tmp;
}
void put_sock_node_free_list(struct sock_n tmp)//用完释放到内存池
{
if(tmp){
tmp->next=sock_node_free_list_he
sock_node_free_list_head=tmp;
}
}
3.2初始化监听端口基于Linux内核的Socket编程与用户大的不同.下面的代码演示了如何创建在内
中创建1个监听Socket[2].
int init_sock(struct socket)
{
struct sockaddr_in sin;
error=sock _ create(PF _ IN ET,S_
STREAM,IPPRO TO_ TCP
⋯
sin.sin_family=A F_ IN Es体,创建TCP类型的socket
sin.sin_addr.s_addr=IN;
sin.sin_port=htons((uno)error=(s)->ops->bind(s,(ct sockaddr)&sin,sizeof(sin))听
(s)->sk->reuse
error=(s)->opss 48);
//开始监听
⋯
}
而在用户态模式下,创建1个监听Sock
通过80h号中断来调用系统调d ys
_listen,其中sys_bind实现代i实现代码与其相似):
asmlinkage long sys_ bind(io dr umyaddr,int addrlen)
int err;
if((sock=sockfd _ loo up(fd,&err
ULL){
if((err=move _ addr _ o _ kernel(um,ddrlen,address))>=0)
err=sock->ops-,(ct ockaddr)address,addrlen)
sockfd_put(sock);
648江南大学学报(自然科学版)第5卷
}
return err;
}
比较后不难看出,最终两者均通过调用sock-
>ops->bind和sock->op->listen,但是用户态的程序sys_bind和sys_listen多调用了move_addr_ to_kernel,增加了内核态和用户态的切换和一些其他的开销.因此,内核态的效率明显更高.
3.3创建内核线程通过内核函数kernel_thread既可创建内核线程.在proxyd函数中创建了多个内核线程,每个线程都通过调用do_thread_work主流程函数.考虑到多个CPU的服务器,KHttpProxyd主流程采用多个内核线程做HTTP代理服务.实现的关键代码如下:
while(i++<thread_number){
canquit[i]=0;thread[i]=i;
(void)kernel_thread(do_thread_work,&thread
[i],CLON E_ FS|CLON E_ FIL ES|CLON E_
SIGHAND);
}
4 KHttp Proxyd服务流程
4.1维护状态机的方法与步骤每个客户端维护着1个状态机,并根据H T TP协议[78],决定当前的状态以及何时接受与发送数
据.KHttp Proxyd状态见图2.
图2 KHttpProxyd状态图
Fig.2 KHttpProxyd state status
1)客户端连接到KHttp Proxyd,KHttp Pro xyd,状态为wait_for_header;
2)从客户端读取数据,分析是否为合法的Http代理请求.如果不是,则关闭该连接.如果是合法的Http请求,则对Http包做完整性分析,是否为1个完整的Http代理请求包头;如果是,则将各项数据解析出来,保存到该客户端的数据结构
中,并连接服务器,状态为connecting_server;
3)检查该客户端与目的服务器的Socket是否已经建立连接,如果是,则状态为Connected _
server;
4)如果服务端的Socket断开,则根据Http协议中的Http版本以及keepalive字段,决定是否断开客户端连接.
4.2轮询过程中的具体操作每次轮询过程中,根据客户端当前的状态去作
相应的读写或是连接关闭操作.具体操作如下:
1)wait_for_head状态,则从客户端读取数据,看是否可以转为Connecting状态;
2)Connecting状态,则读取与目的服务器连接的Socket的状态,看是否已经与目的服务器建立连接;
3)Connected状态,则从与目的服务器建立连接的Socket中读取数据,并转发至客户端.主流程实现的代码如下:
int thread_main_loop(struct sock_queue 3 sq,int
num)
{
⋯
flag+=do_acception(sq);
tmp=get_sock_head(sq);
while(tmp){
//如果和客户端的连接都中断的话,那么清空该sock,无需作任何处理
if(tmp->client_sock==NULL||tmp->client _ sock->sk->state==TCP_
CLOSE||
tmp->client _ sock->sk->state==TCP_
CLOSE_WAIT)
⋯
ret=recv_from_client(tmp);
if(ret>0){//接受到数据
flag=1;
//分析是否为完整的包
tmp->completed=header _ complete
(tmp);
if(tmp->completed==1){//完整的包
//根据H T TP协议分析包的内容并生成新的
H TTP请求包
ret=parse_and_gen_header(tmp);
⋯
}
}
//与目的服务器连接,connect _ server会根据
第6期张黄瞩等:基于Linux内核2.4的高性能H TTP代理649
自己当前的状态决定是否去连接
ret=connect_server(tmp);
⋯
//发送H T TP请求包至目的服务器,send_to_ server会根据自己当前状态决定是否发送
ret=send_to_server(tmp);
⋯
//从目的服务器读取数据,转发至客户端,process_ from_server会根据当前状态决定是否读取转发
ret=process_from_server(tmp,num);
⋯
tmp=tmp->next;
continue;clr:
del=tmp;
tmp=tmp->next;
//上面出现出错或是socket关闭情况,则清除该客户端的所有数据
cleanup_request(del,sq,num);}
return flag;
}
5性能测试
测试工具:LoadRunner
测试环境:CPU:Server CPU XEON 2.0 GHz
内存:DDR 512 M
网卡:1 000 Mbit/s
WebServer:KHttpd Web server(2台)
参考文献:
测试对象:KHttp Proxyd,Apache,SQU ID
LoadRunner设置:vuser:5~15 thinktime:no think time迭代数(Iteration):10 000测试结果见图3.
X轴代表LoaderRunner vuser数目;Y轴代表Http Proxy每秒能响应的次数Hit per second
图3 KHttpProxyd,Apache,SQUID性能测试数据
Fig.3 KHttpProxyd,Apache,SQUID performance test
statistics
6结语
设计和实现了基于Linux内核2.4的高性能H TTP代理KHttp Proxyd,使得性能上相对其他Http Proxy得到大幅度的提高,KHttp Proxyd在使用中取得了良好的效果.由于其是基于Linux内核的,因此便于移植.在KHttp Proxyd基础上若进行进一步细化,便可以实现大型多媒体网关代理服务器或是嵌入式多媒体的网关设备