IO 网络模型-阻塞IO

645 阅读3分钟

阻塞IO blocking IO

在Linux当中, 默认情况下所有的socket都是blocking, 一个典型的读操作过程

image.png

当用户进程调用了read这个系统调用, kernel就开始了IO的第一个阶段: 准备数据. 对于network io来说, 很多时候数据在一开始还没有到达(比如, 还没有收到一个完整的数据包), 这个时候kernel就要等待足够的数据到来. 而在用户进程这边, 整个进程会被阻塞. 当kernel返回结果, 用户进程才解除block的状态, 重新运行起来.

所以, blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block.

比如 listen(), send(), recv() 这些接口都是阻塞型的

image.png 简单的一问一答的Server/Client模型

大部分的socket接口都是阻塞型的, 所谓阻塞型接口是指系统调用(一般都是IO接口)不返回调用结果并让当前线程一直阻塞, 只有当该系统调用获得结果或者超时出错时才返回

实际上, 除非特定指定, 几乎所有IO接口(包括socket接口)都是阻塞型, 这其实挺麻烦, 因为在调用send()的同时, 线程将被阻塞, 在此期间, 线程无法执行任何运算或响应任何的网络请求

改进方案

一个比较简单的改进方案就是在服务端使用多线程或者多进程. 目的是让每个连接都拥有独立的线程或进程, 这样任何一个连接的阻塞都不会影响其他的连接

假设需要让服务器同时为多个客户提供一问一答的服务

image.png

主线程持续等待客户端的连接请求, 如果有连接, 则创建新线程, 并在新线程中提供为前面同样的问答服务

为什么一个socket可以accept多次?

设计socket的时候就是为多客户机做了考虑的, 让accept能够返回一个新的socket

accept接口:

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

s是从socket(), bind(), listen()中沿用下来的socket句柄值. 执行完bind()listen()后, 操作系统已经开始在指定的端口处监听所有的连接请求, 如果有请求, 则将该请求加入请求队列

调用accept()接口是从socket_s的请求队列抽取第一个连接信息, 创建一个与s同类的新的socket返回句柄, 如果请求队列当前没有请求, 则accept进入阻塞状态直到有请求进入队列

其实看似解决了多个客户机的要求, 其实没有解决, 如果要同时响应上千个连接, 无论你用多线程还是多进程都会严重占用系统资源

这种模式可以方便高效的解决小规模的需求, 但是面对大规模的请求会遇到性能瓶颈, 可以用非阻塞式接口来解决这个问题