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

发布于:2024-12-18 ⋅ 阅读:(4) ⋅ 点赞:(0)

1. 引言

在现代计算机网络编程中,处理大量并发连接是一个常见的挑战。为了高效地处理大量的客户端请求,尤其是在I/O密集型应用中,IO多路转接(I/O Multiplexing)技术显得尤为重要。select,poll,epoll是Linux提供的一种高效的多路复用机制,它能够有效处理大量的并发连接,而不会消耗过多的系统资源。

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

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

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

核心思路

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

Select

函数介绍

select函数

int select(int nfds, fd_set *readfds, fd_set *writefds, \
                fd_set *exceptfds, struct timeval *timeout);

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

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

struct timeval {
    long tv_sec;   // 秒
    long tv_usec;  // 微秒
};

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

select函数返回值 

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

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

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的全部位

 简单示例

#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 会被设置为具体错误代码。

 简单示例

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解决的文件描述符上限的问题,循环的问题并没有解决。