IO模型
服务器编程中常用的4种IO模型:
- 同步阻塞IO(Blocking IO)
- 同步非阻塞IO(non-blocking IO)
- IO多路复用(IO Mutiplexing)
- 异步IO(Asynchronous IO)
同步和异步的概念描述的是用户线程与内核的交互方式:
- 同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;
- 异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:
- 阻塞是指IO操作需要彻底完成后才返回到用户空间;
- 非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
同步阻塞IO
同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时被阻塞。
如图,用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。
用户线程使用同步阻塞IO模型的伪代码描述为:
read(socket, buffer);
process(buffer);
即用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。
优点:
- 最快返回数据,无延迟
- 简单易实现
缺点:
- CPU利用率极低
用户空间与内核空间
- 对于32位操作系统,它的虚存为4G,
- 为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚存划分为两部分,一部分为内核空间,一部分为用户空间。
- 对于linux系统,0~3G地址为用户空间,3G-4G地址为内核空间。
- IO的读写都是系统调用,因此当我们read一个socket时,是将数据读到内核空间的buffer,然后再将buffer拷贝到用户空间(copy_to_user)
同步非阻塞IO
同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK。这样做用户线程可以在发起IO请求后可以立即返回。
如图所示,由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。
用户线程使用同步非阻塞IO模型的伪代码描述为:
while(read(socket, buffer) != SUCCESS)
;
process(buffer);
即用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。
优点:
- 比起同步阻塞,可以在等待任务的时间内做其他事(也就是“后台”可以有多个任务同时进行)
缺点:
- 任务完成的响应延迟增大
- 不断轮询造成CPU利用率低
IO多路复用
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的。
如图所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
用户线程使用select函数的伪代码描述为:
select(socket);
while(1)
{
sockets = select();
for(socket in sockets)
{
if(can_read(socket)) {
read(socket, buffer);
process(buffer);
}
}
优点:
- 用户可以在一个线程内同时处理多个socket的IO请求,比起多线程的同步阻塞模型,减少了线程切换的开销。
缺点:
- 增加了select函数的开销,对单个socket连接没有优势
多线程的成本
- 更复杂的设计:
- 多线程直接的交互
- 共享内存
- 线程同步的错误难以检测,复现和修复
- 上下文切换开销
- 本地数据,程序指针等
- 加重资源消耗
- 线程本地堆栈
异步IO
在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。
参考网站:
https://github.com/rainzhaojy/blogs/issues
http://www.cnblogs.com/fanzhidongyzby/p/4098546.html
https://www.jianshu.com/p/486b0965c296