epoll多路复用压力测试
1、epoll见解
epoll相对于select、poll性能优越相当之多,可以说是二者结合加强版本,我们可以设想一下,假如有100w个tcp连接,那么每次有数据过来了,select、poll都需要去从第一个到最后一个进行依次遍历,而epoll会讲队列排序讲发生事件放在前面,后面的就不用遍历了,所以准确的说处理几千的连接可以用select、poll但是太多了就不行,而且select、poll都是从用户态拷贝到内核态,需要花费大量的时间,这样比较起来epoll性能就相当的高了。
2、epoll原理
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,如下所示:
struct eventpoll {
...
/*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
也就是这个epoll监控的事件*/
struct rb_root rbr;
/*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
struct list_head rdllist;
...
};
我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发连接。
- epoll_ctl()
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
系统调用epoll_ctl()能够修改由文件描述符epfd所代表的epoll实例中的兴趣列表。若成功返回0,若出错返回-1。
第一个参数epfd是epoll_create()的返回值; 第二个参数op用来指定需要执行的操作,它可以是如下几种值:
EPOLL_CTL_ADD:将描述符fd添加到epoll实例中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的 结构体中。如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl()将出现EEXIST错误;
EPOLL_CTL_MOD:修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表 中的文件描述符,epoll_ctl()将出现ENOENT错误;
POLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev。如果我们试图移除一个不在epfd的兴 趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表 移除;
第三个参数fd指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道、FIFO、套接字、POSIX消息队 列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符。但是,这里fd不能作为普通文件或目录的文件描述符;
第四个参数ev是指向结构体epoll_event的指针,结构体的定义如下:
typedef union epoll_data {
void *ptr; /* Pointer to user-defind data */
int fd; /* File descriptor */
uint32_t u32; /* 32-bit integer */
uint64_t u64; /* 64-bit integer */ } epoll_data_t;
struct epoll_event {
uint32_t events; /* epoll events(bit mask) */
epoll_data_t data; /* User data */
};
3、测试代码
#include <sys/epoll.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void handler_events(int epfd,struct epoll_event revs[],int num,int listen_sock)
{
struct epoll_event ev;
int i = 0;
for( ; i < num; i++ )
{
int fd = revs[i].data.fd;
// 如果是监听文件描述符,则调用accept接受新连接
if( fd == listen_sock && (revs[i].events & EPOLLIN) )
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(fd,(struct sockaddr *)&client,&len);
if( new_sock < 0 )
{
perror("accept fail ... \n");
continue;
}
printf("get a new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
//因为只是一个http协议:连接成功后,下面就是要 请求和响应
// 而服务器端响应之前:要先去读客户端要请求的内容
ev.events = EPOLLIN;
ev.data.fd = new_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
continue;
}
// 如果是普通文件描述符,则调用read提供读取数据的服务
if(revs[i].events & EPOLLIN)
{
char buf[10240];
ssize_t s = read(fd,buf,sizeof(buf)-1);
if( s > 0 )// 读成功了
{
buf[s] = 0;
printf(" %s ",buf);
// 读成功后,就是要给服务端响应了
// 而这里的事件是只读事件,所以要进行修改
ev.events = EPOLLOUT;// 只写事件
ev.data.fd = fd;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);// 其中EPOLL_CTL_MOD 表示修改
}
else if( s == 0 )
{
printf(" client quit...\n ");
close(fd);// 这里的fd 就是 revs[i].fd
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);// 连接关闭,那么就要把描述该连接的描述符关闭
}
else// s = -1 失败了
{
printf("read fai ...\n");
close(fd);// 这里的fd 就是 revs[i].fd
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);// 连接关闭,那么就要把描述该连接的描述符关闭
}
continue;
}
// 服务器端给客户端响应: 写
if( revs[i].events & EPOLLOUT )
{
const char* echo = "HTTP/1.1 200 ok \r\n\r\n<html>hello epoll server!!!</html>\r\n";
write(fd,echo,strlen(echo));
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
}
}
}
int startup( int port )
{
// 1. 创建套接字
int sock = socket(AF_INET,SOCK_STREAM,0);//这里第二个参数表示TCP
if( sock < 0 )
{
perror("socket fail...\n");
exit(2);
}
// 2. 解决TIME_WAIT时,服务器不能重启问题;使服务器可以立即重启
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);// 地址为任意类型
local.sin_port = htons(port);// 这里的端口号也可以直接指定8080
// 3. 绑定端口号
if( bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0 )
{
perror("bind fail...\n");
exit(3);
}
// 4. 获得监听套接字
if( listen(sock,5) < 0 )
{
perror("listen fail...\n");
exit(4);
}
return sock;
}
int main(int argc,char* argv[] )
{
// 1. 创建一个epoll模型: 返回值一个文件描述符
int epfd = epoll_create(256);
if( epfd < 0 )
{
perror("epoll_create fail...\n");
return 2;
}
// 2. 获得监听套接字
int listen_sock = startup(atoi("8899"));//端口号传入的时候是以字符串的形式传入的,需要将其转为整型
// 3. 初始化结构体----监听的结构列表
struct epoll_event ev;
ev.events = EPOLLIN;//关心读事件
ev.data.fd = listen_sock;// 关心的描述文件描述符
// 4. epoll的事件注册函数---添加要关心的文件描述符的只读事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);
struct epoll_event revs[128];
int n = sizeof(revs)/sizeof(revs[0]);
int timeout = 3000;
int num = 0;
while(1)
{
// 5 . 开始调用epoll等待所关心的文件描述符集就绪
switch( num = epoll_wait(epfd,revs,n,timeout) )
{
case 0:// 表示词状态改变前已经超过了timeout的时间
printf("timeout...\n");
continue;
case -1:// 失败了
printf("epoll_wait fail...\n");
continue;
default: // 成功了
handler_events(epfd,revs,num,listen_sock);
break;
}
}
close(epfd);
close(listen_sock);
return 0;
}