并发服务器搭建之Epoll与Select

        在 Linux 系统中,Select 和 Epoll 是两种最经典的 I/O 多路复用机制。它们看似都在解决 “同时管理多个连接” 的问题,却在底层实现、性能表现和适用场景上有着天壤之别。Select 作为早期的解决方案,曾支撑起一代网络服务的架构;而 Epoll 作为 Linux 2.6 内核后的 “后起之秀”,凭借更高效的事件通知机制,成为高并发服务器的首选技术。​

        本文将深入剖析 Epoll 与 Select 的工作原理、性能差异和适用场景,带你理解它们如何在高并发服务器中发挥核心作用,以及如何根据业务需求选择合适的技术方案,为你的服务器架构筑牢性能基石。

一、Select详解

         Select 的核心思想是 “集中监听,轮询检查”。它允许程序将多个文件描述符(如网络套接字、串口等)添加到监听集合中,然后阻塞等待,直到其中一个或多个文件描述符触发 I/O 事件(如可读、可写),再通过轮询的方式找出就绪的描述符并处理。 

1.1 工作流程拆解​

  • 步骤 1:初始化监听集合​

       程序需要创建三个文件描述符集合(可读、可写、异常),并将需要监听的文件描述符添加到对应集合中。例如,若需监听套接字的 “可读” 事件,就将该套接字加入可读集合。​

  • 步骤 2:阻塞等待事件​

       调用 select() 函数后,进程会进入阻塞状态,内核开始监控集合中的所有文件描述符。此时 CPU 资源会被释放,直到有事件触发或超时。​

  • 步骤 3:轮询就绪描述符​

      当 select() 返回时,内核会修改集合,仅保留就绪的文件描述符。程序需要通过轮询遍历整个集合,找出哪些描述符触发了事件,再进行对应处理(如读取数据、发送响应)。​

  • 步骤 4:重复监听​

     处理完就绪事件后,程序需要重新初始化集合(因为内核会修改原集合),再次调用 select() 进入下一轮监听,形成循环。

1.2 Select 核心函数与参数解析​

在 Linux 系统中,Select 通过 select() 系统调用实现,其函数原型如下:​

#include <sys/select.h>​

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

​

1. 关键参数说明​

  • nfds:需要监听的最大文件描述符值 + 1。内核会根据这个值确定需要遍历的描述符范围,例如若监听的描述符最大为 5,则 nfds 需设为 6。​
  • readfds:可读事件监听集合,包含所有需要检测 “是否有数据可读” 的文件描述符(如客户端发送数据到服务器套接字)。​
  • writefds:可写事件监听集合,包含所有需要检测 “是否可写入数据” 的文件描述符(如套接字缓冲区未满时可发送数据)。​
  • exceptfds:异常事件监听集合,用于检测文件描述符的异常状态(如连接错误)。​
  • timeout:超时时间设置。若为 NULL,则 select() 会一直阻塞直到事件触发;若设置具体时间,则超时后会自动返回(避免无限阻塞)。

2. 核心辅助函数​

Select 依赖一组宏函数操作文件描述符集合:​

  • FD_ZERO(fd_set *set):清空集合,初始化时必须调用。​
  • FD_SET(int fd, fd_set *set):将文件描述符 fd 添加到集合中。​
  • FD_CLR(int fd, fd_set *set):将文件描述符 fd 从集合中移除。​
  • FD_ISSET(int fd, fd_set *set):检查 fd 是否在集合中(用于轮询就绪描述符)。

1.3 Select 代码示例:简单的并发服务器框架​

以下是一个基于 Select 的 TCP 服务器示例,展示如何监听多个客户端连接:

#include <stdio.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAX_FD 1024  // Select 最大支持的文件描述符数量
#define PORT 8080

int main() {
    // 创建监听套接字
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listen_fd, 5);

    fd_set read_fds;  // 可读事件集合
    int max_fd = listen_fd;  // 记录最大文件描述符

    while (1) {
        FD_ZERO(&read_fds);  // 清空集合
        FD_SET(listen_fd, &read_fds);  // 添加监听套接字

        // 阻塞等待事件(超时设为 NULL,无限等待)
        int activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
        if (activity < 0) {
            perror("select error");
            break;
        }

        // 轮询检查就绪的文件描述符
        for (int fd = 0; fd <= max_fd; fd++) {
            if (FD_ISSET(fd, &read_fds)) {  // 检查 fd 是否就绪
                if (fd == listen_fd) {  // 新客户端连接
                    int client_fd = accept(listen_fd, NULL, NULL);
                    FD_SET(client_fd, &read_fds);
                    if (client_fd > max_fd) max_fd = client_fd;  // 更新最大 fd
                    printf("New connection, fd: %d\n", client_fd);
                } else {  // 客户端发送数据
                    char buffer[1024] = {0};
                    int len = read(fd, buffer, sizeof(buffer));
                    if (len <= 0) {  // 连接关闭或错误
                        close(fd);
                        FD_CLR(fd, &read_fds);
                        printf("Connection closed, fd: %d\n", fd);
                    } else {
                        printf("Received from fd %d: %s\n", fd, buffer);
                        write(fd, "OK", 2);  // 回复客户端
                    }
                }
            }
        }
    }
    close(listen_fd);
    return 0;
}

1.4 Select 的局限性:高并发场景下的瓶颈​

       尽管 Select 实现了基本的并发监听功能,但由于设计上的缺陷,它在高并发场景(如同时处理数万连接)中会暴露出明显的性能问题,主要体现在以下几个方面:​

1. 最大文件描述符限制​

       Select 对监听的文件描述符数量有硬限制(通常由内核参数 FD_SETSIZE 定义,默认值为 1024)。这意味着单个 Select 调用最多只能监听 1024 个文件描述符,对于需要支持数万并发连接的现代服务器来说,这是无法接受的瓶颈。​

2. 轮询效率低下​

      每次 select() 返回后,程序必须遍历整个文件描述符范围(从 0 到 max_fd)才能找出就绪的描述符。即使只有少数描述符就绪,也需要遍历所有可能的 fd,时间复杂度为 O(n)。当连接数达到上万时,这种轮询会消耗大量 CPU 资源,导致性能急剧下降。​

3. 集合需重复初始化​

      Select 会修改输入的文件描述符集合(仅保留就绪的 fd),因此每次调用 select() 前都需要重新初始化集合(用 FD_ZERO 和 FD_SET 重新添加所有 fd)。这不仅增加了代码复杂度,还会产生额外的内存拷贝开销(用户态与内核态之间的集合复制)。​

4. 内核态与用户态拷贝开销​

       每次调用 select() 时,程序需要将整个文件描述符集合从用户态拷贝到内核态;返回时,内核又需要将修改后的集合拷贝回用户态。当集合中的 fd 数量庞大时,这种频繁的内存拷贝会成为性能负担。

二、Epoll详解

        在现代互联网应用中,面对百万级并发连接的挑战,传统的 Select 机制已显得力不从心。Linux 内核 2.6 版本引入的 Epoll(Event Poll) 技术,通过全新的事件通知机制,彻底解决了 Select 在高并发场景下的性能瓶颈,成为构建高性能服务器的首选方案。

        Epoll 的设计理念是 "事件驱动,按需响应",它摒弃了 Select 的轮询模式,转而采用事件回调机制,让内核主动通知应用程序哪些文件描述符就绪。这种变革带来了质的飞跃,使服务器能够轻松应对海量并发连接,同时保持极低的 CPU 消耗。

2.1 Epoll 核心原理:事件驱动的高效监听

Epoll 的工作机制可概括为三个核心组件:

  1. epoll 实例(epoll instance)
    内核中创建的一个特殊数据结构,用于存储被监听的文件描述符和事件状态。通过 epoll_create() 创建,本质是一个文件描述符。

  2. 注册监听事件(epoll_ctl)
    通过 epoll_ctl() 函数向 epoll 实例中添加、修改或删除需要监听的文件描述符,并指定监听的事件类型(如可读、可写)。

  3. 等待事件触发(epoll_wait)
    调用 epoll_wait() 进入阻塞状态,当有注册的事件发生时,内核会将就绪的文件描述符列表返回给应用程序,无需轮询所有描述符。

工作流程对比(与 Select)

SelectEpoll
每次调用需重新设置整个监听集合,且集合会被内核修改,需重复初始化。只需通过 epoll_ctl 注册一次监听事件,后续无需重复操作,内核自动维护事件列表。
采用轮询方式遍历所有描述符,时间复杂度 O (n),随着连接数增加性能急剧下降。采用事件回调机制,时间复杂度 O (1),无论连接数多少,响应时间恒定。
监听描述符数量受限(通常为 1024),无法满足大规模并发需求。理论上无连接数限制,仅受系统资源(如内存)约束,可轻松处理数万甚至百万级连接。
每次调用需在用户态和内核态之间复制整个描述符集合,开销大。使用内存映射(mmap)技术避免数据拷贝,仅返回就绪的描述符列表,效率极高。

2.2 Epoll 核心 API 详解

Epoll 提供了三个关键系统调用:

1. epoll_create():创建 Epoll 实例

#include <sys/epoll.h>

int epoll_create(int size);
  • 参数size 在 Linux 2.6.8 之后被忽略,但需传入大于 0 的值以保持兼容性。
  • 返回值:返回一个指向 epoll 实例的文件描述符,后续操作均通过此描述符进行。

2. epoll_ctl():管理监听事件

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 参数
    • epfd:epoll 实例的文件描述符(由 epoll_create() 返回)。
    • op:操作类型,支持三种值:
      • EPOLL_CTL_ADD:添加新的监听描述符。
      • EPOLL_CTL_MOD:修改已注册描述符的监听事件。
      • EPOLL_CTL_DEL:删除监听描述符。
    • fd:需要监听的文件描述符(如套接字)。
    • event:指定监听的事件类型,通过 struct epoll_event 结构体设置:
struct epoll_event {
    uint32_t     events;    /* Epoll 事件类型 */
    epoll_data_t data;      /* 用户数据,通常存储 fd 或指针 */
};

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;
  • 常用事件类型
    • EPOLLIN:文件描述符可读。
    • EPOLLOUT:文件描述符可写。
    • EPOLLET:设置为边缘触发(Edge Triggered)模式(默认是水平触发)。
    • EPOLLERR:文件描述符发生错误。
    • EPOLLHUP:文件描述符被挂断(如连接关闭)。

3. epoll_wait():等待事件触发

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 参数
    • epfd:epoll 实例的文件描述符。
    • events:用于存储就绪事件的数组,由内核填充。
    • maxeventsevents 数组的最大长度,限制一次返回的事件数量。
    • timeout:超时时间(毫秒),-1 表示无限等待,0 表示立即返回。
  • 返回值:返回就绪事件的数量,0 表示超时,-1 表示错误。

2.3 Epoll 工作模式:水平触发(LT)与边缘触发(ET)

Epoll 支持两种事件触发模式,理解它们的差异对正确使用 Epoll 至关重要:

1. 水平触发(Level Triggered,默认模式)

  • 触发条件:只要文件描述符处于就绪状态(如缓冲区有数据可读),就会持续触发事件。
  • 特点
    • 兼容性好,与 Select/Poll 的行为一致。
    • 可靠性高,即使应用程序一次未处理完所有数据,后续仍会继续触发事件。
    • 代码实现简单,适合新手。

2. 边缘触发(Edge Triggered)

  • 触发条件:仅在文件描述符状态发生变化(如从无数据变为有数据)时触发一次事件。
  • 特点
    • 性能更高,减少事件触发次数,降低 CPU 开销。
    • 要求应用程序必须一次性处理完所有数据(如读完缓冲区所有数据),否则未处理的数据不会再次触发事件。
    • 代码复杂度高,需配合非阻塞 I/O 使用,避免阻塞线程。

对比示例

假设客户端发送 100 字节数据到服务器:

  • 水平触发

    1. 数据到达,epoll_wait 返回 EPOLLIN
    2. 应用程序读取 50 字节,缓冲区剩余 50 字节。
    3. epoll_wait 会再次返回 EPOLLIN,直到缓冲区数据被读完。
  • 边缘触发

    1. 数据到达,epoll_wait 返回 EPOLLIN
    2. 应用程序必须读取全部 100 字节,否则剩余数据不会触发新的事件。
    3. 若只读取 50 字节,剩余 50 字节需等到下一次数据到达(状态变化)才会触发事件

2.4 Epoll 代码示例:高性能并发服务器

以下是一个基于 Epoll 的 TCP 服务器示例,展示如何利用 Epoll 处理高并发连接:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
#define PORT 8080

int main() {
    // 创建监听套接字
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 设置套接字为非阻塞模式(边缘触发模式需要)
    int flags = fcntl(listen_fd, F_GETFL, 0);
    fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK);

    // 绑定地址和端口
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(listen_fd, 5) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 创建 Epoll 实例
    int epoll_fd = epoll_create(1);
    if (epoll_fd == -1) {
        perror("epoll_create");
        exit(EXIT_FAILURE);
    }

    // 注册监听套接字到 Epoll
    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN | EPOLLET;  // 边缘触发模式
    ev.data.fd = listen_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
        perror("epoll_ctl: listen_fd");
        exit(EXIT_FAILURE);
    }

    printf("Server started, listening on port %d...\n", PORT);

    while (1) {
        // 等待事件发生
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }

        // 处理就绪事件
        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == listen_fd) {
                // 新连接到来
                while (1) {
                    struct sockaddr_in client_addr;
                    socklen_t client_addr_len = sizeof(client_addr);
                    int client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len);
                    if (client_fd == -1) {
                        if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
                            // 没有更多连接
                            break;
                        } else {
                            perror("accept");
                            break;
                        }
                    }

                    // 设置客户端套接字为非阻塞模式
                    flags = fcntl(client_fd, F_GETFL, 0);
                    fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);

                    // 注册客户端套接字到 Epoll
                    ev.events = EPOLLIN | EPOLLET;  // 边缘触发模式
                    ev.data.fd = client_fd;
                    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
                        perror("epoll_ctl: client_fd");
                        close(client_fd);
                    }

                    printf("New connection from %s:%d, fd=%d\n",
                           inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd);
                }
            } else {
                // 客户端数据可读
                int client_fd = events[i].data.fd;
                char buffer[BUFFER_SIZE];

                while (1) {
                    memset(buffer, 0, BUFFER_SIZE);
                    int n = read(client_fd, buffer, BUFFER_SIZE);
                    if (n == -1) {
                        if (errno != EAGAIN) {
                            perror("read error");
                            close(client_fd);
                            epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
                        }
                        break;  // 已读完所有数据
                    } else if (n == 0) {
                        // 客户端关闭连接
                        printf("Connection closed by client, fd=%d\n", client_fd);
                        close(client_fd);
                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
                        break;
                    } else {
                        // 处理接收到的数据
                        printf("Received from fd=%d: %s\n", client_fd, buffer);
                        // 回显数据给客户端
                        write(client_fd, buffer, n);
                    }
                }
            }
        }
    }

    // 关闭资源
    close(listen_fd);
    close(epoll_fd);
    return 0;
}

 三、Select、Epoll差异对比

       在实际开发中,Select 和 Epoll 的实现差异极大,这些差异直接影响代码复杂度、性能表现和可维护性。以下从API 设计、数据结构、事件处理逻辑三个核心维度对比两者的开发差异,并给出典型代码示例:

3.1 API 设计与数据结构差异

1. Select 的核心结构与 API

  • 文件描述符集合:使用 fd_set 位图结构(固定大小,通常为 1024 位)。
  • 核心函数
    fd_set readfds, writefds, exceptfds;
    FD_ZERO(&readfds);        // 清空集合
    FD_SET(fd, &readfds);     // 添加 fd 到集合
    int nfds = select(max_fd+1, &readfds, &writefds, &exceptfds, timeout);
    
  • 限制:需手动维护三个集合(可读、可写、异常),且每次调用 select 后集合会被内核修改,需重新初始化。

2. Epoll 的核心结构与 API

  • 事件结构体:使用 struct epoll_event 存储事件类型和用户数据。
  • 核心函数
    int epfd = epoll_create(1);                // 创建 epoll 实例
    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN | EPOLLET;             // 设置事件类型(边缘触发)
    ev.data.fd = fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);  // 注册事件
    int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout);  // 等待事件
    
  • 优势:通过 epoll_ctl 集中管理事件,内核维护事件列表,无需每次重置。

3.2 事件处理逻辑差异

1. Select 的轮询模式

  • 工作流程
    1. 将所有待监听的 fd 添加到 fd_set
    2. 调用 select 阻塞等待。
    3. 返回后遍历所有可能的 fd(0 到 max_fd),通过 FD_ISSET 检查哪些就绪。
  • 代码示例
    while (1) {
        FD_ZERO(&readfds);
        for (int i = 0; i < MAX_FD; i++) {
            if (client_fds[i] > 0) {
                FD_SET(client_fds[i], &readfds);
            }
        }
        int activity = select(max_fd + 1, &readfds, NULL, NULL, NULL);
        for (int i = 0; i < MAX_FD; i++) {
            if (FD_ISSET(client_fds[i], &readfds)) {
                // 处理就绪的 fd
            }
        }
    }
    
  • 问题:时间复杂度 O(n),需遍历所有 fd,无论其是否就绪。

2. Epoll 的事件驱动模式

  • 工作流程
    1. 通过 epoll_ctl 注册 fd 和事件类型。
    2. 调用 epoll_wait 阻塞等待。
    3. 直接获取内核返回的就绪事件数组,遍历处理。
  • 代码示例
    while (1) {
        int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
        for (int i = 0; i < nfds; i++) {
            int fd = events[i].data.fd;
            if (events[i].events & EPOLLIN) {
                // 处理可读事件
            }
        }
    }
    
  • 优势:时间复杂度 O(1),仅处理就绪的 fd,无需遍历全部。

3.3  边缘触发(ET)与水平触发(LT)的实现差异

1. Select 仅支持水平触发(LT)

  • 特性:只要 fd 处于就绪状态,就会持续触发事件。
  • 代码特点
    if (FD_ISSET(fd, &readfds)) {
        char buf[1024];
        int n = read(fd, buf, sizeof(buf));  // 读取部分数据即可
        if (n > 0) {
            // 处理数据
        }
    }
    
  • 优势:代码简单,容错性高;即使一次未读完数据,下次仍会触发。

2. Epoll 支持两种模式,边缘触发需特殊处理

  • 边缘触发(ET)特性:仅在 fd 状态变化时触发一次,需一次性处理完所有数据。
  • 代码要求
    if (events[i].events & EPOLLIN) {
        char buf[1024];
        while (1) {  // 必须循环读取直到返回 EAGAIN
            int n = read(fd, buf, sizeof(buf));
            if (n < 0) {
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    break;  // 已读完所有数据
                }
                perror("read error");
                break;
            } else if (n == 0) {
                // 连接关闭
                close(fd);
                break;
            }
            // 处理数据
        }
    }
    
  • 注意事项
    • 必须配合非阻塞 I/O(O_NONBLOCK)使用,否则可能导致死循环。
    • 代码复杂度高,但性能更优(减少事件触发次数)。

3.4 连接数限制与内存占用差异

1. Select 的局限性

  • 文件描述符上限:受 FD_SETSIZE 限制(通常为 1024),需修改内核参数才能提高。
  • 内存占用:每增加一个 fd,需扩展位图空间(如 1024 个 fd 需 128 字节),但整体固定。
  • 性能问题:大量 fd 会导致内核遍历效率降低,且每次需复制整个位图到用户空间。

2. Epoll 的扩展性

  • 连接数无硬性限制:仅受系统资源(如内存)约束,轻松支持数万连接。
  • 内存管理:内核使用红黑树管理 fd,每个 fd 约占 1KB 内存(动态分配)。
  • 零拷贝优化:通过内存映射避免用户态与内核态之间的频繁数据拷贝。

3.5 典型应用场景对比

场景Select 适用性Epoll 适用性
小规模连接(<1000)✅ 简单高效❌ 杀鸡用牛刀
大规模连接(>10000)❌ 性能瓶颈✅ 首选方案
事件处理逻辑简单✅ 代码简洁✅ 更高效
需边缘触发模式❌ 不支持✅ 必须用
跨平台兼容性✅ POSIX 标准❌ Linux 专用

3.6 开发建议

  1. 优先选择 Epoll
    在 Linux 环境下,除非连接数极少或需跨平台,否则应优先使用 Epoll。

  2. 谨慎使用边缘触发
    ET 模式虽性能更高,但需严格遵循 “一次性读完数据” 原则,否则可能导致事件丢失。建议新手先从水平触发(LT)模式入手。

  3. 资源管理

    • Select:注意 FD_SETSIZE 限制,避免创建过多 fd。
    • Epoll:及时通过 epoll_ctl(..., EPOLL_CTL_DEL, ...) 删除不再需要的 fd,避免内存泄漏。
  4. 结合非阻塞 I/O
    Epoll 的 ET 模式必须配合非阻塞 I/O,可通过 fcntl(fd, F_SETFL, O_NONBLOCK) 设置。

总结

维度SelectEpoll
API 复杂度简单,但需重复初始化集合复杂,但只需注册一次事件
事件处理方式轮询所有 fd,O (n) 时间复杂度直接获取就绪 fd,O (1) 时间复杂度
触发模式仅水平触发支持水平和边缘触发
连接数上限受 FD_SETSIZE 限制(默认 1024)无硬性限制,仅受内存约束
内存占用固定大小(与 FD_SETSIZE 相关)动态分配,每个 fd 约 1KB
数据拷贝开销每次调用需复制整个集合使用内存映射,仅复制就绪 fd
代码维护难度低(适合简单场景)高(需处理非阻塞 I/O 和边缘触发)

        理解这些差异后,开发者可根据项目需求(如连接规模、事件处理复杂度)选择合适的技术方案,避免 “用 Select 硬抗高并发” 或 “为小项目过度设计 Epoll” 的陷阱。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小徐不徐说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值