目录html
Linux NIO 系列(01) 五种网络 IO 模型node
Netty 系列目录(http://www.javashuo.com/article/p-hskusway-em.html)linux
在正式开始讲 Linux IO 模型前,先介绍 5 个基本概念。web
如今操做系统都是采用虚拟存储器,那么对 32 位操做系统而言,它的寻址空间 (虚拟存储空间)为 4G (2的32次方)。操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备的全部权限。为了保证用户进程不能直接操做内核 (kernel),保证内核的安全,操做系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对 Linux 操做系统而言,将最高的 1G 字节 (从虚拟地址 0xC0000000 到 0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节 (从虚拟地址 0x00000000 到 0xBFFFFFFF),供各个进程使用,称为用户空间。数据库
为了控制进程的执行,内核必须有能力挂起正在 CPU 上运行的进程,并恢复之前挂起的某个进程的执行。这种行为被称为进程切换。所以能够说,任何进程都是在操做系统内核的支持下运行的,是与内核紧密相关的。总之,进程切换很耗资源。编程
正在执行的进程,因为期待的某些事件未发生,如请求系统资源失败、等待某种操做的完成、新数据还没有到达或无新工做作等,则由系统自动执行阻塞原语(Block),使本身由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也所以只有处于运行态的进程 (得到 CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用 CPU 资源的。缓存
文件描述符 (File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。安全
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者建立一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写每每会围绕着文件描述符展开。可是文件描述符这一律念每每只适用于 UNIX、Linux 这样的操做系统。网络
Linux 内核将全部外部设备都看作一个文件来操做,对一个文件的读写操做会调用内核提供的系统命令,返回一个 file descriptor(fd,文件描述符)。而对一个 socket 的读写也会有相应的描述符,称为 socketfd(socket 描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。多线程
缓存 IO 又被称做标准 IO,大多数文件系统的默认 IO 操做都是缓存 IO。在 Linux 的缓存 IO 机制中,操做系统会将 IO 的数据缓存在文件系统的页缓存 (page cache) 中,也就是说,数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 IO 的缺点:
网络 IO 的本质是 socket 的读取,socket 在 linux 系统被抽象为流,IO 能够理解为对流的操做。刚才说了,对于一次 IO 访问 (以 read 举例),数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。因此说,当一个 read 操做发生时,它会经历两个阶段:
对于 socket 流而言,
网络应用须要处理的无非就是两大类问题,网络 IO,数据计算。相对于后者,网络 IO 的延迟,给应用带来的性能瓶颈大于后者。网络 IO 的模型大体有以下几种:
注:因为 signal driven IO 在实际中并不经常使用,因此这只说起剩下的四种 IO Model。
我和女朋友点完餐后,不知道何时能作好,只好坐在餐厅里面等,直到作好,而后吃完才离开。女朋友本想还和我一块儿逛街的,可是不知道饭能何时作好,只好和我一块儿在餐厅等,而不能去逛街,直到吃完饭才能去逛街,中间等待作饭的时间浪费掉了。这就是典型的阻塞。
同步阻塞 IO 模型是最经常使用的一个模型,也是最简单的模型。在 Linux 中,默认状况下全部的 socket 都是 blocking。它符合人们最多见的思考逻辑。阻塞就是进程 "被" 休息,CPU 处理其它进程去了。
在这个 IO 模型中,用户空间的应用程序执行一个系统调用 (recvform),这会致使应用程序阻塞,什么也不干,直到数据准备好,而且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据处处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络 IO。调用应用程序处于一种再也不消费 CPU 而只是简单等待响应的状态,所以从处理的角度来看,这是很是有效的。在调用 recv()/recvfrom() 函数时,发生在内核中等待数据和复制数据的过程,大体以下图:
当用户进程调用了 recv()/recvfrom() 这个系统调用,kernel 就开始了 IO 的第一个阶段:准备数据 (对于网络 IO 来讲,不少时候数据在一开始尚未到达。好比,尚未收到一个完整的 UDP 包。这个时候 kernel 就要等待足够的数据到来)。这个过程须要等待,也就是说数据被拷贝到操做系统内核的缓冲区中是须要一个过程的。而在用户进程这边,整个进程会被阻塞 (固然,是进程本身选择的阻塞)。第二个阶段:当 kernel 一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存,而后 kernel 返回结果,用户进程才解除 block 的状态,从新运行起来。
因此,blocking IO 的特色就是在 IO 执行的两个阶段都被 block 了。
优势:
缺点:
我女朋友不甘心白白在这等,又想去逛商场,又担忧饭好了。因此咱们逛一会,回来询问服务员饭好了没有,来来回回好屡次,饭都还没吃都快累死了啦。这就是非阻塞。须要不断的询问,是否准备好了。
同步非阻塞就是 “每隔一下子瞄一眼进度条” 的轮询 (polling)方式。在这种模型中,设备是以非阻塞的形式打开的。这意味着 IO 操做不会当即完成,read 操做可能会返回一个错误代码,说明这个命令不能当即知足 (EAGAIN 或 EWOULDBLOCK)。
在网络 IO 时候,非阻塞 IO 也会进行 recvform 系统调用,检查数据是否准备好,与阻塞 IO 不同,“非阻塞将大的整片时间的阻塞分红 N 多的小的阻塞,因此进程不断地有机会 ‘被’ CPU光顾”。
也就是说非阻塞的 recvform 系统调用以后,进程并无被阻塞,内核立刻返回给进程,若是数据还没准备好,此时会返回一个 error。进程在返回以后,能够干点别的事情,而后再发起 recvform 系统调用。重复上面的过程,循环往复的进行 recvform 系统调用。这个过程一般被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。须要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
在 Linux 下,能够经过设置 socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读操做时,流程如图所示:
当用户进程发出 read 操做时,若是 kernel 中的数据尚未准备好,那么它并不会 block 用户进程,而是马上返回一个 error。从用户进程角度讲,它发起一个 read 操做后,并不须要等待,而是立刻就获得了一个结果。用户进程判断结果是一个 error 时,它就知道数据尚未准备好,因而它能够再次发送 read 操做。一旦 kernel 中的数据准备好了,而且又再次收到了用户进程的 system call,那么它立刻就将数据拷贝到了用户内存,而后返回。
因此,nonblocking IO 的特色是用户进程须要不断的主动询问 kernel 数据好了没有。
同步非阻塞方式相比同步阻塞方式:
优势:可以在等待任务完成的时间里干其余活了 (包括提交其余任务,也就是 “后台” 能够有多个任务在同时执行)。
缺点:任务完成的响应延迟增大了,由于每过一段时间才去轮询一次 read 操做,而任务可能在两次轮询之间的任意时间完成。这会致使总体数据吞吐量的下降。
与第二个方案差很少,餐厅安装了电子屏幕用来显示点餐的状态,这样我和女朋友逛街一会,回来就不用去询问服务员了,直接看电子屏幕就能够了。这样每一个人的餐是否好了,都直接看电子屏幕就能够了,这就是典型的 IO 多路复用。
因为同步非阻塞方式须要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的 CPU 时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。若是轮询不是用户的进程,而是有人帮忙就行了。这就是所谓的 “IO 多路复用”。UNIX/Linux 下的 select、poll、epoll 就是干这个的 (epoll 比 poll、select 效率高,作的事情是同样的)。
I/O 复用模型会用到 select、poll、epoll 函数,这几个函数也会使进程阻塞。select 调用是内核级别的。
select 轮询相对非阻塞的轮询的区别在于:select 能够对多个 socket 端口进行监听,当其中任何一个 socket 的数据准好了,就能返回进行可读,而后进程再进行 recvform 系统调用,将数据由内核拷贝到用户进程,固然这个过程是阻塞的
select 相对 blocking IO 阻塞不一样在于:此时的 select 不是等到 socket 数据所有到达再处理,而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。也能够理解为"非阻塞"吧。
对于多路复用,也就是轮询多个 socket。多路复用既然能够处理多个 IO,也就带来了新的问题,多个 IO 之间的顺序变得不肯定了,固然也能够针对不一样的编号。具体流程,以下图所示:
IO multiplexing 就是咱们说的 select,poll,epoll,有些地方也称这种 IO 方式为 event driven IO。select/epoll 的好处就在于单个 process 就能够同时处理多个网络链接的 IO。它的基本原理就是 select,poll,epoll 这个 function 会不断的轮询所负责的全部 socket,当某个 socket 有数据到达了,就通知用户进程。
当用户进程调用了 select,那么整个进程会被 block ,而同时,kernel 会“监视”全部 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操做,将数据从 kernel 拷贝到用户进程。
多路复用的特色是经过一种机制一个进程能同时等待 IO 文件描述符,内核监视这些文件描述符 (套接字描述符),其中的任意一个进入读就绪状态,select, poll,epoll 函数就能够返回。对于监视的方式,又能够分为 select, poll, epoll 三种方式。
上面的图和 blocking IO 的图其实并无太大的不一样,事实上,还更差一些。 由于这里须要使用两个 system call (select 和 recvfrom),而 blocking IO 只调用了一个 system call (recvfrom)。可是,用 select 的优点在于它能够同时处理多个 connection。
因此,若是处理的链接数不是很高的话,使用 select/epoll 的 web server 不必定比使用 multi-threading + blocking IO 的 web server 性能更好,可能延迟还更大。select/epoll 的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。
在 IO multiplexing Model 中,实际中,对于每个 socket,通常都设置成为 non-blocking,可是,如上图所示,整个用户的 process 实际上是一直被 block 的。只不过 process 是被 select 这个函数 block,而不是被 socket IO 给 block。因此 IO 多路复用是阻塞在 select,epoll 这样的系统调用之上,而没有阻塞在真正的 I/O 系统调用如 recvfrom 之上。
了解了前面三种 IO 模式,在用户进程进行系统调用的时候,他们在等待数据到来的时候,处理的方式不同,直接等待,轮询,select 或 poll 轮询,两个阶段过程:
从整个 IO 过程来看,他们都是顺序执行的,所以能够归为同步模型(synchronous)。都是进程主动等待且向内核检查状态。【此句很重要!!!】
高并发的程序通常使用同步非阻塞方式,而非多线程+同步阻塞方式。要理解这一点,首先要扯到并发和并行的区别。好比去某部门办事须要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度。也就是说并发数是指同时进行的任务数 (如同时服务的 HTTP 请求),而并行数是能够同时工做的物理资源数量 (如 CPU 核数)。经过合理调度任务的不一样阶段,并发数能够远远大于并行度,这就是区区几个 CPU 能够支持上万个用户并发请求的奥秘。在这种高并发的状况下,为每一个任务 (用户请求)建立一个进程或线程的开销很是大。而同步非阻塞方式能够把多个 IO 请求丢到后台去,这就能够在一个进程里服务大量的并发 IO 请求。
注意:IO 多路复用是同步阻塞模型仍是异步阻塞模型,在此给你们分析下:
同步是须要主动等待消息通知,而异步则是被动接收消息通知,经过回调、通知、状态等方式来被动获取消息。IO 多路复用在阻塞到 select 阶段时,用户进程是主动等待并调用 select 函数获取数据就绪状态消息,而且其进程状态为阻塞。因此,把 IO 多路复用归为同步阻塞模式。
信号驱动式 I/O:首先咱们容许 Socket 进行信号驱动 IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,能够在信号处理函数中调用 I/O 操做函数处理数据。过程以下图所示:
女朋友不想逛街,又餐厅太吵了,回家好好休息一下。因而咱们叫外卖,打个电话点餐,而后我和女朋友能够在家好好休息一下,饭好了送货员送到家里来。这就是典型的异步,只须要打个电话说一下,而后能够作本身的事情,饭好了就送来了。
相对于同步 IO,异步 IO 不是顺序执行。用户进程进行 aio_read 系统调用以后,不管内核数据是否准备好,都会直接返回给用户进程,而后用户态进程能够去作别的事情。等到 socket 数据准备好了,内核直接复制数据给进程,而后从内核向进程发送通知。IO 两个阶段,进程都是非阻塞的。
Linux 提供了 AIO 库函数实现异步,可是用的不多。目前有不少开源的异步 IO 库,例如 libevent、libev、libuv。异步过程以下图所示:
用户进程发起 aio_read 操做以后,马上就能够开始去作其它的事。而另外一方面,从 kernel 的角度,当它受到一个 asynchronous read 以后, 首先它会马上返回,因此不会对用户进程产生任何 block 。而后,kernel 会等待数据准备完成,而后将数据拷贝到用户内存, 当这一切都完成以后,kernel 会给用户进程发送一个 signal 或执行一个基于线程的回调函数来完成此次 IO 处理过程 ,告诉它 read 操做完成了。
在 Linux 中,通知的方式是 “信号”:
若是这个进程正在用户态忙着作别的事 (例如在计算两个矩阵的乘积),那就强行打断之,调用事先注册的信号处理函数,这个函数能够决定什么时候以及如何处理这个异步任务。因为信号处理函数是忽然闯进来的,所以跟中断处理程序同样,有不少事情是不能作的,所以保险起见,通常是把事件 “登记” 一下放进队列,而后返回该进程原来在作的事。
若是这个进程正在内核态忙着作别的事,例如以同步阻塞方式读写磁盘,那就只好把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
若是这个进程如今被挂起了,例如无事可作 sleep 了,那就把这个进程唤醒,下次有 CPU 空闲的时候,就会调度到这个进程,触发信号通知。
异步 API 说来轻巧,作来难,这主要是对 API 的实现者而言的。Linux 的异步 IO (AIO)支持是 2.6.22 才引入的,还有不少系统调用不支持异步 IO。Linux 的异步 IO 最初是为数据库设计的,所以经过异步 IO 的读写操做不会被缓存或缓冲,这就没法利用操做系统的缓存与缓冲机制。
不少人把 Linux 的 O_NONBLOCK 认为是异步方式,但事实上这是前面讲的同步非阻塞方式。须要指出的是,虽然 Linux 上的 IO API 略显粗糙,但每种编程框架都有封装好的异步 IO 实现。操做系统少作事,把更多的自由留给用户,正是 UNIX 的设计哲学,也是 Linux 上编程框架百花齐放的一个缘由。
从前面 IO 模型的分类中,咱们能够看出 AIO 的动机:
同步阻塞模型须要在 IO 操做开始时阻塞应用程序。这意味着不可能同时重叠进行处理和 IO 操做。
同步非阻塞模型容许处理和 IO 操做重叠进行,可是这须要应用程序根据重现的规则来检查 IO 操做的状态。
这样就剩下异步非阻塞 IO 了,它容许处理和 IO 操做重叠进行,包括 IO 操做完成的通知。
IO 多路复用除了须要阻塞以外,select 函数所提供的功能 (异步阻塞 IO)与 AIO 相似。不过,它是对通知事件进行阻塞,而不是对 IO 调用进行阻塞。
有时咱们的 API 只提供异步通知方式,例如在 node.js 里,但业务逻辑须要的是作完一件过后作另外一件事,例如数据库链接初始化后才能开始接受用户的 HTTP 请求。这样的业务逻辑就须要调用者是以阻塞方式来工做。
为了在异步环境里模拟 “顺序执行” 的效果,就须要把同步代码转换成异步形式,这称为 CPS (Continuation Passing Style)变换。BYVoid 大神的 continuation.js 库就是一个 CPS 变换的工具。用户只需用比较符合人类常理的同步方式书写代码,CPS 变换器会把它转换成层层嵌套的异步回调形式。
天天用心记录一点点。内容也许不重要,但习惯很重要!
转载:https://blog.csdn.net/bpingchang/article/details/51419890