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";
}
相关推荐
r i c k1 小时前
数据库系统学习笔记
数据库·笔记·学习
野犬寒鸦1 小时前
从零起步学习JVM || 第一章:类加载器与双亲委派机制模型详解
java·jvm·数据库·后端·学习
IvorySQL2 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·2 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德2 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫3 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i3 小时前
完全卸载MariaDB
数据库·mariadb
期待のcode3 小时前
Redis的主从复制与集群
运维·服务器·redis
纤纡.3 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql
jiunian_cn3 小时前
【Redis】渐进式遍历
数据库·redis·缓存