当前位置: 首页 > article >正文

【Linux高级IO】掌握Linux高效编程:深入探索多路转接select机制

📝个人主页🌹:Eternity._
⏩收录专栏⏪:Linux “ 登神长阶 ”
🌹🌹期待您的关注 🌹🌹

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

❀ Linux高级IO

  • 其他高级IO
  • 非阻塞IO:fcntl
    • 实现函数SetNoBlock
    • 轮询方式读取标准输入
  • I/O多路转接之select
    • select函数原型
    • socket的就绪条件
    • select的特点
    • select的缺点
    • select使用示例
  • 总结

前言: Linux作为一个功能强大、灵活多变的操作系统,提供了丰富多样的I/O处理方式。从传统的阻塞I/O到非阻塞I/O,再到更加高效的异步I/O和内存映射I/O,每一种方式都有其独特的适用场景和性能特点。掌握这些高级I/O机制,不仅能够帮助我们更好地理解和优化系统性能,还能在开发高并发、高性能的应用程序时游刃有余。

select机制,则是Linux中处理多路复用I/O的一种经典方法。它允许一个进程同时监视多个文件描述符,以等待其中的任何一个变为可读、可写或有错误条件发生。这种机制极大地提高了I/O处理的灵活性和效率,特别是在处理大量并发连接时,select机制的优势更加明显。

让我们携手踏上这段探索之旅,一同揭开Linux高级I/O与select机制的神秘面纱。

其他高级IO


非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射
IO(mmap),这些统称为高级IO,本篇我们则是重点讨论I/O多路转接

非阻塞IO:fcntl


fcntl 是 Linux 系统编程中一个非常重要的函数,全称为 File Control,即文件控制。它提供了对文件描述符的广泛控制,包括复制文件描述符、获取/设置文件描述符标志、获取/设置文件锁以及获取/设置文件描述符的所有者等。fcntl 函数的灵活性使其成为处理文件 I/O 操作时不可或缺的工具

一个文件描述符, 默认都是阻塞IO,函数原型如下:

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

后面追加的参数根据cmd的值的不同而产生不同

fcntl函数有5种功能:

  • 复制一个现有的描述符(cmd=F_DUPFD)
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)

我们现在只需要使用第三个功能,就能满足当前需要,将一个文件描述符设置为非阻塞

实现函数SetNoBlock


基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞

void SetNoBlock(int fd) 
{
	int fl = fcntl(fd, F_GETFL);
	if (fl < 0) 
	{
		perror("fcntl");
		return;
	}
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
  • 使用F_GETFL将当前的文件描述符的属性取出来
  • 然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数,O_NONBLOCK就是设置非阻塞

轮询方式读取标准输入


#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <cstdlib>
#include <fcntl.h>

void SetNoBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0)
    {
        std::cerr << "fcntl error" << std::endl;
        exit(0);
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main()
{
    SetNoBlock(0);
    while (true)
    {
        char buffer[1024];
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "echo# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            std::cout << "end stdin" << std::endl;
            break;
        }
        else
        {
            // 非阻塞等待,如果数据没有准备好就会按照错误返回,s == -1
            // 那我们怎么知道出错的原因是数据没有准备好,还是真的出错了呢?s是怎么区分的?
            // read, recv会以出错的形式告知上层,数据还没有准备好
            if(errno == EWOULDBLOCK)
            {
                std::cout << "OS的底层数据还没有准备好, error: " << errno << std::endl;
                // other
            }
            else if(errno == EINTR)
            {
                std::cout << "IO interrupted by signal, try again" << std::endl;
            }
            else
            {
                std::cout << "read error!" << std::endl;
                break;
            }
        }
        sleep(1);
    }
    return 0;
}

我们不断的去查看数据是否准备好,只要准备好我们就拿走,没有准备好,我们就去做其他事情

I/O多路转接之select


初识select:

系统提供select函数来实现多路复用输入/输出模型:

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

select函数原型


#include <sys/select.h>

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

参数解释:

  • 参数nfds是需要监视的最大的文件描述符值+1
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合
  • 参数timeout为结构timeval,用来设置select()的等待时间

参数timeout取值:

  • NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回

关于fd_set结构:

这个结构就是一个整数数组, 更严格的说, 是一个 "位图",使用位图中对应的位来表示要监视的文件描述符,用比特位的内容来告诉内核是否关心这个位置的发生事件,其中给出了一组接口来方便操作位图

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

关于timeval结构:

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0

函数返回值:

  • 执行成功则返回文件描述词状态已改变的个数
  • 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
  • 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测

错误值可能为:

  • EBADF 文件描述词为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数n 为负值。
  • ENOMEM 核心内存不足

socket的就绪条件


读就绪:

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;

写就绪:

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
    SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号
  • socket使用非阻塞connect连接成功或失败之后
  • socket上有未读取的错误

select的特点


  • 可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd
    • 一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
    • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

select的缺点


  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小

select使用示例


讲了这么多,就让我们用用select正是操作一把,单进程实现多服务器消息交流,体现多路转接的真正实力

SelectServer:


#pragma once

#include <iostream>
#include <string>
#include <sys/select.h>

#include "Log.hpp"
#include "Socket.hpp"

using namespace Net_Work;

const static int gdefaultport = 8888;
const static int gbacklog = 8;
const static int num = sizeof(fd_set) * 8;

class SelectServer
{
private:
    void HandlerEvent(fd_set &rfds)
    {
        for (int i = 0; i < num; i++)
        {
            if (_rfds_array[i] == nullptr)
                continue;

            // 合法的fd
            // 读事件分两种,一类是新链接的到来,一类是新数据的到来
            int fd = _rfds_array[i]->GetSocket();
            if (FD_ISSET(fd, &rfds))
            {
                // 读事件就绪 -> 新链接的到来
                if (fd == _listensock->GetSocket())
                {
                    lg.LogMessage(Info, "get a new link\n");
                    std::string clientip;
                    uint16_t clientport;
                    // 这里不会阻塞,因为select已经检测到listensock就绪了
                    Socket *sock = _listensock->AcceptConnection(&clientip, &clientport);
                    if (!sock)
                    {
                        lg.LogMessage(Error, "accept error\n");
                        return;
                    }
                    lg.LogMessage(Info, "get a client, client info is# %s:%d, fd:%d\n", clientip.c_str(), clientport, sock->GetSocket());
                    // 获取成功了,但是我们不能直接读写,底层的数据不确定是否就绪
                    // 新链接fd到来时,要把新链接fd交给select托管 --- 只需要添加到数组_rfds_array中即可
                    int pos = 0;
                    for (; pos < num; pos++)
                    {
                        if (_rfds_array[pos] == nullptr)
                        {
                            _rfds_array[pos] = sock;
                            break;
                        }
                    }
                    if (pos == num)
                    {
                        sock->CloseSocket();
                        delete sock;
                        lg.LogMessage(Warning, "server is full ... !\n");
                    }
                }
                // 新数据的到来
                else
                {
                    std::string buffer;
                    bool res = _rfds_array[i]->Recv(&buffer, 1024);
                    if(res)
                    {
                        lg.LogMessage(Info, "client say# %s\n", buffer.c_str());
                        buffer.clear();
                    }
                    else
                    {
                        lg.LogMessage(Warning, "client quit, maybe close or error, close fd: %d\n", _rfds_array[i]->GetSocket());
                        _rfds_array[i]->CloseSocket();
                        delete _rfds_array[i];
                        _rfds_array[i] = nullptr;
                    }
                }
            }
        }
    }

public:
    SelectServer(int port = gdefaultport)
        : _port(port), _listensock(new TcpSocket())
    {
    }

    void InitServer()
    {
        _listensock->BuildListenSocketMethod(_port, gbacklog);
        for (int i = 0; i < num; i++)
        {
            _rfds_array[i] = nullptr;
        }
        _rfds_array[0] = _listensock.get();
    }

    void Loop()
    {
        _isrunning = true;

        while (_isrunning)
        {
            // 不能直接accept新连接,而是要将selete交给selete, 只有selete有资格知道IO事件有没有就绪
            fd_set rfds;
            FD_ZERO(&rfds);
            int max_fd = _listensock->GetSocket();
            for (int i = 0; i < num; i++)
            {
                if (_rfds_array[i] == nullptr)
                {
                    continue;
                }
                else
                {
                    int fd = _listensock->GetSocket();
                    FD_SET(fd, &rfds); // 添加所有合法的fd到rfds集合中
                    if (max_fd < fd)   // 更新最大fd
                    {
                        max_fd = fd;
                    }
                }
            }

            // 遍历数组,1.找最大值 2.合法的fd添加到rfds集合中

            // 定义时间
            struct timeval timeout = {0, 0};
            // rfds本质是一个输入输出型参数,rfds是在select调用返回的时候,不断被修改,所以每次都要重置
            PrintDebug();
            int n = select(max_fd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
            switch (n)
            {
            case 0:
                lg.LogMessage(Info, "select timeout ... last time: %u.%u\n", timeout.tv_sec, timeout.tv_usec);
                break;
            case -1:
                lg.LogMessage(Error, "select error !!! \n");
            default:
                lg.LogMessage(Info, "select success, begin event handler, last time: %u.%u\n", timeout.tv_sec, timeout.tv_usec);
                HandlerEvent(rfds);
                break;
            }
        }

        _isrunning = false;
    }

    void stop()
    {
        _isrunning = false;
    }

    void PrintDebug()
    {
        std::cout << "current select rfds list is: ";
        for (int i = 0; i < num; i++)
        {
            if (_rfds_array[i] == nullptr)
                continue;
            else
                std::cout << _rfds_array[i]->GetSocket() << " ";
        }
        std::cout << std::endl;
    }

    ~SelectServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    int _port;
    bool _isrunning;
    Socket *_rfds_array[num];
};

git完整代码链接

总结


虽然说select实现了我们之前从未做到过的功能,select 只负责等待,可以等待多个fd,IO的时候,效率比较高一些,但是对于它的缺点来说,它还是不适合我们使用的

缺点:

  • 1.我们每次都要对select的参数进行重置
  • 2.编写代码的时候,select因为要使用第三方数组,所以充满了遍历。可能会影响select 的效率
  • 3.用户到内核,内核到用户,每次select调用和返回,都要对位图进行重新设置。用户和内核之间,要一直进行效据拷贝
  • 4.select 让OS在底层遍历要关心的所有的fd,这个会造成效率低下
  • 5.fd set:是一个系统提供的类型,fd set大小是固定的,也就是位图个数是固定的,也就是 select最多能够检测的付d总数是有上限的
int n = select(max_fd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);

// max_fd + 1 表示的是

正因为这些缺点,select被我们放弃,但我们也不会损失什么,因为后面还有更厉害的工具等待着我们

随着我们一同走过这段关于Linux高级I/O与select机制的学习之旅,我们不难发现,这些技术不仅是系统编程中的关键要素,更是提升应用程序性能和稳定性的有力武器。从非阻塞I/O到异步I/O,从内存映射到文件锁定,再到select机制的多路复用处理,每一项技术都为我们打开了新的视角,让我们能够更加深入地理解和优化系统行为。

最后,让我们携手开启系统编程的新篇章,继续深入探索Linux的奥秘,共同推动技术的进步和发展。在未来的日子里,愿我们都能在技术的海洋中畅游,收获满满的知识与智慧。再见!

在这里插入图片描述

希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!

在这里插入图片描述


http://www.kler.cn/a/562907.html

相关文章:

  • 3-提前结束训练
  • mac升级系统后聚焦Spotlight Search功能无法使用,进入安全模式可解
  • 【AIGC系列】3:Stable Diffusion模型介绍
  • 智慧物流小程序(论文源码调试讲解)
  • 最新版 (持续更新)docker 加速源 linux yum 源
  • 【Java企业生态系统的演进】从单体J2EE到云原生微服务
  • 现代前端框架渲染机制深度解析:虚拟DOM到编译时优化
  • 【MySql】EXPLAIN执行计划全解析:15个字段深度解读与调优指南
  • 渗透第二次作业
  • Spring Boot启动过程?
  • MySQL的存储引擎
  • 鸿蒙NEXT开发-视频播放绘图能力
  • 每天练打字15:时隔多日后回归,目前赛文速度110.97
  • 【漫话机器学习系列】105.学习速率(Learning Rate)
  • ‌Selenium三大等待方式详解
  • 鸿蒙5.0实战案例:基于RichEditor的评论编辑
  • 特斯拉 FSD 算法深度剖析:软件层面全解读
  • 登录逻辑结合redis
  • 【Linux】vim 设置
  • Selenium 与 Coze 集成