Linux系统编程:采用管道的方式实现进程间通信

news2025/1/21 15:39:45

目录

一. 进程间通信概述

二. 管道的概念 

三. 通过管道实现进程间通信

3.1 实现原理

3.2 匿名管道创建系统接口pipe

3.3 管道通信的模拟实现

3.4 管道通信的访问控制规则

3.5 管道通信的特点

四. 通过匿名管道实现进程池

4.1 进程池的概念

4.2 进程池的模拟实现

五. 命名管道

5.1 命名管道的功能

5.2 命名管道的创建和使用

六. 总结


一. 进程间通信概述

进程间通信的目的:实现进程之间的数据传输、共享资源、事件通知、多进程协同等操作。‘

进程间通信的技术手段:进程间要实现通信,就必须要让不同的进程看到同一块资源(内存空间),而由于进程之间具有独立性,因此这块资源不能隶属于任何一个进程,应当由操作系统内核提供。

进程间通信的方法:管道、SystemV、POSIX

管道:匿名管道、命名管道。

System V:共享内存、消息队列、信号量 。--   用于本地计算机进行单机进程间通信

POSIX:消息队列、共享内存、信号量、互斥量、读写锁、条件变量。 --   在网络中,用于多机之间的进程间通信。

图1.1 进程间通信的方法

二. 管道的概念 

管道,是用于传输资源(数据)的一种媒介,可以实现进程之间的单向通信(也只能单向通信)。

由于进程之间具有相互独立性,因此,管道只能由操作系统内核提供,不能源自任意进程,管道的本质是一种内存级文件,即:内容不会被刷新到磁盘上的文件。

图2.1 管道通信的原理

三. 通过管道实现进程间通信

3.1 实现原理

管道,尤其是匿名管道,一般用于具有亲缘关系的进程之间的通信,其底层实现原理如下:

  1. 父进程以读和写的方式创建匿名管道,由于管道的本质是文件,因此父进程会有两个文件描述符fd分别指向管道(一个读一个写)。
  2. fork创建子进程,子进程的文件描述符及指向与父进程相同。
  3. 关闭不需要的文件描述符,一般父进程用于写数据,子进程用于读数据,因此父进程关闭用来读的fd,子进程关闭用于写的fd。

经过上面的步骤,父进程的写fd和子进程的读fd就指向了相同的内存级文件(管道),通过父进程向管道中写的数据,就能被子进程读出来。

图3.1 通过管道实现进程间通信的原理

3.2 匿名管道创建系统接口pipe

原型:int pipe(int pipefd[2])

参数:pipefd为输出型参数,pipefd[0]为读端文件描述符,pipefd[1]为写端文件描述符。

返回值:如果成功创建管道返回0,失败返回-1并设置全局错误码。

头文件:#include <sys/fcntl.h>、#include <unistd.h>

一般pipe由父进程来调用,调用pipe后父进程要fork创建子进程,在父进程中一般要关闭pipefd[0],在子进程中一般要关闭pipefd[1]。

图3.2 pipe的原理

3.3 管道通信的模拟实现

代码3.1通过管道,实现父进程向子进程发生消息,父进程每隔1s写一次消息,子进程不间断读取并输出消息,由于写慢读快,子进程需要阻塞等待父进程写消息后才能读。

代码3.1:模拟实现管道通信

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/fcntl.h>

#define SIZE 1024

int main()
{
    // 1. 父进程创建管道
    int pipefd[2] = {0};    // 管道读写对应的文件描述符
    int n = pipe(pipefd);   // 创建管道

    if(n == -1)   // 管道创建失败
    {
        perror("pipe");
        exit(1);
    }

    // 2. fork子进程
    pid_t id = fork();    // 创建子进程
    
    if(id < 0)   //子进程创建失败
    {
        perror("fork");
        exit(2);
    }
    else if(id == 0)   //子进程代码
    {
        // 3. 子进程 -- 用于读取管道中的数据
        // 3.1 关闭不需要的文件描述符,子进程关写pipefd[1]
        close(pipefd[1]);

        // 3.2 读取数据并打印到标准输出流
        // 如果写慢读快,那么读要等写
        // 如果写快读慢,那么等待管道被写满后,就不能再继续写
        char read_buffer[1024] = {0};    // 读数据文件缓冲区
        while(true)
        {
            ssize_t sz = read(pipefd[0], read_buffer, SIZE - 1);   //数据读取
            
            // 如果写端退出,那么读端read读到0,读端最终会退出
            // 如果读端退出,OS会强制终止写端进程
            if(sz > 0)    // 确实读到了数据
            {
                read_buffer[sz] = '\0';
                std::cout << "Father# " << read_buffer << std::endl;
            }
            else if(sz == 0)   // 写端关闭
            {
                std::cout << "Father quit, write end, read end!" << std::endl;
                break;
            }
            else // sz < 0
            {
                perror("read");
                break;
            }
        }

        close(pipefd[0]);
        exit(0);
    }

    // 3. 父进程代码 -- 用于写数据
    // 3.1 关闭不需要的文件描述符,父进程关读pipdfd[0];
    close(pipefd[0]);

    // 3.2 向管道写数据
    const char* msg = "I am father process, I am sending message";

    char send_buffer[SIZE] = {0};   // 写数据缓冲区
    int count = 0;

    while(true)
    {
        snprintf(send_buffer, SIZE, "%s,%d", msg, ++count);
        write(pipefd[1], send_buffer, strlen(send_buffer));
        sleep(1);
    }

    close(pipefd[1]);
    return 0;
}

3.4 管道通信的访问控制规则

  • 读快写慢:读端需要阻塞等待写端写入数据后才能读。
  • 写快读慢:管道被写满后就不能继续写入,需要等待数据被读出。
  • 写端关闭:读端read读到0,退出。
  • 读端关闭:OS强制终止写端进程。

3.5 管道通信的特点

  1. 管道(匿名管道),常用于具有亲缘关系的进程的进程间通信。
  2. 管道通信存在访问控制。
  3. 管道通信是一种面向字节流式的通信。-- 面向流式的通信:可以多次写入的内容一次读取,也可以一次写入的内容分多次读取。
  4. 管道的本质是文件(内存级文件),文件的生命周期随进程的结束而结束,因此进程结束时,管道关闭。
  5. 管道通信为单向通信,是半双工通信的一种特殊形式。

半双工通信:通信双方在某一时刻,只能单独进行写或读。(并不是说某一端只能进行写或读,而是不能写和读同时进行)

全双工通信:通信双方可以写和读同时进行。

四. 通过匿名管道实现进程池

4.1 进程池的概念

父进程创建N个子进程,按照一定的规则向子进程派发任务,父进程只负责向子进程派发任务,具体的任务由子进程来完成。

如果父进程将均衡的向每个子进程派发任务,这种算法称为单机版负载均衡。

图4.1 进程池

4.2 进程池的模拟实现

采用rand随机生成来决定选用哪个子进程来执行任务。

task.hpp文件:父进程派发的任务

#ifndef __TASK_DEFINE_

#define __TASK_DEFINE_

#include <iostream>
#include <vector>
#include <functional>

typedef std::function<void()> func;    // 类型重定义

std::vector<func> trace_back;   // 回调函数
std::vector<std::string> desc;  // 任务编号及对应说明

void execuleUrl()
{
    std::cout << "execuleUrl" << std::endl;
}

void save()
{
    std::cout << "save data" << std::endl;
}

void visitSQL()
{
    std::cout << "visit SQL" << std::endl;
}

void online()
{
    std::cout << "take online" << std::endl;
}

void load()
{
    trace_back.emplace_back(execuleUrl);
    desc.emplace_back("execuleUrl");

    trace_back.emplace_back(save);
    desc.emplace_back("save data");

    trace_back.emplace_back(visitSQL);
    desc.emplace_back("visit SQL");

    trace_back.emplace_back(online);
    desc.emplace_back("take online");
}

void show()
{
    int count = 0;

    for(const auto& msg : desc)
    {
        std::cout << count << ": " << msg << std::endl;
        ++count;
    }
}

int handlerSize()
{
    return trace_back.size();
}

#endif

PipePool.cc文件:进程池的实现源文件

#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#include "task.hpp"

#define PROCESS_NUM 5

void distributeTask(int who, int fd, uint32_t command)
{
    // 派发任务
    ssize_t n = write(fd, &command, sizeof(uint32_t));
    assert(n == sizeof(uint32_t));
}

uint32_t waitCommand(int fd, bool &quit)
{
    uint32_t command = 0;
    ssize_t n = read(fd, &command, sizeof(uint32_t));

    if (n == 0)
    {
        quit = true;
        return -1;
    }

    assert(n == sizeof(uint32_t));
    return command;
}

int main()
{
    load(); // 载入任务

    // 创建子进程
    int pipefd[2] = {0};
    std::vector<std::pair<pid_t, int>> slot; // 记录子进程id以及写端文件描述符

    for (int i = 0; i < PROCESS_NUM; ++i)
    {
        // 创建管道
        int ret = pipe(pipefd);
        if (ret == -1) // 管道创建失败
        {
            perror("pipe");
            exit(1);
        }

        // 创建子进程
        pid_t id = fork();

        if (id < 0) // 如果子进程创建失败
        {
            perror("fork");
            exit(2);
        }
        else if (id == 0) // 子进程代码
        {
            // 子进程代码
            // 关闭不需要的文件描述符
            close(pipefd[1]); // 子进程关写

            while (true)
            {
                // 阻塞等待指令
                bool quit = false;
                uint32_t command = waitCommand(pipefd[0], quit);

                if (quit)
                {
                    std::cout << "write close, read also close!" << std::endl;
                    break;
                }

                if (command >= 0 && command < handlerSize())
                {
                    trace_back[command]();
                }
                else
                {
                    std::cerr << "choice wrong" << std::endl;
                }
            }

            close(pipefd[0]);
            exit(0);
        }

        // 父进程代码
        close(pipefd[0]);                 // 关闭读
        slot.emplace_back(id, pipefd[1]); // 将子进程id和对应写端文件描述符插入顺序表
    }

    srand((unsigned int)time(NULL));

    // 父进程,开始派发任务
    int select = 0;

    while (true)
    {
        std::cout << "##############################" << std::endl;
        std::cout << "##   1. show    2. choice   ##" << std::endl;
        std::cout << "##############################" << std::endl;

        std::cout << "Please Select: > ";
        std::cin >> select;

        if (select == 1)
        {
            show();
        }
        else if (select == 2)
        {
            int choice = 0;
            std::cout << "Please chose task: > ";
            std::cin >> choice;

            int proc = rand() % PROCESS_NUM;                             // 选择子进程完成任务
            distributeTask(slot[proc].first, slot[proc].second, choice); // 派发任务

            usleep(100000);
        }
        else
        {
            std::cerr << "choice error" << std::endl;
            break;
        }
    }

    // 父进程关闭写端,子进程退出
    for (const auto &iter : slot)
    {
        close(iter.second);
    }

    // 父进程阻塞等待子进程退出
    for (const auto &iter : slot)
    {
        waitpid(iter.first, NULL, 0);
    }

    return 0;
}

五. 命名管道

5.1 命名管道的功能

一般意义上的管道(匿名管道)只能实现父子进程之间的通信,而命名管道可以实现不相关进程之间的进程间通信。

命名管道是一种特殊类型的文件,具有以下特性:

  1. 可以被打开,但不会将内存中的数据刷新到磁盘。
  2. 具有属于自己的名称。
  3. 在系统中有唯一的路径。

两个不相关的进程,可以通过访问同一管道文件,来实现进程间通信。

图5.1 命名管道实现不相关进程之间的通信

5.2 命名管道的创建和使用

  • 通过指令创建管道文件:mkfifo [文件名]   
  • 删除管道文件:unlink、rm均可
图5.2 通过指令创建管道文件

同样,C语言库函数中也有mkfifo,其功能也是创建管道文件,mkfifo库函数的信息如下:

  • 函数原型:int mkfifo(const char* pathname, mode_t mode);
  • 函数参数:pathname为创建的管道文件名和路径,mode为起始权限。
  • 返回值:创建成功返回0,失败返回-1并设置全局错误码。

也存在unlink库函数,用于删除管道文件,原型为:int unlink(const char* pathname)

假设有两个进程A和B,进程A以写的方式打开管道文件,进程B以读的方式打开管道文件,如果进程B先运行,需要等到进程A以写的方式打开管道文件后,进程B才可以以读打开的方式打开管道文件,否则,进程B要一直等待管道文件被以读的方式打开。

代码5.1通过命名管道,实现服务端进程(serve.exe)和客户端进程(client.exe)之间的进程间通信,serve.cc创建管道文件,并以只读方式打开管道文件,client.cc以只写的方式打开管道文件,执行代码时先运行serve.exe,等待client.exe运行以只写打开管道后,serve.exe才能执行只读打开管道文件的代码,在client中输入的信息,会显示到serve中。

代码5.1:命名管道实现不相关进程间的通信

// log.hpp头文件 -- 日志打印相关声明和实现
// 日志操作
#include <iostream>
#include <string>
#include <ctime>

#ifndef __LOG_DEFINE_
#define __LOG_DEFINE_

#define DEBUG   0
#define NOTICE  1
#define WARNING 2
#define ERROR   3   

std::string msg[] = {
    "Debug",
    "Notice",
    "Waring",
    "Error"
};

std::ostream& log(const std::string& message, int level)
{
    std::cout << (unsigned int)time(NULL) << " | " << msg[level] << " | " << message << std::endl;
}

#endif


// common.hpp头文件 -- 声明宏,包含库文件
#ifndef __COMMON_DEF_
#define __COMMON_DEF_

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

#include "log.hpp"

#define MODE 0666
#define SIZE 1024
#define PROCESS_NUM 3

std::string ipcPath = "fifo.ipc";

#endif


// serve.cc -- 服务端源文件代码
#include "commom.hpp"

// 服务端函数
int main()
{
    // 1. 以只写的方式打开管道文件
    int fd = open(ipcPath.c_str(), O_WRONLY);
    if(fd < 0)
    {
        perror("client open");
        exit(1);
    }

    // 2. 开始执行进程间通信
    std::string send_buffer;   // 信息发送缓冲区

    while(true)
    {
        std::cout << "请输入要发送的信息: > ";
        std::getline(std::cin, send_buffer);  // 逐行读取信息
        write(fd, send_buffer.c_str(), send_buffer.size());   //写数据
    }

    // 3. 关闭文件
    close(fd);
    return 0;
}


// client.cc -- 客户端源文件代码
#include "commom.hpp"

// 信息读取函数
void GetMessage(int fd)
{
    char read_buffer[SIZE] = {0};    //读数据缓冲区

    while(true)
    {
        ssize_t n = read(fd, read_buffer, SIZE - 1);    //从管道文件读数据
        if(n > 0)
        {
            read_buffer[n] = '\0';
            std::cout << "[ " << getpid() << " ]  client say# " << read_buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "client quit, read end, serve quit too! " << std::endl;
            break;
        }
        else  // n < 0 -- 读取出错
        {
            perror("read");
            break;
        }
    }
}

int main()
{
    // 1. 创建命名管道文件
    if(mkfifo(ipcPath.c_str(), MODE) < 0)
    {
        perror("mkfifo");
        exit(1);
    }

    log("管道创建成功", DEBUG);

    // 2. 服务端以只读方式打开文件
    int fd = open(ipcPath.c_str(), O_RDONLY);
    if(fd < 0)  //检验打开是否成功
    {
        perror("serve fopen");
        exit(2);
    }

    log("文件打开成功", DEBUG);

    // 3. 创建子进程,进行进程间通信(读数据)
    for(int i = 0; i < PROCESS_NUM; ++i)
    {
        pid_t id = fork();
        if(id == 0)   //子进程代码
        {
            GetMessage(fd);   //信息读取函数
            exit(0);
        }
    }

    // 4. 阻塞等待子进程退出
    for(int i = 0; i < PROCESS_NUM; ++i)
    {
        waitpid(-1, NULL, 0);
    }

    log("子进程全部退出", NOTICE);

    // 5. 关闭文件,删除命名管道文件
    close(fd);
    unlink(ipcPath.c_str());

    log("文件关闭成功", NOTICE);
    // std::cout << "文件关闭成功" << std::endl;

    return 0;
}
图5.3 代码5.1运行情况

六. 总结

  • 实现进程间通信的方式有三种,分别为管道、System V、POSIX,其中System V用于本地单机进程间通信,POSIX用于网络进程间通信。
  • 管道的本质是内存级文件,由OS内核提供,管道一般用于具有亲缘关系的进程间通信,管道通信为单向通信,是面向字节流式的通信,存在访问控制,管道的生命周期随进程的终止而终止。
  • 通过命名管道,可以实现不相关进程间的通信。

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

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

相关文章

Android 广播发送流程分析

在上一篇文章中Android 广播阻塞、延迟问题分析方法讲了广播阻塞的分析方法&#xff0c;但是分析完这个问题&#xff0c;自己还是有一些疑问&#xff1a; 广播为啥会阻塞呢&#xff1f;发送给接收器就行了&#xff0c;为啥还要等着接收器处理完才处理下一个&#xff1f;由普通…

分布式文件系统(FastDFS)

&#x1f353; 简介&#xff1a;java系列技术分享(&#x1f449;持续更新中…&#x1f525;) &#x1f353; 初衷:一起学习、一起进步、坚持不懈 &#x1f353; 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正&#x1f64f; &#x1f353; 希望这篇文章对你有所帮助,欢…

吐血整理,接口自动化测试-接口依赖/上传接口处理(项目实例)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 常见的两种接口依…

Docker容器与虚拟化技术:Docker资源控制、数据管理

目录 一、理论 1.资源控制 2.Docker数据管理 二、实验 1.Docker资源控制 2.Docker数据管理 三、问题 1.docker容器故障导致大量日志集满&#xff0c;造成磁盘空间满 2、当日志占满之后如何处理 四、总结 一、理论 1.资源控制 (1) CPU 资源控制 cgroups&#xff0…

什么是前端框架?怎么学习? - 易智编译EaseEditing

前端框架是一种用于开发Web应用程序界面的工具集合&#xff0c;它提供了一系列预定义的代码和结构&#xff0c;以简化开发过程并提高效率。 前端框架通常包括HTML、CSS和JavaScript的库和工具&#xff0c;用于构建交互式、动态和响应式的用户界面。 学习前端框架可以让您更高效…

Centos8安装docker并配置Kali Linux图形化界面

鉴于目前网上没有完整的好用的docker安装kali桌面连接的教程&#xff0c;所以我想做一个。 准备工作 麻了&#xff0c;这服务器供应商提供的镜像是真的纯净&#xff0c;纯净到啥都没有。 问题一&#xff1a;Centos8源有问题 Error: Failed to download metadata for repo ap…

Redis基础概念和数据类型详解

目录 1.什么是Redis&#xff1f; 2.为什么要使用Redis&#xff1f; 3.Redis为什么这么快&#xff1f; 4.Redis的使用场景有哪些&#xff1f; 5.Redis的基本数据类型 5.1 5种基础数据类型 5.1.1 String字符串 5.1.2 List列表 5.1.3 Set集合 5.1.4 Hash散列 5.1.5 Zset有序集…

【hive】hive分桶表的学习

hive分桶表的学习 前言&#xff1a; 每一个表或者分区&#xff0c;hive都可以进一步组织成桶&#xff0c;桶是更细粒度的数据划分&#xff0c;他本质不会改变表或分区的目录组织方式&#xff0c;他会改变数据在文件中的分布方式。 分桶规则&#xff1a; 对分桶字段值进行哈…

玩转 VS code 之下载篇

VSCode 简介 Visual Studio Code (简称 VS Code / VSC)&#xff0c;是2015 年由微软公司推出的一款免费开源的现代化轻量级代码编辑器&#xff0c;支持几乎所有主流的开发语言的语法高亮、智能代码补全、GIT 等特性&#xff0c;支持插件扩展等等 可用于 Windows&#xff0c;ma…

使用 Visual Studio GoogleTest编写 C/C++ 单元测试——入门篇

入门教程 Visual Studio 新建 GoogleTest项目&#xff0c;一路选默认参数 pch.h #pragma once#include "gtest/gtest.h"int add(int a, int b);pch.cpp #include "pch.h"int add(int a, int b) {return a b; }test.cpp #include "pch.h"TES…

LoRA微调方法详解

本文要介绍的是大模型的微调训练方法之一----LoRA。 0 背景 现在大模型非常火爆&#xff0c;大家都在想方设法应用大模型。 当前很多大模型虽说可以zero-shot直接使用&#xff0c; 但是在具体应用上一般还是微调一下效果更好&#xff0c; 也就是常说的finetune。 在小模型时代…

栈存储结构详解

目录 栈存储结构详解 进栈和出栈 栈的具体实现 栈的应用 什么是队列&#xff08;队列存储结构&#xff09; 栈存储结构详解 同顺序表和链表一样&#xff0c;栈也是用来存储逻辑关系为 "一对一" 数据的线性存储结构&#xff0c;如图 1 所示。 图 1 栈存储结构示意…

【string】基本用法

目录 前言: string常用接口 一、string的创建,拼接与拷贝构造 1.创建 2.拼接 3.拷贝构造 二、string遍历 方式一&#xff1a;operator[ ]重载 方式二&#xff1a;迭代器 1.正向迭代器&#xff1a; 2.反向迭代器 3.const正向迭代器 4.const反向迭代器 方式三&#…

PyQt5资源的加载和使用,即如何使用Pyrcc

1、打开QtDesigner&#xff0c;选择编辑资源 2、新建资源文件&#xff0c;随便找个地方保存 3、按照自己的喜好命名&#xff0c;然后添加资源 4、保存并退出 5、我们创建一个QLabel&#xff0c;在这里添加资源 6、我们保存界面文件&#xff0c;并编译为py文件&#xff0c;然后…

【C语言】调试技巧

目录 一、什么是bug? 二、调试 1.一般调试的步骤 2.Debug 和 Release 三、调试环境准备 四、调试时要查看的信息 1.查看临时变量的值 2.查看内存信息 3.查看调用堆栈 4.查看反汇编信息 5.查看寄存器 五、练习 六、常见的coding技巧 七、const的作用 八、编程常见…

时序预测 | MATLAB实现基于RF随机森林的时间序列预测-递归预测未来(多指标评价)

时序预测 | MATLAB实现基于RF随机森林的时间序列预测-递归预测未来(多指标评价) 目录 时序预测 | MATLAB实现基于RF随机森林的时间序列预测-递归预测未来(多指标评价)预测结果基本介绍程序设计参考资料 预测结果 基本介绍 MATLAB实现基于RF随机森林的时间序列预测-递归预测未来…

Linux命令200例:kill用来终止或者结束进程(常用)

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;全栈领域新星创作者✌。CSDN专家博主&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &…

服务链路追踪

一、基础概念 1.背景 对于一个大型的几十个、几百个微服务构成的微服务架构系统&#xff0c;通常会遇到下面一些问题&#xff0c;比如&#xff1a; 如何串联整个调用链路&#xff0c;快速定位问题&#xff1f;如何理清各个微服务之间的依赖关系&#xff1f;如何进行各个微服…

uniapp-微信小程序篇

uniapp-微信小程序篇 一、创建项目(以Vue3TS 项目为示例) 可以通过命令行的方式创建也可以通过HBuilderX进行创建&#xff08;通过HBuilderX创建的项目建议选择最简单的模板&#xff09;&#xff0c;个人建议使用命令行方式。 (1) 命令行方式&#xff1a; npx degit dcloudio…