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 时更加高效!如果在实践过程中有任何问题,欢迎随时交流。

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言