基本概念:
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
(1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
(3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
(4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
(5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
select:
系统提供select函数用来实现I/O多路复用输入/输出模型。select系统调用是用来让我们的程序监视多个文件描述状态变化的。程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生状态变化。通常I/O操作有两个步骤,一个是等,另一个是数据搬迁。select主要是在等的这个状态阻塞着直到事件发生。
函数原型:
int select(int nfds,fd_set *reads,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
参数:
nfds:是需要监视的最大的文件描述符的值+1。
fd_set:
fd_set底层是用位图实现的,每一个位都代表一个文件描述符。readfds,writefds,exceptfds分别对应于需要检测的可读文件描述符结合,可写描述符集合,异常文件描述符集合,他们都是输入输出型参数。
当作为输入参数时:只要文件描述符集合中对应的位上为1,就表示select需要监视这个描述符的状态。比如readfds里面的文件描述符就代表他们需要等待读事件,writefds里面的文件描述符就代表他们需要等待写事件。
当作为输出参数时,只要文件描述符集合中对应的位上为1,就代表他们等待的事件已经就绪,这是由内核设定的。
timeout:设置超时时间。
timeout里面的成员设定为特定的时间值:
如果在这段时间里面没有事件发生,select将超时返回。struct timeval结构用于描述一段时间长度,如果在这个时间内,需要监听的描述符没有事件发生则函数返回,返回值为0。
struct timeval
{
long tv_sec; //秒
long tv_usec; //微秒
}
timeout里面的成员等于0:表示非阻塞轮询方式,不断的去检测描述符集合的状态,然后立即返回。
timeout为NULL:表示以阻塞的方式等待事件发生。
返回值:
成功的话,返回文件描述符状态已改变的个数。如果返回0代表在描述符状态改变之前已经超过timeout时间。如果有错误发生的话,则返回-1。
另外,还有一组与fd_set 有关的操作
- FD_SET(fd, _fdset),把fd加入_fdset集合中
- FD_CLR(fd, _fdset),把fd从_fdset集合中清除
- FD_ISSET(fd, _fdset),判定fd是否在_fdset集合中
- FD_ZERO(_fdset),清除_fdset有描述符
1、每次调用select,都需要把fd集合从用户态拷贝到内核态。这个开销在fd很多的时候会很大。
2、select在返回之后,需要我们遍历数组去查找事件就绪的描述符。这个过程的时间复杂度是O(N)。而epoll它查找就绪事件的时候是O(1)。
3、select支持的文件描述符的数量太小了,默认是1024。
实现一个服务器,使用select让服务器可以同时接受多个客户的链接,并将客户发送的数据打印出来。
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<unistd.h>
#include<sys/time.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#define SIZE 128
int startup(char *ip,int port)
{
assert(ip);
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(0);
}
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");
exit(1);
}
if(listen(sock,5)<0)
{
perror("listen");
exit(2);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("usae: %s [IP] [PORT]\n",argv[0]);
return 0;
}
int lis_sock=startup(argv[1],atoi(argv[2]));
int gfds[SIZE];
memset(gfds,-1,SIZE*4);
fd_set rfds;
FD_ZERO(&rfds);
while(1)
{
struct timeval timeout={5,0};
gfds[0]=lis_sock;
int max_fd=-1;
int i=0;
for(;i<SIZE;i++)
{
if(max_fd<gfds[i])
{
max_fd=gfds[i];
}
if(gfds[i]>=0)
{
FD_SET(gfds[i],&rfds);
}
}
int ret=select(max_fd+1,&rfds,NULL,NULL,NULL);
switch(ret)
{
case 0:
printf("timeout...\n");
break;
case -1:
printf("error");
break;
default:
if(FD_ISSET(gfds[0],&rfds))
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int connfd=accept(lis_sock,(struct sockaddr*)&peer,&len);
if(connfd<0)
{
perror("accept");
}
else
{
printf("client: %s:%d fd(%d)\n",inet_ntoa(peer.sin_addr),\
ntohs(peer.sin_port),connfd);
int k=0;
for(;k<SIZE;k++)
{
if(gfds[k]==-1)
{
gfds[k]=connfd;
break;
}
}
if(k>=SIZE)
{
close(connfd);
gfds[k]=-1;
}
}
}
int j=1;
for(;j<SIZE;j++)
{
if(FD_ISSET(gfds[j],&rfds))
{
char buf[SIZE];
ssize_t s=read(gfds[j],buf,sizeof(buf));
if(s<0)
{
perror("read");
continue;
}
else if(s==0)
{
printf("client is quit!\n");
close(gfds[j]);
gfds[j]=-1;
}
else
{
buf[s]=0;
printf("client# %s\n",buf);
}
}
}
break;
}
}
return 0;
}