计算机网络编程(Linux):I/O多路转接之 select,poll

I/O多路复用(I/O Multiplexing)是一种高效的网络编程技术,允许一个线程同时监控多个文件描述符的状态,当某个文件描述符就绪时进行相应处理。这种技术在高并发服务器中广泛使用。本文将介绍I/O多路复用的核心概念及在Linux中的实现方式。

一、I/O多路复用的基本概念

为什么需要I/O多路复用?

在传统的阻塞I/O模型中,一个线程只能处理一个I/O请求,如果我们想要对来自网络的不同的链接同时进行处理,就需要创建线程,或者进程,这导致我们的程序中存在大量线程,或者进程,浪费系统资源。I/O多路复用通过同时监听多个文件描述符的状态(如可读、可写),在任何一个描述符就绪时通知程序进行操作,从而避免了阻塞等待。

核心思路

I/O多路复用的核心思想是利用内核提供的机制(内核通过判断文件是否做出更改,等操作来改变fd_set,进而返回触发事件的文件描述符),集中管理和监听多个文件描述符,并根据事件就绪情况执行特定操作。这样可以大幅减少线程或进程的数量,提高系统资源使用率,

Select

函数介绍

select函数

cpp 复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds, \
                fd_set *exceptfds, struct timeval *timeout);

nfds :指定待检测的文件描述符范围,它的值应该是所有文件描述符中最大值加 1。select 会检查文件描述符从 0nfds-1 的状态。未使用的文件描述符范围无需浪费资源。

**readfds ,writefds,exceptfds:**分别是读,写,异常的文件描述符的位图,我们需要关心哪一个事件,就把对应的文件描述符加入到其位图结构中,传入进函数。

cpp 复制代码
struct timeval {
    long tv_sec;   // 秒
    long tv_usec;  // 微秒
};

timeout: select函数的超时事件,select每一次会返回我们设置超时事件的剩余事件,例如,5秒,运行一秒后会返回4秒。同时,我们可以把参数设置为 NULL 阻塞,直到有事件就绪,select才会返回。

select函数返回值

  • 返回值为正数:表示有文件描述符就绪,返回的数字为就绪的文件描述符数量。
  • 返回值为 0:表示超时,没有任何文件描述符就绪。
  • 返回值为负数:表示调用失败,可通过 errno 获取错误信息

上述所有参数,都需要我们进行一次事件处理后,或者说一次主循环后重新设置,我们需要重新设置关心的文件描述符和事件。

cpp 复制代码
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set);  // 用来清除描述词组set的全部位

简单示例

cpp 复制代码
#include <iostream>
#include <memory>
#include <sys/select.h>
#include <stdio.h>
#include <thread>

#include <unistd.h>  // 为了使用 sleep 函数模拟输入延时

void* example_select(void* args) {
    fd_set read_fds;
    FD_ZERO(&read_fds); // 初始化描述符集
    FD_SET(0, &read_fds); // 添加标准输入(文件描述符 0)
    struct timeval timeout = {5, 0}; // 超时时间 5 秒

    // 使用 select 来监视标准输入
    int ret = select(1, &read_fds, NULL, NULL, &timeout);
    if (ret > 0 && FD_ISSET(0, &read_fds)) {
        std::cout << timeout.tv_sec << std::endl;
        printf("Input is available.\n");
    } else if (ret == 0) {
        printf("Timeout occurred.\n");
    } else {
        perror("select error");
    }

    return NULL;
}

void simulate_input() {
    std::cout << "Simulating input..." << std::endl;
    std::cin.get();  
}

int main() {
    // 创建两个线程
    std::thread monitor_thread(example_select, nullptr);  // 监视输入线程
    std::thread input_thread(simulate_input);  // 模拟输入线程


    monitor_thread.join();
    input_thread.join();

    return 0;
}

程序使用两个线程,一个线程使用select进行监视,另外一个创建事件就绪,但是对于select的fs_set来说,有设计上的限制,他最大可以同时监视的文件描述符数量为1024,我们可以在select.h头文件里看到

Poll

对于select来说,Poll的改进就是可以同时监视的文件描述符的限制不是程序,而是硬件的限制,也就是说,对于程序来说没有上限。

函数介绍

fds: 这个结构是属于用户维护,也就是说**,我们可以使用数据结构对其大小进行动态管理**

fd:要监视的文件描述符(例如,套接字、管道等)。events:指定要监听的事件类型,使用 poll 支持的事件标志。revents:返回时,内核填充的已就绪事件类型。使用该字段可以检查哪些事件已经发生。

常见的 eventsrevents 标志:

  • POLLIN:文件描述符可读取。
  • POLLOUT:文件描述符可写入。
  • POLLERR:文件描述符发生错误。
  • POLLHUP:文件描述符发生挂起(例如,连接关闭)。
  • POLLNVAL:文件描述符无效。

**nfds:**我们设置的结构体数组的大小。

**timeout:**poll里的timeout不是结构体,就只是int,单位是毫秒,大于0,就是超时事件,等于0,就是非阻塞,-1就是阻塞。

poll函数的返回值

  • 正数 :表示已就绪的文件描述符数量。revents 字段会被填充,表示已发生的事件。
  • 0:表示超时,没有文件描述符就绪。
  • -1 :表示错误,errno 会被设置为具体错误代码。

简单示例

cpp 复制代码
void* example_poll(void* args) {
    struct pollfd fds[1];  // 使用 pollfd 结构数组来监视文件描述符
    fds[0].fd = 0;          // 监视标准输入(文件描述符 0)
    fds[0].events = POLLIN; // 监听可读事件
    fds[0].revents = 0;     // 初始化 revents,表示返回的事件状态

    int timeout = 5000;  // 设置超时时间为 5000 毫秒(即 5 秒)

    // 使用 poll 来监视文件描述符
    int ret = poll(fds, 1, timeout);
    if (ret > 0 && (fds[0].revents & POLLIN)) {
        printf("Input is available.\n");
    } else if (ret == 0) {
        printf("Timeout occurred.\n");
    } else {
        perror("poll error");
    }

    return NULL;
}

只需要把头文件加上,上述示例就可以用。

同时,与select一样,每一次循环,需要我们重新设置这些值,那也就是说,poll与select都需要去循环设置,循环查找就绪的文件描述符,然后循环查找空余位置进行文件描述符的插入(没有任何优化),这就导致我们的效率会很低,就算poll解决的文件描述符上限的问题,循环的问题并没有解决。

相关推荐
我们的五年1 分钟前
【Linux课程学习】:第20弹---信号入门专题(基础部分)
linux·服务器·后端·学习·缓存
Camellia-Echo2 分钟前
【Linux从青铜到王者】数据链路层(mac,arp)以及ip分片
linux·运维·服务器
小白—人工智能24 分钟前
Linux —— vim 编辑器
linux·编辑器·vim
qichengzong_right28 分钟前
CNCF云原生生态版图-分类指南(一)- 观测和分析
linux·云原生
怒码ing33 分钟前
【考前预习】2.计算机网络—物理层
网络·计算机网络
linux修理工42 分钟前
centos 7 升级内核到4.19
linux·运维·centos
jekc8681 小时前
Ubuntu安装grafana
linux·ubuntu·grafana
sz66cm1 小时前
Linux基础 -- epoll监听Netlink并实现
linux
枇杷鹭1 小时前
Linux shell 使用 trap 命令优雅处理程序中断: shell 中的回调、锁与事务、以及 debug 调试
linux·运维·服务器
星光璀璨山河无恙2 小时前
【Linux】系统信息和状态命令
大数据·linux