什么是epoll?
按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是 2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通 知方法。
epoll的相关系统调用 epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用。
(1). int epoll_create(int size);
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。
需要注意的是,当创建好 epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这 个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
(2). int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而 是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示: EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件; EPOLL_CTL_DEL:从epfd中删除一个fd; 第三个参数是需要监听的fd。 第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
//保存触发事件的文件描述符相关的数据
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
//感兴趣的事件和被触发的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket的话,需要再次把这个socket加入到EPOLL队列里
(3). int epoll_wait(int epfd, struct epoll_event * events, intmaxevents, int timeout); 收集在epoll监控的事件中已经发送的事件。
参数events是分配好的epoll_event结构体数组, epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复 制到这个events数组中,不会去帮助我们在用户态中分配内存)。
maxevents告之内核这个 events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时 时间(毫秒,0会立即返回,-1是永久阻塞)。如果函数调用成功, 返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。
epoll工作原理
epoll同样只告知那些就绪的⽂描述符,而且当我们调用epoll_wait()获得就绪文件描述符时, 返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一 个数组中依次取得相应数量的⽂文件描述符即可,这里也使用了内存映射(mmap)技术,这 样便彻底省掉了这些⽂文件描述符在系统调用时复制的开销。 另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select中,进程只有在调 用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机 制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
Epoll的2种工作方式-水平触发(LT)和边缘触发(ET)
epoll的优点:
1.支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认 值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是 可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来⽹网络效率的下降, 二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比 较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不 是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的 数目,这个数字一般远大于2048.
2.IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时, 任一时间只有部分的socket是”活跃”的,但是select/poll每次调用都会线性扫描全部的集合, 导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行操作—这 是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。
那么,只有”活跃”的 socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了 ⼀个”伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上 都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多 使⽤用epoll_ctl,效率相比还有稍微的下降。但是⼀旦使用idle connections模拟WAN环境,epoll的 效率就远在select/poll之上了。
* 3.使用mmap加速内核与用户空间的消息传递*
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通 知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户 空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记 手工 mmap这一步的。
server端代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define EP_SIZE 64
static void usage(const char* proc) //使用说明
{
printf("Usage:%s [local_ip] [local_port]\n", proc);
}
int startup(const char* _ip, int _port) //创建套接字
{
int sock = socket(AF_INET, SOCK_STREAM , 0);
if(sock < 0)
{
perror("socket\n");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock, (struct sockaddr*)&local, sizeof(local))<0)
{
perror("bind\n");
exit(3);
}
if(listen(sock, 10) < 0)
{
perror("listen\n");
exit(4);
}
return sock;
}
//tcp_server 127.0.0.1 8080
int main(int argc, char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int listen_sock = startup(argv[1], atoi(argv[2]));
int epfd = epoll_create(EP_SIZE); //创建epoll模型
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
//添加事件
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
int num = -1; //就绪事件数目
struct epoll_event revs[EP_SIZE]; //就绪队列
int timeout = 5000; // 定时器
while(1)
{
switch(num = epoll_wait(epfd, revs, EP_SIZE, timeout))
{
case 0:
printf("timeout\n");
break;
case -1:
perror("wait");
break;
default:
{
int i = 0;
for(; i<num; i++)
{
int sock = revs[i].data.fd;
if(sock == listen_sock && revs[i].events == EPOLLIN)
// 监听事件就绪
{
struct sockaddr_in client;
ssize_t len = sizeof(client);
int new_sock = accept(sock, (struct sockaddr*)&client, &len);
if(new_sock < 0)
{
perror("accept");
continue;
}
printf("get a new client:%s:%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
ev.events = EPOLLIN;
ev.data.fd = new_sock;
//加入到epoll模型中
epoll_ctl(epfd, EPOLL_CTL_ADD, new_sock, &ev);
}
else if(sock != listen_sock)
{
char buf[1024];
if(revs[i].events & EPOLLIN)//普通读事件
{
ssize_t s = read(sock, buf, sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("client# : %s\n", buf);
ev.events = EPOLLOUT;
ev.data.fd = sock;
epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &ev);
continue;
}
else if(s == 0)
{
printf("client is quit...\n");
close(sock);
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
continue;
}
else
{
perror("read");
close(sock);
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
continue;
}
}//fi
if(revs[i].events & EPOLLOUT) //普通写事件
{
char buf[1024];
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if(s > 0)
{
buf[s-1] = 0;
s = write(sock, buf, strlen(buf));
if(s > 0)
{
buf[s] = 0;
printf(" server# : %s\n", buf);
ev.events = EPOLLIN;
ev.data.fd = sock;
epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &ev);
break;
}
else if(s == 0)
{
close(sock);
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
continue;
}
else
{
perror("write");
close(sock);
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
continue;
}
//continue;
}
else if(s == 0)
{
printf("client is quit...\n");
close(sock);
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
continue;
}
else
{
perror("read");
close(sock);
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
continue;
}
}
}//else fi
}//for
}//default
}//switch
}//while
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
static void usage(const char* proc) //使用说明
{
printf("Usage:%s [local_ip] [local_port]\n", proc);
}
//tcp_client server_ip server_port
int main(int argc, char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int sock = socket(AF_INET, SOCK_STREAM , 0);
if(sock < 0)
{
perror("socket\n");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
{
perror("connect\n");
return 3;
}
printf("connect success\n");
char buf[1024];
while(1)
{
printf("please Enter# \n");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if(s > 0)
{
buf[s-1] = 0;
write(sock, buf, strlen(buf));
s = read(sock, buf, sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("server echo# %s\n ", buf);
}
}
}
close(sock);
return 0;
}
运行结果: