目录
概述
在Linux中,select 是一个经典的IO多路复用函数,用于监控多个文件描述符(file descriptors)的状态变化(如可读、可写或异常)。它允许程序同时等待多个文件描述符,并在其中任何一个文件描述符就绪时返回。
函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
-
nfds:-
需要监控的文件描述符的最大值加 1。
-
例如,监控的文件描述符是 0、1、2,则
nfds应为 3。
-
-
readfds:-
指向一个
fd_set类型的文件描述符集合,用于监控是否有数据可读。 -
如果为
NULL,则表示不监控读事件。
-
-
writefds:-
指向一个
fd_set类型的文件描述符集合,用于监控是否可写。 -
如果为
NULL,则表示不监控写事件。
-
-
exceptfds:-
指向一个
fd_set类型的文件描述符集合,用于监控是否有异常事件。 -
如果为
NULL,则表示不监控异常事件。
-
-
timeout:-
指向
struct timeval结构体,用于设置超时时间。 -
如果为
NULL,则表示阻塞等待,直到有文件描述符就绪。 -
如果设置为 0,则表示非阻塞,立即返回。
-
返回值:
-
成功:返回就绪的文件描述符的总数。
-
超时:返回 0。
-
错误:返回 -1,并设置
errno。
fd_set 相关操作函数
fd_set 是一个文件描述符集合,通常通过以下宏来操作:
-
FD_ZERO(fd_set *set):清空集合。 -
FD_SET(int fd, fd_set *set):将文件描述符fd添加到集合中。 -
FD_CLR(int fd, fd_set *set):将文件描述符fd从集合中移除。 -
FD_ISSET(int fd, fd_set *set):检查文件描述符fd是否在集合中。
struct timeval 结构体
timeout 参数是一个指向 struct timeval 的指针,定义如下:
struct timeval {
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
};
select 的工作流程
-
初始化
fd_set集合,并使用FD_SET添加需要监控的文件描述符。 -
调用
select函数,阻塞等待文件描述符就绪。 -
当
select返回时,使用FD_ISSET检查哪些文件描述符就绪。 -
处理就绪的文件描述符。
代码示例
示例1
以下是一个简单的 select 示例,监控标准输入(stdin)的可读事件:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
int main() {
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds); // 监控标准输入
while (1) {
printf("Waiting for input...\n");
fd_set tmp_fds = read_fds; // 复制一份,因为 select 会修改 fd_set
int ret = select(STDIN_FILENO + 1, &tmp_fds, NULL, NULL, NULL);
if (ret == -1) {
perror("select");
exit(EXIT_FAILURE);
}
if (FD_ISSET(STDIN_FILENO, &tmp_fds)) {
printf("Data available on stdin\n");
char buf[1024];
int n = read(STDIN_FILENO, buf, sizeof(buf));
if (n > 0) {
printf("Read: %.*s", n, buf);
} else if (n == 0) {
printf("EOF reached\n");
break;
} else {
perror("read");
exit(EXIT_FAILURE);
}
}
}
return 0;
}
代码说明:
-
使用
FD_SET将标准输入(STDIN_FILENO)添加到read_fds集合中。 -
调用
select阻塞等待标准输入的可读事件。 -
当标准输入有数据可读时,
select返回,程序读取数据并打印。
示例2
下面是一个使用select函数的简单示例,展示如何在一个TCP服务器中监听新的连接请求以及已建立连接上的数据接收。
这个例子中,服务器会创建一个TCP套接字并绑定到指定端口,然后使用select来监听新的连接请求以及已建立连接上的数据接收。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
int main() {
int server_sock, client_socks[MAX_CLIENTS], max_sd, sd;
int activity, valread, new_socket;
fd_set readfds;
struct sockaddr_in address;
char buffer[BUFFER_SIZE];
int addrlen = sizeof(address);
// 初始化所有客户端套接字为0
for (int i = 0; i < MAX_CLIENTS; i++) {
client_socks[i] = 0;
}
// 创建socket
if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// 绑定socket
if (bind(server_sock, (struct sockaddr *)&address, sizeof(address)) == -1) {
perror("Bind failed");
close(server_sock);
exit(EXIT_FAILURE);
}
// 监听socket
if (listen(server_sock, 5) == -1) {
perror("Listen failed");
close(server_sock);
exit(EXIT_FAILURE);
}
printf("Server is running and listening on port 8080...\n");
while (1) {
FD_ZERO(&readfds); // 清空文件描述符集合
FD_SET(server_sock, &readfds); // 将服务器套接字加入集合
max_sd = server_sock;
// 将所有客户端套接字加入集合
for (int i = 0; i < MAX_CLIENTS; i++) {
sd = client_socks[i];
if (sd > 0)
FD_SET(sd, &readfds);
if (sd > max_sd)
max_sd = sd;
}
// 使用select等待活动
activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
perror("Select error");
}
// 如果是新连接请求
if (FD_ISSET(server_sock, &readfds)) {
if ((new_socket = accept(server_sock, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("Accept failed");
continue;
}
printf("New connection, socket fd is %d , ip is : %s , port : %d \n", new_socket,
inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 将新连接添加到数组中
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_socks[i] == 0) {
client_socks[i] = new_socket;
break;
}
}
}
// 检查现有连接是否有数据可读
for (int i = 0; i < MAX_CLIENTS; i++) {
sd = client_socks[i];
if (FD_ISSET(sd, &readfds)) {
// 读取数据
if ((valread = read(sd, buffer, BUFFER_SIZE)) == 0) {
// 客户端断开连接
getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
printf("Host disconnected , ip %s , port %d \n",
inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 关闭套接字并清除数组中的条目
close(sd);
client_socks[i] = 0;
} else {
// 回显数据给客户端
send(sd, buffer, strlen(buffer), 0);
printf("Sent echoed message to client\n");
}
}
}
}
close(server_sock);
return 0;
}
代码说明:
1. 初始化:
- 创建一个服务器套接字,并将其绑定到本地地址和端口。
- 设置监听队列长度为5。
2. 主循环:
- 使用FD_ZERO清空文件描述符集合并使用FD_SET将服务器套接字添加到集合中。
- 遍历客户端套接字数组,将有效套接字添加到集合中,并更新最大文件描述符编号max_sd。
- 调用select函数等待任何套接字有活动(即有新的连接请求或者已有连接上有数据可读)。
3. 处理新连接:
- 当检测到服务器套接字有活动时,表示有新的连接请求到达。调用accept接受连接并将新套接字添加到客户端套接字数组中。
4. 处理已有连接的数据:
- 遍历客户端套接字数组,检查每个套接字是否在select返回的集合中有活动。
- 如果某个套接字有活动,则读取数据。如果读取到的数据长度为0,表示客户端已经关闭连接;否则,回显数据给客户端。
这个示例展示了如何使用select函数来管理多个连接,适用于小型服务器场景。对于更大的应用,可能需要考虑更高效的I/O多路复用机制,比如poll或epoll。
select 的优缺点
优点
-
跨平台支持,几乎所有操作系统都实现了
select。 -
简单易用,适合监控少量文件描述符。
缺点
-
文件描述符数量有限(通常为 1024)。
-
每次调用需要重新设置
fd_set,效率较低。 -
时间复杂度为 O(n),性能较差,不适合高并发场景。
适用场景
-
监控少量文件描述符。
-
需要跨平台兼容性的场景。
-
对性能要求不高的场景。
如果需要高性能或监控大量文件描述符,建议使用 epoll(Linux)或 kqueue(BSD/macOS)。
1508

被折叠的 条评论
为什么被折叠?



