在现代的 *** 作系统对于存储空间都有一套访问限制控制,所以将存储空间分成了用户空间和内核空间。
用户空间是给应用程序使用,应用程序可以访问用户空间的数据,但是不可以访问内核空间中的数据;
内核空间可以访问计算机的所有存储空间,包括用户空间、内核空间以及硬件设备上的数据。
所以当应用程序需要访问硬件设备上的数据或者内核空间的数据时,就必须通过内核空间的程序来实现。
IO模型交互图:
应用程序需要从网卡中读取数据为例的IO交互流程:
- 应用程序调用内核提供的函数发起请求(请求内核函数)
- 内核访问网卡空间获取数据(内存获取数据)
- 内核将获取到的数据复制到用户空间(内核复制数据)
- 应用程序从用户空间中获取需要的数据(应用程序获取数据)
学习IO模型,首先要理解四种模型的分类:
同步IO
应用程序调用内核函数到最终的应用程序从用户空间中获取数据的整个流程是需要用户线程一次性完成的。
异步IO
应用程序调用内核函数请求获取数据和最终从用户空间中拿到数据不是一次性完成的,而是先请求数据,等数据全部准备好了以后再去获取。
阻塞IO
应用程序调用内核函数请求数据,如果此时还没有数据,那么应用程序就一直等待着,直到成功拿到数据为止,此时应用程序是一直等待状态的。
非阻塞IO
应用程序调用内核函数请求数据,如果此时没有数据,那么应用程序就不等待先去处理其他的事情,过一会再重新尝试请求,知道成功拿到数据为止,此时应用程序不会一直处于等待状态。
*** 作系统中有五种IO模型:
*** 作系统的IO模型分为同步IO和异步IO两类,同步IO中又分为阻塞IO和非阻塞IO。
*** 作系统给应用程序提供了recv函数,该函数用于从socket套接字中接收数据,默认情况下会等到网络数据接收完成并复制到用户空间之后才返回结果或者失败之后返回结果,可以通过flags参数设置如果没有数据的话立即返回结果
总的来说,IO *** 作有两个步骤:
等待数据接收到内核空间中。
将数据从内核复制到用户空间中。
- 同步阻塞IO
应用程序调用 *** 作系统的recv函数,recv函数默认会等待数据接收完成并复制到用户空间之后返回结果,而如果数据没有准备好的话,那么应用程序就一直处于等待状态,直到有数据返回,此时应用程序的线程处于阻塞状态,无法执行其他 *** 作
- 同步非阻塞IO
应用程序调用 *** 作系统的recv函数,recv函数设置flags值为立即返回,那么如果内核发现没有数据时就立即返回,应用程序得到结果之后不再等待,而是先处理其他业务,然后轮训不断尝试获取数据,直到数据成功返回,此时应用程序不处于阻塞状态,可以先处理其他 *** 作
- 同步多路复用IO
应用程序先调用 *** 作系统的select函数或者poll函数或者epoll函数,这几个函数的作用是监听网络套接字上的数据状态,如果有数据可读,那么就通知应用程序,此时应用程序再调用recv函数来读取数据,此时肯定是可以读取到数据的。可以发现多路复用IO的特点是不需要尝试获取数据,而是先开启另外一个线程来监控数据的状态,等到有数据的时候再同步获取数据,而在没数据的时候也是不需要等待的。多路复用IO调用select函数之后也会阻塞进程,但是不会真正的IO *** 作线程没有被阻塞,所以实质上是同步非阻塞IO
- 同步信号量驱动IO
通过调用sigaction函数注册信号函数,等内核数据准备好了之后会执行信号函数通知应用程序,应用程序此时再调用recv函数同步的获取数据。信号驱动IO和异步IO有点类似,都是异步通知,不同的是信号驱动IO的真正读取数据的 *** 作还是同步 *** 作的。
- 异步非阻塞IO
通过调用aio_read函数,那么内核会先将数据读取好,并且复制到用户空间之后,再执行回调函数通知应用程序,此时应用程序就可以直接从用户空间中读取数据,而不需要再从内核中读取数据了。
总结:
IO *** 作主要是分为两步。
阻塞IO、非阻塞IO、多路复用IO、信号量IO是在等待数据的阶段上不同导致他们分为阻塞和非阻塞IO,而在将数据从内核空间复制到用户空间这一步都是一样的,所以他们都是同步IO。
异步IO在第一二阶段都是内核主动完成的。
三、Java中的IO模型Java中存在三种IO模型:
- BIO
BIO即为阻塞IO。这里不多讲了。普通的Socket编程就是BIO。
- NIO
NIO就是多路复用在Java中的实现。
NIO的实现主要涉及三大核心内容:
Channel
Channel和IO中的Stream类似,只不过Stream是单向的,而Channel是双向的,既可以用来进行读 *** 作,也可以进行写 *** 作。
Buffer
Buffer实际上就是一个容器,其内部通过一个连续的字节数组存储IO上的数据。在NIO中,Channel在文件、网络上对数据的读取或写入都必须经过Buffer。
客户端在向服务端发送数据时,必须先将数据写入Buffer中,然后将Buffer中的数据写到服务端对应的Channel上。服务端在接受数据时必须通过Channel将数据读入Buffer中。
Selector
Selector用于检测在多个注册的Channel上是否有IO事件发生,并对检测到的IO事件进行相应的响应和处理。因此通过一个Selector线程就可以实现对多个Channel的管理,不必为每一个连接都创建一个线程,避免线程资源的浪费和多线程之间的上下文切换导致的开销。同时,Selector只有在Channel上有读写事件发生时,才会调用IO函数进行读写 *** 作,可极大减少系统开销,提高系统的并发量。
NIO的机制:
NIO通过Selector监听Channel上的事件的变化,在Channel上有数据变化时通知线程进行读写 *** 作,对于读请求而言,在通道上有可用数据时,线程进行Buffer的读 *** 作,在没有数据时,线程可以执行其他业务逻辑 *** 作。对于写 *** 作而言,在使用一个线程执行写 *** 作将一些数据写入到某个通道时,只需将Channel上的数据异步写入Buffer即可,Buffer上的数据会被异步写入目标Channel上,用户线程不需要等待整个数据完全被写入目标Channel就可以继续执行其他业务逻辑。
这个图是BIO与NIO的实现图。
- AIO
即为异步IO的Java实现,这里也不做介绍了。
总结一下从 *** 作系统到Java的实现IO模型,整个IO *** 作主要分为两步骤,等待数据,将数据拿到使用空间, *** 作系统就是从内核拿到用户空间,java就是线程开始进行读写 *** 作,其实也是拿到用户线程的 *** 作。
在这两个 *** 作中,第一步等待数据,如果线程可以做其他的业务逻辑 *** 作,那么这个就是非阻塞IO--多路复用的NIO,如果只能阻塞在这里等待数据,那么这个就是阻塞IO--BIO。如果在第二步,线程在进行读写 *** 作也就是拿到用户空间来进行 *** 作的时候是阻塞的,那么这个IO就是同步IO,如果是可以直接被动拿到结果,那么就是异步IO--AIO。
Java NIO的实现靠的就是 Selector、Channel、Buffer。
这里就是有一个点比较乱,Java中两个步骤与 *** 作系统的两个步骤的匹配。
*** 作系统中的等待数据其实就是Java中通过socket的一个通信的过程,其实也是在等待数据。
*** 作系统中的将内核数据复制到用户空间在Java中其实就是线程进行的读写 *** 作。
上面这一段话是错误的,Java的IO *** 作其实只是在用户态时进行的 *** 作,所以说 *** 作系统的IO模型与Java的IO模型不是一回事。
OK,IO模型就是讲到这里,感谢大家的观看。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)