select实现I/O复用
select可以用来监视文件描述符的变化,可同时监视多个文件描述符,监视项称为事件。首先将要要监视的文件描述符集中到fd_set结构体,在fd_set变量中注册及修改的操作由下列宏完成:
1.FD_ZERO(fd_set* fdset):将fd_set变量的所有位置初始化为0
2.FD_SET(int fd_id,fd_set* fdset):在fdset中注册文件描述符fd_id
3.FD_ISSET(int fd_id,fd_set* fdset):若fdset所指变量中包含文件描述符fd_id的信息则返回真
4.FD_CLR(int fd_id,fd_set * fdset):从fdset中注销文件描述符fd_id
#include <sys/select.h>
int select( int nfds, fd_set FAR* readfds, fd_set * writefds, fd_set * exceptfds, const struct timeval * timeout);
nfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
readfds:(可选)指针,指向一组等待可读性检查的套接口。
writefds:(可选)指针,指向一组等待可写性检查的套接口。
exceptfds:(可选)指针,指向一组等待错误检查的套接口。
timeout:select()最多等待时间,对阻塞操作则为NULL。
以下为select应用实例
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<sys/select.h>
#define BUF_SIZE 100
void error_handling(char *buf);
int main(int argc,int *argv[])
{
int serv_sock,clnt_sock;
struct sockaddr_in serv_addr,clnt_addr;
struct timeval timeout;
fd_set reads,cpy_reads;
socklen_t addr_sz;
int fd_max,str_len,fd_num,i;
char buf[BUF_SIZE];
if(argc!=2)
{
printf("error\n");
exit(1);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0);
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv(1)));
if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
{
error_handling("bind() error");
}
if(listen(serv_sock,5)==-1)
error_handling("listen() error")
FD_ZERO(&reads);
FD_SET(serv_sock,&reads);
fd_max=serv_sock;
while(1)
{
//在while无限循环中调用select,因发生关注的事件时返回大于0的值,超时返回0,出错返回-1
cpy_reads=reads;
//设置超时间隔,防止进入无限阻塞状态
timeout.tv_sec=5;
timeout.tv_usec=5000;
if((fd_num=select(fd_max+1,&cpy_reads,0,0,&timeout))==-1)
break;
if(fd_num==0)//超时
continue;
for(i=0;i<fd_max+1;i++)
{//轮询,查找状态发生变化的文件描述符
if(FD_ISSET(i,&cpy_reads))
{
if(i==serv_sock)//有连接请求
{
addr_sz=sizeof(clnt_addr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&addr_sz);
FD_SET(clnt_sock,&reads);
if(fd_max<clnt_sock)
{
fd_max=clnt_sock;
}
printf("connnected client:%d\n",clnt_sock);
}
else//有消息要读取
{
str_len=read(i,buf,BUF_SIZE);
if(str_len==0)//close request
{//都到了文件结束符EOF,关闭与客户端连接的相应套接字
FD_CLR(i,&reads);
close(i);
printf("closed client:%s\n", i);
}
else
{
write(i,buf,str_len);
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *buf)
{
fputs(buf,stderr);
fputc('\n',stderr);
exit(1);
}
select技术的I/O复用速度慢的原因分析:
1.调用select后常见的对所有文件描述符的循环遍历查询
2.每次调用select时都需要向操作系统传递监视对象信息(这才是select I/O性能差的原因所在),对程序造成负担。
select I/O复用的优点:
1.服务端接入者少
2.程序移植性强
epoll实现I/O复用
1.下面来看相关API
int epoll_create(int size)
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
自从Linux 2.6.8开始,size参数被忽略,但是依然要大于0。
2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
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)
等待事件的产生。参数events用来从内核得到事件的集合,maxevents表示每次能处理的最大事件数,告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞直到有事件产生)。该函数返回需要处理的事件数目,如返回0表示已超时。
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<sys/select.h>
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);
int main(int argc,int *argv[])
{
int serv_sock,clnt_sock;
struct sockaddr_in serv_addr,clnt_addr;
socklen_t addr_sz;
int str_len,i;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd,event_cnt;
if(argc!=2)
{
printf("error\n");
exit(1);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0);
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv(1)));
if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
{
error_handling("bind() error");
}
if(listen(serv_sock,5)==-1)
error_handling("listen() error");
epfd=epoll_create(EPOLL_SIZE);
ep_events=(epoll_event *)malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event.events=EPOLLIN;//需要读取数据的情况
event.data.fd=serv_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&
4);//epoll例程中注册文件描述符serv_sock,目的是监视参数event中的事件
while(1)
{
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);//最后一个参数传递-1表示等待一直到事件发生
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}
for(i=0;i<event_cnt;i++)
{//遍历发生事件的文件描述符集合
if(ep_events[i].data.fd==serv_sock)//有连接请求
{
addr_sz=sizeof(clnt_addr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&addr_sz);
event.events=EPOLLIN;
event.data.fd=clnt_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,clnt_sock,&event);//向epfd例程注册clnt_sock
printf("connected client:%d\n",clnt_sock);
}
else
{
str_len=read(ep_events[i].data.fd,buf,BUF_SIZE);
if(str_len==0)//close request
{
epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);//从epfd例程中注销文件描述符ep_events[i].data.fd
close(ep_events[i].data.fd);
printf("closed client:%d\n",ep_events[i].data.fd);
}
else
{
write(ep_events[i].data.fd,buf,str_len);
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void error_handling(char *buf)
{
fputs(buf,stderr);
fputc('\n',stderr);
exit(1);
}
基于epoll的I/O复用技术的优点:
1.发生变化的文件描述符信息将被填到epoll_event结构体数组中,因此我们只需要遍历这个数组就可以得到所有有事件产生的文件描述符而不是遍历这个进程的所有文件文件描述符(可能这个进程下有很多文件描述符没有事件发生,遍历会造成不必要的时间开销)
2.调用epoll_wait函数时无需每次向操作系统传递对象信息
epoll的条件触发和边缘触发:(select是条件触发)
epoll在默认情况下是条件触发。
1.条件触发:条件触发方式中只要输入缓冲区还有数据就一直通知该事件
2.边缘触发:当输入缓冲区收到数据时仅注册一次该事件,即使输入缓冲区还有数据也不会再触发
在条件触发下,如果服务器输入缓冲区收到数据,但未及时读取(延迟处理)会造成每次执行epoll_wait时都会通知该事件,带来大量的事件通知,可能使服务器无法承受。