一、select
采用的是集合的方式,将关心的事件放置集合队列(最多监听1024个)中,轮询访问(每次都会检测所有的句柄)拿到一个已就绪的就会返回,(内核态到用户态的切换来拿事件),内部再使用位运算,将可读,可写,异常三个事件分开来,
1、select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯的改变进程打开的文件描述符个数,并不能改变select监听的文件个数。
2、解决了1024以下客户端使用select 是很合适的,select采用的是轮询模式,但如果连接客户过多,会大大降低服务器的响应效率。
3、循环次数过多,每次有一个文件描述符准备好就会返回,可能就会一直处于激活状态,因为有文件描述符的拷贝(每次都要扫描注册的文件描述符集合,将已准备好的文件描述符返回给用户),系统从内核态切换用户态的次数会过多,造成性能下降
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
//nfds: 监控的文件描述符的个数
//exceptfds 监控异常发生到达文件描述符集合
//timeout:定时阻塞监控时间
1、null 永久等下去
2、设置timeval ,等待固定时间
3、将timeval中时间设置为0,检查描述字后立即返回,轮询
二、poll
poll 比select 能 好一点,也是在指定时间内轮询一定数量的文件描述符,以测试是否有文件描述符就绪
三、epoll
把用户关心的文件描述符直接放置内核里的一个事件表(红黑树)中,不会像select与poll那样,每次调用都要传入文件描述符集或事件集,epoll_wait()函数当检测到就绪事件时,会将已准备好的文件描述符拷贝到它第二个参数指向的数组(链表)中,系统只需要从数组中取事件即可。用户态与内核态切换的次数并不多,极大地提高了效率
三个系统调用
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);
首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。
epoll_ctl可以操作上面建立的epoll,例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。
epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>
#define SIZE 64
const char* msg = "HTTP/1.0 200 OK\r\n\r\n<html><h1>hello epoll!<h1></html>\r\n";
static void Usage(const char* proc)
{
printf("Usage: \n\t %s [local_ip][local_port]\n\n",proc);
}
int startup(const char *ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);//创建套结字
if(sock<0)
{
perror("socket");
exit(2);
}
//当服务器异常关闭时,清除套结字,就可再次重启
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
//设置结构体,填充自己ip与端口号
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);//端口号的转换
local.sin_addr.s_addr = inet_addr(ip);//ip
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)//绑定套结字
{
perror("bind");
exit(3);
}
if(listen(sock,10)<0)//监听队列,里面有10个文件描述符,返回已经准备好链接的那一个
{
perror("listen");
exit(4);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
return 1;
}
int listen_sock = startup(argv[1],atoi(argv[2]));//把得到的套结字作为监听套结字
int epfd = epoll_create(256);//创建epoll模型
if(epfd < 0)
{
perror("epoll_create is failed\n");
return 5;
}
printf("listen_sock:%d\n",listen_sock);
//设置结构体事件,并把事件与套结字放在就绪队列中
struct epoll_event ev;
ev.events = EPOLLIN;//表示对应的文件描述符可以读
ev.data.fd = listen_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);//事件注册函数,将监听套结字加入监听事件
int nums = -1;
int timeout = 10000;//可设置-1阻塞0非阻塞
//struct timeval timeout = {1,0}; //设置为0,0 非阻塞状态
struct epoll_event revs[SIZE];
//####################################################################################################
//对已连接的客户端进行数据处理
while(1)
{
switch((nums = epoll_wait(epfd,revs,SIZE,timeout)))//监听并判断是否有文件描述符属性发生改变
//后三个参数为输出形,等文件描述符就位,返回0 -1 或着已就位的文件描述符的个数
{
case 0: printf("timeout...\n");break;
case -1:perror("epoll");break;
default:
{
int i = 0;
for(i=0;i<nums;i++)
{
int fd = revs[i].data.fd;//从就绪队列拿出已就绪好的fd
if(fd == listen_sock && (revs[i].events &EPOLLIN))//检测监听套结字是否存在链接
{
//listen socket ready!
struct sockaddr_in client;//
socklen_t len = sizeof(client);
int rw_sock=accept(listen_sock,(struct sockaddr*)&client,&len) ;//客户端向服务器发出链接请求
//服务器利用accept()来接受请求,建立连接,并拿到客户端套结字
if(rw_sock <0)
{
perror("accept failed");
continue;
}
printf("get a new client :[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));//输出客户端ip与端口号
ev.events = EPOLLIN;//设置文件描述符为可读
ev.data.fd = rw_sock;//监听此套结字
epoll_ctl(epfd,EPOLL_CTL_ADD,rw_sock,&ev);//将拿到的套结字加入监听事件
}
else if(fd != listen_sock) //
{
if(revs[i].events & EPOLLIN)//有数据来临时,(接收客户端数据)
{
//read ready
char buf[1024];
ssize_t s = read(fd,buf,sizeof(buf)-1);
if(s>0) // read success
{
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 is quit!\n");
close(fd);//关闭文件描述符
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);//删除
}
else
{
perror("read");
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
}
}
else if(revs[i].events & EPOLLOUT)//有数据要写时
{
//write
write(fd,msg,strlen(msg));//写msg内容
close(fd);//关闭并删除
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
}
}
}
}
}
}
return 0;
}
从上面的调用方式就可以看到epoll比select/poll的优越之处:因为后者每次调用时都要传递你所要监控的所有socket给select/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。而我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。
所以,实际上在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
epoll______LT(电平触发) 默认的,相当于高效的poll
在事件就绪时,epoll_wait()会通知你,你可以不对此事件作出反应。
当下次再调用epoll_wait()时,当你没有对此事作出反应时,还用通知你
ET(边缘触发)epoll的高效工作模式,(需向epoll内核事件表中注册一fd上的EPOLLET事件)
当事件就绪,epoll_wait()通知你,你必须处理此事件,因为下次不会再通知你
三种I/O复用区别:
从原理上看:
select 与poll都是采用了轮询方式去访问文件描述符集,每次返回准备就绪的一个,时间复杂度为O(n),epoll_wait()采用回调方式,当检测到有就绪描述符时,会触发回调函数,将就绪描述符加载到就绪队列中,内核会在合适的时机将事件拷贝到用户空间,时间复杂度为O(1)
从内存消耗:
select/poll将所监控的文件描述符从用户态copy到内核态,切换频率高,效率低下
文件描述符上限
epoll几乎没有上限(与内存有关)