Cpp网络通信04-IO多路复用-Select模型

Cpp网络通信04-IO多路复用-Select模型

select 模型的原理

select 是一种 I/O多路复用 技术,通过一个系统调用同时监控多个文件描述符(如网络套接字、管道、文件等),以判断哪些文件描述符处于可读、可写或出现异常的状态,从而避免为每个文件描述符分别阻塞等待的开销。


核心工作流程

  1. 初始化监听的文件描述符集合
    • select 使用 fd_set 数据结构来表示一组文件描述符。
    • 调用者需将感兴趣的文件描述符(如套接字)添加到 fd_set 中,分为三类:
      • 可读集合readfds):监听是否有数据可读。
      • 可写集合writefds):监听是否可以写入数据。
      • 异常集合exceptfds):监听是否有异常(如错误或带外数据)。
  2. 调用 select 系统调用
    • select 系统调用会阻塞进程,直到以下条件之一满足:
      • 任意一个文件描述符的状态发生变化(如变为可读、可写等)。
      • 达到指定的超时时间(可以设置为永不超时)。
      • 被信号中断。
    • 返回值为状态改变的文件描述符数量。
  3. 检查结果并处理事件
    • 使用 FD_ISSET(fd, &fd_set) 检查哪个文件描述符的状态已改变。
    • 对每个已准备好的文件描述符,执行相应的读、写或异常处理操作。
  4. 循环处理
    • 对于网络服务器等长时间运行的程序,通常会将上述流程放入循环中以持续监听和处理事件。

工作原理解析

  1. 文件描述符集合
    • select 内部对传入的 fd_set 进行拷贝并保存,用户态与内核态之间通过内存共享进行通信。
  2. 内核态监控
    • 内核会遍历传入的文件描述符集合,并检查每个描述符的状态。
    • 如果一个文件描述符有数据到达或准备好写入,内核会将该描述符标记为“已准备好”。
  3. 阻塞等待
    • 如果所有文件描述符都未准备好,select 将进入阻塞状态,直到至少一个文件描述符变为可用或超时。
  4. 事件通知
    • 当有文件描述符状态改变,select 解除阻塞并返回,用户可以通过检查 fd_set 来确认哪些描述符已准备好。

select 的关键特性

  • 多路复用:可以同时监控多个文件描述符,而无需为每个描述符创建一个线程或进程。
  • 水平触发:如果一个文件描述符变为可读,但未处理,它会在下次调用 select 时再次被报告(只要它仍然可读)。
  • 超时控制:支持指定阻塞的时间长度,通过 timeval 参数实现。
    • timeval 为零:非阻塞模式。
    • timeval 为 NULL:永久阻塞,直到有事件发生。

select 的局限性

  1. 文件描述符限制
    • select 的文件描述符数量受 FD_SETSIZE 限制(通常为 1024),可以通过调整系统参数或编译时修改该值,但仍然有上限。
  2. 效率低下
    • 每次调用 select 时,用户需要重新设置和传递文件描述符集合,内核需要逐个遍历这些描述符,效率不高。
  3. 不可区分事件类型
    • 如果一个描述符同时可读和可写,select 只能分别检测,而不能直接判断是哪种事件触发的。
  4. 高并发性能问题
    • 对于大量并发连接(如 10000+),select 的性能会明显下降,因为它需要遍历所有的文件描述符。

适用场景

select 通常适用于以下场景: - 连接数较少(<1000)的多客户端程序。 - 需要跨平台支持(POSIX标准,适用于Linux、Unix、Windows等)。 - 简单的网络服务器或工具程序。


更高效的替代方案

  1. poll
    • 不受 FD_SETSIZE 限制,可以处理更多文件描述符,但仍然需要遍历。
  2. epoll(Linux特有)
    • 提供事件驱动模型,通过回调机制避免遍历文件描述符,适合高并发场景。
  3. kqueue(BSD、macOS特有)
    • 类似于 epoll,效率高且适合高并发。
  4. io_uring(Linux 5.1+)
    • 提供更现代化和高效的异步I/O接口。

实例服务器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
int server_fd, new_socket, max_sd, sd, activity, valread;
int client_socket[MAX_CLIENTS] = {0};
struct sockaddr_in address;
fd_set readfds;

// 创建socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket failed");
exit(EXIT_FAILURE);
}

// 设置socket选项
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("Setsockopt failed");
exit(EXIT_FAILURE);
}

// 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("Bind failed");
exit(EXIT_FAILURE);
}

// 开始监听
if (listen(server_fd, 3) < 0) {
perror("Listen failed");
exit(EXIT_FAILURE);
}

std::cout << "Listening on port " << PORT << "..." << std::endl;

while (true) {
// 清空描述符集合并添加监听socket
FD_ZERO(&readfds);
FD_SET(server_fd, &readfds);
max_sd = server_fd;

// 添加客户端socket到集合
for (int i = 0; i < MAX_CLIENTS; i++) {
sd = client_socket[i];
if (sd > 0)
FD_SET(sd, &readfds);
if (sd > max_sd)
max_sd = sd;
}

// 等待文件描述符变为就绪
activity = select(max_sd + 1, &readfds, nullptr, nullptr, nullptr);
if ((activity < 0) && (errno != EINTR)) {
perror("Select error");
}

// 检查是否有新连接
if (FD_ISSET(server_fd, &readfds)) {
socklen_t addrlen = sizeof(address);
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen)) < 0) {
perror("Accept failed");
exit(EXIT_FAILURE);
}
std::cout << "New connection, socket fd: " << new_socket << std::endl;

// 添加新socket到数组
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_socket[i] == 0) {
client_socket[i] = new_socket;
break;
}
}
}

// 检查客户端socket是否有数据
for (int i = 0; i < MAX_CLIENTS; i++) {
sd = client_socket[i];
if (FD_ISSET(sd, &readfds)) {
char buffer[1024] = {0};
valread = read(sd, buffer, 1024);
if (valread == 0) {
// 客户端断开连接
std::cout << "Client disconnected, socket fd: " << sd << std::endl;
close(sd);
client_socket[i] = 0;
} else {
// 回显消息
buffer[valread] = '\0';
std::cout << "Message: " << buffer << std::endl;
send(sd, buffer, strlen(buffer), 0);
}
}
}
}

return 0;
}

总结

select 是一种经典的I/O多路复用技术,虽然存在性能局限,但其简单易用、跨平台支持使得它仍然适合小规模并发场景。在高性能需求下,应优先考虑使用 epoll 或其他更高效的替代方案。


Cpp网络通信04-IO多路复用-Select模型
https://chordfish-k.github.io/2024/12/31/cpp/cpp-webserver04/
作者
超弦鱼
发布于
2024年12月31日
许可协议