在 Linux 编程领域,I/O 多路复用是一项关键技术,它能让程序同时监听多个文件描述符的事件,显著提升程序的性能和效率。今天咱就来讲讲基于 Linux 的 Select 和 Poll 这两种 I/O 多路复用机制的编程实战。
======================================================================================================================
一、I/O 多路复用基础概念
在传统的 I/O 模型中,程序通常一次只能处理一个 I/O 操作,这在需要处理多个并发 I/O 请求时效率很低。而 I/O 多路复用允许程序在一个线程内同时监控多个文件描述符(比如套接字、管道等),当其中任何一个文件描述符准备好进行 I/O 操作时,程序就能及时处理,避免了不必要的等待。
二、Select 编程实战
1. Select 函数介绍
select
函数是 Linux 提供的一种实现 I/O 多路复用的机制。它的函数原型如下:
arduino
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
:监控的文件描述符集里最大文件描述符加 1。readfds
、writefds
、exceptfds
:分别是监控读、写和异常事件的文件描述符集合。timeout
:设置的超时时间,如果为NULL
,表示一直等待,直到有事件发生。
2. 代码示例
以下是一个简单的使用 select
实现同时监听标准输入和套接字的示例:
lua
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <string.h>#include <sys/select.h>#define PORT 8888#define BUFFER_SIZE 1024int main() { int sockfd, connfd; struct sockaddr_in servaddr, cliaddr; // 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("Socket creation failed"); exit(EXIT_FAILURE); } memset(&servaddr, 0, sizeof(servaddr)); memset(&cliaddr, 0, sizeof(cliaddr)); // 填充服务器地址结构 servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = INADDR_ANY; servaddr.sin_port = htons(PORT); // 绑定套接字 if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("Bind failed"); close(sockfd); exit(EXIT_FAILURE); } // 监听连接 if (listen(sockfd, 5) < 0) { perror("Listen failed"); close(sockfd); exit(EXIT_FAILURE); } fd_set read_fds; FD_ZERO(&read_fds); FD_SET(STDIN_FILENO, &read_fds); // 添加标准输入到读集合 FD_SET(sockfd, &read_fds); // 添加套接字到读集合 int max_fd = sockfd; char buffer[BUFFER_SIZE]; while (1) { fd_set tmp_fds = read_fds; int activity = select(max_fd + 1, &tmp_fds, NULL, NULL, NULL); if (activity < 0) { perror("Select error"); break; } else if (activity > 0) { if (FD_ISSET(STDIN_FILENO, &tmp_fds)) { // 标准输入有数据 memset(buffer, 0, sizeof(buffer)); read(STDIN_FILENO, buffer, sizeof(buffer)); printf("Read from stdin: %s", buffer); } if (FD_ISSET(sockfd, &tmp_fds)) { // 有新连接 socklen_t len = sizeof(cliaddr); connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len); if (connfd < 0) { perror("Accept failed"); continue; } printf("New connection accepted: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); FD_SET(connfd, &read_fds); if (connfd > max_fd) { max_fd = connfd; } } for (int i = sockfd + 1; i <= max_fd; i++) { if (FD_ISSET(i, &tmp_fds)) { // 套接字有数据可读 memset(buffer, 0, sizeof(buffer)); int n = read(i, buffer, sizeof(buffer)); if (n < 0) { perror("Read from socket failed"); close(i); FD_CLR(i, &read_fds); } else if (n == 0) { // 对方关闭连接 printf("Connection closed\n"); close(i); FD_CLR(i, &read_fds); } else { printf("Received from socket: %s", buffer); } } } } } close(sockfd); return 0;}
3. 代码解析
- 首先创建套接字,绑定并监听端口。
- 初始化一个
fd_set
集合,将标准输入(STDIN_FILENO
)和套接字描述符添加到读集合中。 - 在循环中,调用
select
函数等待事件发生。如果select
返回值小于 0,表示出错;大于 0 则表示有事件发生。 - 通过
FD_ISSET
宏检查是哪个文件描述符有事件。如果是标准输入有数据,读取并打印;如果是套接字有新连接,接受连接并将新连接的套接字添加到fd_set
中;如果是已有套接字有数据可读,读取并处理数据。
三、Poll 编程实战
1. Poll 函数介绍
poll
函数也是 Linux 提供的 I/O 多路复用机制,与 select
类似,但在某些方面更灵活。它的函数原型如下:
arduino
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds
:一个结构体数组,每个元素包含文件描述符、要监控的事件类型以及返回的事件类型。nfds
:数组中元素的个数。timeout
:超时时间,单位是毫秒。
2. 代码示例
下面是使用 poll
实现类似功能的代码:
lua
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <string.h>#include <poll.h>#define PORT 8888#define BUFFER_SIZE 1024int main() { int sockfd, connfd; struct sockaddr_in servaddr, cliaddr; // 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("Socket creation failed"); exit(EXIT_FAILURE); } memset(&servaddr, 0, sizeof(servaddr)); memset(&cliaddr, 0, sizeof(cliaddr)); // 填充服务器地址结构 servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = INADDR_ANY; servaddr.sin_port = htons(PORT); // 绑定套接字 if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("Bind failed"); close(sockfd); exit(EXIT_FAILURE); } // 监听连接 if (listen(sockfd, 5) < 0) { perror("Listen failed"); close(sockfd); exit(EXIT_FAILURE); } struct pollfd fds[1024]; fds[0].fd = STDIN_FILENO; fds[0].events = POLLIN; fds[1].fd = sockfd; fds[1].events = POLLIN; int nfds = 2; char buffer[BUFFER_SIZE]; while (1) { int activity = poll(fds, nfds, -1); if (activity < 0) { perror("Poll error"); break; } else if (activity > 0) { for (int i = 0; i < nfds; i++) { if (fds[i].revents & POLLIN) { if (fds[i].fd == STDIN_FILENO) { // 标准输入有数据 memset(buffer, 0, sizeof(buffer)); read(STDIN_FILENO, buffer, sizeof(buffer)); printf("Read from stdin: %s", buffer); } else if (fds[i].fd == sockfd) { // 有新连接 socklen_t len = sizeof(cliaddr); connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len); if (connfd < 0) { perror("Accept failed"); continue; } printf("New connection accepted: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); fds[nfds].fd = connfd; fds[nfds].events = POLLIN; nfds++; } else { // 套接字有数据可读 memset(buffer, 0, sizeof(buffer)); int n = read(fds[i].fd, buffer, sizeof(buffer)); if (n < 0) { perror("Read from socket failed"); close(fds[i].fd); for (int j = i; j < nfds - 1; j++) { fds[j] = fds[j + 1]; } nfds--; } else if (n == 0) { // 对方关闭连接 printf("Connection closed\n"); close(fds[i].fd); for (int j = i; j < nfds - 1; j++) { fds[j] = fds[j + 1]; } nfds--; } else { printf("Received from socket: %s", buffer); } } } } } } close(sockfd); return 0;}
3. 代码解析
- 创建套接字、绑定和监听的过程与
select
示例相同。 - 初始化一个
struct pollfd
数组,将标准输入和套接字的文件描述符及其要监控的事件(POLLIN
表示读事件)添加到数组中。 - 在循环中调用
poll
函数等待事件发生。如果有事件发生,通过检查revents
字段确定是哪个文件描述符的什么事件。处理标准输入、新连接和已有套接字数据的方式与select
示例类似,但在处理新连接和已有套接字关闭时,需要调整pollfd
数组的结构。
四、Select 和 Poll 的比较
- 文件描述符数量限制 :
select
通常受限于FD_SETSIZE
(一般为 1024),而poll
理论上没有这个限制,因为它使用数组而不是固定大小的集合。 - 性能 :在文件描述符数量较少时,两者性能差异不大。但随着文件描述符数量增加,
select
的性能会逐渐下降,因为它采用线性扫描的方式检查文件描述符。而poll
采用链表结构,性能相对更稳定。 - 可移植性 :
select
是 POSIX 标准的一部分,具有更好的可移植性;poll
在一些系统上可能需要额外的头文件或库支持。
宝子们,以上就是 Linux 下 Select 和 Poll 编程实现 I/O 多路复用的实战指南啦。赶紧动手实践,熟练掌握这两项技能,让你的 Linux 程序在处理并发 I/O 时更加高效!如果在实践过程中有任何问题,欢迎随时交流。