IO复用——epoll
目录
一、select、poll于epoll比较
对于select来说,存在着缺点:
(1)单个进程能够监视的文件描述符的数量存在最大限制(通常时1024,因为会受到fd_set中的FD_SETSIZE的大小限制),采用的是轮询方式扫描文件描述符,文件描述符数量越多,性能越差。
(2)select在内核与用户空间内存的拷贝上需要复制大量的句柄数据结构,会产生巨大的开销,并且select的返回是含有整个句柄的数组,应用程序需要遍历整个数组才能获得发生事件的句柄。
(3)select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符调整进程。
相比于select模型,poll使用链表保存文件描述符,因此突破了监视文件描述符的数量桎梏,但也依旧存在上述的缺点。
epoll是Linux内核为处理大批文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无需遍历整个被侦听的描述符集合,只需要遍历被内核IO事件异步唤醒而加入的Ready队列的描述符集合。
epoll除了提供select/poll的IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
LT(level triggered):水平触发是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,(设置相应的字节数,当到达设置的字节数时才会通知,会多次通知)所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET (edge-triggered):边缘触发是高速工作方式,只支持non-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),(只要有字节就通知,且只通知一次,所以使用少)不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。
ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer时里面有数据可以让用户读,就会不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。
二、epoll系统调用
epoll的设计、实现于select完全不同,epoll通过在Linux内核中申请一个简易的文件系统,把原先的select/poll调用分为了三部分:
(1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源);
(2)调用epoll_clt向epoll对象中添加需要连接的套接字(fd)数量;
(3)调用epoll_wait收集发生的事件的连接
1、epoll说明
1. 创建实例:epoll_create()
#include<sys/epoll.h>
int epoll_create(int size)
函数说明:系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空。
参数说明:参数size指定了我们想要通过epoll实例来检查的文件描述符个数。该参数并不是一个上限,而是告诉内核应该如何为内部数据结构划分初始大小。从Linux2.6.8版以来,size参数被忽略不用。
返回值:若成功返回文件描述符,若出错返回-1。
作为函数返回值,epoll_create()返回了代表新创建的epoll实例的文件描述符(即下文中的epfd)。这个文件描述符在其他几个epoll系统调用中用来表示epoll实例。当这个文件描述符不再需要时,应该通过close()来关闭。当所有与epoll实例相关的文件描述符都被关闭时,实例被销毁,相关的资源都返还给系统。
从2.6.27版内核以来,Linux支持了一个新的系统调用epoll_create1()。该系统调用执行的任务同epoll_create()一样,但是去掉了无用的参数size,并增加了一个可用来修改系统调用行为的flags参数。目前只支持一个flag标志:EPOLL_CLOEXEC,它使得内核在新的文件描述符上启动了执行即关闭标志。
2. 修改epoll的兴趣列表:epoll_create()
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数说明:系统调用epoll_ctl()能够修改由文件描述符epfd所代表的epoll实例中的兴趣列表。
参数说明:
第一个参数epfd:是epoll_create()的返回值;
第二个参数op:用来指定需要执行的操作,它可以是如下几种值:
值 |
|
---|---|
EPOLL_CTL_ADD | 将描述符fd添加到epoll实例中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的结构体中。如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl()将出现EEXIST错误; |
EPOLL_CTL_MOD | 修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误 |
EPOLL_CTL_DEL | 将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev。如果我们试图移除一个不在epfd的兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表移除 |
第三个参数fd:指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符。但是,这里fd不能作为普通文件或目录的文件描述符;
第四个参数event是指向结构体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 */
};
参数ev为文件描述符fd所做的设置(epoll_event)如下:
events字段是一个位掩码,它指定了我们为待检查的描述符fd上所感兴趣的事件集合;
data字段是一个联合体,当描述符fd稍后称为就绪态时,联合的成员可用来指定传回给调用进程的信息;
返回值:成功返回0,若出错返回-1。
3. 等待事件:epoll_wait()
#inclued<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
函数说明:系统调用epoll_wait()返回epoll实例中处于就绪态的文件描述符信息,单个epoll_wait()调用能够返回多个就绪态文件描述符的信息。
参数说明:
第一个参数epfd:epoll_create()的返回值;
第二个参数evlist:所指向的结构体数组中返回的是有关就绪态文件描述符的信息(表示的是此次返回所需要处理的事件),数组evlist的空间由调用者负责申请;
第三个参数maxevents:指定所evlist数组里包含的元素个数;
第四个参数timeout:超时,用来确定epoll_wait()的阻塞行为,有如下几种:
timeout值 |
|
---|---|
-1 | 调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生或者直到捕获到一个信号为止(永不超时 ) |
0 | 执行一次非阻塞式地检查,看兴趣列表中的描述符上产生了哪个事件 |
大于0 | 调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止 |
数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。events字段返回了在该描述符上已经发生的事件掩码。data字段返回的是我们在描述符上使用epoll_ctl()注册感兴趣的事件时在ev.data中所指定的值。注意,data字段是唯一可获知同这个事件相关的文件描述符的途径。因此,当我们调用epoll_ctl()将文件描述符添加到感兴趣列表中时,应该要么将ev.date.fd设为文件描述符号,要么将ev.date.ptr设为指向包含文件描述符号的结构体
当我们调用epoll_ctl()时可以在ev.events中指定的位掩码以及由epoll_wait()返回的evlist[].events中的值如下所示:
值 |
| 是否能作为 epoll_ctl()的输入 | 是否能作为 epoll_ctl()的输入 |
---|---|---|---|
EPOLLIN | 可读取非高优先级数据 | 能 | 能 |
EPOLLPRI | 可读取高优先级数据 | 能 | 能 |
EPOLLRDHUP | socket对端关闭(始于Linux 2.6.17) | 能 | 能 |
EPOLLOUT | 普通数据可写 | 能 | 能 |
EPOLLET | 将EPOLL设为边缘触发 | 能 | |
EPOLLONESHOP | 在完成事件通知后禁用检查 | 能 | |
EPOLLERR | 对应文件描述符发生错误 | 能 | |
EPOLLUP | 出现挂断 | 能 |
返回值:
成功:epoll_wait()返回数组evlist中的元素个数
超时:返回0,如果在timeout超时间隔内没有任何文件描述符处于就绪态的话就返回0
出错:返回-1并在errno中设定错误码以表示错误原因。
默认情况下,一旦通过epoll_ctl()的EPOLL_CTL_ADD操作将文件描述符添加到epoll实例的兴趣列表中后,它会保持激活状态(即,之后对epoll_wait()的调用会在描述符处于就绪态时通知我们)直到我们显示地通过epoll_ctl()的EPOLL_CTL_DEL操作将其从列表中移除。如果我们希望在某个特定的文件描述符上只得到一次通知,那么可以在传给epoll_ctl()的ev.events中指EPOLLONESHOT标志。如果指定了这个标志,那么在下一个epoll_wait()调用通知我们对应的文件描述符处于就绪态之后,这个描述符就会在兴趣列表中被标记为非激活态,之后的epoll_wait()调用都不会再通知我们有关这个描述符的状态了。如果需要,我们可以稍后用过调用epoll_ctl()的EPOLL_CTL_MOD操作重新激活对这个文件描述符的检查
2、epoll代码分析
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<ctype.h>
#include<getopt.h>
#include<pthread.h>
#include<libgen.h>
#include<time.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<sys/resource.h>
#define MAX_EVENTS 512
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
static inline void print_usage(char *progname);
int socket_server_init(char *listen_ip,int listen_port);
void set_socket_rlimit(void);
int main(int argc, char *argv[])
{
int listenfd,connfd;
int serv_port = 0;
int daemon_run = 0;
char *progname = NULL;
int opt;
int rv;
int i,j;
int found;
char buf[1024];
int epollfd;
struct epoll_event event;//添加某一个fd
struct epoll_event event_array[MAX_EVENTS];//同时处理的客户端数量
int events;//events个客户端发生事件
struct option long_option[] = {
{"daemon",no_argument,NULL,'d'},
{"port",required_argument,NULL,'p'},
{"help",no_argument,NULL,'h'},
{NULL,0,NULL,0}
};
progname = basename(argv[0]);
while((opt = getopt_long(argc,argv,"dp:h",long_option,NULL)) != -1)
{
switch(opt)
{
case 'd':
daemon_run = 1;
break;
case 'p':
serv_port=atoi(optarg);
break;
case 'h':
print_usage(progname);
return EXIT_SUCCESS;
default:
break;
}
}
if(!serv_port)
{
print_usage(progname);
return -1;
}
set_socket_rlimit();//epoll没有文件描述符的数量限制,但Linux内核有限制
if((listenfd = socket_server_init(NULL,serv_port))<0)
{
printf("Error:%s server listen on port %d failure\n",argv[0],serv_port);
return -2;
}
printf("%s server start to listen on port %d \n",argv[0],serv_port);//进入listen状态
if(daemon_run)//判断后台运行
{
daemon(0,0);
}
if((epollfd=epoll_create(MAX_EVENTS)) < 0)//创建epollfd,为epoll_ctl的第一个参数
{
printf("epoll_create() failure:%s\n",strerror(errno));
return -3;
}
event.events = EPOLLIN|EPOLLET;//关心读事件,设置为边缘触发
event.data.fd = listenfd;//将listenf加入到数组中
if(epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,&event)<0)//添加listenfd到event结构体中
{
printf("epoll add listen socket failure:%s\n",strerror(errno));
return -4;
}
for( ; ; )
{
events = epoll_wait(epollfd,event_array,MAX_EVENTS,-1);//等待事件触发,将发生事件的文件描述符加入到event_array数组中
if(events < 0)//出错
{
printf("epoll failure:%s\n",strerror(errno));
break;
}
else if(events == 0)//超时
{
printf("epoll get timeout\n");
continue;
}
for(i=0;i<events;i++)
{
if((event_array[i].events&EPOLLERR) || (event_array[i].events&EPOLLHUP))//表示事件出错或挂断
{
printf("epoll_wait get error on fd[%d]:%s\n",event_array[i].data.fd,strerror(errno));
epoll_ctl(epollfd,EPOLL_CTL_DEL,event_array[i].data.fd,&event);//移除
close(event_array[i].data.fd);
}
if(event_array[i].data.fd == listenfd)
{//获取到新的文件描述符
if((connfd = accept(listenfd,(struct sockaddr *)NULL,NULL))<0)
{
printf("accept new client failure:%s\n",strerror(errno));
continue;
}
//添加connfd到event中
event.data.fd = connfd;
event.events = EPOLLIN|EPOLLET;
if(epoll_ctl(epollfd,EPOLL_CTL_ADD,connfd,&event)<0)//添加出错,关闭该文件描述符
{
printf("epoll add client socket failure :%s\n",strerror(errno));
close(event_array[i].data.fd);
continue;
}
printf("epoll add new client socket[%d] ok.\n",connfd);
}
else//客户端发生事件
{
if((rv=read(event_array[i].data.fd,buf,sizeof(buf)))<0)//出错,delet
{
printf("socket[%d] read failure or get disconnect and will be remove.\n",event_array[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL,event_array[i].data.fd,&event);
close(event_array[i].data.fd);
continue;
}
else//成功
{
printf("socket[%d] read get %d bytes data\n",event_array[i].data.fd,rv);
for(j=0;j<rv;j++)
{
buf[j]=toupper(buf[j]);
}
if(write(event_array[i].data.fd,buf,rv)<0)
{
printf("socket[%d] write failure:%s\n",event_array[i].data.fd,strerror(errno));
epoll_ctl(epollfd,EPOLL_CTL_ADD,event_array[i].data.fd,&event);
close(event_array[i].data.fd);
}
}
}
}
}
Cleanup:
close(listenfd);
return 0;
}
static inline void msleep(unsigned long ms)
{
struct timeval tv;
tv.tv_sec = ms/1000;
tv.tv_usec = (ms%1000)*1000;
select(0,NULL,NULL,NULL,&tv);
}
static inline void print_usage(char *progname)
{
printf("Usage:%s [OPTION]...\n",progname);
printf("%s is a socket server program,which used to verify client and echo back string from it \n",progname);
printf("-d(--daemon):background running\n");
printf("-p(--port):sepcify server listen port\n");
printf("-h(--help):print this help information\n");
}
int socket_server_init(char *listen_ip,int listen_port)
{
struct sockaddr_in servaddr;
int rv = 0;
int on = 1;
int listenfd;
if((listenfd = socket(AF_INET,SOCK_STREAM,0))< 0)
{
printf("Use socet() to create a TCP socket failure:%s\n",strerror(errno));
return -1;
}
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(listen_port);
if(!listen_ip)
{
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
}
else
{
if(inet_pton(AF_INET,listen_ip,&servaddr.sin_addr) <= 0)
{
printf("inet_pton() ste listen IP address failure.\n");
rv = -2;
goto Cleanup;
}
}
if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))<0)
{
printf("Use bind() to bind the TCP socket failure:%s\n",strerror(errno));
rv = -3;
goto Cleanup;
}
if(listen(listenfd,13)<0)
{
printf("Use listen() failure:%s \n",strerror(errno));
rv = -4;
goto Cleanup;
}
Cleanup:
if(rv < 0)
close(listenfd);
else
rv = listenfd;
return rv;
}
void set_socket_rlimit(void)//设置最大可以打开的文件描述符的数量
{
struct rlimit limit = {0};
getrlimit(RLIMIT_NOFILE,&limit);
limit.rlim_cur = limit.rlim_max;
setrlimit(RLIMIT_NOFILE,&limit);
printf("set socket open fd max count to %ld\n",limit.rlim_max);
}