IO-操作系统
Posted 爱吃草的羊驼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IO-操作系统相关的知识,希望对你有一定的参考价值。
用户态和内核态
现代操作系统,为了保护系统的安全,都会划分出内核空间和用户空间,或者我们经常说的内核态和用户态。简单来说,就是划分为内核态和用户态两个等 级,运行在用户态的进程大都是一些应用程序,能够访问的系统资源受到极大的限制。而运行在内核态的进程权限就非常大,可以"为所欲为”。 这么做的目的是为了保护操作系统的底层资源,例如文件都要存在硬盘,但是如果用户编写的应用程序可以随意的操作硬盘的启动扇区,那就很容易把系统搞崩溃,分为内核态和用户态之后,用户态的应用程序就不能直接操作底层的硬件接口了,如果需要操作硬盘,比如存文件,那就必须经过内核态来协调。这样就可以对所有底层硬件的操作方式进行规范。 有了用户态和内核态的划分后,应用程序就经常需要在用户态和内核态之间进行切换。例如程序要保存一个文件到硬盘,在程序执行的用户态,是不能直接操作磁盘的。只有切换到内核态才能真正去操作磁盘。
IO
IO,英文全称是 Input/Output,翻译过来就是输入/输出 。我们听得挺多,就是磁盘 IO,网络 IO 等。 IO 即输入/输出,到底谁是输入?谁是输出?IO 如果脱离了主体,会让人疑惑。
计算机角度的 IO
我们常说的输入输出,比较直观的意思就是计算机的输入输出 , 计算机就是主体 。 计算机分成分为 5 个部分:运算器、控制器、存储器、输入设备、输出设备。
输入设备是向计算机输入数据和信息的设备,键盘,鼠标都属于输入设备;输出设备是计算机硬件系统的终端设备,用于接收计算机数据的输出显示,一般显示器、打印机属于输出设备。
操作系统角度的 IO
我们要将内存中的数据写入到磁盘的话,那么主体就是一个程序. 操作系统 负责计算机的资源管理和进程的调度,我们电脑上跑着的应用程序,其实是需要经过操作系统 ,才能做一些特殊操作,如 磁盘文件读写、内存的读写 等等。 真正的 IO 是在操作系统 执行的。即应用程序的 IO 操作分为两种动作: IO 调用 和 IO 执行 。IO 调用是由进程(应用程序的运行态)发起,而 IO 执行是 操作系 统内核 的工作。 应用程序发起的一次 IO 操作包含两个阶段: IO 调用:应用程序进程向操作系统内核发起调用。 IO 执行:操作系统内核完成 IO 操作。
IO 模型
阻塞 IO
假设应用程序的进程发起 IO 调用 ,但是如果 内核的数据还没准备好 的话,那应用程序进程就一直在阻塞等待 ,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次 IO 操作,称之为阻塞 IO 。 阻塞 IO 比较经典的应用就是阻塞 socket、Java BIO 。阻塞 IO 的缺点就是:如果内核数据一直没准备好,那用户进程将一直阻塞,浪费性能 ,可以使用 非阻塞IO 优化。
非阻塞 IO
如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求。这就是非阻塞 IO。
- 应用进程向操作系统内核,发起 recvfrom( )读取数据。
- 操作系统内核数据没有准备好,立即返回 EWOULDBLOCK 错误码。
- 应用程序进程轮询调用,继续向操作系统内核发起 recvfrom 读取数据。
- 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。
- 完成调用,返回成功提示。
IO 多路复用
概述
既然 NIO 无效的轮询会导致 CPU 资源消耗,我们等到内核数据准备好了,主动通知应用进程再去进行系统调用。 IO 复用模型核心思路:系统给我们提供一类函数(如 select、poll、epoll),它们可以同时监控多个 fd 的操作,任何一个返回内核数据就绪,应用进程再发起 recvfrom 系统调用。 文件描述符 fd(File Descriptor),它是计算机科学中的一个术语,形式上是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。IO 多路复用之 select
应用进程通过调用 select 函数,可以同时监控多个 fd,在 select 函数监控的 fd中,只要有任何一个数据状态准备就绪了,select 函数就会返回可读状态,这时应用进程再发起 recvfrom( )请求去读取数据。
IO 多路复用之 epoll
为了解决 select/poll 存在的问题,多路复用模型 epoll 诞生,它采用事件驱动来实现,流程图如下:

IO 模型之信号驱动模型
信号驱动不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号,然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用 recvfrom,去读取数据。
还有没有优化方案呢?AIO(真正的异步 IO)!
异步 IO(AIO asynchronous IO)
前面讲的 BIO,NIO 和信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞 的,因此都不算是真正的异步。AIO 实现了 IO 全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是 表示提交成功类似的意思 。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程 IO 操作执行完毕。 流程如下:
异步 IO 的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。日常开发中,有类似思想的业务场景: 比如发起一笔批量转账,但是批量转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。 阻塞、非阻塞、同步、异步 IO 划分:

阻塞 IO(Blocking I/O BIO)
通常在进行同步 I/O 操作时,如果读取数据,代码会阻塞直至有可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。 传统的 Server/Client 模式服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。 这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来了新的问题,如果线程池中有 100 个线程,而有 100 个用户都在进行大文件下载,会导致第 101 个用户的请求无法及时处理,即便第 101 个用户只想请求一个几 KB 大小的页面。传统的 Server/Client 模式如下图所示:
非阻塞( non-blocking IO NIO)
核心思想 NIO 中非阻塞 I/O 调用不会被阻塞,核心是注册感兴趣的特定 I/O 事件,如可读数据到达,新的套接字连接等等,在发生特定事件时,系统再通知我们。 NIO 中实现非阻塞 I/O 的核心对象就是 Selector. Selector 就是注册各种 I/O 事件地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:
NIO
Java NIO 核心部分组成 Channels Buffers Selectors 虽然 Java NIO 中除此之外还有很多类和组件,但 Channel,Buffer 和Selector 构成了核心的 API。其它组件,如 Pipe 和 FileLock,只不过是与三个核心组件共同使用的工具类。Channel
Channel,可以翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream,OutputStream. 而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。因为Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。 Channel 是一个对象,可以通过它读取和写入数据。所有数据都通过 Buffer对象来处理。你永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。 NIO 中的 Channel 的主要实现有: FileChannel:从文件中读写数据 DatagramChannel:通过 UDP 读写网络中的数据 SocketChannel:通过 TCP 读写网络中的数据 ServerSocketChannel:可以监听新进来的 TCP 连接
Buffer
Java NIO 中的 Buffer 用于和 NIO 通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。 NIO 中的关键的 Buffer 实现有: ByteBuffer CharBuffer ShortBuffer IntBuffer LongBuffer FloatBuffer DoubleBuffer 对数据的读取/写入需要使用 buffer,buffer 本质就是一个数组 常用方法: ByteBuffer.allocate(1024); 创建字节数据 byteBuffer.flip(); 翻转这个缓冲区,读操作前使用 byteBuffer.clear(); 清除缓存,写操作前使用 一个基本的 NIO 案例FileInputStream in = new FileInputStream("E:/source.txt");
FileOutputStream out = new FileOutputStream("E:/dest.txt");
FileChannel inchannel = in.getChannel();
FileChannel outchannel = out.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(inchannel.read(byteBuffer)!=-1)
byteBuffer.flip();
outchannel.write(byteBuffer);
byteBuffer.clear();
Selector
Selector 一般称为选择器。它是 Java NIO 核心组件中的一个,用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个 channels,也就是可以管理多个网络链接。 使用 Selector 的好处在于:使用更少的线程来就可以来处理通道了,相比使用多个线程,避免了线程上下文切换带来的开销。
IO模型
原文链接https://www.jianshu.com/p/486b0965c296
写在前面:缓存IO又称为标准IO,大多数文件系统的默认IO操作都是缓存IO。在LINUX的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先拷贝到操作系统内核的缓冲区中,然后才会从操作系统的内核缓冲区拷贝到应用程序的地址空间。
网络IO的本质是socket的读取,socket在Linux系统中被抽象为流,IO可以理解成对流的操作。对于一次IO访问,数据先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统的内核缓冲区拷贝到应用程序的地址空间。所以分为两步:
1.第一阶段 等待数据准备
2.第二阶段 将数据从内核拷贝到进程中
对于socket流而言:
第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区
第二步:把数据从内核缓冲区复制到应用程序缓冲区
网络IO模型分为:
同步模型:1.阻塞IO 2.非阻塞IO 3. 多路复用IO 4.信号驱动式IO
异步IO
信号驱动IO在实际中是不常用的。
以一个生动形象的例子来说明这四个概念。周末我和女友去逛街,中午饿了,我们准备去吃饭。周末人多,吃饭需要排队,我和女友有以下几种方案。
2.1 阻塞IO

同步阻塞IO流程描述
kernel就开始了IO的第一个阶段:准备数据
(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。在用户进程这边,整个进程会被阻塞。第二阶段:当内核一直等待到数据准备好了,它就会将数据从内核拷贝到用户地址空间,然后内核返回结果,用户进程解除阻塞状态。这就是非阻塞
。需要不断的询问,是否准备好了。网络模型:
同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式
。在这种模型中,设备是以非阻塞的形式打开的
。这意味着 IO 操作不会立即完成,read 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或 EWOULDBLOCK)。再linux下,通过设置socket使其变为non-blocking。

这就是典型的IO多路复用
。I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作
。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。对于多路复用,也就是轮询多个socket。多路复用既然可以处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不确定了
,当然也可以针对不同的编号。具体流程,如下图所示:

在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking
,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block
。所以IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。
在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理
。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求
。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小
,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源,I/O多路复用的主要应用场景如下:
服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
服务器需要同时处理多种网络协议的套接字。
了解了前面三种IO模式,在用户进程进行系统调用的时候,他们在等待数据到来的时候,处理的方式不一样,直接等待,轮询,select或poll轮询
,两个阶段过程:
第一个阶段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。
第二个阶段都是阻塞的。
从整个IO过程来看,他们都是顺序执行的,因此可以归为同步模型(synchronous)。都是进程主动等待且向内核检查状态。【此句很重要!!!】
高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式
。要理解这一点,首先要扯到并发和并行的区别。比如去某部门办事需要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度
。也就是说并发数是指同时进行的任务数(如同时服务的 HTTP 请求)
,而并行数是可以同时工作的物理资源数量(如 CPU 核数)
。通过合理调度任务的不同阶段,并发数可以远远大于并行度,这就是区区几个 CPU 可以支持上万个用户并发请求的奥秘。在这种高并发的情况下,为每个任务(用户请求)创建一个进程或线程的开销非常大。而同步非阻塞方式可以把多个 IO 请求丢到后台去,这就可以在一个进程里服务大量的并发 IO 请求
。
用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情
。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知
。IO两个阶段,进程都是非阻塞的
。Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。异步过程如下图所示:
首先它会立刻返回,所以不会对用户进程产生任何block
。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程
,告诉它read操作完成了。五种IO模型总结#
3.1 blocking和non-blocking区别##
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
3.2 synchronous IO和asynchronous IO区别##
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞
。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。
有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作
,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了
,在这段时间内,进程是被block的。
而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成
。在这整个过程中,进程完全没有被block。
各个IO Model的比较如图所示:

通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check
,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知
。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据
。
以上是关于IO-操作系统的主要内容,如果未能解决你的问题,请参考以下文章
Servlet-config.properteis资源文件读取操作