文章目录
- 事件循环线程
- 事件循环相关源码文件
- 事件主循环机制
- 睡眠钩子(Hooks)机制
- 睡眠钩子的实现
- [Epoll 实战:键盘输入监听](#Epoll 实战:键盘输入监听)
事件循环线程
众所周知,Redis 服务器启动时会开启六个线程:
- Thread-1 [redis-server](主线程)
- Thread-2 [bio_close_file](后台 I/O 线程 - 文件关闭)
- Thread-3 [bio_aof](后台 I/O 线程 - AOF 刷盘)
- Thread-4 [bio_lazy_free](后台 I/O 线程 - 惰性删除)
- Thread-5 [jemalloc_bg_thd](内存整理线程)
- Thread-6 [jemalloc_bg_thd](内存整理线程)
而事件循环则位于 Thread-1 [redis-server](主线程)中。
事件循环相关源码文件
| 源码文件 | 说明 |
|---|---|
ae.c |
事件驱动循环核心 |
ae_epoll.c |
Linux 系统实现(epoll) |
ae_evport.c |
Solaris 专用 |
ae_kqueue.c |
macOS 专用 |
ae_select.c |
老旧 Linux 或兼容模式使用 |
ebuckets.c |
事件桶管理 |
eventnotifier.c |
事件通讯器 |
事件主循环机制
Redis 的事件主循环位于 ae.c 中,核心代码如下:
c
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
aeProcessEvents(eventLoop, AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|
AE_CALL_AFTER_SLEEP);
}
}
但死循环非常消耗 CPU,Redis 是如何让线程进入睡眠等待的呢?
真正的"睡眠"发生在 aeApiPoll 中,其核心是调用系统 epoll_wait:
c
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/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|AE_READABLE;
if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
} else if (retval == -1 && errno != EINTR) {
panic("aeApiPoll: epoll_wait, %s", strerror(errno));
}
return numevents;
}
epoll_wait就是实现"睡眠"的系统调用。- 当没有事件时,线程在此阻塞,不消耗 CPU。
睡眠钩子(Hooks)机制
为了在睡眠前后执行必要逻辑,Redis 提供了钩子函数。
在 initServer 函数中注册:
aeSetBeforeSleepProc(server.el, beforeSleep);
aeSetAfterSleepProc(server.el, afterSleep);
在 aeProcessEvents 中调用:
if (eventLoop->beforesleep != NULL && (flags & AE_CALL_BEFORE_SLEEP))
eventLoop->beforesleep(eventLoop);
if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
eventLoop->aftersleep(eventLoop);
作用:
beforeSleep:处理客户端写回、检查阻塞超时、计算下一次epoll_wait的超时时间等。afterSleep:清理或重置休眠期间的状态。
这些钩子避免了无限循环中 CPU 空转,是 Redis 高效的核心之一。
睡眠钩子的实现
beforeSleep钩子和afterSleep钩子的实现都在server.c中,分别是void beforeSleep(struct aeEventLoop *eventLoop)和void afterSleep(struct aeEventLoop *eventLoop)。
实现上beforeSleep远比afterSleep复杂。
Epoll 实战:键盘输入监听
我们可以编写代码练习 epoll 的使用,模拟 Redis 的事件监听机制。
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>
// 简单的错误处理宏
#define panic(fmt, ...) \
do { fprintf(stderr, "FATAL ERROR: " fmt " (%s:%d)\n", ##__VA_ARGS__, __FILE__, __LINE__); \
exit(1); } while(0)
typedef struct epoll_event Event;
int main(void) {
int epfd = epoll_create(1);
if (epfd < 0) {
panic("epoll_create");
return 1;
}
// 设置监听标准输入(键盘)
Event ev;
ev.events = EPOLLIN;
ev.data.fd = STDIN_FILENO;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) < 0) {
panic("epoll_ctl");
return 1;
}
Event events[10];
char buf[1024];
// 事件循环
for (;;) {
int nfds = epoll_wait(epfd, events, 10, -1);
if (nfds < 0) {
panic("epoll_wait");
return 1;
}
// 处理就绪事件
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == STDIN_FILENO) {
int nread = read(STDIN_FILENO, buf, sizeof(buf));
if (nread > 0) {
buf[nread] = '\0';
printf("键盘输入: %s", buf);
}
}
}
}
close(epfd);
return 0;
}
编译运行:
shell
gcc epoll_test.c -o epoll_test
./epoll_test
这个程序会在没有输入时"睡眠",输入内容按回车后被唤醒,完美模拟了 Redis 的事件处理模型。