1 高性能网络编程进阶指南(C/C++)
本文面向有基础 socket 编程经验的 C/C++ 程序员,系统讲解从 I/O 模型、零拷贝、内核旁路到协议栈优化的完整知识体系,目标是让读者能够独立设计并实现百万并发级别的网络服务。
1.1 网络 I/O 模型演进
1.1.1 五种 I/O 模型对比
理解 I/O 模型是一切高性能网络编程的前提。POSIX 定义了五种 I/O 模型,核心区别在于谁等待、等多久。
五种 I/O 模型时序对比:
阻塞 I/O (BIO):
用户进程 内核
│─── recvfrom ───►│
│ (阻塞等待) │── 等待数据到达 ──►│
│ │── 数据拷贝 ──────►│
│◄──── 返回 ───────│
非阻塞 I/O (NIO):
用户进程 内核
│─── recvfrom ───►│── EAGAIN(无数据)
│─── recvfrom ───►│── EAGAIN
│─── recvfrom ───►│── 数据拷贝 ──►│
│◄──── 返回 ───────│
(轮询,CPU 浪费)
I/O 多路复用 (select/poll/epoll):
用户进程 内核
│─── epoll_wait ──►│
│ (阻塞在此) │── 监听多个 fd ──►│
│◄── 就绪通知 ──────│
│─── recvfrom ───►│── 立即返回数据
(一个线程管理 N 个连接)
信号驱动 I/O (SIGIO):
用户进程 内核
│── 注册 SIGIO ───►│
│(继续执行其他工作)│── 等待数据 ──────►│
│◄── SIGIO 信号 ───│
│─── recvfrom ───►│── 立即返回数据
异步 I/O (AIO):
用户进程 内核
│── aio_read ─────►│
│(继续执行其他工作)│── 等待 + 拷贝 ───►│
│◄── 完成通知 ──────│
(数据已在用户缓冲区,真正的异步)
1.1.2 阻塞 I/O --- 最简单但不可扩展
c
// 最朴素的单线程阻塞服务器
// 致命缺陷:同一时刻只能服务一个客户端
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, SOMAXCONN);
while (1) {
int client_fd = accept(server_fd, NULL, NULL);
// 阻塞处理,期间无法接受新连接
handle_client(client_fd);
close(client_fd);
}
问题:C10K(1 万并发连接)时,每个连接一个线程 → 内存耗尽(每线程默认 8MB 栈)、上下文切换开销巨大。
1.1.3 非阻塞 I/O --- 忙等轮询,不实用
c
// 设置非阻塞
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
// 轮询读取(浪费 CPU)
while (1) {
ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 数据未就绪,继续轮询(CPU 空转)
continue;
}
// 真实错误
break;
}
// 处理数据
}
结论:单独使用非阻塞 I/O 意义不大,必须配合 I/O 多路复用才有价值。
1.1.4 select/poll 的局限性
c
// select:最多 FD_SETSIZE(1024) 个 fd,每次调用都要重置
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(server_fd, &read_fds);
// 每次都要传递完整 fd_set 到内核(O(n) 拷贝)
// 返回后还要遍历整个集合找到就绪的 fd(O(n) 扫描)
select(max_fd + 1, &read_fds, NULL, NULL, NULL);
// poll:突破 1024 限制,但仍有 O(n) 扫描问题
struct pollfd fds[MAX_CONNECTIONS];
poll(fds, nfds, -1);
// 每次调用传递整个数组,内核返回就绪个数,用户仍需遍历
select/poll 的根本问题:
- 每次调用都要将监听集合从用户态拷贝到内核态
- 内核无法告知"哪个 fd 就绪",只能全量扫描
- 随连接数增长,性能线性下降(O(n))
1.2 epoll 深度掌握
1.2.1 epoll 的设计哲学
epoll 通过以下机制解决 select/poll 的问题:
-
红黑树存储所有监听的 fd(增删改 O(log n))
-
就绪链表存储有事件的 fd(内核直接维护)
-
事件驱动:只返回就绪的 fd,无需全量扫描
-
减少拷贝 :epoll 本身并没有使用共享内存来减少拷贝,epoll 减少的拷贝指的是,它把"监听集合"永久存在内核的红黑树里,不需要每次调用都从用户态重新传入 。select/poll 每次调用都要把整个监听集合传给内核,epoll 只在
epoll_ctl增删改时传一次,之后epoll_wait不需要再传。这才是它减少的拷贝。epoll 内核数据结构:
epoll 实例 (eventpoll)
├── 红黑树 (rbr) ← epoll_ctl ADD/MOD/DEL 操作此树
│ ├── fd_A (epitem)
│ ├── fd_B (epitem)
│ └── fd_C (epitem)
└── 就绪链表 (rdllist) ← 内核将就绪的 epitem 移入此链表
├── fd_A ← epoll_wait 只从这里取,O(1)
└── fd_C
1.2.2 三个核心 API
c
#include <sys/epoll.h>
// 1. 创建 epoll 实例
// size 参数已被忽略,但必须 > 0
int epfd = epoll_create1(EPOLL_CLOEXEC);
// EPOLL_CLOEXEC:exec 时自动关闭,防止子进程继承
// 2. 注册/修改/删除 fd
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 关注可读事件,边缘触发
ev.data.fd = client_fd; // 或存指针:ev.data.ptr = conn_obj;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev); // 添加
epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &ev); // 修改
epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL); // 删除
// 3. 等待事件
struct epoll_event events[MAX_EVENTS];
int nready = epoll_wait(epfd, events, MAX_EVENTS, timeout_ms);
// timeout = -1:永久阻塞
// timeout = 0 :立即返回(非阻塞检查)
for (int i = 0; i < nready; i++) {
if (events[i].data.fd == server_fd) {
// 新连接
} else {
// 已有连接的 I/O 事件
}
}
1.2.3 LT vs ET:水平触发与边缘触发
这是 epoll 最重要也最容易出错的概念:
水平触发(LT,默认):
┌──────────────────────────────────────────────┐
│ 只要 fd 处于就绪状态,每次 epoll_wait 都通知 │
│ 适合初学者,不易出 bug │
└──────────────────────────────────────────────┘
场景:接收缓冲区有 1000 字节,每次只读 100 字节
→ 只要缓冲区非空,epoll_wait 每次都返回该 fd
→ 可以分多次读取,不会丢数据
边缘触发(ET):
┌──────────────────────────────────────────────┐
│ 仅在 fd 状态变化时通知一次(从不可读变为可读) │
│ 性能更高,但必须一次性读完所有数据 │
└──────────────────────────────────────────────┘
场景:接收缓冲区有 1000 字节,只读了 100 字节
→ epoll_wait 不再通知!剩余 900 字节永久滞留
→ 必须循环读到 EAGAIN 为止
c
// ET 模式下的正确读法:必须循环读到 EAGAIN
void et_read(int fd, Buffer *buf) {
while (1) {
char tmp[4096];
ssize_t n = recv(fd, tmp, sizeof(tmp), 0);
if (n > 0) {
buffer_append(buf, tmp, n);
// 继续读,直到读完
} else if (n == 0) {
// 对端关闭连接
close_connection(fd);
break;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 数据读完,退出循环
break;
}
if (errno == EINTR) {
// 信号中断,继续读
continue;
}
// 真实错误
handle_error(fd);
break;
}
}
}
1.2.4 EPOLLONESHOT --- 多线程安全
c
// 问题:多线程处理时,同一 fd 可能被两个线程同时处理
// 场景:线程A正在处理 fd_X,内核又触发了 fd_X 的新事件
// 线程B也拿到了 fd_X → 数据竞争!
// 解决:EPOLLONESHOT --- 触发一次后自动从 epoll 中禁用该 fd
ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
// 处理完后,必须手动重新激活
void rearm_fd(int epfd, int fd) {
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
1.2.5 常见事件标志速查
| 标志 | 含义 | 说明 |
|---|---|---|
EPOLLIN |
可读 | 接收缓冲区有数据,或对端关闭(读返回 0) |
EPOLLOUT |
可写 | 发送缓冲区有空间 |
EPOLLRDHUP |
对端关闭 | 比检查 read==0 更优雅 |
EPOLLPRI |
紧急数据 | 带外数据(OOB) |
EPOLLERR |
错误 | 总是监听,无需显式设置 |
EPOLLHUP |
挂起 | fd 被挂起,通常伴随 EPOLLERR |
EPOLLET |
边缘触发 | 设置后切换为 ET 模式 |
EPOLLONESHOT |
一次性 | 触发后自动禁用,需手动重启 |
EPOLLEXCLUSIVE |
排他唤醒 | 多个 epoll 监听同一 fd 时,只唤醒一个(防惊群) |
1.3 Reactor 与 Proactor 模式
1.3.1 Reactor 模式
Reactor 是基于同步 I/O 的事件驱动模式,epoll 是其在 Linux 上的标准实现。
Reactor 核心组件:
┌─────────────┐ 事件注册 ┌─────────────────┐
│ Handle │ ◄──────────────── │ Event Handler │
│ (fd) │ │ (业务处理回调) │
└─────────────┘ └─────────────────┘
│ I/O 就绪 ▲
▼ │ dispatch
┌─────────────────────────────────────────┤
│ Reactor (Event Loop) │
│ epoll_wait → 找到对应 Handler → 调用 │
└──────────────────────────────────────────┘
单 Reactor 单线程(Redis 6.0 前的模型):
c
// 伪代码:单线程 Reactor
typedef void (*event_handler_t)(int fd, void *data);
typedef struct {
event_handler_t handler;
void *data;
} EventEntry;
EventEntry table[MAX_FD]; // fd → handler 映射表
void event_loop(int epfd) {
struct epoll_event events[MAX_EVENTS];
while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
// 分发到注册的 handler
table[fd].handler(fd, table[fd].data);
}
}
}
单 Reactor 多线程(处理器密集型业务):
主线程 工作线程池
│ │
epoll_wait │
│── I/O 读写(主线程完成) │
│── 将业务逻辑丢入线程池 ─────────►│── CPU 密集计算
│── 继续 epoll_wait │◄── 结果返回
│── 写响应(主线程完成) │
多 Reactor 多线程(Netty、Nginx 模型,最常用):
Main Reactor(主线程)
│── 只负责 accept 新连接
│── 将 client_fd 分发给 Sub Reactor
│
├──► Sub Reactor 1(线程1)── epoll 管理一批连接 ── 线程池处理业务
├──► Sub Reactor 2(线程2)── epoll 管理一批连接 ── 线程池处理业务
└──► Sub Reactor N(线程N)── epoll 管理一批连接 ── 线程池处理业务
c
// 多 Reactor 的连接分发(Round-Robin)
int g_sub_reactor_count = 4;
SubReactor sub_reactors[4];
int g_next = 0;
void on_new_connection(int epfd, int server_fd) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept4(server_fd,
(struct sockaddr *)&client_addr,
&len,
SOCK_NONBLOCK | SOCK_CLOEXEC);
// Round-Robin 分发到 Sub Reactor
int idx = g_next++ % g_sub_reactor_count;
sub_reactor_add_conn(&sub_reactors[idx], client_fd);
}
1.3.2 Proactor 模式
Proactor 基于异步 I/O,操作系统完成 I/O 后才通知应用(数据已在用户缓冲区)。Linux AIO 实现不完善,Windows IOCP 是经典实现。io_uring 是 Linux 上最接近 Proactor 的现代方案(见第 9 章)。
Reactor vs Proactor 本质区别:
Reactor: 我告诉你 fd 就绪了,你自己去读/写
Proactor: 你告诉我读到哪里,我读完了通知你(数据已就位)
1.4 零拷贝技术
1.4.1 传统数据路径的四次拷贝
传统 read() + write() 发送文件:
磁盘 → 内核页缓存 (DMA 拷贝,硬件完成)
内核页缓存 → 用户缓冲区(CPU 拷贝) ← 可优化掉
用户缓冲区 → Socket 发送缓冲区(CPU 拷贝) ← 可优化掉
Socket 缓冲区 → 网卡(DMA 拷贝,硬件完成)
共 2 次 CPU 拷贝 + 2 次 DMA 拷贝 + 4 次上下文切换
1.4.2 sendfile --- 文件到网络零拷贝
c
#include <sys/sendfile.h>
// sendfile:直接从文件 fd 发送到 socket fd
// 内核内部完成:页缓存 → Socket 缓冲区,无需经过用户态
ssize_t sendfile(int out_fd, // 目标:socket fd
int in_fd, // 源:文件 fd
off_t *offset,// 文件偏移,NULL 从当前位置
size_t count);// 发送字节数
// 实际使用
int file_fd = open("large_file.bin", O_RDONLY);
struct stat st;
fstat(file_fd, &st);
off_t offset = 0;
ssize_t sent = sendfile(socket_fd, file_fd, &offset, st.st_size);
// HTTP 静态文件服务核心逻辑
void serve_file(int client_fd, const char *path) {
int file_fd = open(path, O_RDONLY);
if (file_fd < 0) { send_404(client_fd); return; }
struct stat st;
fstat(file_fd, &st);
// 发送 HTTP 头
char header[256];
int hlen = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Length: %ld\r\n"
"Connection: keep-alive\r\n\r\n",
(long)st.st_size);
send(client_fd, header, hlen, 0);
// 零拷贝发送文件体
off_t offset = 0;
while (offset < st.st_size) {
ssize_t n = sendfile(client_fd, file_fd,
&offset, st.st_size - offset);
if (n <= 0) break;
}
close(file_fd);
}
sendfile 数据路径(支持 scatter-gather DMA 的网卡):
磁盘 → 内核页缓存 (DMA 拷贝)
内核页缓存 → 网卡 (DMA 拷贝,仅传递描述符)
共 0 次 CPU 拷贝 + 2 次 DMA 拷贝 + 2 次上下文切换
1.4.3 mmap --- 内存映射文件
c
#include <sys/mman.h>
// mmap 将文件映射到进程地址空间
// 读取映射区域时,内核直接从页缓存取数据,省去一次拷贝
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
// 场景:对文件内容做处理后发送
int fd = open("data.bin", O_RDONLY);
struct stat st;
fstat(fd, &st);
char *data = mmap(NULL, st.st_size,
PROT_READ,
MAP_PRIVATE | MAP_POPULATE, // 预读
fd, 0);
// 直接访问,无需 read()
process_data(data, st.st_size);
// 配合 write 发送(仍有一次拷贝:页缓存→socket缓冲区)
write(socket_fd, data, st.st_size);
munmap(data, st.st_size);
close(fd);
c
// madvise 优化内存访问模式
madvise(data, st.st_size, MADV_SEQUENTIAL); // 顺序访问,预读
madvise(data, st.st_size, MADV_RANDOM); // 随机访问,禁用预读
madvise(data, st.st_size, MADV_WILLNEED); // 即将使用,预加载
madvise(data, st.st_size, MADV_DONTNEED); // 不再需要,释放页
1.4.4 splice --- 内核管道零拷贝
c
#include <fcntl.h>
// splice:在两个 fd 之间移动数据(必须有一个是管道)
// 完全在内核中完成,零 CPU 拷贝
ssize_t splice(int fd_in, loff_t *off_in,
int fd_out, loff_t *off_out,
size_t len, unsigned int flags);
// 通过管道中转:文件 → 管道 → socket
int pipefd[2];
pipe(pipefd);
// 文件 → 管道(零拷贝)
splice(file_fd, NULL, pipefd[1], NULL, file_size,
SPLICE_F_MOVE | SPLICE_F_MORE);
// 管道 → socket(零拷贝)
splice(pipefd[0], NULL, socket_fd, NULL, file_size,
SPLICE_F_MOVE);
close(pipefd[0]);
close(pipefd[1]);
1.4.5 各零拷贝技术对比
| 方案 | CPU 拷贝次数 | 适用场景 | 限制 |
|---|---|---|---|
| 传统 read+write | 2 | 通用 | --- |
| mmap+write | 1 | 文件处理后发送 | 小文件有额外开销 |
| sendfile | 0 | 静态文件服务 | 只能文件→socket |
| sendfile+SG-DMA | 0 | 静态文件服务(现代网卡) | 需网卡支持 |
| splice | 0 | 数据转发代理 | 必须经过管道 |
1.5 Socket 选项与内核调优
1.5.1 关键 Socket 选项
c
// ── 地址重用(服务重启不用等待 TIME_WAIT)──
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// ── 端口重用(多进程/线程共享监听同一端口,内核负载均衡)──
// Nginx 多 worker 进程模型的核心技术
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
// ── 禁用 Nagle 算法(降低小包延迟)──
// Nagle:积累数据到 MSS 或收到 ACK 才发送
// 交互式应用(游戏、RPC)必须禁用
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
// ── TCP KeepAlive(检测死连接)──
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt));
int keepidle = 60; // 60 秒无数据后开始探测
int keepintvl = 5; // 每 5 秒探测一次
int keepcnt = 3; // 探测 3 次无响应则断开
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(int));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(int));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(int));
// ── 发送/接收缓冲区大小 ──
int sndbuf = 4 * 1024 * 1024; // 4MB 发送缓冲
int rcvbuf = 4 * 1024 * 1024; // 4MB 接收缓冲
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
// 注:内核会将设置值翻倍(用于内部管理),实际是 8MB
// ── 延迟关闭(发完缓冲区再关闭)──
struct linger lg = {.l_onoff = 1, .l_linger = 5};
setsockopt(fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
// ── TCP_CORK:批量发送(与 TCP_NODELAY 互斥)──
// 积累数据,发送完毕后取消 CORK 触发一次发送
int cork = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));
// ... 写入多个小块数据 ...
cork = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));
// ── TCP_QUICKACK:立即发送 ACK(降低延迟)──
setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &opt, sizeof(opt));
// ── accept4:一步创建非阻塞 socket ──
// 比 accept + fcntl 少一次系统调用
int client_fd = accept4(server_fd, NULL, NULL,
SOCK_NONBLOCK | SOCK_CLOEXEC);
1.5.2 系统内核参数调优
bash
# ── 文件描述符限制 ──
# 查看当前限制
ulimit -n
# 临时修改(当前会话)
ulimit -n 1000000
# 永久修改
echo "* soft nofile 1000000" >> /etc/security/limits.conf
echo "* hard nofile 1000000" >> /etc/security/limits.conf
# ── 系统级 fd 总数 ──
sysctl -w fs.file-max=2000000
echo "fs.file-max = 2000000" >> /etc/sysctl.conf
# ── TCP 参数优化 ──
cat >> /etc/sysctl.conf << 'EOF'
# 接收/发送缓冲区(自动调整范围)
net.core.rmem_max = 134217728 # 128MB
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
# 开启 TCP BBR 拥塞控制(需内核 4.9+)
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# 加快 TIME_WAIT 回收
net.ipv4.tcp_tw_reuse = 1 # 复用 TIME_WAIT socket
net.ipv4.tcp_fin_timeout = 15 # FIN_WAIT2 超时时间(秒)
# 增大半连接/全连接队列
net.ipv4.tcp_max_syn_backlog = 65536 # SYN 半连接队列
net.core.somaxconn = 65536 # accept 全连接队列上限
# 本地端口范围(客户端发起连接时)
net.ipv4.ip_local_port_range = 1024 65535
# 禁用慢启动重启(长肥管道场景)
net.ipv4.tcp_slow_start_after_idle = 0
# 开启 TCP Fast Open
net.ipv4.tcp_fastopen = 3
EOF
sysctl -p
1.5.3 listen backlog 与连接队列
c
// Linux TCP 连接的两个队列:
//
// SYN 半连接队列(net.ipv4.tcp_max_syn_backlog):
// 收到 SYN → 发送 SYN-ACK → 等待客户端 ACK
// 队列满时新 SYN 被丢弃 → 客户端超时重传
//
// Accept 全连接队列(min(backlog, net.core.somaxconn)):
// 三次握手完成 → 等待 accept() 取走
// 队列满时新连接被丢弃(或发送 RST,取决于 tcp_abort_on_overflow)
// backlog 参数设置全连接队列大小
listen(server_fd, 65535);
// 监控队列溢出
// ss -lnt 查看 Recv-Q(待 accept 队列长度)
// netstat -s | grep "SYNs to LISTEN" 看丢包数
1.6 多线程网络架构
1.6.1 One Loop Per Thread 模型
这是目前工业界最广泛使用的高性能网络架构(muduo、libevent、Nginx 均采用此思路):
每个 I/O 线程运行一个独立的 event loop(epoll)
线程之间通过消息队列 + eventfd 通信,不共享 fd
主线程(Accept Loop)
│
│── accept 新连接
│── 选择负载最低的 I/O 线程
│── 通过 eventfd 唤醒目标线程
│
▼
I/O 线程1 I/O 线程2 I/O 线程N
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ event loop │ │ event loop │ │ event loop │
│ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │
│ │ conn_A │ │ │ │ conn_C │ │ │ │ conn_E │ │
│ │ conn_B │ │ │ │ conn_D │ │ │ │ ... │ │
│ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │
└──────────────┘ └──────────────┘ └──────────────┘
c
// eventfd:线程间轻量唤醒机制
#include <sys/eventfd.h>
int evfd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
// 唤醒目标线程(写入任意值即可)
void wakeup_thread(int evfd) {
uint64_t val = 1;
write(evfd, &val, sizeof(val));
}
// 目标线程的 event loop 将 evfd 注册到 epoll
// 被唤醒后清空计数
void handle_wakeup(int evfd) {
uint64_t val;
read(evfd, &val, sizeof(val));
// 处理跨线程提交的任务队列
process_pending_tasks();
}
1.6.2 无锁任务队列
c
// 使用原子操作实现跨线程任务投递,避免锁竞争
#include <stdatomic.h>
#define QUEUE_SIZE 4096 // 必须是 2 的幂
typedef struct {
void (*func)(void *);
void *arg;
} Task;
typedef struct {
Task tasks[QUEUE_SIZE];
atomic_size_t head;
atomic_size_t tail;
} LockFreeQueue;
// 生产者:主线程投递任务
int queue_push(LockFreeQueue *q, Task task) {
size_t tail = atomic_load_explicit(&q->tail, memory_order_relaxed);
size_t next = (tail + 1) & (QUEUE_SIZE - 1);
if (next == atomic_load_explicit(&q->head, memory_order_acquire)) {
return -1; // 队列满
}
q->tasks[tail] = task;
atomic_store_explicit(&q->tail, next, memory_order_release);
return 0;
}
// 消费者:I/O 线程处理任务
int queue_pop(LockFreeQueue *q, Task *task) {
size_t head = atomic_load_explicit(&q->head, memory_order_relaxed);
if (head == atomic_load_explicit(&q->tail, memory_order_acquire)) {
return -1; // 队列空
}
*task = q->tasks[head];
atomic_store_explicit(&q->head,
(head + 1) & (QUEUE_SIZE - 1),
memory_order_release);
return 0;
}
1.6.3 SO_REUSEPORT 多进程模型(Nginx 风格)
c
// 多个 worker 进程各自 bind 同一端口
// 内核自动做负载均衡(基于四元组 hash 到不同进程)
// 优点:无主进程转发开销,真正并行 accept
void start_worker(int worker_id) {
int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
listen(fd, 65535);
// 每个 worker 有自己的 epoll,各自 accept
int epfd = epoll_create1(EPOLL_CLOEXEC);
// ... 注册 fd 到 epoll,运行 event loop
}
// 主进程 fork N 个 worker
int cpu_count = sysconf(_SC_NPROCESSORS_ONLN);
for (int i = 0; i < cpu_count; i++) {
if (fork() == 0) {
start_worker(i);
exit(0);
}
}
1.7 内存管理与对象池
1.7.1 内存池 --- 消除 malloc 热点
高并发场景下,频繁的 malloc/free 是严重的性能瓶颈(锁竞争、内存碎片)。
c
// 固定大小对象池
typedef struct PoolNode {
struct PoolNode *next;
} PoolNode;
typedef struct {
PoolNode *free_list; // 可用对象链表
void *memory; // 原始内存块
size_t obj_size; // 每个对象的大小
int capacity; // 总容量
pthread_mutex_t lock;
} ObjectPool;
ObjectPool *pool_create(size_t obj_size, int capacity) {
ObjectPool *pool = malloc(sizeof(ObjectPool));
pool->obj_size = obj_size < sizeof(PoolNode) ?
sizeof(PoolNode) : obj_size;
pool->capacity = capacity;
pool->memory = malloc(pool->obj_size * capacity);
pool->free_list = NULL;
pthread_mutex_init(&pool->lock, NULL);
// 将所有对象串成链表
char *p = pool->memory;
for (int i = 0; i < capacity; i++) {
PoolNode *node = (PoolNode *)p;
node->next = pool->free_list;
pool->free_list = node;
p += pool->obj_size;
}
return pool;
}
void *pool_alloc(ObjectPool *pool) {
pthread_mutex_lock(&pool->lock);
PoolNode *node = pool->free_list;
if (node) pool->free_list = node->next;
pthread_mutex_unlock(&pool->lock);
return node;
}
void pool_free(ObjectPool *pool, void *ptr) {
PoolNode *node = (PoolNode *)ptr;
pthread_mutex_lock(&pool->lock);
node->next = pool->free_list;
pool->free_list = node;
pthread_mutex_unlock(&pool->lock);
}
1.7.2 环形缓冲区(Ring Buffer)
c
// 无锁单生产者单消费者环形缓冲区
// 适合 I/O 线程读取数据 + 业务线程消费数据的场景
typedef struct {
char *buf;
size_t capacity;
atomic_size_t read_idx;
atomic_size_t write_idx;
} RingBuffer;
RingBuffer *ringbuf_create(size_t capacity) {
// capacity 必须是 2 的幂,方便取模
assert((capacity & (capacity - 1)) == 0);
RingBuffer *rb = malloc(sizeof(RingBuffer));
rb->buf = malloc(capacity);
rb->capacity = capacity;
atomic_init(&rb->read_idx, 0);
atomic_init(&rb->write_idx, 0);
return rb;
}
// 可写字节数
size_t ringbuf_writable(RingBuffer *rb) {
size_t w = atomic_load_explicit(&rb->write_idx, memory_order_relaxed);
size_t r = atomic_load_explicit(&rb->read_idx, memory_order_acquire);
return rb->capacity - (w - r);
}
// 写入数据(生产者)
int ringbuf_write(RingBuffer *rb, const char *data, size_t len) {
if (ringbuf_writable(rb) < len) return -1;
size_t w = atomic_load_explicit(&rb->write_idx, memory_order_relaxed);
size_t mask = rb->capacity - 1;
for (size_t i = 0; i < len; i++) {
rb->buf[(w + i) & mask] = data[i];
}
atomic_store_explicit(&rb->write_idx, w + len, memory_order_release);
return 0;
}
1.7.3 scatter-gather I/O(writev/readv)
c
#include <sys/uio.h>
// writev:一次系统调用发送多个不连续的内存块
// 避免拼接 header + body 时的额外拷贝
char header[128];
int hlen = build_http_header(header, sizeof(header), body_len);
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = hlen;
iov[1].iov_base = body;
iov[1].iov_len = body_len;
// 一次系统调用发送 header + body
ssize_t sent = writev(socket_fd, iov, 2);
// 处理部分发送(非阻塞 socket 常见)
void writev_all(int fd, struct iovec *iov, int iovcnt) {
while (iovcnt > 0) {
ssize_t n = writev(fd, iov, iovcnt);
if (n <= 0) break;
// 调整 iov 指针(跳过已发送部分)
while (iovcnt > 0 && n >= (ssize_t)iov->iov_len) {
n -= iov->iov_len;
iov++;
iovcnt--;
}
if (iovcnt > 0) {
iov->iov_base = (char *)iov->iov_base + n;
iov->iov_len -= n;
}
}
}
1.8 协议设计与序列化
1.8.1 私有二进制协议设计
c
// 典型的高性能私有协议帧格式
// 定长头部 + 变长 body,避免 HTTP 文本解析开销
// 帧头:16 字节,字节对齐
#pragma pack(push, 1)
typedef struct {
uint32_t magic; // 魔数,用于帧同步(如 0xCAFEBABE)
uint8_t version; // 协议版本
uint8_t type; // 消息类型(请求/响应/心跳)
uint16_t flags; // 扩展标志位
uint32_t seq_id; // 序列号(用于请求-响应匹配)
uint32_t body_len; // body 长度
} FrameHeader; // sizeof = 16
#pragma pack(pop)
#define MAGIC 0xCAFEBABEU
#define VERSION 1
#define TYPE_REQUEST 0x01
#define TYPE_RESPONSE 0x02
#define TYPE_HEARTBEAT 0x03
// 帧读取状态机(处理 TCP 粘包/拆包)
typedef enum {
STATE_HEADER, // 正在读取 header
STATE_BODY, // 正在读取 body
} ParseState;
typedef struct {
ParseState state;
FrameHeader header;
size_t header_read; // 已读取的 header 字节数
char *body;
size_t body_read; // 已读取的 body 字节数
} FrameParser;
// 增量解析(处理任意大小的数据块)
int frame_parse(FrameParser *p, const char *data, size_t len,
void (*on_frame)(FrameHeader *, char *)) {
size_t pos = 0;
while (pos < len) {
if (p->state == STATE_HEADER) {
// 读 header
size_t need = sizeof(FrameHeader) - p->header_read;
size_t copy = len - pos < need ? len - pos : need;
memcpy((char *)&p->header + p->header_read, data + pos, copy);
p->header_read += copy;
pos += copy;
if (p->header_read == sizeof(FrameHeader)) {
// header 读完:验证魔数,准备读 body
if (ntohl(p->header.magic) != MAGIC) return -1;
p->header.body_len = ntohl(p->header.body_len);
p->body = malloc(p->header.body_len);
p->body_read = 0;
p->state = STATE_BODY;
}
} else {
// 读 body
size_t need = p->header.body_len - p->body_read;
size_t copy = len - pos < need ? len - pos : need;
memcpy(p->body + p->body_read, data + pos, copy);
p->body_read += copy;
pos += copy;
if (p->body_read == p->header.body_len) {
// 完整帧
on_frame(&p->header, p->body);
free(p->body);
p->body = NULL;
p->header_read = 0;
p->state = STATE_HEADER;
}
}
}
return 0;
}
1.8.2 处理 TCP 粘包拆包
TCP 是字节流协议,没有消息边界:
发送方:send("Hello") + send("World")
接收方可能收到:
- "HelloWorld" (粘包)
- "He" + "lloWorld" (拆包)
- "Hello" + "World" (正常)
三种解决方案:
1. 定长消息:每次读取固定字节数(最简单,灵活性差)
2. 分隔符:如 \r\n(HTTP 头),需要扫描缓冲区
3. 长度前缀:头部含 body 长度(推荐,高效)← 上面协议采用此方案
1.9 内核旁路:DPDK 与 io_uring
1.9.1 io_uring --- Linux 异步 I/O 的未来
io_uring(内核 5.1 引入)通过共享内存环形队列,实现真正的异步 I/O,大幅减少系统调用次数。
io_uring 架构:
用户态 内核态
┌──────────────────┐ ┌──────────────────┐
│ SQ(提交队列) │◄─mmap─────│ SQ Ring Buffer │
│ SQE 数组 │───提交────►│ 异步执行 I/O │
├──────────────────┤ ├──────────────────┤
│ CQ(完成队列) │◄─完成─────│ CQ Ring Buffer │
└──────────────────┘ └──────────────────┘
批量提交,无需每次系统调用,内核自动排空队列
c
#include <liburing.h>
#include <unistd.h>
#define QUEUE_DEPTH 256
// 初始化 io_uring
struct io_uring ring;
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
// ── 异步 read ──
void submit_async_read(struct io_uring *ring, int fd,
char *buf, size_t len, void *user_data) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_recv(sqe, fd, buf, len, 0);
io_uring_sqe_set_data(sqe, user_data); // 携带上下文
io_uring_submit(ring); // 提交
}
// ── 异步 write ──
void submit_async_write(struct io_uring *ring, int fd,
const char *buf, size_t len, void *user_data) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_send(sqe, fd, buf, len, 0);
io_uring_sqe_set_data(sqe, user_data);
io_uring_submit(ring);
}
// ── 批量提交(减少系统调用次数)──
void batch_submit(struct io_uring *ring, Request *reqs, int n) {
for (int i = 0; i < n; i++) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_recv(sqe, reqs[i].fd,
reqs[i].buf, reqs[i].len, 0);
io_uring_sqe_set_data(sqe, &reqs[i]);
}
// 一次系统调用提交 n 个 I/O 请求
io_uring_submit(ring);
}
// ── 收割完成事件 ──
void event_loop(struct io_uring *ring) {
struct io_uring_cqe *cqe;
while (1) {
// 等待至少一个完成事件
io_uring_wait_cqe(ring, &cqe);
// 批量处理所有完成事件
unsigned head;
unsigned count = 0;
io_uring_for_each_cqe(ring, head, cqe) {
Request *req = io_uring_cqe_get_data(cqe);
if (cqe->res < 0) {
fprintf(stderr, "I/O error: %s\n", strerror(-cqe->res));
} else {
handle_completion(req, cqe->res);
}
count++;
}
// 一次批量标记所有已处理的 cqe
io_uring_cq_advance(ring, count);
}
}
// 清理
io_uring_queue_exit(&ring);
c
// io_uring 高级特性:固定缓冲区(减少内存注册开销)
struct iovec iovecs[BUFFER_COUNT];
for (int i = 0; i < BUFFER_COUNT; i++) {
iovecs[i].iov_base = malloc(BUFFER_SIZE);
iovecs[i].iov_len = BUFFER_SIZE;
}
// 注册固定缓冲区(与内核共享,无需每次拷贝)
io_uring_register_buffers(&ring, iovecs, BUFFER_COUNT);
// 使用固定缓冲区读取(比普通 prep_recv 更快)
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read_fixed(sqe, fd, iovecs[0].iov_base,
BUFFER_SIZE, 0, 0 /* buffer index */);
1.9.2 DPDK --- 完全绕过内核协议栈
DPDK(Data Plane Development Kit)直接在用户态操作网卡,彻底绕过 Linux 内核网络栈,可实现千万级 pps(packets per second)。
传统网络路径: DPDK 路径:
网卡 → 内核驱动 网卡 → DPDK PMD(用户态驱动)
→ 内核协议栈 → 用户态协议栈(DPDK 自带)
→ socket 缓冲区 → 应用直接处理
→ 系统调用
→ 用户态应用
(多次上下文切换) (零上下文切换,轮询模式)
c
// DPDK 核心概念(简化示意)
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_mbuf.h>
// 初始化 DPDK 环境抽象层
int argc = ...;
char **argv = ...;
rte_eal_init(argc, argv);
// 创建内存池(mbuf 池)
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
"MBUF_POOL",
NUM_MBUFS, // mbuf 数量
MBUF_CACHE_SIZE, // per-core 缓存大小
0, // 私有数据大小
RTE_MBUF_DEFAULT_BUF_SIZE,
rte_socket_id());
// 配置网卡队列
uint16_t port_id = 0;
rte_eth_dev_configure(port_id, 1, 1, &port_conf);
rte_eth_rx_queue_setup(port_id, 0, RX_RING_SIZE,
rte_eth_dev_socket_id(port_id),
NULL, mbuf_pool);
rte_eth_tx_queue_setup(port_id, 0, TX_RING_SIZE,
rte_eth_dev_socket_id(port_id), NULL);
rte_eth_dev_start(port_id);
// 轮询收包(不依赖中断,极低延迟)
struct rte_mbuf *pkts[BURST_SIZE];
while (1) {
uint16_t nb_rx = rte_eth_rx_burst(port_id, 0, pkts, BURST_SIZE);
for (int i = 0; i < nb_rx; i++) {
process_packet(pkts[i]);
rte_pktmbuf_free(pkts[i]);
}
}
DPDK 适用场景:
- 软件路由器、防火墙、负载均衡器
- 高频交易(微秒级延迟)
- 5G 核心网用户面(UPF)
- 流量采集与分析
1.10 性能分析与调优工具链
1.10.1 perf --- 性能热点分析
bash
# 采集 CPU 性能数据(10 秒)
perf stat -a sleep 10
# 找到热点函数(采样分析)
perf record -g -p <pid> sleep 10
perf report --stdio
# 实时查看系统调用频率
perf top -e syscalls:sys_enter_epoll_wait
# 追踪 context switch
perf stat -e context-switches,cpu-migrations -p <pid> sleep 5
1.10.2 strace / ltrace --- 系统调用追踪
bash
# 统计系统调用次数和耗时(用于找出不必要的 syscall)
strace -c -p <pid>
# 输出示例(找到 syscall 热点):
# % time seconds usecs/call calls errors syscall
# ------- ----------- ----------- --------- --------- ------
# 45.23 0.001234 2 617 epoll_wait
# 30.12 0.000823 1 823 read
# ...
# 追踪特定系统调用
strace -e trace=recv,send,epoll_wait -p <pid>
1.10.3 ss --- 网络连接状态诊断
bash
# 查看 TCP 连接状态统计
ss -s
# 查看监听 socket 的队列情况
# Recv-Q:等待 accept 的连接数
# Send-Q:全连接队列最大值
ss -ltn
# 查看 TIME_WAIT 连接数量(过多说明连接复用不足)
ss -tan | grep TIME-WAIT | wc -l
# 查看 TCP 详细信息(RTT、拥塞窗口)
ss -tin dst :8080
# 按进程过滤
ss -tp | grep nginx
1.10.4 netstat / ethtool --- 网卡与协议层统计
bash
# 查看网络错误统计
netstat -s | grep -E "failed|error|drop|overflow"
# 重点关注:
# "listen queue overflows" → accept 队列溢出,需增大 backlog
# "SYNs to LISTEN" → SYN 半连接队列溢出
# "segments retransmited" → 重传率高,网络质量差或发送太快
# 网卡队列统计
ethtool -S eth0 | grep -E "drop|error|miss"
# 调整网卡接收队列深度
ethtool -G eth0 rx 4096 tx 4096
# 查看网卡中断亲和性
cat /proc/interrupts | grep eth0
# 将不同队列的中断绑定到不同 CPU 核(RSS)
echo 1 > /proc/irq/<irq_num>/smp_affinity
1.10.5 火焰图 --- 直观定位热点
bash
# 使用 FlameGraph 生成 CPU 火焰图
git clone https://github.com/brendangregg/FlameGraph.git
# 采集数据(30 秒)
perf record -F 99 -g -p <pid> sleep 30
perf script > out.perf
# 生成火焰图
./FlameGraph/stackcollapse-perf.pl out.perf > out.folded
./FlameGraph/flamegraph.pl out.folded > flamegraph.svg
# 用浏览器打开 flamegraph.svg,宽度代表 CPU 时间占比
# 找到最宽的"平顶"函数即是热点
1.10.6 常见性能问题诊断清单
| 症状 | 诊断命令 | 可能原因 |
|---|---|---|
| 吞吐量低 | perf stat 看 IPC |
内存带宽瓶颈、cache miss |
| 延迟高 | ss -tin 看 RTT |
Nagle 算法、发送缓冲区满 |
| CPU 100% | perf top |
忙等轮询、锁竞争 |
| 连接被丢弃 | netstat -s |
accept 队列溢出 |
| fd 耗尽 | `lsof -p <pid> | wc -l` |
| 内存增长 | valgrind --leak-check=full |
内存泄漏 |
| 大量 TIME_WAIT | `ss -tan | grep TIME-WAIT` |
1.11 完整实战:实现一个高性能 Echo Server
综合运用本文所有技术,实现支持数万并发连接的 Echo Server。
c
// high_perf_echo.c
// 特性:epoll ET 模式 + 非阻塞 + SO_REUSEPORT + accept4
// 编译:gcc -O2 -o echo_server high_perf_echo.c -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#define PORT 8080
#define MAX_EVENTS 1024
#define BUFFER_SIZE 4096
#define WORKER_COUNT 4 // 与 CPU 核数匹配
// ── 连接上下文 ──────────────────────────────
typedef struct {
int fd;
char buf[BUFFER_SIZE];
size_t buf_len;
} Connection;
// ── Worker(每个拥有独立 epoll + 监听 socket)──
typedef struct {
int epfd;
int server_fd;
int worker_id;
} Worker;
// ── 创建非阻塞监听 socket ─────────────────────
static int create_server_socket(int port) {
int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (fd < 0) { perror("socket"); exit(1); }
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); // 多进程共享
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); // 禁用 Nagle
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = INADDR_ANY,
};
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind"); exit(1);
}
if (listen(fd, 65535) < 0) {
perror("listen"); exit(1);
}
return fd;
}
// ── 注册 fd 到 epoll ──────────────────────────
static void epoll_add(int epfd, int fd, uint32_t events, void *ptr) {
struct epoll_event ev;
ev.events = events;
ev.data.ptr = ptr;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) < 0) {
perror("epoll_ctl ADD");
}
}
static void epoll_del(int epfd, int fd) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
// ── 处理新连接 ─────────────────────────────────
static void handle_accept(Worker *w) {
while (1) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
// accept4 直接创建非阻塞 + CLOEXEC 的 fd(减少一次 fcntl)
int client_fd = accept4(w->server_fd,
(struct sockaddr *)&client_addr,
&len,
SOCK_NONBLOCK | SOCK_CLOEXEC);
if (client_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 没有新连接了
if (errno == EINTR) continue;
perror("accept4");
break;
}
// 创建连接上下文
Connection *conn = calloc(1, sizeof(Connection));
conn->fd = client_fd;
// 注册到 epoll(ET 模式)
epoll_add(w->epfd, client_fd,
EPOLLIN | EPOLLET | EPOLLRDHUP | EPOLLERR,
conn);
printf("[Worker %d] New connection fd=%d\n", w->worker_id, client_fd);
}
}
// ── 处理可读事件(ET 模式,必须读到 EAGAIN)──────
static void handle_read(Worker *w, Connection *conn) {
while (1) {
ssize_t n = recv(conn->fd,
conn->buf + conn->buf_len,
BUFFER_SIZE - conn->buf_len,
0);
if (n > 0) {
conn->buf_len += n;
// Echo:将收到的数据原样发回
ssize_t sent = 0;
while (sent < (ssize_t)conn->buf_len) {
ssize_t s = send(conn->fd,
conn->buf + sent,
conn->buf_len - sent,
MSG_NOSIGNAL);
if (s < 0) {
if (errno == EAGAIN) {
// 发送缓冲区满,注册 EPOLLOUT 等待可写
// 简化处理:此处直接丢弃(生产代码需写队列)
break;
}
goto close_conn;
}
sent += s;
}
conn->buf_len = 0;
} else if (n == 0) {
// 对端正常关闭
goto close_conn;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 读完了
if (errno == EINTR) continue;
goto close_conn;
}
}
return;
close_conn:
printf("[Worker %d] Close fd=%d\n", w->worker_id, conn->fd);
epoll_del(w->epfd, conn->fd);
close(conn->fd);
free(conn);
}
// ── Worker 主循环 ──────────────────────────────
static void *worker_main(void *arg) {
Worker *w = (Worker *)arg;
struct epoll_event events[MAX_EVENTS];
// 创建本 worker 的 epoll 实例
w->epfd = epoll_create1(EPOLL_CLOEXEC);
if (w->epfd < 0) { perror("epoll_create1"); pthread_exit(NULL); }
// 每个 worker 有自己的 server_fd(SO_REUSEPORT)
w->server_fd = create_server_socket(PORT);
// 将 server_fd 注册到 epoll(LT 模式监听新连接即可)
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.ptr = NULL; // NULL 表示这是 server_fd
epoll_ctl(w->epfd, EPOLL_CTL_ADD, w->server_fd, &ev);
printf("[Worker %d] Started, listening on :%d\n", w->worker_id, PORT);
while (1) {
int n = epoll_wait(w->epfd, events, MAX_EVENTS, -1);
if (n < 0) {
if (errno == EINTR) continue;
perror("epoll_wait");
break;
}
for (int i = 0; i < n; i++) {
Connection *conn = (Connection *)events[i].data.ptr;
if (conn == NULL) {
// server_fd 事件:新连接到来
handle_accept(w);
} else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
// 连接关闭或错误
epoll_del(w->epfd, conn->fd);
close(conn->fd);
free(conn);
} else if (events[i].events & EPOLLIN) {
// 可读事件
handle_read(w, conn);
}
}
}
close(w->server_fd);
close(w->epfd);
return NULL;
}
// ── 主函数 ────────────────────────────────────
int main(void) {
pthread_t threads[WORKER_COUNT];
Worker workers[WORKER_COUNT];
for (int i = 0; i < WORKER_COUNT; i++) {
workers[i].worker_id = i;
if (pthread_create(&threads[i], NULL, worker_main, &workers[i]) != 0) {
perror("pthread_create");
return 1;
}
}
printf("Echo server started with %d workers on port %d\n",
WORKER_COUNT, PORT);
for (int i = 0; i < WORKER_COUNT; i++) {
pthread_join(threads[i], NULL);
}
return 0;
}
1.11.1 编译与测试
bash
# 编译
gcc -O2 -Wall -o echo_server high_perf_echo.c -lpthread
# 运行(调大 fd 限制)
ulimit -n 100000
./echo_server
# 压测(使用 wrk 或 ab)
# 安装 wrk
apt install wrk
# 连接数压测(需要 HTTP 协议的场景用 wrk,TCP 层用 tcpkali)
# 安装 tcpkali
tcpkali --connections 10000 \
--duration 30s \
--message "ping\n" \
--rate 100 \
127.0.0.1:8080
# 查看连接数
ss -s | grep estab
1.11.2 架构特点总结
本实现采用的技术栈:
epoll ET 模式 → 事件通知效率最高
SO_REUSEPORT → 多线程各自 accept,消除惊群
accept4() → 减少一次 fcntl 系统调用
TCP_NODELAY → 禁用 Nagle,降低延迟
EPOLLRDHUP → 优雅检测对端关闭
连接上下文(ptr) → 直接携带指针,O(1) 找到连接对象
循环读到 EAGAIN → ET 模式必须,不丢数据
MSG_NOSIGNAL → 避免 SIGPIPE 导致进程退出
1.12 参考资料
- 《UNIX 网络编程》 --- W. Richard Stevens(基础必读)
- 《Linux 高性能服务器编程》 --- 游双(中文最佳实践)
- 《深入理解 Linux 网络》 --- 张彦飞(内核视角)
man 7 epoll/man 7 socket/man 2 sendfile- Linux kernel networking documentation
- io_uring 官方文档 --- Jens Axboe
- DPDK Getting Started Guide
- Brendan Gregg's Linux Performance