【Linux】1w字详解自定义Shell管道 | 构建简易进程池

news2024/9/23 1:26:37

目录

续:通信 4 种情况

应用场景

1. 自定义 shell 管道

1. 包含头文件

2. 解析命令函数

详细步骤

3. 执行命令函数

4. 主函数

总结

2. 使用管道实现一个简易版本的进程池

代码结构

代码实现

channel.hpp

tasks.hpp

main.cc

子进程读取任务,重定向

选择进程

主函数

退出任务

测试和观察


续:通信 4 种情况

  1. 读写段正常,管道如果为空,读端就要阻塞
  2. 读写端正常,管道如果被写满,写端就要阻塞
  3. 读端正常读,写端关闭,读端就会读到 0,表面读到了文件(pipe)结尾,不会被阻塞

父进程等待子进程(Z)退出,测试 n=0;

      4.写端是正常写入,读端关闭了的情况呢?

操作系统就要杀掉正在写入的进程,如何干掉?通过信号杀掉

os 是不会做低效,浪费等类似的工作的。如果做了,就是操作系统的 bug

让子进程写入父进程读取是几号信号?代码没跑完,程序出异常了

结论:管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,让write进程退出

查看 信号可以发现

应用场景

这个管道和我们之前学到的知识,哪些是有关系的呢?

  1. 都是 bash 的子进程,匿名管道
cat test.txt | head -10 | tail -5
sleep 666666 | sleep 7777 | sleep 88888

观察 pid 可以发现,具有血缘关系

  1. 我们想让我们的 shell 支持 | 管道,代码该如何写?

1. 自定义 shell 管道

三步:切割 读取 重定向

例如实现:支持单个管道 ls -a -l | wc -l

思路:

0.分析输入的命令行字符串,获取有多少个 l ,命令打散多个子命令字符串

1.malloc 申请空间,pipe 先申请多个管道

2. 循环创建多个子进程,每一个子进程的重定向情况。

  • 最开始,输出重定向,1->指定的一个管道的写端,
  • 中间:输入输出重定向,0 标准输入重定向到上一个管道的读端,1 标准输出重定向到下一根管道的写端
  • 最后一个:输入重定向,将标准输入重定向到最后一个管道的读端

3.分别让不同的子进程执行不同的命令--exec* --exec* 不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向

这段代码是一个简单的管道实现,用于连接多个命令并将它们的输出串联起来。下面是分块解释每部分代码的含义:

1. 包含头文件

#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <cstring>
#include <cerrno>

2. 解析命令函数

void parseCommand(const string& command, vector<string>& cmds) {
    istringstream stream(command);
    string cmd;
    while (getline(stream, cmd, '|')) {
        cmds.push_back(cmd);
    }
}
  • 接收一个字符串 command,并将其拆分为由 | 符号分隔的命令列表。
  • ❓ 使用 istringstream 来从字符串中读取命令。
  • 将每个命令添加到 cmds 向量中。

解答:

istringstream 类是 C++ 标准库中的一个类,它提供了一个类似文件流的接口来从字符串中读取数据。在这个 parseCommand 函数中,istringstream 被用来从一个包含多个命令的字符串中读取各个命令,并将它们存储在一个 vector<string> 容器中。

下面是 parseCommand 函数的详细解释:

void parseCommand(const string& command, vector<string>& cmds) {
    istringstream stream(command); // 创建一个 istringstream 对象
    string cmd;
    while (getline(stream, cmd, '|')) { // 读取命令直到遇到 '|' 字符
        cmds.push_back(cmd); // 将命令添加到 cmds 向量中
    }
}

详细步骤

  1. 创建 istringstream 对象:
istringstream stream(command);

这一行代码创建了一个 istringstream 对象 stream,并将字符串 command 作为构造函数的参数传入。这意味着 stream 现在可以像处理文件一样处理这个字符串。

  1. 读取命令:
while (getline(stream, cmd, '|')) {
    // ...
}

这里使用 getline 函数从 stream 中读取数据getline 的第三个参数是分隔符,这里设置为 '|',这意味着 getline 将读取直到遇到 '|' 字符为止的内容作为一个命令。

    • getline(stream, cmd, '|'):
      • stream: istringstream 对象。
      • cmd: 用来存储读取到的命令的字符串。
      • '|': 用作分隔符的字符。
  1. 存储命令:
cmds.push_back(cmd);

getline 读取到一个命令后,该命令会被存储在 cmd 字符串中,然后将其添加到 cmds 向量中。

  1. 循环继续:while 循环会一直执行,直到 getline 无法再从 stream 中读取到更多数据为止。这意味着当 getline 遇到最后一个命令之后的 '|' 或者到达字符串的结尾时,循环就会结束。

3. 执行命令函数

void executeCommand(const string& cmd, int inputFd, int outputFd) {
    istringstream stream(cmd);
    vector<char*> args;//获取命令段
    string arg;
    while (stream >> arg) {
        char* cstr = new char[arg.size() + 1];
        strcpy(cstr, arg.c_str());
        args.push_back(cstr);
    }
    args.push_back(NULL);

    if (inputFd != 0) {
        dup2(inputFd, 0);
        close(inputFd);
    }
    if (outputFd != 1) {
        dup2(outputFd, 1);
        close(outputFd);
    }

    // 添加 sleep 以便观察进程状态
    sleep(1);

    execvp(args[0], args.data());
    perror("execvp failed");
    exit(EXIT_FAILURE);
}
  • 接收一个命令字符串 cmd,一个输入文件描述符 inputFd 和一个输出文件描述符 outputFd
  • 使用 istringstreamcmd 中读取命令参数。
  • strcpy(cstr, arg.c_str()); 将命令参数转换为 C 风格的字符串数组。
  • 重定向标准输入和输出文件描述符。
  • 使用 execvp 函数执行命令
  • 如果 execvp 失败,则打印错误消息并退出。

4. 主函数

int main() {
    string command = "ls -a -l | wc -l";
    vector<string> cmds;
    parseCommand(command, cmds);

    int numPipes = cmds.size() - 1;//计算需要管道数量
    int pipefds[2 * numPipes];//利用管道数组创建管道

    for (int i = 0; i < numPipes; ++i) {
        if (pipe(pipefds + i * 2) == -1) {
            perror("pipe failed");
            exit(EXIT_FAILURE);
        }
    }

    for (int i = 0; i <= numPipes; ++i) {
        pid_t pid = fork();
        if (pid == -1) {
            perror("fork failed");
            exit(EXIT_FAILURE);
        } else if (pid == 0) {
            if (i != 0) {
                dup2(pipefds[(i - 1) * 2], 0);
            }
            if (i != numPipes) {
                dup2(pipefds[i * 2 + 1], 1);
            }

            for (int j = 0; j < 2 * numPipes; ++j) {
                close(pipefds[j]);
            }

            // 添加 sleep 以便观察进程状态
            sleep(1);

            executeCommand(cmds[i], 0, 1);
        }
    }

    for (int i = 0; i < 2 * numPipes; ++i) {
        close(pipefds[i]);
    }

    // 添加 sleep 以便观察进程状态
    sleep(1);

    for (int i = 0; i <= numPipes; ++i) {
        wait(NULL);
    }

    return 0;
}

❓ 父进程是如何实现对多个子进程和管道数组进行管理的?

  1. 创建管道:父进程首先计算出需要创建的管道数量,然后使用 pipe 函数为数组 pipefds 创建管道数组。
  2. 创建子进程:父进程使用一个循环来创建多个子进程,每个子进程都负责执行命令数组 cmds 中的一个命令。
  3. 重定向子进程的输入输出:在每个子进程中,根据其在管道数组中的位置,使用 dup2 来重定向其标准输入输出到对应的管道。如果子进程不是第一个,它的标准输入会重定向到前一个管道的读端;如果子进程不是最后一个,它的标准输出会重定向到当前管道的写端。
  4. 关闭不需要的管道描述符:在每个子进程中,关闭所有不需要的管道描述符,防止子进程读取或写入错误的管道。
  5. 执行命令:子进程调用 executeCommand 函数来执行其对应的命令。
  6. 关闭父进程中的管道描述符:在所有子进程创建完毕后,父进程关闭所有管道描述符。
  7. 等待子进程结束父进程使用循环调用 wait 函数来等待所有子进程结束。

以下是管理子进程和管道数组的详细步骤:

关闭所有管道描述符: 在子进程中,通过循环关闭所有 pipefds 中的管道描述符,确保子进程只使用需要的管道描述符。

  • cpp复制
for (int j = 0; j < 2 * numPipes; ++j) {
    close(pipefds[j]);
}

在父进程中,在所有子进程创建之后,也关闭所有管道描述符。

cpp复制

for (int i = 0; i < 2 * numPipes; ++i) {
    close(pipefds[i]);
}

总结

  • 首先解析命令字符串,将它分割成一系列单独的命令。
  • 然后创建所需的管道,并为每个命令创建一个子进程。
  • 每个子进程执行一个命令,并使用管道连接命令的输出
  • 最终输出是最后一个命令的结果。

这个程序模拟了 shell 中管道的工作原理,允许你将多个命令的输出串联起来处理。

问题:

解决:

  1. 替换 nullptrNULL
args.push_back(NULL);
  1. 包含头文件以确保 perror 被正确声明:
#include<stdio.h>
#include<errno.h>

2. 使用管道实现一个简易版本的进程池

通过使用管道实现父进程和子进程之间的通信。该进程池可以执行不同的任务,人选择任务,父进程通过管道向子进程发送任务,子进程执行收到的任务。

代码结构

  1. channel 类:用于保存子进程的信息,包括命令管道、子进程 PID 和子进程名称。
  2. 任务函数:定义一些任务供子进程执行。
  3. slaver 函数:子进程执行的函数,等待从管道读取任务并执行。
  4. 初始化进程池:创建子进程和管道,并保存子进程信息到 channels 向量中。
  5. 控制子进程:父进程选择任务并将任务发送给子进程。
  6. 退出任务:关闭管道并等待子进程退出。

代码实现

channel.hpp

建立好了父进程和子进程之间的通道:先写类再用容器

  1. channel 类
    • channel 类用于存储子进程的信息,包括管道写端文件描述符 _cmdfd,子进程 PID _slaverid 和子进程名称 _processname
#pragma once

#include <string>
#include <vector>
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cassert>

class channel {
public:
    channel(int cmdfd, int slaverid, const std::string &processname)
        : _cmdfd(cmdfd), _slaverid(slaverid), _processname(processname) {}

public:
    int _cmdfd;               // 发送任务的文件描述符
    pid_t _slaverid;          // 子进程的PID
    std::string _processname; // 子进程的名字 -- 方便我们打印日志
};

tasks.hpp

命名规范:

  • 输入:const &
  • 输出:*
  • 输入输出:&
  1. 任务函数
    • 定义了一些任务函数 (task1, task2, task3, task4),这些函数表示子进程可以执行的不同任务。
    • LoadTask 函数通过vector<task_t> *tasks将这些任务加载到 tasks 动态数组中。
#pragma once

#include <iostream>
#include <vector>

typedef void (*task_t)();

void task1() {
    std::cout << "lol 刷新日志" << std::endl;
}

void task2() {
    std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}

void task3() {
    std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}

void task4() {
    std::cout << "lol 用户释放技能,更新用的血量和蓝量" << std::endl;
}

void LoadTask(std::vector<task_t> *tasks) {
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
}

注意对函数指针的设置  typedef void (*task_t)();  ,便于后续生成任务码

main.cc

  • slaver 函数
  1. 子进程slaver 函数中运行,等待从标准输入(由管道重定向)读取任务码,并执行相应的任务
  • 初始化进程池
  1. InitProcessPool 函数创建管道和子进程,并将子进程的信息保存到 channels 向量中。每个子进程在创建后会关闭不需要的管道端,确保每个子进程只有一个写端。
  • 控制子进程
  1. ctrlSlaver 函数由父进程运行,显示菜单并根据用户输入选择任务和子进程,将任务码通过管道发送给子进程。
  • 退出任务
  1. QuitProcess 函数关闭管道并等待所有子进程退出。

前备芝士:

为什么要设置缓冲区 | slab 分派器?

slab 分派器是一种内存管理技术,用于高效地分配和释放小块内存,减少系统调用,系统调用是有成本的,(如内存分配)需要从用户空间切换到内核空间,

任务码和管道:使用管道和任务码来选择性地调用子进程,实现进程间通信和任务调度。

后缀解释:

  • .cc:C++源文件。
  • .hpp:C++头文件,包含了声明,有时也包含定义,因为这里没有使用库。

管道使用:在管道中,父进程写入数据,子进程选择读取,实现不同进程间共享数据。

子进程读取任务,重定向
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <functional>
#include <cassert>
#include <cstdlib>
#include <ctime>

#include "channel.hpp"
#include "tasks.hpp"

void slaver(const std::vector<task_t> &tasks) {
    while (true) {
        int cmdcode = 0;
        int n = read(0, &cmdcode, sizeof(int)); // 从标准输入读取命令码
        if (n == sizeof(int)) {
            std::cout << "slaver say@ get a command: " << getpid() << " : cmdcode: " << cmdcode << std::endl;
            if (cmdcode >= 0 && cmdcode < tasks.size()) {
                tasks[cmdcode]();
            }
        }
        if (n == 0) break; // 父进程关闭写端,子进程退出
    }
}

void InitProcessPool(std::vector<channel> *channels, const std::vector<task_t> &tasks, int processnum) {
    std::vector<int> oldfds;
    for (int i = 0; i < processnum; i++) {
        int pipefd[2];
        int n = pipe(pipefd);
        assert(!n); 

        pid_t id = fork();
        if (id == 0) { // child process
            std::cout << "child: " << getpid() << " close history fd: ";
            for (auto fd : oldfds) {
                std::cout << fd << " ";
                close(fd);
            }
            std::cout << std::endl;

            close(pipefd[1]);
            dup2(pipefd[0], 0); // 重定向子进程的读端
            close(pipefd[0]);
            slaver(tasks);
            exit(0);
        } else { // parent process
            close(pipefd[0]);
            std::string name = "process-" + std::to_string(i);
            channels->push_back(channel(pipefd[1], id, name));
            oldfds.push_back(pipefd[1]);
            sleep(1);
        }
    }
}

void Menu() {
    std::cout << "################################################" << std::endl;
    std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;
    std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;
    std::cout << "#                         0. 退出               #" << std::endl;
    std::cout << "#################################################" << std::endl;
}

批注:

close(pipefd[1]);

dup2(pipefd[0], 0);

选择进程

轮询发送的实现

void ctrlSlaver(const std::vector<channel> &channels)
{
    int which = 0;
    // int cnt = 5;
    while(true)
    {
        int select = 0;
        Menu();
        std::cout << "Please Enter@ ";
        std::cin >> select;

        if(select <= 0 || select >= 5) break;
        // select > 0&& select < 5
        // 1. 选择任务
        // int cmdcode = rand()%tasks.size();
        int cmdcode = select - 1;

        // 2. 选择进程
        // int processpos = rand()%channels.size();

        std::cout << "father say: " << " cmdcode: " <<
            cmdcode << " already sendto " << channels[which]._slaverid << " process name: " 
                << channels[which]._processname << std::endl;
        // 3. 发送任务
        write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));

        which++;
        which %= channels.size();

        // cnt--;
        // sleep(1);
    }
}

子进程被唤醒,收到了任务

主函数

void QuitProcess(const std::vector<channel> &channels) {
    for (const auto &c : channels) {
        close(c._cmdfd);
        waitpid(c._slaverid, nullptr, 0);
    }
}

int main() {
    srand(time(nullptr) ^ getpid() ^ 1023);

    std::vector<task_t> tasks;
    LoadTask(&tasks);

    int processnum = 4;
    std::vector<channel> channels;
    InitProcessPool(&channels, tasks, processnum);

    ctrlSlaver(channels, tasks);

    QuitProcess(channels);

    return 0;
}

❓ 为什么要埋种子 srand(time(nullptr)^getpid()^1023);实现负载均衡

调用 srand() 函数并传递一个种子值,可以初始化随机数生成器,使得每次运行程序时生成的随机数序列

  1. 安全性:如果随机数生成器没有设置种子,攻击者可以预测随机数序列,这可能会带来安全风险。
  2. 多样性:在需要不同结果的情况下(如游戏、模拟、负载均衡等),设置种子可以确保每次运行都有不同的体验或结果。

任务是在人输入时选择的,经常需要系统随机选择来实现均衡

整个过程:任务先和子进程挂上钩,父进程通过对任务的转化读取子进程

补充:

在 C++ 的 <functional> 头文件中,包含了许多预定义的函数对象和函数适配器。以下是一些算术运算函数对象

    • std::plus<T>:执行加法。
    • std::minus<T>:执行减法。
    • std::multiplies<T>:执行乘法。
    • std::divides<T>:执行除法。
    • std::modulus<T>:执行取模运算。
    • std::negate<T>:执行取反运算。

监控会发现:父端不同文件的写,字段都是 3 读取

退出任务

重点:解决 子进程之间有互相通信的能力 这一问题

在使用管道进行通信的情况下,如果子进程之间未妥善处理管道的文件描述符,可能会导致子进程之间的意外通信和退出的混乱。解决:

关闭文件描述符:父进程首先关闭所有子进程的写端管道,这样,当子进程在尝试读取管道时会发现到达文件末端,从而退出(因为管道的写端关闭,读取操作会返回 0 表示文件结束)。

测试和观察

问题:

解决:

根据错误信息,我们需要进行以下修改:

  1. 使用正确的类型定义:在 for 循环中需要指定变量的类型。
  2. 引入缺失的头文件:如 <string> 中的 to_string 成员。
  3. 解决 nullptr 问题:使用 NULL 替代 nullptr,因为 nullptr 是 C++11 及之后的特性。
  4. 启用 C++11 标准:大多数编译器默认设置为 C++98,需要手动启用 C++11。

首先,我们在编译时启用 C++11 标准:

shCopy code
g++ -std=c++11 -o mypipe main.cc

通过这些步骤,可以测试简易进程池的功能并观察进程的运行情况。

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

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

相关文章

Stable Diffusion绘画 | 提示词基础原理

提示词之间使用英文逗号“,”分割 例如&#xff1a;1girl,black long hair, sitting in office 提示词之间允许换行 但换行时&#xff0c;记得在结尾添加英文逗号“,”来进行区分 权重默认为1&#xff0c;越靠前权重越高 每个提示词自身的权重默认值为1&#xff0c;但越靠…

Al+CRM:企业增长新引擎

在企业中&#xff0c;GenAI可以帮助改进决策制定、优化运营流程、增强产品和服务的创新能力&#xff0c;以及提供更加个性化的客户体验&#xff0c;在Gartner的调研中&#xff0c;AI将在以下领域发挥重要作用。 AICRM,将改变原有CRM的使用体验。把抽屉式的系统操作&#xff0c;…

【ubuntu20.04 运行sudo apt-get upgrade报错】

ubuntu20.04 运行sudo apt-get upgrade报错 1 确保系统是最新的2 检查你的软件源列表是否正确无误3 修改软件源3.1 备份原来的源3.2 更换源3.2.1 Ubuntu20.04(focal)阿里云软件源3.2.2 Ubuntu20.04清华软件源 3.3 更新软件源 4 修复升级5 重新安装特定软件包6 导入缺失的密钥 1…

7.1.算法分析与设计-算法分析基本概念与算法分析基础

很难 算法基础知识 算法是对特定问题求解步骤的一种描述&#xff0c;它是指令的有限序列&#xff0c;其中每一条指令表示一个或多个操作。简单的说算法就是某个问题的解题思路&#xff0c;算法的五个重要特性如下&#xff1a; 有穷性。一个算法必须总是&#xff08;对任何合…

数据结构——优先队列

文章目录 一、基本介绍二、基本操作三、实现1 实现的思路2 大顶堆实现2.1 概念2.2 完全二叉树的实现方式2.3 优先队列的图示2.4 对于基本操作实现的讲解2.4.1 检查队列是否为空 ( isEmpty )2.4.2 检查队列是否已满 ( isFull )2.4.3 查看 ( peek )2.4.4 插入 ( offer )2.4.5 删除…

计算数学精解【5】-prolog计算精解(1)

文章目录 概述什么是prolog安装 基础控制台增加规则参考文献 概述 什么是prolog Prolog&#xff08;Programming in Logic&#xff09;是一种面向演绎推理的逻辑型程序设计语言&#xff0c;最早于1972年由柯尔麦伦纳&#xff08;Colmeraner&#xff09;及其研究小组在法国马赛…

Python教程(十三):常用内置模块详解

目录 专栏列表1. os 模块2. sys 模块3. re 模块4. json 模块5. datetime 模块6. math 模块7. random 模块8. collections 模块9. itertools 模块10. threading 模块 总结 专栏列表 Python教程&#xff08;十&#xff09;&#xff1a;面向对象编程&#xff08;OOP&#xff09;P…

uniapp h5本地预览pdf教程 (含白屏|跨域解决方案)

第一步 下载pdf.js 很多pdf.js版本在真机ios环境都会白屏 经测试后2.5.207版本比较稳定&#xff0c;Android和IOS环境PDF文件都能加载成功 下载地址 https://github.com/mozilla/pdf.js/releases/tag/v2.5.207https://github.com/mozilla/pdf.js/releases/tag/v2.5.207第二步 解…

leetcode50. Pow(x, n),快速幂算法

leetcode50. Pow(x, n)&#xff0c;快速幂算法 实现 pow(x, n) &#xff0c;即计算 x 的整数 n 次幂函数&#xff08;即&#xff0c;xn &#xff09;。 示例 1&#xff1a; 输入&#xff1a;x 2.00000, n 10 输出&#xff1a;1024.00000 示例 2&#xff1a; 输入&#xff…

贵阳高新区:加强数字人才培育 引领数字经济未来

在近期举行的贵阳高新区&#xff08;贵州科学城&#xff09;2024年科技创新与成果交流夏季活动中&#xff0c;来自清华大学2022级大数据&#xff08;贵州&#xff09;全日制工程硕士专业的学生们展示了他们在城市公交数据挖掘、通勤线路优化、场景数据的稳定训练以及营运车辆风…

数据分析:多诊断指标ROC分析

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 介绍 pROC::roc函数能够使用一个指标(predictor)去区分两个或多个分组(response),并计算95%置信区间的原理基于以下几个关键点: ROC曲线:ROC曲线是一种图形表示,用于展示分类模型在所有…

【轨物洞见】从电磁感应现象的发现和应用理解科学、技术、工程的关系

电磁感应现象是19世纪以来人类最伟大的发现。有了这个发现&#xff0c;才有后来电和无线电这两大改变人类命运的技术&#xff0c;第二次、第三次工业革命才会发生&#xff0c;人类财富才会猛增、我们的生活才会越来越美好。 让我们来回顾一下电磁感应现象的发现和应用&#xff…

《系统架构设计师教程(第2版)》第13章-层次式架构设计理论与实践-05-数据架构规划与设计

文章目录 1 数据库设计与类的设计融合2. 数据库设计与XML设计融合 教材本节实在太敷衍了&#xff0c;没什么有用的内容 1 数据库设计与类的设计融合 不存在唯一正确的数据模型&#xff0c;然而却存在好的数据模型好模型的目标 将工程项目整个生存期内的花费减至最小 而不是单纯…

KL 散度(python+nlp)

python demo KL 散度&#xff08;Kullback-Leibler divergence&#xff09;&#xff0c;也称为相对熵&#xff0c;是衡量两个概率分布之间差异的一种方式。KL 散度是非对称的&#xff0c;也就是说&#xff0c;P 相对于 Q 的 KL 散度通常不等于 Q 相对于 P 的 KL 散度。 一个简…

zabbix7.0TLS-05-快速入门-触发器

文章目录 1 概述2 查看触发器3 添加触发器4 验证触发器5 查看问题6 问题恢复 1 概述 监控项用于收集数据&#xff0c;但是我们并不能时刻观测每个监控项的数据&#xff0c;看看哪个监控项的数据超过了正常可接受的数值或状态&#xff0c;比如 CPU 负载高于 90%、磁盘使用率低于…

不平衡数据:Handling Imbalanced Dataset with SMOTE导致ValueError ⚖️

不平衡数据&#xff1a;Handling Imbalanced Dataset with SMOTE导致ValueError ⚖️&#x1f4c8; 不平衡数据&#xff1a;Handling Imbalanced Dataset with SMOTE导致ValueError ⚖️&#x1f4c8;摘要引言详细介绍什么是不平衡数据集&#xff1f;⚖️SMOTE简介&#x1f4c8…

加密案例分享:电子设备制造行业

企业核心诉求选择 1.某企业规模庞大&#xff0c;分支众多&#xff0c;数据安全管理方面极为复杂&#xff1b; 2.企业结构复杂&#xff0c;包括研发、销售、财务、总部、分部、办事处、销售等单位连结成为一个庞大的企业组织&#xff0c;数据产生、存储、流转、使用、销毁变化…

Selenium + Python 自动化测试08(截图)

我们的目标是&#xff1a;按照这一套资料学习下来&#xff0c;大家可以独立完成自动化测试的任务。 上一篇我们讨论了滑块的操作方法&#xff0c;本篇文章我们讲述一下截图的操作方法。希望能够帮到爱学的小伙伴。 在实际的测试项目组中我们经常要截屏保存报错信息&#xff0c…

做个一套C#面试题

1.int long float double 分别是几个字节 左到右范围从小到大&#xff1a;byte->short->int->long->float->double 各自所占字节大小&#xff1a;1字节、2字节、4字节、8字节、4字节、8字节 2.System.Object四个公共方法的申明 namespace System {//// 摘要…

C#如何解决引用类型的“深度”克隆问题

前言 在C#中我们new一个引用类型的对象称为对象1&#xff0c;如果我们再次new一个引用类型的对象称为对象2&#xff0c;如果直接将第一个对象直接赋值给第二个对象&#xff0c;然后如果我们这时候改变对象2的值&#xff0c;你会发现对象1的值也会被更改&#xff0c;这就是引用…