阻塞IO和非阻塞IO,同步IO和异步IO
一、阻塞io
我这个进程调用了一个功能需要磁盘io,那么我整个进程就会被阻塞住,在做完磁盘io之前我都不能动。当内核把数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用 户线程,用户线程才解除block状态。
二、非阻塞io
我调用了一个磁盘io,但是我不用等他io完,我可以去进行别的东西,只要他io完就会通知我去读取数据;
三、同步IO
非阻塞io在进行磁盘io的时候,虽然不需要等磁盘io这个过程,但是当磁盘io完成之后,他还需要把数据从内核空间移动到用户空间,这个时间也是阻塞的,这就是同步.
四、异步IO
我调用了磁盘io,我会创建一个新的线程去处理这整个io,我只需要调用read,随后发生的什么时候,我创建了一个新的线程去帮我处理,等他处理完,我再回来接收已经准备好的数据.完全不用花费时间在等待,可以做别的事情。
这里粗略地举个栗子,比如你要玩英雄联盟。
阻塞IO就是,游戏登录进去之后你要先匹配队友(磁盘IO),匹配完队友后,你还要自己玩排位(把数据从内核复制到用户空间)。而非阻塞IO就是你下课回到宿舍,让你室友先帮你排队(磁盘IO),你先去干点别的事情,比如上厕所,等你干完别的时候回来,发现游戏已经进入了,你可以直接选用英雄开始打了。同步IO就是虽然你让你室友帮你匹配了队友,但是你还是得自己打游戏才能上分(把数据从内核复制到用户空间),这个时间段你也无法做别的事情。而异步IO就是,你请一个代练,你只需要告诉他,我要上王者100点,那么他就会把所有事情都做得明明白白(磁盘io并且把数据从内核复制到用户空间),你拿到手的时候就已经是一个段位为王者的号了。
五、Socket
1.文件标识符fd
每一个程序都有一个基础流,文件标识符0、1、2,输入流、输出流、错误流。如果这个进程监听了一个端口,那就会多一个3,是socket
2.当服务端监听了某个端口
只有监听了端口才会有3socket
3.当客户端与服务端链接时
服务端的文件标识符会变成4个,3是服务端监听了一个端口,就会创建这个socket,当客户端跟服务端链接的时候,就会多了一个socket,表示已经跟客户端建立了链接
4.看源码
nc localhost 8080 (建立连接的命令行)
read() 读文件标识符
accept() 当客户端想要跟你连接时,接收到一个fd,然后建立链接
六、IO的内存模型
时间片:当时间到了,调用进程调度,回调collBack,切换另一个进程
所以进程越多,对CPU的性能压力越大
切换的方式是利用晶振的规律震动,这里可以扩展到Redis的知识点,Redis是单线程, 采用单线程,避免了不必要的上下文切换.
七、IO的发展
1.阻塞模型
如果fd4在read()阻塞了,此时如果有另外一个fd5,即使建立了连接,但是因为scoket在fd4阻塞住了.
[缺点]:会发生阻塞
2.抛出线程,阻塞在线程
但是仍然有缺点,因为线程多了起来,线程的切换也需要消耗很大性能
3.非阻塞I/O(NIO)
只需要设置一下read,调用了read函数之后,不需要等待磁盘io,它会先返回一个结果,这样子不会阻塞,也不需要抛出线程.减少了线程的切换.
但是仍然还有缺点:因为即使进程不需要等到磁盘io,但是磁盘读取完数据之后,我们还是需要把数据从内核空间复制到用户空间,这样子也会消耗性能.(因为read是对系统调用的方法)并且,如果有一万个客户端发来信息,但是我们每次去调用read的时候,是需要遍历1万个fd,才能发现哪个fd有事件(event),这是一个时间复杂度特别高的事情.而且一万个fd里面可能只有几个fd有事件,这就造成了浪费.
4.多路复用
在内核上进行优化,因为我们之前每次都是要把fd传到内核空间,然后再调用read判断它有没有事件,所以我们包装了一个select(),假设有一万个fd,我们一次性把fd传到内核里面,内核把这一万个fd遍历一遍,然后再把有事件的fd返回出去.在这里read只需要调用有事件的fd次数,比如只有5个fd有事件发生,那么read只需要调用5次.
但是仍然还有缺点,因为每一次调用read,都要在内核空间里面循环一万个fd,查看有没有事件(select),而且每次都要把fds这个集合复制到内核空间,这样子也会浪费资源.
5.epoll
所以我们在内核空间划出一块空间,把整个fds集合放在内核空间里面,这样子每此调用read的时候就不用涉及到内核态和用户态的切换了.
客户端数据到达就会产生中断,然后查找一下这个终端号是那个fd的,然后返回给read就可以了
八、NIO
1.特点
①、是基于流的形式,但又采用了缓冲区和管道来处理数据的;
②、NIO是双向的;
③、NIO是使用多路复用的IO模型.
2.管道是什么?
通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象(通道)。一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。 Channel是一个对象,可以通过它读取和写入数据。
3.缓冲区(buffer)
4.selector
Selector运行单线程处理多个Channel