nginx epoll 里黑科技位运算+指针复用

nginx epoll 里黑科技位运算+指针复用

从nginx 源码里面的疑惑到深入探索

下面的代码是从 ngx_epoll_process_events 这个函数提取出来的,该函数主要就是对epoll事件的处理,是worker进程主要干的事情,当看到下面代码时我比较疑惑,为啥是下面的写法呢?不要着急,咱们慢慢探索。

c 复制代码
events = epoll_wait(ep, event_list, nevents, timer);
for (i = 0; i < events; i++) {
        c = event_list[i].data.ptr;

        instance = (uintptr_t) c & 1;
        c = (ngx_connection_t*) ((uintptr_t) c & (uintptr_t) ~1);

        rev = c->read;
}

背景:epoll_event的使用

epoll_event 结构体的 data 字段是一个 union,常见成员有 void *ptr、int fd、uint64_t u64等

c 复制代码
typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

struct epoll_event
{
  uint32_t events;	/* Epoll events */
  epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;

Nginx 在注册事件时,通常把对应的 ngx_connection_t *(连接结构体指针)放到 data.ptr 中,这样 epoll_wait() 返回事件时能快速定位到对应的连接对象。

问题:为啥么要存储额外的信息呢

以下是我的推断: 在高并发服务器中,epoll_wait() 可能返回大量事件。除了指针,Nginx 希望为每个事件额外携带一个1 位标志(称为 instance)来:区分"陈旧事件"与"当前事件",防止使用已关闭或已重用的连接结构;避免为每个连接额外分配内存或扩展 epoll 数据结构;在不增加内存与 syscalls 的前提下提高健壮性。为了做到"零开销"地携带这个标志,Nginx 利用了指针对齐的特性,把标志存进指针的最低位。

关键思路:利用指针对齐"偷位"存标志

现代 CPU 与编译器会对多数数据结构强制对齐(alignment)。例如:

  • short:通常 2 字节对齐(地址最低 bit0 = 0)

  • int:通常 4 字节对齐(地址最低 bit0、bit1 = 0)

  • pointer(64 位系统):通常 8 字节对齐(最低 3位 = 0)

也就是说,指针的最低若干位通常为 0,是"闲置"的。Nginx 利用这一点把一个布尔标志放到 ptr 的最低位:

  • 设置时:data.ptr = (void *)((uintptr_t)c | instance);
  • 取出时:先用 & 1 取标志,再用 & ~1 清标志得到真实指针

为什么这在大多数平台是安全的(对齐保证)

安全性的根基在于对齐(alignment)保证:

  • C 语言与编译器会确保较大类型(如结构体、指针)在内存中的地址满足对齐约束;

  • 例如在 64 位系统上,malloc() 返回的指针通常至少是 8 字节对齐的,因此最低 3 位为 0;

  • 因此在这些系统上把最低 1(或更多)位拿来存 flags 是可行的。

在 Nginx 的目标平台(Linux x86 / x86_64、常见 Unix)上,这个假设成立,所以程序员可以"偷用"低位。

数学证明

前提

  • 地址 A 对齐到 (2^k),即 A 是 (2^k) 的倍数。
  • 标志 f 占用低于或等于 k 位。

证明步骤

  1. 对齐 ⇒ 低位为 0

    如果 A = m * 2^k,则 A % 2^k = 0

    也就是说 A 的二进制最低 k 位全为 0。

  2. 写入标志

    通过按位或 P = A | f,把标志写入低 k 位,因为 A 的低 k 位是 0,所以不会覆盖高位。

  3. 恢复

    • 提取标志:f = P & (2^k - 1)
    • 恢复地址:A = P & ~(2^k - 1)

简言之:低 k 位全 0 可以安全存放 k 位标志,位操作即可恢复原指针。

示例

C 复制代码
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

#define MAX_EVENTS 10
#define PORT 9000

/* 每个连接的自定义结构体 */
typedef struct {
    int fd;                   // 文件描述符
    char name[32];            // 自定义名字
    struct sockaddr_in addr;  // 对端地址
    int instance;             // 当前连接的标志位(0/1)
} conn_t;

/* 注册 epoll 事件,支持标志位复用 */
void add_epoll(int epfd, int fd, uint32_t events, conn_t* c) {
    struct epoll_event ev;
    ev.events = events;

    /* 黑科技:指针最低位写入 instance 标志 */
    uintptr_t ptr_with_flag = ((uintptr_t) c) | (c->instance & 1);
    ev.data.ptr = (void*) ptr_with_flag;

    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
        perror("epoll_ctl ADD");
        exit(1);
    }
}

/* 删除事件 */
void del_epoll(int epfd, int fd) {
    if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1) {
        perror("epoll_ctl DEL");
    }
    close(fd);
}

/* 创建监听 socket */
int create_server() {
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        perror("socket");
        exit(1);
    }

    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(listenfd, (struct sockaddr*) &addr, sizeof(addr)) < 0) {
        perror("bind");
        exit(1);
    }

    if (listen(listenfd, 5) < 0) {
        perror("listen");
        exit(1);
    }

    printf("Server listening on port %d...\n", PORT);
    return listenfd;
}

int main() {
    int epfd = epoll_create1(0);
    if (epfd < 0) {
        perror("epoll_create1");
        return 1;
    }

    int listenfd = create_server();

    conn_t* listen_conn = malloc(sizeof(conn_t));
    listen_conn->fd = listenfd;
    strcpy(listen_conn->name, "listener");
    listen_conn->instance = 0;

    add_epoll(epfd, listenfd, EPOLLIN, listen_conn);

    struct epoll_event events[MAX_EVENTS];

    while (1) {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (n < 0) {
            if (errno == EINTR) continue;
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < n; i++) {
            /* 从 epoll 取出指针 */
            uintptr_t raw_ptr = (uintptr_t) events[i].data.ptr;
            int instance = raw_ptr & 1;                        // 取出最低位标志
            conn_t* c = (conn_t*) (raw_ptr & ~(uintptr_t) 1);  // 去掉最低位,恢复真实指针

            printf("[debug] epoll返回: ptr=%p, instance=%d, real_ptr=%p\n", (void*) raw_ptr,
                   instance, (void*) c);

            if (c->fd == listenfd) {
                /* 新客户端连接 */
                struct sockaddr_in cliaddr;
                socklen_t len = sizeof(cliaddr);
                int connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &len);
                if (connfd < 0) {
                    perror("accept");
                    continue;
                }

                conn_t* client = malloc(sizeof(conn_t));
                client->fd = connfd;
                client->addr = cliaddr;
                client->instance = instance ^ 1;  // 轮流切换 0/1
                snprintf(client->name, sizeof(client->name), "client-%d", connfd);

                printf("[+] New connection %s (%s:%d), instance=%d\n", client->name,
                       inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), client->instance);

                add_epoll(epfd, connfd, EPOLLIN, client);
            } else {
                /* 客户端数据到来 */
                char buf[1024];
                ssize_t nread = read(c->fd, buf, sizeof(buf) - 1);
                if (nread <= 0) {
                    printf("[-] %s disconnected\n", c->name);
                    del_epoll(epfd, c->fd);
                    free(c);
                    continue;
                }

                buf[nread] = '\0';
                printf("[*] Received from %s (instance=%d): %s\n", c->name, instance, buf);

                /* 回显给客户端 */
                write(c->fd, buf, strlen(buf));
            }
        }
    }

    close(epfd);
    return 0;
}

程序说明

  • 我们定义了结构体 conn_t,其中有一个字段 instance(0 或 1)。

  • 当注册 epoll 时,用 (uintptr_t)c | (c->instance & 1) "偷位"。

  • 当 epoll 返回时,再用:

c 复制代码
instance = raw_ptr & 1;
c = (conn_t*)(raw_ptr & ~((uintptr_t)1));

恢复真实指针。

测试程序,你可观察到下面的现象:

  • 指针 ptr 与 real_ptr 只差最低 1 bit。

  • instance 能正确取出(0 或 1)。

  • 程序运行稳定,说明指针对齐确实保证了最低位为 0,可安全偷用。

shell 复制代码
[epoll_test]$ ./a.out 

Server listening on port 9000...
[debug] epoll返回: ptr=0x3436e6b0, instance=0, real_ptr=0x3436e6b0
[+] New connection client-5 (127.0.0.1:49124), instance=1
[debug] epoll返回: ptr=0x3436e6f1, instance=1, real_ptr=0x3436e6f0
[*] Received from client-5 (instance=1): 1

[debug] epoll返回: ptr=0x3436e6f1, instance=1, real_ptr=0x3436e6f0
[*] Received from client-5 (instance=1): 2

[debug] epoll返回: ptr=0x3436e6f1, instance=1, real_ptr=0x3436e6f0
[*] Received from client-5 (instance=1): 3

[debug] epoll返回: ptr=0x3436e6f1, instance=1, real_ptr=0x3436e6f0
[*] Received from client-5 (instance=1): 4

[debug] epoll返回: ptr=0x3436e6f1, instance=1, real_ptr=0x3436e6f0
[*] Received from client-5 (instance=1): 5
相关推荐
Ashlee_code2 小时前
经纪柜台系统解析:从今日国际金融动荡看证券交易核心引擎的变革
python·架构·系统架构·区块链·vim·柜台·香港券商
NON-JUDGMENTAL2 小时前
在 Ubuntu 上安装 Ollama 并通过 Open WebUI 运行本地大语言模型
linux·ubuntu·语言模型
ZzzZZzzzZZZzzzz…2 小时前
RHCSA---权限管理
linux·运维·权限管理·特殊权限·rhcsa·acl权限·权限掩码原理
TinyPiXOS开发者联盟2 小时前
轻量级嵌入式系统的 Lottie 动画实现
linux·c++·动画·嵌入式开发·lottie·tinypixos·tpgui
海蓝可知天湛3 小时前
Ubuntu24.10禁用该源...+vmware无法复制黏贴“天坑闭环”——从 DNS 诡异解析到 Ubuntu EOL 引发的 apt 404排除折
linux·ubuntu
快手技术3 小时前
从“拦路虎”到“修路工”:基于AhaEdit的广告素材修复
前端·算法·架构
SoulKuyan3 小时前
android su执行命令
linux·运维·服务器
Zhao_yani3 小时前
Centos 7安装Apache Drill
linux·centos·drill
一叶飘零_sweeeet4 小时前
Linux 安装 Elasticsearch:避坑指南 + 性能调优实战
linux·运维·elasticsearch