Linux I/O 多路复用 Select/Poll,编程实战方案

在 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。
  • readfdswritefdsexceptfds:分别是监控读、写和异常事件的文件描述符集合。
  • 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 时更加高效!如果在实践过程中有任何问题,欢迎随时交流。

相关推荐
刘一说32 分钟前
CentOS 8开发测试环境:直接安装还是Docker更优?
linux·服务器·docker·centos
AOwhisky34 分钟前
7. if 条件语句的知识与实践
linux·运维·云计算·运维开发·shell·选择结构
陌上花开缓缓归以36 分钟前
linux cma内存分析
linux
2302_799525741 小时前
【ansible】2.实施ansible playbook
linux·运维·ansible
刘一说2 小时前
Win/Linux笔记本合盖不睡眠设置指南
linux·运维·stm32·电脑
AI视觉网奇4 小时前
zsh 使用笔记 命令行智能提示 bash智能
linux·运维·笔记
xiaok5 小时前
使用PM2之后,是不是xshell断开了跟服务器的连接,退出来了,nodejs服务一样在线的
linux
2302_799525745 小时前
【ansible】4.实施任务控制
linux·服务器·ansible
pwj去战斗吧5 小时前
一、部署LNMP
linux·运维