高性能网络

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

相关推荐
IMPYLH39 分钟前
Linux 系统备份与镜像
linux·服务器·网络
淘气包海鸟2 小时前
水声网络框架
网络·ns3·水
aaa最北边2 小时前
Linux 上的交叉编译
linux·运维·服务器
蒲公英内测分发2 小时前
智能门锁量产时,工厂测试 App 一直在更新,产线怎么避免装错 APK?
服务器·人工智能·github·智能硬件·智能手表
垆边人似月.2 小时前
Python中如何使用get函数实现频次查询
服务器·前端·python
小张成长计划..2 小时前
【Linux】27:日志
linux·运维·服务器
国科安芯3 小时前
星间激光通信终端CANFD控制网络的时延确定性与辐射可靠性
网络·嵌入式硬件·信息与通信·canfd·低轨卫星·商业航天·抗辐射
dlhto3 小时前
Windows 使用 Rufus 制作 AlmaLinux 10.2 启动 U 盘:从 ISO 下载到服务器安装
运维·服务器·windows
中屹指纹浏览器svip3 小时前
网络抖动会标记账号异常?分清两种波动
网络
承渊政道3 小时前
Linux网络学习【传输层协议UDP】
linux·网络·网络协议·学习·ubuntu·udp