目录
1.同步阻塞I/O模型:sync blocking I/O 简称BIO
2.非阻塞-同步I/O模型:nonblocking sync I/O
3.I/O多路复用(非阻塞)-同步I/O模型:I/O multiplexing
前言:
网络编程:
- 网络编程的基本模型是Client/Server模型,也就是两个进程间的相互通信,其中服务端提供位置信息(绑定的IP地址+端口),客户端通过连接操作向服务端监听的位置(服务端绑定的IP地址+端口)发起连接请求,通过三次握手建立连接,连接建立后,双方就可以通过网络套接字(socket)进行通信了。
- 传统的同步阻塞模型中,ServerSocket负责绑定ip地址和端口,Socket负责发起连接操作。连接建立后,双方通过输入和输出流进行同步阻塞式通信。
linux内核中文件的读写操作
- linux内核将所有的外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个文件描述符(file discriptor,简称fd),对socket的读写会返回一个socket 文件描述符(socketfd)。文件描述符是一个数字,它指向内核中的一个结构体,这个结构体包含了文件路径等信息。
- 默认情况下,所有的文件操作都是阻塞的。
- 一次⽹络I/O的读操作会包含下面这两个阶段:
- 等待数据准备就绪
- 数据从内核复制到用户空间(用户进程)
1.同步阻塞I/O模型:sync blocking I/O 简称BIO
BIO通信模型图
概念:
用户进程(中的I/O线程)从阻塞的socket中读取数据时,只有当数据包到达该socket的接收缓存区且数据被复制到服务器进程空间的缓存区中 或 发生错误时 才返回,在此期间用户进程(中处理请求的线程)会一直阻塞等待。
缺点:
- 同步IO的缺点:当I/O线程处理(客户端发送请求、服务器应答消息)的速度较慢 或 网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞。
- 阻塞IO的缺点:对于客户端的每一个请求,服务端都会去创建一个新的I/O线程来处理客户端的请求。当客户端的并发访问量比较高时,服务端需要创建大量的线程来响应请求,当线程的数量膨胀后,服务器的性能会急剧下降甚至宕机。
伪异步I/O通信模型
优点:
由于线程池和等待队列都是有界的,所以无论客户端并发连接数多大,都不会导致线程数量过于膨胀,从而可以避免服务器因线程膨胀而导致的奔溃。
缺点:
底层仍然采用的是同步IO模型,因此同步IO本身的缺点并没有解决。
2.非阻塞-同步I/O模型:nonblocking sync I/O
概念:
用户进程(中的I/O线程)从非阻塞的socket中读取数据时,若该套接字的接收缓存区中没有数据,则内核会直接返回一个错误。
缺点:
一般当socket设为非阻塞状态时,用户进程会轮询内核(eg:循环调用recvfrom函数),直到内核有数据返回为止,因此会导致大量cpu资源被占用。
3.I/O多路复用(非阻塞)-同步I/O模型:I/O multiplexing
概念:
I/O多路复用技术通过把多个IO的阻塞复用到同一个 [select | poll | epoll] 系统调用的阻塞上。
即:将BIO模式中 [n个IO同步阻塞调用] 变为 [n个请求阻塞在一个select 调用上,当要访问的资源准备好后,select才发起同步IO操作(每次执行IO的线程可以是同一个线程,故IO线程可以复用多次),因为资源已经准备好了,所以发起的同步IO操作并不会被阻塞]。
这样一来,就不存在阻塞的IO调用了,也就不用为每个请求专门创建一个线程来执行阻塞的IO调用了。所以I/O多路复用模型下,I/O是非阻塞的。
I/O多路复用通信模型图:
NIO服务端通信序列图:
NIO客户端通信序列图:
服务端进程(中处理请求的线程)将每个客户端请求要访问的fd传递给selector,selector触发 [select | poll | epoll] 系统调用,服务端进程(中处理请求的线程)阻塞在selector上。
selector ([select | poll | epoll]系统调用) 可以帮我们侦测多个fd是否就绪。当fd就绪后,服务端进程(中处理请求的线程)将I/O操作任务提交到一个专门用于处理IO请求到线程池中。
I/O多路复用可以看作是Reactor线程模型的一种具体实现。
触发方式:
- 水平触发:若就绪的fd未被用户进程处理,则该fd在下一次查询时依旧会返回。
- 边缘触发:无论就绪的fd是否被用户进程处理,该fd在下一次查询时将不再返回。
优点:
与传统的多线程/多进程模型相比,I/O多路复用的最大优势是系统开销小,系统不需要创建额外的线程,也不需要管理这些线程的运行。
适用场景:
- 服务器需要同时处理多个处于监听状态或多个连接状态的套接字。
举例:
java中NIO
selector类型:
select调用:
- 概念:多路复用器(selector)由select函数实现。
- 监听机制:
- 轮询注册的fd,并根据fd的状态做相应的处理:
- 读就绪状态 -> 读数据 并 删除该读就绪事件
- 写就绪状态 -> 写数据 并 删除该写就绪事件
- 接收(accept)就绪状态 -> 注册新的读就绪事件 并 删除该接收就绪事件
- ...
- 获取fd的状态:内核把所有监听的fd的信息整体复制到用户空间。
- 轮询注册的fd,并根据fd的状态做相应的处理:
- 触发方式:水平触发。
- 优点:与非阻塞式I/O模式相比,select调用不需要客户端不断地发出请求。
- 缺点:单个进程可以打开的fd数量有限,默认1024个,如果要修改这个默认值,需要重新编译内核。每次select调用都会线性地扫描所有监听的fd(即:无论fd是否就绪,select都会去检查它的状态),当监听的fd数量比较多时,I/O效率呈线性下降。
poll调用:
- 概念:多路复用器(selector)由poll函数实现。
- 监听机制:同select调用。
- 触发方式:水平触发。
- 优点: 单个进程可以打开的fd数量不受限制。
- 缺点:线性地扫描所有监听的fd,当监听的fd数量比较多时,I/O效率呈线性下降。
epoll调用:
-
概念:selector由一系列epoll_函数实现。
-
监听机制:
-
当fd就绪时,fd会立即回调rollback函数,而那些没有就绪的fd则不会回调rollback函数。
-
获取fd的状态:使用内存映射,不需要把fd的信息从内核复制到用户空间。
-
- 触发方式:默认是水平触发,支持边缘触发。
- 优点:
- 单个进程可以打开的fd数量不受限制。(仅受限于操作系统的最大文件句柄数,1g内存的最大文件句柄数为10w左右)
- 由于epoll采用的是回调函数的方式,而不是线性扫描的方式,故I/O效率不会随着fd数量的增加而线性下降。
- 使用内存映射,避免了内存复制的开销,加速了内核与用户空间的消息传递。
- 缺点:在连接数少并且连接都十分活跃的情况下,epoll的性能可能比select和poll的性能差,毕竟epoll的通知机制需要很多函数回调。
4.信号驱动I/O模型:SIGIO
- 概念:开启套接字信号驱动IO功能,并通过sigaction系统调用执行一个信号处理函数(此系统调用立即返回,用户进程继续工作,故该系统调用是非阻塞的),当数据准备好时,内核就为该进程产生一个SIGIO信号,用户进程收到这个信号后,就可以开始进行I/O操作了。
5.异步I/O模型:POSIX定义的异步IO函数
- 概念:调用异步IO函数,让内核在整个I/O操作(包括将数据从内核复制到用户自己的缓冲区)完成后通知用户进程。
- 信号驱动I/O和异步I/O的区别:
- 信号驱动I/O:内核通知用户进程何时可以开始一个I/O操作,此时I/O操作还未执行。
- 异步I/O:内核通知用户进程一个I/O操作在何时已经操作完成。
- 举例:java中的AIO