Linux 网络 IO 模型

写在前面

本文主要介绍 Unix/Linux 下五种网络 IO 模型,但是。为了更好的理解下面提到的五种网络 IO 的概念,我们有必要先理清下面这几个概念。

用户空间与内核空间

一个计算机通常有一定大小的内存空间,如一台计算机有 4GB 的地址空间,但是程序并不能完全使用这些地址空间,因为这些地址空间是被划分为 用户空间和内核空间 的。用户应用程序只能使用用户空间的内存,这里所说的使用是指应用程序能够申请的内存空间,并不是真正访问的地址空间。下面看下什么是用户空间和内核空间:

用户空间

用户空间是常规进程所在的区域,什么是常规进程,打开任务管理器看到的就是常规进程:

1326851-20190909204654342-1471181329.png

JVM 就是常规进程,驻守于用户空间,用户空间是非特权区域,比如在该区域执行的代码不能直接访问硬件设备。

内核空间

内核空间主要是指操作系统运行时所使用的用于程序调度、虚拟内存的使用或者连接硬件资源等的程序逻辑。内核代码有特别的权利,比如它能与设备控制器通讯,控制着整个用于区域进程的运行状态。和 I/O 相关的一点是:所有 I/O 都直接或间接通过内核空间。

那么,为什么要划分用户空间和内核空间呢?这也是为了保证操作系统的稳定性和安全性。用户程序不可以直接访问硬件资源,如果用户程序需要访问硬件资源,必须调用操作系统提供的接口,这个调用接口的过程也就是系统调用。每一次系统调用都会存在两个内存空间之间的数据交互,通常的网络传输也是一次系统调用,通过网络传输的数据先是从内核空间接收到远程机器的数据,然后再从内核空间复制到用户空间,供用户程序使用。

下面通过一张图更形象的描述这一过程:

1326851-20190909204336898-1564278537.png

小贴士:这种内核空间与用户空间的数据的复制很费时,虽然保住了程序运行的安全性和稳定性,但是牺牲了一部分的效率。但是,目前的操作系统已经针对这一块进行了不错的优化,这里不是我们讨论的重点。

小贴士:如何分配用户空间和内核空间的比例也是一个问题,是更多地分配给用户空间供用户程序使用,还是首先保住内核有足够的空间来运行,还是要平衡一下。在当前的 Windows 32 位操作系统中,默认用户空间:内核空间的比例是 1:1,而在 32 位 Linux 系统中的默认比例是 3:1(3GB 用户空间、1GB 内核空间)。

同步和异步

同步和异步是一种思想,涉猎到的领域也比较多,在 I/O 领域(同步 IO,异步 IO),请求调用领域(同步请求,异步请求,同步调用,异步调用)。虽然,涉及多种领域,但是思想是一样的。同步和异步,真正的关注点是 消息通信机制

同步

以 “调用” 为例,所谓同步,就是 在发出一个 “调用请求” 时,在没有得到结果之前,该 “调用请求” 就不返回,但是一旦调用返回就得到返回值了。换句话说,就是由 "调用者" 主动等待 “被调用者” 的结果。像我们平时写的,方法 A 调用 Math.random() 方法、方法 B 调用 String.substring() 方法都是同步调用,因为调用者主动在等待这些方法的返回。

异步

所谓异步,则正好相反,当一个异步调用请求发出之后,调用者不会立刻得到这个请求真正的执行完后得出的结果,立即返回的可能只是一个伪结果 。因此异步调用适用于那些对数据一致性要求不是很高的场景,或者是执行过程很耗时的场景。如果这种场景下,我们希望获取异步调用的结果,"被调用者"可以通过状态、通知来通知调用者,或通过回调函数处理这个调用,对应 Java 中的有 Future/FutureTask、wait/notify 体现了这一思想。

阻塞和非阻塞

阻塞和非阻塞其实是针对进程或者是线程的状态来判定的。比如下面的,用户进程从操作系统的内核缓冲区读取数据的时候,如果此时内核缓冲区中的数据还没准备好的话,操作系统可采用的一种方式就是将用户进程阻塞在那儿,那么此时该用户进程的状态就会从运行状态变为阻塞状态,也就是阻塞了。

了解了上面的基础知识之后,接下来我们就正式进入 Linux 的网络 IO 模型。

Linux 网络 IO 模型

理解这五种网络 I/O 模型之前,我们还得得先清楚一个网络 IO 事件发生,会涉及到哪些对象,会经历哪些步骤:

网络 IO 涉及到的对象

对于一个网络 IO (这里我们以 read 举例),它会涉及到两个系统对象,一个是调用这个 IO 的进程或者是线程,另一个就是 Linux 系统内核空间和用户空间。

进程执行 I/O 操作的步骤

进程执行 I/O 操作,归结起来,就是向操作系统发出请求,让它要么把缓冲区里的数据排干净(写),要么用数据把缓冲区填满(读)。进程利用这一机制处理所有数据进出操作,操作系统内部处理这一任务的机制,其复杂程度可能超乎想像,但就概念而言,却非常直白易懂,对于一个网络 IO,这里我们以 read 为例,当一个 read 操作发生时,会经历两个阶段:

  1. 内核缓冲区准备数据

  2. 内核缓冲区数据拷贝到用户缓冲区

几种 IO 模型的区别就体现在这两阶段,下面对这几种 IO 模型进行详细介绍。

阻塞 IO

当用户进程开始调用了 recvfrom 这个函数后,就开始了 IO 的 第一阶段:内核缓冲区准备数据。对于网络 IO 来说,数据只有在积累到一定的量的时候才会发送,这个时候内核缓冲区就要等待足够的数据到来。而在用户缓冲区这边,用户进程会一直被操作系统阻塞,当内核缓冲区数据准备好了,此时就会将内核缓冲区中的数据拷贝到用户缓冲区,然后 由操作系统唤醒被阻塞的用户进程 并将结果返回给用户进程,此时用户进程才重新运行起来。所以,阻塞 IO 的特点就是在 IO 执行的两个阶段都被阻塞了

1326851-20190909204405695-1117290097.png

非阻塞 IO

从图中可以看出,当用户进程发出 read 操作时,如果内核缓冲区中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个 error。从用户进程角度讲 ,它发起一个 read 操作后,并没有被阻塞,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作,就这样一直进行下去,到这里第一阶段都是一直在轮训。一旦内核缓冲区中的数据准备好了,并且又再次收到了用户进程的 read 请求,那么它马上就将数据从内核缓冲区拷贝到用户缓冲区,然后返回给用户线程,这是第二阶段。所以,用户进程在第一阶段其实并没有被操作系统一直阻塞,而是需要不断的主动询问内核缓冲区数据好了没有。只有在第二阶段数据拷贝到时候会被阻塞

1326851-20190909204426854-1310320501.png

IO 多路复用

IO 多路复用实际上就是通过一种机制,一个进程可以监视多个描 fd,一旦某个 fd 就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,这种机制目前有 select、pselect、poll、epoll,但它们本质上都是同步 IO。

1326851-20190909204442958-923461485.png

注意,上面的阻塞 IO 和非阻塞 IO 用户进程都是只是调用 recvfrom 一个函数,而这里用户进程还会再调用一个 select 函数,当用户进程调用了 select,那么整个进程会被阻塞,而同时,操作系统会 “监视” 所有 select 负责的 socket 所对应的的内核缓冲区的数据,当任何一个 socket 所对应的内核缓冲区中的数据准备好了,就会返回可读条件的通知。此时用户进程再调用 read 操作,将数据从内核缓冲区拷贝到用户缓冲区。

这个图和阻塞 IO 的图其实并没有太大的区别,事实上,还更差一些。因为这里需要使用两个 system call (select 和 recvfrom),而阻塞 IO 只调用了一个system call (recvfrom)。但是,调用 select 的优势在于它可以同时处理多个 socket。(所以,如果处理的连接数不是很高的话,使用 select 的 web server 不一定比使用 多线程 + 阻塞 IO 的 web server 性能更好,可能延迟还更大。

小贴士:强调一下,select 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

在 IO 多路复用模型中,实际上,对于每一个 socket,一般都设置成为非阻塞的,但是,如上图所示,整个用户进程实际上是一直被阻塞的。只不过用户进程是被 select 这个函数阻塞的 ,而不是被 socket IO 给阻塞的(或者也可以理解为是操作系统阻塞的)。

这里肯定有人要问那 select 的作用不就是阻塞多个用户进程,然后将这些用户进程与服务器建立的 socket 监视起来,看看哪个 socket 对应的内核缓冲区中的数据准备好了,然后再通知用户进程,让用户进程再发一次 recvfrom 请求来进行数据拷贝。那 epoll 的作用也是这个呀,为啥人家就说 epoll 的效率更高呢?下面,我就来详细的介绍为啥 epoll 效率更高。要想知道这个我们就要先了解一下 Linux 的 select,poll,epoll 函数。

我们先来看一下 Linux 的 select,poll,epoll 具体作用是什么,有什么区别?

select

select 函数监视的 fd(磁盘描述符,注意:Linux 下系统各组件都是以磁盘描述符的形式存在的,例如 socket) 分 3 类,分别是 writefds,readfds,和 exceptfds。调用 select 函数后会阻塞,直到有 fd 就绪(有数据可读、可写、或者有 except),或者超时(timeout 指定等待时间,如果立即返回则设为 null 即可),它就会将它刚刚监控的所有的 fd 对应的标识符的集合 fd_set (注意,这里将内核缓冲区中数据已经就绪的 fd 的标识会打上一个标记)返回给用户进程,然后用户进程再去遍历 fd_set 找出其中内核缓冲区中数据已经就绪的 fd 的标识符,然后再去发送 recvfrom 请求,开始第二阶段。

select 优点:

  1. select 目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

select 缺点:

  1. select 的很大的缺陷就是单个进程能监控的 fd 的数量是有一定限制的,它由 FD_SETSIZE 限制,默认值是 1024,如果修改的话,就需要重新编译内核,不过这会带来网络效率的下降

  2. select 模型下内核地址空间和用户地址空间每次数据复制都是复制所有的 fd; 随着 fd 数目的增加,可能只有很少一部分 fd 是活跃的,但是 select 每次调用时都会遍历整个 fd_set,检查每个 fd 的数据就绪的状态,这就导致效率很低。

poll

poll 本质上和 select 没有区别,它也是将整个 fd_set 告诉给用户进程。和 select 不同的是它没有最大连接数的限制,原因是它是基于链表来存储的。

poll 缺点:

  1. 模型下内核地址空间和用户地址空间每次数据复制都是复制所有的 fd。

  2. poll 还有一个特点是:水平触发,如果报告了 fd 处于就绪状态后,没有被处理,那么下次 poll 时会再次报告该 fd;fd 增加时,线性扫描导致性能下降。

epoll

epoll 支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些 fd 变为就绪态,并且只会通知一次。还有一个特点是,epoll 使用 事件 的就绪通知方式,通过 epoll_ctl 注册 fd,一旦该 fd 就绪,内核就会采用类似 callback 的回调机制来激活该 fd,epoll_wait 就可以收到通知。

epoll的优点:

  1. 没有最大并发连接的限制,它支持的 fd 上限受操作系统最大文件句柄数;

  2. 效率提升,不同于 select 和 poll,epoll 只会对 活跃(数据处于就绪状态) 的 fd 进行操作,这是因为在内核实现中 epoll 是根据每个 fd 上面的 callback 函数实现的,只有活跃的 fd 才会主动的去调用 callback 函数,其他 idle 状态的 fd 则不会。epoll 的性能不会受 fd 总数的限制。

  3. select/poll 都需要内核把 fd 消息通知给用户空间,而 epoll 是通过内核和用户空间 mmap 同一块内存实现。
    epoll 对 fd 的操作有两种模式:LT(level trigger)和 ET(edge trigger),默认模式是 LT。

小贴士:
LT 模式与 ET 模式的区别如下:
LT 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件,下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。
ET 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件,如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。

这里用一张表格展示一下几个函数的区别:

类别selectpollepoll
支持的最大连接数由 FD_SETSIZE 限制基于链表存储,没有限制受系统最大句柄数限制
fd 剧增的影响线性扫描 fd 导致性能很低同 select基于 fd 上 callback 实现,没有性能下降的问题
消息传递机制内核需要将消息传递到用户空间,需要内核拷贝同 selectepoll 通过内核与用户空间共享内存来实现

到这里,我们大概就知道了为什么 epoll 比 select 和 poll 效率更高了。

信号驱动 IO

进程和内核的 fd 建立一个 sigio 的处理程序,然后自己做其他事情,并不会阻塞,当内核数据准备好的时候会触发 Sigaction 系统调用告诉用户进程数据准备好了,此时,用户进程发出 recvfrom 进行第二阶段。

1326851-20190909204509548-1593226143.png

异步 IO

用户进程发出异步 IO后,IO 操作立即返回,用户进程这时就可以去做别的事情了,之后的一切工作都又内核来完成。当内核数据准备好的时候,内核自动将数据拷贝到用户空间 (不阻塞用户进程,这里是和上面几个都不同的),拷贝完成后向用户进程发送信号。

1326851-20190909204525942-1810879921.png

小贴士:Linux 下 的 asynchronous IO 其实用得很少。

最后看一下 Linux 下五种网络 IO 模型的比较:

1326851-20190909204542722-445301507.png

写这篇网络 IO 模型是为了后面深入研究 NIO 和 Netty 做准备,也希望能够为大家解决一些疑问。

参考:

《码农翻身》
https://blog.csdn.net/baidu_39511645/article/details/78283680
https://www.cnblogs.com/wlwl/p/10291397.html
https://juejin.im/entry/585ba7038d6d810065d3d54a
https://www.cnblogs.com/xrq730/p/5074199.html
https://blog.51cto.com/xingej/1971598
https://www.cnblogs.com/javalyy/p/8882066.html
https://www.jianshu.com/p/6f132d27aeaf?utm_campaign
https://blog.csdn.net/u013374645/article/details/82808301
http://baijiahao.baidu.com/s?id=1604983471279587214&wfr=spider&for=pc

转载于:https://www.cnblogs.com/tkzL/p/11494134.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值