poll的调用和select完全一致
优点:连接数(也就是文件描述符)没有限制(链表存储)
缺点:大量拷贝,水平触发(当报告了fd没有被处理,会重复报告,很耗性能)
epoll
- epoll是linux内核为处理大批量文件描述符而作了改进的poll,是linux下IO复用select/poll的增强版本。
- 并发性高,可同时监听C10K以上级别的客户事件
select ----> fd—>fd_set --轮训
poll ---- fd—>pollfd
epoll ----> fd—event
epoll : 大规模并发服务框架
epoll+threadpool+mysql
epoll对象
epoll_fd epoll_event
创建 epollfd对象:
方法1:int epoll_create(int size); 返回值 epoll_fd epoll_event
//size是支持的最大句柄数。该函数会返回一个新的epoll句柄,之后的函数调用都用这个句柄来操作。用完之后,记得用close()关闭这个创建出来的epoll句柄,否则可能导致系统fd被耗尽
方法2:int epoll_reate1(int flag);
//上面创建的方法在linux 2.6.8之后,maxfds是被忽略的,所以建议采用epoll_create1(0)这种方法。另外epoll_create1(EPOLLCLOEXEC)表示生成的epoll fd具有“执行后关闭”的特性。
设置 epollfd对象:
创建 epoll_event对象
struct epoll_event ep_ev;
设置 epoll_event对象
ep_ev.events = EPOLLIN;//数据的读取
ep_ev.data.fd = listen_sock;
使用 epoll_event对象
epoll_ctl(int epfd,int op, int fd, struct epoll_event *event);
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&ep_ev);
使用epollefd对象:
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
//等待事件发生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告诉内核这个events有多大,maxevents不能大于epoll_create时设置的size,参数timeout是超时时间(毫秒,0立即返回,-1将永久阻塞)。该函数返回需要处理的事件数目,如果返回0表示已超时。
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 */
};
epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //对事件进行操作
epfd为第一步返回的epoll句柄
int fd为要监听的fd
int op:
EPOLL_CTL_ADD //添加事件
EPOLL_CTL_MOD //修改事件
EPOLL_CTL_DEL //删除事件
event为要监听的事件
events可以是一下宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll的两种工作模式
epoll默认的工作模式是Level Triggered,通过epoll_ctl可以设置epoll的工作模式为Edge Triggered。
LT(levet triggered)同时支持block和no-block socket。在该模式下,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错的可能性要小一点。传统的select/poll都是这种模型的代表。
ET(edge triggered)是高速工作方式,只支持non-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。如果描述符没有再次发生IO操作(导致它再次变成未就绪),内核不会发送更多的通知。例如:ET模式下epoll_wait返回,当前缓存中接收到了2KB的数据,调用read读取1KB的数据。下次循环调用epoll_wai时将不会受到内核通知,将阻塞在这里,直到发生IO操作(如又收到数据)。当调用read或者write返回EAGAIN时,才需要挂起。但我们一般在处理循环读时,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
epoll相对于select/poll的优势
1、epoll通过epoll_ctl注册监听的事件,而不像select/poll在每次循环调用select/poll函数时,设置监听事件,把fd集合从用户态拷贝到内核态),这个开销在fd很多时会很大。
2、select/poll在监听事件发生后,需要遍历所有fd,这个开销在fd很多时也很大,而epoll_wait返回的就是需要处理的事件。
3、select支持的最大文件描述符个数默认为1024,不适合处理大批量的文件描述符。而epoll就没有限制。
4、在并发连接数较大而活动连接数较小时,epoll比poll效率更高;而如果所有连接基本都是活跃的,比如一个高速LAN环境,epoll并不比select/poll有什么效率。
demo
tcp_epoll.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
int creat_socket(char *ip,char* port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0){
perror("socket");
exit(2);
}
//调用setsockopt使当server先断开时避免进入TIME_WAIT状态,\
将其属性设定为SO_REUSEADDR,使其地址信息可被重用
int opt = 1;
if(setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)) < 0){
perror("setsockopt");
exit(3);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(port));
local.sin_addr.s_addr = inet_addr(ip);
if( bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0 ){
perror("bind");
exit(4);
}
if(listen(sock,5) < 0){
perror("listen");
exit(5);
}
printf("listen and bind succeed\n");
return sock;
}
int set_noblock(int sock)
{
int fl = fcntl(sock,F_GETFL);
return fcntl(sock,F_SETFL,fl|O_NONBLOCK);
}
int main(int argc,char *argv[])
{
if(argc != 3){
printf("please use:%s [ip] [port]",argv[0]);
exit(1);
}
int listen_sock = creat_socket(argv[1],argv[2]);
int epoll_fd = epoll_create(256);
if(epoll_fd < 0){
perror("epoll creat");
exit(6);
}
struct epoll_event ep_ev;
ep_ev.events = EPOLLIN;//数据的读取
ep_ev.data.fd = listen_sock;
//添加关心的事件
if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&ep_ev) < 0){
perror("epoll_ctl");
exit(7);
}
struct epoll_event ready_ev[128];//申请空间来放就绪的事件。
int maxnum = 128;
int timeout = 1000;//设置超时时间,若为-1,则永久阻塞等待。
int ret = 0;
int done = 0;
while(!done){
switch(ret = epoll_wait(epoll_fd,ready_ev,maxnum,timeout)){
case -1:
perror("epoll_wait");
break;
case 0:
printf("time out...\n");
break;
default://至少有一个事件就绪
{
int i = 0;
for(;i < ret;++i){
//判断是否为监听套接字,是的话accept
int fd = ready_ev[i].data.fd;
if((fd == listen_sock) && (ready_ev[i].events & EPOLLIN)){
struct sockaddr_in remote;
socklen_t len = sizeof(remote);
int accept_sock = accept(listen_sock,(struct sockaddr*)&remote,&len);
if(accept_sock < 0){
perror("accept");
continue;
}
printf("accept a client..[ip]: %s,[port]: %d\n",inet_ntoa(remote.sin_addr),ntohs(remote.sin_port));
//将新的事件添加到epoll集合中
ep_ev.events = EPOLLIN | EPOLLET;
ep_ev.data.fd = accept_sock;
set_noblock(accept_sock);
if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_sock,&ep_ev) < 0){
perror("epoll_ctl");
close(accept_sock);
}
}
else{//普通IO
if(ready_ev[i].events & EPOLLIN){
//申请空间同时存文件描述符和缓冲区地址
char buf[102400];
memset(buf,'\0',sizeof(buf));
ssize_t _s = recv(fd,buf,sizeof(buf)-1,0);
if(_s < 0){
perror("recv");
continue;
}else if(_s == 0){
printf("remote close..\n");
//远端关闭了,进行善后
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
}else{
//读取成功,输出数据
printf("client# %s",buf);
fflush(stdout);
//将事件改写为关心事件,进行回写
ep_ev.data.fd = fd;
ep_ev.events = EPOLLOUT | EPOLLET;
//在epoll实例中更改同一个事件
epoll_ctl(epoll_fd,EPOLL_CTL_MOD,fd,&ep_ev);
}
}else if(ready_ev[i].events & EPOLLOUT){
const char*msg = "HTTP/1.1 200 OK \r\n\r\n<h1> hi girl </h1>\r\n";
send(fd,msg,strlen(msg),0);
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
}
}
}
}
break;
}
}
close(listen_sock);
return 0;
}