select、poll、epoll


select

select() 是 Unix/Linux 系统中的一个系统调用,用于监视多个文件描述符的状态变化,从而得知哪些文件描述符是可读、可写或有异常待处理的。这对于实现 I/O 多路复用非常有用,特别是在网络编程中,服务器可能需要同时处理多个客户端的连接。

以下是 select() 函数的基本原型:

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

参数说明:

  • nfds:需要监视的文件描述符集合中的最大文件描述符加 1。
  • readfds:指向 fd_set 类型的指针,该集合中的文件描述符将被检查是否可读。
  • writefds:指向 fd_set 类型的指针,该集合中的文件描述符将被检查是否可写。
  • exceptfds:指向 fd_set 类型的指针,该集合中的文件描述符将被检查是否有异常条件待处理。
  • timeout:指向 timeval 结构的指针,用于指定 select() 函数的超时时间。如果设置为 NULL,select() 将无限期地等待直到至少有一个文件描述符准备好。

返回值:

  • 如果成功,select() 返回准备好的文件描述符集合中最高文件描述符加 1。
  • 如果出现错误,select() 返回 -1,并设置全局变量 errno 以指示错误类型。

fd_set 是一个位图,用于表示文件描述符集合。可以使用以下宏来操作 fd_set

  • FD_ZERO(fd_set *set):清除 set 中的所有位。
  • FD_SET(int fd, fd_set *set):将文件描述符 fd 对应的位设置为 1。
  • FD_CLR(int fd, fd_set *set):将文件描述符 fd 对应的位清零。
  • FD_ISSET(int fd, fd_set *set):检查文件描述符 fd 对应的位是否为 1。

使用 select() 时,通常的做法是首先使用 FD_ZERO() 初始化 fd_set,然后使用 FD_SET() 将要监视的文件描述符添加到相应的集合中。接着调用 select(),它会阻塞当前线程,直到至少有一个文件描述符准备好或超时。select() 返回后,使用 FD_ISSET() 检查各个文件描述符集合,以确定哪些文件描述符已经准备好进行读取、写入或异常处理。

需要注意的是,select() 在处理大量文件描述符时可能不够高效,因为它使用位图来存储文件描述符集合,这会导致内存使用效率不高。此外,select() 在某些系统上存在文件描述符数量限制(通常是 1024)。因此,对于需要处理大量并发连接的应用,更高效的 I/O 多路复用机制如 epoll(在 Linux 中)或 kqueue(在 BSD 系统中)可能更为适用。



poll

poll 是 Unix 和类 Unix 系统中用于 IO 多路复用的一个函数。IO 多路复用允许程序同时监控多个文件描述符,以查看它们中的哪一个是可读的、可写的或有其他事件待处理。这对于需要处理大量并发连接的服务器应用程序来说是非常有用的。

poll 函数原型如下:

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明:

  • fds:一个指向 pollfd 结构体数组的指针,该数组用于存储需要监控的文件描述符和对应的事件。
  • nfdsfds 数组中的元素数量。
  • timeout:等待事件发生的最长时间(以毫秒为单位)。如果设置为 -1,则无限期等待;如果设置为 0,则立即返回,不等待。

pollfd 结构体定义如下:

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};
  • fd:要监控的文件描述符。
  • events:请求监控的事件类型,可以是以下一个或多个值的按位或:
    • POLLIN:数据可读
    • POLLOUT:数据可写
    • POLLPRI:高优先级数据可读
    • POLLERR:错误条件
    • POLLHUP:挂起条件
    • POLLNVAL:无效的文件描述符
  • revents:实际发生的事件,由 poll 函数返回。

poll 函数返回发生事件的文件描述符数量,或者在发生错误时返回 -1。

使用 poll 进行 IO 多路复用的基本步骤如下:

  1. 创建一个 pollfd 结构体数组,并设置每个元素的 fdevents 字段。
  2. 调用 poll 函数,将数组和超时时间作为参数传递。
  3. 检查 poll 的返回值。如果返回值为 0,则超时;如果返回值大于 0,则检查每个 pollfd 结构体的 revents 字段,以确定哪些文件描述符上有事件发生。
  4. 对有事件发生的文件描述符执行相应的操作。
  5. 重复以上步骤,直到程序结束。

需要注意的是,poll 函数在处理大量文件描述符时可能会比 select 函数更高效,因为它没有 select 函数中文件描述符数量限制的问题。然而,epoll 是 Linux 特有的 IO 多路复用机制,它在某些情况下可能比 poll 更具优势,特别是在处理大量并发连接时。



poll与select的区别

pollselect都是Unix和类Unix系统中用于IO多路复用的机制,它们的主要区别在于实现方式、性能和功能限制。

  1. 实现方式和数据结构

    • select使用fd_set结构体来存储需要监控的文件描述符集合。每次调用select之前,都需要重置fd_set,因为每次文件描述符发生改变,相应的fd_set结构体就会被改变。
    • poll使用pollfd数组来存储需要监控的文件描述符及其感兴趣的事件。pollfd数组只需要设置一次,不需要在每次调用poll之前重置。
  2. 性能

    • selectpoll的时间复杂度都是O(n),因为当selectpoll检测到有I/O事件发生时,它们需要遍历所有注册的文件描述符来找出哪些文件描述符上有事件发生。这意味着随着文件描述符数量的增加,无差别轮询的时间也会增加。
  3. 功能限制

    • select在注册事件上有一定的限制,这通常是由FD_SETSIZE宏定义的值决定的。在32位系统上,这个值通常是1024,而在64位系统上,这个值通常是2048。
    • poll则没有这样的限制,因为它基于链表来存储文件描述符,所以没有最大连接数的限制。

总的来说,poll相对于select在某些方面可能更具优势,比如没有文件描述符数量的限制,以及不需要在每次调用之前重置数据结构。然而,对于某些特定的应用场景,如需要处理大量并发连接,epoll可能是更好的选择,因为它具有更高的性能和更灵活的事件通知机制。



epoll

epoll是Linux内核中提供的I/O多路复用机制,特别适用于需要同时处理大量文件描述符的场景,如高并发的网络服务器。epoll通过改进传统的select/poll模型,提供了更高效的事件通知机制。

epoll的基本原理

  1. 事件驱动:epoll是事件驱动的,意味着它只会在文件描述符就绪(即有数据可读、可写或有异常条件)时通知应用程序。这与传统的poll模型不同,后者需要应用程序定期轮询所有文件描述符以检查它们的状态。
  2. 高效的数据结构:epoll内部使用红黑树来存储和管理注册的文件描述符,这使得查找、添加和删除操作都非常高效,即使面对大量的文件描述符也是如此。
  3. 基于回调的通知机制:当有文件描述符就绪时,epoll可以使用回调机制通知应用程序,而不是让应用程序轮询所有文件描述符。这大大减少了不必要的CPU和内存开销。
  4. 水平触发和边缘触发:epoll支持两种事件触发模式:水平触发(Level Triggered)和边缘触发(Edge Triggered)。水平触发模式下,只要文件描述符就绪,epoll就会持续通知应用程序;而在边缘触发模式下,epoll只会在文件描述符从非就绪状态变为就绪状态时通知一次。

epoll的API

epoll的三个主要系统调用分别是epoll_create()(或epoll_create1()),epoll_ctl(),和epoll_wait()。这些系统调用共同提供了在Linux环境下高效处理大量并发I/O操作的能力。

  1. epoll_create() / epoll_create1()
    这两个函数用于创建一个新的epoll实例,并返回一个指向该实例的文件描述符。在较新的Linux内核版本中,推荐使用epoll_create1(),因为它提供了额外的标志(flags)参数来指定创建过程中的行为。原始的epoll_create()函数在现代系统中通常作为epoll_create1()的特定情况(传递0作为flags参数)存在。

    函数原型:

    int epoll_create(int size); // size参数在较新的内核版本中已被忽略
    int epoll_create1(int flags);
    

    epoll_create1()的flags参数可以是0(与epoll_create()相同行为)或者EPOLL_CLOEXEC(在执行新的程序时关闭文件描述符)。

  2. epoll_ctl()
    epoll_ctl()函数用于控制epoll实例,通过它可以向epoll实例注册、修改或删除感兴趣的文件描述符及其事件。

    函数原型:

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    

    参数说明:

    • epfd:epoll实例的文件描述符,由epoll_create()epoll_create1()返回。
    • op:表示操作类型,可以是EPOLL_CTL_ADD(添加新的文件描述符),EPOLL_CTL_MOD(修改已注册的文件描述符),或EPOLL_CTL_DEL(删除文件描述符)。
    • fd:需要注册的文件描述符。
    • event:指向epoll_event结构体的指针,该结构体指定了事件类型和与文件描述符关联的数据(通常是一个回调函数或用户数据)。

    需要注意的是,epoll_event结构体中的events字段用于指定感兴趣的事件类型,如EPOLLIN(数据可读)、EPOLLOUT(数据可写)等。

  3. epoll_wait()
    epoll_wait()函数用于等待注册在epoll实例上的文件描述符上的事件发生。当有一个或多个文件描述符就绪时,该函数将返回并将就绪的文件描述符信息填充到提供的数组中。

    函数原型:

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    

    参数说明:

    • epfd:epoll实例的文件描述符。
    • events:指向epoll_event结构体数组的指针,用于接收就绪事件的信息。
    • maxevents:指定events数组的大小,即最多可以返回多少个就绪事件的信息。
    • timeout:指定等待的超时时间(以毫秒为单位),如果设置为-1,则epoll_wait()将永远等待,直到有事件发生;如果设置为0,则epoll_wait()将立即返回,即使没有任何事件发生。

这三个系统调用共同协作,使得应用程序能够高效地处理多个文件描述符上的I/O事件,特别适用于需要同时处理大量并发连接的网络服务器。

epoll的优势

epoll相较于select和poll的优点在于:

  1. 没有最大文件描述符的限制:select和poll都有最大文件描述符的限制,而epoll则没有这个限制。这使得epoll可以处理更多的并发连接。
  2. 使用红黑树来管理待检测集合:select和poll都是基于线性方式处理待检测集合的,而epoll则使用红黑树来管理待检测集合。这使得epoll在处理大量文件描述符时更加高效。
  3. 采用基于事件的驱动方式:epoll采用基于事件的驱动方式,只有"活跃"的socket才会主动去调用callback函数,其他idle状态的socket则不会。这使得系统不会浪费CPU去轮询无用的socket。
  4. 支持水平触发和边缘触发两种模式:除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

总结,epoll是一种高效的IO多路复用技术,特别适用于处理大量并发连接的情况。它通过使用一个文件描述符来管理多个文件描述符,并将用户关心的文件描述符事件存放到内核的一个事件表中,从而提高了系统的效率和性能。



epoll中的阻塞和非阻塞行为的设定时机

在Linux的网络编程中,epoll是一个高效的I/O事件通知机制,它允许应用程序监视多个文件描述符,以查看它们是否准备好进行非阻塞的读/写操作。epoll本身并不直接提供阻塞或非阻塞的I/O操作,这些行为是由文件描述符的设定来决定的。

  1. 非阻塞行为的设定

    • 非阻塞行为通常是在设置文件描述符时指定的。对于socket来说,可以通过设置O_NONBLOCK标志来实现非阻塞行为。例如,使用fcntl函数可以设置或更改一个已存在的socket的文件状态标志。
    • 当一个socket被设置为非阻塞时,对其进行的读/写操作将不会阻塞调用者。如果操作不能立即完成(例如,没有数据可读或缓冲区已满),则会返回一个错误,通常是EAGAINEWOULDBLOCK
  2. epoll中的使用

    • 当使用epoll时,通常会先将相关的socket设置为非阻塞模式。然后,将这些socket注册到epoll实例中,并指定感兴趣的事件(如EPOLLIN表示可读事件,EPOLLOUT表示可写事件)。
    • epoll本身可以工作在两种模式下:水平触发(LT)和边沿触发(ET)。在边沿触发模式下,只有当状态从非就绪变为就绪时,才会通知应用程序。这要求应用程序在接收到通知后,尽可能地处理所有的就绪事件,直到再次阻塞或没有更多的事件可处理。
    • 对于非阻塞的socket和epoll的边沿触发模式,通常需要采用循环读取/写入的方式,直到read/write返回EAGAINEWOULDBLOCK错误。
  3. 阻塞行为的考虑

    • 虽然非阻塞模式与epoll很好地协同工作,但在某些情况下,可能仍然需要使用阻塞模式。例如,当需要等待多个条件同时满足时(如多个socket都有数据可读),使用阻塞模式可能更简单或更有效。
    • 然而,请注意,在使用epoll时,通常不建议将文件描述符设置为阻塞模式,因为这可能会导致应用程序在等待I/O操作时阻塞,从而无法充分利用epoll的优势。
  4. 设定时机

    • 阻塞或非阻塞模式的设定通常在socket创建之后、注册到epoll之前进行。这样,当socket被添加到epoll实例并开始监视事件时,它的行为方式(阻塞或非阻塞)就已经确定了。
    • 当然,也可以在将socket添加到epoll实例之后更改其模式,但这样做需要谨慎处理,以确保应用程序的逻辑能够正确地处理可能的阻塞和非阻塞情况。


epoll可读和可写是什么意思

Linux中套接字可读|可写--SO_RCVLOWAT和SO_SNDLOWAT

epoll 是 Linux 系统中的一种 I/O 多路复用技术,它允许用户在一个进程中同时监控多个文件描述符(如套接字)的 I/O 事件,而不需要为每个描述符设置单独的阻塞等待。这样可以更高效地管理大量并发连接,特别适用于高性能服务器的开发。

epoll 中,"可读" 和 "可写" 是指文件描述符(常指套接字)上的两种 I/O 事件:

  1. 可读 (EPOLLIN):这意味着关注的文件描述符上有数据可读。当一个套接字接收到新的数据,或者之前的数据已经被读取完毕但还有更多数据可读时,epoll 会标记这个套接字为可读状态。应用程序接收到这个事件后,就可以安全地从这个套接字中读取数据,而不必担心读操作会阻塞。

  2. 可写 (EPOLLOUT):这表示文件描述符现在准备好接受写入数据。当一个套接字之前因为缓冲区满无法写入更多数据时,epoll 会等待直到该套接字有足够的空间来接收新的输出数据。一旦缓冲区有足够空间,epoll 就会通知应用程序该套接字变为可写状态,这时程序就可以向套接字写入数据而较大概率上不会导致阻塞。

通过监听这些事件,应用程序可以更加高效地管理其 I/O 操作,尤其是对于需要处理大量并发连接的服务来说,能够显著提高处理能力和响应速度。



用户程序和内核程序的视角

要理解这句话,首先需要了解传统的poll模型和改进后的epoll模型之间的区别。这两种模型都是用于处理I/O多路复用的机制,它们允许一个进程同时监视多个文件描述符(例如套接字),以确定是否有一个或多个已经准备好进行读写操作。

传统 poll 模型

在传统的poll模型中,应用程序会调用poll系统调用,并传入它想要监视的一组文件描述符以及超时时间。poll函数将阻塞当前线程直到以下任意一种情况发生:

  • 至少一个文件描述符准备好了(即有数据可读、可以写入或者发生了异常)。
  • 超时时间到达。

每次调用poll时,操作系统都需要遍历整个文件描述符列表来检查哪些描述符是就绪状态。这对于小数量的文件描述符来说是可以接受的,但当文件描述符的数量增加时,效率就会变得很低下,因为每次都要重复检查所有文件描述符的状态。

epoll 模型

epoll是一种更为高效的I/O事件通知方式,特别是在Linux环境下。epoll通过三个主要的接口函数与用户空间交互:epoll_createepoll_ctlepoll_wait

  • epoll_create 创建一个epoll实例,返回一个文件描述符,这个描述符用于后续的epoll操作。
  • epoll_ctl 用来注册、修改或移除对特定文件描述符的兴趣。
  • epoll_wait 等待epoll实例中的文件描述符变成就绪状态。

epoll的主要优势在于它使用了事件驱动的方式。这意味着:

  1. 内核维护了一个事件表:当你添加一个文件描述符到epoll实例时,内核会记录下你对该文件描述符感兴趣的事件(如读、写等)。只有当这些事件发生时,内核才会通知应用程序。
  2. 只通知变化:一旦epoll_wait被调用,它会阻塞直到至少一个文件描述符变成就绪状态。与poll不同的是,epoll不会每次都检查所有的文件描述符;相反,它只会告诉你自上次调用以来哪些文件描述符的状态发生了变化。
  3. 边缘触发和水平触发epoll支持两种工作模式——边缘触发(Edge Triggered, ET)和水平触发(Level Triggered, LT)。在ET模式下,仅当状态从未就绪变为就绪时才触发事件;而在LT模式下,只要文件描述符处于就绪状态,每次调用epoll_wait都会返回该文件描述符。

因此,epoll比传统的poll更高效,特别是在处理大量文件描述符的情况下。它减少了不必要的系统调用和上下文切换,提高了程序的性能。


为了从用户程序和内核程序的视角来介绍selectpollepoll的区别,我们需要分别探讨它们的工作流程和机制。这三种I/O多路复用技术在处理多个文件描述符时有着不同的效率和适用场景。

用户程序与 select

  • 用户程序:用户程序准备一个包含所有待监视文件描述符的集合(读集合、写集合和异常集合),并调用select函数。它还可以指定超时时间。
  • 内核程序:接收到select系统调用后,内核会遍历用户提供的文件描述符集合,检查每个文件描述符的状态。如果没有任何文件描述符准备好,那么内核会让当前进程进入等待状态,直到有至少一个文件描述符变成就绪或超时发生。一旦条件满足,内核将返回给用户程序一个更新后的集合,指示哪些文件描述符现在是可操作的。

select的一个重要限制是它有一个固定的上限(通常是1024),这限制了它可以监视的最大文件描述符数量。

用户程序与 poll

  • 用户程序:用户程序准备一个pollfd结构体数组,每个条目对应一个文件描述符,并指定了对哪些事件(如读就绪、写就绪)感兴趣。然后调用poll函数,并可以设置超时时间。
  • 内核程序:接收到poll系统调用后,内核会遍历用户提供的pollfd数组,检查每个文件描述符的状态。如果没有文件描述符准备好,内核会让当前进程进入等待状态,直到有至少一个文件描述符变成就绪或超时发生。之后,内核返回给用户程序一个更新后的pollfd数组,指示哪些文件描述符现在是可操作的。

poll解决了select的文件描述符数量限制问题,但每次调用poll时,内核仍然需要重新扫描整个数组,这在大量文件描述符的情况下会导致性能问题。

用户程序与 epoll

  • 用户程序

    • 创建一个epoll实例,通常通过调用epoll_create实现。
    • 使用epoll_ctl添加、修改或删除感兴趣的文件描述符,并指定关心的事件类型。
    • 调用epoll_wait等待事件的发生,可以选择指定超时时间。
  • 内核程序

    • 内核维护一个与每个epoll实例关联的数据结构(如红黑树和事件队列)。当某个文件描述符变成就绪状态时,内核会自动将其对应的事件添加到该epoll实例的事件队列中。
    • 当用户程序调用epoll_wait时,内核直接从事件队列中获取已经发生的事件并返回给用户程序,而不是重新扫描所有的文件描述符。这使得epoll能够高效地处理大量文件描述符。

区别

  1. 文件描述符数量select有文件描述符数量的限制(通常为1024),而pollepoll没有这种限制,理论上可以处理更多的文件描述符。
  2. 效率epollselectpoll更高效,因为它使用了事件驱动的方式,只有当文件描述符变成就绪状态时才会通知用户程序。selectpoll则需要每次都检查所有文件描述符的状态,导致在大量文件描述符的情况下效率低下。
  3. 扩展性epoll设计用来处理大量文件描述符的情况,具有良好的扩展性;而selectpoll在高并发场景下的表现不如epoll
  4. 事件传递模式epoll支持边缘触发(Edge Triggered, ET)和水平触发(Level Triggered, LT),允许更细粒度地控制何时接收事件通知;selectpoll总是以水平触发的方式工作。
  5. 资源占用epoll创建了一个新的文件描述符来表示epoll实例,这可能会稍微增加一些开销,但对于现代操作系统来说是可以忽略不计的。相比之下,selectpoll没有这种额外的开销,但在高并发场景下,它们的性能劣势更加明显。

总之,epoll为用户程序提供了更好的性能和更大的灵活性,特别是在面对大规模并发连接时。selectpoll虽然也能完成类似的任务,但在现代高性能网络应用中,epoll是更优的选择。


优质文章

彻底搞懂 select/poll/epoll,就这篇了!
IO多路复用——深入浅出理解select、poll、epoll的实现
Windows与Linux下的select网络模型对比分析
socket技术详解
Epoll详解及源码分析
「底层原理」epoll源码分析,还搞不懂epoll的看过来
深入理解Linux网络(六):IO 复用 epoll 内部实现

posted @   guanyubo  阅读(32)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示