前言
本篇文章带大家来学习一下多路IO复用select函数的使用。
一、什么是多路IO复用
1.多路I/O复用(Multiplexing I/O)是一种用于同时监视和处理多个输入/输出(I/O)源的技术。它允许一个进程可以同时监听和处理多个文件描述符(sockets、文件、管道等),从而实现高效的事件驱动的编程模型。
2.在传统的I/O模型中,通常采用阻塞I/O或非阻塞I/O方式进行读写操作,为每个I/O源(例如一个socket连接)都创建一个线程或进程来处理。这种方式在高并发场景下,会导致线程或进程数目的剧增,系统资源浪费,且上下文切换开销大。
3.而多路I/O复用通过使用操作系统提供的机制,如select、poll、epoll(在Linux中),允许程序同时监听多个I/O源,实现了异步I/O操作。应用程序可以将多个I/O源注册到多路复用器中,并在多路复用器上等待事件的发生,一旦有事件就绪(如读写准备就绪),程序就可以针对这些就绪的事件进行相应的操作。
多路I/O复用的主要优点如下:
1.高效:通过事件驱动的方式,避免了线程和进程的频繁切换,减小了系统开销。
2.节省资源:使用较少的线程或进程来处理多个I/O源,节省了系统资源,并能够处理大量的并发连接。
3.简化编程模型:与传统的多线程或多进程编程方式相比,使用多路I/O复用可以简化编程模型,使得代码更加清晰和易于维护。
需要注意的是,多路I/O复用并非适用于所有情况,特别是在处理高延迟和大数据量的场景下可能不太适合。此外,不同的操作系统上多路复用器的机制和性能表现也有所不同。
二、select函数讲解
select()函数是一个用于多路复用的I/O模型,可以同时监视多个文件描述符,并在其中任何一个文件描述符就绪时进行相应的处理。它被广泛用于实现高效的事件驱动编程。
select()函数的原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
nfds:监视的文件描述符的最大值加1。
readfds:指向可读文件描述符集合的指针。
writefds:指向可写文件描述符集合的指针。
exceptfds:指向异常文件描述符集合的指针。
timeout:指定超时时间,选择阻塞的时长。
fd_set是一个文件描述符集合类型,通过宏定义和操作函数进行操作。
使用select()函数的步骤如下:
创建并初始化文件描述符集合。
将需要监视的文件描述符添加到对应的文件描述符集合中。
调用select()函数并传递文件描述符集合。
检查返回值,若为负数则表示出错,若为0则表示超时,若大于0则表示有文件描述符就绪。
使用FD_ISSET()宏检查具体哪些文件描述符就绪,并进行相应的处理。
下面是一个简单的示例代码:
#include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int main() { fd_set readfds; int max_fd, ret; FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); // 监视标准输入 max_fd = STDIN_FILENO + 1; struct timeval timeout; timeout.tv_sec = 5; // 设置超时时间为5秒 timeout.tv_usec = 0; ret = select(max_fd, &readfds, NULL, NULL, &timeout); if (ret == -1) { perror("select"); exit(EXIT_FAILURE); } else if (ret == 0) { printf("Timeout\n"); } else { if (FD_ISSET(STDIN_FILENO, &readfds)) { printf("Stdin is ready for reading\n"); // 读取标准输入数据 char buffer[100]; fgets(buffer, sizeof(buffer), stdin); printf("Received: %s", buffer); } } return 0; }
三、使用select编程并发服务器
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/select.h> int main() { int server = 0; struct sockaddr_in saddr = {0}; int client = 0; struct sockaddr_in caddr = {0}; socklen_t asize = 0; int len = 0; char buf[32] = {0}; int maxfd;//最大文件描述符 int ret = 0; int i = 0; server = socket(PF_INET, SOCK_STREAM, 0); if( server == -1 ) { printf("server socket error\n"); return -1; } saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = htonl(INADDR_ANY); saddr.sin_port = htons(8888); if( bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1 ) { printf("server bind error\n"); return -1; } if( listen(server, 128) == -1 ) { printf("server listen error\n"); return -1; } printf("server start success\n"); maxfd = server; fd_set rets; fd_set allrets; FD_ZERO(&allrets); FD_SET(server, &allrets); while( 1 ) { rets = allrets; ret = select(maxfd + 1, &rets, NULL, NULL, NULL);//使用select函数进行监听 if(ret > 0) { if(FD_ISSET(server, &rets)) { /*有客户端连接上来了*/ asize = sizeof(caddr); client = accept(server, (struct sockaddr*)&caddr, &asize); FD_SET(client, &allrets);//将连接上来的客户端设置进去 if(maxfd < client) { /*更新最大文件描述符*/ maxfd = client; } } else if(ret == -1) { for(i = server + 1; i <= maxfd; i++) { if(FD_ISSET(i, &rets))//判断是哪一个客户端有信息 { len = read(i, buf, 1024); if(len == 0)//客户端断开了连接 { FD_CLR(i, &allrets);//将断开连接的客户端清除出去 close(i);//关闭客户端 } else { printf("read len : %d read buf : %s\n", len, buf); write(i, buf, len); } } } } } else { printf("select is err\n"); } } close(server); return 0; }
注意点:
使用select()函数时,传入和传出的readfds等参数可能是不一样的。这是因为select()函数会修改传入的文件描述符集合,以指示哪些文件描述符已经就绪。
所以在使用select时需要先将readfds做一个备份,以免丢失一些文件描述符。
四、select函数的缺点
1.低效性:select()函数在功能上存在一些限制。它使用线性扫描的方式来遍历监视的文件描述符集合,因此在大量文件描述符的情况下效率较低。每次调用select()都需要将整个文件描述符集合传递给内核,并在返回时重新检查整个集合以确定哪些文件描述符已就绪。这种线性扫描的开销随着监视的文件描述符数量的增加而增加。
2.文件描述符数量限制:select()函数所能监视的文件描述符数量存在限制,在某些系统上,这个限制可能是比较小的,例如1024。因此,如果要监视的文件描述符数量超过了限制,就需要采用其他方法来解决。
3.阻塞模式:select()函数是一种阻塞式调用,即当没有任何文件描述符就绪时,它会一直阻塞等待。这会导致程序无法进行其他操作。虽然可以通过设置超时时间来解决这个问题,但超时时间过短可能导致错误的超时,而超时时间过长则会影响程序的响应性。
4.受限的事件类型:select()函数只能监视文件描述符的可读性、可写性和异常情况。如果需要监视其他类型的事件,例如定时器事件或信号事件,就无法使用select()函数。
5.每次调用重新初始化:每次调用select()函数之前,需要重新初始化文件描述符集合,将需要监视的文件描述符重新添加到集合中。这样的初始化过程比较繁琐,尤其当文件描述符集合发生变化时,需要手动更新集合。
总结
本篇文章主要讲解了使用select函数进行多路IO服用实现了并发服务器程序的编写。