Redis 网络模型:IO 多路复用与 ae 事件循环
前言
Redis 之所以能成为高性能的内存数据库,除了基于内存操作之外,其高效的网络模型 功不可没。Redis 采用IO 多路复用 模型配合ae (A Simple Event Loop) 事件循环,实现了单线程处理大量并发连接的能力。本文将从源码层面深入剖析 Redis 的网络模型实现,带你理解其高性能背后的设计哲学。
本文基于 Redis 7.2.5 源码分析
一、为什么需要 IO 多路复用?
1.1 传统 IO 模型的困境
在理解 IO 多路复用之前,我们先看看传统 IO 模型的问题:
| IO 模型 | 描述 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 阻塞 IO | 进程调用 recvfrom,系统调用直到数据准备好才返回 | 简单易用 | 每个连接需要独立线程/进程,资源消耗大 | 连接数少,简单场景 |
| 非阻塞 IO | 轮询调用 recvfrom,立即返回,需要不断重试 | 单线程可处理多连接 | CPU 空转严重,效率低 | 连接数少,响应快 |
| IO 多路复用 | 通过 select/poll/epoll 等系统调用监控多个文件描述符 | 单线程处理大量连接,效率高 | 编程复杂度较高 | 高并发连接场景(如 Redis) |
| 信号驱动 IO | 内核准备好后发信号通知应用 | 异步处理,不阻塞 | 信号处理复杂,可靠性差 | 较少使用 |
| 异步 IO | 内核完成所有操作后通知应用 | 理论性能最优 | 支持不完善,编程难度极大 | Windows IOCP |
1.2 Redis 的选择
Redis 选择了 IO 多路复用 + 单线程 的模型,原因如下:
- 内存操作为主:Redis 的主要瓶颈在内存操作而非 CPU 计算
- 避免锁竞争:单线程避免了多线程的锁竞争和上下文切换开销
- 高效网络处理:IO 多路复用允许单线程同时监听多个连接
多个客户端连接
IO 多路复用
select/poll/epoll
ae 事件循环
单线程事件处理器
命令执行
响应返回客户端
二、ae 事件循环架构
2.1 ae 的核心组件
Redis 的 ae 事件循环由以下几个核心部分组成:
c
// ae.h (Redis 7.2.5)
typedef struct aeEventLoop {
int maxfd; // 当前监听的最大文件描述符
int setsize; // 文件描述符集合大小
long long timeEventNextId; // 下一个时间事件的 ID
time_t lastTime; // 最后一次执行时间
aeFileEvent *events; // 注册的文件事件数组
aeFiredEvent *fired; // 已就绪的文件事件数组
aeTimeEvent *timeEventHead; // 时间事件链表头
int stop; // 事件循环停止标志
void *apidata; // IO 多路复用 API 的私有数据
aeBeforeSleepProc *beforesleep; // 每次事件循环前执行的函数
} aeEventLoop;
2.2 文件事件与时间事件
Redis 将事件分为两类:
| 事件类型 | 描述 | 触发时机 | 示例 |
|---|---|---|---|
| 文件事件 | socket 可读/可写事件 | 客户端发送请求、数据可写 | 客户端发送 SET 命令 |
| 时间事件 | 定时任务 | 到达指定时间 | serverCron 定时清理过期键 |
EventHandler IO_Multiplexing aeEventLoop Client EventHandler IO_Multiplexing aeEventLoop Client loop [事件循环] 连接建立 注册可读事件 调用 aeApiPoll (阻塞) 发送命令 返回就绪事件 执行回调函数 返回响应
三、IO 多路复用实现
3.1 多平台适配策略
Redis 针对不同平台使用了不同的 IO 多路复用机制:
c
// ae.c (Redis 7.2.5)
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
| 平台 | IO 多路复用技术 | 性能 | 说明 |
|---|---|---|---|
| Linux | epoll | ⭐⭐⭐⭐⭐ | Redis 首选,支持百万级连接 |
| macOS/BSD | kqueue | ⭐⭐⭐⭐ | 类似 epoll,BSD 系统原生支持 |
| Solaris | evport | ⭐⭐⭐ | Solaris 10+ 事件端口框架 |
| 其他平台 | select | ⭐⭐ | 通用方案,性能较差 |
3.2 epoll 核心实现(Linux)
c
// ae_epoll.c (Redis 7.2.5)
typedef struct aeApiState {
int epfd; // epoll 文件描述符
struct epoll_event *events; // epoll_wait 返回的事件数组
} aeApiState;
// 创建 epoll 实例
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
// epoll_create1 是 epoll_create 的增强版,更安全
state->epfd = epoll_create1(EPOLL_CLOEXEC);
if (state->epfd == -1) {
zfree(state);
return -1;
}
// 分配事件数组空间
state->events = zmalloc(sizeof(struct epoll_event) * eventLoop->setsize);
eventLoop->apidata = state;
return 0;
}
// 注册/修改事件到 epoll
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0};
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD; // 新增或修改
ee.events = 0;
mask |= eventLoop->events[fd].mask; // 合并已有事件
// 将 AE 的事件类型转换为 epoll 类型
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 int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
// 计算超时时间(毫秒)
int timeout = tvp ? (tvp->tv_sec * 1000 + tvp->tv_usec / 1000) : -1;
// 等待事件就绪
retval = epoll_wait(state->epfd, state->events, eventLoop->setsize, timeout);
if (retval > 0) {
int j;
// 处理每个就绪的事件
numevents = retval;
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events + j;
// 将 epoll 事件类型转换为 AE 事件类型
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;
// 记录到 fired 数组中
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
return numevents; // 返回就绪事件数量
}
代码解析:
- epoll_create1 :创建 epoll 实例,
EPOLL_CLOEXEC标志在 exec 时自动关闭 - epoll_ctl:注册/修改/删除事件监听
- epoll_wait:阻塞等待事件就绪,返回就绪事件列表
四、事件循环主流程
4.1 主函数入口
c
// server.c (Redis 7.2.5)
int main(int argc, char **argv) {
// ... 初始化代码 ...
// 初始化事件循环
server.el = aeCreateEventLoop(server.maxclients + CONFIG_FDSET_INCR);
// 创建 TCP 监听事件处理器
if (server.port != 0 &&
listenToPort(server.port, server.ipfd, &server.ipfd_count) == C_ERR) {
exit(1);
}
// 为每个监听套接字注册 accept 回调
for (int j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler, NULL) == AE_ERR) {
serverPanic("Unrecoverable error creating TCP socket event.");
}
}
// 启动事件循环
aeMain(server.el);
// 清理资源
aeDeleteEventLoop(server.el);
return 0;
}
4.2 aeMain 事件循环
c
// ae.c (Redis 7.2.5)
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 每次循环前调用的函数
if (eventLoop->beforesleep != NULL) {
eventLoop->beforesleep(eventLoop);
}
// 处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
否
是
读事件
写事件
否
是
开始 aeMain
stop 标志?
执行 beforesleep 回调
调用 aeProcessEvents
调用 aeApiPoll
阻塞等待事件
有事件就绪?
遍历就绪事件
事件类型?
执行 read 回调
执行 write 回调
处理时间事件
结束事件循环
4.3 aeProcessEvents 事件处理核心
c
// ae.c (Redis 7.2.5)
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
int processed = 0, numevents;
// 检查是否有时间事件
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
// 获取最近的时间事件超时时间
if (flags & AE_TIME_EVENTS) {
timeval = getLongestTimeEvent(eventLoop);
}
// 如果有文件事件,调用 IO 多路复用 API 等待
if (flags & AE_FILE_EVENTS) {
numevents = aeApiPoll(eventLoop, timeval);
// 处理就绪的文件事件
for (int j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;
// 处理读事件
if (fe->mask & mask & AE_READABLE) {
rfired = 1;
fe->rfileProc(eventLoop, fd, fe->clientData, mask);
}
// 处理写事件
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop, fd, fe->clientData, mask);
}
processed++;
}
}
// 处理时间事件
if (flags & AE_TIME_EVENTS) {
processed += processTimeEvents(eventLoop);
}
return processed; // 返回处理的事件总数
}
五、客户端连接处理流程
5.1 连接建立
c
// networking.c (Redis 7.2.5)
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[NET_IP_STR_LEN];
CLIENT_ENTRY ce;
// 循环接受多个连接(避免饥饿)
while (max--) {
// accept 新连接
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (errno != EWOULDBLOCK) {
serverLog(LL_WARNING,
"Accepting client connection: %s", server.neterr);
}
return;
}
serverLog(LL_VERBOSE, "Accepted %s:%d", cip, cport);
// 创建客户端对象
client *c = createClient(cfd);
// 如果超过最大连接数,拒绝连接
if (listLength(server.clients) > server.maxclients) {
char *err = "-ERR max number of clients reached\r\n";
if (write(c->fd, err, strlen(err)) == -1) {
// Nothing to do
}
server.stat_rejected_conn++;
freeClient(c);
return;
}
server.stat_numconnections++;
}
}
5.2 创建客户端并注册事件
c
// networking.c (Redis 7.2.5)
client *createClient(int fd) {
client *c = zmalloc(sizeof(client));
// 设置文件描述符为非阻塞模式
anetNonBlock(NULL, fd);
anetEnableTcpNoDelay(NULL, fd);
if (server.tcpkeepalive)
anetKeepAlive(NULL, fd, server.tcpkeepalive);
// 注册读事件处理器
if (aeCreateFileEvent(server.el, fd, AE_READABLE,
readQueryFromClient, c) == AE_ERR) {
close(fd);
zfree(c);
return NULL;
}
// 初始化客户端状态
c->fd = fd;
c->name = NULL;
c->bufpos = 0;
c->querybuf = sdsempty();
c->querybuf_peak = 0;
c->reqtype = 0;
c->argc = 0;
c->argv = NULL;
c->cmd = NULL;
// ... 更多初始化代码 ...
// 添加到客户端链表
listAddNodeTail(server.clients, c);
initClientMultiState(c);
return c;
}
5.3 命令读取与处理
c
// networking.c (Redis 7.2.5)
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
client *c = (client *) privdata;
int nread, readlen;
size_t qblen;
// 读取客户端数据到查询缓冲区
readlen = PROTO_IOBUF_LEN; // 默认 16KB
qblen = sdslen(c->querybuf);
// 如果缓冲区需要扩容
if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
// 调整缓冲区大小
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
// 从 socket 读取数据
nread = read(fd, c->querybuf + qblen, readlen);
if (nread == -1) {
if (errno == EAGAIN) {
return;
} else {
serverLog(LL_VERBOSE, "Reading from client: %s", strerror(errno));
freeClient(c);
return;
}
} else if (nread == 0) {
// 客户端关闭连接
serverLog(LL_VERBOSE, "Client closed connection");
freeClient(c);
return;
}
// 更新缓冲区长度
sdsIncrLen(c->querybuf, nread);
c->lastinteraction = server.unixtime;
// 解析并处理命令
if (c->flags & CLIENT_MASTER) {
c->reploff += nread;
}
server.stat_net_input_bytes += nread;
// 处理输入缓冲区中的命令
processInputBuffer(c);
}
命令执行器 processInputBuffer readQueryFromClient aeEventLoop epoll Socket 客户端 命令执行器 processInputBuffer readQueryFromClient aeEventLoop epoll Socket 客户端 发送 SET key value 数据到达 返回就绪事件 调用回调函数 read() 读取数据 数据写入 querybuf 调用 processInputBuffer 解析 RESP 协议 执行 SET 命令 写入 key=value 到内存 注册写事件 写入响应 "+OK\r\n" 返回响应
5.4 响应发送
c
// networking.c (Redis 7.2.5)
void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
client *c = (client *) privdata;
int nwritten = 0, totwritten = 0;
// 处理固定缓冲区的数据
while (c->bufpos > 0) {
nwritten = write(fd, c->buf + c->sentlen, c->bufpos - c->sentlen);
if (nwritten <= 0) break;
c->sentlen += nwritten;
totwritten += nwritten;
// 如果发送完毕,重置缓冲区
if ((int)c->sentlen == c->bufpos) {
c->bufpos = 0;
c->sentlen = 0;
}
}
// 处理回复链表的数据
while (listLength(c->reply)) {
clientReplyBlock *o = listNodeValue(listFirst(c->reply));
nwritten = write(fd, o->buf + c->sentlen, o->used - c->sentlen);
if (nwritten <= 0) break;
c->sentlen += nwritten;
totwritten += nwritten;
// 如果当前块发送完毕
if (c->sentlen == o->used) {
listDelNode(c->reply, listFirst(c->reply));
c->sentlen = 0;
}
}
// 更新统计信息
server.stat_net_output_bytes += totwritten;
// 如果所有数据都发送完毕,删除写事件监听
if (c->bufpos == 0 && listLength(c->reply) == 0) {
aeDeleteFileEvent(server.el, c->fd, AE_WRITABLE);
}
}
六、性能优化与最佳实践
6.1 Redis 网络模型优化点
| 优化项 | 优化方法 | 效果 |
|---|---|---|
| 非阻塞 IO | 所有 socket 设置为非阻塞模式 | 避免单连接阻塞影响其他连接 |
| 边缘触发 vs 水平触发 | Redis 使用水平触发(LT) | 简化编程模型,可靠性强 |
| 批量接受连接 | acceptTcpHandler 循环接受多个连接 | 避免高并发下连接建立延迟 |
| 零拷贝(可选) | sendfile 系统调用传输大文件 | 减少 CPU 和内存开销 |
| TCP_NODELAY | 禁用 Nagle 算法 | 减少小包延迟 |
| TCP_KEEPALIVE | 启用 TCP 保活机制 | 及时检测死连接 |
6.2 边缘触发 vs 水平触发
c
// epoll 两种触发模式对比
/*
* 水平触发(Level Triggered, LT)- Redis 采用
*
* 特点:
* 1. 只要缓冲区有数据,就会一直触发
* 2. 一次没处理完,下次还会触发
* 3. 编程简单,不易出错
*
* 适用场景:大多数应用(包括 Redis)
*/
/*
* 边缘触发(Edge Triggered, ET)
*
* 特点:
* 1. 只有状态变化时才触发一次
* 2. 必须一次性读完所有数据
* 3. 性能更高,但编程复杂
*
* 适用场景:Nginx 等高性能服务器
*/
// Redis 使用水平触发的原因:
// 1. 单线程模型,ET 优势不明显
// 2. 简化代码逻辑,提高可靠性
// 3. 避免复杂的错误处理
6.3 实战代码示例
下面是一个简化版的 Redis 风格的事件循环实现:
c
// mini_redis_event_loop.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAX_EVENTS 64
#define PORT 6379
#define BACKLOG 128
// 设置 socket 为非阻塞模式
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl get");
return -1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl set");
return -1;
}
return 0;
}
// 简化的命令处理函数
void process_command(int client_fd, const char *cmd) {
char response[128];
// 简单的 PING 命令处理
if (strncmp(cmd, "PING", 4) == 0) {
strcpy(response, "+PONG\r\n");
}
// 简单的 GET 命令处理(模拟)
else if (strncmp(cmd, "GET", 3) == 0) {
strcpy(response, "$5\r\nhello\r\n");
}
// 简单的 SET 命令处理(模拟)
else if (strncmp(cmd, "SET", 3) == 0) {
strcpy(response, "+OK\r\n");
}
else {
strcpy(response, "-ERR unknown command\r\n");
}
// 发送响应
write(client_fd, response, strlen(response));
}
int main() {
int listen_fd, epoll_fd;
struct epoll_event event, *events;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
// 创建监听 socket
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket");
exit(1);
}
// 设置 socket 选项,允许地址重用
int optval = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
// 绑定地址和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(listen_fd);
exit(1);
}
// 开始监听
if (listen(listen_fd, BACKLOG) == -1) {
perror("listen");
close(listen_fd);
exit(1);
}
// 设置为非阻塞模式
set_nonblocking(listen_fd);
// 创建 epoll 实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(listen_fd);
exit(1);
}
// 注册监听 socket 的可读事件
event.events = EPOLLIN | EPOLLET; // 使用边缘触发
event.data.fd = listen_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) == -1) {
perror("epoll_ctl");
close(listen_fd);
close(epoll_fd);
exit(1);
}
// 分配事件数组
events = malloc(sizeof(struct epoll_event) * MAX_EVENTS);
if (!events) {
perror("malloc");
close(listen_fd);
close(epoll_fd);
exit(1);
}
printf("Mini Redis server started on port %d\n", PORT);
// 事件循环
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
if (errno == EINTR) continue;
perror("epoll_wait");
break;
}
// 处理就绪事件
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) {
// 接受新连接
while (1) {
int client_fd = accept(listen_fd,
(struct sockaddr *)&client_addr,
&client_len);
if (client_fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 没有更多连接
} else {
perror("accept");
break;
}
}
printf("New connection from %s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 设置为非阻塞
set_nonblocking(client_fd);
// 注册到 epoll
event.events = EPOLLIN | EPOLLET;
event.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
perror("epoll_ctl client");
close(client_fd);
}
}
} else {
// 处理客户端数据
int client_fd = events[i].data.fd;
char buffer[1024];
ssize_t count;
while (1) {
count = read(client_fd, buffer, sizeof(buffer) - 1);
if (count == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 数据读完
} else {
perror("read");
close(client_fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
break;
}
} else if (count == 0) {
// 客户端关闭连接
printf("Client disconnected\n");
close(client_fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
break;
} else {
buffer[count] = '\0';
printf("Received: %s", buffer);
// 处理命令
process_command(client_fd, buffer);
}
}
}
}
}
// 清理资源
free(events);
close(listen_fd);
close(epoll_fd);
return 0;
}
编译运行:
bash
gcc -o mini_redis mini_redis_event_loop.c
./mini_redis
测试连接:
bash
telnet localhost 6379
# 输入: PING
# 响应: +PONG
七、总结与展望
7.1 Redis 网络模型核心要点
| 核心组件 | 作用 | 技术要点 |
|---|---|---|
| ae 事件循环 | 统一调度文件事件和时间事件 | 单线程事件驱动 |
| IO 多路复用 | 同时监听多个 socket | epoll/kqueue/select |
| 非阻塞 IO | 避免单连接阻塞 | O_NONBLOCK 标志 |
| 事件处理器 | 处理连接、读写、命令 | 回调函数机制 |
| 响应缓冲 | 异步发送响应数据 | 固定缓冲区 + 链表 |
7.2 性能优势总结
Redis 高性能
内存操作
高效网络模型
IO 多路复用
单线程事件循环
非阻塞 IO
零拷贝可选
单线程处理 10万+ 连接
无锁竞争, 无上下文切换
避免连接相互影响
降低 CPU 和内存开销
7.3 未来发展方向
- IO 优化:探索 io_uring 等新一代异步 IO 接口
- 多线程扩展:Redis 6.0+ 引入多线程网络 IO
- 零拷贝优化:更多场景使用 sendfile/splice
- 性能监控:更细粒度的性能指标采集
参考资料
- Redis 官方文档:https://redis.io/documentation
- Redis 7.2.5 源码:https://github.com/redis/redis/tree/7.2.5
- Linux epoll 手册:man 7 epoll
- 《Redis 设计与实现》 - 黄健宏
- 《UNIX 网络编程》 - W. Richard Stevens
标签:Redis, 网络模型, IO多路复用, 事件循环, epoll, 源码分析
作者简介:专注于高性能网络编程和分布式系统研究,深入理解 Redis、Nginx 等开源项目源码。
版权声明:本文基于 Redis 7.2.5 源码分析,遵循 CC-BY-NC-SA 4.0 协议。