高性能网络

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 参考资料

相关推荐
认真的薛薛1 小时前
阿里云: A记录 & CNAME
服务器·前端·阿里云
步十人1 小时前
【FastAPI】ORM-02.使用 ORM 高效处理数据库逻辑
服务器·数据库·fastapi
坚持就完事了1 小时前
Linux的ln命令
linux·运维·服务器
绿豆人1 小时前
操作系统上电后流程
linux·服务器
Tingjct2 小时前
Linux开发工具
linux·运维·服务器
Shingmc32 小时前
【Linux】应用层协议HTTP
网络·网络协议·http
cui_ruicheng2 小时前
Linux线程(三):线程同步、互斥与生产者消费者模型
linux·服务器·开发语言
Harvy_没救了2 小时前
【网络运维】从开发到上线全流程简化方案
运维·网络
苍煜2 小时前
K8s 网络与存储(容器网络互通与数据持久化)
网络·容器·kubernetes