700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > Redis之I/O多路复用技术(multiplexing)

Redis之I/O多路复用技术(multiplexing)

时间:2020-07-30 23:40:24

相关推荐

Redis之I/O多路复用技术(multiplexing)

在面试中通常会有这样子的场景

↓↓↓↓↓↓

面试官:看你的简历写到项目中有用到redis,可以聊聊redis吗

求职者:可以哇。我在项目中主要使用了redis做商品信息的缓存,我会先从缓存中拿商品信息,如果缓存失效了再去数据库拿商品信息,最后更新缓存,这样子做直接提高了程序的性能并减少了DB的压力。

求职者:redis很快,主要是因为完全基于内存,而且是单线程,使用了I/O多路复用模型。

面试官:那你了解I/O多路复用技术在redis中的应用吗

求职者:不是很了解。。。

在了解I/O多路复用之前,我们可以先了解一下传统的I/O模型

同步阻塞I/O模型

首先,要从你常用的IO操作谈起,比如read和write,通常IO操作都是阻塞I/O的,也就是说当你调用read时,如果没有数据收到,那么线程或者进程就会被挂起,直到收到数据。无法处理并发

同步非阻塞IO模型

当你调用read时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回一个错误,这样是不会阻塞线程了。此时用户进程需要不断轮询,如果轮询频繁,则浪费了大量的CPU资源;如果轮询频率低,则不能实时地获取数据。过度浪费CPU资源

异步IO模型

当调用read时,不需要等到数据返回就可以继续去干别的事情,调用write的时候同理。但是频繁的切换线程会出现浪费过多CPU资源的问题。

多路复用I/O模型

这里"多路"指的是多个网络连接,"复用"指的是复用同一个线程。

多路复用I/O模型是一种同步I/O模型。实现一个线程监听多个文件句柄(也叫做文件描述符,FileDescription,简称FD),当有一个FD就绪时,则通知对应的应用程序进行读写操作。当没有FD就绪时,就会阻塞并交出CPU。

多路复用I/O模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作(比如多线程之间的切换,频换的切换线程会造成大量的资源浪费)。

采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)。

大概对这张图做一个解释:

当进程调用select,进程就会被阻塞此时内核会监视所有select负责的的socket,当socket的数据准备好后,就立即返回。进程再调用read操作,数据就会从内核拷贝到进程。

Redis为什么要引入多路复用I/O技术

I/O多路复用的本质是同步阻塞I/O模型,但是,它最大的优势在于可以在一次阻塞中监听多个文件描述符(FD)。我们带入redis的场景,来思考一下redis为什么使用多路复用I/O技术

首先采用普通的同步阻塞I/O,那么Redis可能会在一个客户端上长期阻塞。该客户端可能长期没有数据到达,而Redis需要处理多个客户端的通信,当其他客户端有请求到达时,Redis则无法处理了,这显然是无法接受的。如果使用同步非阻塞I/O,那么就需要不断轮循客户端,那么这种频繁的轮循会很浪费CPU资源,如果轮循不频繁,那么可能就会出现数据不能实时获取的问题。如果使用异步IO模型,线程的创建和频繁的上下文切换会浪费更多的资源。其次Redis本身就是单进程单线程的模式工作,多线程等待多个客户端显然与其系统思想不符。

综上,多路复用I/O技术是首选。

IO多路复用的三种实现方式
select函数 单个进程所打开的FD是有限制的,通过FD_SETSIZE设置,默认1024每次调用select,都需要把FD集合从用户态拷贝到内核态,这个开销在FD很多时会很大对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时) poll函数 poll与select相比,只是没有FD的限制,其它基本一样 epoll函数 epoll只能在linux下工作epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是"高速"模式 LT模式下,只要这个FD还有数据可读,每次epoll_wait都会返回它的事件,提醒用户程序去操作ET模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论FD中是否还有数据可读。所以在ET模式下,read一个FD的时候一定要把它的buffer读完,或者遇到EAGAIN错误
epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll的操作过程

epoll操作过程需要三个接口,分别如下:

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

int epoll_create(int size);

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。

当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);函数是对指定描述符fd执行op操作。 epfd:是epoll_create()的返回值op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件fd:是需要监听的fd(文件描述符)epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:

struct epoll_event {__uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */};//events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);EPOLLOUT:表示对应的文件描述符可以写;EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待epfd上的io事件,最多返回maxevents个事件。该函数返回需要处理的事件数目,如返回0表示已超时。 参数events用来从内核得到事件的集合maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。

epoll 之所以做到了高效,最关键的两点
内部管理 fd 使用了高效的红黑树结构管理,做到了增删改之后性能的优化和平衡epoll 池添加 fd 的时候,调用 file_operations->poll ,把这个 fd 就绪之后的回调路径安排好。通过事件通知的形式,做到最高效的运行epoll 池核心的两个数据结构:红黑树和就绪列表。红黑树是为了应对用户的增删改需求,就绪列表是 fd 事件就绪之后放置的特殊地点,epoll 池只需要遍历这个就绪链表,就能给用户返回所有已经就绪的 fd 数组
事件处理器

监听到有读、写事件发生以后,接着要进行事件处理。Redis会为每个监听的文件描述符绑定一个读事件处理器或者写事件处理器,当事件发生时,会执行相应的事件处理函数。对于不同的客户端绑定的事件处理器可能会不相同。

文件事件(file event):Redis服务器通过套接字与客户端(或其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作时间事件(time event):Redis服务器的一些操作(比如serverCron函数)需要在给定的时间执行,而时间事件就是服务器对这类定时操作的抽象

文件事件处理器
文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的时间处理器来处理这些事件
时间事件处理器

时间事件分为以下两类:

定时事件:让一段程序在指定的时间之后执行一次,比如,让程序A在当前时间30秒后执行一次周期性事件:让一段程序每隔指定时间就执行一次,比如,让程序B每隔30秒就执行一次

时间事件主要有以下三个属性构成:

id:服务器为时间事件创建的全局唯一ID(标识),ID按从小到大的顺序递增,新事件的ID号比旧事件的ID号要大when:毫秒精度的UNIX时间戳,记录了时间事件的到达(arrive)时间timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件

总结
Redis使用的是同步阻塞I/O模型,I/O多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。
参考
/subjects/np/introduction/unix_network_programming_v1.3/ch06lev1sec2.html/a/1190000003063859

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。