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 位。
证明步骤
-
对齐 ⇒ 低位为 0
如果
A = m * 2^k,则A % 2^k = 0,也就是说
A的二进制最低 k 位全为 0。 -
写入标志
通过按位或
P = A | f,把标志写入低 k 位,因为 A 的低 k 位是 0,所以不会覆盖高位。 -
恢复
- 提取标志:
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