在Linux中,select
函数是用于监控多个文件描述符(file descriptors)的系统调用。它允许程序员同时等待多个I/O事件的发生,如文件读取、写入或异常状态。它在处理多路复用I/O时非常常用,尤其适合编写网络服务器或客户端程序。
select
函数的定义
#include <sys/select.h>
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
参数解释:
nfds
:指定监控的文件描述符数量。它应该是所有监控的文件描述符集合中最大值加1,因为文件描述符是从0开始计数的。readfds
:指向一个文件描述符集合,用于监控是否有文件可读。可以使用宏函数FD_SET()
将描述符添加到集合。writefds
:指向一个文件描述符集合,用于监控是否有文件可写。exceptfds
:指向一个文件描述符集合,用于监控异常状态。timeout
:指定select
等待的时间,可以是:- NULL :
select
将无限期等待,直到有文件描述符准备好。 - 0秒的时间 :表示非阻塞模式,
select
立即返回。 - 自定义时间 :例如等待5秒,可以通过
struct timeval
指定。
- NULL :
返回值:
- 返回值为大于0的数值表示有多少文件描述符准备好。
- 返回0表示超时。
- 返回-1表示出错,并且设置
errno
。
使用步骤
-
清空文件描述符集合 : 在使用
select
之前,首先需要初始化或清空文件描述符集合。FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
-
设置需要监控的文件描述符 : 使用
FD_SET()
函数将需要监控的文件描述符加入到集合中。FD_SET(fd, &readfds);
-
调用
select
函数 : 通过调用select
来监控多个文件描述符。int ready = select(nfds, &readfds, &writefds, &exceptfds, &timeout);
-
检查哪些文件描述符已准备好 :
select
返回后,可以使用FD_ISSET()
函数检查哪些文件描述符已经准备好。if (FD_ISSET(fd, &readfds)) { // 该文件描述符可读 }
示例代码
以下是一个简单的例子,演示如何使用 select
同时监控标准输入和一个网络套接字的读事件:
cpp
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in server;
fd_set readfds;
struct timeval timeout;
// 创建一个socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
server.sin_family = AF_INET;
server.sin_port = htons(8080);
server.sin_addr.s_addr = inet_addr("127.0.0.1");
// 连接服务器
connect(sockfd, (struct sockaddr *)&server, sizeof(server));
while (1) {
// 清空集合并添加文件描述符
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds); // 标准输入
FD_SET(sockfd, &readfds); // 套接字
// 计算 nfds
int nfds = sockfd + 1;
// 设置超时时间,5秒
timeout.tv_sec = 5;
timeout.tv_usec = 0;
// 调用 select 函数
int ready = select(nfds, &readfds, NULL, NULL, &timeout);
if (ready == -1) {
perror("select error");
return 1;
} else if (ready == 0) {
printf("Timeout, no data available\n");
} else {
// 检查是否标准输入可读
if (FD_ISSET(STDIN_FILENO, &readfds)) {
char buffer[256];
read(STDIN_FILENO, buffer, sizeof(buffer));
printf("Input: %s", buffer);
}
// 检查套接字是否可读
if (FD_ISSET(sockfd, &readfds)) {
char buffer[256];
int bytes = read(sockfd, buffer, sizeof(buffer));
if (bytes > 0) {
printf("Received from server: %s\n", buffer);
} else {
printf("Server closed connection\n");
break;
}
}
}
}
close(sockfd);
return 0;
}
文件描述符集合的操作
为了管理 select
函数的文件描述符集合,使用了以下几个宏:
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
是否在集合中,返回非0值表示在集合中。
select
的限制
-
文件描述符的数量 :
select
对于监控的文件描述符数量有限制,通常为FD_SETSIZE
,在许多系统中默认为1024。对于大量连接的场景,推荐使用poll
或epoll
。 -
性能问题 : 当文件描述符数量非常多时,
select
的性能会急剧下降,因为它需要遍历所有的描述符来检查哪些可用。
相关系统调用
poll
:与select
类似,但没有文件描述符数量限制,并且性能在大文件描述符集上更好。epoll
:Linux特有的多路复用系统调用,性能高于select
和poll
,适合处理大规模并发连接。
总结
select
函数是一个经典的多路复用I/O处理函数,适用于监控少量文件描述符的场景。尽管有一定的限制和性能瓶颈,但在简单的网络编程场景下仍然常用。对于需要处理大量并发连接的场景,推荐使用 epoll
等更现代化的系统调用。