阻塞IO blocking IO
在Linux当中, 默认情况下所有的socket
都是blocking
, 一个典型的读操作过程
当用户进程调用了read
这个系统调用, kernel
就开始了IO
的第一个阶段: 准备数据. 对于network io
来说, 很多时候数据在一开始还没有到达(比如, 还没有收到一个完整的数据包), 这个时候kernel
就要等待足够的数据到来. 而在用户进程这边, 整个进程会被阻塞. 当kernel
返回结果, 用户进程才解除block
的状态, 重新运行起来.
所以, blocking IO
的特点就是在IO
执行的两个阶段(等待数据和拷贝数据两个阶段)都被block
.
比如 listen()
, send()
, recv()
这些接口都是阻塞型的
简单的一问一答的Server/Client模型
大部分的socket
接口都是阻塞型的, 所谓阻塞型接口是指系统调用(一般都是IO接口)不返回调用结果并让当前线程一直阻塞, 只有当该系统调用获得结果或者超时出错时才返回
实际上, 除非特定指定, 几乎所有IO接口(包括socket
接口)都是阻塞型, 这其实挺麻烦, 因为在调用send()
的同时, 线程将被阻塞, 在此期间, 线程无法执行任何运算或响应任何的网络请求
改进方案
一个比较简单的改进方案就是在服务端使用多线程或者多进程. 目的是让每个连接都拥有独立的线程或进程, 这样任何一个连接的阻塞都不会影响其他的连接
假设需要让服务器同时为多个客户提供一问一答的服务
主线程持续等待客户端的连接请求, 如果有连接, 则创建新线程, 并在新线程中提供为前面同样的问答服务
为什么一个
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
进入阻塞状态直到有请求进入队列
其实看似解决了多个客户机的要求, 其实没有解决, 如果要同时响应上千个连接, 无论你用多线程还是多进程都会严重占用系统资源
这种模式可以方便高效的解决小规模的需求, 但是面对大规模的请求会遇到性能瓶颈, 可以用非阻塞式接口来解决这个问题