linux中IO多路复用技术主要有三种:select 、poll 、epoll。
1. 什么是多路IO复用:
其实就是通过一种机制,可以监视多个描述符(Linux中一切皆文件,打开一个文件就需要一个文件描述符,同样,在套接字中建立一个连接也需要一个描述符,这个描述符在计算机中是有个数限制的),一旦某个描述符可读或可写或某些我们需要的状态发生变化,则这种机制就能通知程序,从而进行读写或其它操作。
本节只做select函数说明,poll、epoll将在之后说明
2. 网络多路IO复用模型之select
使用select可以完成非阻塞方式工作的程序,它能够监测需要被监视的文件描述符的变化情况---读写或者异常。但是select监视文件描述符的个数是固定的(1024/2048)。
select函数原型:
int select(int nfd, fd_set *readfd, fd_set *writefd,fd_set *errfd, struct timeval *timeout);
maxfd : 指明需要监视所有文件描述符的最大值,并且要+1
readfd: 是一个指向fd_set结构的指针,fd_set可以理解为一个集合,该集合中存放一些文件描述符,这个变量里存放了一些要监视读变化的描述符。
writefd: 同readfd,只不过,这里的文件描述符监视的是写变化的(如果有一个文件可写,则select返回大于0的数)
errfd: 同理,监视发生错误的文件描述符号
timeout: 设置超时时间,主要有三种状态,A.传入NULL,一直阻塞,直到状态变化 B.传入0,则一直返回,无变化为0,有变化为正值,C.大于0,则是超时等待的时间,状态无变化,则超时返回,返回值为0,否则为正值
3. 示例,使用select提高服务器处理能力(服务端代码)
int func_select(int serverfd)
{//serverfd为服务端套接字描述符,该函数实现服务端监听客户端连接,并监听已连接客户端的发送数据
fd_set client_fdset; //监听文件描述符集合
int maxsock = serverfd; //文件描述符中最大文件描述符号
struct timeval timeout; //超时时间
int client_fd[5] = {0}; //已连接客户端套接字
int connectcount = 0; //描述符数量
char buff[1024];
int ret = 0;
const int connecte = 10; //可连接最大数
while(1)
{
FD_ZERO(&client_fdset); //初始化集合
FD_SET(serverfd,&client_fdset); //将该描述符加入到描述符集合中
timeout.tv_sec = 30; //设置select超时时间
timeout.tv_usec = 0;
for(int i = 0;i<connecte;i++) //将可能存在的描述符加入监听
{
if(client_fd[i] != 0)
{
FD_SET(client_fd[i],&client_fdset);
}
}
/*select只监听可读状态*/
ret = select(maxsock+1,&client_fdset,NULL,NULL,&timeout);
if(ret == -1)
break;
else if(ret == 0)
continue;
//遍历已经连接描述符,检查是否有可都状态
for(int i = 0;i<connectcount;i++)
{
if(FD_ISSET(client_fd[i],&client_fdset)) //判断当前描述符是否加入监视集合
{
ret = recv(client_fd[i],buff,1024,0);
if(ret <= 0)
{
close(client_fd[i]);
FD_CLR(client_fd[i],&client_fdset);
client_fd[i] = 0;
}
else
{
printf("%d -->recv data %s\n",i,buff);
}
}
}
if(FD_ISSET(serverfd,&client_fdset)) //检查是否有新的连接
{
struct sockaddr_in addr_cl;
size_t socksize = sizeof(struct sockaddr_in);
int sockclient = accept(serverfd,(struct sockaddr*)&addr_cl,&socksize);
if(sockclient <= 0)
{
printf("accept err!");
continue;
}
if(connectcount < connecte)
{
client_fd[connectcount++] = sockclient; //保存新连接,方便加入描述符集合
bzero(buff,1024);
strcpy(buff,"this is server! welcome!\n");
send(sockclient,buff,1024,0);
bzero(buff,sizeof(buff));
ret = recv(sockclient,buff,1024,0);
if(ret < 0)
{
printf("recv error!");
close(sockclient);
return 0;
}
printf("recv data:%s\n",buff);
if(sockclient > maxsock)
{
maxsock = sockclient; //记录最大套接字描述符
}
else
{
printf("max connect ::quit:::\n");
}
}
}
}
return 0;
}
上面代码主要介绍了如何使用select函数来实现服务端的并发能力,当用户调用了select,则当前进程被阻塞,内核会检测select集合中的套接字描述符,如果有任何一个要监听的状态发生变化,则select会返回。
select的优势并不是对单个连接处理的更快,所以,连接数太少的化,性能可能比多线程阻塞IO更低,但是它的优点是处理很多连接。
select的优点:
1.可以监视多个文件描述符,减少了平均等待时间
2.处理大批量连接时,有效减轻进程调度的压力
select缺点:
1.监听文件描述符有上限,这个上限是由fd_set决定的。
2.它返回的只是就绪事件的个数,要判断是那个事件满足,需要遍历文件描述符。
3.select监听的集合是输入输出参数,每次监听都需要重新初始化。
4.每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
5.内核采用遍历fd集合的方式来检测就绪事件,这个开销在fd很多时也很大
6.select和poll都只能工作在低效的LT(水平触发)模式