首先,还是需要理解io过程:io过程总体来看分两步,第一步就是等,第二步才是数据搬迁。而如果要想提高io的性能与效率,就要减少等的比重。
可以假想一个场景:
你去钓鱼,但是你只有一个鱼竿。你的同伴也和你一起去钓鱼,但是他带了100个鱼竿。假设每条鱼上钩的概率都是一样的,那么你和他相同的时间内,你在死盯着一个鱼竿,而他只需要来回巡视所有的鱼竿,一旦有鱼上钩,拿上来即可。很明显,它的这种方式就要比你高效得多。
如果理想情况下它的鱼钩足够多,就会出现一种情况,每秒内都有鱼上钩。
替换到我们的io模型中,多路转接就能够实现这种近似理想化的情况。
但是最重要的一点:
不论是之前的select还是poll与epoll模型,本质上都是为了io过程中的等待这一过程。
poll
poll在用法上与select大致相同。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//pollfd结构
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
参数说明:
- fds是一个poll函数监听的结构列表,每一个元素中,包含了三个部分:文件描述符,监听的事件集合,返回的事件集合。
- nfds:表示fds数组的长度。
- timeout:表示函数的超时时间,单位是毫秒
events的取值:
revents的取值:
返回值;
- 小于0:表示出错
- 等于0:表示poll函数等待出错
- 大于0:表示poll函数由由于监听的文件描述符就绪而返回
优点
select使用三个位图来表示关心的事件类型,而poll使用了一个结构体指针实现。
并且使用位图,对于关心的文件描述符上限也受制于位图的大小。
- pollfd结构包含了要关心的文件描述符和关心的事件以及发生的事件,比select使用起来更方便
- poll对文件描述符没有数量的限制。
缺点
- 和select函数一样,poll返回后,需要使用pollfd轮询来获取就绪的文件描述符。
- 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内存中。
随着文件描述符上升,效率也会逐渐下降。
代码实现:
#define MAX 1024
typedef struct pollfd pollfd;
void Add(int fd,pollfd* fd_list,int size)
{
int i=0;
for(i=0;i<size;i++)
{
if(fd_list[i].fd==-1)
{
fd_list[i].fd=fd;
fd_list[i].events=POLLIN;
break;
}
}
}
void Init(pollfd* fd_list,int size)
{
int i=0;
for(i=0;i<size;i++)
{
fd_list[i].fd=-1;
fd_list[i].events=0;
fd_list[i].revents=0;
}
}
int startup(int port)
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_addr.s_addr=htonl(INADDR_ANY);
local.sin_port=htons(port);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
exit(2);
}
if(listen(sock,5)<0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("Usage:[%s port]\n",argv[0]);
return 1;
}
int new_sock=startup(atoi(argv[1]));
pollfd fd_list[MAX];
Init(fd_list,sizeof(fd_list)/sizeof(pollfd));//???
Add(new_sock,fd_list,sizeof(fd_list)/sizeof(pollfd));
for(;;)
{
int ret=poll(fd_list,sizeof(fd_list)/sizeof(pollfd),1000);
if(ret<0)
{
perror("poll");
continue;
}
else if(ret==0)
{
printf("timeout...\n");
continue;
}
size_t i=0;
for(i=0;i<sizeof(fd_list)/sizeof(pollfd);i++)
{
if(fd_list[i].fd==-1)
{
continue;
}
if(!(fd_list[i].revents & POLLIN))
{
continue;
}
if(fd_list[i].fd==new_sock)
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
int connect_sock=accept(new_sock,(struct sockaddr*)&client,&len);
if(connect_sock<0)
{
perror("accpet");
continue;
}
Add(connect_sock,fd_list,sizeof(fd_list)/sizeof(pollfd));
}
else
{
char buf[MAX];
ssize_t s=read(fd_list[i].fd,buf,sizeof(buf)-1);
if(s<0)
{
perror("read");
continue;
}
else if(s==0)
{
printf("client quit!\n");
close(fd_list[i].fd);
fd_list[i].fd=-1;
}
else
{
printf("client say# %s\n",buf);
//write(fd_list[i].fd,buf,strlen(buf));
}
}
}
}
}
epoll模型
epoll,从命名上就可以看出与poll应该是有关联的。按照man手册的说法:epoll是为处理大批量句柄而做了改进的poll。他几乎具备了之前的poll与select的所有优点。
相关系统调用
epoll_create
调用该函数,操作系统会帮我们做三件事。
- 创建一颗红黑二叉树
- 创建一个就绪队列
- 创建回调机制
以上称作创建一个epoll模型。
#include <sys/epoll.h> int epoll_create(int size);
其中参数size是被忽略的(在Linux2.6.8以后),虽然是被忽略的,为了防止跨版本的问题,所以尽可能地给一个较大的值。
该函数用完之后必须用close函数关闭。
epoll_ctl
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
该函数是epoll的注册事件函数:
- 第一个参数是epoll_create的返回值
- 第二个参数表示动作,用三个宏表示
- 宏的取值:
- EPOLL_CTL_ADD:注册新的fd至epfd中
- EPOLL_CTL_MOD :修改已经注册的fd的监听事件
- EPOLL_CTL_DEL:从epfd中删除一个fd
- 第三个参数是需要监听的fd
- 第四个参数是告诉内核需要监听什么事。
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 events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
event的取值:
enum EPOLL_EVENTS
{
EPOLLIN = 0x001,//关心读事件
#define EPOLLIN EPOLLIN
EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI
EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT
EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM
EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND
EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM
EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND
EPOLLMSG = 0x400,
#define EPOLLMSG EPOLLMSG
EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR
EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP
EPOLLRDHUP = 0x2000,
#define EPOLLRDHUP EPOLLRDHUP
EPOLLWAKEUP = 1u << 29,
#define EPOLLWAKEUP EPOLLWAKEUP
EPOLLONESHOT = 1u << 30,
#define EPOLLONESHOT EPOLLONESHOT
EPOLLET = 1u << 31
#define EPOLLET EPOLLET
};
可以看出这些宏是一个个的宏,并且是一个二进制序列且只有一个1不会重复。所以需要关心多个使用多个按位或即可。
epoll_wait
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
该函数收集在epoll监控的事件中已经发送的事件。
- 参数events事分配好的epoll_event结构体数组
- epoll将会把发生的事件赋值到events数组中
- maxevents告诉内核这个events有多大,不能大于创建epoll模型时的size大小
- 参数timeout是超时时间
- 如果函数调用成功,返回对应i/o上已准备好的文件描述符数目,返回0表示已超时,小于0表示失败。
工作原理
当调用epoll_create函数时,系统会创建一个epoll模型,也就是做三件事,创建红黑树,就绪的队列,回调机制。
红黑树将存储epoll所监听的套接字。用来存储所有的套接字,当进行add或者del的时候,都从红黑树上去处理,这样时间复杂度就可以保持在O(logn)。
当添加事件以后,这个事件就会和相应的设备驱动程序建立回调关系,当相应的时间发生的时候,这个时候就会去调用回调函数。回调函数就完成了把时间添加到链表当中。
当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
总结下来:epoll的使用方法分三步:
- 创建epoll模型
- 进行注册关心的文件描述符
- 等待文件描述符就绪
epoll的优点
其实从epoll的工作原理就能够看出epoll虽然原理复杂,但是使用起来会比select方便。不管是poll还是select数据结构都需要自己去维护,而epoll不需要,只需要你无脑式地将你关心地文件描述符事件注册进去,并且直接去就绪队列中取即可。
优点总结:
- 文件描述符无上限:通过epoll_ctl来注册一个文件描述符,内核中使用红黑树来管理所有需要监控地文件描述符。
- 基于事件的就绪通知方式:一旦被监听的某个文件描述符就绪,内核会采用回调机制,迅速激活该文件描述符,即是就绪的文件描述符增多,也不会影响性能。
- 维护就绪队列:当文件描述符就绪,就会被放到内核中的一个就绪队列中,只需要取队列中元素即可。
void service(int epfd,struct epoll_event *revs,int num,int listen_sock)
{
//既关心读有关心写
int i=0;
struct epoll_event ev;
for(i=0;i<num;i++)
{
int fd=revs[i].data.fd;
if(revs[i].events&EPOLLIN)
{
//read
if(fd==listen_sock)//accept
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
int new_fd=accept(fd,(struct sockaddr*)&client,&len);
if(new_fd<0)
{
perror("accept");
continue;
}
printf("get a connection!\n");
ev.events=EPOLLIN;
ev.data.fd=new_fd;
epoll_ctl(epfd,EPOLL_CTL_ADD,new_fd,&ev);
}
else
{
//route
char buf[1024];
ssize_t s=read(fd,buf,sizeof(buf));
if(s>0)
{
buf[s]=0;
printf("client:#%s\n",buf);
ev.events=EPOLLOUT;
ev.data.fd=fd;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);
}
else if(s==0)
{
printf("client quit!\n");
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);//删除该fd
}
else
{
perror("read");
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);//删除该fd
}
}
}
if(revs[i].events&EPOLLOUT)
{
//write
const char* msg="HTTP/1.0 200 OK\r\n\r\n<html><h1>EPOLL SUCCESS:)</h1><></html>\r\n";
write(fd,msg,strlen(msg));
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
}
}
}
int startup(int port)
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_addr.s_addr=htonl(INADDR_ANY);
local.sin_port=htons(port);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
exit(2);
}
if(listen(sock,5)<0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("Usage:[%s port]\n",argv[0]);
return 1;
}
int listen_sock=startup(atoi(argv[1]));
int epfd=epoll_create(MAX);
if(epfd<0)
{
perror("epoll_create");
return 5;
}
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=listen_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);
struct epoll_event r_ev_s[MAX];
int size=0;
for(;;)
{
switch(size=epoll_wait(epfd,r_ev_s,MAX,-1))
{
case -1:
perror("epoll_wait");
break;
case 0:
printf("timeout....\n");
break;
default:
service(epfd,r_ev_s,size,listen_sock);
break;
}
}
}