深入理解网络 I/O 多路复用:SELECT、POLL

news2025/1/17 14:12:56

在这里插入图片描述

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:网络 I/O
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

  • 前言
  • I/O 复用模型
  • 图解分析
    • SELECT 函数
    • POLL 函数
    • 两者函数区别
  • 源码实践
    • 服务端代码
    • 流程说明
    • 测试挥手问题
  • NIO、I/O 多路复用
  • 总结

前言

Unix/Linux 下可用的 I/O 模型有以下五种:

  1. 阻塞式 I/O
  2. 非阻塞式 I/O
  3. I/O 复用(select、poll)
  4. 信号驱动式 I/O(SIGIO)
  5. 异步 I/O

在 Linux 中操作内核时,所有的无非三种操作,分别是输入、输出、报错输出

0-输入
1-输出
2-报错输出

一个输入操作通常包括两个不同的阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于一个套接字(Socket)的输入操作,第一步通常涉及等待数据从网络中;当所等待分组到达时,它被复制到内核中的某个缓冲区,第二步就是把数据从内核缓冲区复制到应用进程缓冲区

I/O 复用模型

I/O 复用(I/O multiplexing):SELECT、POLL,阻塞在这两个系统「用户态、内核态」调用中的某一个之上,而不是阻塞在真正的 I/O 系统调用上

在这里插入图片描述

将阻塞于 select 调用,等待数据报套接字变为可读,当 select 返回的套接字可读这一条件时,再调用 recvfrom 把所读的数据报复制到应用进程缓冲区

使用 select 的优势在于我们可以等待多个描述符就绪

与 I/O 复用密切相关的另外一种模型就是在多线程的场景下使用阻塞时 I/O 模型 BIO,两者极其相似,但它没有使用 select 阻塞在多个文件描述符上,而是使用多个线程「每个文件描述符分配一个线程的方式」这样每个线程都可以自由地调用诸如:recvfrom 之类的阻塞式 I/O 系统调用了.

图解分析

SELECT 属于 synchronous I/O multiplexing 同步 I/O 多路复用

它允许程序监视多个文件描述符,等待一个或多个文件描述符尾某种类型(read、write)的 I/O 操作

在这里插入图片描述

SELECT 仅用一次系统调用,recv | recvfrom 接收数据都只是会接收具体可操作的文件描述符数量,例如:图中带有 data 的 IO 操作

SELECT 函数

该函数允许进程指示内核等待多个事件「文件描述符」中的任何一个发生,并且只有在一个或多个时间发生或经历一段指定的时间后才能唤醒它

以上图中的 IO 块作为例子延迟,当调用 select 函数时,告知内核仅在以下情况发生时才进行返回

  1. IO 集合:1、7 中任何描述符准备好读
  2. IO 集合:4、5 中任何描述符准备好写
  3. IO 集合:2、3、6 中任何描述符有异常条件待处理
  4. 这个过程经历了 Xxx 秒

当调用 select 时要告知内核对哪些描述符(read、write、exception)感兴趣以及需等待多长时间

通过 Linux 内核帮助文档来学习 SELECT

man 2 select
在已经有的 Linux 内核中已经不支持使用 SELECT 函数了,查看文档时会出现:none - deprecated system calls,例如内核版本:Linux version 5.11.12-300.el7.aarch64

当然在 x86_64 内核版本仍然可以查看该帮助文档,例如内核版本:Linux version 3.10.0-1160.90.1.el7.x86_64

可通过 cat /proc/version 命令来查看

在它的文档中可以看到以下这些函数都是同步 I/O 多路复用使用的.

select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing

观察 select 函数源码,如下:

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

nfds:代表文件描述符的数量
*readfds:代表读取的文件描述符
*writefds:代表写入的文件描述符
*exceptfds:代表异常条件的文件描述符
*timeout:代表等待多长的时间

它的 timeval 结构用于指定这段时间的秒数、微妙数,结构源码如下:

struct timeval {
    long    tv_sec;         /* seconds */
    long    tv_usec;        /* microseconds */
};

其中 select > timeout 参数有以下三种可能性:

  1. 永远等待下去:仅在有一个描述符准备好 I/O 时才返回,为此,可以将该参数设置为空
  2. 等待一段固定的时间:在有一个描述符准备好 I/O 时才返回,但是不会超过该参数所设置的秒数、微妙数
  3. 根本不等待:检查描述符完成后立即返回,称之为 轮询(polling),为此,该参数中的秒数、微妙数必须设置为 0

前面两种可能性的等待通常会被进程在等待期间捕获的信号中断,并从信号处理函数中返回

其中 select > readfds、writefds、exceptfds 三个参数指向的是一组描述符结果集

最大描述符数
SELECT 最大描述符数,大多数应用程序不会用到许多描述符,臂如说很少能够找到一个同时使用几百个描述符的应用程序,然而使用那么多描述符的应用程序确实村再,它们往往使用 select 来复选描述符。最初设计 select 时,操作系统通常对每个进程可用的最大描述符数设置了上限

取自于 4.4 BSD <sys/types.h> 头文件中,最大的描述符在内核被定义为一个常量:FD_SETSIZE 值为 1024,此时可以使用 poll 代表 select,这样可以避免描述符有限的问题.

POLL 函数

执行与 SELECT 类似的任务,它等待一组文件描述符中的一个准备好执行 I/O,不过在处理流设备时,它能够提供额外的信息

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

第一个参数是一个指向一个结构数组第一个元素的指针,每个数组元素都是一个 pollfd 结构,用于指定测试某个给定描述符 fd 的条件.

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

要测试的条件由 events 成员指定,函数在相应的 revents 成员中返回该描述符的状态(每个描述符都有两个变量,一个为调用值,另外一个为返回结果,从而避免了「值-结果」参数)这两个成员的每一个都由指定某个特定条件的一位或多位构成

值-结果参数:代表传入的值和输出的值不是同一个参数,对比 select 来说,它的中间三个参数都是「值-结果」参数

字段 events:代表一个输入参数,一个指定应用程序对文件描述符 fd 感兴趣的事件的位 bit 掩码;若将此字段指定为零,则忽略 fd 中所有的事件,并且 revents 返回 0

字段 revents:代表一个输出参数,由内核填充实际发生的事件,事件返回的 bit 位可以包括事件中指定的任何位,也可以包括:POLLERR、POLLHUP 或 POLLNVAL 值之一

POLLERR、POLLHUP 或 POLLNVAL 这三个值在 events 字段中设置是没有意义的,当相应的条件为真时,将在 revents 字段中设置.

用于指定 events 标志以及 revents 标志的一些常量值,如下表格:

常量值作为 events 输入作为 events 结果说明
POLLIN☑️☑️普通或优先级带数据可读
POLLRDNORM
(Read Normal)
☑️☑️普通数据可读
POLLRDBAND
(Read Band)
☑️☑️优先级带数据可读
POLLPRI
(Priority)
☑️☑️高优先级数据可读
POLLOUT☑️☑️普通数据可写
POLLWRNORM
(Write Normal)
☑️☑️普通数据可写
POLLWRBAND
(Write Band)
☑️☑️优先级带数据可写
POLLERR
(Error)
☑️发生错误条件(仅输出)
POLLHUP
(Hang up)
☑️发生挂起(仅输出)
POLLNVAL☑️无效请求:fd未打开(仅输出)
描述符不是一个打开的文件

该表格分为三个部分:第一部分是处理输入的四个常量值,第二部分是处理输出的三个常量值,第三部分是处理错误的三个常量值,其中第三部分的常量值不能在 events 中设置,但是当相应条件(第三部分常量值)存在时就在 revents 中返回.

POLL 识别三类数据:普通(Normal)、优先级带(Priority Band)、高优先级(Hign Priority)

POLLIN 可被定义为 POLLRDNORM、POLLRDBAND 的逻辑或

第二个参数 nfds:调用者应该在 nfds 中指定 fds 数组中的项数

第三个参数 timeout:timeout 参数指定 poll() 将阻塞的最小毫秒数(这个时间间隔将四舍五入到系统时钟粒度,内核调度延迟意味着阻塞时间间隔可能会超出一小部分)在 timeout 中指定负值意味着无限超时。指定超时为零将导致 poll() 立即返回,即使没有文件描述符准备好

负值:无限超时
0:立即返回
正数:阻塞的最小毫秒数

两者函数区别

在 SELECT 函数中提及到了 FD_SETSIZE,每个描述符集最大的描述符数量,当有了 POLL 就无须考虑这个问题,因为分配了一个 pollfd 结构的数组并把数组中元素的数目通知内核成了调用者的责任,内核不再需要知道 fd_set(SELECT 中的参数类型) 固定大小的数据类型

POSIX 规范对 select、poll 都有需要,然而从当今的可移植性来考虑,支持 select 系统比支持 poll 系统要多,另外除了 select 函数还定义了 pselect 函数,它能够处理信号阻塞并提供了更高时间分辨率的 select 的增强版本,而在 POSIX 规范中没有为 poll 定义类似的东西.

源码实践

以下代码是服务端侧的所有代码,处理三种类型事件:注册事件、写事件、读事件,客户端代码可以沿用之前 BIO、NIO 中的源码,或者说可以在命令窗口通过 nc 命令进行连接!!!

服务端代码

package org.vnjohn.select;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @author vnjohn
 * @since 2023/12/7
 */
public class SelectMultiplexingSocketThread extends Thread {
    private Selector selector = null;

    /**
     * 初始化 socket 服务端实例,进行 accept
     */
    public void initServer() {
        try {
            ServerSocketChannel server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(8090));
            //  select、poll、*epoll 都是使用同样的方式打开
            selector = Selector.open();
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void start() {
        initServer();
        System.out.println("Socket Server start...");
        try {
            while (true) {
                // select()
                // select(long timeout):毫秒级别的等待
                while (selector.select() > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        iterator.remove();
                        if (key.isAcceptable()) {
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            // 只处理了{read}并注册一个对这个 key 感兴趣的 write 事件
                            readHandler(key);
                        } else if (key.isWritable()) {
                            // 写事件->当 Send-Queue 为空时,就一定会给你返回可以写的事件,就会回调我们的写方法
                            // 多路复用器能不能写是参考:Send-Queue 有没有空间
                            // 1、你准备好要写什么了,这是第一步
                            // 2、第二步你才关心 Send-Queue 是否有空间
                            // 3、so,读 read 一开始就要注册,但是 write 依赖以上关系,什么时候用什么时候注册
                            // 4、若一开始就注册了 write 事件,进入死循环,一直调起!!!
                            writeHandler(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 该方法用于接收新的客户端连接进来,在此处就会向 SELECT 结果集注册一个 read 事件,用于接收客户端发送过来的数据
     *
     * @param key
     */
    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel client = ssc.accept();
            // NIO->NON_BLOCKING!!!
            client.configureBlocking(false);
            // 分配给 8M 写入的空间
            ByteBuffer buffer = ByteBuffer.allocate(8192);
            client.register(selector, SelectionKey.OP_READ, buffer);
            System.out.println("-------------------------------------------");
            System.out.println("new SocketClient:" + client.getRemoteAddress());
            System.out.println("-------------------------------------------");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     *
     * @param key
     */
    private void writeHandler(SelectionKey key) {
        System.out.println("write handler...");
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.flip();
        // 判断服务端与客户端之间所在的缓冲区是否有数据存在,有则进行写入操作,将数据发送给客户端
        while (buffer.hasRemaining()) {
            try {
                client.write(buffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 模拟一下业务延迟,将注册进当前客户端的写事件注销掉,下一次 select 时该事件不会被获取到
        // 当下一次客户端有数据要读取时,写入该客户端的事件将会再次被注册
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        buffer.clear();
        key.cancel();
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 写事件,针对当前选中的键做读取操作,当读取到的数据不为空时,在我们这里模拟写的操作,此时 Send-Queue 肯定是有数据存在的
     * 所以在这里会模拟>注册上一个 write 写事件,把数据写给对应的客户端
     *
     * @param key 当前选中的键 > 文件描述符
     */
    public void readHandler(SelectionKey key) {
        System.out.println("readHandler...");
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        int read;
        try {
            while (true) {
                read = client.read(buffer);
                if (read > 0) {
                    // 关心 OP_WRITE 其实就是关心 Send-Queue 是不是有空间
                    client.register(key.selector(), SelectionKey.OP_WRITE, buffer);
                } else if (read == 0) {
                    break;
                } else {
                    client.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SelectMultiplexingSocketThread service = new SelectMultiplexingSocketThread();
        service.start();
    }
}

通过模拟一个线程,死循环的方式从 Selector 中获取所有的事件 Key,通过 Key 类型的不同来执行不同的处理逻辑

注册 > accept:acceptHandler
写 > write:writeHandler
读 > read:readHandler

Selector 是 SELECT/POLL、Epoll 在 Java 应用程序中的抽象,通过 Selector 优先选择的是 Epoll,但是可以通过 -D 参数来修正,主要就是更改所依赖的 Selector 实现类

select/poll-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider
epoll-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider

  1. Selector#open:在 select/poll 模型下,它和内核之间是没有交互的,至于 Epoll 我们在下篇文章详细讲解
  2. SelectableChannel#register(Selector, SelectionKey.OP_ACCEPT):相当于在内核中是一个 listen 函数的调用

register:在 select/poll 模型中,JVM 里面会为其开辟一个数组将监听的文件描述符放进去,期间是无系统调用的

  1. Selector#select:相当于就是内核中的 select(fd)、poll(fd) 函数

select 有两种方式支持调用,select()、select(long timeout)
当设置了时间,等待对应参数值的毫秒级别数返回
当未设置时间,值为 0

也就是在介绍两个函数时所描述的一样,0 代表立即返回,设置了时间那么就等待一定时间以后才返回调用的结果

  1. Selector#selectedKeys:返回的是哪些有状态的 fds 文件描述符集合 > SelectionKey

无论是什么样的多路复用器:select、poll、epoll、kqueue,只需要返回给我有状态的 fd 文件描述符即可

在 NIO 处理时,是服务端这一侧对着每一个 fd 触发系统调用,这样肯定是会浪费很多资源的,而这里调用一次 select 就可以知道那么 fd 可以进行 R/W

在这个处理期间仍然会有两种类型的 Socket:服务端 Socket 用于 accept 接收客户端连接的、客户端连接后的 Socket(用于读写数据使用的)

  1. SelectionKey#isAcceptable:准备好去接收新的 Socket 连接

在语义上,accept 接收连接且返回新连接的 FD,那么在 select/poll 中 FD 是如何存储的呢?

因为在它们内核是没有空间的,所在会在 JVM 中保存这些 FD 信息,与第二点所描述的 SelectableChannel#register(Selector, SelectionKey.OP_ACCEPT) 存放在一起

在源码中的 acceptHandler 方法中,出现 client.register(selector, SelectionKey.OP_READ, buffer) 将其装入 JVM 数组

  1. SelectionKey#isReadable:准备去接收读取的 Socket 连接,在这里会将 R/W 同时都处理

在当前线程,这个方法可能会被阻塞住(处理过多的数据与逻辑)

所以,后面提出了 IO Threads 概念(拿到数据以后就不管后续的事情了,丢给后面的 worker 线程去进行处理)

Redis 就是运用 epoll 模型,同时它里面也有 IO Threads 概念,Redis 工作线程虽然是单线程的,但是它的网络 I/O、数据处理流都是采用多线程的

在源码中的 readHandler 方法中,拿到 FD buffer 中的数据以后,进行 read 读取,有以下几种情况:
1、若返回 > 0,就会注册上一个 write 写事件,把数据写给对应的客户端
2、若返回为 0,就退出当前循环
3、若返回为 -1,说明客户端已经断开连接,同时将当前读取的 socket 连接关闭

流程说明

以上对源码以及相关的方法进行了说明,接下来我们来测试 select/poll 模型与 TCP/IP 挥手的问题

通过 select/poll I/O 多路复用运行,并 strace 追踪如上代码:

1、先将首行 package 通过 vi 命令移除
2、编译源文件为 class 文件:javac SelectMultiplexingSocketThread.java
3、以 select/poll 模型追踪运行 class 文件:strace -ff -o poll java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider SelectMultiplexingSocketThread

运行服务端以后,观察内核所输出的系统调用函数,如下:

在这里插入图片描述

ppoll([{fd=5, events=POLLIN}, {fd=4, events=POLLIN}], 2, NULL, NULL, 0
fd:4、5,请求事件:POLLIN,无返回的事件类型

开启另外一个命令窗口,通过 nc localhost 8090 连接服务端,此时在服务端会打印出具体的客户端连接信息,但观察内核文件中时它不会有任何的具体输出内容,由此可见,它并未与内核发生调用关系,而是存储在了自身的 JVM 数组中

在这里插入图片描述

此时,再观察 netstat -natp TCP/IP 连接信息,会出现以下两条条目:

在这里插入图片描述

此时在客户端写入一定内容,观察服务端的执行过程,它会先读取客户端写入的内容,然后通过写事件将内容回放给客户端的窗口中,如下图所示:

客户端:

在这里插入图片描述

服务端:

在这里插入图片描述

在内核侧做出的具体处理,如下图:

在这里插入图片描述

正常状态下,将 nc localhost 8090 开启的客户端,正常退出,再观察 TCP/IP 网络条目,可以发现只有服务端侧的一条条目信息存在,也就是进行了四次挥手的正常流程!!!

在这里插入图片描述

测试挥手问题

在流程说明的最后,客户端与服务端之间的挥手流程是正常走完的,也就是说不会在服务端中存在非正常状态的客户端 sockfd 存在

若我们将源码中 client.close(); 代码进行注释,然后再测试客户端关闭的一个过程

1、注释 client.close(); 客户端关闭的代码,然后重新编译执行服务端代码

2、通过 nc localhost 8090 模拟客户端再次连接到服务端上.

3、关闭客户端的连接以后,此时再观察 TCP/IP 条目,如下所示:

在这里插入图片描述

tcp6       0      0 ::1:50306               ::1:8090                FIN_WAIT2   -
tcp6       0      0 ::1:8090                ::1:50306               CLOSE_WAIT  16838/java

TCP 需要经过四次挥手,虽然在客户端这边 FIN 标记成功了,但是在服务端这边还没有关闭,因为服务端的 FIN 标记在客户端那一侧未作出 ACK 反应,所以在 TCP/IP 条目中就会出现不完整的状态 FIN_WAIT2 | CLOSE_WAIT 信息

由此可见,TCP 四次挥手的过程,是两端都需要经过 FIN+ACK 处理以后,整个信息才能完全保证安全的释放

以上就会造成一个问题:服务端一直持有客户端的连接信息,但是这条信息是已经是没必要的,它会浪费在服务端这一侧的一条「四元组:源 IP:端口>目标 IP:端口」信息,内核中的 socketfd 就一直会被占用,相同的对端服务端也就一直不能使用这个资源建立新的连接,浪费的资源和名额!!!

NIO、I/O 多路复用

在这里插入图片描述

在 NIO 中,每次都需要遍历获取 I/O 状态,每次都需要经过用户态、内核态之间的转换才能实现,然后再交由给应用程序去进行 R/W

在 I/O 多路复用中,通过一个系统调用,可以获取到所有 I/O 状态,由应用程序对有状态的 IO 进行 R/W

总结

该篇博文主要介绍的是 I/O 模型中的多路复用:SELECT、POLL,简要分析了 I/O 多路复用的模型,通过图解分析的方式告知多路复用所带来的好处「介绍了 SELECT、POLL 函数以及它们的区别」,通过实践代码的方式来分析 I/O 多路复用在系统调用中所涉及到的内核流程代码,同时也当代码编写不当时会给 TCP 挥手带来的问题,最后介绍了上篇 NIO 博文与 I/O 多路复用之间的区别,希望能够得到你的支持,感谢三连

四元组唯一:源 IP、源端口、目标 IP、目标端口

🌟🌟🌟愿你我都能够在寒冬中相互取暖,互相成长,只有不断积累、沉淀自己,后面有机会自然能破冰而行!

博文放在 网络 I/O 专栏里,欢迎订阅,会持续更新!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

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

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

相关文章

K-means算法通俗原理及Python与R语言的分别实现

K均值聚类方法是一种划分聚类方法&#xff0c;它是将数据分成互不相交的K类。K均值法先指定聚类数&#xff0c;目标是使每个数据到数据点所属聚类中心的总距离变异平方和最小&#xff0c;规定聚类中心时则是以该类数据点的平均值作为聚类中心。 01K均值法原理与步骤 对于有N个…

Redis安全与性能

文章目录 第1关&#xff1a;持久化第2关&#xff1a;复制第3关&#xff1a;Redis事务与流水线 第1关&#xff1a;持久化 1、创建快照的方式有&#xff1a; A、执行 SAVE 命令 B、执行 BESAVE 命令 C、使用 save 选项配置自动快照 D、通过客户端发送关闭服务器请求 E、以上…

Python---time库

目录 时间获取 时间格式化 程序计时 time库包含三类函数&#xff1a; 时间获取&#xff1a;time() ctime() gmtime() 时间格式化&#xff1a;strtime() strptime() 程序计时&#xff1a;sleep() perf_counter() 下面逐一介绍&#…

[Linux] Bash脚本多函数应该如何执行?使用eval提高脚本编写效率!

在工作过程中经常会编写一些测试脚本&#xff0c;有些脚本里有多个函数&#xff0c;要通过用户输入执行对应的函数&#xff0c;如这样&#xff1a; 这也太麻烦了吧 执行如下&#xff1a; 这样在函数多的情况下需要写很多判断&#xff0c;效率低下。 我们可以使用eval命令来进行…

【深度学习】一维数组的聚类

在学习聚类算法的过程中&#xff0c;学习到的聚类算法大部分都是针对n维的&#xff0c;针对一维数据的聚类方式较少&#xff0c;今天就来学习下如何给一维的数据进行聚类。 方案一&#xff1a;采用K-Means对一维数据聚类 Python代码如下&#xff1a; from sklearn.cluster im…

Zabbix自定义飞书webhook告警媒介1

说明&#xff1a;此配置仅适用于7版本及以上&#xff0c;低版本可能有问题 JavaScript 内容如下&#xff1a; try {var sourceData JSON.parse(value),req new HttpRequest(),response;if (sourceData.HTTPProxy) {req.setProxy(sourceData.HTTPProxy);}req.addHeader(Conte…

vuepress-----20、全文搜索

默认主题自带的搜索, 只会为页面的标题、h2、h3 以及 tags构建搜索索引。所以尽量将围绕知识点的关键字体现到标题上。而 tags 更为灵活&#xff0c;可以把相关的能想到的关键字都配置到 tags 中&#xff0c;以方便搜索。 默认插件介绍 (opens new window) 默认主体配置 (ope…

Latex公式中矩阵的方括号和圆括号表示方法

一、背景 在使用Latex写论文时&#xff0c;不可避免的涉及到矩阵公式。有的期刊要求矩阵用方括号&#xff0c;有的期刊要求矩阵用圆括号。因此&#xff0c;特记录一下Latex源码在两种表示方法上的区别&#xff0c;以及数组和方程组的扩展。 二、矩阵的方括号表示 首先所有的…

CnetSDK .NET OCR Library SDK Crack

CnetSDK .NET OCR Library SDK Crack CnetSDK .NET OCR Library SDK 是一款高精度 .NET OCR 扫描仪软件&#xff0c;用于从图像中识别字符&#xff0c;如文本、手写和符号。该.NET OCR库软件采用Tesseract OCR引擎技术&#xff0c;将字符识别准确率提高高达99%。通过将 .NET OC…

时序数据库选型TimescaleDB

最近要做一个数字车间的物联网项目&#xff0c;数据存储成了首先要解决的问题&#xff0c;整个车间一共104台数控机床&#xff0c;1s钟采集1次数据&#xff0c;360024365*1043,279,744,000 &#xff0c;一年要产生32亿条记录&#xff0c;这个数据量用常见的关系型数据库肯定是不…

PHP对接企业微信

前言 最近在做项目中&#xff0c;要求在后台管理中有企业微信管理的相关功能。相关准备工作&#xff0c;需要准备好企业微信账号&#xff0c;添加自建应用&#xff0c;获得相应功能的权限&#xff0c;以及agentid、secre等。 参考文档&#xff1a; 企业微信开发文档 功能实现 因…

MacBook 逆水寒下载安装使用教程,支持最新版本 MacOS 流畅不闪退

最近 MacBook 系统更新到了 MacOS 14.1 很多朋友的逆水寒玩不了了&#xff0c;我尝试了一番可以正常玩了&#xff0c;看图&#xff1a; 其实操作也很简单&#xff0c;我们从头开始&#xff0c;因为 MacOS 系统的更新所以我们也需要更新新版本的 playCover 来适配新的系统&#…

【Vue第3章】使用Vue脚手架_Vue2_笔记

笔记 脚手架文件结构 ├── node_modules ├── public │ ├── favicon.ico: 页签图标 │ └── index.html: 主页面 ├── src │ ├── assets: 存放静态资源 │ │ └── logo.png │ │── component: 存放组件 │ │ └── HelloWorld.vue …

配置禁止BT下载的示例

如图1所示,企业内用户通过交换机连接到RouterA的Eth2/0/0,并通过RouterA的GE0/0/1接口连接到WAN侧网络。 现在要求在RouterA上通过配置基于智能应用控制SAC(Smart Application Control)的流分类,禁止企业用户进行BT下载。 图1 配置禁止BT下载的组网图: 操作步骤 1.Rout…

[原创][6]探究C#多线程开发细节-“ConcurrentDictionary<T,T>解决多线程的无顺序性的问题“

[简介] 常用网名: 猪头三 出生日期: 1981.XX.XX QQ联系: 643439947 个人网站: 80x86汇编小站 https://www.x86asm.org 编程生涯: 2001年~至今[共22年] 职业生涯: 20年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、D…

电商系统架构演进

聊聊电商系统架构演进 具体以电子商务网站为例&#xff0c; 展示web应用的架构演变过程。 1.0时代 这个时候是一个web项目里包含了所有的模块&#xff0c;一个数据库里包含了所需要的所有表&#xff0c;这时候网站访问量增加时&#xff0c;首先遇到瓶颈的是应用服务器连接数&a…

【动态规划】03使用最小花费爬楼梯(easy1)

题目链接&#xff1a;leetcode使用最小花费爬楼梯 目录 题目解析&#xff1a; 算法原理 1.状态表示 2.状态转移方程 3.初始化 4.填表顺序 5.返回值 编写代码 题目解析&#xff1a; 题目让我们求达到楼梯顶部的最低花费. 由题可得&#xff1a; cost[i] 是从楼梯第 i 个…

AI并行计算:CUDA和ROCm

1 介绍 1.1 CUDA CUDA&#xff08;Compute Unified Device Architecture&#xff09;是Nvidia于2006年推出的一套通用并行计算架构&#xff0c;旨在解决在GPU上的并行计算问题。其易用性和便捷性能够方便开发者方便的进行GPU编程&#xff0c;充分利用GPU的并行能力&#xff0…

利用管道、信号量、信号、共享内存和消息队列进行多进程通信

一.管道&#xff08;分为命名管道和匿名管道&#xff09; 管道的特点&#xff1a; ①无论是命名管道还是匿名管道&#xff0c;写入管道的数据都存放在内存之中。 ②管道是一种半双工的通信方式&#xff08;半双工是指终端A能发信号给终端B&#xff0c;终端B也能发信号给终端…

ISIS默认路由下发的各种机制

作者简介&#xff1a;大家好&#xff0c;我是Asshebaby&#xff0c;热爱网工&#xff0c;有网络方面不懂的可以加我一起探讨 :1125069544 个人主页&#xff1a;Asshebaby博客 当前专栏&#xff1a; 网络HCIP内容 特色专栏&#xff1a; 常见的项目配置 本文内容&am…