Redis 源码分析-Redis 中的事件驱动

Redis 源码分析-Redis 中的事件驱动

之前写过 Socket网络通信及IO模型演进,现在看起来还是有些浅显了,恰好最近在读 redis 的源码,又对其有了一些新的理解。

网络编程模型及对比

select、poll、epoll 都是用来实现 IO 多路复用的,而多路复用发展的背景就是 socket 网络通信下提升系统的并发处理能力,单纯依靠多进程或多线程来实现对硬件要求很高,而且上限比较低,采用 IO 多路复用,可以很轻易的处理上千的并发。

虽然多路复用的机制有多种,但每一种都有一些通用的我们需要关注的问题:

  • 该机制监听套接字上的什么事件
  • 该机制能监听多少套接字
  • 当有套接字就绪时,该机制是如何找到就绪套接字的

Linux 针对每一个套接字都会有一个文件描述符(fd),也就是一个非负整数,用来唯一标识该套接字。所以,在多路复用机制的函数中,Linux 通常会用文件描述符作为参数。有了文件描述符,函数也就能找到对应的套接字,进而进行监听、读写等操作。

select

select 机制的核心函数就是 select 函数

c 复制代码
/*
	@Param __nfds 监听的套接字个数
	@Param __readfds 监听读事件的套接字数组
	@Param __writefds 监听写事件的套接字数组
	@Param __exceptfds 监听异常事件的套接字数组
	@Param __timeout 本次调用的阻塞时间
*/
int select (int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)
    
调用后 select 函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。当 select 函数返回后,可以 通过遍历 fdset,来找到就绪的描述符。    

fd_set 的结构体定义是这样的:

c 复制代码
# define __FD_SETSIZE 1024
# define __NFDBITS 32
# define (long int) __fd_mask
typedef struct {
   ...
   __fd_mask  __fds_bits[__FD_SETSIZE / __NFDBITS];
   ...
} fd_set

所以 __fds_bits 字段其实就是 long int 的一个数组,数组中每个元素 32 位,共有 (1024/32 = 32) 个元素,每一位用来标识一个 fd,因此最多标识 1024 个 fd。该字段虽然可以

使用时,系统需要首先创建好需要监听的读事件、写事件、异常事件的描述符集合,接着调用 select 函数,阻塞等待返回就绪的描述符数目。

以下是示例代码:

c 复制代码
int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd)   //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字

fd_set rset;  //被监听的描述符集合,关注描述符上的读事件
 
int max_fd = sock_fd

//初始化rset数组,使用FD_ZERO宏设置每个元素为0 
FD_ZERO(&rset);
//使用FD_SET宏设置rset数组中位置为sock_fd的文件描述符为1,表示需要监听该文件描述符
FD_SET(sock_fd,&rset);

//设置超时时间 
struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;
 
while(1) {
   //调用select函数,检测rset数组保存的文件描述符是否已有读事件就绪,返回就绪的文件描述符个数
   n = select(max_fd+1, &rset, NULL, NULL, &timeout);
 
   //调用FD_ISSET宏,在rset数组中检测sock_fd对应的文件描述符是否就绪
   if (FD_ISSET(sock_fd, &rset)) {
       //如果sock_fd已经就绪,表明已有客户端连接;调用accept函数建立连接
       conn_fd = accept();
       //设置rset数组中位置为conn_fd的文件描述符为1,表示需要监听该文件描述符
       FD_SET(conn_fd, &rset);
   }

   //依次检查已连接套接字的文件描述符
   for (i = 0; i < maxfd; i++) {
        //调用FD_ISSET宏,在rset数组中检测文件描述符是否就绪
       if (FD_ISSET(i, &rset)) {
         //有数据可读,进行读数据处理
       }
   }
}

虽然 select 函数可以一次性监听多个 socket,但是当有 socket 就绪时,需要遍历来查找就绪套接字。

poll

Poll 机制的核心函数是 poll

c 复制代码
/*
   @Param __fds  包括套接字fd的一个结构体数组
   @Param __nfds __fds 数组的元素个数
   @Param __timeout    阻塞等待的超时时间
*/
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);

pollfd 的定义是这样的:

c 复制代码
struct pollfd {
    int fd;        			 //进行监听的文件描述符
    short int events;        //要监听的事件类型
    short int revents;       //实际发生的事件类型
};

Poll 机制的主要流程如下:

  • 创建 pollfd 数组和监听套接字,并进行绑定
  • 将监听套接字加入 pollfd 数组,并设置其监听读事件,也就是客户端的连接请求
  • 循环调用 poll 函数,检测 pollfd 数组中是否有就绪的文件描述符。
    • 如果是连接套接字就绪,这表明是有客户端连接,我们可以调用 accept 接受连接,并创建已连接套接字,并将其加入 pollfd 数组,并监听读事件;
    • 如果是已连接套接字就绪,这表明客户端有读写请求,我们可以调用 recv/send 函数处理读写请求。

流程图:

实例代码:

c 复制代码
int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd)   //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字

//poll函数可以监听的文件描述符数量,可以大于1024
#define MAX_OPEN = 2048

//pollfd结构体数组,对应文件描述符
struct pollfd client[MAX_OPEN];

//将创建的监听套接字加入pollfd数组,并监听其可读事件
client[0].fd = sock_fd;
client[0].events = POLLRDNORM; 
maxfd = 0;

//初始化client数组其他元素为-1
for (i = 1; i < MAX_OPEN; i++)
    client[i].fd = -1; 

while(1) {
   //调用poll函数,检测client数组里的文件描述符是否有就绪的,返回就绪的文件描述符个数
   n = poll(client, maxfd+1, &timeout);
   //如果监听套件字的文件描述符有可读事件,则进行处理
   if (client[0].revents & POLLRDNORM) {
       //有客户端连接;调用accept函数建立连接
       conn_fd = accept();

       //保存已建立连接套接字
       for (i = 1; i < MAX_OPEN; i++){
         if (client[i].fd < 0) {
           client[i].fd = conn_fd; //将已建立连接的文件描述符保存到client数组
           client[i].events = POLLRDNORM; //设置该文件描述符监听可读事件
           break;
          }
       }
       maxfd = i; 
   }
   
   //依次检查已连接套接字的文件描述符
   for (i = 1; i < MAX_OPEN; i++) {
       if (client[i].revents & (POLLRDNORM | POLLERR)) {
         //有数据可读或发生错误,进行读数据处理或错误处理
       }
   }
}

和 select 函数相比,poll 函数的改进之处主要就在于,它允许一次监听超过 1024 个文件描述符。但是当调用了 poll 函数后,我们仍然需要遍历每个文件描述符,检测该描述符是否就绪,然后再进行处理。

epoll

epoll 机制就稍微复杂些了

c 复制代码
typedef union epoll_data
{
  ...
  int fd;  				//记录文件描述符
  ...
} epoll_data_t;


struct epoll_event
{
  uint32_t events;  	// epoll 监听的事件类型
  epoll_data_t data; 	// 应用程序数据
};

对于 epoll 机制来说,我们则需要先调用 epoll_create 函数,创建一个 epoll 实例。这个 epoll 实例内部维护了两个结构,分别是记录要监听的文件描述符已经就绪的文件描述符 ,而对于已经就绪的文件描述符来说,它们会被返回给用户程序进行处理,而不需要用户遍历查找

epoll 机制中有以下几个重要函数:

c 复制代码
// 创建一个 epoll 的句柄(通过它操作 epoll 实例对象),size 参数已经没有实际意义了
int epoll_create(int size);

/*
  对指定描述符 fd 执行 op 操作
  @Param epfd epoll_create(int size)函数返回的 epoll 实例句柄
  @Param op 操作标识(添加、删除、修改)
  @Param fd 需要监听的那个描述符
  @Param event 需要监听的描述符对应的事件
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

/*
  等待 epfd 上的 io 事件,最多返回 maxevents 个事件。
  @Param epfd epoll_create(int size)函数返回的 epoll 实例句柄
  @Param events 存放返回的就绪事件
  @Param maxevents 最多返回的事件数,也是标识 events 的容量
  @Param timeout 超时时间(毫秒,正整数时间,0是非阻塞,-1 永久阻塞直到事件发生)
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll 作为大多数场景下性能最好的 IO 模型,其主要函数的底层的实现逻辑都值得学习

epoll_create

c 复制代码
1. int epoll_create(int size);
创建一个 epoll 的句柄,size 用来告诉内核这个监听的数目一共有多大,在2.6.8之后这个参数就没有实际价值了,因为内核维护一个动态的队列了。

当创建好 epoll 句柄后,它就会占用一个 fd 值,在 linux下 如果查看/proc/进程id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。当某一进程调用 epoll_create 方法时,Linux内核会创建一个 eventpoll 结构体,这个结构体中有两个成员与 epoll 的使用方式密切相关。
    
struct eventpoll {
    // 红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控的事件
    struct rb_root rbr;
    // 双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
    struct list_head rdlist;
    ...
}
  • 在使用完 epoll 后,必须调用close()关闭,否则可能导致 fd 被耗尽。

epoll_ctl

c 复制代码
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上

struct eventpoll {
    // 红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控的事件
    struct rb_root rbr;
    // 双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
    struct list_head rdlist;
    ...
}

每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
  • 所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫 **ep_poll_callback**,它会将发生的事件添加到 rdlist 双链表中

epoll_wait

c 复制代码
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待 epfd 上的 io 事件,最多返回 maxevents 个事件。
通过回调函数内核会将 I/O 准备好的描述符添加到 rdlist 双链表管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,参数 timeout 是超时时间(毫秒,正整数时间,0 是非阻塞,-1 永久阻塞直到事件发生)。该函数返回需要处理的事件数目,如返回 0 表示已超时。

当然epoll对文件描述符的操作有两种模式:LT (level trigger)(默认)和ET (edge trigger)。LT模式是默认模式。

示例代码

c 复制代码
int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd)   //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字
    
epfd = epoll_create(EPOLL_SIZE); //创建epoll实例,
//创建epoll_event结构体数组,保存套接字对应文件描述符和监听事件类型    
ep_events = (epoll_event*)malloc(sizeof(epoll_event) * EPOLL_SIZE);

//创建epoll_event变量
struct epoll_event ee
//监听读事件
ee.events = EPOLLIN;
//监听的文件描述符是刚创建的监听套接字
ee.data.fd = sock_fd;

//将监听套接字加入到监听列表中    
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ee); 
    
while (1) {
   //等待返回已经就绪的描述符 
   n = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); 
   //遍历所有就绪的描述符     
   for (int i = 0; i < n; i++) {
       //如果是监听套接字描述符就绪,表明有一个新客户端连接到来 
       if (ep_events[i].data.fd == sock_fd) { 
          conn_fd = accept(sock_fd); //调用accept()建立连接
          ee.events = EPOLLIN;  
          ee.data.fd = conn_fd;
          //添加对新创建的已连接套接字描述符的监听,监听后续在已连接套接字上的读事件      
          epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ee); 
                
       } else { //如果是已连接套接字描述符就绪,则可以读数据
           ...//读取数据并处理
       }
   }
}

对比

select

  • 优点

    • select 目前几乎在所有的平台(POSIX)上支持,具有良好的跨平台支持。
  • 缺点

    • 单个进程能够监视的文件描述符的数量存在最大限制,它由 FD_SETSIZE 设置,默认值是1024。虽然能够修改宏定义或重新编译内核等进行修改,但可能造成效率的降低。
    • fd 集合在内核被置位过,与传入的 fd 集合不同,不可重用。(内核在发现对应的 fd 有事件后,可能会直接修改 fd 的一些标志,导致 fd 与传入的时候不同,再次调用 select 前需要恢复)。
    • 调用时 fd 集合需要从用户态拷贝到内核态,当 fd 很多时开销会很大。
    • 需要遍历寻找发生事件的 fd 集合,当 fd 很多时开销会很大。

poll

  • 优点
    • 利用 pollfd 数组可以监视的文件描述符无上限,解决了 select 的缺点1。
    • 每次在扫描完成后,只需要重置 pollfd 结构体中的 revents 即可复原,解决了 select 的缺点2。
  • 缺点
    • 调用时 pollfd 数组需要从用户态拷贝到内核态,当 fd 很多时开销会很大。
    • 需要遍历 pollfd 数组寻找发生事件的 fd 集合,当 fd 很多时开销会很大。

epoll

  • 优点
    • 灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。
    • 无需遍历,当调用 epoll_wait 检查是否有事件发生时,只需要检查 eventpoll 对象中的 rdlist 双链表中是否有 epitem 元素即可。如果 rdlist 不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户 。
  • 缺点
    • 每次只遍历活跃的 fd (如果是 LT,也会遍历先前活跃的 fd),在活跃 fd 较少的情况下就会很有优势,如果大部分 fd 都是活跃的,epoll 的效率可能还不如 select/poll。

Redis 中的 select 和 epoll

Redis 针对不同操作系统,会选择不同的 IO 多路复用机制来封装事件驱动框架

c 复制代码
// ae.c
#ifdef HAVE_EVPORT
#include "ae_evport.c"  // Solaris
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"   // Linux
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"  // MacOS
        #else
        #include "ae_select.c"  // Windows
        #endif
    #endif
#endif

按照这个判断逻辑,在 linux 会使用 epoll 而在 windows 会使用 select

在 ae.h 头文件中

c 复制代码
/* File event structure */
/*  IO 事件结构体         */
typedef struct aeFileEvent {
    int mask;                	// 事件类型掩码   /* one of AE_(READABLE|WRITABLE|BARRIER) */
    aeFileProc *rfileProc;  	// 读事件处理函数
    aeFileProc *wfileProc;  	// 写事件处理函数
    void *clientData;       	// 客户端私有数据指针
} aeFileEvent;

/* Time event structure */
/* 时间事件结构体         */
typedef struct aeTimeEvent {
    long long id; /* time event identifier. */
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    aeTimeProc *timeProc;
    aeEventFinalizerProc *finalizerProc;
    void *clientData;
    struct aeTimeEvent *prev;
    struct aeTimeEvent *next;
} aeTimeEvent;

/* A fired event */
/* 已经发生的事件 */
typedef struct aeFiredEvent {
    int fd;			// 文件描述符
    int mask;		// 事件标志
} aeFiredEvent;

/* State of an event based program */
typedef struct aeEventLoop {
    int maxfd;   /* highest file descriptor currently registered */
    int setsize; /* max number of file descriptors tracked */
    long long timeEventNextId;
    time_t lastTime;     /* Used to detect system clock skew */
    aeFileEvent *events; /* Registered events */    // I/O 事件,会涉及到文件描述符
    aeFiredEvent *fired; /* Fired events */         // 已触发事件
    aeTimeEvent *timeEventHead;                     // 时间事件
    int stop;
    void *apidata; /* This is used for polling API specific data */
    aeBeforeSleepProc *beforesleep;
    aeBeforeSleepProc *aftersleep;
} aeEventLoop;

select(ae_select.c)

c 复制代码
#include <sys/select.h>
#include <string.h>

/*
  前面提到 select 传入的 fd 集合可能在内核态被修改,导致无法复用
  redis 设置了 _rfds, _wfds 备份在调用前的 fd 集合, 用于后续的遍历
*/
typedef struct aeApiState {
    fd_set rfds, wfds;
    /* We need to have a copy of the fd sets as it's not safe to reuse
     * FD sets after select(). */
    fd_set _rfds, _wfds;
} aeApiState;

static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));

    if (!state) return -1;
    FD_ZERO(&state->rfds);
    FD_ZERO(&state->wfds);
    
    // 把当前 state 关联进 eventLoop 中
    eventLoop->apidata = state;
    return 0;
}

static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
    /* Just ensure we have enough room in the fd_set type. */
    if (setsize >= FD_SETSIZE) return -1;
    return 0;
}

static void aeApiFree(aeEventLoop *eventLoop) {
    zfree(eventLoop->apidata);
}

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;

    if (mask & AE_READABLE) FD_SET(fd, &state->rfds);
    if (mask & AE_WRITABLE) FD_SET(fd, &state->wfds);
    return 0;
}

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;

    if (mask & AE_READABLE) FD_CLR(fd, &state->rfds);
    if (mask & AE_WRITABLE) FD_CLR(fd, &state->wfds);
}

// 捕获事件
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, j, numevents = 0;

    // 在 select() 系统调用中,fd_set 集合是由内核维护的,并且它在每次调用 select() 时会被内核修改和置位
    // 因此 aeApiState 中使用 _rfds _wfds 备份了原来的 fd,在每次 select 调用前进行备份
    // 保存当前的 rfds 和 wfds 状态,以便后续操作(如 select() 调用后判断文件描述符的状态)时能够使用这些副本而不影响原始的文件描述符集合
    memcpy(&state->_rfds, &state->rfds, sizeof(fd_set));
    memcpy(&state->_wfds, &state->wfds, sizeof(fd_set));

    // select 调用
    // int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    retval = select(eventLoop->maxfd + 1,
                    &state->_rfds, &state->_wfds, NULL, tvp);
    if (retval > 0) {
        for (j = 0; j <= eventLoop->maxfd; j++) {
            int mask = 0;
            aeFileEvent *fe = &eventLoop->events[j];

            if (fe->mask == AE_NONE) continue;
            if (fe->mask & AE_READABLE && FD_ISSET(j, &state->_rfds))
                mask |= AE_READABLE;
            if (fe->mask & AE_WRITABLE && FD_ISSET(j, &state->_wfds))
                mask |= AE_WRITABLE;
            eventLoop->fired[numevents].fd = j;
            eventLoop->fired[numevents].mask = mask;
            numevents++;
        }
    }
    // 返回就绪的事件数量
    return numevents;
}

static char *aeApiName(void) {
    return "select";
}

epoll(ae_epoll.c)

c 复制代码
#include <sys/epoll.h>

typedef struct aeApiState {
    int epfd;       			// epoll 实例描述符
    struct epoll_event *events; // 结构体数组,记录监听事件
} aeApiState;

static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));

    if (!state) return -1;
    // 创建 events 数组
    state->events = zmalloc(sizeof(struct epoll_event) * eventLoop->setsize);
    if (!state->events) {
        zfree(state);
        return -1;
    }

    // 创建 epoll 实例, 参数中的 size 在新版本内核中已无实际意义
    state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
    if (state->epfd == -1) {
        zfree(state->events);
        zfree(state);
        return -1;
    }

    // 将 epoll 信息存入 eventLoop
    eventLoop->apidata = state;
    return 0;
}

static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
    aeApiState *state = eventLoop->apidata;

    state->events = zrealloc(state->events, sizeof(struct epoll_event) * setsize);
    return 0;
}

static void aeApiFree(aeEventLoop *eventLoop) {
    aeApiState *state = eventLoop->apidata;

    close(state->epfd);
    zfree(state->events);
    zfree(state);
}

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0}; /* avoid valgrind warning */
    
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
             EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    
    // 通过 epoll_ctl 注册希望监听的事件
    if (epoll_ctl(state->epfd, op, fd, &ee) == -1) return -1;
    return 0;
}

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0}; /* avoid valgrind warning */
    int mask = eventLoop->events[fd].mask & (~delmask);

    ee.events = 0;
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    if (mask != AE_NONE) {
        epoll_ctl(state->epfd, EPOLL_CTL_MOD, fd, &ee);
    } else {
        /* Note, Kernel < 2.6.9 requires a non null event pointer even for
         * EPOLL_CTL_DEL. */
        epoll_ctl(state->epfd, EPOLL_CTL_DEL, fd, &ee);
    }
}

// 捕获事件
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    // epoll_wait 调用,返回监听到的事件数量
    retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,
                        tvp ? (tvp->tv_sec * 1000 + tvp->tv_usec / 1000) : -1);
    if (retval > 0) {
        int j;

        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events + j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;

            // 保存事件信息
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

static char *aeApiName(void) {
    return "epoll";
}
相关推荐
香饽饽~、1 小时前
【第十一篇】SpringBoot缓存技术
java·开发语言·spring boot·后端·缓存·intellij-idea
大只鹅2 小时前
分布式部署下如何做接口防抖---使用分布式锁
redis·分布式
格调UI成品2 小时前
预警系统安全体系构建:数据加密、权限分级与误报过滤方案
大数据·运维·网络·数据库·安全·预警
MonkeyKing_sunyuhua6 小时前
Ehcache、Caffeine、Spring Cache、Redis、J2Cache、Memcached 和 Guava Cache 的主要区别
redis·spring·memcached
MonkeyKing_sunyuhua6 小时前
Guava Cache 本地项目缓存
缓存·guava
心平愈三千疾6 小时前
通俗理解JVM细节-面试篇
java·jvm·数据库·面试
我科绝伦(Huanhuan Zhou)9 天前
Oracle|Oracle SQL*Plus 配置上下翻页功能
数据库·sql·oracle
Cachel wood9 天前
Spark教程6:Spark 底层执行原理详解
大数据·数据库·分布式·计算机网络·spark
java—大象9 天前
基于java SSM的房屋租赁系统设计和实现
java·开发语言·数据库·spring boot·layui·mybatis
Mutig_s9 天前
Spring Boot动态数据源切换:优雅实现多数据源管理
java·数据库·spring boot·后端·mybatis