linux进程间通信——学习与应用命名管道, 日志程序的使用与实现

news2024/9/21 2:14:55

        前言:本节主要讲解linux进程间通信里面的命名管道, 命名管道和我们学过的匿名管道是类似的。 博主将会带着友友们先看一下原理, 然后就会着手使用以下命名管道是怎么使用的。 最后我们还会试着引入日志系统, 我们从本节开始就会引入日志这个东西了!以后我们可能会将本节的日志拿过来用, 所以友友们要着重学习了!!

        ps:本节内容建议学习了匿名管道的友友们进行观看!

命名管道原理

         具有血缘关系的进程进行进程间通信, 如果毫不相关的进程进行进程间通信呢? 这个时候就用到了mkfifo mkfifo可以创建出一种管道文件这种文件是一种命名管道可以用来让不相关的进程之间进行通信。 

这里我们创建一个以myfifo为文件名的命名管道:

        然后我们就可以看到这个以p开头的文件, 叫做命名管道。 并且这个管道是在磁盘上面的。 

        但是, 我们如果向这个管道之中写入数据, 就会阻塞住:

        所以呢,我们如果再打开一个终端, 在我们的管道中去读取, 就会看到读取到了内容, 并且不会再被阻塞住了!!! mkfifo管道文件不会保存数据, 他只相当于一个中转层。 

        理解:如果两个不同的进程, 打开同一个文件的时候, 在内核中, 操作系统会打开几个文件? 那么我们想一下, 对于操作系统来说, 如果两个进程打开同一个文件,首先文件的struct file因为是一样的, 所以就不需要写两份, 只需要写一份。 而我们的文件的inode, operators, 文件缓冲区这些操作系统创建几份呢? 操作系统会不会担心错乱呢? ——答案是只会创建一份,并且不会错乱的, 因为操作系统认为两个进程都打开一个文件了, 用户都不怕发生冲突, 操作系统更不怕发生冲突!!!

        所以, 我们的两个不同进程打开同一个文件本质上也是这一张图:

如何理解mkfifo

        进程间通信的前提:先让不同进程看到同一份资源——也就是struct file。 那么问题来了, 为什么我们不去直接让文件去磁盘里面刷盘呢?要知道, 我们只需要文件缓冲区就可以实现数据的修改与存储。 那么他就不需要再去文件去刷盘了!!!所以说, mkfifo的文件, 是一个内存级别的文件。

        那么, 我们怎么保证, 我们打开的是同一个文件呢? 以及, 我们为什么要打开同一个文件呢? ——看到同一个路径下面的同一个文件名 = 路径 + 文件名(路径 + 文件名具有唯一性!!!) ——也就是说, 命名管道, 是利用使用路径 + 文件名的方案, 让不同的进程, 看到同一份文件资源, 进而实现不同文件之间的通信的!!!

        那么问题来了, 命名管道能不能也设计成我们之前写的进程池的样子呢?——可以的!只是不需要创建子进程,直接使用mkfifo创建就好了!!!

        命名管道和普通管道是一样的——都会面向字节流, 进程同步与互斥, 生命周期随进程, 当使用时需要打开一个写端, 一个读端, 单向通信等等。 不同点就是命名管道可以作用于没有血缘关系的进程之间进行通信。 

日志

        对于日志, 博主也不太熟悉, 也没有查过相关资料。 这里只是谈一些本篇内容需要用到的东西:

        日志包括: 日志的内容包括日志的时间日志的等级日志的内容文件的名称行号等。

        其中, 日志时间, 日志等级, 日志内容一般的日志都会有。 并且日志的等级一般有以下几个等级:

  • Info:常规消息。
  • Waring:报警信息, 不影响整个代码的向后执行, 但是需要让用户知道, 否则可能会引发一些问题。
  • Error:比较严重的问题, 可能需要立即处理, 可能不影响继续向后进程, 比如飞机起飞, 机长就广播消息, 这些消息, 是Info消息; 当飞机起飞时, 这个时候飞机可能会抖动, 可能会遇到强气流等等——这就是Waring;后来飞机的发动机出问题了, 这个问题非常严重了, 虽然飞机仍旧可以飞行, 但是可能下一秒就会出事故。——这就是Error
  • Fatal:致命的 当程序遇到致命问题时, 如果不解决, 程序无法向后执行。
  • Debug:调试问题。

创建文件

首先我们要创建下面这五个文件,其中Client.cpp是用于客户端输入, Server.cpp是用于服务端读入。 然后Log.hpp是用于日志文件。

makefile

make用来生成客户端和服务端的可执行程序

.PHONY:all
all: server.exe client.exe

server.exe:Server.cpp
	g++ -o $@ $^ -g -std=c++11

client.exe:Client.cpp
	g++ -o $@ $^ -g -std=c++11

.PHONY:clean
clean:
	rm -f server.exe client.exe

Com.hpp头文件准备

        我们在Com.hpp可以先枚举一下管道操作的错误类型。 

enum
{
    FIFO_CREAT_ERR = 1,   //创建出错的错误码
    FIFO_DELE_ERR = 2,    //删除出错的错误码
    FIFO_OPEN_ERR = 3     //打开出错的错误码
};

        我们打开创建命名管道的时候需要指定命名管道的文件名, 所以我们可以定义一个FIFO_FILE作为文件名, 然后定义一个MODE当作创建文件名的时候的权限码

#define FIFO_FILE "./myfifo"
#define MODE 0660

       然后我们就可以定义一个class类, 类的构造函数用来创建管道, 类的析构函数用来删除管道。 ——这是一种设计, 当我们以后创建管道的时候, 只需要定义一个对象,这个时候管道就创建好了, 并且当这个对象析构的时候, 管道文件就删除了!!!

class Init
{
public:
    Init()
    {
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1) 
        {
            perror("mkfifo");
            exit(FIFO_CREAT_ERR);
        }

    }

    ~Init()
    {
        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELE_ERR);
        }
    }

};

 ps:头文件包含哪一个博主没有说, 友友们自己查一下man手册即可!

        以下是整个Com.hpp的文件代码

#pragma once
#include <iostream>
using namespace std;
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include<unistd.h>

#define FIFO_FILE "./myfifo"
#define MODE 0660

enum
{
    FIFO_CREAT_ERR = 1,
    FIFO_DELE_ERR = 2,
    FIFO_OPEN_ERR = 3
};

class Init
{
public:
    Init()
    {
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1) 
        {
            perror("mkfifo");
            exit(FIFO_CREAT_ERR);
        }

    }

    ~Init()
    {
        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELE_ERR);
        }
    }

};

日志文件的准备

         日志文件是本节很重要的一块。 我们需要注意:

#define部分

        先定义好日志等级

#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4

        定义打印方式——分为在显示器上打印, 在一个文件里面打印,在多个文件里面打印:

#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4

        定义打印到目标文件的文件名

#define LogFile "log.txt"

        我们打印日志文件到管道里面, 所以一定要有日志文件的缓冲区, 所以这里要定义一个缓冲区的最大大小:

#define SIZE 1024

定义类 

//然后我们定义一个日志的class类:

class Log
{
public:
    Log()
    {
        printStyle = Screen;    //默认打印到屏幕上
        path = "./log/";        //默认的路径是./log/
    }

public:
    int printStyle; //打印风格
    string path; //打印到那个路径下面
};

 我们重载()来当作打印日志内容:

    void operator()(int level, const char* format, ...)
    {
        //内容先不写
    }

LevelToString 

重载()里面的内容我们先不写, 因为这里面会用到许多别的接口。 我们先实现一下别的接口, 首先就是我们知道打印日志内容需要日志等级, 而日志等级我们在上面define成了数字码0,1, 2, 3等。 所以我们接受日志等级使用的是int, 现在我们打印日志等级不能只打印数字, 所以我们还要将这个数字转变成string类型。 所以, 我们可以实现一个LevelToString函数, 用来将数字转变为string串:

    string LevelToString(int level)
    {
        switch(level)
        {
            case Info:
                return "Info";
            case Debug:
                return "Debug";
            case Waring:
                return "Waring";
            case Error:
                return "Error";
            default:
                return "None";
        }
    }

        有了这个接口之后我们就可以着手实现重载()了。

operator()

        重载()有两个步骤, 第一个步骤是将要打印的数据先保存到缓冲区中。 第二个步骤是将缓冲区的数据打印到日志文件里面。 

        我们先实现第一个步骤:

    void operator()(int level, const char* format, ...)
    {
        //第一个步骤
        //获取时间
        time_t t = time(nullptr); 
        struct tm* ctime = localtime(&t); 
        //先将日志等级和时间放到一个缓冲区之中
        char leftbuffer[SIZE];   
        snprintf(leftbuffer, sizeof(leftbuffer), 
        "[%s][%d-%d-%d-%d-%d-%d]", LevelToString(level).c_str(), ctime->tm_year
        ,ctime->tm_mon, ctime->tm_yday, ctime->tm_hour, ctime->tm_min
        , ctime->tm_sec);    //向缓冲区中打印数据

        //再向要打印的内容放到第二个缓冲区之中
        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format,s); //
        va_end(s);

        //合并两个缓冲区
        char logtxt[SIZE * 2]; //文本缓冲区, 用来将两个文本文件合起来;
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer
        , rightbuffer);
        

        //第二个步骤
        printLog(logtxt);
    }

printLog

        我们将第二个步骤用一个printLog函数封装。为什么printLog函数要传送日志等级呢? 我们前面不是已经打印了日志等级了吗? 难道还要再打印一遍吗?——不是的, 因为我们的打印日志的时候, 定义的是三种打印方式, 一种Screen在屏幕上面打印, 一种是Onefile在一个文件里面打印, 一种是Classfile在多个文件里面打印,当我们在多个文件里面打印的时候我们就可以使用level来区分到底去哪个文件里面打印了。下面我们实现printfLog:

    void printLog(int level, const string logtxt)
    {
        switch(printStyle)
        {
            case Screen:
                cout << logtxt << endl;
                break;
            case Onefile:
                printOneFile(LogFile, logtxt); //向文件里面打, 这个文件名定义的是LogFile            
                break;
            case Classfile:
                printClassFile(level, logtxt);//想多个文件里面打印, 根据level区分去哪个文件里面打印
                break;
            default:
                break;
        }
    }

         上面的printOneFile用来封装向一个文件里面打印的代码; printClassFile用来封装向多个文件打印到代码。

printOneFile

        向一个文件里面写就是很标准的文件写入, 先打开文件获取fd, 然后判断文件是否打开成功。 如果成功就像里面写入数据。最后关闭文件。 ——值得一提的是, 这里的logname是打印到的目标文件的文件名。 我们在前面加上path(这个path是./log/)是为了创建一个文件夹, 以后打印数据都向当前目录的log目录下创建文件进行打印数据。


    void printOneFile(const string& logname, const string& logtxt)
    {
        string _logname = path + logname;   //打印到的文件路径
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0660);
        if (fd < 0) return;
        write(fd, logtxt.c_str(), logtxt.size()); //文件描述符,文件源, 文件大小
        close(fd);
    }

printClassFile

        打印到多个文件夹我们就可以根据level转换成字符串的不同, 创建出不同的文件名。 再将数据打印到这些文件名的文件夹中:

    void printClassFile(int level, const string& logtxt)
    {
        //创建文件夹的文件名
        string filename = LogFile;
        filename += ".";
        filename += LevelToString(level);

        printOneFile(filename, logtxt);  //然后去这个文件里打印数据
    }

Enable

        最后我们再加一个用来修改打印方式的函数, 日志系统就完成了

    void Enable(int method)
    {
        printStyle = method;
    }

下面是真个日志系统全部代码
全部代码:

#pragma once
#include<iostream>
using namespace std;
#include<stdarg.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printStyle = Screen;    //默认打印到屏幕上
        path = "./log/";        //默认的路径是./log/
    }

    void Enable(int method)
    {
        printStyle = method;
    }

    void printOneFile(const string& logname, const string& logtxt)
    {
        string _logname = path + logname;   //打印到的文件路径
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0660);
        if (fd < 0) return;
        write(fd, logtxt.c_str(), logtxt.size()); //文件描述符,文件源, 文件大小
        close(fd);
    }

    void printClassFile(int level, const string& logtxt)
    {
        //创建文件夹的文件名
        string filename = LogFile;
        filename += ".";
        filename += LevelToString(level);

        printOneFile(filename, logtxt);  //然后去这个文件里打印数据
    }


    void printLog(int level, const string logtxt)
    {
        switch(printStyle)
        {
            case Screen:
                cout << logtxt << endl;
                break;
            case Onefile:
                printOneFile(LogFile, logtxt); //向文件里面打, 这个文件名定义的是LogFile            
                break;
            case Classfile:
                printClassFile(level, logtxt);
                break;
            default:
                break;
        }
    }


    void operator()(int level, const char* format, ...)
    {
        //获取时间
        time_t t = time(nullptr); 
        struct tm* ctime = localtime(&t); 
        char leftbuffer[SIZE];   

        snprintf(leftbuffer, sizeof(leftbuffer), 
        "[%s][%d-%d-%d-%d-%d-%d]", LevelToString(level).c_str(), ctime->tm_year
        ,ctime->tm_mon, ctime->tm_mday, ctime->tm_hour, ctime->tm_min
        , ctime->tm_sec);    //向缓冲区中打印数据

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];

        vsnprintf(rightbuffer, sizeof(rightbuffer), format,s); //
        va_end(s);

        char logtxt[SIZE * 2]; //文本缓冲区, 用来将两个文本文件合起来;
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer
        , rightbuffer);

        //将要打印的数据放到缓冲区里面后就可以打印数据了
        printLog(level, logtxt);
    }

    string LevelToString(int level)
    {
        switch(level)
        {
            case Info:
                return "Info";
            case Debug:
                return "Debug";
            case Waring:
                return "Waring";
            case Error:
                return "Error";
            default:
                return "None";
        }
    }


public:
    int printStyle; //打印风格
    string path; //打印到那个路径下面
};

Sever端

接下来准备我们的Sever端, 这里我们写日志, 我们是向不同的文件中打印(因为测试向不同的文件中打印成功的话, 向显示器, 向单个文件中打印一定可以成功)

#include"Com.hpp"
#include"Log.hpp"

int main()
{
    Init init; //创建管道和删除管道
    Log log;   //日志系统
    log.Enable(Classfile);  //向不同的文件中打印

    return 0;
}

        然后我们打开信道——这个打开信道和创建管道是不同的, 创建管道只是将我们的命名管道文件给创建出来。 而我们的打开信道就是打开这个管道文件, 并且我们现在写的是服务端, 所以打开管道的方式是只读, 也就是O_RDONLY;然后报错信息等等都通过日志系统的()重载打印到日志系统之中。

#include"Com.hpp"
#include"Log.hpp"

int main()
{
    Init init; //创建管道和删除管道
    Log log;   //日志系统
    log.Enable(Classfile);  //向不同的文件中打印

//打开信道
    int fd = open(FIFO_FILE, O_RDONLY); //Sever端是读取
    if (fd < 0)
    {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }



//开始通信
    log(Info, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log(Waring, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log(Error, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    while (true)
    {
        char buffer[1024];
        int x = read(fd, buffer, sizeof(buffer));
        if (x > 0)
        {
            buffer[x] = 0;
            cout << "client say#: " << buffer << endl;
        } 
        else if (x == 0) 
        {
            log(Fatal, "client quit, me too!!! error string: %s, error code: %d", strerror(errno), errno);
            break;
        }
        else break;
    }
    close(fd);

    return 0;
}

Client端

我们的客户端同样是打开管道文件, 同时以写的方式打开。 然后我们创建缓冲区,将数据写到缓冲区中, 再从缓冲区通过write函数写到文件里面

#include"Com.hpp"
#include"Log.hpp"

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 Entering#: ";
        getline(cin, line); //向缓冲去中写入数据

        write(fd, line.c_str(), line.size());
    }

    close(fd);
    return 0;
}

运行测试

        写完所有的代码之后我们就可以运行测试一下了。 首先我们打开两个终端:

        先在目录下面创建一个log文件, 用来给日志系统打印数据。

        然后运行先运行我们的服务端, 再运行客户端——一定先运行服务端。 因为如果先运行客户端, 我们的客户端打开的管道文件和我们的服务端打开的管道文件就不是同一个管道文件——打开后如下:

然后我们每向客户端输入一条数据, 服务端就会打印一条数据

并且我们在客户端ctrl + c后,服务端读取到0, 那么久else if判断, 程序就退出了!

然后我们打开我们的日志文件, 就写入了许多数据: 

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!

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

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

相关文章

npm 安装 与 切换 淘宝镜像

一、镜像源 npm默认镜像源是国外的&#xff0c;安装依赖速度较慢&#xff0c;使用国内的镜像源速度会快一些。 1、设置淘宝镜像源&#xff1a; #最新地址 淘宝 NPM 镜像站喊你切换新域名啦! npm config set registry https://registry.npm.taobao.org&#xff08;弃用了&…

网站采用H5+CSS3开发的优势和劣势

在现代网站开发中&#xff0c;HTML5和CSS3的结合使用已经成为一种趋势。以下是其优势和劣势的介绍&#xff1a; 优势 增强的多媒体支持&#xff1a;HTML5引入了新的标签&#xff0c;使开发者能够轻松嵌入音频、视频和图形&#xff0c;无需依赖第三方插件如Flash。这大大简化了…

【AI大模型】ChatGPT模型原理介绍(下)

目录 &#x1f354; GPT-3介绍 1.1 GPT-3模型架构 1.2 GPT-3训练核心思想 1.3 GPT-3数据集 1.4 GPT-3模型的特点 1.5 GPT-3模型总结 &#x1f354; ChatGPT介绍 2.1 ChatGPT原理 2.2 什么是强化学习 2.3 ChatGPT强化学习步骤 2.4 监督调优模型 2.5 训练奖励模型 2.…

基于单片机的风机故障检测装置的设计与实现(论文+源码)

1 系统总体设计方案 通过对风机故障检测装置的设计与实现的需求、可行性进行分析&#xff0c;本设计风机故障检测装置的设计与实现的系统总体架构设计如图2-1所示&#xff0c;系统风机故障检测装置采用STM32F103单片机作为控制器&#xff0c;并通过DS18B20温度传感器、ACS712电…

macOS使用brew安装并配置python环境

1.确认已安装brew环境,如没有安装,参考: macOS系统Homebrew工具安装及使用-CSDN博客 2.安装python python安装成功 3.添加pip路径到/etc/paths 4.查看python与pip默认安装版本

【leetcode】树形结构习题

二叉树的前序遍历 返回结果&#xff1a;[‘1’, ‘2’, ‘4’, ‘5’, ‘3’, ‘6’, ‘7’] 144.二叉树的前序遍历 - 迭代算法 给你二叉树的根节点 root &#xff0c;返回它节点值的 前序 遍历。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,…

git 更换远程地址的方法

需要将正在开发的代码远程地址改成新的地址&#xff0c;通过查询发现有三个方法可以实现&#xff0c;特此记录。具体方法如下&#xff1a; &#xff08;1&#xff09;通过命令直接修改远程仓库地址 git remote 查看所有远程仓库git remote xxx 查看指定远程仓库地址git remote…

外卖会员卡是不是一个骗局?

大家好&#xff0c;我是鲸天科技千千&#xff0c;大家都知道我是做小程序开发的&#xff0c;平时会给大家分享一些互联网相关的创业项目&#xff0c;感兴趣的可以跟我关注一下。 首先就是要搭建一个自己的外卖会员卡系统小程序&#xff0c;我们自己的工作就是把这个小程序推广…

JDBC注册驱动及获取连接

文章目录 1. JDBC注册驱动1.1 导入驱动 Jar 包1.2 注册驱动1.2.1 API介绍1.2.2 使用步骤1.2.3 案例代码 2. 获取连接2.1 API介绍2.2 参数说明2.3 注意事项2.4 使用步骤3.5 案例代码 1. JDBC注册驱动 Connection表示Java程序与数据库之间的连接&#xff0c;只有拿到Connection才…

TCP/IP网络模型分层

应用层 应用层是最上层的&#xff0c;也就是我们能直接接触到的就是应用层(Application Layer),手机和电脑上的应用软件都是在应用层实现。当两个不同设备的应用需要通信的时候&#xff0c;应用就会把数据传输给下一层&#xff0c;也就是传输层 所以&#xff0c;应用层只需要…

PMP--一模--解题--91-100

文章目录 13.干系人管理91、 [单选] 在项目执行期间&#xff0c;一名外部干系人反对一项重大范围变更。除非重新评估干系人的决定&#xff0c;否则项目进展将受到影响。项目经理下一步该怎么做&#xff1f; 5.范围管理92、 [单选] 一客户给你一复杂项目的采购工作说明书&#x…

「数组」堆排序 / 大根堆优化(C++)

目录 概述 核心概念&#xff1a;堆 堆结构 数组存堆 思路 算法过程 up() down() Code 优化方案 大根堆优化 Code(pro) 复杂度 总结 概述 在「数组」快速排序 / 随机值优化|小区间插入优化&#xff08;C&#xff09;中&#xff0c;我们介绍了三种基本排序中的冒泡…

数学学习记录

9月14日 1.映射&#xff1a; 2.函数: 9月15日 3.反函数&#xff1a; 4.收敛数列的性质 5.反三角函数&#xff1a; 9月16日 6.函数的极限&#xff1a; 7.无穷小和无穷大 极限运算法则&#xff1a;

MySQL_简介及安装、配置、卸载(超详细)

课 程 推 荐我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448;入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448;虚 拟 环 境 搭 建 &#xff1a;&#x1…

小麦病害检测数据集【‘细菌叶斑病‘, ‘褐斑病‘, ‘叶瘤病‘】

小麦病害检测数据集】nc3 标签names:[Bacteria Leaf Blight,Brown Spot, Leaf smut] 名称&#xff1a;【细菌叶斑病, 褐斑病, 叶瘤病】共6715张&#xff0c;8:1:1比例划分&#xff0c;&#xff08;train;5372张&#xff0c;val&#xff1a;671张&#xff0c;test&#xff1a;67…

【AI视频】复刻抖音爆款AI数字人作品初体验

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AI视频 | AI数字人 文章目录 &#x1f4af;前言&#x1f4af;抖音上的爆火AI数字人视频&#x1f4af;注册HeyGen账号&#x1f4af;复刻抖音爆款AI数字人&#x1f4af;最终生成效果&#x1f4af;小结 对比原视频效果&#xff1a;…

JVM面试真题总结(十一)

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 解释双亲委派模型及其优势 双亲委派模型是Java类加载器的一个重要…

Qt:静态局部变量实现单例(附带单例使用和内存管理)

前言 本文主要写静态局部变量实现的单例以及单例的释放&#xff0c;网上很多教程只有单例的创建&#xff0c;但是并没有告诉我们单例的内存管理&#xff0c;这就很头疼。静态局部变量实现的单例应该是最简单的单例&#xff0c;而且不需要内存管理。 正文 静态局部变量单例&a…

微服务实战系列之玩转Docker(十五)

前言 博主的玩转Docker系列&#xff0c;今天正式开启第十五篇的征程&#xff01; 在过去的十四篇中&#xff0c;涉及的内容有知识、有原理、有工具、更有实践。当你打开每一篇文章时&#xff0c;均会获得一个特定主题的知识和技巧&#xff0c;助你在云原生的世界里&#xff0c…

C++实现unordered_map和unordered_set

1. 结构示意 2. 模版参数控制 我们知道&#xff0c;unordered_set和unordered_map与set和map是一样的&#xff0c;前者不是真正的键值对&#xff0c;它的value值和key值相同&#xff1b;后者是真正的键值对。STL非常注重代码的复用&#xff0c;它们在底层使用了同一棵红黑树模…