IO多路转接之epoll
程序员文章站
2022-06-14 11:18:31
...
I/O多路转接之epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll接口
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
(1) int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数是对指定描述符fd执行op操作。
- epfd:是epoll_create()的返回值。
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是需要监听的fd(文件描述符)
- epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
typedef union epoll_data {
void ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
epoll_data是一个联合体,其中fd使用的最多,表示指定事件的文件描述符。ptr可以用来指定与fd相关的用户数据。如果要将文件描述符和用户数据关联起来,可以让ptr指向的用户数据这种包含fd。
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
功能:该函数成功时返回就绪的文件描述符的个数,失败返回-1并设置errno
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
工作模式:
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.LT同时支持阻塞和非阻塞方式。
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,所以ET要更高效。使用ET模式的文件描述符都应该是非阻塞的,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务都饿死。ET只支持非阻塞方式。
工作原理:
1、当调用epoll_wait获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定一个数组依次取得相应数量的文件描述符即可,这里使用了内存映射(mmap)技术,会节省一些文件描述符在系统调用时的复制开销。
2、一但文件描述符就绪,内核会采用类似callback的回调机制,迅速**这个文件描述符,当进程调用epoll_wait时便得到通知。
3、epoll与select和poll不同,epoll将事件表完全交给内核去管理,用户只需要将要监测的文件描述符添加进入内核表中即可,等到事件就绪后内核会自动将就绪事件的文件描述符**
epoll优点:
1、底层采用红黑树给内核管理,增删改查效率高
2、采用回调机制.
3、一旦就绪,放入就绪队列,上层直接从就绪队列读取,算法复杂度为O(1)
4、就绪队列连续存储(遍历快)
5、采用内核映射技术
6、可以采用ET模式.
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/epoll.h>
#define _SIZE_ 128
typedef struct epoll_msg //创建一个结构体类型,里面保存文件描述符,和一个缓冲区
{
int fd;
char buf[_SIZE_];
}epoll_t,*epoll_p,**epoll_pp;
static void* allocator(int fd)
{
epoll_p buf=(epoll_p)malloc(sizeof(epoll_t));
if(NULL==buf)
{
perror("malloc");
exit(6);
}
buf->fd=fd;
return buf;
}
void delalloc(void* ptr)
{
if(NULL!=ptr)
{
free(ptr);
}
}
int startup(const char* ip,int port)
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(1);
}
int opt=1;
if(setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))<0)
{
perror("setsockopt");
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");
exit(3);
}
if(listen(sock,5)<0)
{
perror("listen");
exit(4);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("usage:%s ip_local port_local\n",argv[0]);
return 1;
}
int listen_sock=startup(argv[1],atoi(argv[2]));
int epfd=epoll_create(256);
if(epfd<0)
{
perror("epoll_create");
exit(5);
}
struct epoll_event envs;
envs.events=EPOLLIN|EPOLLET;
envs.data.ptr=allocator(listen_sock);
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&envs);
while(1)
{
int num=0;
int timeout=-1;
struct epoll_event evs[32];
int max=32;
switch((num=epoll_wait(epfd,evs,max,timeout)))
{
case 0:
printf("timeout...");
break;
case -1:
perror("epoll_wait");
break;
default:
{
int i=0;
for(i=0;i<num;i++)
{
int fd=((epoll_p)(evs[i].data.ptr))->fd;
if(fd==listen_sock&&evs[i].events&EPOLLIN)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int connfd=accept(listen_sock,(struct sockaddr*)&peer,&len);
if(connfd<0)
{
perror("accept");
continue;
}
envs.events=EPOLLIN;
envs.data.ptr=allocator(connfd);
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&envs);
} //fi
else if(fd!=listen_sock&&evs[i].events&EPOLLIN)
{
int s=read(fd,((epoll_p)(evs[i].data.ptr))->buf,_SIZE_-1);
if(s>0)
{
char* buf=((epoll_p)(evs[i].data.ptr))->buf;
buf[s]=0;
printf("client# %s\n",buf);
evs[i].events=EPOLLOUT;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&evs[i]);
}
else if(s==0)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
delalloc(evs[i].data.ptr);
evs[i].data.ptr=NULL;
close(fd);
}
else
{
perror("read");
}
} //fi
else if(fd!=listen_sock&&evs[i].events&EPOLLOUT)
{
char *msg="http/1.0 200 ok\r\n\r\n<html><h1>hello wrold</h1></html>";
write(fd,msg,strlen(msg));
delalloc(evs[i].data.ptr);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&evs[i]);
// close(fd);
}
}//for
}
}
}
return 0;
上一篇: Dom探索之基础详解
推荐阅读
-
python 之 并发编程(非阻塞IO模型、I/O多路复用、socketserver的使用)
-
Python—IO多路复用之select模块详解(select、poll、epoll之间的区别)
-
IO多路复用(二) -- select、poll、epoll实现TCP反射程序
-
Linux IO多路复用之epoll网络编程
-
python 之 并发编程(非阻塞IO模型、I/O多路复用、socketserver的使用)
-
IO多路复用之epoll全面总结(必看篇)
-
IO多路复用select/poll/epoll详解以及在Python中的应用
-
linux进程间通信---本地socket套接字(五)---多路IO转接服务器实现一个server对应多个client---poll实现
-
I/O多路转接之epoll(实现epoll版本的TCP服务器)
-
I/O多路转接——select、poll 和 epoll