Socket 的多路复用
多路复用(Multiplexing)是一种允许单个线程同时监控多个文件描述符(如 Socket)的技术。通过多路复用,程序可以在一个线程中高效地处理多个 I/O 操作,而不需要为每个连接创建一个单独的线程。
常见的多路复用技术包括:
select
:- 最早的 I/O 多路复用技术。
- 通过轮询检查文件描述符的状态。
- 缺点:
- 文件描述符数量有限(通常 1024)。
- 每次调用都需要将文件描述符集合从用户态拷贝到内核态。
- 时间复杂度为 O(n),效率较低。
poll
:- 改进版的
select
,支持更多的文件描述符。 - 使用链表存储文件描述符,没有数量限制。
- 缺点:
- 仍然需要轮询检查文件描述符的状态。
- 每次调用需要将文件描述符集合从用户态拷贝到内核态。
- 时间复杂度为 O(n),效率较低。
- 改进版的
epoll
:- Linux 特有的高效多路复用技术。
- 使用事件驱动机制,避免了轮询。
- 优点:
- 支持更多的文件描述符。
- 时间复杂度为 O(1),效率高。
- 不需要每次调用都拷贝文件描述符集合。
epoll
的优点
epoll
是 Linux 下高性能的多路复用机制,相比于 select
和 poll
,它具有以下优点:
- 高效的事件驱动机制:
epoll
使用事件驱动模型,只有当文件描述符状态发生变化时才会通知程序,避免了轮询的开销。- 时间复杂度为 O(1),而
select
和poll
的时间复杂度为 O(n)。
- 支持更多的文件描述符:
epoll
没有文件描述符数量的限制(仅受系统资源限制),而select
通常限制为 1024。
- 减少数据拷贝:
epoll
使用内核事件表来管理文件描述符,只有在初始化时需要将文件描述符集合从用户态拷贝到内核态。- 后续的事件通知不需要拷贝文件描述符集合,减少了系统调用和数据拷贝的开销。
- 边缘触发(ET)和水平触发(LT)模式:
- 水平触发(LT):只要文件描述符处于就绪状态,
epoll
就会持续通知程序。 - 边缘触发(ET):只有当文件描述符状态发生变化时,
epoll
才会通知程序。 - ET 模式可以减少事件通知的次数,提高效率。
- 水平触发(LT):只要文件描述符处于就绪状态,
- 动态管理文件描述符:
epoll
支持动态添加、修改和删除文件描述符,而select
和poll
每次调用都需要重新设置文件描述符集合。
epoll
的核心 API
epoll_create
:- 创建一个
epoll
实例,返回一个文件描述符。 - 示例:
int epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); }
- 创建一个
epoll_ctl
:- 向
epoll
实例中添加、修改或删除文件描述符。 - 示例:
struct epoll_event event; event.events = EPOLLIN; // 监听读事件 event.data.fd = sock_fd; // 监听的 Socket 文件描述符 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event) == -1) { perror("epoll_ctl"); exit(EXIT_FAILURE); }
- 向
epoll_wait
:- 等待文件描述符上的事件。
- 示例:
struct epoll_event events[MAX_EVENTS]; int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 阻塞等待 for (int i = 0; i < n; ++i) { if (events[i].events & EPOLLIN) { // 处理读事件 } }
epoll
的工作模式
- 水平触发(LT):
- 默认模式。
- 只要文件描述符处于就绪状态,
epoll_wait
就会持续通知程序。
- 边缘触发(ET):
- 需要显式设置
EPOLLET
标志。 - 只有当文件描述符状态发生变化时,
epoll_wait
才会通知程序。
- 需要显式设置
示例代码
以下是一个简单的 epoll
服务器示例:
#include <iostream>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
#define MAX_EVENTS 10
int main() {
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(8080);
if (bind(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
close(sock_fd);
exit(EXIT_FAILURE);
}
if (listen(sock_fd, SOMAXCONN) == -1) {
perror("listen");
close(sock_fd);
exit(EXIT_FAILURE);
}
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(sock_fd);
exit(EXIT_FAILURE);
}
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = sock_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event) == -1) {
perror("epoll_ctl");
close(sock_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}
struct epoll_event events[MAX_EVENTS];
while (true) {
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; ++i) {
if (events[i].data.fd == sock_fd) {
// 处理新连接
int client_fd = accept(sock_fd, nullptr, nullptr);
if (client_fd == -1) {
perror("accept");
continue;
}
event.events = EPOLLIN | EPOLLET; // 设置为 ET 模式
event.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
perror("epoll_ctl");
close(client_fd);
}
} else {
// 处理客户端数据
char buffer[1024];
ssize_t len = read(events[i].data.fd, buffer, sizeof(buffer));
if (len <= 0) {
close(events[i].data.fd);
} else {
write(events[i].data.fd, buffer, len);
}
}
}
}
close(sock_fd);
close(epoll_fd);
return 0;
}
总结
- 多路复用:允许单个线程同时监控多个文件描述符。
epoll
的优点:- 事件驱动,效率高。
- 支持更多的文件描述符。
- 减少数据拷贝。
- 支持边缘触发和水平触发模式。
- 适用场景:高并发、高性能的网络服务器。
THE END
暂无评论内容