1. 用户空间和内核态空间
1.1 为什么要区分用户和内核
服务器大多都选用 Linux 体系,这里咱们以 Linux 为例来解说:
ubuntu 和 Centos 都是 Linux 的发行版,发行版能够看成对 linux 包了一层壳,任何 Linux 发行版,其体系内核都是 Linux 。咱们的运用都需求通过 Linux 内核与硬件交互
用户的运用,比方 redis ,mysql 等其实是没有办法去履行拜访咱们操作体系的硬件的,所以咱们能够通过发行版的这个壳子去拜访内核,再通过内核去拜访计算机硬件
计算机硬件包括,如 cpu,内存,网卡等等,内核(通过寻址空间)能够操作硬件的,可是内核需求不同设备的驱动,有了这些驱动之后,内核就能够去对计算机硬件去进行 内存办理,文件体系的办理,进程的办理等等
咱们想要用户的运用来拜访,计算机就必须要通过对外露出的一些接口,才干拜访到,从而简介的完结对内核的控制,可是内核自身上来说也是一个运用,所以他自身也需求一些内存,cpu 等设备资源,用户运用自身也在耗费这些资源,假如不加任何约束,用户去操作随意的去操作咱们的资源,就有或许导致一些冲突,甚至有或许导致咱们的体系呈现无法运转的问题,因此咱们需求把用户和内核隔离开
1.2 进程寻址空间
进程的寻址空间划分红两部分:内核空间、用户空间
什么是寻址空间呢?咱们的运用程序也好,还是内核空间也好,都是没有办法直接去物理内存的,而是通过分配一些虚拟内存映射到物理内存中,咱们的内核和运用程序去拜访虚拟内存的时分,就需求一个虚拟地址,这个地址是一个无符号的整数。
比方一个 32 位的操作体系,他的带宽便是 32,他的虚拟地址便是 2 的 32 次方,也便是说他寻址的规模便是 0~2 的 32 次方, 这片寻址空间对应的便是 2 的 32 个字节,便是 4GB,这个 4GB,会有 3 个 GB 分给用户空间,会有 1GB 给内核体系
在 linux 中,他们权限分红两个等级,0 和 3,用户空间只能履行受限的指令(Ring3),而且不能直接调用体系资源,必须通过内核提供的接口来拜访内核空间能够履行特权指令(Ring0),调用全部体系资源,所以一般状况下,用户的操作是运转在用户空间,而内核运转的数据是在内核空间的,而有的状况下,一个运用程序需求去调用一些特权资源,去调用一些内核空间的操作,所以此刻他俩需求在用户态和内核态之间进行切换。
比方:
Linux 体系为了进步 IO 功率,会在用户空间和内核空间都加入缓冲区:
- 写数据时,要把用户缓冲数据复制到内核缓冲区,然后写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后复制到用户缓冲区
针对这个操作:咱们的用户在写读数据时,会去向内核态申请,想要读取内核的数据,而内核数据要去等候驱动程序从硬件上读取数据,当从磁盘上加载到数据之后,内核会将数据写入到内核的缓冲区中,然后再将数据复制到用户态的 buffer 中,然后再回来给运用程序,整体而言,速度慢,便是这个原因,为了加快,咱们希望 read 也好,还是 wait for data 也最好都不要等候,或许时刻尽量的短。
2. 网络模型
2.1 堵塞IO
- 进程 1:运用程序想要去读取数据,他是无法直接去读取磁盘数据的,他需求先到内核里边去等候内核操作硬件拿到数据,这个进程是需求等候的,比及内核从磁盘上把数据加载出来之后,再把这个数据写给用户的缓存区。
- 进程 2:假如是堵塞 IO,那么整个进程中,用户从发起读恳求开端,一直到读取到数据,都是一个堵塞状况。
用户去读取数据时,会去先发起 recvform 一个指令,去测验从内核上加载数据,假如内核没有数据,那么用户就会等候,此刻内核会去从硬件上读取数据,内核读取数据之后,会把数据复制到用户态,而且回来 ok,整个进程,都是堵塞等候的,这便是堵塞 IO
总结如下:
望文生义,堵塞 IO 便是两个阶段都必须堵塞等候:
阶段一:
- 用户进程测验读取数据(比方网卡数据)
- 此刻数据没有抵达,内核需求等候数据
- 此刻用户进程也处于堵塞状况
阶段二:
- 数据抵达并复制到内核缓冲区,代表已安排妥当
- 将内核数据复制到用户缓冲区
- 复制进程中,用户进程仍然堵塞等候
- 复制完结,用户进程免除堵塞,处理数据
能够看到,堵塞 IO 模型中,用户进程在两个阶段都是堵塞状况。
2.2 非堵塞 IO
望文生义,非堵塞 IO 的 recvfrom 操作会当即回来成果而不是堵塞用户进程
阶段一:
- 用户进程测验读取数据(比方网卡数据)
- 此刻数据没有抵达,内核需求等候数据
- 回来反常给用户进程
- 用户进程拿到 error 后,再次测验读取
- 循环往复,直到数据安排妥当
阶段二:
- 将内核数据复制到用户缓冲区
- 复制进程中,用户进程仍然堵塞等候
- 复制完结,用户进程免除堵塞,处理数据
- 能够看到,非堵塞 IO 模型中,用户进程在第一个阶段对错堵塞,第二个阶段是堵塞状况。虽然对错堵塞,但功能并没有得到进步。而且忙等机制会导致 CPU 空转,CPU 运用率暴增。
2.3 信号驱动
信号驱动 IO 是与内核树立 SIGIO 的信号关联并设置回调,当内核有 FD 安排妥当时,会宣布 SIGIO 信号告诉用户,期间用户运用能够履行其它事务,无需堵塞等候。
阶段一:
- 用户进程调用 sigaction ,注册信号处理函数
- 内核回来成功,开端监听 FD
- 用户进程不堵塞等候,能够履行其它事务
- 当内核数据安排妥当后,回调用户进程的 SIGIO 处理函数
阶段二:
- 收到 SIGIO 回调信号
- 调用 recvfrom ,读取
- 内核将数据复制到用户空间
- 用户进程处理数据
当有很多 IO 操作时,信号较多,SIGIO 处理函数不能及时处理或许导致信号行列溢出,而且内核空间与用户空间的频繁信号交互功能也较低。
2.4 异步 IO
这种办法,不仅仅是用户态在试图读取数据后,不堵塞,而且当内核的数据预备完结后,也不会堵塞
他会由内核将全部数据处理完结后,由内核将数据写入到用户态中,然后才算完结,所以功能极高,不会有任何堵塞,悉数都由内核完结,能够看到,异步 IO 模型中,用户进程在两个阶段都对错堵塞状况。
2.5 IO 多路复用
场景引入
为了更好的理解 IO ,现在假定这样一种场景:一家餐厅
- A 状况:这家餐厅中现在只要一位服务员,而且选用客户排队点餐的办法,就像这样:
每排到一位客户要吃到饭,都要通过两个过程:
考虑要吃什么 顾客开端点餐,厨师开端炒菜
由于餐厅只要一位服务员,因此一次只能服务一位客户,而且还需求等候当时客户考虑出成果,这浪费了后续排队的人十分多的时刻,功率极低。这便是堵塞 IO。
当然,为了缓解这种状况,老板完全能够多雇几个人,但这也会增加本钱,而在极大客流量的状况下,仍然不会有很高的功率提高
- B 状况: 这家餐厅中现在只要一位服务员,而且选用客户排队点餐的办法。
每排到一位客户要吃到饭,都要通过两个过程:
- 考虑要吃什么
- 顾客开端点餐,厨师开端炒菜
与 A 状况不同的是,此刻服务员会不断询问顾客:“你想吃西红柿鸡蛋盖浇饭吗?那滑蛋牛肉呢?那肉末茄子呢?……”
虽然服务员在不停的问,可是在网络中,这并不会增加数据的安排妥当速度,首要还是等顾客自己确认。所以,这并不会进步餐厅的功率,说不定还会招来更多差评。这便对错堵塞 IO。
- C 状况: 这家餐厅中现在只要一位服务员,可是不再选用客户排队的办法,而是顾客自己获取菜单并点餐,点完后告诉服务员,就像这样:
每排到一位客户要吃到饭,还是都要通过两个过程:
- 看着菜单,考虑要吃什么
- 告诉服务员,我点好了
与 A B 不同的是,这种状况服务员不用再等候顾客考虑吃什么,只需求在收到顾客告诉后,去接收菜单就好。这样相当于餐厅在只要一个服务员的状况下,一起服务了多个人,而不像 A B,同一时刻只能服务一个人。此刻餐厅的功率天然就进步了很多。
映射到咱们的网络服务中,便是这样:
- 客人:客户端恳求
- 点餐内容:客户端发送的实际数据
- 老板:操作体系
- 人力本钱:体系资源
- 菜单:文件状况描述符。操作体系对于一个进程能够一起持有的文件状况描述符的个数是有约束的,在 linux 体系中 $ulimit -n 查看这个约束值,当然也是能够 (而且应该) 进行内核参数调整的。
- 服务员:操作体系内核用于 IO 操作的线程 (内核线程)
- 厨师:运用程序线程 (当然厨房便是运用程序进程咯)
- 餐单传递办法:包括了堵塞式和非堵塞式两种。
- 办法 A: 堵塞 IO
- 办法 B: 非堵塞 IO
- 办法 C: 多路复用 IO
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,可是巨细整体是有约束的 ),这个调集的长度巨细是有约束的,一起在这个调集中,标明出来咱们要控制哪些数据。
其内部流程:
用户态下:
- 创立 fd_set 调集,包括要监听的 读事情、写事情、反常事情 的调集
- 确认要监听的 fd_set 调集
- 即将监听的调集作为参数传入 select () 函数中,select 中会将 调集复制到内核 buffer 中
内核态:
- 内核线程在得到 调集后,遍历该调集
- 没数据安排妥当,就休眠
- 当数据来时,线程被唤醒,然后再次遍历调集,标记安排妥当的 fd 然后将整个调集,复制回用户 buffer 中
- 用户线程遍历 调集,找到安排妥当的 fd ,再发起读恳求。
不足之处:
- 调集巨细固定为 1024 ,也便是说最多维持 1024 个 socket,在海量数据下,不够用
- 调集需求在 用户 buffer 和内核 buffer 中重复复制,涉及到 用户态和内核态的切换,十分影响功能
2.6.2 poll
poll 形式对 select 形式做了简略改进,但功能提高不明显。
IO 流程:
- 创立 pollfd 数组,向其中增加重视的 fd 信息,数组巨细自定义
- 调用 poll 函数,将 pollfd 数组复制到内核空间,转链表存储,无上限
- 内核遍历 fd ,判别是否安排妥当
- 数据安排妥当或超时后,复制 pollfd 数组到用户空间,回来安排妥当 fd 数量 n
- 用户进程判别 n 是否大于 0, 大于 0 则遍历 pollfd 数组,找到安排妥当的 fd
与 select 对比:
- select 形式中的 fd_set 巨细固定为 1024,而 pollfd 在内核中选用链表,理论上无上限,但实际上不能这么做,由于的监听 FD 越多,每次遍历耗费时刻也越久,功能反而会下降
2.6.3 epoll
epoll 形式是对 select 和 poll 的改进,它提供了三个函数:eventpoll 、epoll_ctl 、epoll_wait
- eventpoll 函数内部包括了两个东西 :
- 红黑树 :用来记载全部的 fd
- 链表 : 记载已安排妥当的 fd
- epoll_ctl 函数 ,即将监听的 fd 增加到 红黑树 上去,而且给每个 fd 绑定一个监听函数,当 fd 安排妥当时就会被触发,这个监听函数的操作便是 将这个 fd 增加到 链表中去。
- epoll_wait 函数,安排妥当等候。一开端,用户态 buffer 中创立一个空的 events 数组,当安排妥当之后,咱们的回调函数会把 fd 增加到链表中去,当函数被调用的时分,会去检查链表(当然这个进程需求参考装备的等候时刻,能够等一定时刻,也能够一直等),假如链表中没有有 fd 则 fd 会从红黑树被增加到链表中,此刻再将链表中的的 fd 复制到 用户态的空 events 中,而且回来对应的操作数量,用户态此刻收到呼应后,会从 events 中拿到已经预备好的数据,在调用 读办法 去拿数据。
2.6.4 总结:
select 形式存在的三个问题:
- 能监听的 FD 最大不超过 1024
- 每次 select 都需求把全部要监听的 FD 都复制到内核空间
- 每次都要遍历全部 FD 来判别安排妥当状况
poll 形式的问题:
- poll 使用链表处理了 select 中监听 FD 上限的问题,但仍然要遍历全部 FD,假如监听较多,功能会下降
epoll 形式中怎么处理这些问题的?
- 根据 epoll 实例中的红黑树保存要监听的 FD,理论上无上限,而且增修改查功率都十分高
- 每个 FD 只需求履行一次 epoll_ctl 增加到红黑树,今后每次 epol_wait 无需传递任何参数,无需重复复制 FD 到内核空间
- 使用 ep_poll_callback 机制来监听 FD 状况,无需遍历全部 FD,因此功能不会随监听的 FD 数量增多而下降
2.7 根据 epoll 的服务器端流程
一张图搞定:
咱们来整理一下这张图
- 服务器启动今后,服务端会去调用 epoll_create,创立一个 epoll 实例,epoll 实例中包括两个数据
-
红黑树(为空):rb_root 用来去记载需求被监听的 FD
-
链表(为空):list_head,用来寄存已经安排妥当的 FD
-
创立好了之后,会去调用 epoll_ctl 函数,此函数会会将需求监听的 fd 增加到 rb_root 中去,而且对当时这些存在于红黑树的节点设置回调函数。
-
当这些被监听的 fd 一旦预备安排妥当,与之相关联的回调函数就会被调用,而调用的成果便是将红黑树的 fd 增加到 list_head 中去 (可是此刻并没有完结)
-
fd 增加完结后,就会调用 epoll_wait 函数,这个函数会去校验是否有 fd 预备安排妥当(由于 fd 一旦预备安排妥当,就会被回调函数增加到 list_head 中),在等候了一段时刻 (能够进行装备)。
-
假如等够了超时时刻,则回来没有数据,假如有,则进一步判别当时是什么事情,假如是树立连接事情,则调用 accept () 接受客户端 socket ,拿到树立连接的 socket ,然后树立起来连接,假如是其他事情,则把数据进行写出。
2.8 五种网络模型对比:
最后用一幅图,来阐明他们之间的区别
3. redis 通信协议
3.1 RESP 协议
Redis 是一个 CS 架构的软件,通信一般分两步(不包括 pipeline 和 PubSub):
- 客户端(client)向服务端(server)发送一条指令,服务端解析并履行指令
- 回来呼应成果给客户端,因此客户端发送指令的格局、服务端呼应成果的格局必须有一个标准,这个标准便是通信协议。
而在 Redis 中选用的是 RESP(Redis Serialization Protocol)协议:
- Redis 1.2 版别引入了 RESP 协议
- Redis 2.0 版别中成为与 Redis 服务端通信的标准,称为 RESP2
- Redis 6.0 版别中,从 RESP2 晋级到了 RESP3 协议,增加了更多数据类型而且支撑 6.0 的新特性–客户端缓存
但现在,默许运用的仍然是 RESP2 协议。在 RESP 中,通过首字节的字符来区分不同数据类型,常用的数据类型包括 5 种:
- 单行字符串:首字节是 ‘+’ ,后边跟上单行字符串,以 CRLF( “\r\n” )结束。例如回来”OK”: “+OK\r\n”
- 过错(Errors):首字节是 ‘-’ ,与单行字符串格局相同,仅仅字符串是反常信息,例如:”-Error message\r\n”
- 数值:首字节是 ‘:’ ,后边跟上数字格局的字符串,以 CRLF 结束。例如:”:10\r\n”
- 多行字符串:首字节是 ‘$’ ,表示二进制安全的字符串,最大支撑 512MB:
- 假如巨细为 0,则代表空字符串:”$0\r\n\r\n”
- 假如巨细为 – 1,则代表不存在:”$-1\r\n”
- 数组:首字节是 ‘*’,后边跟上数组元素个数,再跟上元素,元素数据类型不限 :
本文由
传智教育博学谷
教研团队发布。假如本文对您有帮助,欢迎
重视
和点赞
;假如您有任何建议也可留言谈论
或私信
,您的支撑是我坚持创造的动力。转载请注明出处!