【Hello Linux】高级IO Select

news2024/11/18 1:39:08

本篇博客介绍:介绍Linux中的高级IO

高级IO

    • IO
    • 五种IO模型
    • 非阻塞IO
      • 如何判断异常读取
    • IO多路转接之Select
    • select的优缺点

网络通信的本质其实就是一种IO

而我们知道的是 IO的效率实际上是十分低下的

所以说我们需要一些机制或者方法来解决这个效率问题

IO

IO实际上就是 input && output 在冯诺依曼体系中就是与外设交互的意思

IO 为什么低效

我们以读取为例介绍IO

  • 情况一: 当我们底层调用read函数的时候 如果缓冲区没有数据 此时会怎么样?
  • 情况二: 当我们底层调用read函数的时候 如果缓冲区有数据 此时会怎么样?

很明显 在情况一中我们的程序会阻塞住 在情况二中我们的程序会拷贝数据

那么阻塞的本质是什么呢?

从原理上讲 阻塞就是将PCB放到等待队列中 从本质上讲阻塞的本质就是

所以说IO实际上就是 等 + 数据拷贝

实际上不光是在网络中 在本地主机进行IO的时候也是进行这两个阶段

在这里插入图片描述
当我们的程序需要读取磁盘中的内容时 磁盘需要先将内容加载到内存里面

而在加载还未完成时 我们的程序在做什么呢? 阻塞 或者说

这也就是为什么我们使用scanfcin等输入函数的时候 命令行会阻塞住

因为此时它们在IO的 阶段

所以说现在我们可以这么定义低效的IO

在进行IO的时间里 大部分时间都在等待

如何提高IO效率

在进行IO的时间里 让等待时间所占的比重变小


那么此时 我们的问题就变成了 如何让IO的比重降低

实际上在经历了这么长时间的发展之后 计算机的前辈们已经总结出来了五种IO模型

我们现在开始学习这五种IO模型

五种IO模型

大家肯定都看过或者自己钓过鱼 我们这里把钓鱼的模型简化一下分成两步 等+钓

那么此时大家可以思考一下 在什么情况下 我们会认为一个人的钓鱼效率特别高呢? 肯定是这个人钓的动作特别多 而等的动作特别少的时候 我们认为这个人的钓鱼效率很高

下面我们使用五个关于钓鱼的小故事来介绍这五种IO模型

在这里插入图片描述

鱼漂是一种垂钓时鱼儿咬钩的讯息反应的工具

我们可以通过观察鱼漂来得知鱼儿有没有上钩

故事一

张三去钓鱼的时候不喜欢被打扰 甩钩之后就一直盯着鱼漂 等什么时候余漂有反应了就立刻拉钩

故事二

李四去钓鱼的时候专心不了 甩钩之后就喜欢刷刷手机 每刷一会儿手机就看一眼鱼漂 如果有反应了就拉钩 如果没反应就继续刷手机

故事三

王五去钓鱼的时候喜欢在鱼漂上挂个铃铛 之后就去刷手机玩了 如果铃铛响了 那么王五就去拉钩 如果没响 就一直玩手机

故事四

赵六去钓鱼的时候喜欢多备几根鱼竿 所有鱼竿下水之后赵六就在旁边巡视 哪一根鱼竿的鱼漂动了就去拉哪根鱼竿

故事五

田七去钓鱼的时候带着一个小跟班 每次只需要布置任务让小跟班钓多少鱼就好 自己处理自己的事情去了

上面的五个小故事分别代表了五个IO模型 分别是

  • 故事一: 阻塞
  • 故事二: 非阻塞轮询
  • 故事三: 信号驱动
  • 故事四: 多路复用多路转接
  • 故事五: 异步IO

那么这里就有个问题了 谁钓鱼是最高效的呢?

很显然 故事四中的赵六效率最高 因为其他人都只有一个鱼竿 而赵六有很多个 所以说赵六钓到鱼的概率比其他人高很多了

同样的 因为赵六有很多个鱼竿 所以说它肯定一直在拉钩 所以说在相同时间内 它等待的时间也是最短的

什么是同步IO 异步IO

如果一个人(或者进程)参与到IO的过程 那么我们就把这个IO称为同步IO

反之就是异步IO

阻塞和非阻塞的区别主要在哪里

我们都知道 IO = 等 + 拷贝

阻塞和非阻塞拷贝的过程是相同的 但是它们等的过程是不同的


学到这里的时候同学们可能会发现一点问题

我们这里讲的同步和多线程部分讲的同步好像不太一样?

多线程部分讲的同步是按照一定的顺序访问临界资源

而我们这里讲的同步则是 线程或者进程要参与IO的过程

这是因为在计算机中 关于同步有很多不同的概念 具体是什么意思我们带入到具体的场景中才会明白

非阻塞IO

我们如果想让IO进行非阻塞的话 打开文件的时候就可以进行非阻塞设置

比如说 open socket

但是如果我们使用每个函数的时候都记住它们的非阻塞标志未免也有点太麻烦了

所以说我们这里使用 fcntl 函数来统一设置

该函数原型如下

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

返回值:

  • 如果设置失败会返回-1 并且错误码会被设置 成功返回大于等于0

参数:

  • 参数一是文件描述符
  • 参数二是一个标志位 它有以下几种功能
  1. 复制一个现有的描述符(cmd=F_DUPFD).
  2. 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  3. 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
  4. 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
  5. 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)

我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞

  • 参数三是一个可变参数模板 我们可以传入各种参数

其实我们在刚刚学习C语言的时候 就学到了如何进行一个阻塞读取了

那就是从0号文件描述符中读取数据 (0代表标准输入)

代码标识如下

#include <iostream>                                                                                                             
using namespace std;    
#include <unistd.h>    
    
int main()    
{    
  char buff[1024];    
    
  while (true)    
  {    
    ssize_t s = read(0 , buff , sizeof(buff)-1);    
    
    if (s > 0)    
    {    
      buff[s] = 0;    
      cout << "echo# " << buff << endl;    
    }    
    else    
    {    
      cout << "read \"error\"" << endl;    
    }    
  }    
  return 0;    
}  

当我们编译运行该程序的时候 该程序会直接阻塞住 这是因为我们标准输入缓冲区中没有数据
在这里插入图片描述
只有当我们输入数据之后 程序才会继续运行

在这里插入图片描述

其实我们的cout也是阻塞式的 只不过因为没有到达等的条件 或者说等的时间很少 所以我们认为它的非阻塞的

此时我们将 标准输入 设置为非阻塞式看看效果

bool SetNonBlock(int fd)      
{      
  int f1 = fcntl(fd , F_GETFL);      
  if (f1 < 0)      
  {      
    return false;      
  }      
  fcntl(fd , F_SETFL , f1 | O_NONBLOCK);      
  return true;                                                                                           
} 

这段代码的意思是 我们首先得到一个fd的各个参数 然后再设置该fd为非阻塞

此时我们再次编译运行就会出现下面的情况

在这里插入图片描述
程序会一直输出read “error”

需要注意的是 虽然我们看上去系统一直在打印read error 但是实际上它是一直在调用read函数 只不过read函数里面没有数据 所以说read的字符为0 所以才会打印 read error

如何判断异常读取

但是这里又会出现一个问题了

  • 在正常读取的时候 我们通过打印的字符串可以知道是正常读取了
  • 可是在异常读取的时候 我们怎么分辨是读取失败还是缓冲区中没有数据呢

在目前的情况下 我们是无法分辨的

因为不管是读取失败 还是缓冲区中没有数据 显示器上都会打印 read error

所以说 我们这个时候就需要 错误码来告诉我们 究竟是为什么没有读取成功

我们首先要包含下面两个头文件

#include <cstring>    
#include <cerrno>    

紧接着 在每次读取失败的打印后面加上错误码和错误码描述

  cout << "read \"error\" " << "error: " << errno << "strerrno: " << strerror(errno) << endl; 

之后编译运行

在这里插入图片描述

之后我们就可以发现 异常读取是因为缓冲区为空了

EWOULDBLOCK 在Linux中是一个宏定义 是11号错误码 代表的就是缓冲区为空 再试一次

EINTR 在Linux中是7号错误码 代表的是当前IO过程被信号中断了 如果出现该错误码 我们让进程继续读取信号即可

非阻塞读取的意义

当我们使用了非阻塞IO的时候 每次读取如果遇到了 EWOULDBLOCK EINTR 我们就可以让我们的进程去做一会儿其他事情

当然如果遇到了其他错误码我们就要考虑进行错误排查了 是不是自己的代码哪里写的有问题

IO多路转接之Select

我们前面说过了 IO的本质就是 等+数据拷贝

而我们select的主要做的工作就是在 这一步

它的工作过程如下

  1. 帮用户一次等待多个sock
  2. 如果有sock就绪了 select就要通知用户 这些sock就绪了 让用户调用read/recv函数来进行读取

我们下面会通过7步来帮大家理解下select 步骤如下

  • 认识下select函数的各个接口
  • 理解fd_set
  • 理解一个select函数的重要参数
  • 推而广之 理解所有参数
  • 快速上手写代码
  • 理解select代码模式
  • 完成代码

第一步 认识select函数各个接口

select函数的头文件如下

#include <sys/select.h>  

函数原型如下

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

参数:


参数一:int nfds

它代表的含义是让我们等待的文件描述符的 最大值+1

这个很好理解 因为我们的文件描述符的本质就是数组的下标 而下标是连续增长的 所以说我们只需要啊给一个要等待的文件描述符最大值就能够得到一个要等待的文件描述符的范围

有同学可能会有疑问 这样子的话我们岂不是要等待从 0 ~ nfds-1所有文件描述符了呢 关于这个问题看了下面的参数介绍就能明白了


参数二 三 四:

参数二 三 四 五全部是输入输出形参数

对于参数二 三 四来说我们要传入的是一个fd_set类型的指针

这些参数在输入的时候分别表示

  • 我们是否关心读就绪
  • 我们是否关心写就绪
  • 我们是否关心有异常

在输出的时候分别表示

  • 哪些读就绪了
  • 哪些写就绪了
  • 出现哪些异常了

参数五:timeval *timeout

timeval实际上是一个结构体 在Linux系统中 它的定义如下

在这里插入图片描述

它的第一个成员可以获取当前时间的秒 它的第二个成员可以获取当前时间的微秒

我们让select进行等待的时候 有三种模式可以供我们选择

  • 阻塞式
  • 非阻塞式
  • 阻塞一段时间 之后返回

对于这个参数来说

  • 如果我输入nullptr 那么它就是阻塞式的
  • 如果我们输入结构体 {0 , 0} 那么它就是非阻塞式的
  • 如果我们输入结构体{5, 0}那么它就会等待五秒钟 之后返回 但是如果说五秒内有文件描述符就绪了的话 这个参数就会显示出输出性 比如说我们要求等五秒 而实际上2秒就有文件描述符就绪了 那么它就会返回{3 , 0}

返回值:int

它表示的是就绪的文件描述符的个数

只要让我们等待的文件描述符中 有一个就绪了 它就会返回


第二步 理解fd_set

fd_set 叫做文件描述符集 它本质上是一个位图 如果有关位图不理解的同学可以参考我的这篇博客

位图

在这里插入图片描述

系统提供了四个函数来让我们进行文件描述符集操作 它们的作用如下

  • 清除某个文件描述符
  • 判断某个文件描述符是否被设置
  • 设置文件描述符
  • 清空文件描述符

第三步 挑选一个参数细致的分析下

我们以读文件描述符集为例

fd_set *readfds

当它作为一个输入参数时

  • 它是用户通知内核的一种方式
  • 在比特位中 比特位的下标表示文件描述符
  • 比特位下标对应的内容是否为1表示我对于该文件的读是否关心
  • 比如 0101 就是我对于2号和0号文件描述符的读关心

当它作为一个输出参数时

  • 它是内核通知用户的一种方式
  • 在比特位中 比特位的下标表示文件描述符
  • 比特位下标对应的内容是否为1表示该文件描述符的读是否就绪
  • 比如说 0100 就是用户让系统关心的0号和2号文件描述符中 2号文件描述符就绪了

我们可以总结下

用户和内核都会使用同一个位图结构

所以说 我们只要使用过一次这个参数 它就需要被重新设置一次

第四步 推而广之

推而广之

对于 fd_set *writefds fd_set *exceptfds 来说 它们的作用也和 fd_set *readfds 类似

只不过关心和通知的内容分别变成了 是否关心写和是否关心异常

第五步 快速上手写代码

我们首先先包含之前写的 Sock.hpp err.hpp log.hpp 这三个文件内容可以参考这个gitee连接

参考代码

接下来我们上手开始写select服务器

关于select服务器 我们只完成读取 写入和异常不做处理

雏形如下

class SelectServer
{
  private:
    uint16_t _port;
    int _listensock;
  public:
    SelectServer(const uint16_t& port = 8080)
      :_port(port)
    {
      _listensock = Sock::Socket();
      Sock::Bind(_listensock , _port); 
      Sock::Listen(_listensock);
      logMessage(DEBUG , "create base socket success");
    }
                                                                                                         
    ~SelectServer()
    {              
      if (_listensock > 0)
      {                   
        close(_listensock);
      }  
};

现在要让这个服务器运行起来我们只需要写一个start函数

让它不停的接收新的套接字

    void Start()          
    {                     
      while (true)         
      {                    
        int sock = Sock::Accept(_listensock , ...);          
      }
    }

而我们接收套接字接收的过程实际上就是一个IO 因为它就是 + 数据拷贝(拷贝套接字信息)

而当没有数据到来的时候我们依旧调用 accept 实际上这个程序就被阻塞住了

那我们这里使用select改写下这段代码

      fd_set rfds;
      FD_ZERO(&rfds);
      while (true)
      {
        // int sock = Sock::Accept(_listensock , ...);                                                   
        FD_SET(_listensock , &rfds);
        int n = select(_listensock + 1 , &rfds , nullptr , nullptr , nullptr);
        switch (n)
        {
          case 0:
            logMessage(DEBUG , "time out..");
            break;
          case -1:
            logMessage(WARNING , "select error: %d : %s" , errno , strerror(errno));
          default:
            break;
        }
      }

我们使用 select方法 对于上面的函数进行重写 当然我们也可以加上一个 timeout

struct timeval timeout = {5 , 0}; 

此时我们的select函数便会五秒进行一次检测了

在这里插入图片描述

为什么在经过一次五秒的检测之后 等待的时间就变成0了呢?

因为timeout是一个输入输出参数 在五秒的等待之后变成 {0 , 0}了

如果我们想要一直经历五秒的检测的话 只需要在每次进入循环的时候设置下就好


接下来为了不让多余的信息干扰我们 我们将等待模式变成阻塞式等待

 int n = select(_listensock + 1 , &rfds , nullptr , nullptr , nullptr);

如果说我们得到新连接的话 我们就打印一条debug信息

          default:    
            logMessage(DEBUG , "get a new link...");    
            break;   

演示结果如下
在这里插入图片描述

为什么select会一直通知呢?

原因 同 timeout rfds是一个输入输出参数

所以说 我们需要一个函数去处理接收的文件描述符 并且重新设置


处理函数如下

    void HandlerEvent(const fd_set& rfds)    
    {                                       
      string clientip;    
      uint16_t clientport = 0;                                             
      if (FD_ISSET(_listensock , &rfds))    
      {      
        int sock = Sock::Accept(_listensock , &clientip , &clientport);    
        if (sock < 0)    
        {    
          logMessage(WARNING , "Accept error");              return;                                                                                        
        }    
        logMessage(DEBUG , "get a new line success :[%s : %d] , %d" , clientip.c_str() , clientport , sock);        
      }    
    } 

此时就可以正常的accept了

在这里插入图片描述


那么当我们正常接收到一个文件描述符后 我们现在要立刻读取嘛?

答案肯定是不行的 因为现在我们并没有使用任何的多线程 多进程 这是一个单执行流程序

如果我们现在读取那么这个进程就会在这里阻塞住了

当然我们也不能够创建多线程或者是多进程来处理这些数据 否则这些内容就和我们之前学的东西没区别了

我们这里的思路是让select函数来帮助我们进行处理何时读取的问题

可是这里的问题就来了 我们应该怎么让select函数来帮我们进行处理呢? 这两个地方都跨函数了啊

那么要解决这个问题 我们就要进行到第六步 理解select代码模式

第六步 理解select代码模式

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

我们再次回顾下select函数

  1. 随着我们获取的sock越来越多 我们添加到select的sock也越来越多 这就注定了nfds每次都要变化 我们就需要对它进行一个动态的计算
  2. rfds / wfds / efds 是输入输出形参数 所以注定了我们每一次都要对于它们进行重新添加
  3. timeout是输入输出形参数 如果我们需要每次等待的时间一样的话 我们也需要每次进行重新设置

其中1 和 2 注定了 我们必须要将合法的文件描述符保存起来 用来支持

  1. 更新最大文件描述符
  2. 更新位图结构

所以说我们的select函数一般来说需要一个第三方数组 用来保存所有的合法fd

所以说我们的伪代码如下

while(true)    
  {    
    1. 遍历数组 更新出最大值    
    2. 遍历数组 添加所有需要关心的fd 到位图中    
    3. 调用select进行时间检测                                
  } 

select能够检测的文件描述符有最大值嘛?

有的 我们只需要计算出fd_set的大小然后*8就好

所以说我们设置一个第三方数组的时候大小要刚好覆盖到

数组用什么

我们这里不选择使用vector容器 而使用一个原生的int类型的数组来做

这主要是因为vector容器做的太容易了 不能很好的暴露出select服务器的一些缺点来


第七步 : 完成select服务器

我们将数组的大小设置为select所能检测的文件描述符最大值 并且全部初始化为-1

      int _fdarr[ARRNUM];
      for (int i = 0; i < ARRNUM ; i++)    
        {                                                    
          _fdarr[i] = FD_NONE;    
        }    

同时 我们约定数组的0号下标为listensock套接字

与此同时 当我们每次进入while循环的时候 我们将合法的文件描述符全部添加到fd_set中 并且 我们在循环的时候就可以比较文件描述符的大小 找到最大的文件描述符 maxfd

          FD_ZERO(&rfds);    
          // int sock = Sock::Accept(_listensock , ...);    
          int maxfd = _listensock;    
          for (int i = 0; i < ARRNUM; i++)    
          {    
            if(_fdarr[i] == FD_NONE)                                                                     
            {    
              ;    
            }    
            else     
            {    
              FD_SET(_fdarr[i] , &rfds);
              if (maxfd < _fdarr[i])
              {
                maxfd = _fdarr[i];
              }
            }
          }

但是光是这样还没完 我们还需要设置好我们accept的sock套接字 这样子 在每次进入while循环的时候 我们的sock套接字就会自动被设置到rfds中

          int pos = 1;       
          for(pos = 1; pos < static_cast<int>(ARRNUM) ; pos++)    
          {                  
            if (_fdarr[pos] == FD_NONE)    
            {                
              break;         
            }                
          }                  
                             
          if (pos == static_cast<int>(ARRNUM))    
          {
             logmessage...; 
          }    
          else               
          {                  
            _fdarr[pos] = sock;    
          }    
        } 

测试

我们可以写一个 debugprint 来打印 fdarry 中所有的文件描述符来看看结果是否和我们的预期相符合

      void DebugPrint()    
      {    
        cout << "fd_array: ";                                                                            
        for (int i = 0; i < static_cast<int>(ARRNUM); i++)    
        {    
          if (_fdarr[i] == FD_NONE)    
          {    
            ;    
          }    
          else     
          {    
            cout << _fdarr[i] << " ";    
          }    
        }    
        cout << endl;    
      }  

我们发现结果符合预期

在这里插入图片描述


但是我们这里又会遇到一个新的问题

当走到我们的select函数的时候 我们rfds里面有两种套接字 一个是listen套接字 一个是普通的套接字 并且我们select中 就绪的fd会越来越多

但是 我们可以回顾下我们上面的handler方法 是不是里面只处理了接收新连接的请求啊? 这样子显然是不可以的

所以说我们要重写下 handler 方法 让该方法可以处理所有就绪的请求

代码框架如下

W>    void HandlerEvent(const fd_set& rfds)
      {
        for (int i = 0; i < static_cast<int>(ARRNUM); i++)    
        {    
          if (_fdarr[i] == FD_NONE)    
          {    
            ;                                  
          }    
          else                                                
          {    
            if (_fdarr[i] == _listensock)    
            {    
              // accept new link;    
            }
            else 
            {
              // read                                                                                    
            }
          }
        }
      } 

我们之前的handler方法由于只判断了listen套接字 只完成了accpet的任务

所以说我们将它封装成acceptr方法

而对于非listensock套接字 我们则直接进行读取即可 我们这里使用logmessage进行测试

在这里插入图片描述

我们发现 结果符合预期

我们现在暂时不写读取的代码 因为这里会涉及到tcp粘包问题 需要我们定制一个协议 而目前我们主要学习的是select服务器的设计模式 这些东西可以先放一放 等到epoll服务器再说

select服务器整体代码如下

select服务器代码

select的优缺点

优点

  1. 效率高 IO等的时间少 尤其是在有大量连接 并且只有少量活跃的情况下
  2. 单进程 占用资源少

缺点

  1. 为了维护第三方数组 select服务器充满大量的遍历操作
  2. 每一次都要对select参数进行重新设定
  3. 能够同时管理的fd的个数是有上限的
  4. 由于参数是输入输出的 所以避免不了大量用户和内核之间的拷贝
  5. 编码比较复杂

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1036208.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

R and RStudio的安装教程【2023】

首先需要安装R&#xff0c;才能安装RStudio。 安装包文末获取或者去官网获取&#xff0c;一样的&#xff1a; R的安装&#xff1a;&#xff08;很简单&#xff0c;如果你想安装到C盘&#xff0c;全部选项无脑选下一步&#xff0c;不想安装到C盘&#xff0c;就改一下就ok.&…

软件测试同行评审到底是什么?

【软件测试面试突击班】如何逼自己一周刷完软件测试八股文教程&#xff0c;刷完面试就稳了&#xff0c;你也可以当高薪软件测试工程师&#xff08;自动化测试&#xff09; “同行评审是一种通过作者的同行(开发、测试、QA等)来确认缺陷和需要变更区域的检查方法。”在软件测试中…

五、C#—字符串

&#x1f33b;&#x1f33b; 目录 一、字符串1.1 字符类型1.2 转义字符1.3 字符串的声明及赋值1.3.1 c# 中的字符串1.3.2 声明字符串1.3.3 使用字符串1.3.4 字符串的初始化1.3.4.1 引用字符串常量之初始化1.3.4.2 利用字符数组初始化1.3.4.3 提取数组中的一部分进行初始化 1.3.…

企业微信自建小程序应用踩坑实践

最近开发了一个小程序接入企业微信的需求&#xff0c;企业微信的权限限制诸多&#xff0c;网上的完整示例又少之又少&#xff0c;因此踩了比较多坑&#xff0c;与大家分享。 开发调试 在开发者工具中如果直接使用微信小程序模式&#xff0c;调用wx.qy接口会提示不存在&#x…

Qt5开发及实例V2.0-第二十一章-Qt.Quick Controls开发基础

Qt5开发及实例V2.0-第二十一章-Qt.Quick Controls开发基础 第21章 Qt Quick Controls开发基础21.1 Qt Quick Controls概述21.1.1 第一个Qt Quick Controls程序21.1.2 Qt Quick窗体应用程序的构成 21.2 Qt Quick控件21.2.1 概述21.2.2 基本控件21.2.3 高级控件21.2.4 样式定制 2…

光电开关-NPN-PNP

基础概念 有信号 “检测到物体/有物体遮挡” 工作原理 NPN&#xff1a;表示共正电压&#xff0c;输出负电压【只能输出低电压或者悬空 常开常闭是指 输出有没有跟“地”接通】&#xff1b; NPN NO&#xff1a;表示常态下是常开的&#xff0c;检测到物体时黑色线输出一个负电压…

docker jira 一键安装含PJ(docker 一键安装jira)

docker jira 一键安装含PJ&#xff08;docker 一键安装jira&#xff09; 本文仅供参考学习&#xff0c;请勿用于商业用途本文用于Jira在Docker的安装&#xff0c;仅用于记录安装方式转载请注明来源Linux安装可参考链接Windows安装可查考链接Docker一键安装Confluence PJ条件允…

配置HBase和zookeeper

一、上传文件 二、解压 tar -zxf ./zookeeper-3.4.5-cdh5.14.2.tar.gz -C /opt/soft/ tar -zxf ./hbase-2.3.5-bin.tar.gz -C ../soft/ 三、改名字 mv ./zookeeper-3.4.5-cdh5.14.2/ zk345 mv ./hbase-2.3.5/ hbase235 四、配置映射 vim /etc/profile#ZK export ZOOKEEPE…

pytorch学习------实现手写数字识别

目录 目标一、思路和流程分析二、准备训练集和测试集2.1、图形数据处理方法2.1.1、torchvision.transforms.ToTensor2.1.2、torchvision.transforms.Normalize(mean, std)2.1.3、torchvision.transforms.Compose(transforms) 2.2、准备MNIST数据集的Dataset和DataLoader三、构建…

NodeRed拖拉拽实现OPCUA数据订阅,发布至MQTT并落库MySQL

背景 几年前曾根据 Node-Red 官网示例进行了简单的体验&#xff0c;当时觉得这东西就是个玩具&#xff0c;拿过来玩一玩可以&#xff0c;不实用&#xff1b;但是如今发现有不少产品对其进行了集成&#xff0c;并做出了复杂的商业应用&#xff0c;这确实是极大的震撼。 使用看似…

Vulnhub系列靶机---JANGOW 1.0.1

文章目录 网卡配置信息收集主机发现端口扫描 漏洞利用反弹Shell提权 靶机文档&#xff1a;JANGOW 1.0.1 下载地址&#xff1a;Download (Mirror) 难易程度&#xff1a;. 网卡配置 水果味儿 信息收集 主机发现 端口扫描 访问80端口 点击site目录 点击页面上方的一个选项&…

【跟小嘉学习区块链】一、区块链基础知识与关键技术解析

系列文章目录 【跟小嘉学习区块链】一、区块链基础知识与关键技术解析 文章目录 系列文章目录[TOC](文章目录) 前言一、区块链基础1.1、区块链的来龙去脉1.1.1、区块链技术起源1.1.2、核心技术发展脉络 1.2、比特币产生的背景与现状1.2.1、现行货币体系存在的问题1.2.2、数字货…

HTTP代理与VPN:网络代理技术的比较

HTTP代理和VPN是两种常见的网络代理技术&#xff0c;它们可以帮助用户隐藏自己的IP地址、保护网络隐私、绕过网络限制等。本文将介绍HTTP代理和VPN的定义、工作原理、优缺点以及使用场景。 一、HTTP代理 HTTP代理是一种通过代理服务器转发网络请求的技术。当用户发起网络请求时…

c语言进阶部分详解(经典回调函数qsort()详解及模拟实现)

大家好&#xff01;上篇文章&#xff08;c语言进阶部分详解&#xff08;指针进阶2&#xff09;_总之就是非常唔姆的博客-CSDN博客&#xff09;我已经对回调函数进行了初步的讲解和一个简单的使用事例&#xff0c;鉴于篇幅有限没有进行更加详细的解释&#xff0c;今天便来补上。…

echarts的Y轴设置为整数

场景&#xff1a;使用echarts&#xff0c;设置Y轴为整数。通过判断Y轴的数值为整数才显示即可 yAxis: [{name: ,type: value,min: 0, // 最小值// max: 200, // 最大值// splitNumber: 5, // 坐标轴的分割段数// interval: 100 / 5, // 强制设置坐标轴分割间隔度(取本Y轴的最大…

【算法与数据结构】JavaScript实现十大排序算法(二)

文章目录 关于排序算法快速排序堆排序计数排序桶排序基数排序 关于排序算法 稳定排序&#xff1a; 在排序过程中具有相同键值的元素&#xff0c;在排序之后仍然保持相对的原始顺序。意思就是说&#xff0c;现在有两个元素a和b&#xff0c;a排在b的前面&#xff0c;且ab&#xf…

Windows使用JEnv实现JDK多版本管理

Windows使用JEnv实现JDK多版本管理 JEnv安装Jenv命令 JEnv安装 JEnv是一个帮助我们管理多个JDK安装的工具&#xff0c;并将每个代码库配置为使用特定的JDK版本&#xff0c;而不必改变JAVA_HOME环境变量. 下载链接 windows版 windows版地址JEnvGithub地址: JEnv Jenv命令 添…

如何查看电脑详细配置、型号?这4个技巧 yyds!

知道自己电脑的配置和型号&#xff0c;可以更合理合适的去安装软件&#xff0c;避免出现电脑系统和软件不兼容问题。 了解详细配置信息可以检测一下电脑组件是否是二手的。 从解决实际问题的角度&#xff0c;推荐这4个技巧&#xff1a; 1、右键“此电脑” 2、设备管理器查看…

AJAX的奇妙之旅(1)基础知识

一、简介 AJAX&#xff08;Asynchronous JavaScript and XML&#xff09;是一种使用现有标准的新方法。它是一种用于创建快速动态网页的技术。AJAX 最大的优点是在不重新加载整个页面的情况下&#xff0c;可以与服务器交换数据并更新部分网页内容。AJAX 不需要任何浏览器插件&a…

【操作系统笔记一】程序运行机制CPU指令集

内存地址 指针 / 引用 指针、引用本质上就是内存地址&#xff0c;有了内存地址就可以操作对应的内存数据了。 不同的数据类型 字节序 大端序&#xff08;Big Endian&#xff09;&#xff1a;字节顺序从低地址到高地址顺序存储的字节序小端序&#xff08;Little Endian&#…