Linux 为什么要区分内核空间与用户空间?
Linux 操作系统的 IO 模型有哪几种?有啥区别?
常说的阻塞现象,到底是咋回事?
网络编程研发时,那块到底耗时最多,代码是否还有优化空间?
前几期的分享,我们站在编码视角去聊 Java IO,旨在理解与编码,本次从 Linux 操作系统层面了解一下 IO 模型,这样方能做到知其然,知其所以然。
01. 内核空间、用户空间
万事万物我们看到的皆是表象,操作系统也不例外。我们经常打交道的用户界面,是操作系统的外在表象,内核才是操作系统的内在核心。
内核,可以访问受保护的内存空间,拥有访问底层硬件设备的所有权限(比如读写磁盘文件,分配回收内存,从网络接口读写数据等等)。
为了内核的安全,操作系统将虚拟空间划分为内核空间(内核代码运行的地方)和用户空间(用户程序代码运行的地方)。内核空间和用户空间是隔离的,这样即使用户的程序崩溃了,内核也不受影响。
另外,用户程序不能直接操作内核,需要通过系统调用来与内核进行通信(应用程序通过内核提供的接口来完成访问)。
02. Socket 通信流程
Socket 通信流程应该不再陌生,本次分享着重剖析图中圈住的部分。
站在服务端的视角,对于一次 Socket 的数据读取操作流程,如图示意,网络数据到达网卡,数据先被拷贝到内核缓冲区中,然后从内核缓冲区拷贝到进程用户空间。
站在服务端的视角,当一个读操作发生时,稍微再细化一下,其实会经历两个阶段。
第一阶段:等待数据准备。
例如:recv() 等待数据,需要等待网络上的数据分组到达,然后被复制到内核的缓冲区。
第二阶段:将数据从内核缓冲区拷贝到用户空间。
例如:recv() 接收连接发送的数据后,需要把数据复制到内核缓冲区,再从内核缓冲区复制到进程用户空间。
一定记住这两个阶段,也正因为存在这两个阶段,Linux系统升级迭代中出现了五种网络 IO 模型。
03. Linux 网络 IO 模型
(一)阻塞 IO 模型 - Blocking IO
图解:当应用进程调用了 recv() 这个系统调用,内核就开始了 IO 的第一个阶段:准备数据。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除阻塞的状态。
特点:在 IO 执行的两个阶段都被阻塞了。
场景:阻塞 Socket、Java BIO。适用并发较小的网络应用,并发较大的不适用,因为一个请求 IO 阻塞进程,就要为每个请求分配一个进程(线程)来响应,开销大。
(二)非阻塞 I/O 模型 - Non-Blocking IO
图解:当用户进程发出 recv() 操作时,如果内核中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个错误码。一旦内核中的数据准备好了,并且又再次收到了用户进程的系统调用,那么它马上就将数据拷贝到了用户内存,然后返回。
特点:用户进程需要不断的主动询问内核数据好了没有,进程轮询调用,消耗 CPU 资源。
场景:SOCKET 设置 NON BLOCKING 属性。支持并发量小,不用及时响应的网络应用。
(三)I/O多路复用 - IO multiplexing
在 Linux 内核代码迭代过程中,依次支持了 SELECT、POLL、EPOLL 三种多路复用的网络 I/O 模型。
IO 多路复用的的基本原理是指单个线程就可以同时处理多个网络连接。 具体实现是 SELECT、POLL、EPOLL这些函数,会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。
图解:以 select 为例,当用户进程调用了 select,那么整个进程会被阻塞,而此时,内核会监视所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 recv 操作,将数据从内核拷贝到用户进程。
特点:IO 多路复用是阻塞在 select,poll,epoll 这样的系统调用之上,而没有阻塞在真正的I/O系统调用(如recv());专一进程解决多个进程 IO 的阻塞问题,性能好,Reactor模式。
场景:Java NIO,Nginx。适用高并发服务应用开发,一个进程/线程响应多个请求。
(四)信号驱动 I/O - Signal driven IO
我们也可以用信号,让内核在描述字就绪时发送 SIGIO 信号通知我们,称这种模型为信号驱动I/O(signal-driven I/O)。
应用进程使用 sigaction 系统调用,预先告知内核,向内核注册这样一个函数,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recv() 将数据从内核复制到应用进程中。
(五)异步 I/O 模型 - Asynchronous IO
图解:应用进程执行 aio_read() 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
特点:不阻塞,一步到位。
场景: Java 7 AIO、高性能服务器,高性能高并发。
异步 IO 模型,要求等待数据和数据拷贝操作的两个处理阶段上都不能等待(blocking),内核自行去准备好数据并将数据从内核缓冲区中复制到应用进程的缓冲区,再通知应用进程读操作完成了,然后应用进程再去处理。
遗憾的是,Linux 的网络 IO 模型中是不存在异步 IO 的,Linux 的网络 IO 处理的第二阶段总是阻塞等待数据 copy 完成的。
04. Linux 网络 IO 模型比较
上图已经把 Linux 中的 IO 模型归档的很到位,还是稍微总结一下。前四种 IO 模型都是同步 IO 模型,主要区别在于第一阶段的处理不同,第二阶段的处理是相同的,都是在数据从内核复制到用户空间时,进程阻塞于 recv() 调用。而异步 IO 模型的处理都是非阻塞的,用户进程将整个 IO 操作交由内核去完成,内核完成后会发送通知。
好了,今天就扯这么多,希望大家能够喜欢。