解析 select 函数
解析 select
函数
select
函数是 Unix/Linux 系统中用于多路复用的系统调用,主要用于在多个文件描述符(file descriptors)上等待事件的发生。它允许程序同时监视多个 I/O 通道,并在任意一个通道准备好进行 I/O 操作时通知程序,从而实现高效的 I/O 处理。
函数原型
#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds: 监视的文件描述符集合中最大的文件描述符值加1。
- readfds: 指向文件描述符集合的指针,监视其中是否有可读事件。
- writefds: 指向文件描述符集合的指针,监视其中是否有可写事件。
- exceptfds: 指向文件描述符集合的指针,监视其中是否有异常事件。
- timeout: 超时时间,指定
select
等待的最长时间。如果为 NULL,则select
会一直阻塞直到有事件发生。
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
是否在集合中。
工作原理
select
的工作流程如下:
- 初始化文件描述符集合: 使用
FD_ZERO
初始化readfds
、writefds
和exceptfds
。 - 添加监视的文件描述符: 使用
FD_SET
将需要监视的文件描述符添加到相应的集合中。 - 调用
select
:select
会阻塞,直到至少有一个文件描述符准备好进行 I/O 操作,或者超时。 - 检查结果: 使用
FD_ISSET
检查哪些文件描述符准备好了,并根据需要进行处理。
应用场景
1. 网络服务器
描述: 网络服务器需要同时处理多个客户端连接。select
可以监视所有客户端的套接字,检测哪些套接字有数据可读或可写。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>#define PORT 8080
#define MAX_CLIENTS 1024int main() {int server_fd, new_socket, client_sockets[MAX_CLIENTS];fd_set read_fds;struct sockaddr_in address;int opt = 1;int addrlen = sizeof(address);int max_sd;// 初始化客户端套接字数组for (int i = 0; i < MAX_CLIENTS; i++) {client_sockets[i] = 0;}// 创建服务器套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 绑定套接字if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {perror("setsockopt");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");exit(EXIT_FAILURE);}while (1) {// 清空文件描述符集合FD_ZERO(&read_fds);// 添加服务器套接字到集合中FD_SET(server_fd, &read_fds);max_sd = server_fd;// 添加客户端套接字到集合中for (int i = 0; i < MAX_CLIENTS; i++) {int sd = client_sockets[i];if (sd > 0) {FD_SET(sd, &read_fds);}if (sd > max_sd) {max_sd = sd;}}// 调用 selectint activity = select(max_sd + 1, &read_fds, NULL, NULL, NULL);if (activity < 0) {perror("select error");exit(EXIT_FAILURE);}// 检查是否有新连接if (FD_ISSET(server_fd, &read_fds)) {if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}printf("New connection, socket fd is %d\n", new_socket);// 添加到客户端数组中for (int i = 0; i < MAX_CLIENTS; i++) {if (client_sockets[i] == 0) {client_sockets[i] = new_socket;break;}}}// 检查客户端套接字是否有数据可读for (int i = 0; i < MAX_CLIENTS; i++) {int sd = client_sockets[i];if (FD_ISSET(sd, &read_fds)) {char buffer[1024] = {0};int valread = read(sd, buffer, 1024);if (valread == 0) {// 客户端断开连接printf("Connection closed, socket fd is %d\n", sd);close(sd);client_sockets[i] = 0;} else {// 处理数据printf("Received message: %s\n", buffer);// 回显send(sd, buffer, valread, 0);}}}}return 0;
}
2. 终端应用
描述: 终端应用需要同时处理用户输入和定时事件。select
可以监视标准输入和定时器文件描述符。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>int main() {fd_set read_fds;struct timeval timeout;while (1) {// 清空文件描述符集合FD_ZERO(&read_fds);// 添加标准输入到集合中FD_SET(STDIN_FILENO, &read_fds);// 设置超时时间为10秒timeout.tv_sec = 10;timeout.tv_usec = 0;int activity = select(STDIN_FILENO + 1, &read_fds, NULL, NULL, &timeout);if (activity < 0) {perror("select");break;} else if (activity == 0) {printf("Timeout occurred!\n");} else {if (FD_ISSET(STDIN_FILENO, &read_fds)) {char buffer[100];int n = read(STDIN_FILENO, buffer, sizeof(buffer));if (n > 0) {buffer[n] = '\0';printf("You entered: %s\n", buffer);}}}}return 0;
}
优缺点
优点
- 简单易用:
select
提供了一种简单的方法来监视多个文件描述符。 - 跨平台: 在大多数 Unix/Linux 系统上都有实现,具有良好的可移植性。
- 灵活性: 可以监视不同类型的 I/O 事件(读、写、异常)。
缺点
- 性能问题:
select
的性能在处理大量文件描述符时较差,因为每次调用都需要遍历所有监视的文件描述符。 - 文件描述符数量限制:
select
通常有一个上限(通常是 1024),对于需要监视大量连接的应用不适用。 - 可读性差: 使用
select
的代码通常较为复杂,难以维护。
替代方案
由于 select
的局限性,现代应用中更常使用以下替代方案:
-
poll:
- 描述:
poll
提供了与select
类似的功能,但使用不同的接口,支持更大的文件描述符集合。 - 优点: 没有文件描述符数量限制,性能优于
select
。 - 缺点: 仍然需要遍历所有文件描述符,效率提升有限。
- 描述:
-
epoll (Linux):
- 描述:
epoll
是 Linux 特有的高效 I/O 多路复用机制,适用于处理大量并发连接。 - 优点: 高效,支持边缘触发和水平触发,事件驱动。
- 缺点: 仅适用于 Linux 系统。
- 描述:
-
kqueue (BSD/macOS):
- 描述:
kqueue
是 BSD 系统(如 macOS)上的高效 I/O 多路复用机制。 - 优点: 高效
- 描述:
select
函数在以下情况下会检测写就绪:
-
发送缓冲区有足够空间:
- 当发送缓冲区中有足够的空间来容纳要写入的数据时,
select
会将对应的文件描述符标记为写就绪。这意味着对文件描述符执行写操作不会阻塞【5†source】【8†source】。
- 当发送缓冲区中有足够的空间来容纳要写入的数据时,
-
写操作被关闭:
- 当对文件描述符的写操作被关闭(例如,通过
close
或shutdown
函数)时,对这个写操作被关闭的文件描述符进行写操作会触发SIGPIPE
信号。在这种情况下,select
会将文件描述符标记为写就绪【5†source】【8†source】。
- 当对文件描述符的写操作被关闭(例如,通过
-
非阻塞
connect
操作完成:- 对于非阻塞的
connect
操作,当连接成功或失败时,select
会将文件描述符标记为写就绪【8†source】。
- 对于非阻塞的
-
异常事件:
- 虽然主要与写操作相关,但
select
也会检测异常事件。例如,当socket
收到带外数据时,select
会将文件描述符标记为异常就绪【8†source】。
- 虽然主要与写操作相关,但
select监听写就绪
-
发送缓冲区空间:
- 当发送缓冲区的空闲空间大于或等于低水位标记
SO_SENDLOWAT
时,select
会将文件描述符标记为写就绪。这意味着程序可以无阻塞地写入数据【5†source】。
- 当发送缓冲区的空闲空间大于或等于低水位标记
-
写操作关闭:
- 如果对文件描述符的写操作被关闭,尝试写入数据会导致
SIGPIPE
信号。在这种情况下,select
会将文件描述符标记为写就绪,以便程序可以处理这种情况【5†source】。
- 如果对文件描述符的写操作被关闭,尝试写入数据会导致
-
非阻塞
connect
:- 对于非阻塞的
connect
操作,select
可以用来检测连接是否成功或失败。当连接操作完成时,select
会将文件描述符标记为写就绪【8†source】。
- 对于非阻塞的
示例代码
以下是一个简单的示例,展示了如何使用 select
来检测写就绪:
#include <stdio.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {perror("socket");return 1;}struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET;addr.sin_port = htons(8080);addr.sin_addr.s_addr = inet_addr("127.0.0.1");// 尝试连接int ret = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));if (ret < 0) {perror("connect");close(sockfd);return 1;}fd_set writefds;struct timeval timeout;timeout.tv_sec = 5;timeout.tv_usec = 0;FD_ZERO(&writefds);FD_SET(sockfd, &writefds);ret = select(sockfd + 1, NULL, &writefds, NULL, &timeout);if (ret < 0) {perror("select");close(sockfd);return 1;} else if (ret == 0) {printf("Timeout occurred!\n");} else {if (FD_ISSET(sockfd, &writefds)) {printf("Socket is ready for writing!\n");}}close(sockfd);return 0;
}
在这个示例中,select
会等待最多5秒钟,直到 sockfd
准备好进行写操作。如果 sockfd
准备好,select
会返回,并通过 FD_ISSET
检查是否就绪。
总结
select
函数通过监视文件描述符的读写和异常状态,提供了一种有效的多路复用机制。