小米C++软开二面:select、poll、epoll 三者之间的区别,epoll 为什么高效?
IO 多路复用技术允许一个线程同时监视多个文件描述符(可以理解为每个客人的 “服务需求信号”),当其中任何一个或多个文件描述符就绪(有数据可读、可写或发生异常,就好比客人有新的需求,如点菜、加饮料等)时,内核就会通知应用程序进行相应的处理 。这样,应用程序就无需为每个文件描述符单独创建线程,避免了线程资源的浪费和频繁的上下文切换,大大提高了系统的效率和性能。
在 Linux 系统中,常见的 IO 多路复用技术有 select、poll 和 epoll。它们就像是不同等级的 “服务助手”,虽然都能实现一个线程处理多个 I/O 流的功能,但在性能、使用方式和适用场景上却各有千秋。接下来,就让我们深入了解一下这三位 “服务助手”。
一、生活实例举例
在正式介绍 select、poll、epoll 之前,我们先来设想一个场景。假如你是一个便利店老板,你的店里有很多顾客进进出出,你需要关注每个顾客的行为,比如是否有新顾客进门,是否有顾客需要结账,是否有顾客存在偷窃行为。在这个场景中,顾客就相当于文件描述符(fd),而你关注的这些行为就相当于文件描述符上发生的事件。
select 方式:select 就像是你雇了一个保安,这个保安会帮你盯着所有的顾客。他会告诉你有顾客发生了某些事件,但是他不会告诉你具体是哪个顾客发生了什么事件。所以,当保安通知你有事件发生时,你就需要一个个地去问每个顾客 “你是不是要结账了?”“你是不是有什么问题?”,直到找到那个发生事件的顾客。而且,这个保安一次最多只能帮你盯着 1024 个顾客。poll 方式:poll 也像是一个保安,他和 select 保安的功能差不多,也是会告诉你有顾客发生了事件,但同样不会告诉你具体是哪个顾客。不过,poll 保安比 select 保安厉害的地方在于,他没有顾客数量的限制,理论上可以帮你盯着无数个顾客。但是,当他通知你有事件发生时,你还是得像问 select 保安那样,一个个地去问顾客发生了什么事。epoll 方式:epoll 则像是一个超级智能监控系统,这个系统不仅能监控无数个顾客,而且当有顾客发生事件时,它会直接告诉你是哪个顾客发生了什么事件。比如,它会直接告诉你 “3 号顾客要结账了”“5 号顾客有偷窃行为”,你可以直接根据这些信息去处理相应的事件,而不需要再一个个地去询问顾客。二、select:基础但有限的 “监控”
2.1 select的工作原理
select 是最早被广泛使用的 I/O 多路复用技术,它就像是一个基础款的 “服务助手”,在网络编程的历史长河中,曾发挥过重要的作用 ;select 函数的使用需要传入三个文件描述符集合(可读集合 readfds、可写集合 writefds、异常集合 exceptfds),分别用于监听文件描述符的读、写和异常事件。同时,还需要传入一个超时时间参数 timeout,用于控制 select 函数的阻塞时间。
在使用 select 之前,我们需要先初始化这三个文件描述符集合,将需要监听的文件描述符添加到相应的集合中。例如,在一个简单的 TCP 服务器中,我们可能会将监听套接字添加到读集合中,以监听客户端的连接请求。
当我们调用 select 函数时,它会将这三个文件描述符集合从用户空间拷贝到内核空间,然后内核会遍历这些文件描述符,检查是否有任何一个文件描述符上有事件发生(例如,是否有数据可读、是否可以写入数据、是否发生异常等)。如果有事件发生,内核会将对应的文件描述符在相应的集合中标记出来,然后将这三个集合从内核空间拷贝回用户空间 。select 函数返回后,我们需要再次遍历这三个文件描述符集合,使用 FD_ISSET 宏来检查哪些文件描述符上有事件发生,并进行相应的处理。例如,如果某个文件描述符在可读集合中被标记,那么我们就可以对该文件描述符进行读操作。
select 的工作方式就像是一个大管家,它通过检查一组文件描述符(fd)的状态,来判断是否有 I/O 事件发生。它维护了三个集合:读集合、写集合和异常集合。在调用 select 时,我们把需要监控的 fd 分别加入到这三个集合中,然后 select 就会去检查这些 fd 是否有可读、可写或异常的事件发生。如果有,select 就会返回,告诉我们哪些 fd 有事件发生。为了更形象地理解,我们可以把 fd 想象成一个个的小房间,每个房间都可能发生一些事件,比如有人敲门(可读事件)、可以把东西送进去(可写事件)或者房间里发生了异常情况(异常事件)。select 就像是一个保安,它会在所有的房间外面巡逻,检查每个房间的情况。如果有房间发生了事件,保安就会回来告诉我们是哪些房间发生了事件。
2.2 select 的特点与局限性
尽管 select 曾经在网络编程中占据重要地位,但随着高并发需求的不断增长,它的缺点也逐渐暴露出来,就像一位能力有限的 “服务助手”,在面对大规模的 “客人” 时,显得力不从心。
首先,select 对单个进程可监视的文件描述符数量存在限制,这个限制通常是 1024 个(在 Linux 系统中,可通过修改宏定义 FD_SETSIZE 来改变这个值,但这并不是一个理想的解决方案,因为它会带来兼容性和维护性的问题)。在如今的高并发场景下,一个服务器可能需要同时处理成千上万的连接,1024 个文件描述符的限制显然无法满足需求。例如,一个大型的在线游戏服务器,可能需要同时处理数万玩家的连接,如果使用 select,根本无法实现如此大规模的连接管理。
其次,select 每次调用都需要将整个文件描述符集合从用户空间拷贝到内核空间,在内核检查完事件后,又需要将结果从内核空间拷贝回用户空间。这种频繁的数据拷贝操作会带来较大的开销,降低系统的性能。想象一下,每次服务员都要将所有客人的信息在两个地方来回传递,不仅浪费时间,还容易出错。
最后,select 返回后,我们需要遍历整个文件描述符集合,才能找到哪些文件描述符上有事件发生,这个过程的时间复杂度为 O (n)。当文件描述符数量较多时,遍历的时间开销会非常大,导致系统的响应速度变慢。例如,当有 1000 个文件描述符时,select 返回后,我们需要进行 1000 次检查,才能确定哪些文件描述符就绪,这无疑会消耗大量的时间和资源。
2.3 select的适用场景
由于 select 的局限性,它更适用于小规模连接和处理简单的并发场景。比如一些简单的服务器程序,并发连接数较少,对性能要求不是特别高,这时使用 select 就可以满足需求,因为它代码实现简单,易于理解和使用 。就像一个小商店,顾客数量不多,老板自己就可以轻松地照顾到每个顾客的需求,不需要太复杂的管理方式。
三、poll:改进但仍有不足的 “监控”
3.1 poll 的工作原理
poll 是对 select 的改进,它就像是 select 的进阶版 “服务助手”,在功能上与 select 有许多相似之处,但也有一些自己的特点;poll 同样用于监听多个文件描述符上的事件,以确定哪些文件描述符可以进行读、写或发生了异常等操作 。与 select 不同的是,poll 使用一个 pollfd 结构体数组来表示文件描述符集合。每个 pollfd 结构体包含三个成员:fd(文件描述符)、events(表示需要监听的事件,如 POLLIN 表示可读事件,POLLOUT 表示可写事件等)和 revents(表示实际发生的事件,由内核在 poll 函数返回时设置)。
下面是一个使用 poll 的简单示例代码,展示了如何使用 poll 监听套接字的读事件:
在使用 poll 时,我们将需要监听的文件描述符及其感兴趣的事件填充到 pollfd 数组中,然后调用 poll 函数。poll 函数会将这个数组从用户空间拷贝到内核空间,内核会遍历这个数组,检查每个文件描述符上的事件是否发生 。如果有事件发生,内核会将对应的文件描述符在 revents 成员中标记出来,然后将数组从内核空间拷贝回用户空间 ;poll 函数返回后,我们通过检查 pollfd 数组中每个元素的 revents 成员,来确定哪些文件描述符上有事件发生,并进行相应的处理。例如,如果某个文件描述符的 revents 成员中设置了 POLLIN 标志,就表示该文件描述符有数据可读。
poll的工作方式是,将用户传入的pollfd数组拷贝到内核空间,然后内核会遍历这个数组,查询每个fd对应的设备状态。如果设备状态满足events中设置的事件(比如可读、可写、异常等),就会在revents中标记该事件。当所有的fd都检查完后,poll函数返回,返回值表示有多少个fd的状态发生了变化。
我们还是以便利店的例子来说明,poll就像是一个升级版的保安,他手里拿着一个顾客信息表(pollfd数组),上面记录了每个顾客的信息以及需要关注的事件(events)。保安会逐个检查每个顾客的情况,然后把发生了事件的顾客信息记录在表格的另一栏(revents)中。最后,保安把这个表格交还给老板,老板可以根据表格上的信息知道哪些顾客发生了什么事件。
3.2 poll 相对 select 的改进
poll 相对于 select 有一些明显的改进,这使得它在处理高并发场景时比 select 更具优势,就像一位经过培训提升的 “服务助手”,能力有所增强。
首先,poll 解决了 select 文件描述符数量的限制问题。在 select 中,由于使用固定大小的 fd_set 来表示文件描述符集合,导致单个进程可监视的文件描述符数量受到 FD_SETSIZE 的限制(通常为 1024)。而 poll 使用 pollfd 结构体数组,理论上对文件描述符的数量没有限制(实际受限于系统资源,如内存等),这使得它能够处理更多的并发连接。例如,在一个需要支持大量并发连接的网络服务器中,poll 可以轻松应对成千上万的客户端连接,而 select 则会因为文件描述符数量的限制而无法满足需求。
其次,poll 在编程接口上更加灵活和简洁。select 需要分别处理读、写和异常三个文件描述符集合,并且每次调用 select 之前都需要重新设置这些集合。而 poll 将监听事件和返回事件合并在一个 pollfd 结构体中,只需要维护一个数组,使用起来更加方便。例如,在添加或修改一个文件描述符的监听事件时,poll 只需要修改 pollfd 数组中对应元素的 events 成员即可,而 select 则需要分别在三个文件描述符集合中进行操作,代码更加繁琐。
3.3 poll的特点与局限性
poll 相比于 select 有一些优点:
无连接数限制:理论上对文件描述符的数量没有限制,不像 select 受限于FD_SETSIZE。这就好比保安可以管理无数个顾客,而没有数量上限。接口使用方便:pollfd结构体中包含了要监视的events和发生的revents,对于监听集合的输入输出是分离的,不需要在调用poll函数之前重新对参数进行设置 。不像 select,每次调用都要重新设置fd_set集合。这就好比保安每次巡逻回来,不需要重新整理顾客信息表,只需要查看哪些顾客发生了事件即可。然而,poll 也存在一些局限性:
效率问题:虽然 poll 没有了文件描述符数量的限制,但它和 select 一样,每次调用都需要遍历整个文件描述符集合,时间复杂度为 O (n)。当文件描述符数量很多时,效率仍然较低。就像保安检查顾客时,还是需要一个个地检查,不管顾客是否有事件发生,都要检查一遍,当顾客数量很多时,这个过程会非常耗时。数据复制开销:每次调用 poll 都需要将pollfd数组从用户态拷贝到内核态,当fd很多时,这个开销也不容小觑。这就好比每次保安巡逻都需要把顾客信息表从老板那里拿到自己手里,这个过程也会消耗一定的时间和精力。触发方式单一:poll 只支持水平触发(LT),这意味着只要文件描述符上还有未处理的数据,它就会一直通知应用程序,可能会导致应用程序重复处理一些事件。而在一些场景下,边缘触发(ET)会更加高效,它只会在文件描述符状态发生变化时通知应用程序。3.4 poll 的适用场景
由于 poll 没有连接数限制且使用相对简单,适用于连接数较多但活跃连接比例不确定的场景 。例如,一些即时通讯服务器,可能会有大量的用户连接,但并不是每个用户都会频繁发送消息,这时使用 poll 就可以较好地管理这些连接。在这种场景下,poll 的无连接数限制可以满足大量用户连接的需求,而水平触发虽然可能会导致一些重复通知,但对于即时通讯这种对实时性要求较高的场景来说,影响相对较小 。就像便利店在节假日可能会迎来大量顾客,虽然不是每个顾客都会马上有需求,但使用 poll 这种方式可以有效地管理这些顾客,及时响应有需求的顾客。
四、epoll:高效强大的 “监控”
在 select 和 poll 逐渐难以满足高并发需求的情况下,epoll 这位 “王者选手” 闪亮登场,它就像是一位拥有超能力的 “超级服务助手”,专为解决高并发场景下的 I/O 处理难题而生 。
epoll 是 Linux 内核为处理大批量文件描述符而改进的 poll,是 select/poll 的增强版本,诞生于 Linux 2.5.44 内核版本 。它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率 。epoll 采用了与 select 和 poll 截然不同的设计理念,摒弃了传统的轮询方式,采用事件驱动机制,使得它在高并发场景下能够如鱼得水,高效地处理大量的 I/O 请求。
4.1 epoll的工作方式
①水平触发(LT):持续通知的 “贴心管家”水平触发(LT)可是 epoll 的默认工作模式,就像一位贴心管家,时刻关注着文件描述符的状态。当某个文件描述符处于就绪状态,比如有数据可读或者可写,内核就会通知应用程序。要是应用程序这次没处理完数据,或者没来得及处理,别担心,下次调用 epoll_wait 时,内核依旧会不厌其烦地再次通知,直到数据被处理完或者缓冲区里没数据可读、可写了为止。
举个例子,在处理 HTTP 报文时,数据可能是一段段陆续到达的。使用 LT 模式,只要缓冲区还有没读完的报文片段,每次 epoll_wait 都会把对应的文件描述符事件返回,让应用程序可以分次从容地解析报文,不用担心错过任何数据,大大降低了编程复杂度,对新手程序员那是相当友好,就像有个老师在旁边,不停提醒你还有作业没做完呢。
②边缘触发(ET):高效灵敏的 “情报员”边缘触发(ET)模式则像一位高效灵敏的情报员,奉行 “只报新事” 原则。只有在文件描述符的状态发生改变时,比如从无数据变为有数据可读,或者从不可写变为可写,内核才会触发事件通知应用程序。一旦通知了,它就默认你知晓此事,后续除非状态再次改变,否则不会重复通知。这意味着应用程序得打起十二分精神,在收到通知后,必须立刻、马上处理数据,而且要尽可能把当前就绪的数据一次性处理完。
比如说读取大型文件,使用 ET 模式,一旦检测到文件描述符可读,就得赶紧用 while 循环一股脑把数据全读完,不然下次 epoll_wait 可不会再提醒你还有剩余数据。要是读数据时遇到 EAGAIN 或 EWOULDBLOCK 错误,那就说明这次数据真读完了。这种模式虽然编程难度稍高,需要精细处理数据,但减少了不必要的唤醒次数,系统开销小,在追求极致性能的场景下,那可是 “利器”,能让数据如闪电般高效流转。
当然,在 LT 模式下开发基于 epoll 的应用要简单一些,不太容易出错,而在 ET 模式下事件发生时,如果没有彻底地将缓冲区的数据处理完,则会导致缓冲区的用户请求得不到响应。注意,默认情况下 Nginx 采用 ET 模式使用 epoll 的。
4.2 epoll核心数据结构
(1)红黑树 —— 精准管理的 “魔法树”红黑树在 epoll 里可是扮演着 “大管家” 的关键角色,它专门负责存储和管理海量的文件描述符。这棵树有着独特的 “魔力”,它是一种自平衡的二叉搜索树,意味着无论插入、删除还是查找操作,时间复杂度都能稳稳地保持在 O (log n)。
想象一下,在高并发场景下,每秒有成千上万个新连接涌入,每个连接对应一个文件描述符。要是没有红黑树,查找一个特定的文件描述符就如同大海捞针,效率极其低下。而有了红黑树,就好比给每个文件描述符都安排了一个专属的智能导航。当需要添加新连接(即新文件描述符)时,它能快速指引插入位置;要关闭某个连接删除对应描述符,也能迅速定位并移除,丝毫不乱。举个例子,在大型在线游戏服务器里,同时在线玩家众多,网络连接频繁变动,红黑树就能高效管理这些连接,确保游戏运行顺畅,玩家操作即时响应,不会因连接管理混乱而卡顿。
(2)就绪链表 —— 即时响应的 “情报站”就绪链表就像是 epoll 的 “情报收集站”。当某个被监听的文件描述符状态发生变化,比如有数据可读、可写,内核立马知晓,并通过回调机制,闪电般地将这个就绪的文件描述符添加到就绪链表中。这个链表通常是用双向链表实现,插入和删除操作那叫一个快,时间复杂度仅 O (1)。
打个比方,这就好比快递驿站收到了你的包裹(数据就绪),立马把你的取件码(文件描述符)放到一个专门的 “待取货架”(就绪链表)上,你一来就能快速拿到包裹。对于应用程序而言,当调用 epoll_wait 时,根本不用费时费力去遍历所有文件描述符,直接到这个 “情报站”—— 就绪链表瞅一眼,就能瞬间获取所有已就绪的文件描述符,第一时间进行数据读写操作,大大提升了响应速度,让数据处理快如闪电。
(3)mmap—— 高效传输的 “隐形桥梁”mmap 堪称 epoll 实现高效的幕后英雄,它搭建起了内核空间与用户空间的 “隐形桥梁”—— 共享内存。在传统的 I/O 操作里,数据从内核缓冲区拷贝到用户缓冲区,这个过程就像搬运工来回搬货,费时费力,还增加系统开销。
但有了 mmap 就不一样了,它直接让内核空间和用户空间共享同一块内存区域,数据来了,双方都能直接访问,减少了数据拷贝次数。这就好比图书馆有个公共书架,管理员(内核)和读者(用户程序)都能直接在上面取放书籍(数据),无需来回搬运。像是视频流处理应用,大量视频数据频繁传输,mmap 使得数据能快速从内核流向用户空间,减少传输延迟,让视频播放流畅无卡顿,极大提升了系统整体性能。
4.3 三个核心 API
epoll 提供了三个核心 API,它们是 epoll 实现高效 I/O 多路复用的关键工具,就像 “超级服务助手” 的三件 “法宝”,各司其职,协同工作。
(1)epoll_create:用于创建一个 epoll 实例,并返回一个文件描述符(epfd),这个文件描述符将用于后续对 epoll 实例的操作 。在创建 epoll 实例时,内核会为其分配相应的数据结构,包括红黑树和就绪链表等,并初始化相关的参数。例如:
(2)epoll_ctl:用于控制 epoll 实例,它可以向 epoll 实例中添加、删除或修改文件描述符及其感兴趣的事件 。epoll_ctl 函数有四个参数:epfd(epoll 实例的文件描述符)、op(操作类型,如 EPOLL_CTL_ADD 表示添加,EPOLL_CTL_DEL 表示删除,EPOLL_CTL_MOD 表示修改)、fd(要操作的文件描述符)和 event(指向 epoll_event 结构体的指针,用于指定事件类型和关联的数据)。例如,当我们要将一个监听套接字添加到 epoll 实例中,监听读事件时,可以这样使用:
(3)epoll_wait:用于等待文件描述符上的事件发生,它会阻塞调用线程,直到有事件触发或超时 。epoll_wait 函数有四个参数:epfd(epoll 实例的文件描述符)、events(指向 epoll_event 结构体数组的指针,用于返回发生的事件)、maxevents(events 数组的大小,即一次最多处理的事件数)和 timeout(等待事件发生的超时时间,以毫秒为单位,如果设置为 - 1,则表示无限等待)。当有事件发生时,epoll_wait 会将就绪的事件从就绪链表中取出,填充到 events 数组中,并返回就绪事件的数量。例如:
4.4 epoll实现原理
我们以 linux 内核 2.6 为例,说明一下 epoll 是如何高效的处理事件的。当某一个进程调用 epoll_create 方法的时候,Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个重要的成员。
第一个是 rb_root rbr,这是红黑树的根节点,存储着所有添加到 epoll 中的事件,也就是这个 epoll 监控的事件。第二个是 list_head rdllist 这是一个双向链表,保存着将要通过 epoll_wait 返回给用户的、满足条件的事件。每一个 epoll 对象都有一个独立的 eventpoll 结构体,这个结构体会在内核空间中创造独立的内存,用于存储使用 epoll_ctl 方法向 epoll 对象中添加进来的事件。这些事件都会挂到 rbr 红黑树中,这样就能够高效的识别重复添加的节点。
所有添加到 epoll 中的事件都会与设备(如网卡等)驱动程序建立回调关系,也就是说,相应的事件发生时会调用这里的方法。这个回调方法在内核中叫做 ep_poll_callback,它把这样的事件放到 rdllist 双向链表中。在 epoll 中,对于每一个事件都会建立一个 epitem 结构体。
当调用 epoll_wait 检查是否有发生事件的连接时,只需要检查 eventpoll 对象中的 rdllist 双向链表中是否有 epitem 元素,如果 rdllist 链表不为空,则把这里的事件复制到用户态内存中的同时,将事件数量返回给用户。通过这种方法,epoll_wait 的效率非常高。epoll-ctl 在向 epoll 对象中添加、修改、删除事件时,从 rbr 红黑树中查找事件也非常快。这样,epoll 就能够轻易地处理百万级的并发连接。
五、epoll 高效的原因
epoll 在高并发场景下的高效表现,得益于其多方面的优化设计,就像一位拥有多项绝技的 “超级英雄”,在应对高并发挑战时游刃有余。
5.1 事件驱动机制
epoll 摒弃了 select 和 poll 的传统轮询方式,采用了高效的事件驱动机制 。在传统的轮询方式中,就好比在一个巨大的仓库里,不管货物有没有变化,都要逐个去查看,在连接数量众多时,大量的时间和资源就浪费在了这些无效的检查上。而 epoll 采用事件驱动机制,当文件描述符状态发生变化,比如有数据可读或可写时,内核会主动发出通知,应用程序只需关注这些有事件发生的文件描述符即可,这大大减少了无效操作。例如,在一个拥有大量并发连接的服务器中,epoll 能够精准地定位到那些有数据传输的活跃连接,避免对众多无事件连接的遍历,从而显著提高了系统的效率。
5.2 数据结构优势
epoll 的数据结构设计堪称精妙绝伦,为其高效运行提供了有力支持 。红黑树用于管理注册的文件描述符,其插入、删除和查找操作的时间复杂度仅为 O (log N),远优于传统线性结构,这使得 epoll 在处理大量文件描述符时能够快速定位和操作,不会因为文件描述符数量的增加而导致性能大幅下降。
当文件描述符就绪时,内核将其从红黑树移至就绪链表,epoll_wait 只需遍历该链表,就能获取就绪事件,提升了事件获取效率 。相比之下,select 和 poll 在获取就绪事件时需要遍历整个文件描述符集合,时间复杂度为 O (n),当文件描述符数量较多时,效率会显著降低。例如,在一个处理数万并发连接的服务器中,epoll 通过就绪链表能够快速获取就绪事件,而 select 和 poll 则需要花费大量时间遍历所有连接,导致系统响应速度变慢。
5.3 数据传输优化
epoll 借助 mmap 技术在内核与用户空间建立共享内存,减少了数据在内核缓冲区与用户空间应用程序缓冲区之间的拷贝次数,提高了传输效率 。在传统的数据传输过程中,数据从内核缓冲区到用户空间应用程序缓冲区,往往需要多次拷贝,这无疑增加了时间和资源开销。而 epoll 通过共享内存,让数据传输更直接高效,减少了拷贝次数,加快了数据传输速度,就像开辟了一条数据传输的 “高速公路”。
epoll 支持水平触发(LT)和边缘触发(ET)两种模式 。水平触发是默认的工作方式,只要文件描述符上有指定的事件(如数据可读),每次调用 epoll_wait 都会返回此事件,除非事件被处理(如数据被读走)。边缘触发则更为高效,仅在文件描述符状态变化瞬间通知一次应用程序,促使应用程序一次性处理完相关数据,减少了 epoll_wait 的调用次数,进一步提升了效率 。例如,在一些对实时性要求较高的场景中,如网络监控系统,使用边缘触发模式可以减少不必要的系统调用,提高系统的响应速度。
六、I/O多路复用高频面试题
问题 1:什么是 I/O 多路复用?
I/O 多路复用是一种同步 I/O 模型,允许一个线程监视多个文件句柄,一旦某个文件句柄就绪(通常是可读、可写或出现异常等情况),就能通知应用程序进行相应的读写操作。没有文件句柄就绪时会阻塞应用程序,交出 CPU。其可让服务器用单线程支持更多并发连接请求。
问题 2:为什么需要 I/O 多路复用机制?
无此机制时,处理并发 I/O 存在问题。例如,同步阻塞 I/O(BIO)用单线程无法处理并发,多线程方式在请求增多时,大量线程占用内存且切换开销大。同步非阻塞 I/O(NIO)需轮询所有文件描述符,即便无读写事件也轮询,浪费 CPU 资源。I/O 多路复用通过 select、epoll 等系统调用获取有事件的文件描述符列表处理请求,减少资源消耗,提升并发处理能力。
问题 3:I/O 多路复用的常见实现方式有哪些?
主要有 select、poll、epoll 和 kqueue 等。其中 select 几乎各平台都支持,但文件描述符数量常限制为 1024。poll 类似 select 但无文件描述符数量限制。epoll 是 Linux 特有,高效且无文件描述符限制,支持水平与边缘触发。kqueue 用于 FreeBSD、Mac OS X 等,能处理多种事件,性能出色。
问题 4:select 有哪些缺点?
单个进程可监视的文件描述符数量受限,由 FD_SETSIZE 设置,默认 1024。每次调用都需把文件描述符集合从用户态拷贝至内核态,文件描述符多时开销大。扫描 socket 是线性轮询,高并发场景下效率低。仅支持水平触发模式。问题 5:poll 和 select 的区别是什么?
poll 功能与 select 基本类似,主要区别是 poll 通过基于链表存储数据,没有文件描述符数量的限制。不过,它每次调用时,仍需把文件描述符集合从用户态拷贝到内核态,且扫描时也是线性扫描,高并发时效率低。
问题 6:epoll 为什么效率高?
事件通知机制:采用事件驱动,通过 epoll_ctl 注册文件描述符,就绪时内核用类似回调的机制激活,epoll_wait 接收通知,不必像 select/poll 轮询,时间复杂度达 O (1)。减少内存拷贝:借助 mmap () 文件映射内存加速与内核空间消息传递,减少数据拷贝开销。无显著连接限制:可打开的文件描述符上限较高,1G 内存机器可监听约 10 万个端口。问题 7:epoll 的 LT 和 ET 模式区别是什么?
LT(水平触发)是默认模式,只要文件描述符有数据可读或可写,每次 epoll_wait 都会返回其事件提醒操作。ET(边缘触发)是高速模式,仅在文件描述符状态从不可读 / 写变为可读 / 写时通知一次。ET 效率高,但需应用程序一次处理完数据,常需读到 EAGAIN 错误确保读完缓冲区数据。
问题 8:I/O 多路复用适用于什么场景?
适用于需处理大量并发连接,且每个连接读写操作不频繁的场景,像 Redis、Nginx 等常处理大量并发客户端请求的服务器程序。它们通过 I/O 多路复用机制,用少量线程高效管理众多客户端连接。
问题 9:epoll 存在缺点吗?
epoll 仅能在 Linux 操作系统上使用,限制了基于其开发的程序的跨平台能力。若程序需跨平台,用 select 或考虑封装支持多平台的 I/O 多路复用抽象层更合适。
问题 10:select、poll、epoll 是同步 I/O 还是异步 I/O?
它们均属同步 I/O。因即便 select、poll、epoll 可帮获取就绪的文件描述符,但实际读写数据过程仍需应用程序自行负责,读写操作会阻塞线程,而异步 I/O 是内核完成数据从内核空间到用户空间的拷贝等工作后通知应用程序。