网络模型
用户空间、内核空间
1、任何 Linux 发行版,其系统内核都是 Linux,应用都需要通过 Linux 内核与硬件交互
2、为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的
(1)进程的寻址空间会划分为两部分:内核空间、用户空间
(2)用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问
(3)内核空间可以执行特权命令(Ring0),调用一切系统资源
3、Linux 系统为了提高 I/O 效率,会在用户空间和内核空间都加入缓冲区
(1)写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
(2)读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
5 种 I/O 模型
1、阻塞 IO(Blocking IO)
2、非阻塞 IO(Nonblocking IO)
3、IO 多路复用(IO Multiplexing)
4、信号驱动 IO(Signal Driven IO)
5、异步 IO(Asynchronous IO)
阻塞IO
1、两个阶段都必须阻塞等待
2、阶段一
(1)用户进程尝试读取数据
(2)此时数据尚未到达,内核需要等待数据
(3)此时用户进程也处于阻塞状态
3、阶段二
(1)数据到达并拷贝到内核缓冲区,代表已就绪
(2)将内核数据拷贝到用户缓冲区
(3)拷贝过程中,用户进程依然阻塞等待
(4)拷贝完成,用户进程解除阻塞,处理数据
4、用户进程在两个阶段都是阻塞状态
非阻塞 IO
1、recvfrom 操作会立即返回结果,而不是阻塞用户进程
2、阶段一
(1)用户进程尝试读取数据
(2)此时数据尚未到达,内核需要等待数据
(3)返回异常给用户进程
(4)用户进程获取 error 后,再次尝试读取
(5)循环往复,直到数据就绪
3、阶段二
(1)将内核数据拷贝到用户缓冲区
(2)拷贝过程中,用户进程依然阻塞等待
(3)拷贝完成,用户进程解除阻塞,处理数据
4、用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态,虽然是非阻塞,但性能并没有得到提高,而且忙等机制会导致 CPU 空转,CPU 使用率暴增
IO 多路复用
1、利用单个线程来同时监听多个 FD,并在某个 FD 可读、可写时得到通知,从而避免无效的等待,充分利用 CPU 资源
2、文件描述符(File Descriptor):简称 FD,是一个从 0 开始的无符号整数,用来关联 Linux 中的一个文件,在Linux中,一切皆文件,例如常规文件、视频、硬件设备、网络套接字(Socket)
3、阶段一
(1)用户进程调用 select,指定要监听的 FD 集合
(2)内核监听 FD 对应的多个 socket
(3)任意一个或多个 socket 数据就绪,则返回 readable
(4)此过程中用户进程阻塞
4、阶段二
(1)用户进程找到就绪的 socket
(2)依次调用 recvfrom 读取数据
(3)内核将数据拷贝到用户空间
(4)用户进程处理数据
5、监听 FD 方式、通知方式有多种实现
(1)select
(2)poll
(3)epoll
(4)select、poll:只会通知用户进程有 FD 就绪,但不确定具体 FD,需要用户进程逐个遍历 FD 来确认
(5)epoll:会在通知用户进程 FD 就绪的同时,把已就绪的 FD 写入用户空间
6、select 是 Linux 最早的 I/O 多路复用技术
// 定义类型别名 __fd_mask,本质是 long int
typedef long int __fd_mask;
/* fd_set 记录要监听的fd集合,及其对应状态 */
typedef struct {
// fds_bits是long类型数组,长度为 1024/32 = 32
// 共1024个bit位,每个bit位代表一个fd,0代表未就绪,1代表就绪
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
// ...
} fd_set;
// select函数,用于监听fd_set,也就是多个fd的集合
int select(
int nfds, // 要监视的fd_set的最大fd + 1
fd_set *readfds, // 要监听读事件的fd集合
fd_set *writefds,// 要监听写事件的fd集合
fd_set *exceptfds, // // 要监听异常事件的fd集合
// 超时时间,null-用不超时;0-不阻塞等待;大于0-固定等待时间
struct timeval *timeout
);
7、select 模式存在的问题
(1)需要将整个 fd_set 从用户空间拷贝到内核空间,select 结束还要再次拷贝回用户空间
(2)select 无法得知具体 fd 就绪,需要遍历整个 fd_set
(3)fd_set 监听的 fd 数量不能超过 1024
8、poll 模式对 select 模式做简单改进,但性能提升不明显
// pollfd 中的事件类型
#define POLLIN //可读事件
#define POLLOUT //可写事件
#define POLLERR //错误事件
#define POLLNVAL //fd未打开
// pollfd结构
struct pollfd {
int fd; /* 要监听的fd */
short int events; /* 要监听的事件类型:读、写、异常 */
short int revents;/* 实际发生的事件类型 */
};
// poll函数
int poll(
struct pollfd *fds, // pollfd数组,可以自定义大小
nfds_t nfds, // 数组元素个数
int timeout // 超时时间
);
9、poll 模式 IO 流程
(1)创建 pollfd 数组,向其中添加关注的 fd 信息,数组大小自定义
(2)调用 poll 函数,将 pollfd 数组拷贝到内核空间,转链表存储,无上限
(3)内核遍历 fd,判断是否就绪
(4)数据就绪或超时后,拷贝 pollfd 数组到用户空间,返回就绪 fd 数量 n
(5)用户进程判断 n 是否大于 0
(6)大于 0 则遍历 pollfd 数组,找到就绪的 fd
10、poll 与 select 对比
(1)select 模式中的 fd_set 大小固定为 1024,而 pollfd 在内核中采用链表,理论上无上限
(2)监听 FD 越多,每次遍历消耗时间也越久,性能反而会下降
11、epoll 模式是对 select 和 poll 的改进
(1)基于 epoll 实例中的红黑树保存要监听的 FD,理论上无上限,而且增删改查效率都非常高
(2)每个 FD 只需要执行一次 epoll_ctl 添加到红黑树,以后每次 epol_wait 无需传递任何参数,无需重复拷贝 FD 到内核空间
(3)利用 ep_poll_callback 机制来监听 FD 状态,无需遍历所有 FD,因此性能不会随监听的 FD 数量增多而下降
struct eventpoll {
//...
struct rb_root rbr; // 一颗红黑树,记录要监听的FD
struct list_head rdlist;// 一个链表,记录就绪的FD
//...
};
// 1.创建一个epoll实例,内部是event poll,返回对应的句柄epfd
int epoll_create(int size);
// 2.将一个FD添加到epoll的红黑树中,并设置ep_poll_callback
// callback触发时,就把对应的FD加入到rdlist这个就绪列表中
int epoll_ctl(
int epfd, // epoll实例的句柄
int op, // 要执行的操作,包括:ADD、MOD、DEL
int fd, // 要监听的FD
struct epoll_event *event // 要监听的事件类型:读、写、异常等
);
// 3.检查rdlist列表是否为空,不为空则返回就绪的FD的数量
int epoll_wait(
int epfd, // epoll实例的句柄
struct epoll_event *events, // 空event数组,用于接收就绪的FD
int maxevents, // events数组的最大长度
int timeout // 超时时间,-1用不超时;0不阻塞;大于0为阻塞时间
);
12、两种通知的模式
(1)LevelTriggered:LT,水平触发,只要某个 FD 中有数据可读,每次调用 epoll_wait 都会得到通知
(2)EdgeTriggered:ET,边沿触发,只有在某个 FD 有状态变化时,调用 epoll_wait 才会被通知
(3)LT:事件通知频率较高,会有重复通知,影响性能
(4)ET:仅通知一次,效率高,可以基于非阻塞 IO 循环读取,解决数据读取不完整问题
(5)select、poll 仅支持 LT 模式,epoll 可以自由选择 LT 或 ET 两种模式
13、基于 epoll 模式的 web 服务的基本流程
信号驱动 IO
1、与内核建立 SIGIO 信号关联并设置回调,当内核有 FD 就绪时,会发出 SIGIO 信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待
2、阶段一
(1)用户进程调用 sigaction,注册信号处理函数
(2)内核返回成功,开始监听 FD
(3)用户进程不阻塞等待,可以执行其它业务
(4)当内核数据就绪后,回调用户进程的 SIGIO 处理函数
3、阶段二
(1)收到 SIGIO 回调信号
(2)调用 recvfrom,读取
(3)内核将数据拷贝到用户空间
(4)用户进程处理数据
4、当有大量 IO 操作时,信号较多,SIGIO 处理函数不能及时处理,可能导致信号队列溢出,而且内核空间与用户空间的频繁信号交互性能也较低
异步 IO
1、整个过程都是非阻塞的,用户进程调用完异步 API 后,就可以去做其它事情,内核等待数据就绪,并拷贝到用户空间后才会递交信号,通知用户进程
2、阶段一
(1)用户进程调用 aio_read,创建信号回调函数
(2)内核等待数据就绪
(3)用户进程无需阻塞,可以做任何事情
3、阶段二
(1)内核数据就绪
(2)内核数据拷贝到用户缓冲区
(3)拷贝完成,内核递交信号触发 aio_read 中的回调函数
(4)用户进程处理数据
4、用户进程在两个阶段都是非阻塞状态
Redis
1、核心业务部分(命令处理)是单线程
2、整个 Redis 是多线程
3、在 Redis 迭代过程中,引入多线程的支持
(1)Redis 4.0:引入多线程异步处理一些耗时较旧的任务,例如异步删除命令 unlink
(2)Redis 6.0:在核心网络模型中引入多线程,进一步提高对于多核 CPU 利用率
4、对于 Redis 核心网络模型,在 Redis 6.0 之前是单线程,利用 epoll(Linux 系统)I/O 多路复用技术,在事件循环中不断处理客户端情况
5、Redis 选择单线程原因
(1)Redis 纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟,而不是执行速度,因此多线程并不会带来巨大的性能提升
(2)多线程会导致过多的上下文切换,带来不必要的开销
(3)引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣
6、Redis通过 IO 多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件库 API 库 AE
7、Redis 6.0 解析客户端命令、写响应结果时采用多线程;核心的命令执行、IO 多路复用模块依然是由主线程执行
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战