redis高性能之epoll对比select,poll和IO多路复用深度学习
什么是IO多路复用?
- 一个进程处理多个socker连接。
- Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 /O 操作在一般情况下往往不能直接返回,这会导致某一文件的 /O 阻塞导致整个进程无法对其它客户提供服务,而 I/0 多路复用就是为了解决这个问题而出现
- 所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符旦某个描述符就绪 (般是读就绪或写就绪) , 能够通知程序进行相应应用程序只需要在一个阻塞对象上等待,无需的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合多个连接共用-个阻塞对象阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
同步异步阻塞非阻塞组合理解?
-
同步阻塞 排队等待
-
同步 非阻塞 排队等待 并且在做其他事
-
异步阻塞 不排队让出队伍 在旁边等待
-
异步 非阻塞 不排队让出队伍 在旁边坐其他事
BIO:new socket每个线程对应一个socket
问题:内存资源问题
NIO: ArrayList socketList = new ArrayList<>(); 将每次连接加入list容器中,轮询socket轮流解决每个客户端的问题
问题:类似BIO ,如何解决?1.socket遍历的和处理问题的socket一个是用户态,一个是内核态2.我们不想遍历,希望哪个socket要处理问题,就自己报告!>>>>(select-poll-epoll)
IOmultiplexingIO多路复用:
首先File Descriptor
文件描述符(File descriptor) 是计算机操作系统中用来标识已打开的文件或输入/输出设备的抽象概念。在Unix及类Unix系统中,文件描述符是一个非负整数,它作为文件表中的索引,用于访问已打开的文件或设备。
每个进程都有一个文件描述符表(File Descriptor Table),其中记录了该进程打开的文件和设备的信息。文件描述符可以看作是一个指向文件表条目的索引,通过它可以进行读取、写入、关闭等文件操作。
文件描述符一般以 0、1、2 开始,分别对应标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)。其他的文件描述符则由操作系统自动分配和管理。
在C语言中,文件描述符通常使用整数类型的变量表示,常见的函数如open、read、write、close等可以通过文件描述符来操作文件。在网络编程中,也可以使用文件描述符来操作套接字(socket)进行网络通信。
IO multiplexing就是我们说的select, poll,epol,有些技术书籍也称这种10方式为event driven IO事件驱动10。就是通过一种机制,
个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。可以基于一个阻塞对象
并同时在多个描述符上等待就绪 ,而不是使用多个线程(每个文件描述符一个线程,每次new一个线程),这样可以大大节省系统资源。所以,
I/0 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符(套接字描述符》其中的任意一个进入读就绪状态
select,poll,epoll等函数就可以返回。
这里的多个描述符是多个线程吗?
不是
不,I/O多路复用中的多个文件描述符并不是指多个线程。相反,它是为了避免创建多个线程而使用的一种机制。
在传统的多线程模型中,每个文件描述符通常需要对应一个线程来处理读写操作。这样会导致线程数量过多,消耗大量系统资源。
而I/O多路复用机制通过使用select、poll、epoll等函数,可以在一个阻塞对象上等待多个文件描述符就绪,一旦有文件描述符就绪,它可以通知程序进行相应的读写操作。这样 ,一个进程可以同时管理多个文件描述符,而无需为每个文件描述符创建独立的线程。
因此,I/O多路复用是一种在单线程下处理多个文件描述符的技术,它能够节省系统资源,并提高程序的性能和效率。
反应器(Reactor)驱动编程模式: 用于处理输入/输出(I/O)事件。在该模式中,存在一个中心调度器(通常称为反应器),它负责监听和分发各种事件,并调用适当的处理程序进行处理。
反应器模式基于事件循环机制,其主要组件包括以下几个部分:
-
反应器(Reactor):它是一个管理和调度事件的中心组件。它会注册、监听和分发事件,并将事件分派给相应的处理程序进行处理。
-
处理程序(Handler):也称为事件处理器,是实际处理特定事件的组件或回调函数。每个事件类型都有对应的处理程序。
-
事件源(Event Source):产生事件的实体,可以是网络套接字、文件描述符等。事件源会将事件通知给反应器。
-
事件(Event):对发生的特定操作或状态改变的抽象表示。例如,读就绪事件、写就绪事件等。
反应器模式的工作流程如下:
-
反应器初始化,并注册事件源和对应的处理程序。
-
反应器进入事件循环,不断监听事件。
-
当事件源产生事件时,将事件通知给反应器。
-
反应器根据事件类型找到对应的处理程序,并调用该处理程序进行处理。
-
处理程序执行相应的操作,完成后返回。
-
反应器继续监听和处理其他事件,循环往复。
通过使用反应器模式,可以实现高效的事件驱动编程,避免了线程的创建和管理开销,提高了系统的并发性能和可扩展性。在许多框架和库中,比如React、Node.js等,都使用了反应器模式来实现异步、非阻塞的事件处理机制。
在反应器模式中,事件循环(Event Loop)是指一个持续运行的循环结构,用于不断监听和处理事件。它是反应器的核心机制。
事件循环采用轮询(Polling)的方式,周期性地检查是否有事件发生。具体来说,事件循环会执行以下步骤:
-
等待事件:事件循环阻塞等待事件的发生,通常通过系统调用(如select、poll、epoll等)来实现阻塞等待。
-
检查事件:一旦有事件发生(如文件可读、套接字可写等),事件循环会将该事件标记为就绪状态。
-
分发事件:事件循环将就绪的事件分派给相应的处理程序进行处理。这通常涉及在注册表中查找事件对应的处理程序,并调用该处理程序进行响应。
-
处理事件:处理程序执行相应的操作,可能会包括读取、写入数据,执行业务逻辑等。
-
回到步骤1:事件处理完成后,事件循环会继续等待下一个事件的发生,进入下一次循环。
这样,事件循环不断地检查、分发和处理事件,使得程序可以高效地响应多个并发事件而无需创建额外的线程。
需要注意的是,事件循环通常是单线程的,即在一个主线程中执行。它的循环是在同一个线程内反复执行的,而不是指多个线程之间的循环。这使得事件循环模型更容易实现和管理,并且避免了多线程编程中的竞态条件和线程安全性问题。
select 其实就是把NIO中用户态要遍历的fd(File Descriptor)数组(我们的每一socket接) 拷贝到了内核态让内核态来遍历,因为用户态判断socket 是否有数据还是要调用内核态的,所有拷贝到内核态后,这样遍历判断的时候就不用一直用户态和内核态频繁切换了
分析select函数的执行流程
1.select是一个阻塞函数,当没有数据时,会一直阻塞在select那一行。
2.当有数据时会将rset中对应的那一位置为1
3.select数返回,不再阻塞
4.遍历文件描述符数组,判断哪个fd被置位了
5.读取数据,然后处理
select缺点
1.bitmap最大1024位,一个进程最多只能处理1024个客户端
2.&rset不可重用,每次socket有数据就相应的位会被置位
3、文件描述符数组拷贝到了内核态(只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)),仍然有开销。selet调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。 (可优化为不复制)
4、select并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历。select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)
下面是 select 函数的详细执行流程解析:
-
创建 fd_set 集合:用户需要创建三个 fd_set 集合,分别用于监听可读事件、可写事件和异常事件。这些集合可以使用 FD_ZERO 和 FD_SET 宏进行操作,将待监听的文件描述符添加到相应的集合中。
-
设置超时时间:可以使用 timeval 结构体设置超时时间,或者将 timeval 结构体的指针设置为 NULL,表示无限等待,即一直阻塞直到有事件发生。
-
调用 select 函数:将创建的 fd_set 集合作为参数传递给 select 函数,同时传递一个整数值,表示监听的最大文件描述符加 1。
-
阻塞等待事件:在这一步,select 函数会阻塞程序,直到有文件描述符的事件发生、超时或出错。如果有文件描述符就绪,内核会修改相应的 fd_set 集合。
-
检查就绪事件:当 select 函数返回时,需要遍历检查 fd_set 集合,确定哪些文件描述符有事件就绪。
-
处理就绪事件:对于就绪的文件描述符,可以执行相应的操作,如读取数据、写入数据、关闭连接等。
-
循环重复步骤 3-6:如果还有其他文件描述符有事件就绪,可以循环回到步骤 3 继续等待和处理事件。
需要注意的是,select 函数有一些限制,如最大监听的文件描述符数量限制、效率低下(需要遍历整个 fd_set 集合)等。而且 select 采用的是轮询机制,即不断地遍历 fd_set 集合来检查就绪状态,会导致性能下降。
总结来说,select 的执行流程包括创建 fd_set 集合、设置超时时间、调用 select 函数阻塞等待事件、检查就绪事件、处理就绪事件,并可以循环重复这些步骤。在并发量较小的情况下,select 是一个常用的多路复用方法。但对于高并发场景,epoll 在性能上更具优势。
poll的执行流程
1.将五个fd从用户态拷贝到内核态,使用pollfds数组
2.poll为阻塞方法,执行poll方法,如果有数据会将fd对应的revents置为POLLIN
3.poll方法返回
4.循环遍历,查找哪个fd被置位为POLLIN了
5将revents重置为0便于复用
6.对置位的fd进行读取和处理
问题:同上3,4
下面是 poll 的详细执行流程解析:
-
创建 pollfd 数组:用户需要创建一个包含待监听文件描述符的 pollfd 数组,每个 pollfd 结构体包含一个文件描述符和关注的事件类型。
-
调用 poll 函数:将创建的 pollfd 数组作为参数传递给 poll 函数。poll 函数会将该数组从用户态拷贝到内核态,并开始等待事件的发生。
-
阻塞等待事件:在这一步,poll 函数会阻塞程序,直到有文件描述符的事件发生或超时。如果有文件描述符就绪,内核会将相应的 revents 字段置为相应的事件类型,如 POLLIN(可读事件)等。
-
检查就绪事件:当 poll 函数返回时,需要遍历检查 pollfd 数组中的每个文件描述符的 revents 字段,以确定哪些文件描述符有事件就绪。
-
处理就绪事件:对于 revents 字段被置位的文件描述符,可以执行相应的操作,如读取数据、写入数据、关闭连接等。处理完毕后,需要将 revents 字段重置为 0,以便下次复用。
-
循环重复步骤 3-5:如果还有其他文件描述符有事件就绪,可以循环回到步骤 3 继续等待和处理事件。
需要注意的是,poll 方法是阻塞的,即程序会一直等待事件的发生或超时。对于大量的文件描述符,使用 poll 的效率可能较低,因为需要遍历整个 pollfd 数组来检查就绪事件。
总结来说,poll 的执行流程包括创建 pollfd 数组、调用 poll 函数阻塞等待事件、检查就绪事件、处理就绪事件,并可以循环重复这些步骤。与 select 方法相比,poll 提供了更好的可移植性,并且不受文件描述符数量限制。但在高并发场景下,epoll 更具性能优势。
epoll的执行流程
epoll 是非阻塞的 是非阻塞的!!
epoll的执行流程
1.当有数据的时候,会把相应的文件描述符“置位”,但是epoll没有revent标志位,所以并不是真正的置位。这时候会把有数据的文件描述符放到队首。
2.epoll会返回有数据的文件描述符的个数
3.根据返回的个数 读取前N个文件描述符即可
4.读取、处理
下面是 epoll 的详细步骤解析:
-
创建 epoll 实例:使用 epoll_create 或 epoll_create1 函数创建一个 epoll 实例,该实例会返回一个文件描述符,用于后续的操作。
-
注册事件:使用 epoll_ctl 函数将需要监听的文件描述符以及感兴趣的事件注册到 epoll 实例中。可以指定事件类型,如 EPOLLIN(可读事件)、EPOLLOUT(可写事件)等。
-
等待事件:使用 epoll_wait 函数等待事件的发生。epoll_wait 在这一步会阻塞程序,直到有事件发生或超时。
-
检查事件:当有事件发生时,epoll_wait 函数会返回并提供一个可以存储事件的数组(epoll_event 结构体数组)。该数组中包含了就绪的文件描述符和相应的事件信息。
-
处理事件:遍历从 epoll_wait 中返回的 epoll_event 数组,根据每个事件的文件描述符执行相应的操作。可以是读取数据、写入数据、关闭连接等。
-
重复步骤 3-5:处理完当前的事件后,可以再次调用 epoll_wait 进行下一轮的等待和事件处理。如果还有其他事件需要处理,可以继续重复上述步骤。
需要注意的是 ,epoll 使用的是回调机制,只有事件发生时才会通知应用程序。它不需要像 select 和 poll 那样轮询所有的文件描述符,因此在高并发的场景中具有更好的性能表现。
总结来说,epoll 的执行流程包括创建 epoll 实例、注册事件、等待事件发生、检查就绪事件、处理事件,并可以反复重复这些步骤,以实现高效的 I/O 复用。
如何理解epoll中epoll_wait的阻塞?
**
**
对于 epoll_wait 函数来说,它可以实现非阻塞的操作。当调用 epoll_wait 函数时,如果没有就绪的事件发生,epoll_wait 会立即返回,并返回值为 0,不会阻塞程序。这种非阻塞的特性使得应用程序可以同时监视多个文件描述符的就绪状态,而无需依次轮询或阻塞等待每个文件描述符。
在使用 epoll_wait 函数时,可以通过设置 timeout 参数来控制阻塞等待的时间。如果将 timeout 设置为 0,则表示非阻塞模式,即使没有事件发生,epoll_wait 也会立即返回。如果将 timeout 设置为正整数,则表示最长等待的时间,超过设定的时间后如果没有事件发生,epoll_wait 会返回。
与 select 和 poll 不同的是,epoll 使用的是事件通知机制,只有在事件就绪时才会通知应用程序,而不需要应用程序不断地进行轮询。这使得 epoll 在高并发场景中具有更好的性能,因为它能够高效地处理大量的并发连接。
因此,尽管 epoll_wait 函数可以阻塞等待事件发生,但由于其非阻塞的特性以及对事件通知的支持,我们常常将 epoll 归类为一种非阻塞的 I/O 复用机制。
多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 环里多次系统调用,变成了一次系统调用 + 内核层遍历这些文件描述符。