下面是几种常见I/O模型的对比:
IO
一次文件的读取和输出,正常的文件读取(输出则反之)大致有以下几个步骤
- 磁盘 -> 2. 内核 空间-> 3. 用户空间
所以IO会有用户核和内核的切换
所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在"干活",而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。
- 最简单的IO - 阻塞IO
用户程序调用 内核函数 recvfrom 获取文件数据,但是如果recvfrom 数据没有准备好,用户程序就一直占用线程(阻塞) 等待recvfrom 完成数据复制工作,等数据完成返回继续后面的程序工作,由于阻塞整个流程即同步执行(会有线程上下文切换)
(
优点:贼鸡儿简单
缺点:1. 等待数据报准备时间消耗,2. 数据拷贝消耗
)
//doSomeThing()
//代码同步执行,等待recvfrom的返回值
File file = recvfrom();
//processIOContent();
这是一个经典的每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情
但是线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
- 优化版 IO-非阻塞IO
轮询机制(类似淘宝的二维码登录,前端一直轮询服务端扫码状态,如果成功则跳转显示登录成功),
用户调用recvfrom()函数轮询内核数据准备状态,如果没准备好,则等待下次请求,知道内存数据准备完毕用户调用recvfrom() 函数进行数据赋值,拷贝数据的时候还是阻塞的,只是等待数据报的过程变成了非阻塞
优点:大量减少了数据报准备的消耗
缺点:1. 等待数据报准备时间消耗优化没到极致,2. 数据拷贝消耗
- 超级升级版IO - 信号驱动IO
为了避免轮询查询内存数据报准备状态的消耗,产生了信号驱动IO,用户建立sigaction系统调用通知内核,内核收到sigaction 信号后立即返回,并异步准备数据,准备完成后提交 sigio信号通知用户,数据准备完成,
用户收到sigio信号,发起recvfrom系统调用,等待内核数据拷贝到用户控件,内核拷贝完成后返回成功,程序继续处理
优点:避免了阻塞,节省了数据准备的时间
缺点:1.数据拷贝时间消耗 2. 但是一个IO请求还是一个线程,导致数据浪费
- IO复用模型
用户发起select 系统调用,查询所有注册到该通道的数据是否有主句报准备好,如果有一个数据报准备好用户就发起recvfrom系统调用,等待内核复制数据报,数据报完成后通知用户数据拷贝完成,
(
优点:之前一个人管一个通道,现在,一个人管N个通道,有数据的时候就会发起系统调用节省了人力,对应到系统 即减少了线程的开支
缺点:1. 等待数据报还是阻塞,2. 数据报复制 还是阻塞
- 异步IO模型 - AIO (代理模式)
用户发起aio_read()请求后,给内核传递描述符、缓存区指针、缓存区大小等信息。告诉内核完成整个操作和如何通知他,然后就立刻去做其他事情了,内核收到aio_read() 后立即返回请求表示收到,然后自己将数据拷贝到用户指定的缓存区,完成后通知用户
优点: 1. 纯自动贼给力
缺点:难搞哦