【Linux】进程间通信 -- 匿名管道的应用

news2024/11/16 10:29:50

前言

上篇博客初步学习了匿名管道的周边知识和使用,本篇文章将基于这些知识,实现一下进程间通信
话不多说,马上开始今天的内容

在这里插入图片描述

文章目录

  • 前言
  • 一. 大体框架
  • 二. 分配任务
  • 三. 创建控制模块
  • 四. 开始通信
  • 五. 关闭程序
  • 六. 完整代码
  • 结束语

一. 大体框架

在这里插入图片描述
我们创建一个进程,以这个进程为父进程,创建5个子进程和对应的管道,父进程进行写操作,子进程进行读操作,根据父进程写入的数据,进行相应的动作。

部分细节,变量在单独部分没有展示,可以查看完整代码部分

二. 分配任务

这个部分我们可以编写在Task.hpp

#pragma once

#include<iostream>
#include<vector>
#include<unistd.h>

//函数指针
typedef void(*fun_t)();

void PrintLog()
{
    std::cout<<"pid:"<<getpid()<<" 打印日志任务,正在被执行"<<std::endl;
}

void InsertMySQL()
{
    std::cout<<"pid:"<<getpid()<<" 执行数据库任务,正在被执行"<<std::endl;
}

void NetRequest()
{
    std::cout<<"pid:"<<getpid()<<" 执行网络请求任务,正在被执行"<<std::endl;
}

//数字对应的指令
#define COMMAND_LOG 1
#define COMMAND_MYSQL 2
#define COMMAND_REQUEST 3

class Task
{
public:
    Task()
    {
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }

    void Execute(int command)
    {
        //commend是执行第几个命令
        if(command>=0&&command<funcs.size())
        {
            funcs[command]();
        }
    }

    ~Task()
    {

    }
public:
    std::vector<fun_t>funcs;
};

我们使用函数指针的方式。定义一个Task类,内部有一个存储函数指针的vector,并且我们在其构造函数中,就添加以上三个任务。然后还有一个接口,通过传入一个数字command,然后执行对应vector里第几个的任务。

三. 创建控制模块

从这部分开始,我们编写在ctrlProcess.cpp中

匿名管道用于具有“亲戚关系”的进程,常用的就是父子进程,而我们需要让子进程有父进程创建的管道,所以需要先创建管道,然后再创建子进程,这样因为写时拷贝,子进程就会继承父进程的部分进程信息,当然包括文件描述符。
然后成功创建一个子进程后,父进程要先关闭当前管道的读端,这样不会影响下一次的创建。
同时,因为我们要创建多个子进程,而每次创建的管道,都使用同一个数组,存储其读写文件描述符,所以我们使用一个,内部存储子进程的名称,子进程的pid,子进程对应的管道的写端,因为父进程需要向该写端写入数据。这样也符合,先描述,再组织的思想。

我们将这部分封装成一个函数,将每一步操作封装起来,这样也可以让代码更有逻辑性,可读性更好。

子进程的waitCommand函数将在下一部分讲解,因为waitCommand是读取管道数据,属于通信部分。

// 用于存储子进程的pid和相对应管道的文件描述符
class EndPoint
{
    //计数器
    static int number;
public:
    EndPoint(pid_t child_id, int write_id)
        : _child_id(child_id), _write_id(write_id)
    {
        //进程名的格式:process-0[pid,fd]
        char namebuffer[64];
        snprintf(namebuffer,sizeof(namebuffer),"process-%d[%d:%d]",number++,_child_id,_write_id);
        processname=namebuffer;
    }

    std::string name() const
    {
        return processname;
    }

    ~EndPoint()
    {
    }

public:
    pid_t _child_id; // 子进程的pid
    int _write_id;   // 相对应管道的文件描述符
    std::string processname; //进程的名字 
};
int EndPoint::number=0;


// 构建控制结构,父进程写入,子进程读取
void createProcesses(vector<EndPoint> &end_points)
{
    // 1.先进行构建控制结构:父进程进行写入,子进程读取
    for (int i = 0; i < gnum; i++)
    {
        // 1.1 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void )n; // 防止release版本将为使用的变量删除

        // 1.2 创建子进程
        pid_t id = fork();
        assert(id != -1);
        (void )id; // 防止release版本将为使用的变量删除

        if (id == 0)
        {
            // 子进程

            //将从父进程那继承来的其他进程的读端关闭
            cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端:";
            for(const auto&ep:end_points)
            {
                cout<<ep._write_id<<" ";
                close(ep._write_id);
            }
            cout<<endl;

            // 关闭自己的写端
            close(pipefd[1]);

            // 1.3 通信

            // 子进程读取“指令”,都从标准输出中获取
            // 将管道的读重定向到标准输出中
            dup2(pipefd[0], 0);

            // 1.4 子进程开始等待命令。
            WaitCommand();

            // 关闭读端然后退出子进程
            close(pipefd[0]);
            exit(0);
        }

        // 到这的一定是父进程

        // 关闭读端
        close(pipefd[0]);

        // 将新创建的子进程的fd和管道的写的文件描述符存储起来
        end_points.push_back(EndPoint(id, pipefd[1]));
    }
}

但这里,我们还需要注意一个事项,就是子进程创建成功后,还有一个循环

//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端"<<endl;
for(const auto&ep:end_points)
{
	cout<<ep._write_id<<" ";
    close(ep._write_id);
}
cout<<endl;

这一步我们在最后的退出程序再详细讲解。

四. 开始通信

我们现在已经创建好父子进程,并且还存储好了子进程的pid和对应管道的写端的文件描述符。
接下来,我们就可以开始通信了。

父进程往管道写入

//展示面板
int ShowBoard()
{
    cout<<endl;
    cout<<"#######################################"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"# 0. 执行日志任务   1. 执行数据库任务 #"<<endl;
    cout<<"# 2. 执行请求任务   3. 退出           #"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"请选择# ";

    int command=0;
    std::cin>>command;

    return command;
}

//父进程写入
void ctrlProcess(const vector<EndPoint>&end_points)
{
    // 父进程开始发布命令
    int cnt=0;
    while(true)
    {
        //1. 选择任务
        int command=ShowBoard();
        //为3就退出
        if(command==3)
        {
            break;
        }

        if(command<0&&command>2)
        {
            cout<<"输入有误,请重新输入"<<endl;
            continue;
        }

        //2. 按顺序给子进程派发任务
        int indix=cnt++;
        cnt%=end_points.size();

        cout<<"你选择了进程:"<<end_points[indix].name()<<" | 处理"<<command<<"号任务"<<endl;

        //4. 下发任务
        write(end_points[indix]._write_id,&command,sizeof(command));
        
        sleep(1);
    }
}

子进程读取管道,获取数据,并执行相应任务

// 子进程读数据
void WaitCommand()
{
    while(true)
    {
        int command;
        //一次读取4个字节
        int n = read(0, &command, sizeof(int));
        //成功读取4字节,就执行对应的命令
        if (n == sizeof(int))
        {
            t.Execute(command);
            cout<<endl;
        }
        else if (n == 0)
        {
            //相对应的写端关闭了
            cout<<"父进程让我退出,我就退出了"<<getpid()<<endl;
            break;
        }
    }
}

五. 关闭程序

在关闭程序时,我们要结束子进程,只需要将对应的写端关闭,子进程读取到文件尾,就会自动退出循环,结束进程。
然后父进程还需要回收子进程的僵尸状态。
不过这里我们要讲解第二步. 分配任务时的一个疑问

为什么需要下面这个循环

//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端"<<endl;
for(const auto&ep:end_points)
{
	cout<<ep._write_id<<" ";
    close(ep._write_id);
}
cout<<endl;

我们知道,子进程会继承父进程所有的文件描述符,那么当我们创建第二个子进程前,父进程是有第一个子进程管道的写端的。所以第二个子进程同样会继承这个文件描述符,这样就导致,我们创建越多的子进程,前面的子进程的管道的链接数越多引用计数不为1,这样顺序一个一个关闭时,无法关闭子进程的写端,子进程就不会读到文件尾,而是处于阻塞状态,不会退出进程,父进程就回收不到子进程了。
所以我们有三种解决这个问题的办法

解决方法一:
我们可以一次性将所有子进程的写端都关闭,再回收子进程

	//1.关闭子进程的写端
    for(const auto&ep:end_points)
    {
        close(ep._write_id);
    }

    sleep(5);
    cout<<"父进程让所有的子进程都退出"<<endl;
    
    //2. 父进程回收子进程的僵尸状态
    for(const auto&ep:end_points)
    {
        waitpid(ep._child_id,nullptr,0);
    }

    cout<<"父进程回收了所有的子进程"<<endl;
    sleep(5);

解决方法二:
我们可以倒着关闭子进程的写端,然后再回收子进程

	//倒着关闭子进程的写端,再回收子进程
    for(int i=end_points.size()-1;i>=0;i--)
    {
        close(end_points[i]._write_id);
        cout<<"父进程让"<<end_points[i]._child_id<<"子进程退出"<<endl;

        waitpid(end_points[i]._child_id,nullptr,0);
        cout<<"父进程回收了"<<end_points[i]._child_id<<"子进程"<<endl;
        cout<<endl;

        sleep(1);
    }

解决方法三:
在新的子进程创建后,关闭从父进程那继承的其他子进程的管道的文件描述符,就是那个循环,
然后我们就可以顺序的,一个一个关闭写端并回收了

//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端"<<endl;
for(const auto&ep:end_points)
{
	cout<<ep._write_id<<" ";
    close(ep._write_id);
}
cout<<endl;

//一个一个退出
for(const auto&ep:end_points)
{
	close(ep._write_id);
    cout<<"父进程让"<<ep._child_id<<"子进程退出"<<endl;

    waitpid(ep._child_id,nullptr,0);
    cout<<"父进程回收了"<<ep._child_id<<"子进程"<<endl;
    cout<<endl;

    sleep(1);
}

六. 完整代码

Task.hpp

#pragma once

#include<iostream>
#include<vector>
#include<unistd.h>

//函数指针
typedef void(*fun_t)();

void PrintLog()
{
    std::cout<<"pid:"<<getpid()<<" 打印日志任务,正在被执行"<<std::endl;
}

void InsertMySQL()
{
    std::cout<<"pid:"<<getpid()<<" 执行数据库任务,正在被执行"<<std::endl;
}

void NetRequest()
{
    std::cout<<"pid:"<<getpid()<<" 执行网络请求任务,正在被执行"<<std::endl;
}

//数字对应的指令
#define COMMAND_LOG 1
#define COMMAND_MYSQL 2
#define COMMAND_REQUEST 3

class Task
{
public:
    Task()
    {
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }

    void Execute(int command)
    {
        //commend是执行第几个命令
        if(command>=0&&command<funcs.size())
        {
            funcs[command]();
        }
    }

    ~Task()
    {

    }
public:
    std::vector<fun_t>funcs;
};

ctrlProcess.cpp

#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <vector>
#include <sys/types.h>
#include <sys/wait.h>
#include "task.hpp"

using namespace std;

const int gnum = 3;
Task t; // 定义为全局的

// 用于存储子进程的pid和相对应管道的文件描述符
class EndPoint
{
    //计数器
    static int number;
public:
    EndPoint(pid_t child_id, int write_id)
        : _child_id(child_id), _write_id(write_id)
    {
        //进程名的格式:process-0[pid,fd]
        char namebuffer[64];
        snprintf(namebuffer,sizeof(namebuffer),"process-%d[%d:%d]",number++,_child_id,_write_id);
        processname=namebuffer;
    }

    std::string name() const
    {
        return processname;
    }

    ~EndPoint()
    {
    }

public:
    pid_t _child_id; // 子进程的pid
    int _write_id;   // 相对应管道的文件描述符
    std::string processname; //进程的名字 
};
int EndPoint::number=0;

// 子进程读数据
void WaitCommand()
{
    while(true)
    {
        int command;
        //一次读取4个字节
        int n = read(0, &command, sizeof(int));
        //成功读取4字节,就执行对应的命令
        if (n == sizeof(int))
        {
            t.Execute(command);
            cout<<endl;
        }
        else if (n == 0)
        {
            //相对应的写端关闭了
            cout<<"父进程让我退出,我就退出了"<<getpid()<<endl;
            break;
        }
    }
}

// 构建控制结构,父进程写入,子进程读取
void createProcesses(vector<EndPoint> &end_points)
{
    // 1.先进行构建控制结构:父进程进行写入,子进程读取
    for (int i = 0; i < gnum; i++)
    {
        // 1.1 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void )n; // 防止release版本将为使用的变量删除

        // 1.2 创建子进程
        pid_t id = fork();
        assert(id != -1);
        (void )id; // 防止release版本将为使用的变量删除

        if (id == 0)
        {
            // 子进程

            //将从父进程那继承来的其他进程的读端关闭
            cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端:";
            for(const auto&ep:end_points)
            {
                cout<<ep._write_id<<" ";
                close(ep._write_id);
            }
            cout<<endl;

            // 关闭自己的写端
            close(pipefd[1]);

            // 1.3 通信

            // 子进程读取“指令”,都从标准输出中获取
            // 将管道的读重定向到标准输出中
            dup2(pipefd[0], 0);

            // 1.4 子进程开始等待命令。
            WaitCommand();

            // 关闭读端然后退出子进程
            close(pipefd[0]);
            exit(0);
        }

        // 到这的一定是父进程

        // 关闭读端
        close(pipefd[0]);

        // 将新创建的子进程的fd和管道的写的文件描述符存储起来
        end_points.push_back(EndPoint(id, pipefd[1]));
    }
}

int ShowBoard()
{
    cout<<endl;
    cout<<"#######################################"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"# 0. 执行日志任务   1. 执行数据库任务 #"<<endl;
    cout<<"# 2. 执行请求任务   3. 退出           #"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"请选择# ";

    int command=0;
    std::cin>>command;

    return command;
}

void ctrlProcess(const vector<EndPoint>&end_points)
{
    // 父进程开始发布命令
    int cnt=0;
    while(true)
    {
        //1. 选择任务
        int command=ShowBoard();
        //为3就退出
        if(command==3)
        {
            break;
        }

        if(command<0&&command>2)
        {
            cout<<"输入有误,请重新输入"<<endl;
            continue;
        }

        //2. 按顺序给子进程派发任务
        int indix=cnt++;
        cnt%=end_points.size();

        cout<<"你选择了进程:"<<end_points[indix].name()<<" | 处理"<<command<<"号任务"<<endl;

        //4. 下发任务
        write(end_points[indix]._write_id,&command,sizeof(command));
        
        sleep(1);
    }
}

//回收子进程
void waitProcess(vector<EndPoint>&end_points)
{
    //如果我们创建管道后,直接再创建子进程,那么子进程将继承父进程的所有文件描述符,
    //后创建的子进程会保留指向先创建的管道的读写文件描述符
    //所以顺序同时关闭子进程的写端和回收僵尸进程,其实并没有关闭子进程的写端,因为此时其引用计数仍>0

    // //这种写法会在waitpid时堵塞,因为子进程的写端还没有关闭
    // //所以子进程的读端处于堵塞状态,不会退出
    // for(const auto&ep:end_points)
    // {
    //     close(ep._write_id);
    //     cout<<"父进程关闭了"<<ep._child_id<<"的写端"<<endl;
    //     waitpid(ep._child_id,nullptr,0);
    //     cout<<"父进程回收了"<<ep._child_id<<endl;
    // }

    //解决方法一:倒着关闭写端,回收僵尸进程
    //解决方法二:在创建新的子进程后,子进程关闭从父进程那边继承的其他子进程的读写端


    //我们只需要让父进程关闭子进程的写端,子进程的读端会读到文件尾,然后自己就退了。

    // //1.关闭子进程的写端
    // for(const auto&ep:end_points)
    // {
    //     close(ep._write_id);
    // }

    // sleep(5);
    // cout<<"父进程让所有的子进程都退出"<<endl;
    
    // //2. 父进程回收子进程的僵尸状态
    // for(const auto&ep:end_points)
    // {
    //     waitpid(ep._child_id,nullptr,0);
    // }

    // cout<<"父进程回收了所有的子进程"<<endl;
    // sleep(5);

    //倒着关闭子进程的写端,再回收子进程
    for(int i=end_points.size()-1;i>=0;i--)
    {
        close(end_points[i]._write_id);
        cout<<"父进程让"<<end_points[i]._child_id<<"子进程退出"<<endl;

        waitpid(end_points[i]._child_id,nullptr,0);
        cout<<"父进程回收了"<<end_points[i]._child_id<<"子进程"<<endl;
        cout<<endl;

        sleep(1);
    }



    //一个一个退出
    for(const auto&ep:end_points)
    {
        close(ep._write_id);
        cout<<"父进程让"<<ep._child_id<<"子进程退出"<<endl;

        waitpid(ep._child_id,nullptr,0);
        cout<<"父进程回收了"<<ep._child_id<<"子进程"<<endl;
        cout<<endl;

        sleep(1);
    }

}

int main()
{
    // 用于存储子进程的pid和相对应管道的文件描述符
    vector<EndPoint> end_points;

    // 创建控制模块
    createProcesses(end_points);
    sleep(1);
    //开始通信
    ctrlProcess(end_points);

    //回收子进程
    waitProcess(end_points);

    cout<<"程序成功退出,欢迎下次使用"<<endl;

    return 0;
}

以下为部分运行结果

在这里插入图片描述

结束语

本篇文章内容到此结束,感谢你的阅读

如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

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

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

相关文章

每日一题 leetcode1026 2023-4-18

1026. 节点与其祖先之间的最大差值 力扣题目链接 给定二叉树的根节点 root&#xff0c;找出存在于 不同 节点 A 和 B 之间的最大值 V&#xff0c;其中 V |A.val - B.val|&#xff0c;且 A 是 B 的祖先。 &#xff08;如果 A 的任何子节点之一为 B&#xff0c;或者 A 的任何…

【CSS】使用 z-index 属性值控制定位盒子的堆叠顺序 ( 多个盒子堆叠次序问题 | z-index 属性值简介 | 控制盒子堆叠次序 )

文章目录一、多个盒子堆叠次序问题二、z-index 属性值简介三、控制盒子堆叠次序一、多个盒子堆叠次序问题 在 网页布局 中 , 如果 多个盒子都设置 绝对定位 , 那么这些盒子会堆叠在一起 ; 设置了定位样式的盒子会压住标准流盒子 , 如果有多个设置定位的盒子 , 后面的盒子会压住…

数组篇刷题总结

二分查找&#xff1a; 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 示例 1: 输入: nums [-1,0,3,5,9,12], target …

【brtc】视频下行弱网对抗优化

百度云 音视频实时通信五个部分 1 RTC基础 需要云端服务器参与大量边缘服务器参与采集、前处理(图像处理)、压缩编码音频 3 A 回声消除、增益健全的六大指标 </

nodejs扫描文件夹搜索包含关键词文件,可灵活配置

代码放在在末尾 文件说明&#xff1a; 关键代码&#xff1a;search.js 搜索结果&#xff1a;searchResult.txt 搜索日志&#xff1a;search.log 注&#xff1a;只保留一次的&#xff0c;需要多次自行修改logFile配置即可&#xff1b; 使用方式&#xff1a; 将代码放到需要…

c/c++:一维数组,初始化数组,循环打印数组,计算数组存储空间,数组元素个数,数组逆序算法

c/c:一维数组&#xff0c;初始化数组&#xff0c;循环打印数组&#xff0c;计算数组存储空间&#xff0c;数组元素个数&#xff0c;数组逆序算法 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;此时学会c的话&#xff0c; …

一文讲解系统性能分析之|iowait是什么?

我们对系统性能进行优化时&#xff0c;一般会使用 top 命令来查看系统负载和系统中各个进程的运行情况&#xff0c;从而找出影响系统性能的因素。如下图所示&#xff1a; top top 命令会输出很多系统相关的信息&#xff0c;如&#xff1a;系统负载、系统中的进程数、CPU使用率…

联诚发携多款创新产品及解决方案惊艳亮相ISLE 2023展!

这里写自定义目录标题4月7日-9日&#xff0c;ISLE 2023国际智慧显示及系统集成展览会在深圳国际会展中心&#xff08;宝安新馆&#xff09;隆重举行。来自全球各地1000余家企业参与展出&#xff0c;展出面积达8万㎡&#xff0c;吸引了众多业内专家、企业家以及广大观众前来观看…

《攻防演练》在没有基础安全能力的情况下如何做好蓝队防守

目的&#xff1a; 1、净化企业或机构的网络环境、强化网络安全意识&#xff1b; 2、防攻击、防破坏、防泄密、防重大网络安全故障&#xff1b; 3、检验企业关键基础设施的安全防护能力&#xff1b; 4、提升关键基础设施的网络安全防范能力和水平。 现状&#xff1a; 那么问…

什么是 prompts, completions, and tokens

从字面上看&#xff0c;任何文本都可以用作提示(prompts)——输入一些文本然后得到一些文本。 我们虽然知道 GPT-3 对随机字符串的处理很有趣&#xff0c;但是编写一个有效的提示才能更好的真正的让GPT理解我们要它做什么。 提示&#xff08;prompts&#xff09; Prompt是怎么…

DHTMLX Gantt入门使用教程【引入】:如何开始使用 dhtmlxGantt

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的大部分开发需求&#xff0c;具备完善的甘特图图表库&#xff0c;功能强大&#xff0c;价格便宜&#xff0c;提供丰富而灵活的JavaScript API接口&#xff0c;与各种服务器端技术&am…

vue3插槽的使用

插槽就是子组件中的提供给父组件使用的一个占位符&#xff0c;用 表示&#xff0c;父组件可以在这个占位符中填充任何模板代码&#xff0c;如 HTML、组件等&#xff0c;填充的内容会替换子组件的标签。 1.插槽基本使用 子组件SlotComponent.vue <template><div cla…

Salesforce Admin管理员中文学习教程_如何高效筛选出具有Admin权限的用户!

组织中最常见的错误之一就是拥有太多具有系统管理员简档的用户。不幸的是&#xff0c;这在某些行业中非常普遍。 实际上这存在着很大的潜在风险。拥有这些权限的用户可能会暴露、窃取或删除组织中的数据&#xff0c;甚至影响到其他用户。防止过多的管理员访问权限是保护Salesf…

CSDN博客写作编辑器如何使用?

文章目录0.引言1.快捷键2.文字3.链接和代码4.注脚和注释5.公式6.表7.图0.引言 笔者阅读CSDN博客已有五年&#xff0c;从最初的学习跟随者&#xff0c;到现在的CSDN博客创造者&#xff0c;这其中的转变来源于自身发展的思考&#xff0c;有学的输入&#xff0c;又有创作的输出&am…

GPT关键词挖掘,自动关键词文章生成

随着互联网的发展&#xff0c;内容营销已成为企业营销策略中不可或缺的一环。有效的关键词文章生成可以帮助企业吸引更多的潜在客户&#xff0c;提高品牌曝光度和转化率&#xff0c;从而实现营销目标。 关键词文章生成是指根据特定的关键词和主题&#xff0c;使用软件工具自动生…

计算机的工作原理

文章目录前言一、计算机组成二、工作原理1.首先指令输入——由鼠标/键盘完成&#xff1a;2.计算机对指令/输出的处理——由CPU完成&#xff1a;3.计算机对信息的储存——由内存、磁盘完成&#xff1a;4.计算机输出信息——由显卡、显示器完成&#xff1b;总结前言 电脑最直白、…

剪枝与重参第五课:前置知识

目录前置知识前言1.CIFAR10数据集1.1 简介1.2 数据集的获取1.3 数据集的加载2.VGG网络搭建2.1 VGGNet2.2 VGG网络实现3.Batch Normalize3.1 简介3.2 BN层实现4.L1&L2正则4.1 L1正则化(Lasso回归)4.2 L2正则化(岭回归)4.3 思考5.train5.1 parse_opt5.2 train5.3 test5.4 sav…

基于Amazon S3的通用封装oss-spring-boot-starter,支持前端分片直传

前段时间使用minio-java封装了一个 minio-spring-boot-starter&#xff0c;但是有局限性&#xff0c;不能很好的支持各个云服务厂商的OSS服务&#xff0c;为此&#xff0c;利用 aws-java-sdk-s3 做一个统一的封装&#xff0c;兼容S3 协议的通用文件存储工具类 &#xff0c;支持…

打造高效的跑腿配送系统,分享源码与经验

打造高效的跑腿配送系统&#xff0c;则是每一个快递公司和物流企业所追求的目标。在这篇文章中&#xff0c;我们将分享一些跑腿配送系统源码的技术解析、跑腿系统骑手端、商家端的优点以及跑腿配送相关的功能点介绍。 一、跑腿配送系统源码的技术解析 跑腿配送系统源码主要采…

1679_电子生产中的治具了解

全部学习汇总&#xff1a; GreyZhang/g_hardware_basic: You should learn some hardware design knowledge in case hardware engineer would ask you to prove your software is right when their hardware design is wrong! (github.com) 工作中切换了一下角色&#xff0c;做…