使用select语句在C语言中进行I/O多路复用
在C语言中,我们通常使用阻塞式I/O函数(如read()
和write()
)来处理文件或套接字的读写操作,这些函数会阻塞程序的执行,直到数据传输完成,为了提高程序的并发性和响应性,我们可以使用非阻塞式I/O函数或者I/O多路复用技术,select语句是一种常用的I/O多路复用技术,它允许程序同时监视多个文件描述符的状态变化,从而实现在一个线程中处理多个I/O事件。
(图片来源网络,侵删)
select语句的基本用法
select语句的原型如下:
#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
:等待I/O事件发生的最长时间,如果为NULL,则表示无限等待。
返回值:
成功:返回发生读、写或异常事件的总文件描述符数。
超时:返回0。
出错:返回1。
select语句的使用步骤
1、将需要监视的文件描述符添加到相应的集合中。
2、调用select函数,等待I/O事件发生。
3、检查select函数的返回值和文件描述符集合,判断哪个文件描述符上发生了I/O事件。
4、对发生的I/O事件进行处理。
5、重复步骤14,直到程序结束。
示例代码
下面是一个简单的使用select语句的示例,该程序同时监视标准输入、标准输出和标准错误三个文件描述符的读、写和异常事件:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/select.h> #include <time.h> int main() { fd_set rfds, wfds, efds; struct timeval tv; int ret; char buf[1024]; char oldPath[] = "/tmp"; char newPath[] = "/var/tmp"; char oldPathFile[] = "/tmp/file"; char newPathFile[] = "/var/tmp/file"; // 初始化文件描述符集合和时间变量 FD_ZERO(&rfds); FD_ZERO(&wfds); FD_ZERO(&efds); tv.tv_sec = 5; // 等待5秒 tv.tv_usec = 0; // 将需要监视的文件描述符添加到相应的集合中 FD_SET(STDIN_FILENO, &rfds); // 监视标准输入的读事件 FD_SET(STDOUT_FILENO, &wfds); // 监视标准输出的写事件 FD_SET(STDERR_FILENO, &efds); // 监视标准错误的异常事件(如输出到终端) // 循环等待I/O事件发生,并处理发生的事件 while (1) { ret = select(3, &rfds, &wfds, &efds, &tv); // 监视3个文件描述符(标准输入、标准输出和标准错误)的事件,最多等待5秒 if (ret == 1) { // select函数出错,退出程序 perror("select"); exit(EXIT_FAILURE); } else if (ret == 0) { // 超时,继续等待下一个事件循环 continue; } else { // select函数返回值大于0,表示有I/O事件发生,处理发生的事件并更新时间变量和文件描述符集合 if (FD_ISSET(STDIN_FILENO, &rfds)) { // 标准输入有读事件发生,读取数据并显示在屏幕上 if (fgets(buf, sizeof(buf), stdin) != NULL) { // 读取成功,显示数据并清空缓冲区 printf("%s", buf); fflush(stdout); // 确保数据已经写入到标准输出缓冲区并显示在屏幕上(如果需要的话) } else { // 读取失败,可能是已经读到文件末尾或发生其他错误,退出程序 break; } } else if (FD_ISSET(STDOUT_FILENO, &wfds)) { // 标准输出有写事件发生,将数据写入到文件中(这里只是演示,实际上不会发生写事件) // do nothing, just for demonstration purposes } else if (FD_ISSET(STDERR_FILENO, &efds)) { // 标准错误有异常事件发生(如输出到终端),关闭文件描述符并退出程序(这里只是演示,实际上不会发生异常事件) close(STDERR_FILENO); // 关闭标准错误文件描述符(这里只是演示,实际上不会发生异常事件) exit(EXIT_SUCCESS); // 正常退出程序(这里只是演示,实际上不会发生异常事件) } else { // select函数返回值大于0,但没有检测到任何I/O事件发生,可能是已经读到文件末尾或发生其他错误,退出程序(这里只是演示,实际上不会发生这种情况) break; } } } unlink(oldPathFile); // 删除旧路径下的文件(这里只是演示,实际上不会发生写事件) link(newPathFile, oldPathFile); // 创建新路径下的链接(这里只是演示,实际上不会发生写事件) unlink(newPathFile); // 删除新路径下的文件(这里只是演示,实际上不会发生写事件) link(oldPathFile, newPathFile); // 恢复旧路径下的文件(这里只是演示,实际上不会发生写事件) close(STDERR_FILENO); // 关闭标准错误文件描述符(这里只是演示,实际上不会发生异常事件) // select语句的基本用法和使用步骤就介绍到这里了,通过使用select语句,我们可以在一个线程中同时处理多个I/O事件,从而提高程序的并发性和响应性,需要注意的是,select语句只能用于Linux和类Unix系统,Windows系统使用不同的API来实现类似的功能,select语句还有一些限制和不足之处,例如不能处理大量文件描述符、不能处理网络套接字等,在实际开发中,我们通常会使用更高级的I/O多路复用技术,如epoll、kqueue等。