【Linux】(32)详解命名管道 | 日志管理 | 进程池2.0

news2024/11/10 12:58:18

目录

1. 介绍

2. 理解

3.运用

3.1 简易通信

makefile

comm.hpp

server.cc (服务端,读取显示)

client.cc (客户端,写入)

3.2 日志

log.hpp

1. 定义日志级别

2. 实现日志函数

可变参数

3. 日志输出管理

3.3 进程池 2.0

channel.hpp

tasks.hpp

main.cc

🏷️ 解释


1. 介绍

具有血缘关系的进程进行进程间通信,是匿名管道,如果毫不相关的进程进行通信呢?命名管道 mkfifo

eg. echo 和 cat 的实现,就是一种命名

🎢mkfifo() 函数

mkfifo() 函数用于创建一个FIFO(First In First Out)或者称为命名管道,它允许进程之间进行通信。下面是关于 mkfifo() 函数的详细介绍:man mkfifo

函数原型


#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • pathname:要创建的命名管道的路径名
  • mode:创建命名管道时设置的权限模式,通常以 8 进制表示,比如 0666。

返回值

  • 成功,返回值为 0;若失败,返回值为 -1,并设置errno来指示错误类型。

功能

mkfifo() 函数的作用是在文件系统中创建一个特殊类型的文件,该文件在外观上类似于普通文件,但实际上是一个FIFO,用于进程之间的通信。这种通信方式是单向的,即数据写入FIFO的一端,可以从另一端读取出来,按照先进先出的顺序。

🎢创建命名管道

std::string fifoPath = "/tmp/my_named_pipe";  // 命名管道的路径名
mkfifo(fifoPath.c_str(), 0666); // 创建权限为0666的命名管道

🏷️注意事项

  • 路径名:确保要创建的命名管道路径名合法且没有重复。
  • 权限模式:根据实际需求设置合适的权限模式,确保可被需要访问该管道的进程所访问。
  • 错误处理:对 mkfifo() 函数的返回值进行适当的错误处理,根据具体的错误原因进行相应的处理和日志记录。

创建命名管道并处理错误的示例

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
int main() {
    std::string fifoPath = "/tmp/my_named_pipe";  // 命名管道的路径名
    // 尝试创建命名管道
    if (mkfifo(fifoPath.c_str(), 0666) == -1) {
        // 检查错误类型
        if (errno == EEXIST) {
            std::cerr << "Named pipe already exists" << std::endl;
        } else {
            // 输出错误信息
            perror("Error creating named pipe");
        }
    } else {
        std::cout << "Named pipe created successfully" << std::endl;
    }
    return 0;
}

使用命名管道进行读写操作

open后,可以通过 read()write() 函数对其进行读写操作。

关闭命名管道

关闭命名管道是确保在进程使用完毕后释放相关资源的重要步骤。

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
int main() {
    int fd = open("/tmp/my_named_pipe", O_RDONLY);  // 以只读方式打开命名管道
    // 进行读取操作...
    // 关闭命名管道
    if (close(fd) == -1) {
        perror("Error closing named pipe");
    } else {
        std::cout << "Named pipe closed successfully" << std::endl;
    }
    return 0;
}

注意事项

  • 关闭顺序:如果有多个文件描述符指向同一个命名管道,需要依次关闭这些文件描述符,直到所有相关资源都得到释放。

通信方式

  • 单向通信:命名管道提供单向通信方式,一个进程写入,另一个进程读取。
  • 持久性:命名管道以文件形式存在于文件系统中,即使创建进程终止,管道仍然存在。
  • 阻塞和非阻塞:可以选择阻塞或非阻塞模式进行通信。

用法示例

进程 A 写入数据到命名管道

int fd = open("/tmp/my_named_pipe", O_WRONLY);  // 以只写方式打开命名管道
write(fd, "Hello, named pipe!", 18);  // 向管道中写入数据
close(fd);  // 关闭命名管道

进程 B 从命名管道读取数据

int fd = open("/tmp/my_named_pipe", O_RDONLY);  // 以只读方式打开命名管道
char buffer[50];
read(fd, buffer, 50);  // 从管道中读取数据
close(fd);  // 关闭命名管道

2. 理解

对创建的文件,进行只读/只写的 open

  1. 如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统会维持几份呢?一份

还是这样的:

管道是文件缓冲区,不要进行刷盘!所以就有了内存级文件的存在

  1. 怎么打开同一个文件?为什么要打开同一个文件?

同路径下同一个文件名=路径+文件 因为唯一性,就可以保证看到同一份资源了

3.运用

3.1 简易通信

makefile

.PHONY:all
all:server client

server:server.cc
	g++ -o $@ $^ -g -std=c++11
client:client.cc
	g++ -o $@ $^ -g -std=c++11

.PHONY:clean
clean:
	rm -f server client
  • .PHONY:all这行声明 all 是一个伪目标。即使文件系统中存在一个名为 all 的文件,make all 命令也会执行与 all 相关的规则,而不是认为目标已经是最新的。
  • all:server client这行定义了 all 伪目标的依赖,即 serverclient。当运行 make all 时,Makefile 会首先尝试构建 serverclient 目标

comm.hpp

#ifndef COMM_HPP
#define COMM_HPP

#include <iostream>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <cstdlib>
#include <cstdio>

#define FIFO_FILE "/tmp/my_fifo"
#define FIFO_OPEN_ERR 1

#endif // COMM_HPP

server.cc (服务端,读取显示)

#include <iostream>
#include "comm.hpp"

using namespace std;

int main()
{
    // 创建命名管道文件
    if (mkfifo(FIFO_FILE, 0666) == -1)
    {
        if (errno != EEXIST)
        {
            perror("mkfifo");
            exit(FIFO_OPEN_ERR);
        }
    }

    // 打开管道
    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    cout << "Server open file done" << endl;

    // 开始通信
    while (true)
    {
        char buffer[1024] = {0};
        int x = read(fd, buffer, sizeof(buffer) - 1);
        if (x > 0)
        {
            buffer[x] = 0;
            cout << "Client says: " << buffer << endl;
        }
        else if (x == 0)
        {
            cout << "Client quit, server will also quit." << endl;
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }

    close(fd);
    return 0;
}

批注:

  1. 要等待写入方打开之后,自己才会打开文件,向后执行

client.cc (客户端,写入)

#include <iostream>
#include "comm.hpp"

using namespace std;

int main()
{
    // 打开管道
    int fd = open(FIFO_FILE, O_WRONLY);
    if (fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    cout << "Client open file done" << endl;

    string line;
    while (true)
    {
        cout << "Please Enter: ";
        getline(cin, line);

        if (!line.empty())
        {
            write(fd, line.c_str(), line.size());
        }
    }
    close(fd);
    return 0;
}
write(fd, line.c_str(), line.size());  //将其字符流化

打开两个窗口,运行测试如下,成功~

接下来我们将来学习将错误日志化

3.2 日志

  1. 日志时间
  2. 日志等级
  • Info:常规消息
  • Warning:报错信息
  • Error:必要严重了,可能需要立即处理
  • Fatal:致命的
  • Debug:调试

    3.日志内容

    4.文件的名称和行号

实现一个简单的日志函数,在自己的代码中慢慢的引入日志

log.hpp

为了实现上述日志系统,我们可以按照以下步骤进行:

  1. 定义日志级别:定义常见的日志级别,如 InfoWarningErrorFatalDebug
  2. 实现日志函数:使用可变参数实现一个通用的日志函数,该函数能够记录不同级别的日志信息,并且包含时间戳、文件名和行号等信息。
  3. 日志输出管理:实现日志的输出方式,如输出到控制台或文件,支持按日志级别分类输出。
  4. 封装日志接口:提供一个简洁的接口,方便在代码中随时记录日志。

以下是代码实现示例:

1. 定义日志级别

log.hpp 中定义日志级别的枚举类型,并实现日志级别到字符串的映射。

// log.hpp
#pragma once

#include <string>
#include <ctime>
#include <iostream>
#include <fstream>
#include <cstdarg>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>

enum LogLevel {
    Info,
    Warning,
    Error,
    Fatal,
    Debug
};

inline std::string levelToString(LogLevel level) {
    switch (level) {
        case Info: return "INFO";
        case Warning: return "WARNING";
        case Error: return "ERROR";
        case Fatal: return "FATAL";
        case Debug: return "DEBUG";
        default: return "UNKNOWN";
    }
}

2. 实现日志函数

使用可变参数实现一个通用的日志函数 logMessage,能够记录日志级别、时间戳、文件名、行号,以及用户自定义的日志内容。

// log.hpp
#define SIZE 1024

void logMessage(LogLevel level, const char *filename, int line, const char *format, ...) {
    time_t t = time(nullptr);
    struct tm *ctime = localtime(&t);
    char leftbuffer[SIZE];
    snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%02d-%02d %02d:%02d:%02d][%s:%d]",
             levelToString(level).c_str(),
             ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
             ctime->tm_hour, ctime->tm_min, ctime->tm_sec,
             filename, line);

    char rightbuffer[SIZE];
    va_list args;
    va_start(args, format);
    vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);
    va_end(args);

    char logtxt[SIZE * 2];
    snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

    printLog(level, logtxt);
}
可变参数
int sum(int n,...)  //之后会从右向左入栈
{
    va_list s;//char * 
    va_start(s,n);//s=&n+1 
}

sum函数是一个示例,它是一个可变参数函数,可以接受任意数量的整数参数,并返回它们的和。va_list类型用于遍历这些参数

sum函数中:

  1. int sum(int n,...):声明了一个可变参数函数,它接受一个整数n和任意数量的整数参数。
  2. va_list s;:声明了一个va_list类型的变量s,用于遍历可变参数列表。
  3. va_start(s, n);:初始化va_list变量s,将可变参数列表的起始地址指向n之后的第一个参数
  4. va_end(s);:清理va_list变量s,准备释放它占用的内存。

可变参数函数是C语言中非常强大的特性,它可以使代码更加通用和灵活。

// sum 函数的实例化
int sum(int n, ...) {
    va_list s;
    va_start(s, n);

    int result = 0;
    for (int i = 0; i <= n; i++) {
        result += va_arg(s, int);
    }

    va_end(s);
    return result;
}

// 示例使用 sum 函数
int main() {
    int sumResult = sum(3, 1, 2, 3);
    printf("The sum of the numbers is: %d\n", sumResult);
    return 0;
}

在这个例子中,定义了一个sum函数,它接受一个整数n和任意数量的整数参数。调用sum(3, 1, 2, 3)来实例化这个函数,它将计算1 + 2 + 3的和,并打印结果。

注意:sum函数的参数n是可选的,如果省略,则默认值为0,意味着它将接受任意数量的整数参数。在这个例子中,n的值为第一个数 3。

3. 日志输出管理

如何实现对多个日志分门别类的管理?

用'std::string _logname = path + logname' 都放到 log 中,实现管理

实现日志输出到控制台或文件的功能,支持按日志级别分类输出。

// log.hpp
enum PrintMethod {
    Screen,
    Onefile,
    Classfile
};

PrintMethod printMethod = Screen;
std::string path = "./";
std::string LogFile = "log.txt";

void printLog(LogLevel level, const std::string &logtxt) {
    switch (printMethod) {
        case Screen:
            std::cout << logtxt;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
    }
}

void printOneFile(const std::string &logname, const std::string &logtxt) {
    std::string _logname = path + logname;
    int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
    if (fd < 0) return;
    write(fd, logtxt.c_str(), logtxt.size());
    close(fd);
}

void printClassFile(LogLevel level, const std::string &logtxt) {
    std::string filename = LogFile;
    filename += ".";
    filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
    printOneFile(filename, logtxt);
}

4. 封装日志接口

定义一个简洁的宏 LOG,方便在代码中使用日志功能。

// log.hpp
#define LOG(level, format, ...) logMessage(level, __FILE__, __LINE__, format, ##__VA_ARGS__)

5. 使用日志系统

在代码中可以使用 LOG 宏来记录日志信息,示例如下:

// main.cpp
#include "log.hpp"

int main() {
    LOG(Info, "Server started successfully.");
    LOG(Warning, "This is a warning message.");
    LOG(Error, "An error occurred: %s", "file not found");
    LOG(Fatal, "Fatal error, shutting down...");
    LOG(Debug, "Debugging info: variable x = %d", 42);

    // 更改日志输出方式为按级别分类
    printMethod = Classfile;
    LOG(Info, "Server started successfully with classified logs.");

    return 0;
}

6. 管理多级别日志

通过在 printClassFile 中使用 log.txt.Debug, log.txt.Warning, log.txt.Fatal 等文件名,可以自动将不同级别的日志写入不同的文件中,方便后续查找和调试

🎢 日志管理总结

通过这种方式实现的日志系统能够灵活地处理不同级别的日志,并支持输出到控制台或文件中。通过使用 LOG,日志功能可以很方便地集成到代码中,提供有效的调试和运行时信息支持。

后续还可以不断扩展和完善,例如添加日志轮转、异步日志、网络日志等高级功能,以适应更复杂的应用场景。对于错误不用再 printf,可以直接查日志啦

3.3 进程池 2.0

命名管道,能不能也设计成我们上一篇文章讲的进程池的样子呢?

使用命名管道(FIFO)实现一个进程池可以通过以下步骤完成:

  1. 创建命名管道:使用 mkfifo 创建两个命名管道,一个用于父进程向子进程发送任务,另一个用于子进程向父进程发送结果
  2. 初始化进程池:创建子进程并为每个子进程分配命名管道。
  3. 父进程控制逻辑:父进程从标准输入获取任务,并通过命名管道将任务发送给子进程,然后读取子进程的结果。
  4. 子进程工作逻辑:子进程从命名管道读取任务,执行任务后将结果写回命名管道。

以下是使用命名管道实现进程池的代码:

channel.hpp

#pragma once

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

class channel {
public:
    channel(const std::string &cmdfifo, const std::string &outfifo, int slaverid, const std::string &processname)
        : _cmdfifo(cmdfifo), _outfifo(outfifo), _slaverid(slaverid), _processname(processname) {}

public:
    std::string _cmdfifo;     // 发送任务的命名管道
    std::string _outfifo;     // 接收结果的命名管道
    pid_t _slaverid;          // 子进程的PID
    std::string _processname; // 子进程的名字 -- 方便我们打印日志
};

tasks.hpp

#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);
}

main.cc

#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <cstdlib>
#include <ctime>
#include <sstream>
#include "channel.hpp"
#include "tasks.hpp"

void slaver(const std::vector<task_t> &tasks, const std::string &cmdfifo, const std::string &outfifo) {
    int cmdfd = open(cmdfifo.c_str(), O_RDONLY);
    int outfd = open(outfifo.c_str(), O_WRONLY);

    while (true) {
        int cmdcode = 0;
        int n = read(cmdfd, &cmdcode, sizeof(int)); // 从命令管道读取命令码
        if (n == sizeof(int)) {
            // 使用 stringstream 重定向 std::cout 到一个字符串
            std::ostringstream oss;
            std::streambuf *oldCoutStreamBuf = std::cout.rdbuf();
            std::cout.rdbuf(oss.rdbuf());

            if (cmdcode >= 0 && cmdcode < tasks.size()) {
                tasks[cmdcode]();
            }

            std::cout.rdbuf(oldCoutStreamBuf); // 恢复 std::cout 的重定向

            std::string output = oss.str();
            write(outfd, output.c_str(), output.size()); // 写到输出管道中
        }
        if (n == 0) break; // 父进程关闭写端,子进程退出
    }

    close(cmdfd);
    close(outfd);
}

void InitProcessPool(std::vector<channel> *channels, const std::vector<task_t> &tasks, int processnum) {
    for (int i = 0; i < processnum; i++) {
        std::string cmdfifo = "/tmp/cmdfifo_" + std::to_string(i);
        std::string outfifo = "/tmp/outfifo_" + std::to_string(i);
        mkfifo(cmdfifo.c_str(), 0666);
        mkfifo(outfifo.c_str(), 0666);

        pid_t id = fork();
        if (id == 0) { // child process
            slaver(tasks, cmdfifo, outfifo);
            exit(0);
        } else { // parent process
            std::string name = "process-" + std::to_string(i);
            channels->push_back(channel(cmdfifo, outfifo, id, name));
            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;
}

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

        if (select <= 0 || select >= 5) break;

        int cmdcode = select - 1;

        int cmdfd = open(channels[which]._cmdfifo.c_str(), O_WRONLY);
        write(cmdfd, &cmdcode, sizeof(cmdcode));
        close(cmdfd);

        int outfd = open(channels[which]._outfifo.c_str(), O_RDONLY);
        char buffer[256];
        int n = read(outfd, buffer, sizeof(buffer) - 1);
        if (n > 0) {
            buffer[n] = '\0';
            std::cout << buffer;
        }
        close(outfd);

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

void QuitProcess(const std::vector<channel> &channels) {
    for (const auto &c : channels) {
        unlink(c._cmdfifo.c_str());
        unlink(c._outfifo.c_str());
        waitpid(c._slaverid, NULL, 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;
}

🏷️ 解释

  1. channel 类
    • channel 类保存了命令和输出的命名管道文件路径 _cmdfifo_outfifo,以及子进程的 PID 和名字。
  1. slaver 函数
    • 子进程在 slaver 函数中运行,从命令管道读取任务码,执行任务并将结果写入输出管道。
  1. InitProcessPool 函数
    • 创建子进程并为每个子进程分配命令和输出的命名管道。
    • 使用 mkfifo 创建命名管道,并使用 fork 创建子进程。
  1. ctrlSlaver 函数
    • 父进程从标准输入获取任务码,并通过命令管道发送给子进程。
    • 通过输出管道读取子进程的结果并打印出来。
  1. QuitProcess 函数
    • 删除所有命名管道,并等待所有子进程退出。

通过这些步骤,父进程就可以通过命名管道与子进程通信,并实现一个简单的进程池啦

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

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

相关文章

【经验分享】快速制图 AI + Mermaid

文章目录 【经验分享】快速制图 AI Mermaid使用教程选图改Prompt出图 【经验分享】快速制图 AI Mermaid 使用教程 选图 在Mermaid的Diagram Syntax中&#xff0c;选择需要转换的样式图&#xff0c;复制对应的示例代码&#xff1a; 改Prompt 请根据这份文档帮我制作一份…

镜舟科技与西南证券合作,构建极速、高效数据平台

《金融科技发展规划&#xff08;2022-2025年&#xff09;》明确了高质量推进金融数字化转型的总体思路&#xff0c;云计算、人工智能等新兴技术开始被广泛应用&#xff0c;提升金融服务的便捷性&#xff0c;但随着日益增长的数据体量&#xff0c;数据的存储和处理能力日渐。 数…

【前 K 个高频元素】python刷题记录

R4-排序篇 class Solution:def topKFrequent(self, nums: List[int], k: int) -> List[int]:#使用哈希表记录值&#xff0c;再sortdictdefaultdict(int)ret[]for num in nums:dict[num]1thesorted(dict.keys(),keylambda x:dict[x], reverseTrue)for i in range(k):ret.appe…

python-docx-template实现docx模板编程

1、python-docx-template简介 python-docx库来创建word文档&#xff0c;但是对于文档的修改功能并不灵活。python-docx-template 模块主要依赖两个库&#xff0c; python-docx用于读取&#xff0c;编写和创建子文档 &#xff0c; jinja2用于管理插入到模板docx中的标签 。 其基…

90天SSL证书时代来临的应对策略-SSL证书自动化运维

SSL证书有效期长期以来都是网络安全专业人士关注的重点&#xff0c;尤其是去年3月Google在CA/B论坛投票提案中提议将SSL证书的有效期限制在90天之后&#xff0c;毕竟这对高度依赖手动管理证书的企业影响巨大。本文将概述对证书有效期缩短的理解以及应对策略。 90天是最终的有效…

Opencv-绘制几何图形

1. 绘制圆形 1.1 circle()函数原型 void cv::circle(InputOutputArray img, Point center, int radius, const Scalar & color, int thickness 1, int lineType LINE_8, int shift 0 ) img&#xff1a;需要绘制圆形的图像。 center&#xff1a;圆形的圆心位置坐标。 …

Python中的 `continue` 语句:掌握循环控制的艺术

Python中的 continue 语句&#xff1a;掌握循环控制的艺术 下滑即可查看博客内容 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地&#xff01;&#x1f387; &#x1f393; 博主简介&#xff1a;985高校的普通…

国产数据库备份恢复实现

数据库备份恢复是数据库高可用的基本能力&#xff0c;如何通过备份数据快速高效的恢复业务并且满足不同场景下的恢复需求&#xff0c;是各数据库厂商需要关注的要点。本文将介绍几种国产数据库的备份恢复功能&#xff0c;以加深了解。 1、数据库备份恢复方案 数据库备份是生产…

为什么选择在Facebook投放广告?

2024年了你还没对 Facebook 广告产生兴趣&#xff1f;那你可就亏大了&#xff01; 今天这篇文章&#xff0c;我们会分享它对你扩大业务的好处。要知道&#xff0c;Facebook 广告凭借它庞大的用户群和先进的定位选项&#xff0c;已经是企业主们有效接触目标受众的必备神器。接下…

<数据集>固定视角监控牧场绵羊识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;3615张 标注数量(xml文件个数)&#xff1a;3615 标注数量(txt文件个数)&#xff1a;3615 标注类别数&#xff1a;1 标注类别名称&#xff1a;[Sheep] 序号类别名称图片数框数1Sheep361529632 使用标注工具&#…

leetcode26_删除有序数组中的重复项

思路 双指针 func removeDuplicates(nums []int) int {if len(nums) < 2 {return len(nums)}// 双指针//区间 [0, slow] 代表已遍历且不重复元素//区间 [fast,len(nums)) 代表还未遍历的元素slow, fast : 0,1for ;fast < len(nums);{if nums[slow] ! nums[fast] {slown…

DICT运维服务目录

1、CT类项目或交付物的运维工作由网络条线负责,相关工作的SLA、流程、职责划分等按网络条线的相关制度执行 2、本服务目录主要面向IT部分的运维工作IT模式分类一级服务名称二级服务名称服务内容服务目标服务指标服务指标目标值建议工作结果呈现基础运维包增值服务指标参考费用…

【Qt】多种控件实现“hello world“

使用编辑框的方式实现"hello wordl" 使用编辑框实现"hello world"的方式有俩种&#xff1a; 单行编辑框&#xff1a;LineEdit多行编辑框&#xff1a;TextEdit 图形化界面 纯代码方式 代码展示&#xff1a; #include "widget.h" #include &qu…

Domcomp:省心好用的国外域名比价网站

先简单播报下今日奥运热点&#xff1a;1、金牌榜&#xff1a;中国29枚暂列第二&#xff1b;2、赛事预告&#xff1a;21&#xff1a;00央视5套乒乓男团决赛。言归正传&#xff0c;话说域名注册商Dynadot和NameSilo虽性价比较高&#xff0c;但也不能说域名价格每时每刻都是最低的…

RT-Thread 操作系统 之 线程间同步 IO设备模型

RT-Thread 操作系统 之 线程间同步 IO设备模型 一、线程间同步1.1、信号量1.1.1、信号量结构体1.1.2、信号量的使用和管理1.1.3、信号量同步例程 1.2、互斥量1.2.1、互斥量的使用和管理 1.3、事件集1.3.1、事件集使用和管理方法1.3.2、事件集三个线程同步实例 二、IO设备模型2.…

云手机在海外社交媒体运营中的作用

随着社交媒体的全球普及&#xff0c;海外社交媒体运营成为众多企业与个人提升品牌影响力和扩大市场份额的重要策略。在这一进程中&#xff0c;海外云手机以其独特的功能&#xff0c;为海外社交媒体运营提供了强大的支持。 那么&#xff0c;海外云手机在海外社交媒体运营中究竟扮…

UCOSIII的任务管理详解

前言 对于操作系统而言&#xff0c;最重要的就是任务的创建、挂起、删除和调度等&#xff0c;简单的创建任务可能大家都会&#xff0c;但是做大型项目的话&#xff0c;任务多了就可能需要对UCOSIII的任务管理做更深层次的一些理解。 一、任务状态 UCOSIII是单核系统&#xff…

【网络】协议,OSI参考模型,局域网通信,跨网络通信

1.协议 1.1.什么是协议/协议的由来&#xff1f; 这个就要回到我们说的阿帕网了&#xff01;&#xff01; 在阿帕网&#xff08;ARPA&#xff09;产生运作之初&#xff0c;通过接口信号处理机实现互联的电脑并不多&#xff0c;大部分电脑相互之间不兼容。 在一台电脑上完成的工…

贪心/前后缀优化dp,CF 575F - Bulbo

一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 575F - Bulbo 二、解题报告 1、思路分析 F1 前后缀分离优化dp 点 的值域很大&#xff0c;但是线段数目很少&#xff0c;只有5000量级 也就是说最多有10000 个 不同的点 我们将所有点从小到大排序得到点集…

【python】OpenCV—Greedy Snake

文章目录 1、代码实现2、涉及到的库——cv2.setWindowProperty 1、代码实现 import cv2 import numpy as np from random import choiceclass SnakePart:def __init__(self, front, x, y):self.front frontself.x xself.y ydef move(self):# 跟随它前面的部分移动self.x s…