1.实现并发:
多线程、多进程、IO多路复用
IO多路复用(I/O Multiplexing)是一种能够让单个进程或线程同时处理多个I/O操作的技术,常用于网络编程中,尤其是在需要处理大量并发连接时。它的核心思想是通过非阻塞的方式,让程序能够在单一的线程或进程中同时监听多个I/O操作,而不需要为每一个连接创建一个独立的线程或进程。
2.常见技术
委托内核检测文件描述符(监听:1个,通信:n个)状态
1. select
select
是最早的一种IO多路复用方式,可以同时监听多个文件描述符的可读、可写和异常事件。它的优点是跨平台兼容性好,但存在以下缺点:
-
文件描述符数量有限制(通常是1024)。
-
性能在文件描述符数量较多时会降低,因为需要线性扫描所有文件描述符。
2. poll
poll
是select
的改进版本,能够同时监听多个文件描述符的可读、可写和异常事件。与select
相比,poll
没有文件描述符数量的限制,但其性能问题依然存在,因为poll
也需要线性扫描文件描述符数组。
3. epoll
epoll
是Linux特有的IO多路复用机制,性能优于select
和poll
,特别适用于高并发环境。epoll
支持两种工作模式:(红黑树查询)
-
水平触发(LT,Level Triggered):只要有数据可读或可写,就会通知应用程序。
-
边缘触发(ET,Edge Triggered):只有当文件描述符的状态发生变化时才会通知应用程序。
epoll
通过回调机制通知应用程序事件的发生,避免了线性扫描的开销,因此在处理大量并发连接时效率更高。
3.函数详解
1. select
select
函数的原型如下:
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
-
nfds
:指定要监视的文件描述符的最大值加1(即max(fd) + 1
)。select
会检查从0
到nfds-1
的所有文件描述符。 -
readfds
:指向fd_set
类型的指针,表示需要检查可读性的文件描述符集合。如果为nullptr
,表示不检查可读性。 -
writefds
:指向fd_set
类型的指针,表示需要检查可写性的文件描述符集合。如果为nullptr
,表示不检查可写性。 -
exceptfds
:指向fd_set
类型的指针,表示需要检查异常条件的文件描述符集合。如果为nullptr
,表示不检查异常条件。 -
timeout
:指向timeval
结构的指针,表示select
调用的超时时间。如果为nullptr
,select
将阻塞直到有事件发生;如果timeout
指向的timeval
结构的值为{0, 0}
,select
将立即返回。
fd_set
是一个位集合,用于存储文件描述符。可以通过FD_ZERO
、FD_SET
、FD_CLR
和FD_ISSET
宏来操作fd_set
:
-
FD_ZERO(fd_set* set)
:清空fd_set
集合。 -
FD_SET(int fd, fd_set* set)
:将文件描述符fd
添加到fd_set
集合中。 -
FD_CLR(int fd, fd_set* set)
:从fd_set
集合中移除文件描述符fd
。 -
FD_ISSET(int fd, fd_set* set)
:检查文件描述符fd
是否在fd_set
集合中。
timeval
结构定义如下:
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#define MAX_CLIENTS 10
#define PORT 8080
#define BUFFER_SIZE 1024
// 设置服务器套接字
int setup_server_socket() {
int server_fd;
struct sockaddr_in server_addr;
// 创建 TCP 套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听套接字
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
return server_fd;
}
// 处理客户端消息
void handle_client_message(int client_fd) {
char buffer[BUFFER_SIZE];
int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
if (bytes_read <= 0) {
if (bytes_read == 0) {
printf("Client disconnected\n");
} else {
perror("read failed");
}
close(client_fd);
} else {
buffer[bytes_read] = '\0'; // 确保缓冲区以 '\0' 结尾
printf("Received from client: %s\n", buffer);
// 回传收到的消息
write(client_fd, buffer, bytes_read);
}
}
int main() {
int server_fd, new_client_fd, max_fd;
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
fd_set read_fds, active_fds;
server_fd = setup_server_socket();
// 初始化文件描述符集合
FD_ZERO(&active_fds);
FD_SET(server_fd, &active_fds);
max_fd = server_fd;
while (1) {
read_fds = active_fds; // 每次调用 select 都需要重新设置
// 使用 select 监控多个文件描述符
int activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
if (activity < 0) {
perror("select error");
break;
}
// 检查是否有新的客户端连接
if (FD_ISSET(server_fd, &read_fds)) {
new_client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (new_client_fd < 0) {
perror("accept failed");
continue;
}
printf("New client connected\n");
FD_SET(new_client_fd, &active_fds); // 将新连接的客户端添加到活动集合
if (new_client_fd > max_fd) {
max_fd = new_client_fd; // 更新最大文件描述符
}
}
// 处理所有已经连接的客户端
for (int i = 0; i <= max_fd; i++) {
if (FD_ISSET(i, &read_fds) && i != server_fd) {
handle_client_message(i); // 处理客户端的消息
}
}
}
close(server_fd);
return 0;
}
2. poll
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
-
fds
:指向pollfd
结构数组的指针,每个pollfd
结构表示一个文件描述符及其感兴趣的事件。 -
nfds
:fds
数组中元素的数量。 -
timeout
:指定poll
调用的超时时间,单位为毫秒。如果为-1
,poll
将阻塞直到有事件发生;如果为0
,poll
将立即返回。
pollfd
结构定义如下:
struct pollfd {
int fd; // 文件描述符
short events; // 感兴趣的事件
short revents;// 实际发生的事件
};
-
fd
:需要监视的文件描述符。 -
events
:指定对文件描述符感兴趣的事件,可以是以下值的组合:-
POLLIN
:数据可读。 -
POLLOUT
:数据可写。 -
POLLERR
:发生错误。 -
POLLHUP
:挂起。 -
POLLNVAL
:无效的文件描述符。
-
-
revents
:poll
返回时,revents
字段会包含实际发生的事件,可以是events
中指定的事件的组合。
3. epoll
epoll
是一个基于事件的IO多路复用机制,主要由以下三个函数组成:
int epoll_create(int size);
-
size
:指定epoll
实例的大小(在Linux 2.6.8及更高版本中,这个参数已被忽略)。 -
返回值:成功时返回一个非负的文件描述符,表示创建的
epoll
实例;失败时返回-1
。
epoll_ctl
用于向epoll
实例中添加、修改或删除文件描述符:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
-
epfd
:由epoll_create
创建的epoll
实例的文件描述符。 -
op
:指定操作类型:-
EPOLL_CTL_ADD
:向epoll
实例中添加文件描述符。 -
EPOLL_CTL_MOD
:修改已注册的文件描述符的事件。 -
EPOLL_CTL_DEL
:从epoll
实例中删除文件描述符。
-
-
fd
:需要操作的文件描述符。 -
event
:指向epoll_event
结构的指针,用于指定感兴趣的事件。对于EPOLL_CTL_DEL
操作,event
可以为nullptr
。
epoll_event
结构定义如下:
struct epoll_event {
uint32_t events; // 感兴趣的事件
epoll_data_t data; // 用户数据
};
-
events
:指定感兴趣的事件,可以是以下值的组合:-
EPOLLIN
:数据可读。 -
EPOLLOUT
:数据可写。 -
EPOLLRDHUP
:对端关闭连接。 -
EPOLLERR
:发生错误。 -
EPOLLHUP
:挂起。
-
-
data
:用户数据,可以是文件描述符、指针或其他数据类型。
epoll_wait
等待epoll
实例中的事件发生
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
-
epfd
:由epoll_create
创建的epoll
实例的文件描述符。 -
events
:指向epoll_event
结构数组的指针,用于存储发生的事件。 -
maxevents
:events
数组的最大容量。 -
timeout
:指定epoll_wait
调用的超时时间,单位为毫秒。如果为-1
,epoll_wait
将阻塞直到有事件发生;如果为0
,epoll_wait
将立即返回。
4.epoll
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define MAX_EVENTS 10
void handle_new_connection(int server_fd, int epoll_fd);
void handle_client_request(int client_fd);
int main() {
int server_fd, epoll_fd, conn_fd;
struct sockaddr_in server_addr;
struct epoll_event event, events[MAX_EVENTS];
int nfds, port = 8080;
// 创建套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 初始化地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听
if (listen(server_fd, 5) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server is listening on port %d\n", port);
// 创建 epoll 实例
epoll_fd = epoll_create1(0);
if (epoll_fd < 0) {
perror("epoll_create1 failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 将服务器套接字添加到 epoll 实例
event.data.fd = server_fd;
event.events = EPOLLIN | EPOLLET; // 边缘触发
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) {
perror("epoll_ctl: server_fd");
close(server_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}
// 等待事件
while (1) {
nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait failed");
break;
}
for (int n = 0; n < nfds; ++n) {
if (events[n].data.fd == server_fd) {
// 处理新连接
handle_new_connection(server_fd, epoll_fd);
} else if (events[n].events & EPOLLIN) {
// 处理客户端请求
handle_client_request(events[n].data.fd);
}
}
}
// 清理
close(server_fd);
close(epoll_fd);
return 0;
}
void handle_new_connection(int server_fd, int epoll_fd) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd;
while ((client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len)) > 0) {
printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 将客户端套接字设置为非阻塞模式
int flags = fcntl(client_fd, F_GETFL, 0);
fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
// 将客户端套接字添加到 epoll 实例
struct epoll_event event;
event.data.fd = client_fd;
event.events = EPOLLIN | EPOLLET; // 边缘触发
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) < 0) {
perror("epoll_ctl: client_fd");
close(client_fd);
}
}
if (client_fd == -1) {
if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) {
perror("accept failed");
}
}
}
void handle_client_request(int client_fd) {
char buffer[1024];
ssize_t bytes_read;
while ((bytes_read = read(client_fd, buffer, sizeof(buffer) - 1)) > 0) {
buffer[bytes_read] = '\0';
printf("Received from client: %s\n", buffer);
// 回复客户端
write(client_fd, buffer, bytes_read);
}
if (bytes_read == 0) {
printf("Client disconnected\n");
close(client_fd);
} else if (bytes_read == -1) {
if (errno != EAGAIN && errno != EINTR) {
perror("read failed");
close(client_fd);
}
}
}
1.epoll 的三大核心优势
1. O(1) 时间复杂度的事件检测
-
传统 select/poll 缺陷:
-
每次调用需要遍历所有监控的文件描述符(FD)
-
时间复杂度 O(n),万级连接时性能急剧下降
-
-
epoll 优化:
-
通过红黑树(
epoll_ctl
)和就绪链表(epoll_wait
)分离监控与事件获取 -
仅返回就绪的 FD,与总连接数无关
-
注册用红黑树是
O(logN)
,触发是就绪链表O(1)
,整体远快于poll/select
的O(N)
-
2. 无重复内存拷贝
-
select/poll:
-
每次调用需要将 FD 集合从用户态拷贝到内核态
-
万级连接时内存拷贝开销显著
-
-
epoll:
-
通过
epoll_ctl
预先注册 FD 到内核 -
epoll_wait
只需传递一个空的事件数组 -
内存开销对比(监控1万个FD):
方法 单次系统调用内存拷贝量 select ~80KB(sizeof(fd_set)) epoll ~4KB(epoll_event数组)
-
3. 支持边缘触发(ET)模式
-
水平触发(LT):
-
默认模式,只要 FD 可读/可写就会持续通知
-
可能导致重复唤醒
-
-
边缘触发(ET):
-
仅在状态变化时通知一次
-
必须一次性处理完所有数据
-
性能提升场景:
-
2.epoll 的底层实现
1. 关键数据结构
组件 | 数据结构 | 作用 |
---|---|---|
兴趣列表 | 红黑树 | 存储所有监控的 FD(快速查找/插入/删除) |
就绪队列 | 双向链表 | 存放已就绪的 FD 事件 |
回调机制 | 内核回调函数 | 当 FD 就绪时自动加入就绪队列 |
2. 工作流程
[用户态]
│
├── epoll_create() 创建epoll实例
│
├── epoll_ctl() 添加/修改/删除监控的FD(红黑树维护)
│
└── epoll_wait() 获取就绪事件(从就绪链表拷贝事件数组)
↑
[内核态] │
└── 当FD就绪时,内核回调函数将其加入就绪链表
-
将它插入 epoll 的 红黑树 rbr,用于快速查找管理;
-
为该 fd 的“事件触发点”注册一个 回调函数 ep_poll_callback;
-
当 fd 上的事件发生,比如 socket 可读,会调用这个回调函数;
-
回调函数会把对应
epitem
加入到 epoll 实例的 rdlist 链表; -
然后你调用
epoll_wait()
,从这个链表里直接取出就绪 fd。