其他IO模型:
阻塞:一个接口如果无法完成功能,就会一直等待
非阻塞:一个接口如果无法完成功能,就会直接报错返回
阻塞与非阻塞:通常用于讨论接口特性,表示一个函数调用后是否会立即返回
同步:描述一个任务是由自己完成的
异步:描述一个任务操作是由其他人完成的
同步与异步:讨论一个操作完成的方式,是由自己完成的还是由别人完成的。
几种典型的I/O模型:
I/O操作的操作过程:1.等待i/o就绪 2.拷贝数据
- 阻塞IO:一个函数实现i/o操作 若IO未完成则一直等待,直到I/O完成后返回
优点:实现流程思想非常简单
缺点:阻塞浪费了CPU资源
2.非阻塞io:一个函数实现IO操作,若IO无法立即完成 则立即报错返回
通常需要循环操作
优点:CPU资源利用更加充分
缺点:需要循环进行,流程稍微复杂,不够实时。
阻塞和非阻塞的差别:一个函数若无法立即完成功能的情况下,是否会立即返回
3.信号驱动io:初始化IO 就绪信号,IO就绪时信号通知进程,进程发起IO操作
优点:相较于非阻塞更加实时,CPU利用更加充分
缺点:相较于非阻塞操作操作流程更加复杂
4.异步IO:我自己发起IO操作但是实际的IO过程由其他人完成
优点:对于资源的利用最为充分
缺点:流程的控制复杂度最高
同步和异步的差别:一个任务的完成方式,是有自己完成还是由其他人完成
5.多路复用/多路转接IO模型:
功能:针对大量描述符进行IO就绪事件监控,监控哪个描述符就绪了IO事件
单执行流tcp服务器:只能与一个客户端通信一次/只能与一个客户端循环通信
本质原因:因为无法获知那个描述符就绪了io事件,因此只能在固定的流程中发起多种不同的阻塞操作(accept,recv,send)
多路转接IO:是实现高并发服务器的关键技术。
io就绪事件分类:
可读事件:一个描述符当前有数据可以读(接收缓冲区数据量达到高水位标记)
可写事件:一个描述符当前写缓冲区有空闲空间(发送缓冲区空闲空间达到高水位标记)
异常事件:一个描述符当前存在异常
二、多路复用的具体实现:select,poll,epoll
select模型:
功能:针对大量描述符进行IO就绪事件监控。
函数接口:
Int select(int maxfd, fd_set*readset,fd_set*writeset,fd_set*excepts, struct timeval*timeout)
功能:针对大量描述符进行不同的io事件的监控
Maxfd:所有要监控的描述符集合中最大的描述符+1
原因:假设此时在内核中,我们要监控3号描述符,在内核中能监控的最大描述符的数量是有限的,默认是1024,但是如果我们将这1024全部遍历一遍效率就会很低。但是如果我们只需要将要监控最大描述符+1,那么后面的剩余的就不遍历,极大的提高的遍历效率。

Readset&writeset&exceptset:描述符集合,存放的要监控的描述符要对那个描述符监控什么事件,就把那个描述符添加到对应的集合中fd_set{_fds_bits\[\]} 实际上这个描述符集合,是一个位图数据结构,添加描述符,实际上就是将描述符这个数字对应的比特位置1,这个位图是通过数组实现的,意味着位图的大小是固定的,所能监控的描述符数量是有限的:取决于**_FD_SETSIZE宏大小,默认是1024**
在64位平台下 是8个字节
位图:一种数据结构,存放二进制比特位。


为了能够快速获知哪些数据块未使用,因此维护一个很大的数据:标记哪些使用了,哪些未使用
数据结构:位图(存放二进制比特位,每个比特位存放一个0/1)
一个文件存放到磁盘中的时候,必须对该文件进行描述描述:
Inode结构体:文件大小,文件权限,时间属性,存储位置.......
struct timeval{
size_t tv_sec;//存放秒
size_t tv_usec;//存放微秒
};
timeout参数:控制select接口的阻塞属性
若timeout中数据为0,则表示select非阻塞
若timeout中数据不为0,则表示阻塞多少秒
若timeout为空则表示一直阻塞
一个是数据0 也就是数值0 一个是NULL;
阻塞情况下,select会阻塞等待集合中有描述符就绪了所要监控的io事件
集合中有描述符就绪了事件 select才会返回
非阻塞情况下:集合中即使没有描述符就绪,也会立即返回
限时阻塞:在限定时间内 ,若集合中没有买哦舒服就绪,就超时返回。
select函数返回值:表示当前集合中所有集合中共有多少个描述符就绪
返回值<0表示出错;返回值==0表示没有描述符就绪(等超时了或者非阻塞情况)
描述符集合操作辅助宏:
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); 清空集合
select使用流程:
1.定义所要监控的事件集合
2.将要监控的描述符,根据所要监控的事件,添加到不同的集合中
3.根据需要,决定是否使用timeout设置监控超时时间
若需要设置监控超时时间,则定义struct timeval结构体变量
4.调用select接口开始进行监控
(1)将几个集合拷贝到内核中
(2)内核根据maxfd遍历集合,找出所要监控的描述符,添加监控
①遍历这些描述符,看看是否存在描述符已经就绪
②若没有就绪的描述符,则为描述符添加监控
③当有描述符就绪时,进程被唤醒
④遍历描述符集合,从集合中移除未就绪的描述符
(3)监控超时/有描述符就绪时接口调用返回(返回时集合中只有就绪描述符)
5.遍历所有的描述符,通过判断描述符是否还在集合中确定是否就绪了事件
6.若就绪了事件则根据事件进行不同的处理即可
代码实现:
代码实现1:
1.简单使用:
编写一个简单的样例,认识select的使用及细节
通过select对标准输入进行可读监控,当触发可读事件,进行读操作
cs
/*
编写一个简单的样例,认识select的使用及细节
通过select对标准输入进行可读监控,当触发可读事件,进行读操作
*/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/select.h>
#include<unistd.h>
#include<fcntl.h>
int main(){
//1.定义事件集合 并对集合进行初始化
fd_set rfds;//定义了一个变量
//定义变量中的数据我们并不知道可能会影响我们后面的操作
//所以就要对集合进行初始化 也就是清空集合
FD_ZERO(&rfds);//清空集合
//2.将描述符添加到集合中
FD_SET(0,&rfds);//添加描述符0的到集合中
int maxfd=0;//所以此时最大描述符
//4.开始监控
while(1){
//3.定义监控超时时间
//放到循环里面就是因为select每次都会清空
struct timeval tv;
tv.tv_sec=3;//设置3s监控超时时间
tv.tv_usec=0;
fd_set tmp_rfds=rfds;//每次创建临时集合
//这一步是因为
//如果我门要监控的描述符监控超时
//那么此时就意味着这个描述符就是未就绪的描述符
//select就会将这个我们本要监控的描述符移出去
//select(maxfd+1,rfds,wfds,efds,timeout)
int ret=select(maxfd+1,&tmp_rfds,NULL,NULL,&tv);
if(ret<0){
perror("select error");
continue;
}
if(ret==0){
printf("监控超时,没有描述符就绪\n");
continue;
}
//5.通过判断描述符是否在集合中,确定是否描述符就绪了事件
for(int i=0;i<=maxfd;i++){
if(FD_ISSET(0,&rfds)){
//6.若就绪了事件,根据事件进行处理
char buffer[1024]={0};
read(0,buffer,1023);
printf("获取可读数据:%s\n",buffer);
}
}
}
return 0;
}
使用注意事项:
1.select接口在调用返回时,修改timeout的值,因此每次监控需要重置需要超时时间
2.select接口咋返回时,会修改所监控的描述符集合(移除未就绪描述符),因此,每次监控前都需要重新添加描述符到集合中
代码2:基于select模型 实现一个但执行流高并发TCP服务器
伪代码:


cs
/*
基于select模型 实现一个但执行流高并发TCP服务器
*/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/select.h>
#include<unistd.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<time.h>
#define CHECK_RESULT(ret,msg)\
if((ret)<0){perror(msg);return -1};
void init_server_addr(struct sockaddr_in*addr,char*argv[]){
// ./sever 192.168.14.128 9000
const char*ip=argv[1];
int16_t port=atoi(argv[2]);
addr->sin_family=AF_INET;
addr->sin_port=htons(port);
addr->sin_addr.s_addr=inet_adrr(ip);
}
#define MAX_LISTEN 1024
int main(int argc,char*argv[]){
if(argc!=3){
printf("usage:./server ip port\n");
return -1;
}
//创建套接字bind(listen_fd,&addr,addrlen)
int listen_fd=socket(AF_INET,SOCK_STREAM,0);
CHECK_RESULT(listen_fd,"创建套接子失败");
//为套接字绑定地址信息
struct sockaddr_in addr;
init_server_addr(&addr,argv);
socklen_t addrlen=sizeof(struct sockaddr_in);
int ret=bind(listen_fd,&addr,addrlen);
CHECK_RESULT(ret,"绑定地址失败");
//开始监听
ret=listen(listen_fd,MAX_LISTEN);
CHECK_RESULT(ret,"监听失败");
//定义可读事件的描述符集合
fd_set rfds;
FD_ZERO(&rfds);
//将监听套接字,添加到集合中,并重置maxfd
FD_SET(listen_fd,&rfds);
int maxfd=listen_fd;
while(1){
//定义监控超时间结构
struct timeval timeout;
timeout.tv_sec=3;
timeout.tv_usec=0;
//定义临时集合,对临时集合开始监控
fd_set tmprfds=rfds;
ret=select(maxfd+1,&rfds,NULL,NULL,&timeout);
//若出错,或超时,则continue
if(ret<=0){
perror("监控出错或超时");
continue;
}
//遍历所有描述符,找到就绪的描述符
for(int i=0;i<=maxfd;i++){
if(FD_ISSET(i,&tmprfds)){
// 若就绪的描述符是监听套接字,则获取新连接
if(i==listen_fd){
//将获取到的通信套接字添加到可读事件描述符集合中
struct sockaddr_in peer;
int newfd=accept(i,&peer,&addrlen);
if(newfd<0){
perror("accept error");
continue;
}
FD_set(newfd,&rfds);//将新连接添加监控
maxfd=maxfd>newfd?maxfd:newfd;
}
else{
//若就绪的描述符不是监听套接字 则接收客户端通信数据 并进行响应
char buffer[1024]={0};
ret=recv(i,buffer,1023,0);
if(ret<=0){
perror("recv error");
close(i);
FD_CLR(i,&rfds);//移除描述符监控
for(int j=maxfd;j>=0;j--){
if(FD_ISSET(j,&rfds)){
maxfd=j;
break;
}
}
continue;
}
printf("客户端数据:%s\n",buffer);
}
}
//若就绪的描述符不是监听套接字 则接收客户端通信数据 并进行响应
}
}
//关闭套接字
close(listen_fd);
return 0;
}
优缺点总结:
优点:
1.监控超时阻塞时间,可以精确到微秒
2.select跨平台移植性好(不仅Linux下可以使用,Windows下包括其他平台也都支持)
缺点:
1.select所能监控的描述符数量是有限的,默认FD_SETSIZE=1024
2.select操作稍显麻烦:每次监控都需要重置超时时间,监控集合
3.select的监控性能偏低:内核进行监控的时候,会涉及多次描述符集合遍历
4.select监控完成之后需要程序员再次进行遍历描述符,确定描述符就绪事件
适用场景:
1.适用于监控描述符数量不多,甚至只有一个(仅用于进行超时控制)的场景,或者需要支持跨平台的场景。