epoll 系列系统调用(I/O复用函数)

本文详细介绍了Linuxepoll的内核事件表机制,比较了LT和ET模式的异同,以及EPOLLONESHOT事件如何避免竞态问题,提高并发处理效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

内核事件表

epoll 是liunx特有的I/O复用函数 。它在实现和使用上与select 。poll有很大差异。
首先,epoll 使用一组函数来完成任务,而不是单个函数
其次,epoll把用户关心的文件描述符上面的事件放在内核里的一个事件表中,
从而无须向select和poll那样每次调用都要重复传入文件描述符 或者事件集 。
但是epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。
这个文件描述符使用如下 epoll_create 函数来创建:

#include<sys/epoll.h>
int epoll_create(int size )

size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大
该函数返回的文件描述符将作用于其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

下面的函数用来操作 epoll的内核事件表:

#include <sys/epoll.h>
int epoll_ctl(int epfd,int op, int fd ,struct epoll_event *event)

fd参数是要操作的文件描述符 ,op参数则指定操作类型 。操作类型有如下三种

  • EOPLL_CTL_ADD, 往事件表中注册 fd上的事件
  • EOPLL_CTL_MOD , 修改fd上的注册事件。
  • EOPLL_CTL_DEL , 删除fd上的注册事件。

event参数指定事件,它是epoll_event结构指针类型。 epoll_event的定义如下:

struct epoll_event
{	
	_uint32_t events; /* epoll 事件  */
	epoll_data_t  data; /* 用户数据  */
}

其中events成员描述事件类型。epoll 支持的事件类型和poll基本相同。表示epoll 事件类型的宏是在poll对应的宏前加上"E",
比如epoll的数据可读事件是EPOLLIN。 但是epoll有关额外的事件类型–EPOLLET 和EPOLLONESHOT。 它们对于epoll 的高效运行非常关键。 将在后面讨论。
data成员用于存储用户数据,其类型Eopll_data_t定义如下:

 typedef union epoll_data
 {
	void * ptr ;
	int fd ;
	uint32_t u32;
	uint64_t u64;
}epoll_data_t ;

epoll_data_t 是个联合体,其四个成员 中使用最多的是fd,它是指定事件所从属的目标文件描述符。
ptr成员可用来指定与fd相关的用户数据。但由于epoll_data_t 是联合体, 我们不能同时使用 其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问。只能使用其他手段。
比如放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd
epoll成功时返回0 失败则返回-1 并设置相关的errno

epoll_wait函数

epoll 系列函数调用的主要接口是epoll_wait函数 。它在一段超时时间 内等待一组文件描述符上面的事件,其原型如下

#include <sys/epoll.h>
int epoll_wait(int epfd ,struct epoll_event *events,int maxevents, int timeout);

该函数成功时返回就绪的文件描述符的个数,失败时 返回-1 并设置相关的errno。
关于该函数的参数 从后往前讨论。

  • timeout参数的函数和poll接口的timeout参数 相同.
  • maxevents参数指定最多的监听多少个事件,它必须大于0
  • epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中(由epfd参数指定) 中复制到它的第二个参数 events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪时间,而不像select和poll的数组参数那样即用于传入用户注册事件,又用户输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述的效率。

下面展示了这个差别

/* 如何索引 poll返回的就绪文件 描述符*/
int ret = poll(fds ,MAX_EVENT_NUMBER ,-1)
/* 必须遍历到所有已经注册文件描述符 并找到其中就绪者  (当然可以利用ret来做点优化)*/
for(int i = 0; i,MAX_EVENT_NUMBER;++i)
{
	if(fds[i].revents & POLLIN) /* 判断第i 个 文件描述符是否就绪*/
	{
		int sockfd = fds[i].fd ;
		/* 处理 sockfd */ 
	}
}

/*如何索引到epoll 返回的就绪文件描述符 */
int ret =epoll_wait(epollfd, events,MAX_EVENT_NUMBER, -1);
/* 仅遍历就绪的ret个文件描述符 */
for(int i =0 ;i<ret ;i++)
{
	int sockfd = events[i].data.fd;
	/*sockfd 肯定就绪,直接处理*/

}

LT 和 ET模式

epoll对文件描述符的操作 有两种模式:LT (Level Trigger ,电平触发)模式和ET (Edge Trigger,边沿触发)模式。LT模式是默认的工作模式,这种模式下的eopll相关于一个效率较高的poll。当往epoll内核事件上注册一个文件描述符上的EPOLLET事件时 ,epoll将以ET模式来操作当前文件描述符.ET模式是epoll的高效工作模式。
对于采用LE工作模式的文件描述符 ,当epoll_wait检测到其上有事件发生并将此事件通知给应用程序后,应用程序可以不立刻处理改时间。这样,当应用程序下一次调用epoll_wait时,epoll函数在想应用程序告知 此事件。知道该事件被处理为止。
而对于采用ET工作模式的文件描述符。当epoll_wait检测到其上有时间发生并将此时间通知给应用程序后,应用程序必须立刻处理该事件 ,因为后续的epoll_wait调用将不再向应用程序通知此之间。可见ET模式在很大程度上降低了 同一个epoll事件被重复触发的次数。
因此效率要比LT模式高。

代码可以演示差距/结果待补充/

/*
show the diff  about ET and LT
*/

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <stdbool.h>
#include <pthread.h>
#include <libgen.h>

#define  MAX_EVENT_NUMBER 1024
#define  BUFFER_SIZE 10

/* 将文件描述符设置为非阻塞的*/
int setnonblocking (int fd)
{
    int old_option = fcntl(fd ,F_GETFL) ;
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd ,F_SETFL, new_option );
    return old_option;
}
/* 将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中 ,参数enable_et指定是否对fd启用ET模式 */
void addfd(int epollfd ,int fd ,bool enable_et)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events =EPOLLIN;
    if(enable_et)
    {
        event.events |= EPOLLET;
    }
    epoll_ctl(epollfd ,EPOLL_CTL_ADD,fd,&event );
    setnonblocking(fd);
}

/** LT模式工作流程 */
void lT(struct epoll_event *events ,int number ,int epollfd ,int listenfd)
{
    char buf[ BUFFER_SIZE ];
    for(int i =0 ;i< number ;i++)
    {
        int sockfd =events[i].data.fd;
        if( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof(client_address );
            int connfd = accept(listenfd ,(struct sockaddr *) &client_address, &client_addrlength);
            addfd (epollfd ,connfd ,false) ; /* 对connfd禁用了 ET模式*/
        }
        else if( events[i].events & EPOLLIN)
        {
            /* 只要socket 读缓存中还有数据没有读出 ,这段代码就会被触发*/
            printf("event trigger once \n");
            memset(buf ,'\0',BUFFER_SIZE);
            int ret = recv(sockfd ,buf,BUFFER_SIZE-1,0);
            if(ret <= 0)
            {
                close(sockfd);
                continue;
            }
            printf("get %d bytes of content :%s \n ",ret ,buf);
        }
        else
        {
            printf("something else happend \n");
        }
    }
}

/* ET模式的工作流程 */
void et(struct epoll_event * events,int number ,int epollfd ,int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for(int i =0 ;i< number ;i++)
    {
        int sockfd =events[i].data.fd;
        if( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof(client_address );
            int connfd = accept(listenfd ,(struct sockaddr *) &client_address, &client_addrlength);
            addfd (epollfd ,connfd ,false) ; /* 对connfd禁用了 ET模式*/
        }
        else if( events[i].events & EPOLLIN)
        {
            /* 这段代码不会被重复触发 ,所以我们循环读取数据 以确保socket读取缓存中的所有数据读取*/
            printf("event trigger once \n");
            while(1)
            {
                memset(buf, '\0', BUFFER_SIZE);
                int ret = recv (sockfd ,buf ,BUFFER_SIZE-1,0);
                if(ret < 0)
                {
                    /*对于非阻塞IO ,下面的条件成立表示数据已经全部读取完毕 ,此后,epoll能够再次触发 sockfd上EPOLLIN事件 以驱动下一次读操作 */
                    if((errno == EAGAIN) || (errno == EWOULDBLOCK))
                    {
                        printf("read later \n");
                        break;
                    }
                    close(sockfd);
                    break;
                }
                else if(ret == 0)
                {
                    close(sockfd);
                }
                else
                {
                    printf("get %d bytes of content :%s \n", ret ,buf);
                }
            }
        }
        else
        {
            printf("something else happend \n");
        }
    }
}

int main(int argc , char * argv[])
{
    if(argc <= 2)
    {
        printf("usage : %s ip_address port_number \n", basename(argv[0]));
        return 1;
    }
    const char * ip = argv [1];
    int port =atoi(argv[2]);

    int ret = 0;
    struct sockaddr_in address;
    bzero(&address ,sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip ,&address.sin_addr);
    address.sin_port =htons(port);

    int listenfd = socket( PF_INET, SOCK_STREAM, 0);
    assert(ret >= 0);

    ret =bind(listenfd ,(struct sockaddr *) &address ,sizeof(address ));
    assert(ret != -1);

    ret = listen(listenfd ,5);
    assert(ret != -1);

    struct epoll_event events[ MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    assert(epollfd != -1);

    addfd(epollfd ,listenfd ,true);

    while(1)
    {
        int ret = epoll_wait(epollfd , events, MAX_EVENT_NUMBER, -1);

        if(ret < 0)
        {
            printf("epoll failure \n");
            break;
        }

        lT(events ,ret,epollfd ,listenfd); /*使用 LT模式*/
        //et(events ,ret,epollfd ,listenfd ); /*使用ET模式*/
    }

    close (listenfd );
    return 0;
}

结果

当我们启用LT模式 水平触发

用telnet 去连接这个 地址和端口的时候
在这里插入图片描述
可以看到 当数据没有读完的时候 一直在触发

启动ET 边缘触发

在这里插入图片描述

可以看到边缘触发只触发了一次

EPOLLONESHOT事件

  • 即使我们使用ET模式,一个socket上面的某个事件还是可能被多次触发,这在并发的程序中就会引起一个文件,比如一个线程在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket又有新的数据可出(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据,于是就出现两个线程同时操作一个socket的局面。这当然不是我们锁期望的。我们期望的是一个socket连接在任意时刻都只被一个线程处理。这一点可以使用epoll的EPOLLONESHOT事件实现。
  • 对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读可写或者异常的事件,且只触发一次,除非我们使用epoll_ctl函数重置了该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时。其他线程是不可能有机会操作该socket。但是反过来思考 ,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就立刻重置这个socket上的EPOLLONESHOT事件 ,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket

源码

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024
struct fds
{
   int epollfd;
   int sockfd;
};

int setnonblocking( int fd )
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}

void addfd( int epollfd, int fd, bool oneshot )
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    if( oneshot )
    {
        event.events |= EPOLLONESHOT;
    }
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}

void reset_oneshot( int epollfd, int fd )
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event );
}

void* worker( void* arg )
{
    int sockfd = ( (fds*)arg )->sockfd;
    int epollfd = ( (fds*)arg )->epollfd;
    printf( "start new thread to receive data on fd: %d\n", sockfd );
    char buf[ BUFFER_SIZE ];
    memset( buf, '\0', BUFFER_SIZE );
    while( 1 )
    {
        int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
        if( ret == 0 )
        {
            close( sockfd );
            printf( "foreiner closed the connection\n" );
            break;
        }
        else if( ret < 0 )
        {
            if( errno == EAGAIN )
            {
                reset_oneshot( epollfd, sockfd );
                printf( "read later\n" );
                break;
            }
        }
        else
        {
            printf( "get content: %s\n", buf );
            sleep( 5 );
        }
    }
    printf( "end thread receiving data on fd: %d\n", sockfd );
}

int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );

    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );

    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );

    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

    ret = listen( listenfd, 5 );
    assert( ret != -1 );

    epoll_event events[ MAX_EVENT_NUMBER ];
    int epollfd = epoll_create( 5 );
    assert( epollfd != -1 );
    addfd( epollfd, listenfd, false );

    while( 1 )
    {
        int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
        if ( ret < 0 )
        {
            printf( "epoll failure\n" );
            break;
        }
    
        for ( int i = 0; i < ret; i++ )
        {
            int sockfd = events[i].data.fd;
            if ( sockfd == listenfd )
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof( client_address );
                int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
                addfd( epollfd, connfd, true );
            }
            else if ( events[i].events & EPOLLIN )
            {
                pthread_t thread;
                fds fds_for_new_worker;
                fds_for_new_worker.epollfd = epollfd;
                fds_for_new_worker.sockfd = sockfd;
                pthread_create( &thread, NULL, worker, ( void* )&fds_for_new_worker );
            }
            else
            {
                printf( "something else happened \n" );
            }
        }
    }

    close( listenfd );
    return 0;
}

从工作线程函数worker来看,如果一个工作线程处理完某个socket上的一次请求(我们用休眠五秒来模拟这个过程)之后,有收到了新的改socket上的客户请求,则改线程将继续为这个socket服务,并且因为改socket上注册了EPOLLONESHOT事件,其他线程没有机会接触到这个socket。如果工作线程等待5s后仍然没有收到该socket上的下一批客户数据,则它将放弃为该socket服务器,同时它调用retset_ontshot函数去重置 该socket上的注册时间,这将使epoll有机会再次检测到该socket的EPOLLIN事件,进而使得其他线程有机会为该socket服务器。由此看来同一个socket在不同时间可能被不同的线程处理,但是统一时刻肯定只有一个线程为它服务,这就保证了连接的完整性。从而避免了很多竞态问题。

<think>好的,我现在需要为用户介绍Linux下的epoll多路I/O复用模型。首先,用户可能已经了解过select和poll,所以需要对比说明epoll的优势。接下来,要解释epoll的三个主要函数epoll_create、epoll_ctl、epoll_wait,以及它们的作用。 然后,需要详细说明epoll的工作模式,LT(水平触发)和ET(边缘触发)的区别,以及各自的适用场景。用户可能对这两种模式的实际应用不太清楚,所以需要举例子说明,比如在LT模式下,如果没有处理完数据,会反复通知,而ET模式则只在状态变化时通知一次,需要一次处理完所有数据。 还要提到epoll的高效性,比如使用红黑树管理文件描述符,以及就绪列表的结构,这样在处理大量连接时性能更好。同时,需要强调epoll适合高并发的场景,比如Web服务器。 可能用户对epoll的使用步骤不太清楚,需要分步骤说明:创建epoll实例、注册感兴趣的事件、等待事件发生并处理。每个步骤对应的函数和参数需要简要说明。 另外,用户可能会遇到常见问题,比如ET模式下的非阻塞IO为什么是必须的,需要解释ET模式下如果不一次读完数据,可能因为没有新事件到来而导致数据滞留。这时候需要提醒用户使用非阻塞socket,并在读取时循环读取直到EAGAIN错误。 还需要提到epoll的优点和缺点,比如优点是可扩展性好,适合大量连接,缺点是在连接数少时可能不如select和poll高效,或者实现相对复杂。 最后,给出一个简单的代码示例,帮助用户理解epoll的基本用法,比如创建epoll实例,添加监听socket,然后进入事件循环处理连接和读写事件。代码中要注意错误处理,以及ET模式的设置方法。 要确保语言通俗易懂,避免过多术语堆砌,结构清晰,分点说明。同时,检查是否有技术细节错误,比如epoll的数据结构确实是红黑树和就绪链表,函数参数是否正确等。还要注意编译时需要链接的库,比如是否需要-lrt或者其他选项。 可能还需要对比epoll与其他IO复用机制的差异,突出其优势,比如时间复杂度更低,不需要每次调用都传递全部fd集合。这样用户能更好地理解为什么epoll在大规模并发下更高效。 最后,提醒用户在实际开发中需要注意的事项,比如正确处理错误码,避免文件描述符泄漏,以及合理设置事件类型等。</think>### Linux epoll多路I/O复用模型详解 --- #### 一、核心概念对比 | 特性 | select | poll | **epoll** | |--------------------|-------------------------------------|------------------------------------|------------------------------------| | 时间复杂度 | $O(n)$ 线性扫描 | $O(n)$ 线性扫描 | $O(1)$ 事件驱动 | | 最大文件描述符数 | FD_SETSIZE(1024) | 无限制 | 系统级限制(百万级) | | 内存管理 | 每次传递完整fd_set | 动态数组 | 内核红黑树+就绪链表 | | 触发方式 | 水平触发(LT) | 水平触发(LT) | 支持LT/ET两种模式 | --- #### 二、epoll三大核心函数 1. **epoll_create** ```c int epoll_create(int size); // 创建epoll实例(size在Linux 2.6.8后已弃用) ``` - 返回:epoll文件描述符(需手动关闭) - 内核数据结构:红黑树(存储监控fd) + 就绪链表(存储活跃事件) 2. **epoll_ctl** ```c int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); ``` - 操作类型: - `EPOLL_CTL_ADD` 添加监控 - `EPOLL_CTL_MOD` 修改事件 - `EPOLL_CTL_DEL` 移除监控 - 事件结构: ```c struct epoll_event { uint32_t events; // EPOLLIN | EPOLLOUT | EPOLLET等 epoll_data_t data; // 用户数据(常存fd) }; ``` 3. **epoll_wait** ```c int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); ``` - 返回:就绪事件数量 - 参数: - `events`:预分配的接收缓冲区 - `timeout`:-1阻塞 / 0立即返回 / >0毫秒超时 --- #### 三、工作模式详解 1. **水平触发(LT)** - 特性:只要fd处于就绪状态,持续通知 - 行为模式: - 读:缓冲区有未读数据时反复触发 - 写:缓冲区可写入时反复触发 - 优点:编程简单,兼容select/poll行为 - 适用场景:传统服务器开发 2. **边缘触发(ET)** - 特性:仅在fd状态变化时通知一次 - 必须配合: - 非阻塞IO(`fcntl(fd, F_SETFL, O_NONBLOCK)`) - 循环读取直到`EAGAIN`错误 - 优点:减少系统调用次数,更高性能 - 适用场景:高频交易系统、高性能服务器 --- #### 四、完整代码示例 ```c #include <sys/epoll.h> #include <fcntl.h> #include <unistd.h> #define MAX_EVENTS 10 int main() { int epoll_fd = epoll_create1(0); struct epoll_event ev, events[MAX_EVENTS]; // 添加监听socket到epoll(示例) int listen_sock = socket(...); fcntl(listen_sock, F_SETFL, O_NONBLOCK); // 非阻塞 ev.events = EPOLLIN | EPOLLET; // ET模式 ev.data.fd = listen_sock; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &ev); while(1) { int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); for(int i=0; i<n; i++) { if(events[i].data.fd == listen_sock) { // 处理新连接 while(1) { // 必须循环处理ET模式 int conn_fd = accept(listen_sock, ...); if(conn_fd == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) break; // 已无新连接 // 设置新连接为非阻塞并加入epoll监控 } } else { // 处理数据I/O(必须循环读取到EAGAIN) char buf[1024]; while(read(events[i].data.fd, buf, sizeof(buf)) > 0); } } } close(epoll_fd); } ``` --- #### 五、性能优化要点 1. **事件注册策略** - 读事件:`EPOLLIN | EPOLLRDHUP`(检测对端关闭) - 写事件:仅在需要时注册,避免频繁触发 2. **惊群问题** - 解决方案:Linux 3.9+ 使用`EPOLLEXCLUSIVE`标志 3. **内存管理** - 避免每次`epoll_wait`都动态分配事件数组 4. **超时设置** - 定时器整合:配合`timerfd`实现精确超时控制 --- #### 六、适用场景分析 | 场景 | 推荐模式 | 说明 | |---------------------|---------------|------------------------------| | HTTP服务器 | LT模式 | 简单可靠,兼容短连接 | | 实时消息推送 | ET模式 | 高频事件处理更高效 | | 文件传输服务 | ET+非阻塞IO | 大文件传输需高效处理 | | 金融交易系统 | ET+多epoll实例| 超低延迟要求,多核负载均衡 | --- #### 七、调试与监控 1. **状态查看** ```bash lsof -p [pid] | grep epoll # 查看epoll实例 cat /proc/[pid]/fdinfo/[epoll_fd] # 查看监控fd列表 ``` 2. **性能分析** ```bash perf record -e syscalls:sys_enter_epoll* # 跟踪epoll系统调用 strace -e epoll_ctl,epoll_wait ./program # 监控函数调用 ``` --- **选择建议**:在连接数大于1000的高并发场景优先选择epoll,传统场景可考虑select/poll。ET模式需要更严谨的代码设计,建议在性能关键路径使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值