概述
概念:IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。
select:select系统调用是用来让我们的程序监视多个文件句柄的状态变化。
1.函数原型:intselect(int nfds,fd_set *readfds,fd_set
*writefds,fd_set *exceptsfds,conststruct timeval *timeout)
返回值:就绪描述符的数目,超时返回0,出错返回-1。
参数:nfds是需要监听的最大文件描述符+1(即文件描述的个数),第2,3,4分别对应需要检测的可读/写/异常文件描述的集合,struct timeval结构设置超时范围。
可通过以下四个宏对文件描述符进行设置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写
运用select编写的一个tcp服务器:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<sys/select.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define _SIZE_ 128
int gfds[_SIZE_];
int main(int argc,char* argv[])
{
if(argc!=3)
{
exit(1);
}
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("sock");
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_addr.s_addr=inet_addr(argv[1]);
local.sin_port=htons(atoi(argv[2]));
socklen_t len=sizeof(local);
if(bind(sock,(struct sockaddr*)&local,len)<0)
{
perror("bind");
exit(3);
}
if(listen(sock,5)<0)
{
perror("listen");
exit(4);
}
//select
int i=0;
for(;i<_SIZE_;i++)
{
gfds[i]=-1;
}
gfds[0]=sock;
while(1)
{
int max_fd=-1;
struct timeval timeout={5,0};
fd_set rfds;
FD_ZERO(&rfds);
int j=0;
for(;j<_SIZE_;j++)
{
if(gfds[j]>=0)
FD_SET(gfds[j],&rfds); //设置读集
if(max_fd<gfds[j])
max_fd=gfds[j]; //更新最大文件描述符
}
switch(select(max_fd+1,&rfds,NULL,NULL,&timeout))
{
case 0:
printf("timeoutn");
break;
case -1:
printf("faildn");
break;
default: //成功
{
int k=0;
for(;k<_SIZE_;k++)
{
//没就绪
if(gfds[k]<0)
{
continue;
}
if(gfds[k]==sock&&FD_ISSET(gfds[k],&rfds))
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int new_sock=accept(gfds[k],(struct sockaddr*)&peer,&len);
int m=0;
for(;m<_SIZE_;m++)
{
if(gfds[m]==-1)
{
gfds[m]=new_sock;
break;
}
}
if(m==_SIZE_)
{
close(new_sock);
}
}//if
else if(FD_ISSET(gfds[k],&rfds))
{
char buf[128];
int s=read(gfds[k],buf,sizeof(buf)-1);
if(s==0)//对端连接关闭了,行当于读到文件结尾
{
printf("client is quitn");
close(gfds[k]);
gfds[k]=-1;
}
if(s>0)
{
buf[s-1]=0;
printf("%sn",buf);
}
}
}
}
}
}
}
select的几大缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024
epoll:
1. epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
2. epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
运用epoll编写的一个tcp服务器:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<stdlib.h>
#include<string.h>
typedef struct epbuf
{
int fd;
char buf[1024];
}epbuf_t,*epbuf_p,**epbuf_pp;
static epbuf_p alloc_epbuf(int fd)
{
epbuf_p ptr=(epbuf_p)malloc(sizeof(epbuf_t));
if(ptr==NULL)
{
perror("malloc");
exit(5);
}
ptr->fd=fd;
return ptr;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("参数错误n");
exit(1);
}
int listen_sock=socket(AF_INET,SOCK_STREAM,0);
if(listen_sock<0)
{
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_addr.s_addr=inet_addr(argv[2]);
local.sin_port=htons(atoi(argv[1]));
socklen_t len=sizeof(local);
if(bind(listen_sock,(struct sockaddr*)&local,len)<0)
{
perror("bind");
exit(3);
}
if(listen(listen_sock,5)<0)
{
perror("listen");
exit(4);
}
//1.创建一个epoll
int epfd=epoll_create(200);
struct epoll_event ev;
ev.events=EPOLLIN|EPOLLET;
ev.data.ptr=alloc_epbuf(listen_sock);
//2.注册
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);
while(1)
{
int nums=0;
struct epoll_event evs[32];
int max_evs=32;
int timeout=5000;
switch(nums=epoll_wait(epfd,evs,max_evs,timeout))
{
case 0: //超时
printf("timeoutn");
break;
case -1:
perror("epoll_wait");
break;
default:
{
int i=0;
for(;i<nums;i++)
{
int fd=((epbuf_p)(evs[i].data.ptr))->fd;
if((evs[i].events&EPOLLIN)&&fd==listen_sock)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int new_sock=accept(fd,(struct sockaddr*)&peer,&len);
if(new_sock<0)
{
perror("accept" );
continue;
}
ev.events=EPOLLIN;
ev.data.ptr=alloc_epbuf(new_sock);
epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
}//if
else if((evs[i].events&EPOLLIN)&&fd!=listen_sock)//读事件就绪
{
char* buf=((epbuf_p)(evs[i].data.ptr))->buf;
ssize_t s=read(fd,buf,1023);
if(s>0)
{
buf[s]=0;
printf("%sn",buf);
//回写
ev.events=EPOLLOUT;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);
}
else if(s==0)
{
free(evs[i].data.ptr);
evs[i].data.ptr=NULL;
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
}
}//else if
else if((evs[i].events&EPOLLOUT)&&fd!=listen_sock)//写事件就绪
{
const char* msg="HTTP/1.0 200 OK rnrn<html><h1>HELLO WORLD!</h1></html>n";
write(fd,msg,strlen(msg));
free(evs[i].data.ptr);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd ,NULL);
}
}
}
}
}
return 0;
}
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
select与epoll的区别与联系:
epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。
1. epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
2. epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。
3. epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目
最后
以上就是阳光砖头为你收集整理的I/O多路复用之select、epoll的实现和区别 ,ET与LT模式的全部内容,希望文章能够帮你解决I/O多路复用之select、epoll的实现和区别 ,ET与LT模式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复