括号内都是个人理解
使用环境:一个进程同时处理多路输入输出流时,比如 搭建服务器
特点:让每一个输入输出流及时得到响应,使cpu不在一个输入输出流处阻塞
个人理解:让输入输出流“排好队”等着cpu处理,而不是让cpu等他们。
构建I/O多路复用的基本思路
1.先构造一张有关文件描述符的表(集合,数组)(类似银行大厅)
2.将需要用的文件描述符添加到这个表中;(先坐着歇一歇)
3.调用相应的函数(select、poll、epoll)阻塞
4.当这些文件描述符中有一个准备好I/O操作(排好队)了,函数就返回(停止阻塞)
5.判断是哪一个或哪些文件描述符产生了I/O操作(要办的事)
6.CPU做出对应的逻辑处理(办事,办事有时候还需要“本人”配合)
实现IO多路复用的方式
1.select实现
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:select用于监测是哪个或哪些文件描述符产生事件;
有一个或多个同时产生时间返回值。
参数:nfds: 监测的最大文件描述个数
(这里是个数,使用的时候注意,与文件中最后一次打开的文件
描述符所对应的值的关系是什么?)
readfds: 读事件集合; //读(用的多)
writefds: 写事件集合; //NULL表示不关心
exceptfds:异常事件集合;
timeout:超时检测 1
如果不做超时检测:传 NULL
select返回值: <0 出错
>0 表示有事件产生;
如果设置了超时检测时间:&tv
select返回值:
<0 出错
>0 表示有事件产生;
==0 表示超时时间已到;
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
void FD_CLR(int fd, fd_set *set);//将fd从表中清除
int FD_ISSET(int fd, fd_set *set);//判断fd是否在表中
void FD_SET(int fd, fd_set *set);//将fd添加到表中
void FD_ZERO(fd_set *set);//清空表
实例(核心代码)简易服务器
//I/O并发准备工作
fd_set sockfds, tempfds;
FD_ZERO(&sockfds);
FD_SET(sockfd, &sockfds); //把文件描述符放进表里
int maxfd = sockfd; //定义最大的描述符用来计数(做标志)
char buf[128];
int ret, i, recvbyte,maxfdtemp;
while (1)
{
tempfds = sockfds;
//阻塞等待描述符事件
ret = select(maxfd + 1, &tempfds, NULL, NULL, NULL);
if (ret < 0) //日常一判
{
perror("select err");
return -1;
}
maxfdtemp = maxfd; //临时用来便利的,防止后面改maxfd出错(我不知道有没有出错的可能)
for (i = 0; i <= maxfdtemp; i++) //i代表描述符
{
if (FD_ISSET(i, &tempfds)) //判断描述符是否在表内,在就进来
{
if (i == sockfd) //判断是不是等待链接的描述符
{
//建立与客户端的链接,接收文件描述符与客户端ip信息
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0) //日常一判
{
perror("accept err");
return -1;
}
FD_SET(acceptfd, &sockfds); //把新的描述符放进表里
if (acceptfd > maxfd) //如果它是最大的描述符,那就把它跟maxfd换一换
maxfd = acceptfd;
printf("用户 %d 已登录:ip:%s ,port:%d\n",
acceptfd, inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port)); //将客户端信息打印
}
else//只剩下新建的客户端的链接了,没写发数据的代码
{
recvbyte = recv(i, buf, sizeof(buf), 0); //接收客户端描述符i的信息,recvbyte用来判断描述符状态
if (recvbyte < 0)//日常一判
{
perror("recv err");
return -1;
}
else if (recvbyte == 0) //退出
{
printf("用户%d 已退出\n", i);
FD_CLR(i, &sockfds); //先把它从表里删除
close(i); //再关闭描述符
if (i == maxfd) //如果它是最大的,那么就把maxfd这个标志给第二大的
{
for (int j = i; j > 0; j--)
if ((FD_ISSET(j, &sockfds))) //从j往下一个一个遍历,如果在表里就给它
maxfd = j;
}
}
else
{
printf("用户%d: %s \n", i, buf); //输出客户端发的信息
}
}
}
}
}
总结select实现IO多路复用特点
- 一个进程最多只能监听1024个文件描述符 (千级别)(在不改底层的情况下)
- select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源);
- select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低(一个进程04G,03G是用户态,3G~4G是内核态,拷贝是非常耗时的);