做百度网站需不需要备案,东莞市研发网站建设品牌,设置wordpress数据库用户名密码,海南旅游网网页制作上文select/epoll
在上文《Redis#xff08;09#xff09;| Reactor模式》 思考问题可以使用I/O多路复用技术解决多多客户端TCP连接问题#xff0c;同时也提到为了解决最早期的UNIX系统select调用存在的四个问题。 select(int nfds, fd_set *r, fd_set *w, fd_set *e, stru…上文select/epoll
在上文《Redis09| Reactor模式》 思考问题可以使用I/O多路复用技术解决多多客户端TCP连接问题同时也提到为了解决最早期的UNIX系统select调用存在的四个问题。 select(int nfds, fd_set *r, fd_set *w, fd_set *e, struct timeval *timeout)
这3个bitmap有大小限制FD_SETSIZE通常为1024由于这3个集合在返回时会被内核修改因此我们每次调用时都需要重新设置全量fd集合扫描效率比较低下每次扫描筛选出实际发生时间的fd在读/写比较稀疏的情况下同样存在效率问题
如何解决的呢:
poll传递的不是固定大小的bitmap解决问题1;poll将感兴趣事件和实际发生事件分开了解决问题2系统调用返回的是实际发生相应事件的fd集合解决问题3有状态epoll和kqueue创建一个上下文context解决问题4
什么是 IO 多路复用机制 看上图通俗讲就是I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因是尽量多的提高服务器的吞吐能力。
那么什么是IO多路复用机制呢Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流就是我们经常听到的 select/epoll 机制。简单来说在 Redis 只运行单线程的情况下该机制允许内核中同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达就会交给 Redis 线程处理这就实现了一个 Redis 线程处理多个 IO 流的效果。
下图就是基于多路复用的 Redis IO 模型。图中的多个 FD 就是刚才所说的多个套接字。Redis 网络框架调用 epoll 机制让内核监听这些套接字。此时Redis 线程不会阻塞在某一个特定的监听或已连接套接字上也就是说不会阻塞在某一个特定的客户端请求处理上。正因为此Redis 可以同时和多个客户端连接并处理请求从而提升并发性。 为了在请求到达时能通知到 Redis 线程select/epoll 提供了基于事件的回调机制即针 对不同事件的发生调用相应的处理函数。
那么回调机制是怎么工作的呢 其实select/epoll 一旦监测到 FD 上有请求到达时就会触发相应的事件。 这些事件会被放进一个事件队列Redis 单线程对该事件队列不断进行处理。这样一来Redis 无需一直轮询是否有请求实际发生这就可以避免造成 CPU 资源浪费。同时Redis 在对事件队列中的事件进行处理时会调用相应的处理函数这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理所以能及时响应客户端请求提升 Redis 的响应性能。
我再以连接请求和读数据请求为例具体解释一下。这两个请求分别对应 Accept 事件和 Read 事件Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时就会触发 Accept 事件和 Read 事件此时内核就会回调 Redis 相应的 accept 和 get 函数进行处理。 不过需要注意的是即使你的应用场景中部署了不同的操作系统多路复用机制也是适用的。因为这个机制的实现有很多种既有基于 Linux 系统下的 select 和 epoll 实现也有基于 FreeBSD 的 kqueue 实现以及基于 Solaris 的 evport 实现这样你可以根据 Redis 实际运行的操作系统选择相应的多路复用实现。
所以IO多路复用模型是建立在内核提供的多路分离函数select基础之上的使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。 用户首先将需要进行IO操作的socket添加到select中然后阻塞等待select系统调用返回。当数据到达时socket被激活select函数返回。用户线程正式发起read请求读取数据并继续执行。 从流程上来看使用select函数进行IO请求和同步阻塞模型没有太大的区别甚至还多了添加监视socket以及调用select函数的额外操作效率更差。但是使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket然后不断地调用select读取被激活的socket即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中必须通过多线程的方式才能达到这个目的。 用户线程使用select函数的伪代码描述为
{
select(socket);while(1) {sockets select();for(socket in sockets) {if(can_read(socket)) {read(socket, buffer);process(buffer);}}}
}其中while循环前将socket添加到select监视中然后在while内一直调用select获取被激活的socket一旦socket可读便调用read函数将socket中的数据读取出来。 然而使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求但是每个IO请求的过程还是阻塞的在select函数上阻塞平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求然后去做自己的事情等到数据到来时再进行处理则可以提高CPU的利用率。 IO多路复用模型使用了Reactor设计模式实现了这一机制。这一步可以读上一篇《Redis09| Reactor模式》 。 上图EventHandler抽象类表示IO事件处理器它拥有IO文件句柄Handle通过get_handle获取以及对Handle的操作handle_event读/写等。继承于EventHandler的子类可以对事件处理器的行为进行定制。Reactor类用于管理EventHandler注册、删除等并使用handle_events实现事件循环不断调用同步事件多路分离器一般是内核的多路分离函数select只要某个文件句柄被激活可读/写等select就返回阻塞handle_events就会调用与文件句柄关联的事件处理器的handle_event进行相关操作。 上图所示通过Reactor的方式可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作异步而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时则通知相应的用户线程或执行用户线程的回调函数执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的因此多路IO复用模型也被称为异步阻塞IO模型。注意这里的所说的阻塞是指select函数执行时线程被阻塞而不是指socket。一般在使用IO多路复用模型时socket都是设置为NONBLOCK的不过这并不会产生影响因为用户发起IO请求时数据已经到达了用户线程一定不会被阻塞。
阻塞IOBlocking I/O 虽然还有很多其它的 I/O 模型但是在这里都不会具体介绍。 在linux中默认情况下所有的socket都是blocking一个典型的读操作流程大概是这样 当用户进程调用了recvfrom这个系统调用kernel就开始了IO的第一个阶段准备数据。对于network io来说很多时候数据在一开始还没有到达比如还没有收到一个完整的UDP包这个时候kernel就要等待足够的数据到来。
而在用户进程这边整个进程会被阻塞。当kernel一直等到数据准备好了它就会将数据从kernel中拷贝到用户内存然后kernel返回结果用户进程才解除block的状态重新运行起来。 所以blocking IO的特点就是在IO执行的两个阶段等待数据和拷贝数据两个阶段都被block了。 几乎所有的程序员第一次接触到的网络编程都是从listen()、send()、recv() 等接口开始的使用这些接口可以很方便的构建服务器/客户机的模型。然而大部分的socket接口都是阻塞型的。如下图 实际上除非特别指定几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题如在调用recv(1024)的同时线程将被阻塞在此期间线程将无法执行任何运算或响应任何的网络请求。
先来看一下传统的阻塞 I/O 模型到底是如何工作的当使用 read 或者 write 对某一个文件描述符File Descriptor 以下简称 FD)进行读写时如果当前FD 不可读或不可写整个 Redis 服务就不会对其它的操作作出响应导致整个服务不可用。 这也就是传统意义上的也就是我们在编程中使用最多的阻塞模型 阻塞模型虽然开发中非常常见也非常易于理解但是由于它会影响其他 FD 对应的服务所以在需要处理多个客户端任务的时候往往都不会使用阻塞模型。
I/O 多路复用
在 I/O 多路复用模型中最重要的函数调用就是 select该方法能够同时监控多个文件描述符的可读可写情况当其中的某些文件描述符可读或者可写时select 方法就会返回可读以及可写的文件描述个数。文件事件处理器使用 I/O 多路复用模块同时监听多个 FD当 accept、read、write 和 close 文件事件产生时文件事件处理器就会回调 FD 绑定的事件处理器。虽然整个文件事件处理器是在单线程上运行的但是通过 I/O 多路复用模块的引入实现了同时对多个 FD 读写的监控提高了网络通信模型的性能同时也可以保证整个 Redis 服务实现的简单。Redis 是跑在单线程中的所有的操作都是按照顺序线性执行的但是由于读写操作等待用户输入或输出都是阻塞的所以 I/O 操作在一般情况下往往不能直接返回**这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务**而I/O 多路复用就是为了解决这个问题而出现的。
简述下源码
I/O 多路复用模块封装了底层的 select、epoll、avport 以及 kqueue 这些 I/O 多路复用函数为上层提供了相同的接口。 在这里我们简单介绍 Redis 是如何包装 select 和 epoll 的简要了解该模块的功能整个 I/O 多路复用模块抹平了不同平台上 I/O 多路复用函数的差异性提供了相同的接口
static int aeApiCreate(aeEventLoop *eventLoop)
static int aeApiResize(aeEventLoop *eventLoop, int setsize)
static void aeApiFree(aeEventLoop *eventLoop)
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask)
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)同时因为各个函数所需要的参数不同我们在每一个子模块内部通过一个 aeApiState 来存储需要的上下文信息
// select
typedef struct aeApiState {fd_set rfds, wfds;fd_set _rfds, _wfds;
} aeApiState;// epoll
typedef struct aeApiState {int epfd;struct epoll_event *events;
} aeApiState;这些上下文信息会存储在 eventLoop 的 void *state 中不会暴露到上层只在当前子模块中使用。
封装 select 函数
select 可以监控 FD 的可读、可写以及出现错误的情况。 在介绍 I/O 多路复用模块如何对 select 函数封装之前先来看一下 select 函数使用的大致流程
int fd /* file descriptor */
//初始化一个可读的 fd_set 集合保存需要监控可读性的 FD
fd_set rfds;
//使用 FD_SET 将 fd 加入 rfds
FD_ZERO(rfds);
FD_SET(fd, rfds)
for ( ; ; ) {//调用 select 方法监控 rfds 中的 FD 是否可读select(fd1, rfds, NULL, NULL, NULL);//当 select 返回时检查 FD 的状态并完成对应的操作。if (FD_ISSET(fd, rfds)) {/* file descriptor fd becomes readable */}
}而在 Redis 的 ae_select 文件中代码的组织顺序也是差不多的首先在 aeApiCreate 函数中初始化 rfds 和 wfds
static int aeApiCreate(aeEventLoop *eventLoop) {aeApiState *state zmalloc(sizeof(aeApiState));if (!state) return -1;FD_ZERO(state-rfds);FD_ZERO(state-wfds);eventLoop-apidata state;return 0;
}而 aeApiAddEvent 和 aeApiDelEvent 会通过 FD_SET 和 FD_CLR 修改 fd_set 中对应 FD 的标志位
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {aeApiState *state eventLoop-apidata;if (mask AE_READABLE) FD_SET(fd,state-rfds);if (mask AE_WRITABLE) FD_SET(fd,state-wfds);return 0;
}整个 ae_select 子模块中最重要的函数就是 aeApiPoll它是实际调用 select 函数的部分其作用就是在 I/O 多路复用函数返回时将对应的 FD 加入 aeEventLoop 的 fired 数组中并返回事件的个数
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {aeApiState *state eventLoop-apidata;int retval, j, numevents 0;memcpy(state-_rfds,state-rfds,sizeof(fd_set));memcpy(state-_wfds,state-wfds,sizeof(fd_set));retval select(eventLoop-maxfd1,state-_rfds,state-_wfds,NULL,tvp);if (retval 0) {for (j 0; j eventLoop-maxfd; j) {int mask 0;aeFileEvent *fe eventLoop-events[j];if (fe-mask AE_NONE) continue;if (fe-mask AE_READABLE FD_ISSET(j,state-_rfds))mask | AE_READABLE;if (fe-mask AE_WRITABLE FD_ISSET(j,state-_wfds))mask | AE_WRITABLE;eventLoop-fired[numevents].fd j;eventLoop-fired[numevents].mask mask;numevents;}}return numevents;
}封装 epoll 函数
Redis 对 epoll 的封装其实也是类似的使用 epoll_create 创建 epoll 中使用的 epfd
static int aeApiCreate(aeEventLoop *eventLoop) {aeApiState *state zmalloc(sizeof(aeApiState));if (!state) return -1;state-events zmalloc(sizeof(struct epoll_event)*eventLoop-setsize);if (!state-events) {zfree(state);return -1;}state-epfd epoll_create(1024); /* 1024 is just a hint for the kernel */if (state-epfd -1) {zfree(state-events);zfree(state);return -1;}eventLoop-apidata state;return 0;
}在 aeApiAddEvent 中使用 epoll_ctl 向 epfd 中添加需要监控的 FD 以及监听的事件
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {aeApiState *state eventLoop-apidata;struct epoll_event ee {0}; /* avoid valgrind warning *//* If the fd was already monitored for some event, we need a MOD* operation. Otherwise we need an ADD operation. */int op eventLoop-events[fd].mask AE_NONE ?EPOLL_CTL_ADD : EPOLL_CTL_MOD;ee.events 0;mask | eventLoop-events[fd].mask; /* Merge old events */if (mask AE_READABLE) ee.events | EPOLLIN;if (mask AE_WRITABLE) ee.events | EPOLLOUT;ee.data.fd fd;if (epoll_ctl(state-epfd,op,fd,ee) -1) return -1;return 0;
}由于 epoll 相比 select 机制略有不同在 epoll_wait 函数返回时并不需要遍历所有的 FD 查看读写情况在 epoll_wait 函数返回时会提供一个epoll_event 数组
typedef union epoll_data {void *ptr;int fd; /* 文件描述符 */uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event {uint32_t events; /* Epoll 事件 */epoll_data_t data;
};其中保存了发生的 epoll 事件EPOLLIN、EPOLLOUT、EPOLLERR 和 EPOLLHUP以及发生该事件的 FD。 aeApiPoll 函数只需要将epoll_event 数组中存储的信息加入 eventLoop 的 fired 数组中将信息传递给上层模块
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {aeApiState *state eventLoop-apidata;int retval, numevents 0;retval epoll_wait(state-epfd,state-events,eventLoop-setsize,tvp ? (tvp-tv_sec*1000 tvp-tv_usec/1000) : -1);if (retval 0) {int j;numevents retval;for (j 0; j numevents; j) {int mask 0;struct epoll_event *e state-eventsj;if (e-events EPOLLIN) mask | AE_READABLE;if (e-events EPOLLOUT) mask | AE_WRITABLE;if (e-events EPOLLERR) mask | AE_WRITABLE;if (e-events EPOLLHUP) mask | AE_WRITABLE;eventLoop-fired[j].fd e-data.fd;eventLoop-fired[j].mask mask;}}return numevents;
}子模块的选择
因为 Redis 需要在多个平台上运行同时为了最大化执行的效率与性能所以会根据编译平台的不同选择不同的 I/O 多路复用函数作为子模块提供给上层统一的接口在 Redis 中我们通过宏定义的使用合理的选择不同的子模块
#ifdef HAVE_EVPORT
#include ae_evport.c
#else
#ifdef HAVE_EPOLL
#include ae_epoll.c
#else
#ifdef HAVE_KQUEUE
#include ae_kqueue.c
#else
#include ae_select.c
#endif
#endif
#endif因为 select 函数是作为 POSIX 标准中的系统调用在不同版本的操作系统上都会实现所以将其作为保底方案 Redis 会优先选择时间复杂度为 的 I/O 多路复用函数作为底层实现包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue上述的这些函数都使用了内核内部的结构并且能够服务几十万的文件描述符。 但是如果当前编译环境没有上述函数就会选择 select 作为备选方案由于其在使用时会扫描全部监听的描述符所以其时间复杂度较差 并且只能同时服务 1024 个文件描述符所以一般并不会以 select 作为第一方案使用。
select/poll/epoll区别 共同点就是大家都是io多路复用不同点就是epoll采用事件驱动的方式。使得连接没有上限。
总结
Redis 对于 I/O 多路复用模块的设计非常简洁通过宏保证了 I/O 多路复用模块在不同平台上都有着优异的性能将不同的 I/O 多路复用函数封装成相同的 API 提供给上层使用。整个模块使 Redis 能以单进程运行的同时服务成千上万个文件描述符避免了由于多进程应用的引入导致代码实现复杂度的提升减少了出错的可能性。