浅析Nginx中水平触发和边缘触发模式
边缘触发(Edge Triggered, ET)和水平触发(Level Triggered, LT)的区别
基本概念:
- 水平触发(LT):
// 水平触发示例
while (1) {
// 只要缓冲区有数据,就会一直触发
n = read(fd, buffer, sizeof(buffer));
if (n <= 0) {
if (errno == EAGAIN) {
// 数据读完了,等待下次通知
break;
}
// 处理错误
break;
}
// 处理数据
process_data(buffer, n);
}
- 边缘触发(ET):
// 边缘触发示例
while (1) {
// 必须循环读取直到EAGAIN,因为不会再次触发
n = read(fd, buffer, sizeof(buffer));
if (n < 0) {
if (errno == EAGAIN) {
// 数据全部读完
break;
}
// 处理错误
break;
} else if (n == 0) {
// 连接关闭
break;
}
// 处理数据
process_data(buffer, n);
}
主要区别
- 触发时机
// 水平触发
if (ngx_event_flags & NGX_USE_LEVEL_EVENT) {
// 只要缓冲区有数据就触发
if (!rev->active && !rev->ready) {
if (ngx_add_event(rev, NGX_READ_EVENT, NGX_LEVEL_EVENT) != NGX_OK) {
return NGX_ERROR;
}
}
}
// 边缘触发
if (ngx_event_flags & NGX_USE_CLEAR_EVENT) {
// 仅在有新数据到达时触发
if (!rev->active && !rev->ready) {
if (ngx_add_event(rev, NGX_READ_EVENT, NGX_CLEAR_EVENT) != NGX_OK) {
return NGX_ERROR;
}
}
}
- 处理要求
// 水平触发可以分多次读取
static void lt_read_handler(ngx_event_t *rev) {
ssize_t n = read(fd, buffer, SOME_SIZE);
if (n < 0) {
if (errno == EAGAIN) {
// 下次继续读取
return;
}
}
process_data(buffer, n);
}
// 边缘触发必须一次性读完
static void et_read_handler(ngx_event_t *rev) {
for (;;) {
ssize_t n = read(fd, buffer, SOME_SIZE);
if (n < 0) {
if (errno == EAGAIN) {
break;
}
// 错误处理
return;
}
process_data(buffer, n);
}
}
优缺点对比
- 水平触发:
- 优点:
// 1. 编程更简单,不容易出错
void lt_handler(ngx_event_t *rev) {
// 可以每次只读取部分数据
n = read(fd, buffer, BUFFER_SIZE);
if (n < 0 && errno == EAGAIN) {
// 剩余数据下次再读
return;
}
}
// 2. 不会丢失数据
if (busy) {
// 数据还在缓冲区,下次会再次触发
return;
}
- 缺点:
// 1. 可能会频繁触发
void lt_event_loop() {
while (1) {
// 如果数据没读完,会重复触发
events = epoll_wait(epfd, events, MAX_EVENTS, -1);
// 处理事件
}
}
- 边缘触发:
- 优点:
// 1. 减少系统调用,性能更好
void et_event_loop() {
while (1) {
// 只在新数据到达时触发一次
events = epoll_wait(epfd, events, MAX_EVENTS, -1);
// 处理事件
}
}
// 2. 更好的多路复用性能
if (ngx_event_flags & NGX_USE_CLEAR_EVENT) {
// 减少重复通知
events_processed++;
}
- 缺点:
// 1. 必须一次性读完所有数据,否则会丢失
void et_handler(ngx_event_t rev) {
for (;;) {
n = read(fd, buffer, BUFFER_SIZE);
if (n < 0 && errno == EAGAIN) {
break; // 必须读到EAGAIN
}
process_data(buffer, n);
}
}
// 2. 编程复杂度更高
if (rev->available) { // 需要额外的标记
// 确保数据被完全处理
handle_remaining_data();
}
Nginx 默认使用边缘触发模式(在epoll下),因为:
- 性能更好
- 减少系统调用
- 适合 Nginx 的事件驱动模型