在聊epoll()之前,我们首先聊聊为什么需要epoll()。我们知道select会进行FD数组的遍历,但主要还会受1024个最大容量限制。而poll虽然解决了通过使用链表保存FD的方式解决了最大容量限制,但其不管是“活跃”的socket还是“不活跃”的socket的也会进行遍历,这就导致了在并发数很大的情况下,他们的工作效率都比较低。
epoll:是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
特点:
①获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
②epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
实现的基本方式:
① 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
②调用epoll_ctl向epoll对象中添加这100万个连接的套接字
③调用epoll_wait收集发生的事件的连接
拓展:
什么是水平触发,边缘触发?
触发方式 | 功能 |
---|---|
水平触发(LT) | 缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。 |
边缘触发(ET) | 高速工作方式,只支持non-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。 |
#include //头文件包含
int epoll_create(int size);
参数size:指定了我们想要通过epoll实例来检查的文件描述符个数。(从Linux 2.6.8开始:size参数被忽略,但是依然要大于0)
返回值:
成功:返回一个非负数识别描述符
失败:返回-1
拓展:
从2.6.27版内核以来,Linux支持了一个新的系统调用epoll_create1()。
①去掉了无用的参数size
②增加了一个可用来修改系统调用行为的flags参数
参数 | 功能 |
---|---|
EPOLL_CLOEXEC | 它使得内核在新的文件描述符上启动了执行即关闭标志 |
注意:
当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
epoll_ctl():能够修改由文件描述符epfd所代表的epoll实例中的兴趣列表。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
参数 | 功能 |
---|---|
epfd | epoll_create()的返回值 |
op | 指定需要执行的操作 |
fd | 指明了要修改兴趣列表中的哪一个文件描述符的设定 |
ev | 指向结构体epoll_event的指针 |
返回值:
成功:返回0
失败:返回-1
①参数op的可选值:
可选值 | 功能 |
---|---|
EPOLL_CTL_ADD | 将描述符fd添加到epoll实例中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的结构体中。如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl()将出现EEXIST错误。 |
EPOLL_CTL_MOD | 修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。 |
EPOLL_CTL_DEL | 将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev。关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表移除。 |
②参数fd的对象:
允许 | 不允许 |
---|---|
管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符 | 普通文件或目录的文件描述符 |
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的设置:
events字段:是一个位掩码,它指定了我们为待检查的描述符fd上所感兴趣的事件集合。
data字段:是一个联合体,当描述符fd稍后称为就绪态时,联合的成员可用来指定传回给调用进程的信息。
events=以下宏的集合:
常量 | 功能 |
---|---|
EPOLLIN | 表示对应的文件描述符可以读(包括对端SOCKET正常关闭); |
EPOLLOUT | 表示对应的文件描述符可以写; |
EPOLLPRI | 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); |
EPOLLERR | 表示对应的文件描述符发生错误; |
EPOLLHUP | 表示对应的文件描述符被挂断; |
EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 |
EPOLLONESHOT | 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。 |
注意:
①在编程时就不应该同事使用epoll_data联合体中的两个成员,比如,有时为了方便确定那个描述符就绪,以及获取自己定义的一些内容,我们常常ptr和fd同时使用,结果发现程序会崩溃。
②每次添加/修改/删除被侦听文件描述符都需要调用epoll_ctl,所以要尽量少地调用epoll_ctl,防止其所引来的开销抵消其带来的好处。有的时候,应用中可能存在大量的短连接(比如说Web服务器),epoll_ctl将被频繁地调用,可能成为这个系统的瓶颈。
epoll_wait():返回epoll实例中处于就绪态的文件描述符信息,单个epoll_wait()调用能够返回多个就绪态文件描述符的信息。
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
参数 | 功能 |
---|---|
epfd | epoll_create()的返回值 |
struct epoll_event *evlist | 从内核得到事件的集合 |
maxevents | 每次能处理的最大事件数 |
timeout | 超时时间(大于0:阻塞至timeout毫秒。等于0:立即返回。等于-1:将永久阻塞) |
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) //字节长除于首字节=个数
#define MAX_EVENTS 512 //设置事件数
int socket_server_init(char *listen_ip,int listen_port);
static inline void print_usage(char *progname);
void set_socket_rlimit(void);
int main(int argc,char **argv)
{
int daemon_run=0;
int serv_port=0;
int listen_fd;
char *progname=NULL;
int opt;
int i,k;
int conn_fd;
int max_fd=0;
char buf[1024];
int rv;
int found;
int epoll_fd;
struct epoll_event event;
struct epoll_event event_array[MAX_EVENTS];
int events;
//一、命令帮助提示
//另外:打印命令提示进行函数抽象
progname=basename(argv[0]); //从arg[0]中截取文件名
//1.opts结构体定义
struct option opts[]=
{
{"daemon",no_argument,NULL,'b'}, //守护进程
{"port",required_argument,NULL,'p'},
{"help",no_argument,NULL,'h'},
{0,0,0,0}
};
//2.命令获取与解析
while((opt=getopt_long(argc,argv,"d:p:h",opts,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;
}
}
//3.判断输入值是否正确
//serv_port置-1,还需要if(!...)吗
if(!serv_port)
{
print_usage(progname);
return -1;
}
//判断是否需要守护进程(即是否要后台运行)
if(daemon_run)
{
daemon(0,0);
}
//判断服务器是否监听端口
if((listen_fd=socket_server_init(NULL,serv_port))<0)
{
printf("ERROR:%s server listen on port %d failure",argv[0],serv_port);
return -2;
}
//二、epoll
set_socket_rlimit(); //虽说epoll没有限制,但是linux本身创建的socket是有限制的。
//判断epoll创建是否成功
if((epoll_fd=epoll_create(MAX_EVENTS))<0)
{
printf("epoll_create() failure:%s\n",strerror(errno));
return -3;
}
event.events=EPOLLIN;
event.data.fd=listen_fd;
for( ; ;)
{
//判断等待感兴趣的事件
events=epoll_wait(epoll_fd,event_array,MAX_EVENTS,-1);
if(events<0)
{
printf("epoll failure:%s\n",strerror(errno));
break;
}
else if(events==0)
{
printf("epoll get timeout\n");
continue;
}
//events>0 事件进来
for(i=0;i<events;i++)
{
if((event_array[i].events&EPOLLERR)||(event_array[i].events&EPOLLHUP)) //ERR:有错误发生, HUP:socket对端关闭
{
printf("epoll_wait get error on fd[%d]:%s\n",event_array[i].data.fd,strerror(errno));
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);//将fd从epfd的列表中移除
close(event_array[i].data.fd);
}
}
//新客户端连接
if(event_array[i].data.fd==listen_fd)
{
if((conn_fd=accept(listen_fd,(struct sockaddr *)NULL,NULL))<0)
{
printf("accept new client failure:%s\n",strerror(errno));
continue;
}
else
{
event.data.fd=conn_fd;
event.events=EPOLLIN;
if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,conn_fd,&event)<0) //将fd添加到pfd列表中
{
printf("epoll add client socket failure:%s\n",strerror(errno));
close(event_array[i].data.fd);
continue;
}
printf("epool add new client socket[%d] sueccess.\n",conn_fd);
}
}
else //已连客户:1.数据请求 2.中断连接
{
//判断是否已连接
if((rv=read(event_array[i].data.fd,buf,sizeof(buf)))<=0)
{
printf("socket[%d] read failure or get disconnect and will be removed.\n",event_array[i].data.fd);
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);//将fd从pfd列表中删除
close(event_array[i].data.fd);
continue; //结束循环
}
else
{
printf("socket[%d] read get %d bytes data \n",event_array[i].data.fd,rv);
//字符:小转大字母
for(k=0;k<rv;k++)
{
buf[k]=toupper(buf[k]);
}
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(epoll_fd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);//将fd从pfd列表中删除
close(event_array[i].data.fd);
}
}
}
}
CleanUp:
close(listen_fd);
return 0;
}
//命令提示打印
static inline void print_usage(char *progname) //了解static inlin 意义
{
printf("Usage:%s [OPTION]...\n",progname);
printf("%s is a socket server program\n",progname);
printf("Mandatory arguments:long or short options\n");
printf("-b[daemon] set program running on background\n");
printf("-p[port] socket server port address\n");
printf("-h[help] display this help information\n");
printf("\nExample: %s -b -p 1111 \n",progname);
return ;
}
int socket_server_init(char *listen_ip,int listen_port)
{
int on=1;
int rv=0;
int listen_fd;
struct sockaddr_in servaddr;
//1.socket
if((listen_fd=socket(AF_INET,SOCK_STREAM,0))<0)
{
printf("socket () create a TCP socket failure:%s\n",strerror(errno));
return -1;
}
//端口重用
setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
//初始化操作
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(listen_port);
//判断IP地址,并初始化。意义:这里if else相当于两用。监听特定IP,或监听所有IP。
if(!listen_ip)
{
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
printf("listen other IP address [%d]",listen_ip);
}
else
{
if(inet_pton(AF_INET,listen_ip,&servaddr.sin_addr)<=0)
{
printf("inet_pton() set listen IP address failure.\n");
rv=-2;
goto CleanUp;
}
}
//2.bind
if(bind(listen_fd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
printf("bind() bind the TCP socket failure: %s\n",strerror(errno));
rv=-3;
goto CleanUp;
}
//3.listen
if(listen(listen_fd,13)<0)
{
printf("bind() bind the TCP socket failure: %s\n",strerror(errno));
rv=-4;
goto CleanUp;
}
CleanUp:
if(rv<0)
{
close(listen_fd);
}
else
{
rv=listen_fd;
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 %d\n",limit.rlim_max);
}