场景设定
- 客户端发送:
GET /index.html HTTP/1.1
- nginx监听80端口
- nginx使用epoll处理连接
完整流程
阶段0:nginx启动初始化
1. nginx master进程启动
2. 创建监听socket: listen_fd = socket(AF_INET, SOCK_STREAM, 0)
3. 绑定80端口: bind(listen_fd, server_addr, sizeof(server_addr))
4. 开始监听: listen(listen_fd, backlog)
5. 创建epoll实例: epfd = epoll_create1(0)
6. 将监听socket加入epoll: epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev)
7. fork出worker进程
阶段1:nginx进程等待连接
nginx worker进程状态: RUNNING
├── 执行事件循环
├── 调用 epoll_wait(epfd, events, MAX_EVENTS, -1)
└── 进程状态变为: SLEEPING (阻塞等待)
CPU状态: 调度其他进程运行
内核状态: nginx进程在epoll的等待队列中
阶段2:客户端建立连接
客户端动作:
├── socket() 创建客户端socket
├── connect() 连接nginx的80端口
└── 发送SYN包
网络层面:
├── 三次握手完成
├── 内核为新连接创建socket (client_fd)
└── listen_fd变为可读状态 (有新连接待accept)
内核动作:
├── 检查epoll监听列表: "listen_fd有epoll在监听"
├── 将事件加入epoll就绪队列
├── 唤醒nginx进程: wake_up_process(nginx_worker)
└── nginx进程状态: SLEEPING → READY
阶段3:nginx处理新连接
nginx进程状态: READY → RUNNING (被CPU调度执行)
nginx执行流程:
├── epoll_wait() 返回: nfds = 1, events[0].fd = listen_fd
├── 检查事件类型: events[0].events & EPOLLIN (可读)
├── 识别为监听socket: if (fd == listen_fd)
├── 接受新连接: client_fd = accept(listen_fd, &client_addr, &len)
├── 设置非阻塞: fcntl(client_fd, F_SETFL, O_NONBLOCK)
├── 创建连接对象: connection_t *c = create_connection(client_fd)
├── 加入epoll监听: epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &client_ev)
└── 继续事件循环: 回到epoll_wait()
nginx进程状态: RUNNING → SLEEPING (再次等待事件)
阶段4:客户端发送HTTP请求
客户端动作:
├── send() 发送HTTP请求
└── "GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n"
网络传输:
├── TCP数据包传输
├── 服务器网卡接收数据
├── 硬件中断: IRQ触发
└── 网卡DMA传输数据到内存
内核中断处理:
├── 网卡中断处理程序执行
├── 更新client_fd的socket接收缓冲区
├── 缓冲区内容: "GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n"
├── 标记client_fd为可读状态
├── 查找监听client_fd的epoll实例
├── 将读事件加入epoll就绪队列
├── 唤醒nginx进程: wake_up_process(nginx_worker)
└── nginx进程状态: SLEEPING → READY
阶段5:nginx读取和解析请求
nginx进程状态: READY → RUNNING
nginx执行流程:
├── epoll_wait() 返回: nfds = 1, events[0].fd = client_fd
├── 检查事件类型: events[0].events & EPOLLIN
├── 识别为客户端socket: if (fd != listen_fd)
├── 获取连接对象: connection_t *c = get_connection(client_fd)
├── 读取数据:
│ ├── bytes = read(client_fd, buffer, sizeof(buffer))
│ ├── 系统调用陷入内核
│ ├── 内核从socket缓冲区复制数据到nginx内存
│ └── 返回用户空间,buffer现在包含HTTP请求
├── 解析HTTP请求:
│ ├── 解析请求行: "GET /index.html HTTP/1.1"
│ ├── 解析头部: "Host: localhost"
│ └── 构建request对象
├── 处理请求:
│ ├── 查找文件: /var/www/html/index.html
│ ├── 检查权限和存在性
│ └── 准备响应数据
└── 生成HTTP响应:
├── "HTTP/1.1 200 OK\r\n"
├── "Content-Type: text/html\r\n"
├── "Content-Length: 1024\r\n\r\n"
└── "<html>...</html>"
阶段6:nginx发送响应
nginx发送流程:
├── write(client_fd, response_buffer, response_length)
├── 系统调用陷入内核
├── 内核将数据复制到client_fd的发送缓冲区
├── TCP层处理数据发送
├── 网卡发送数据包给客户端
└── 如果发送缓冲区满,则:
├── write()返回EAGAIN
├── 将client_fd注册EPOLLOUT事件
└── 等待下次可写事件继续发送
连接处理:
├── 检查Connection头: "keep-alive" or "close"
├── 如果keep-alive:
│ ├── 重置连接状态
│ ├── 继续监听client_fd的读事件
│ └── 准备处理下一个请求
└── 如果close:
├── epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL)
├── close(client_fd)
└── 释放连接对象
nginx状态: 继续事件循环 → SLEEPING
状态转换总结
时间轴 nginx状态 CPU在做什么 内核socket状态
───── ───────── ────────────── ─────────────
T0 RUNNING nginx初始化 监听socket创建
T1 SLEEPING 执行其他进程 等待连接
T2 READY 准备调度nginx 新连接到达
T3 RUNNING 处理accept() 连接建立
T4 SLEEPING 执行其他进程 等待数据
T5 READY 准备调度nginx HTTP数据到达
T6 RUNNING 读取+解析+响应 处理请求
T7 SLEEPING 执行其他进程 等待下一个请求
关键性能要点
1. 异步非阻塞
- nginx永远不会因为I/O而阻塞
- 一个worker进程可以处理数万个连接
2. 零拷贝优化
- sendfile(): 文件直接从磁盘到网卡,不经过用户空间
- 减少内存拷贝次数
3. 事件批处理
- epoll_wait一次可能返回多个事件
- nginx批量处理,减少系统调用开销
4. 连接复用
- HTTP Keep-Alive: 一个连接处理多个请求
- 减少连接建立/销毁开销
这就是nginx能够以极低的内存占用处理大量并发连接的核心原理!
评论区