一、 IO读写的基础原理:read、write
1、编程模型一致性以及底层系统调用的理解(缓冲区与直接调用):
1.1、无论是Socket的读写还是文件的读写,在Java层面的应用开发或者是linux系统底层开发,都属于输入input和输出output的处理,简称为IO读写。在原理上和处理流程上,都是一致的。区别在于参数的不同。
1.2、用户程序进行IO的读写,基本上会用到read&write两大系统调用。可能不同操作系统,名称不完全一样,但是功能是一样的。
1.3、read系统调用,并不是把数据直接从物理设备,读数据到内存。write系统调用,也不是直接把数据,写入到物理设备。
1.4、read系统调用,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区。这个两个系统调用,都不负责数据在内核缓冲区和磁盘之间的交换。底层的读写交换,是由操作系统kernel内核完成.。
2.理解内核缓冲区和进程缓冲区:(为什么要有缓冲区?减少IO中断)
1.为什么要有缓冲去?&读写调用的本质(减少中断,仅将数据在两个缓冲区进行复制,和设备的交互则由内核决定)
缓冲区的目的,是为了减少频繁的系统IO调用。大家都知道,系统调用需要保存之前的进程数据和状态等信息,而结束调用之后回来还需要恢复之前的信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区。
有了缓冲区,操作系统使用read函数把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区中。等待缓冲区达到一定数量的时候,再进行IO的调用,提升性能。至于什么时候读取和存储则由内核来决定,用户程序不需要关心。
在linux系统中,系统内核也有个缓冲区叫做内核缓冲区。每个进程有自己独立的缓冲区,叫做进程缓冲区。所以,用户程序的IO读写程序,大多数情况下,并没有进行实际的IO操作,而是在读写自己的进程缓冲区。
2.为什么要区分内核空间以及用户空间?(用户进程无法直接调用系统资源,只能通过操作系统来访问)
1.内核态和用户态(kernel mode和user mode),在内核态可以访问系统资源,比如:
1.处理器cpu:cpu控制着一个程序的执行。
2.输入输出IO:linux有句话叫“一切都是流”,也就是所有输入输出设备的数据,包括硬盘,内存,终端都可以像流一样操作。
3.进程管理:类似对进程的创建,休眠,唤醒,释放之类的调度。比如linux下的fork和windows下的CreateProcess()函数。
4.内存:包括内存的申请,释放等管理操作。
5.设备:这个就是常常说的外设了,比如鼠标,键盘。
6.计时器:计算机能计时是因为晶体振荡器产生的电磁脉冲。那么所有的定时任务都是以它为基础的。
7.进程间通信IPC:进程之间是不能够互相访问内存的,所以进程与进程之间的交互需要通信,而通信也是一种资源。
8.网络通信:网络通信可以看做是进程见通信的特殊形式。
而上面所说的这些系统资源,在用户进程中是无法被直接访问的,只能通过操作系统来访问,所以也把操作系统提供的这些功能成为:“系统调用”。
首先看看一个典型Java 服务端处理网络请求的典型过程:
(1)客户端请求
Linux通过网卡,读取客户断的请求数据,将数据读取到内核缓冲区。
(2)获取请求数据
服务器从内核缓冲区读取数据到Java进程缓冲区。
(1)服务器端业务处理
Java服务端在自己的用户空间中,处理客户端的请求。
(2)服务器端返回数据
Java服务端已构建好的响应,从用户缓冲区写入系统缓冲区。
(3)发送给客户端
二、Linux五种IO模型
(1)阻塞IO(Blocking IO):用户空间一直等待。
首先,解释一下这里的阻塞与非阻塞:
阻塞IO,指的是需要内核IO操作彻底完成后,才返回到用户空间,执行用户的操作。阻塞指的是用户空间程序的执行状态,用户空间程序需等到IO操作彻底完成。传统的IO模型都是同步阻塞IO。在java中,默认创建的socket都是阻塞的。
其次,解释一下同步与异步:
同步IO,是一种用户空间与内核空间的调用发起方式。同步IO是指用户空间线程是主动发起IO请求的一方,内核空间是被动接受方。异步IO则反过来,是指内核kernel是主动发起IO请求的一方,用户线程是被动接受方。
举个例子,发起一个blocking socket的read读操作系统调用,流程大概是这样:
1.当用户线程调用了read系统调用,内核(kernel)就开始了IO的第一个阶段:准备数据。很多时候,数据在一开始还没有到达(比如,还没有收到一个完整的Socket数据包),这个时候kernel就要等待足够的数据到来。
2.当kernel一直等到数据准备好了,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。
从开始IO读的read系统调用开始,用户线程就进入阻塞状态。一直到kernel返回结果后,用户线程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在内核进行IO执行的两个阶段,用户线程都被block了。
BIO的优点:
1.程序简单,在阻塞等待数据期间,用户线程挂起。用户线程基本不会占用 CPU 资源。
BIO的缺点:
2.一般情况下,会为每个连接配套一条独立的线程,或者说一条线程维护一个连接成功的IO流的读写。在并发量小的情况下,这个 没有什么问题。但是,当在高并发的场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,基本上,BIO模型在高并发场景下是不可用的。
(2)非阻塞IO(Non-blocking IO):用户空间轮训遍历内核空间,很少用。
非阻塞IO,指的是用户程序不需要等待内核IO操作完成后,内核立即返回给用户一个状态值,用户空间无需等到内核的IO操作彻底完成,可以立即返回用户空间,执行用户的操作,处于非阻塞的状态。
简单的说:阻塞是指用户空间(调用线程)一直在等待,而且别的事情什么都不做;非阻塞是指用户空间(调用线程)拿到状态就返回,IO操作可以干就干,不可以干,就去干的事情。
非阻塞IO要求socket被设置为NONBLOCK。
NIO的特点:
应用程序的线程需要不断的进行 I/O 系统调用,轮询数据是否已经准备好,如果没有准备好,继续轮询,直到完成系统调用为止。
NIO的优点:每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。
NIO的缺点:需要不断的重复发起IO系统调用,这种不断的轮询,将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低。
总之,NIO模型在高并发场景下,也是不可用的。一般 Web 服务器不使用这种 IO 模型。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。java的实际开发中,也不会涉及这种IO模型。
1.与java nio 的区别:
再次说明,Java NIO(New IO) 不是IO模型中的NIO模型,而是另外的一种模型,叫做IO多路复用模型( IO multiplexing )。
(3)IO多路复用(IO Multiplexing)
即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。linux提供了poll/select系统调用,进程通过将一个或多个fd(文件描述符)传递给poll/select系统调用,阻塞在select操作上,这样poll/select可以帮我们监听多个fd是否处于就绪状态,poll/select顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了限制。linux还提供了epoll系统调用,epoll使用了基于事件驱动的方式代替顺序扫描,因此性能更高,当有fd就绪时,立即回到哈数rooback。
在这种模式中,首先不是进行read系统调动,而是进行select/epoll系统调用。当然,这里有一个前提,需要将目标网络连接,提前注册到select/epoll的可查询socket列表中。然后,才可以开启整个的IO多路复用模型的读流程。
1.进行select/epoll系统调用,查询可以读的连接。kernel会查询所有select的可查询socket列表,当任何一个socket中的数据准备好了,select就会返回。当用户进程调用了select,那么整个线程会被block(阻塞掉)。
2.用户线程获得了目标连接后,发起read系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。
3.用户线程才解除block的状态,用户线程终于真正读取到数据,继续执行。
多路复用IO的特点:
IO多路复用模型,建立在操作系统kernel内核能够提供的多路分离系统调用select/epoll基础之上的。多路复用IO需要用到两个系统调用(system call), 一个select/epoll查询调用,一个是IO的读取调用。
和NIO模型相似,多路复用IO需要轮询。负责select/epoll查询调用的线程,需要不断的进行select/epoll轮询,查找出可以进行IO操作的连接。
另外,多路复用IO模型与前面的NIO模型,是有关系的。对于每一个可以查询的socket,一般都设置成为non-blocking模型。只是这一点,对于用户程序是透明的(不感知)。
多路复用IO的优点:
用select/epoll的优势在于,它可以同时处理成千上万个连接(connection)。与一条线程维护一个连接相比,I/O多路复用技术的最大优势是:系统不必创建线程,也不必维护这些线程,从而大大减小了系统的开销。
Java的NIO(new IO)技术,使用的就是IO多路复用模型。在linux系统上,使用的是epoll系统调用。
(4)信号驱动IO模型
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。信号驱动 I/O 的 CPU 利用率很高.
(5)异步IO(Asynchronous IO)
异步IO,指的是用户空间与内核空间的调用方式反过来。用户空间线程是变成被动接受的,内核空间是主动调用者。
这一点,有点类似于Java中比较典型的模式是回调模式,用户空间线程向内核空间注册各种IO事件的回调函数,由内核去主动调用。
1.当用户线程调用了read系统调用,立刻就可以开始去做其它的事,用户线程不阻塞。
2.内核(kernel)就开始了IO的第一个阶段:准备数据。当kernel一直等到数据准备好了,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存)。
3.kernel会给用户线程发送一个信号(signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。
4.用户线程读取用户缓冲区的数据,完成后续的业务操作。
异步IO模型的特点:
在内核kernel的等待数据和复制数据的两个阶段,用户线程都不是block(阻塞)的。用户线程需要接受kernel的IO操作完成的事件,或者说注册IO操作完成的回调函数,到操作系统的内核。所以说,异步IO有的时候,也叫做信号驱动 IO 。
异步IO模型缺点:
需要完成事件的注册与传递,这里边需要底层操作系统提供大量的支持,去做大量的工作。
三、JAVA体系的IO模型
1、 BIO讲解(对应linux中的阻塞IO)
2、 NIO讲解(对应linux中的IO多路复用)
3、AIO讲解(对应linux中的异步IO)
四、IO模型影响的参数:(ulimit 默认1024)
1.句柄限制:
文件句柄:文件(普通文件、目录文件、链接文件、设备文件(socket))描述符是内核为了高效管理已被打开文件所创建的索引,非负整数。指代被打开的文件,所有的IO系统调用,包括SOCKET的读写调用,都是通过文件描述符。
ulimit: 单个进程打开的最大文件句柄数。
报错:Cant open many files。