C++ select()函数的用法(附带实例)
select() 函数是一个早期在 UNIX 系统的 Berkeley Software Distribution(BSD)中出现的 I/O 多路复用技术,后被 POSIX 标准采纳。
select() 函数允许程序同时监视多个文件描述符,检测它们的状态变化(如数据可读或可写),从而高效地管理多个 I/O 操作而不需为每个操作创建独立线程或进程。这种能力尤其在网络通信和服务器编程中提高了并发性能。select() 还支持非阻塞 I/O,允许程序在等待 I/O 事件的同时执行其他任务,有效利用 CPU 资源。
尽管 select() 函数的跨平台性使其被广泛应用,但在处理大量连接时它有局限,如对监视的文件描述符数量有系统限制。为了应对这些挑战,开发出了更先进的技术如 poll 和 epoll,它们提供了扩展的功能和更高的效率。
本节将详细地探讨 select() 函数的原型和其参数,以及如何在实际编程中应用这些知识。
timeout 的设定有 3 种情况:
select() 函数的返回值有 3 种可能:
在使用 select() 函数后,通过检查 readfds、writefds 和 exceptfds 集合的变化,可以精确地知道哪些文件描述符已经准备好进行读、写或异常处理。这使得程序能够有效地响应多个 I/O 请求,提高程序的整体性能和响应速度。
1) 初始化文件描述符集合:
2) 调用 select() 函数:传入文件描述符集合和超时时间(timeval 结构),允许 select() 在超时或有描述符就绪时返回,避免无限等待。
3) 阻塞与等待 I/O 事件:select() 阻塞程序执行,直至至少一个文件描述符就绪或超时。
4) 检查就绪的文件描述符:当 select() 返回后,检查各文件描述符集合的状态,确定哪些文件描述符准备好进行读、写或异常处理。
5) 循环监控:如果继续监控是必要的,就重置文件描述符集合并重新调用 select(),这支持持续监控多个 I/O 源。
需要理解的是,文件描述符集合(fd_set)在计算机底层通常是以位数组(bit array)的形式实现的。在这个数组中,每个位代表一个文件描述符。如果某一位设为 1,那么对应的文件描述符就包含在这个集合里。
这样的设计主要是为了效率,操作位比操作整个数组元素要快,这在处理大量文件描述符时尤其有优势,能显著提高程序的运行速度。
然而,由于 fd_set 的大小是固定的,它能表示的文件描述符数量有限。这个数量通常由一个叫作 FD_SETSIZE 的常量决定,这个常量定义了 fd_set 可以跟踪的最大文件描述符数量。
在许多 UNIX 和 Linux 系统中,这个常量通常被设置为 1024,意味着 fd_set 和 select 函数默认能处理的文件描述符从 0 到 1023。
在本例中,当任意一个管道中有数据可读时,select() 函数就返回,并允许程序通过读操作来获取并输出这些数据。这展示了如何在 Linux 环境下使用 select() 函数来处理多个 I/O 源的事件驱动模型。
对于需要处理大量文件描述符或需要更高效事件处理机制的应用程序,可能需要考虑其他技术:
总的来说,尽管 select 提供了一个基本且广泛支持的跨平台解决方案,但在设计要求更高的现代应用中,开发者可能会考虑使用更高效的系统特定工具来优化性能和资源利用率。
select() 函数允许程序同时监视多个文件描述符,检测它们的状态变化(如数据可读或可写),从而高效地管理多个 I/O 操作而不需为每个操作创建独立线程或进程。这种能力尤其在网络通信和服务器编程中提高了并发性能。select() 还支持非阻塞 I/O,允许程序在等待 I/O 事件的同时执行其他任务,有效利用 CPU 资源。
尽管 select() 函数的跨平台性使其被广泛应用,但在处理大量连接时它有局限,如对监视的文件描述符数量有系统限制。为了应对这些挑战,开发出了更先进的技术如 poll 和 epoll,它们提供了扩展的功能和更高的效率。
本节将详细地探讨 select() 函数的原型和其参数,以及如何在实际编程中应用这些知识。
C++ select()函数的语法格式
select() 函数是 I/O 多路复用的经典实现,其基本原型如下:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);参数解释如下:
- nfds:这个参数的值是监控的文件描述符集合中最大文件描述符的值加 1。在使用 select() 函数时,必须确保这个参数被正确设置,以便函数能监视所有相关的文件描述符。
-
readfds、writefds、exceptfds:这 3 个参数分别代表读、写和异常监视的文件描述符集合。它们使用 fd_set 类型表示,这是一种通过位图来管理文件描述符的数据结构。以下是对 fd_set 操作的常用宏定义:
- FD_SET(fd, &set): 将文件描述符 fd 添加到集合 set 中。
- FD_CLR(fd, &set): 从集合 set 中移除文件描述符 fd。
- FD_ISSET(fd, &set): 检查文件描述符 fd 是否已被加入集合 set。
- FD_ZERO(&set): 清空集合 set 中的所有文件描述符。
- timeout:这是一个指向 timeval 结构的指针,该结构用于设定 select 等待 I/O 事件的超时时间。timeout 结构定义如下:
struct timeval { long tv_sec; // seconds long tv_usec; // microseconds };
timeout 的设定有 3 种情况:
- 当 timeout 为 NULL 时,select 会无限等待,直到至少有一个文件描述符就绪。
- 当 timeout 设置为 0(即 tv_sec 和 tv_usec 都为 0)时,select 会立即返回,用于轮询。
- 设置具体的时间,select 将等待直到该时间过去或者有文件描述符就绪。
select() 函数的返回值有 3 种可能:
- 大于 0:表示就绪的文件描述符数量,即有多少文件描述符已经准备好进行 I/O 操作;
- 等于 0:表示超时,没有文件描述符在指定时间内就绪;
- 小于 0:发生错误。错误发生时,应使用 perror 或 strerror 函数来获取具体的错误信息。
在使用 select() 函数后,通过检查 readfds、writefds 和 exceptfds 集合的变化,可以精确地知道哪些文件描述符已经准备好进行读、写或异常处理。这使得程序能够有效地响应多个 I/O 请求,提高程序的整体性能和响应速度。
C++ select()的原理和工作机制
select() 函数的原理和工作机制可以概括为以下几个步骤:1) 初始化文件描述符集合:
- 使用 fd_set 类型的集合来监控不同的 I/O 操作(读、写、异常)。
- 操作这些集合可使用宏:FD_SET(添加描述符)、FD_CLR(移除描述符)、FD_ISSET(检查描述符是否存在)、FD_ZERO(清空集合)。
2) 调用 select() 函数:传入文件描述符集合和超时时间(timeval 结构),允许 select() 在超时或有描述符就绪时返回,避免无限等待。
3) 阻塞与等待 I/O 事件:select() 阻塞程序执行,直至至少一个文件描述符就绪或超时。
4) 检查就绪的文件描述符:当 select() 返回后,检查各文件描述符集合的状态,确定哪些文件描述符准备好进行读、写或异常处理。
5) 循环监控:如果继续监控是必要的,就重置文件描述符集合并重新调用 select(),这支持持续监控多个 I/O 源。
需要理解的是,文件描述符集合(fd_set)在计算机底层通常是以位数组(bit array)的形式实现的。在这个数组中,每个位代表一个文件描述符。如果某一位设为 1,那么对应的文件描述符就包含在这个集合里。
这样的设计主要是为了效率,操作位比操作整个数组元素要快,这在处理大量文件描述符时尤其有优势,能显著提高程序的运行速度。
然而,由于 fd_set 的大小是固定的,它能表示的文件描述符数量有限。这个数量通常由一个叫作 FD_SETSIZE 的常量决定,这个常量定义了 fd_set 可以跟踪的最大文件描述符数量。
在许多 UNIX 和 Linux 系统中,这个常量通常被设置为 1024,意味着 fd_set 和 select 函数默认能处理的文件描述符从 0 到 1023。
C++ select()函数实例
以下是一个简单的示例,展示如何使用 select() 函数监控多个文件描述符的读操作。#include <sys/types.h> #include <sys/select.h> int main() { // 创建两个管道 std::array<int, 2> pipefds1, pipefds2; pipe(pipefds1.data()); // 创建第一个管道 pipe(pipefds2.data()); // 创建第二个管道 // 向管道写入数据 write(pipefds1[1], "Hello", 5); // 写入数据到第一个管道 write(pipefds2[1], "World", 5); // 写入数据到第二个管道 fd_set readfds; // 文件描述符集合,用于select调用 struct timeval timeout; // 时间结构体,用于设置超时 int ret, fd_max; // 用于存储select的返回值和文件描述符的最大值 while (true) { FD_ZERO(&readfds); // 清空文件描述符集 FD_SET(pipefds1[0], &readfds); // 将pipefds1[0]加入读集合 FD_SET(pipefds2[0], &readfds); // 将pipefds2[0]加入读集合 // 计算最大的文件描述符 fd_max = std::max(pipefds1[0], pipefds2[0]); // 设置超时时间为5秒 timeout.tv_sec = 5; timeout.tv_usec = 0; // 调用select等待文件描述符准备好或超时 ret = select(fd_max + 1, &readfds, nullptr, nullptr, &timeout); if (ret == -1) { perror("select"); // select调用失败 exit(EXIT_FAILURE); } else if (ret == 0) { std::cout << "Timeout!" << std::endl; // select超时 break; } else { // 检查文件描述符是否准备好读取数据 if (FD_ISSET(pipefds1[0], &readfds)) { char buf[6]; read(pipefds1[0], buf, 5); // 从pipefds1[0]读取数据 buf[5] = '\0'; std::cout << "Data from pipe1: " << buf << std::endl; } if (FD_ISSET(pipefds2[0], &readfds)) { char buf[6]; read(pipefds2[0], buf, 5); // 从pipefds2[0]读取数据 buf[5] = '\0'; std::cout << "Data from pipe2: " << buf << std::endl; } break; } } // 关闭管道文件描述符 close(pipefds1[0]); close(pipefds1[1]); close(pipefds2[0]); close(pipefds2[1]); return 0; }在这个示例中,首先创建了两个管道,并向这些管道分别写入了 "Hello" 和 "World" 两个字符串。接着,利用 select() 函数来监控这两个管道的读文件描述符。这个函数的功能是等待直到一个或多个文件描述符准备好进行 I/O 操作。
在本例中,当任意一个管道中有数据可读时,select() 函数就返回,并允许程序通过读操作来获取并输出这些数据。这展示了如何在 Linux 环境下使用 select() 函数来处理多个 I/O 源的事件驱动模型。
对于需要处理大量文件描述符或需要更高效事件处理机制的应用程序,可能需要考虑其他技术:
- 在类 UNIX 系统中:可以使用 poll 或 epoll(仅限 Linux)作为更现代且效率更高的替代方案;
- 在 Windows 系统中:通常使用 I/O 完成端口(IOCP),这是一种专为高性能 I/O 操作和高并发设计的机制。
总的来说,尽管 select 提供了一个基本且广泛支持的跨平台解决方案,但在设计要求更高的现代应用中,开发者可能会考虑使用更高效的系统特定工具来优化性能和资源利用率。