1. 用户空间和内核态空间

1.1 为什么要区分用户和内核

服务器大多都采用 Linux 系统,这里我们以 Linux 为例来讲解:

ubuntu 和 Centos 都是 Linux 的发行版,发行版可以看成对 linux 包了一层壳,任何 Linux 发行版,其系统内核都是 Linux 。我们的应用都需要通过 Linux 内核与硬件交互

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

用户的应用,比如 redis ,mysql 等其实是没有办法去执行访问我们操作系统的硬件的,所以我们可以通过发行版的这个壳子去访问内核,再通过内核去访问计算机硬件
最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

计算机硬件包括,如 cpu,内存,网卡等等,内核(通过寻址空间)可以操作硬件的,但是内核需要不同设备的驱动,有了这些驱动之后,内核就可以去对计算机硬件去进行 内存管理,文件系统的管理,进程的管理等等

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

我们想要用户的应用来访问,计算机就必须要通过对外暴露的一些接口,才能访问到,从而简介的实现对内核的操控,但是内核本身上来说也是一个应用,所以他本身也需要一些内存,cpu 等设备资源,用户应用本身也在消耗这些资源,如果不加任何限制,用户去操作随意的去操作我们的资源,就有可能导致一些冲突,甚至有可能导致我们的系统出现无法运行的问题,因此我们需要把用户和内核隔离开

1.2 进程寻址空间

进程的寻址空间划分成两部分:内核空间、用户空间

什么是寻址空间呢?我们的应用程序也好,还是内核空间也好,都是没有办法直接去物理内存的,而是通过分配一些虚拟内存映射到物理内存中,我们的内核和应用程序去访问虚拟内存的时候,就需要一个虚拟地址,这个地址是一个无符号的整数。

比如一个 32 位的操作系统,他的带宽就是 32,他的虚拟地址就是 2 的 32 次方,也就是说他寻址的范围就是 0~2 的 32 次方, 这片寻址空间对应的就是 2 的 32 个字节,就是 4GB,这个 4GB,会有 3 个 GB 分给用户空间,会有 1GB 给内核系统

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

在 linux 中,他们权限分成两个等级,0 和 3,用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问内核空间可以执行特权命令(Ring0),调用一切系统资源,所以一般情况下,用户的操作是运行在用户空间,而内核运行的数据是在内核空间的,而有的情况下,一个应用程序需要去调用一些特权资源,去调用一些内核空间的操作,所以此时他俩需要在用户态和内核态之间进行切换。

比如:

Linux 系统为了提高 IO 效率,会在用户空间和内核空间都加入缓冲区:

针对这个操作:我们的用户在写读数据时,会去向内核态申请,想要读取内核的数据,而内核数据要去等待驱动程序从硬件上读取数据,当从磁盘上加载到数据之后,内核会将数据写入到内核的缓冲区中,然后再将数据拷贝到用户态的 buffer 中,然后再返回给应用程序,整体而言,速度慢,就是这个原因,为了加速,我们希望 read 也好,还是 wait for data 也最好都不要等待,或者时间尽量的短。

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

2. 网络模型

2.1 阻塞IO

用户去读取数据时,会去先发起 recvform 一个命令,去尝试从内核上加载数据,如果内核没有数据,那么用户就会等待,此时内核会去从硬件上读取数据,内核读取数据之后,会把数据拷贝到用户态,并且返回 ok,整个过程,都是阻塞等待的,这就是阻塞 IO

总结如下:

顾名思义,阻塞 IO 就是两个阶段都必须阻塞等待:

阶段一:

阶段二:

可以看到,阻塞 IO 模型中,用户进程在两个阶段都是阻塞状态。

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

2.2 非阻塞 IO

顾名思义,非阻塞 IO 的 recvfrom 操作会立即返回结果而不是阻塞用户进程

阶段一:

阶段二:

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

2.3 信号驱动

信号驱动 IO 是与内核建立 SIGIO 的信号关联并设置回调,当内核有 FD 就绪时,会发出 SIGIO 信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。

阶段一:

阶段二:

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了
当有大量 IO 操作时,信号较多,SIGIO 处理函数不能及时处理可能导致信号队列溢出,而且内核空间与用户空间的频繁信号交互性能也较低。

2.4 异步 IO

这种方式,不仅仅是用户态在试图读取数据后,不阻塞,而且当内核的数据准备完成后,也不会阻塞

他会由内核将所有数据处理完成后,由内核将数据写入到用户态中,然后才算完成,所以性能极高,不会有任何阻塞,全部都由内核完成,可以看到,异步 IO 模型中,用户进程在两个阶段都是非阻塞状态。

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

2.5 IO 多路复用

场景引入

为了更好的理解 IO ,现在假设这样一种场景:一家餐厅

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了
每排到一位客户要吃到饭,都要经过两个步骤:

思考要吃什么
顾客开始点餐,厨师开始炒菜

由于餐厅只有一位服务员,因此一次只能服务一位客户,并且还需要等待当前客户思考出结果,这浪费了后续排队的人非常多的时间,效率极低。这就是阻塞 IO。

当然,为了缓解这种情况,老板完全可以多雇几个人,但这也会增加成本,而在极大客流量的情况下,仍然不会有很高的效率提升

每排到一位客户要吃到饭,都要经过两个步骤:

  1. 思考要吃什么
  2. 顾客开始点餐,厨师开始炒菜

与 A 情况不同的是,此时服务员会不断询问顾客:“你想吃番茄鸡蛋盖浇饭吗?那滑蛋牛肉呢?那肉末茄子呢?……”

虽然服务员在不停的问,但是在网络中,这并不会增加数据的就绪速度,主要还是等顾客自己确定。所以,这并不会提高餐厅的效率,说不定还会招来更多差评。这就是非阻塞 IO。

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

每排到一位客户要吃到饭,还是都要经过两个步骤:

  1. 看着菜单,思考要吃什么
  2. 通知服务员,我点好了

与 A B 不同的是,这种情况服务员不必再等待顾客思考吃什么,只需要在收到顾客通知后,去接收菜单就好。这样相当于餐厅在只有一个服务员的情况下,同时服务了多个人,而不像 A B,同一时刻只能服务一个人。此时餐厅的效率自然就提高了很多。

映射到我们的网络服务中,就是这样:

2.6 多路复用 IO 的实现

目前流程的多路复用 IO 实现主要包括四种: select、poll、epoll、kqueue。下表是他们的一些重要特性的比较:

IO 模型 相对性能 关键思路 操作系统 JAVA 支持情况
select 较高 Reactor windows/Linux 支持,Reactor 模式 (反应器设计模式)。Linux 操作系统的 kernels 2.4 内核版本之前,默认使用 select;而目前 windows 下对同步 IO 的支持,都是 select 模型
poll 较高 Reactor Linux Linux 下的 JAVA NIO 框架,Linux kernels 2.6 内核版本之前使用 poll 进行支持。也是使用的 Reactor 模式
epoll Reactor/Proactor Linux Linux kernels 2.6 内核版本及以后使用 epoll 进行支持;Linux kernels 2.6 内核版本之前使用 poll 进行支持;另外一定注意,由于 Linux 下没有 Windows 下的 IOCP 技术提供真正的 异步 IO 支持,所以 Linux 下使用 epoll 模拟异步 IO
kqueue Proactor Linux 目前 JAVA 的版本不支持

多路复用 IO 技术最适用的是 “高并发” 场景,所谓高并发是指 1 毫秒内至少同时有上千个连接请求准备好。其他情况下多路复用 IO 技术发挥不出来它的优势。另一方面,使用 JAVA NIO 进行功能实现,相对于传统的 Socket 套接字实现要复杂一些,所以实际应用中,需要根据自己的业务需求进行技术选择。

2.6.1 select

select 是 Linux 最早是由的 I/O 多路复用技术:

linux 中,一切皆文件,socket 也不例外,我们把需要处理的数据封装成 FD,然后在用户态时创建一个 fd_set 的集合(这个集合的大小是要监听的那个 FD 的最大值 + 1,但是大小整体是有限制的 ),这个集合的长度大小是有限制的,同时在这个集合中,标明出来我们要控制哪些数据。

其内部流程:

用户态下:

  1. 创建 fd_set 集合,包括要监听的 读事件、写事件、异常事件 的集合
  2. 确定要监听的 fd_set 集合
  3. 将要监听的集合作为参数传入 select () 函数中,select 中会将 集合复制到内核 buffer 中

内核态:

  1. 内核线程在得到 集合后,遍历该集合
  2. 没数据就绪,就休眠
  3. 当数据来时,线程被唤醒,然后再次遍历集合,标记就绪的 fd 然后将整个集合,复制回用户 buffer 中
  4. 用户线程遍历 集合,找到就绪的 fd ,再发起读请求。

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

不足之处:

2.6.2 poll

poll 模式对 select 模式做了简单改进,但性能提升不明显。

IO 流程:

与 select 对比:

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

2.6.3 epoll

epoll 模式是对 select 和 poll 的改进,它提供了三个函数:eventpoll 、epoll_ctl 、epoll_wait

2.6.4 总结:

select 模式存在的三个问题:

poll 模式的问题:

epoll 模式中如何解决这些问题的?

2.7 基于 epoll 的服务器端流程

一张图搞定:

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

我们来梳理一下这张图

  1. 服务器启动以后,服务端会去调用 epoll_create,创建一个 epoll 实例,epoll 实例中包含两个数据
  1. 创建好了之后,会去调用 epoll_ctl 函数,此函数会会将需要监听的 fd 添加到 rb_root 中去,并且对当前这些存在于红黑树的节点设置回调函数。

  2. 当这些被监听的 fd 一旦准备就绪,与之相关联的回调函数就会被调用,而调用的结果就是将红黑树的 fd 添加到 list_head 中去 (但是此时并没有完成)

  3. fd 添加完成后,就会调用 epoll_wait 函数,这个函数会去校验是否有 fd 准备就绪(因为 fd 一旦准备就绪,就会被回调函数添加到 list_head 中),在等待了一段时间 (可以进行配置)。

  4. 如果等够了超时时间,则返回没有数据,如果有,则进一步判断当前是什么事件,如果是建立连接事件,则调用 accept () 接受客户端 socket ,拿到建立连接的 socket ,然后建立起来连接,如果是其他事件,则把数据进行写出。

2.8 五种网络模型对比:

最后用一幅图,来说明他们之间的区别

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

3. redis 通信协议

3.1 RESP 协议

Redis 是一个 CS 架构的软件,通信一般分两步(不包括 pipeline 和 PubSub):

而在 Redis 中采用的是 RESP(Redis Serialization Protocol)协议:

但目前,默认使用的依然是 RESP2 协议。在 RESP 中,通过首字节的字符来区分不同数据类型,常用的数据类型包括 5 种:

最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了

本文由传智教育博学谷教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!

发表回复