IO模型
操作系统级网络编程IO模型:


发展历程与请求的过程如下:
1.服务器端调用socket(),bind(),listen()方法生成套接字,在注册ip和port之前进行了网络字节序的转化,再之后listen()函数才真正开始生成文件描述符和准备好队列,然后开始监听客户端的请求,然后有新的请求连接过来,如果线程空闲,那么就处理这个请求,处理请求的时候虽然没有显示的调用socket()生成新的套接字,但是accept()自动隐式生成了,并且套接字的参数为(源ip,目的ip,源端口,目的端口),因此就算很多服务都是请求的80端口,那么生成新的端口的时候,因为客户端不同,就算源端口都是80,那么也不会冲突。如果如果请求过多,那么就会放入到队列,但是每次只能服务一个请求,其他请求都排队,造成阻塞,由此出现多线程,每个请求服务上来都会开一个新的线程去处理,(阻塞IO)但是在操作系统中线程也是非常宝贵的资源,频繁创建和销毁线程,加上线程上下文切换,造成资源浪费,因此出现伪异步IO,即用线程池来解决。但是也要注意到,这也没有从根本上解决问题,因为还是会阻塞两个阶段。
2.BIO因为每次是阻塞的,那么是哪阻塞呢?由图中可以看出调用完read,进程会一直等待内核完成IO操作,那么内核IO操作做了什么呢?
1.内核等待物理设备上的数据
2.内核将内核缓冲区的数据写到用户缓冲区
由此可以看出来,应用程序在内核完成IO的两个阶段都阻塞了,那么就要想办法优化一下;针对第一阶段的优化,先出现了非阻塞IO,就是说第一个阶段,等待内核准备好数据的阶段不再阻塞,而是采取轮询的方式去访问,如果内核没有准备好数据,那么就返回一个错误码,进程得到错误码就知道没有数据准备好,那么本身进程还可以去做别的事情,不必一直等待,但是这个非阻塞IO模型的利用率不是很高,因为它要频繁的切换上下文。
3.因此出现了IO复用模型,操作系统提供了select(),poll(),epoll(),来解决前面两种方式带来的问题。select函数提供了一个底层为数组的数据结构记录socket,即将需要它监视的文件描述符(也就是socket文件),放入数组,然后每次去循环遍历这个数组有标记的事件发生。如果有就进行处理,但是select函数也有很大的问题,首先它需要每次去遍历文件描述符有没有发生变化,因为会发生变化, 所以之间会记录旧的文件描述符,并且每次调用select函数都会将新的数组传进去,也就是传到操作系统,那么我们为什么要把这个数组传给操作系统呢?因为这个数组中记录的是socket,用户态无法进行操作,只能内核才能进行操作。
select:利用select函数可以同时监视多个文件描述符[套接字],
首先要把需要监视的文件描述符集中到一起

详解:int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
int fds[] = 存放需要监听的socket
while(1){
int n = select(..., fds, ...)
for(int i=0; i < fds.count; i++){
if(FD_ISSET(fds[i], ...)){
//fds[i]的数据处理
}
}
}
通俗的说,就是有一个数组将所有需要监视的socket保存起来,然后在调用select的时候,将这个数组传进去,而如何查看这个socket数组中哪个socket对象是否可以读取了呢,比如取fd_set长度为1字节,那么就有8个文件描述符,此时状态为00000000,然后执行FD_SET(5,set),变为00010000,然后再执行FD_SET(1,set),FD_SET(2,set),就变为00010011,然后若是执行select(6,set,0,0)阻塞等待,然后fd1,fd2发生可读事件,则select返回,此时set变为00000011,因为没有发生事件的fd=5被清空
缺点:
调用select函数后针对所有文件描述符的循环语句
每次调用select函数时都需要向该函数传递监视对象信息(更耗性能)
为什么要把监视对象信息传递给操作系统呢?
因为select函数与文件描述符有关,更准确的说,是监视套接字变化的函数,而套接字是由操作系统管理的,所以select函数需要借助操作系统才能完成功能。

尤其是第二个问题,无法通过代码优化,所以先后出现了poll,epoll
epoll
不需要循环语句
也不需要每次传递监视对象信息
epoll_create:创建保存epoll文件描述符的空间
epoll_ctl:向空间注册并注销文件描述符
epoll_wait:与select函数类似,等待文件描述符发生变化
select与epoll方式对比:
1.select方式中为了保存监视对象文件描述符直接声明了fd_set变量。但是epoll方式直接由操作系统负责监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间。
2.在select中添加和删除监视对象文件描述符是通过fd_set函数,在epoll中是通过epoll_ctl函数请求操作系统来完成
3.select方式调用select函数等待文件描述符发生变化,而epoll中调用epoll_wait函数
4.select方式通过fd_set变量查看监视对象的状态变化,而epoll方式通过epoll_event将发生变化的文件描述符集中到一起。所以不需要循环
epoll_create函数创建的资源由操作系统管理
epoll条件触发和边缘触发区别在于发生事件的时间点
条件触发方式中,只要输入缓冲有数据就会一直通知该事件。
边缘触发中输入缓冲收到数据时仅注册1次该事件
4.另外还有信号驱动型IO与AIO,区别为:
信号驱动型得到信号的时候是代表:我可以读了
AIO得到信号的时候是代表:已经读完了
ps:注意到除了AIO之外所有的IO模型都是同步的,因为在真正数据传输的过程中,都是同步进行的。

浙公网安备 33010602011771号