一、epoll的系统调用函数
epoll的实现和select和poll有很大的区别,首先epoll是由一组函数来实现的而不是单个函数;其次epoll把关心的文件描述符上的事件放在内核的一个事件表里面,从而不需要向select和poll那样每次调用都要重复传入文件描述符集或者事件集。但是epoll需要一个额外的文件描述符来标识内核中的这个时间表。
1、epoll_create
创建一个epoll模型(内核事件表);
#include <sys/epoll.h>
int epoll_create(int size);
size参数并不起作用,它只是给内核一个提示,告诉它事件表需要多大。这个函数的返回值作为其他两个epoll函数的第一个参数,用来指定要访问的内核事件表;
创建一个内核事件表其实就是在Linux内核中创建一颗空的红黑树和空的就绪队列,具体见下文。。。
2、epoll_ctl
维护该epoll模型,主要的体现就是采用回调机制激活红黑树中的结点到就绪队列中;
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl是epoll的事件注册函数,它不同于select函数的是select是在监听事件的时候告诉内核要监听什仫类型的事件,epoll_ctl
是先注册要监听的事件类型;
epfd:是epoll_create()的返回值,用来指定监听的是哪一个epoll模型;
op:指定操作类型,一般由三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
fd:指定需要监听的文件描述符fd;
event:指定参数,它是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事件 */
epoll_data_t data; /* 用户数据 */
};
epoll_data是一个联合体,其中fd使用的最多,表示指定事件的文件描述符;ptr可以用来指定与fd相关的用户数据。如果要将文件描述符和用户数据关联起来,可以让ptr指向的用户数据这种包含fd。
在epoll_event中的events成员是从以下几个宏来体现的:
EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(LevelTriggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里;
3、epoll_wait
在该文件上等待特定文件描述符上的就绪事件,它是在一段超时事件内等待一组文件描述符上的事件;
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
返回值:成功返回等待文件描述符的个数;失败返回-1并设置错误码;超时返回0;
epfd:是epoll_create()的返回值,用来指定监听的是哪一个epoll模型;
events:这个参数也是epoll_event结构体的一个成员;
epoll_wait()如果检测到事件,就将所有的就绪事件从内核事件表(epfd所指定的epoll模型)中复制它的第二个参数events中去,这个结构体类型的数组只用于输出检测到的就绪事件,而不是向select一样是一个输入输出型参数,这样就极大地提高了应用程序查找就绪文件描述符的效率(这里采用的是mmap技术);
maxevents:该参数用来指定最多监听多少个事件,它必须大于0;
timeout:这个参数和poll接口中的timeout参数类似,是用来设置超时时间的;
二、ET模式和LT模式
epoll对文件描述符的操作有两种方式:LT(水平触发)和ET(边沿触发),epoll的默认工作方式是LT,在这种模式下epoll相当于一个相对较高效的poll。ET模式是epoll相对较为高效的工作方式;
对于LT工作模式的文件描述符,当检测到有事件发生并将此事件通知应用程序之后,应用程序可以不立即处理该事件,当下一次调用epoll_wait()
的时候,epoll_wait()
还会再次向应用程序通告此事件,直到该事件被处理;
对于ET工作模式的文件描述符,当检测到有事件发生并将此事件通知应用程序之后,应用程序就会立即处理该数据,因为后续的epoll调用将不再向应用程序通知这一事件。ET模式在很大程度上降低了同一个epoll事件被触发的次数,因为比LT模式效率要高。
每个使用ET模式的文件描述符都应该是非阻塞的。如果文件描述符是阻塞的,那仫读或者写操作会因为没有后续事件而一直处于阻塞状态(饥渴状态);
三、epoll的工作原理
四、epoll的优点
1>、底层采用回调机制激活某个节点,将已经就绪的文件描述符添加到就绪队列中去;
2>、当有新的事件发生的时候,该结点会被插入到epoll模型中的红黑树中去,红黑树增删查改效率比较高,它的时间复杂度为O(N*lgN);
3>、epoll_wait获得就绪的文件描述符是从就绪队列中获得的,它的时间复杂度为O(1),这也是epoll的时间复杂度同时也是epoll高效的原因;
4>、epoll所关心的文件描述符是无上限的;
5>、将就绪队列中的数据结点映射到epoll_wait的events结构体中,节省了一次内核态至用户态的拷贝,这是mmap技术(内存映射技术);
6>、就绪事件的陈列方式不同,epoll访问的是就绪队列避免了访问无价值的数据,而select是访问的是一个数组;
五、一个关于epoll的例子
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#define SIZE 10240
//创建一个结构体类型,里面保存一个文件描述符和一段缓冲区
typedef struct epbuf
{
int fd;
char buf[SIZE];
}epbuf_t,*epbuf_p,**epbuf_pp;
static epbuf_p alloc_epbuf(int fd)
{
epbuf_p ptr=(epbuf_p)malloc(sizeof(epbuf_t));
if(NULL == ptr)
{
perror("malloc");
exit(6);
}
ptr->fd=fd;
return ptr;
}
static void del_epbuf(epbuf_p ptr)
{
if(NULL != ptr)
{
free(ptr);
ptr=NULL;
}
}
int StartUp(const char *ip,int port)
{
assert(ip);
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket error");
exit(2);
}
//端口号复用
int opt=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
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 error");
exit(3);
}
if(listen(sock,10) < 0)
{
perror("listen error");
exit(4);
}
return sock;
}
int myread(int sock,char *buf)
{
int len=0;
int total=0;
while((len=read(sock,buf+total,1024) > 0) && (len == 1024))
{
//读成功并且读到的值与期望值相符合
total += len;
}
//没有读到1024个字节,更新total
if(len > 0 && len < 1024){
total += len;
}
buf[total]='\0';
return total;
}
int set_fd_nonblock(int sock) //设置非阻塞
{
int old_opt=fcntl(sock,F_GETFL);
int new_opt=old_opt | O_NONBLOCK;
fcntl(sock,F_SETFL,new_opt);
return old_opt;
}
static void Usage(const char *proc)
{
printf("Usage:%s [local_ip] [local_port]\n",proc);
}
int main(int argc,char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
int listenfd=StartUp(argv[1],atoi(argv[2]));
int epfd=epoll_create(256); //创建一个epoll模型,其实底层就是创建一颗红黑树和就绪队列
if(epfd < 0)
{
perror("epoll_create error");
return 5;
}
struct epoll_event ev;
ev.events=EPOLLIN | EPOLLET; //工作模式是ET模式
ev.data.ptr=alloc_epbuf(listenfd);
set_fd_nonblock(listenfd); //如果工作在ET模式则必须设置非阻塞
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); //将监听套接字添加到epoll模型中
while(1)
{
struct epoll_event evs[32];
int max_evs=32;
int timeout=1000; //设置超时时间
//int timeout=-1; //此时说明阻塞
int nums=0;
switch(nums=epoll_wait(epfd,evs,max_evs,timeout))
{
case -1:
perror("epoll_wait error");
break;
case 0:
printf("timeout...\n");
break;
default:
{
int i=0;
for(;i<nums;i++){
int sock=((epbuf_p)(evs[i].data.ptr))->fd;
if((sock == listenfd) && (evs[i].events & EPOLLIN)){
//监听事件到来并且发生了EPOLLIN事件
struct sockaddr_in client;
socklen_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,ip is %s,port is %d\n",\
inet_ntoa(client.sin_addr),ntohs(client.sin_port));
ev.events=EPOLLIN | EPOLLET; //ET
set_fd_nonblock(new_sock);
ev.data.ptr=alloc_epbuf(new_sock);
epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
}//if
else if((sock != listenfd) && (evs[i].events & EPOLLIN)){
//普通事件到达并且发生了EPOLLIN事件
char *buf=((epbuf_p)(evs[i].data.ptr))->buf;
//ssize_t s=read(sock,buf,SIZE-1);
ssize_t s=myread(sock,buf);
if(s > 0){ //读成功
buf[s-1]='\0';
printf("client# %s\n",buf);
//修改事件的状态并将其添加到就绪队列中去
ev.events=EPOLLOUT;
epoll_ctl(epfd,EPOLL_CTL_MOD,sock,&ev);
}
else if(s == 0){ //有客户端退出
printf("client is quit\n");
del_epbuf(evs[i].data.ptr);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,NULL);
close(sock);
}
else{ //读失败
perror("read");
del_epbuf(evs[i].data.ptr);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,NULL);
close(sock);
}
}//else if
else if((sock != listenfd) && (evs[i].events & EPOLLOUT))
{
//是普通事件并且发生了EPOLLOUT事件
char *msg="HTTP/1.0 200 OK\r\n<html><h1>HELLO EPOLL!!!</h1></html>\n";
ssize_t ret=write(sock,msg,strlen(msg));
if(ret < 0)
{
perror("write");
return 6;
}
del_epbuf(evs[i].data.ptr);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,NULL);
close(sock);
}
else //其他事件不做任何处理
{}
}//for
}
break;
}
}
return 0;
}
使用telnet测试结果展示:
在这里就分享结束了~~~