IO-操作系统
Posted 爱吃草的羊驼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IO-操作系统相关的知识,希望对你有一定的参考价值。
用户态和内核态
现代操作系统,为了保护系统的安全,都会划分出内核空间和用户空间,或者我们经常说的内核态和用户态。简单来说,就是划分为内核态和用户态两个等 级,运行在用户态的进程大都是一些应用程序,能够访问的系统资源受到极大的限制。而运行在内核态的进程权限就非常大,可以"为所欲为”。 这么做的目的是为了保护操作系统的底层资源,例如文件都要存在硬盘,但是如果用户编写的应用程序可以随意的操作硬盘的启动扇区,那就很容易把系统搞崩溃,分为内核态和用户态之后,用户态的应用程序就不能直接操作底层的硬件接口了,如果需要操作硬盘,比如存文件,那就必须经过内核态来协调。这样就可以对所有底层硬件的操作方式进行规范。 有了用户态和内核态的划分后,应用程序就经常需要在用户态和内核态之间进行切换。例如程序要保存一个文件到硬盘,在程序执行的用户态,是不能直接操作磁盘的。只有切换到内核态才能真正去操作磁盘。 内核态运行操作系统程序,操作硬件,用户态运行用户程序;当程序运行在 3 级特权级上时,可以称之为运行在用户态,当程序运行在 0 级特权级上时,称之为运行在内核态。 特权级别 R0、R1、R2 和 R3 R0 相当于内核态,R3 相当于用户态; 不同级别能够运行不同的指令集合;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。非阻塞 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 模型(NIO)中,需要 N(N>=1)次轮询系统调用,然而借助select 的 IO 多路复用模型,只需要发起一次询问就够了,大大优化了性能。 但是呢,select 有几个缺点: 监听的 IO 最大连接数有限,在 Linux 系统上一般为 1024。select 函数返回后,是通过遍历 fdset,找到就绪的描述符 fd。(仅知道有 I/O 事件发生,却不知是哪几个流,所以遍历所有流 ). 因为 存在连接数限制 ,所以后来又提出了 poll 。与 select 相比, poll 解决了 连接数限制问题 。但是,select 和 poll 一样,还是需要通过遍历文件描述符来获取已经就绪的 socket。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长, 效率也会线性下降 。 因此经典的多路复用模型 epoll 诞生。IO 多路复用之 epoll
为了解决 select/poll 存在的问题,多路复用模型 epoll 诞生,它采用事件驱动来实现,流程图如下: epoll 先通过 epoll_ctl()来注册一个 fd(文件描述符),一旦基于某个 fd 就绪时,内核会采用回调机制,迅速激活这个 fd,当进程调用 epoll_wait()时便得到通知。这里去掉了 遍历文件描述符 的坑爹操作,而是采用 监听事件回调 的机制。 这就是 epoll 的亮点。 总结一下 select、poll、epoll 的区别 epoll 明显优化了 IO 的执行效率,但在进程调用 epoll_wait()时,仍然可能被阻塞。能不能不用我老是去问你数据是否准备就绪,等我发出请求后,你数据准备好了通知我就行了,这就诞生了信号驱动 IO 模型 。IO 模型之信号驱动模型
信号驱动不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号,然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用 recvfrom,去读取数据。 信号驱动 IO 模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。它已经有异步操作的感觉了。但是上面的流程图,发现数据复制到应用缓冲的时候 ,应用进程还是阻塞的。回过头来看下,不管是 BIO,还是 NIO,还是信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的。还有没有优化方案呢?AIO(真正的异步 IO)!
异步 IO(AIO asynchronous IO)
前面讲的 BIO,NIO 和信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞 的,因此都不算是真正的异步。AIO 实现了 IO 全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是 表示提交成功类似的意思 。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程 IO 操作执行完毕。 流程如下:异步 IO 的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。日常开发中,有类似思想的业务场景: 比如发起一笔批量转账,但是批量转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。 阻塞、非阻塞、同步、异步 IO 划分:
一个通俗例子读懂 BIO、NIO、AIO 同步阻塞(blocking-IO)简称 BIO 同步非阻塞(non-blocking-IO)简称 NIO 异步非阻塞(asynchronous-non-blocking-IO)简称 AIO 一个经典生活的例子: BIO 小明去吃同仁四季的椰子鸡,就这样在那里排队,等了一小时 ,轮到她了,然后才开始吃椰子鸡。 NIO 小红也去同仁四季的椰子鸡,她一看要等挺久的,于是去逛会商场,每次逛一下, 就跑回来看看,是不是轮到她了 。于是最后她既购了物,又吃上椰子鸡了。 AIO 小华一样,去吃椰子鸡,由于他是高级会员,所以店长说,你去商场随便逛会吧, 等下有位置,我立马打电话给你 。于是小华不用干巴巴坐着等,也不用每过一会儿就跑回来看有没有等到,最后也吃上了美味的椰子鸡 Java NIO 概述 Java NIO(Non Blocking IO)是从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API,NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。
阻塞 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 事件地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件,如下图所示: 从图中可以看出,当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 SelectionKey,同时从 SelectionKey 中可以找到发生的事件和该事件所发生的具体的 SelectableChannel,以获得客户端发送过来的数据。 非阻塞指的是 IO 事件本身不阻塞,但是获取 IO 事件的 select()方法是需要阻塞等待的. 区别是 BIO 会阻塞在 IO 操作上,NIO 阻塞在事件获取上,没有事件就没有IO,从高层次看 IO 就不阻塞了.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资源文件读取操作