苏州网站建设凡科,国家653建筑工程网,网站的转化率,wordpress数据库清理文章目录 用户空间 内核空间阻塞IO非阻塞IO信号驱动IO异步IOIO多路复用selectpollepoll Web服务流程Redis 网络模型Redis单线程网络模型的整个流程Redis多线程网络模型的整个流程 用户空间 内核空间
为了避免用户应用导致冲突甚至内核崩溃#xff0c;用… 文章目录 用户空间 内核空间阻塞IO非阻塞IO信号驱动IO异步IOIO多路复用selectpollepoll Web服务流程Redis 网络模型Redis单线程网络模型的整个流程Redis多线程网络模型的整个流程 用户空间 内核空间
为了避免用户应用导致冲突甚至内核崩溃用户应用与内核是分离的
进程的寻址空间会划分为两部分内核空间、用户空间用户空间只能执行受限的命令Ring3而且不能直接调用系统资源必须通过内核提供的接口来访问内核空间可以执行特权命令Ring0调用一切系统资源 阻塞IO
顾名思义阻塞IO就是两个阶段都必须阻塞等待 阶段一
用户进程尝试读取数据比如网卡数据此时数据尚未到达内核需要等待数据此时用户进程也处于阻塞状态
阶段二
数据到达并拷贝到内核缓冲区代表已就绪将内核数据拷贝到用户缓冲区拷贝过程中用户进程依然阻塞等待拷贝完成用户进程解除阻塞处理数据 非阻塞IO
顾名思义非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程
阶段一
用户进程尝试读取数据比如网卡数据此时数据尚未到达内核需要等待数据返回异常给用户进程用户进程拿到error后再次尝试读取循环往复直到数据就绪
阶段二
将内核数据拷贝到用户缓冲区拷贝过程中用户进程依然阻塞等待拷贝完成用户进程解除阻塞处理数据 可以看到非阻塞IO模型中用户进程在第一个阶段是非阻塞第二个阶段是阻塞状态。虽然是非阻塞但性能并没有得到提高。而且忙等机制会导致CPU空转CPU使用率暴增
信号驱动IO
信号驱动IO是与内核建立SIGIO的信号关联并设置回调当内核有FD就绪时会发出SIGIO信号通知用户期间用户应用可以执行其它业务无需阻塞等待。
阶段一
用户进程调用sigaction注册信号处理函数内核返回成功开始监听FD用户进程不阻塞等待可以执行其它业务当内核数据就绪后回调用户进程的SIGIO处理函数
阶段二
收到SIGIO回调信号调用recvfrom读取内核将数据拷贝到用户空间用户进程处理数据 当有大量IO操作时信号较多SIGIO处理函数不能及时处理可能导致信号队列溢出而且内核空间与用户空间的频繁信号交互性能也较低。
异步IO
异步IO的整个过程都是非阻塞的用户进程调用完异步API后就可以去做其它事情内核等待数据就绪并拷贝到用户空间后才会递交信号通知用户进程.
阶段一
用户进程调用aio_read创建信号回调函数内核等待数据就绪用户进程无需阻塞可以做任何事情
阶段二
内核数据就绪内核数据拷贝到用户缓冲区拷贝完成内核递交信号触发aio_read中的回调函数用户进程处理数据 判断同步还是异步的依据
IO多路复用
无论是阻塞IO还是非阻塞IO用户应用在第一阶段都需要调用recvfrom来获取数据差别在于无数据时的处理方案
如果调用recvfrom时恰好没有数据阻塞IO会使CPU阻塞非阻塞IO使CPU空转都不能充分发挥CPU的作用。如果调用recvfrom时恰好有数据则用户进程可以直接进入第二阶段读取并处理数据
而在单线程情况下只能依次处理IO事件如果正在处理的IO事件恰好未就绪数据不可读或不可写线程就会被阻塞所有IO事件都必须等待性能自然会很差。
文件描述符File Descriptor简称FD是一个从0 开始的无符号整数用来关联Linux中的一个文件。在Linux中一切皆文件例如常规文件、视频、硬件设备等当然也包括网络套接字Socket。
IO多路复用是利用单个线程来同时监听多个FD并在某个FD可读、可写时得到通知从而避免无效的等待充分利用CPU资源
select
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位代表一个fd0代表未就绪1代表就绪__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];// ...
} fd_set;// select函数用于监听fd_set也就是多个fd的集合
int select(int nfds, // 要监视的fd_set的最大fd 1fd_set *readfds, // 要监听读事件的fd集合fd_set *writefds,// 要监听写事件的fd集合fd_set *exceptfds, // // 要监听异常事件的fd集合// 超时时间null-用不超时0-不阻塞等待大于0-固定等待时间struct timeval *timeout
);这里对参数作以下说明
nfds要监听的文件描述符的最大值 1目的告诉内核服务组要遍历的最大范围readfds、writefds、exceptfds:分别表示需要监听的事件集合。需要监听fd的哪一个操作就将对应的文件描述都写入到相应的集合timeout设置select的等待时间 ①NULL表示select没有timeoutselect将一直被阻塞直到某个文件描述符上发生了事件 ②0仅检测描述符集合的状态然后立即返回并不等待外部事件的发生 ③特定的时间值如果在指定时间内没有事件发生select将超时返回fd_set:形式上来讲这个结构是一个整数数组但是实际上是按照位图的思想来使用的。使用位图中对应的比特位来表示要监视的文件描述符
阶段一
用户进程调用select指定要监听的FD集合内核监听FD对应的多个socket任意一个或多个socket数据就绪则返回readable此过程中用户进程阻塞
阶段二
用户进程找到就绪的socket依次调用recvfrom读取数据内核将数据拷贝到用户空间用户进程处理数据 下面通过一个例子来说明 假设我们现在需要监听的fd为125并且内核中监听到了fd1的文件描述符上有事件发生那么我们会得到如下的结果 紧接着内核需要将更新后的位图信息copy到用户空间中的位图中这样用户空间通过遍历位图就可以得到就绪的文件描述符进而做相关业务处理。
而后需要继续监听刚开始的125号文件描述符的时候就需要我们手动设置位图内容后才能正常监听。
select模式存在的问题
需要将整个fd_set从用户空间拷贝到内核空间select结束还要再次拷贝回用户空间select无法得知具体是哪个fd就绪需要遍历整个fd_setfd_set监听的fd数量有限每次调用select都需要手动设置fd集合从接口使用角度来讲非常不便
poll
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 // 超时时间
);IO流程
创建pollfd数组向其中添加关注的fd信息数组大小自定义调用poll函数将pollfd数组拷贝到内核空间转链表存储无上限内核遍历fd判断是否就绪数据就绪或超时后拷贝pollfd数组到用户空间返回就绪fd数量n用户进程判断n是否大于0大于0则遍历pollfd数组找到就绪的fd
与select的对比
select模式中的fd_set大小固定为sizeof(fd_set)而pollfd在内核中采用链表理论上无上限监听FD越多每次遍历消耗时间也越久性能反而会下降
epoll
epoll模式是对select和poll的改进它提供了三个函数
struct eventpoll {//...struct rb_root rbr; // 一颗红黑树记录要监听的FDstruct 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、DELint fd, // 要监听的FDstruct epoll_event *event // 要监听的事件类型读、写、异常等
);// 3.检查rdlist列表是否为空不为空则返回就绪的FD的数量
int epoll_wait(int epfd, // epoll实例的句柄struct epoll_event *events, // 空event数组用于接收就绪的FDint maxevents, // events数组的最大长度int timeout // 超时时间-1用不超时0不阻塞大于0为阻塞时间
);我们可以看到。epoll中是将select的功能进行了拆分拆分为epoll_ctl 和 epoll_wait。 其中epoll_ctl是将一个FD添加到epoll的红黑树中并设置ep_poll_callback 在callback触发时就把对应的FD加入到rdlist 这个 绪列表中。 epoll_wait主要是检查rdlist列表是否为空不为空则返回就绪的FD的数量
总结
select模式存在的问题
能监听的FD最大不超过sizeof(fd_set)每次select都需要把所有要监听的FD都拷贝到内核空间每次都要遍历所有FD来判断就绪状态每次调用select都需要手动设置fd集合从接口使用角度来讲非常不便
poll模式的问题
和select函数一样poll返回后需要轮询pollfd来获取就绪的文件描述符每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中同时连接的大量客户端在一时刻可能只有很少的处于就绪状态因此随着监视的文件描述符数量的增长其效率也会线性下降
epoll模式中如何解决这些问题的
基于epoll实例中的红黑树保存要监听的FD理论上无上限而且增删改查效率都非常高每个FD只需要执行一次epoll_ctl添加到红黑树以后每次epol_wait无需传递任何参数无需重复拷贝FD到内核空间利用ep_poll_callback机制来监听FD状态无需遍历所有FD因此性能不会随监听的FD数量增多而下降
事件通知机制 当FD有数据可读时我们调用epoll_wait或者select、poll可以得到通知。但是事件通知的模式有两种
LevelTriggered简称LT也叫做水平触发。只要某个FD中有数据可读每次调用epoll_wait都会得到通知。EdgeTriggered简称ET也叫做边沿触发。只有在某个FD有状态变化时调用epoll_wait才会被通知。
举个栗子
假设一个客户端socket对应的FD已经注册到了epoll实例中客户端socket发送了2kb的数据服务端调用epoll_wait得到通知说FD就绪服务端从FD读取了1kb数据回到步骤3再次调用epoll_wait形成循环
结果 如果我们采用LT模式因为FD中仍有1kb数据则第⑤步依然会返回结果并且得到通知
如果我们采用ET模式因为第③步已经消费了FD可读事件第⑤步FD状态没有变化因此epoll_wait不会返回数据无法读取客户端响应超时。
LT是epoll的默认行为使用ET能够减少epoll触发的次数但是代价就是要求程序员一次响应就绪过程中就把所有的数据都处理完
相当于一个文件描述符就绪后不会反复被提示就绪看起来就是比LT更高效一些但是在LT情况下如果也能做到每次就绪文件描述符都立刻处理不让这个就绪信息被重复提示的话其实性能也是一样的。另一方面。ET的代码复杂度相比之下更高。
Web服务流程
基于epoll模式的web服务的基本流程如图
epoll_create创建实例 ①创建红黑树用于记录要监听的fd ②创建链表用于记录就绪的fd创建serverSocket,得到对应的fd调用epoll_ctl监听步骤2中获取到的fd ①将fd注册到步骤1中的红黑树中 ②注册相应的回调函数在fd就绪时通过该函数处理调用epoll_wait等待fd就绪判断就绪事件类型根据类型作出相应的处理
Redis 网络模型
问题引入 Q1Redis到底是单线程还是多线程
如果仅仅聊Redis的核心业务部分命令处理答案是单线程如果是聊整个Redis那么答案就是多线程在Redis版本迭代过程中在两个重要的时间节点上引入了多线程的支持 ①Redis v4.0引入多线程异步处理一些耗时较旧的任务例如异步删除命令unlink ②Redis v6.0在核心网络模型中引入 多线程进一步提高对于多核CPU的利用率
因此对于Redis的核心网络模型在Redis 6.0之前确实都是单线程。是利用epollLinux系统这样的IO多路复用技术在事件循环中不断处理客户端情况
Q2为什么Redis要选择单线程
抛开持久化不谈Redis是纯内存操作执行速度非常快它的性能瓶颈是网络延迟而不是执行速度因此多线程并不会带来巨大的性能提升。多线程会导致过多的上下文切换带来不必要的开销引入多线程会面临线程安全问题必然要引入线程锁这样的安全手段实现复杂度增高而且性能也会大打折扣
Redis单线程网络模型的整个流程 根据上面的模型结合web服务流程我们对其进行详细展开 server Socket初始化阶段 上图源码中包含了server socket的创建IO多路复用模型的epoll的初始化客户端连接server的处理函数注册客户端读、写事件 处理函数的注册 客户端读事件触发流程 ①client_server 发出读请求 ②aeApiPoll监听到该就绪事件 ③触发命令请求处理器readQueryFromClient ④将请求写入输入缓冲区c-queryBuf ⑤解析queryBuf数据为redis 命令 ⑥选择并执行命令将结果视情况写入到buf或者reply。(c-buf可以写的下就写否则写到c-reply这是一个链表理论无上限) ⑦将客户端添加到server.clients_pending_write队列。等待被命令回复处理器sendReplyToClient处理返回
Redis多线程网络模型的整个流程
Redis 6.0版本中引入了多线程目的是为了提高IO读写效率。
因此在解析客户端命令、写响应结果时采用了多线程。
核心的命令执行、IO多路复用模块依然是由主线程执行。