1.概念与原理
定义:I/O 多路复用是一种同步 I/O 模型,它允许单个进程同时监视多个文件描述符(如套接字),以确定它们是否准备好进行 I/O 操作(可读、可写或出现异常)。select
是一种在 Unix 和类 Unix 系统中实现 I/O 多路复用的系统调用。
工作原理:select
函数会阻塞进程,直到被监视的文件描述符集合中的一个或多个满足指定的条件(可读、可写或异常)。它维护了三个文件描述符集合:readfds
(可读)、writefds
(可写)和exceptfds
(异常)。当调用select
时,内核会检查这些集合中的文件描述符状态,当有满足条件的文件描述符时,select
返回,应用程序可以通过遍历集合来确定具体是哪些文件描述符就绪。
2.函数原型与参数
#include <sys/select.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解释:
nfds
:是所有集合(readfds
、writefds
和exceptfds
)中最大文件描述符值加 1。这个参数限制了内核在检查文件描述符时的范围。readfds
:指向一个fd_set
结构的指针,用于监视可读事件的文件描述符集合。如果一个文件描述符在这个集合中,并且对应的 I/O 设备(如套接字)有数据可读,那么select
会返回并且这个文件描述符会在返回后的readfds
集合中被标记为就绪。writefds
:指向一个fd_set
结构的指针,用于监视可写事件的文件描述符集合。如果一个文件描述符在这个集合中,并且对应的 I/O 设备(如套接字)可以写入数据(例如,套接字的发送缓冲区有足够的空间),那么select
会返回并且这个文件描述符会在返回后的writefds
集合中被标记为就绪。exceptfds
:指向一个fd_set
结构的指针,用于监视异常事件的文件描述符集合。异常情况可能包括带外数据到达等情况。timeout
:指向一个struct timeval
结构的指针,用于指定select
的超时时间。如果这个参数为NULL
,select
会一直阻塞,直到有文件描述符就绪。struct timeval
结构有两个成员:tv_sec
(秒)和tv_usec
(微秒),用于指定超时的时间长度。
3.fd_set
结构与操作函数
fd_set
结构:fd_set
是一个用于存储文件描述符集合的结构。它在内部是一个位向量,每个位对应一个文件描述符。在实际使用中,通常不需要直接操作fd_set
的内部结构,而是通过一组宏来操作。
操作宏:
FD_ZERO(fd_set *set)
:将set
所指向的fd_set
清空,即把所有位都设置为 0,用于初始化一个文件描述符集合。FD_SET(int fd, fd_set *set)
:将文件描述符fd
添加到set
所指向的fd_set
中,即将对应的位置 1。FD_CLR(int fd, fd_set *set)
:将文件描述符fd
从set
所指向的fd_set
中清除,即将对应的位置 0。FD_ISSET(int fd, fd_set *set)
:检查文件描述符fd
是否在set
所指向的fd_set
中,如果在则返回 1,否则返回 0。
4.使用步骤与示例
步骤:
- 首先,使用
FD_ZERO
初始化readfds
、writefds
和exceptfds
(根据需要)。 - 然后,使用
FD_SET
将需要监视的文件描述符添加到相应的集合中。 - 接着,设置
timeout
参数来指定超时时间(如果需要)。 - 调用
select
函数,等待文件描述符就绪。 - 在
select
返回后,使用FD_ISSET
检查哪些文件描述符就绪,并进行相应的 I/O 操作。
示例(简单的 TCP 服务器使用select
处理多个客户端连接):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
int main() {
int server_socket, client_sockets[MAX_CLIENTS];
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len;
fd_set readfds;
int max_fd;
char buffer[BUFFER_SIZE];
// 创建服务器套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("Socket creation failed");
return 1;
}
// 初始化服务器地址结构
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 绑定服务器套接字到地址
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Binding failed");
close(server_socket);
return 1;
}
// 监听连接请求
if (listen(server_socket, MAX_CLIENTS) == -1) {
perror("Listening failed");
close(server_socket);
return 1;
}
// 初始化客户端套接字数组
for (int i = 0; i < MAX_CLIENTS; i++) {
client_sockets[i] = -1;
}
while (1) {
// 每次循环开始时重新初始化文件描述符集合
FD_ZERO(&readfds);
// 将服务器套接字添加到可读集合中
FD_SET(server_socket, &readfds);
max_fd = server_socket;
// 将客户端套接字添加到可读集合中
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i]!= -1) {
FD_SET(client_sockets[i], &readfds);
if (client_sockets[i] > max_fd) {
max_fd = client_sockets[i];
}
}
}
// 调用select等待文件描述符就绪
int activity = select(max_fd + 1, &readfds, NULL, NULL, NULL);
if (activity == -1) {
perror("Select error");
return 1;
} else if (activity == 0) {
// 超时情况,这里没有设置超时,所以一般不会进入这个分支
continue;
} else {
// 检查服务器套接字是否可读,即有新的连接请求
if (FD_ISSET(server_socket, &readfds)) {
client_addr_len = sizeof(client_addr);
int new_client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
if (new_client_socket == -1) {
perror("Accept failed");
} else {
// 将新连接的客户端套接字添加到数组中
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == -1) {
client_sockets[i] = new_client_socket;
break;
}
}
}
}
// 检查客户端套接字是否可读,即有数据可读
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i]!= -1 && FD_ISSET(client_sockets[i], &readfds)) {
int bytes_read = read(client_sockets[i], buffer, BUFFER_SIZE);
if (bytes_read <= 0) {
// 客户端关闭连接或读取错误
close(client_sockets[i]);
client_sockets[i] = -1;
} else {
buffer[bytes_read] = '\0';
// 在这里可以对读取的数据进行处理,例如打印或转发等
printf("Received from client: %s", buffer);
// 简单地回显数据给客户端
write(client_sockets[i], buffer, strlen(buffer));
}
}
}
}
}
// 关闭服务器套接字
close(server_socket);
return 0;
}
示例解释:
1.首先创建一个 TCP 服务器套接字,绑定到指定端口并监听连接请求。
2.然后进入一个无限循环,在每次循环中:
- 初始化
readfds
集合,将服务器套接字添加进去,并遍历客户端套接字数组,将非-1
(表示有效的客户端连接)的客户端套接字也添加到readfds
集合中,同时记录最大的文件描述符值。 - 调用
select
函数等待文件描述符就绪。 - 如果
select
返回后,首先检查服务器套接字是否在readfds
集合中(即是否有新的连接请求),如果是,则接受新连接并将新的客户端套接字添加到客户端套接字数组中。 - 接着遍历客户端套接字数组,检查每个客户端套接字是否在
readfds
集合中(即是否有数据可读),如果是,则读取数据,处理数据(这里是简单地回显),并根据读取结果判断是否关闭连接。
5.优缺点
优点:
- 可以同时处理多个文件描述符,提高服务器的并发处理能力,适用于处理大量的 I/O 操作,如在网络服务器中处理多个客户端连接。
- 跨平台性较好,在许多 Unix 和类 Unix 系统中都有支持。
缺点:
- 每次调用
select
都需要将文件描述符集合从用户空间复制到内核空间,并且在返回时还要从内核空间复制回用户空间,当文件描述符数量很大时,这种复制操作会带来较大的开销。 - 单个进程可监视的文件描述符数量有限,通常由
FD_SETSIZE
定义,虽然可以通过一些手段修改这个限制,但会增加系统的复杂性。 - 每次调用
select
都需要遍历所有的文件描述符集合来确定哪些文件描述符就绪,当文件描述符数量很大时,效率较低。