[ Linux ] 进程间通信之共享内存

news2025/8/10 3:16:51

在上篇博文我们了解了通过管道完成进程间通信,我们了解匿名管道和命名管道,并且通过编码模拟实现使用了匿名管道和命名管道。我们知道要让进程间完成通信必须让这两个进程首先看到同一份资源,因此给予这个前提,本篇博文我们了解另外一种可以进程间通信的方式 -- 共享内存。

目录

1.system V共享内存

1.1共享内存原理的理解

共享内存示意图

1.2 共享内存编码

1.2.1 共享内存函数

1.2.2 删除共享内存

1.2.3 使用共享内存完成进程间通信

1.2.4 Cli写字符 Ser获取

附录:

ipcShmCli.cc

ipcShmSer.cc

comm.hpp

Log.hpp

makefile


1.system V共享内存

system V是一套标准,这是系统级别的接口。我们知道进程间通信的前提是:先让不同的进程看到同一份资源!

1.1共享内存原理的理解

首先我们来了解一下共享内存的原理:

我们在之前知道了一个进程由自己的tast_struct和进程的地址空间,那么其中对我们来讲tast_struct有指向自己的地址空间。我们知道进程地址空间内有栈区,堆区,数据区等等.....我们在动静态库中也知道我们可以把磁盘中的库加在到内存中通过页表映射到我们进程的进程地址空间中的共享区。其中共享库就在这里。而进程间通信一定至少有两个进程(如下图),那么这两个进程是毫无关系的,每一个进程都有自己的页表,也可以通过页表映射到自己的地址空间。假设今天有一个特性的接口,首先能够在物理内存中创建一份空间,然后各进程调用这个接口,将这份空间通过页表映射到自己进程的地址空间上。那么进程就可以通过自己的地址空间看到物理内存中的这份空间。因此各进程都可以看到物理内存中的同一份资源。此时对我们来讲这种机制就是共享内存。

共享内存示意图

1.2 共享内存编码

1.2.1 共享内存函数

  • shmget函数

功能:shmget是用来创建共享内存的

参数:

  1. key : 这个共享内存段的名字
  2. size : 共享内存的大小 建议设置成页(4KB)的整数倍
  3. shmflg :由9个权限标志构成,他们的用法和创建文件时使用的mode模式表示是一样的

返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

IPC_CREAT:这个参数就和命名管道一样,如果存在就不用再创建了就获取,如果不存在就创建之。

IPC_EXCL : 要和IPC_CREAT一起使用(按位或 | ),如果不存在指定的共享内存,就创建之;如果存在指定的共享内存就出错返回。能够保证如果shmget函数调用成功一定是一个全新的share memory共享内存!

这个共享内存是存在内核中的 内核会给我们维护共享内存的结构!共享内存也要被内核管理起来,先描述再组织。下面就是共享内促的内核数据结构:

struct shmid_ds {
 struct ipc_perm shm_perm; /* operation perms */
 int shm_segsz; /* size of segment (bytes) */
 __kernel_time_t shm_atime; /* last attach time */
 __kernel_time_t shm_dtime; /* last detach time */
 __kernel_time_t shm_ctime; /* last change time */
 __kernel_ipc_pid_t shm_cpid; /* pid of creator */
 __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
 unsigned short shm_nattch; /* no. of current attaches */
 unsigned short shm_unused; /* compatibility */
 void *shm_unused2; /* ditto - used by DIPC */
 void *shm_unused3; /* unused */
};

那么我怎么知道共享内存属于存在还是不存在呢?

我们在上面知道共享内存要被内核管理再组织->struct shmid_ds{} -> struct ipc_perm -> key(shmget) 共享内存的唯一值!而通过这个key我们就能表示这个共享内存。因此如果一个共享内存存在,就有一个key值,我们只要拿到这个key值,是不是就相当于拿到了共享内存。因此为了更好的进行控制,这个key值由我们的用户来提供。我们可以通过key值来标定这个共享内存,在内核中让不同的进程看到同一份共享内存做法是让他们拥有同一个key值即可。

这个key我们一般要使用ftok来生成。

我们使用一下来看看效果:

comm.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

using namespace std;

#define PATH_NAME "/home/Lxy/code"
#define PROJ_ID 0x14
#define MEM_SIZE 4096



key_t CreatKey()
{
    key_t key = ftok(PATH_NAME,PROJ_ID);
    if(key < 0 )
    {
        cerr<< "ftok:" << strerror(errno) <<endl;
        exit(1);
    }

    return key;
}


IpcShmCli.cc

#include "comm.hpp"
#include "Log.hpp"

int main()
{
    key_t key = CreatKey();
    cout<<"key :" <<key << endl;
    return 0;
}


IpcShmSer.cc

#include "comm.hpp"
#include "Log.hpp"

//充当创建共享内存的角色
int main()
{
    key_t key = CreatKey();
    Log() << "key " << key <<endl;

    //创建一个全新的共享内存
    int shmid = shmget(key,MEM_SIZE,IPC_CREAT|IPC_EXCL);
    if(shmid < 0)
    {
        Log()<<"shmget:" << strerror(errno) << endl;
        return 2;
    }

    Log() << "creat shm success ,shmid: " << shmid << endl;

    return 0;
}


Log.hpp

#pragma once

#include <iostream>
#include <ctime>

std::ostream &Log()
{
    std::cout << "For Debug | " << "timestamp: " << (uint64_t)time(nullptr) << " ";
    return std::cout;
}


makefile

.PHONY:all
all: ipcShmCli ipcShmSer

ipcShmCli:IpcShmCli.cc
	g++ -Wall -o $@ $^ -std=c++11

ipcShmSer:IpcShmSer.cc
	g++ -Wall -o $@ $^ -std=c++11
	
.PHONY:clean
clean:
	rm -f ipcShmCli ipcShmSer

当我们第二次运行ipcShmSer时,此时共享内存已经存在了,因此IPC_EXCL会失败返回,我们查看退出码果然是2,因此system V下的共享内存生命周期是随内核的!而且共享内存如果不显示删除的话,只有等OS重启才能删除。因此接下来,我们要学习一个 共享内存的删除接口

1.2.2 删除共享内存

1.用指令来删除

要删除共享内存,首先我们要查看共享内存是否存在,我们可以在命令行输入

ipcs -m

当我们查看到我们当前系统所有的共享内存后我们在命令行输入下面这条指令就可以删除指定shmid的共享内存

ipcrm -m shmid

此时我们将共享内存删除后,我们再运行ipcShmSer就可以再次创建一个新的共享内存了

2.用系统接口来删除

  • shmctl

功能: 用于控制共享内存

参数:

  1. shmid : 由shmget返回的共享内存标识码
  2. cmd:将要采取的动作(有三个可取值) 其中IPC_RMID 为删除共享内存
  3. buf : 指向一个保存着共享内存的模式状态和访问权限的数据结构

返回值: 成功返回 0 ; 失败返回 -1

话不多说 我们直接把用在我们刚刚所写的ipcShmSer.cc中

#include "comm.hpp"
#include "Log.hpp"

//充当创建共享内存的角色
int main()
{
    key_t key = CreatKey();
    Log() << "key " << key <<endl;


    Log() << "create share memory begin! \n";
    sleep(5);
    //创建一个全新的共享内存
    int shmid = shmget(key,MEM_SIZE,IPC_CREAT|IPC_EXCL);
    if(shmid < 0)
    {
        Log()<<"shmget:" << strerror(errno) << endl;
        return 2;
    }

    Log() << "creat shm success ,shmid: " << shmid << endl;


    //使用
    sleep(5);


    //删除
    shmctl(shmid,IPC_RMID,nullptr);
    Log()<<"delete shm : " << shmid << "sucess \n";
    sleep(5);
    return 0;
}

为了能够主观感受到共享内存从创建到删除的全过程,我们写一个监控脚本来每隔1秒查看一下

while :; do ipcs -m; sleep 1; done

1.2.3 使用共享内存完成进程间通信

在上述的准备工作中,我们把共享内存的原理,已经共享内存的创建,删除都已经能够完成了,接下来到了最重要的地方,就是不同进程间要使用共享内存完成进程间通信.此时共享内存在内存中创建完成了,我们需要将物理内存中的共享内存挂接到进程的地址空间内。这里就需要使用另外一个函数

  • shmat

功能:将共享内存连接到进程地址空间

参数:

  1. shmid : 共享内存标识符
  2. shmaddr :指定连接的地址
  3. shmflg : 它的两个可能取值是SHM_RND 和 SHM_RDONLY

返回值: 成功返回一个指针,指向共享内存的第一个节;失败返回-1

那么我们直接在代码中使用:

//使用
    //1.将共享内存挂接到进程地址空间内
    char * str = (char*)shmat(shmid,nullptr,0);
    Log()<< " attach shm : " << shmid << "sucess !\n";

那么如果我们挂接之后不想用了我们要去关联,我们可以使用下面这个函数

  • shmdt函数

功能:将共享内存段与当前进程脱离

参数:

  1. shmaddr: 由shmat所返回的指针

返回值:成功返回0;失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段

我们直接在代码中体现

//使用
    //1.将共享内存挂接到进程地址空间内
    char * str = (char*)shmat(shmid,nullptr,0);
    Log()<< " attach shm : " << shmid << "sucess !\n";
    
    //2.不想用就去关联
    shmdt(str);
    Log()<< " detach shm : " << shmid << "sucess !\n";
    sleep(5);

现在我们再来写ipcShmCli.cc

#include "comm.hpp"
#include "Log.hpp"

int main()
{
    //创建相同的Key值
    key_t key = CreatKey();
    Log() <<"key :" <<key << endl;

    //获取 shmid 
    int shmid = shmget(key,MEM_SIZE,IPC_CREAT);
    if(shmid < 0)
    {
        Log()<<"shmget:" << strerror(errno) << endl;
        return 2;
    }

    //获取成功之后挂接
    char* str = (char*)shmat(shmid,nullptr,0);

    //用它  
    sleep(5);


    //去关联
    shmdt(str);

    //不用删除共享内存 共享内存的创建和销毁全权由Ser管理
    return 0;
}

当我们同时将Cli和Ser同时挂接到了一个共享内存上时,此时保证了两个进程已经同时看到了同一份资源,接下来我们来写Cli的操作。这里需要注意的是,我们把共享内存是映射到了我们进程地址空间上了(堆栈之间),因此对于每一个进程来说,挂接到自己的上下文中的共享内存是属于自己的空间的。类似于堆空间或者栈空间,因此可以被用户直接使用,不需要调用任何的系统接口!

1.2.4 Cli写字符 Ser获取

当我们建立好共享内存并且同时挂接到两个进程之后,我们可以进行进程间通信了,我们首先可以完成一个较为简单的通信。我们让Cli往共享内存里面写26个英文字符,每个一秒写一个,让Ser每隔一秒读取一次共享内存里面的内容。这样当我们同时启动程序的时候,我们应该看到的现象是Ser每隔一秒在屏幕打印出英文字符(从A开始到Z),每隔一秒多一个字符并换行。我们运行起来看看结果:

   //用它  
    //没有使用任何的系统调用接口
    int cnt = 0;
    while(cnt <= 26)
    {
        str[cnt] = 'A' + cnt;
        ++cnt;
        str[cnt] = '\0';
        sleep(1);
    }

   //2.使用
    while(true)
    {
        printf("%s\n",str);
        sleep(1);
    }

运行结果:

因此共享内存因为自身的特性,没有任何访问控制!共享内存被进程双方直接看到,可以直接通信,属于用户双方,但是不安全!因此共享内存挂接到两个进程的时候,进程1向共享内存写入内容是,进程2立马就可以看到。因此共享内存是所有进程间通信中速度最快的!

完成上面的功能后,我们知道了在Cli中写入字符,Ser可以立马获取。因此我们只需要稍稍修改Cli的代码就可以传输我们自定义的信息了。我们一起来看看代码的实现。

#include "comm.hpp"
#include "Log.hpp"

int main()
{
    //创建相同的Key值
    key_t key = CreatKey();
    Log() <<"key :" <<key << endl;

    //获取 shmid 
    int shmid = shmget(key,MEM_SIZE,IPC_CREAT);
    if(shmid < 0)
    {
        Log()<<"shmget:" << strerror(errno) << endl;
        return 2;
    }

    //获取成功之后挂接
    char* str = (char*)shmat(shmid,nullptr,0);

    //自定义输入
    while(true)
    {
        printf("Please Enter# ");
        fflush(stdout);
        ssize_t s = read(0,str,MEM_SIZE);
        if(s > 0)
        {
            str[s] = '\0';
        }
    }



    //去关联
    shmdt(str);

    //不用删除共享内存 共享内存的创建和销毁全权由Ser管理
    return 0;
}

这种通信方式就是共享内存!

(本篇完)

附录:

ipcShmCli.cc

#include "comm.hpp"
#include "Log.hpp"

int main()
{
    //创建相同的Key值
    key_t key = CreatKey();
    Log() <<"key :" <<key << endl;

    //获取 shmid 
    int shmid = shmget(key,MEM_SIZE,IPC_CREAT);
    if(shmid < 0)
    {
        Log()<<"shmget:" << strerror(errno) << endl;
        return 2;
    }

    //获取成功之后挂接
    char* str = (char*)shmat(shmid,nullptr,0);

    while(true)
    {
        printf("Please Enter# ");
        fflush(stdout);
        ssize_t s = read(0,str,MEM_SIZE);
        if(s > 0)
        {
            str[s] = '\0';
        }
    }

    //用它  
    //没有使用任何的系统调用接口
    // int cnt = 0;
    // while(cnt <= 26)
    // {
    //     str[cnt] = 'A' + cnt;
    //     ++cnt;
    //     str[cnt] = '\0';
    //     sleep(1);
    // }


    //去关联
    shmdt(str);

    //不用删除共享内存 共享内存的创建和销毁全权由Ser管理
    return 0;
}

ipcShmSer.cc

#include "comm.hpp"
#include "Log.hpp"

//充当创建共享内存的角色
int main()
{
    key_t key = CreatKey();
    Log() << "key " << key <<endl;


    Log() << "create share memory begin! \n";
    //sleep(5);
    //创建一个全新的共享内存
    int shmid = shmget(key,MEM_SIZE,IPC_CREAT|IPC_EXCL | 0666);
    if(shmid < 0)
    {
        Log()<<"shmget:" << strerror(errno) << endl;
        return 2;
    }

    Log() << "creat shm success ,shmid: " << shmid << endl;

    sleep(2);
    //使用
    //1.将共享内存挂接到进程地址空间内
    char * str = (char*)shmat(shmid,nullptr,0);
    Log()<< " attach shm : " << shmid << "sucess !\n";
    //sleep(2);

    //2.使用
    while(true)
    {
        //有数据了再读
        printf("%s\n",str);
        //sleep(1);
    }
    
    //2.不想用就去关联
    shmdt(str);
    Log()<< " detach shm : " << shmid << "sucess !\n";

    
    sleep(2);


    //删除
    shmctl(shmid,IPC_RMID,nullptr);
    Log()<<"delete shm : " << shmid << "sucess \n";
    //sleep(5);
    return 0;
}

comm.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

using namespace std;

#define PATH_NAME "/home/Lxy/code"
#define PROJ_ID 0x14
#define MEM_SIZE 4096



key_t CreatKey()
{
    key_t key = ftok(PATH_NAME,PROJ_ID);
    if(key < 0 )
    {
        cerr<< "ftok:" << strerror(errno) <<endl;
        exit(1);
    }

    return key;
}

Log.hpp

#pragma once

#include <iostream>
#include <ctime>

std::ostream &Log()
{
    std::cout << "For Debug | " << "timestamp: " << (uint64_t)time(nullptr) << " ";
    return std::cout;
}

makefile

.PHONY:all
all: ipcShmCli ipcShmSer

ipcShmCli:IpcShmCli.cc
	g++ -Wall -o $@ $^ -std=c++11

ipcShmSer:IpcShmSer.cc
	g++ -Wall -o $@ $^ -std=c++11
	
.PHONY:clean
clean:
	rm -f ipcShmCli ipcShmSer

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

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

相关文章

【Linux】自动化构建工具-make/Makefile第一个小程序

大家好我是沐曦希&#x1f495; 文章目录一.项目自动化构建工具-make/Makefile1.背景2. 举例3. 原理4. 总结5. 项目清理6. 习题习题一习题二二.第一个小程序&#xff0d;进度条1.行缓冲区2.倒计时3.进度条一.项目自动化构建工具-make/Makefile 1.背景 会不会写makefile&#x…

使用 elasticdump 跨版本迁移 ES 数据

1、elasticdump 用途介绍 elasticdump 是一个在 Github 开源的 Elasticsearch 的数据迁移工具&#xff0c;项目 Github 地址&#xff1a;[传送门](Github 地址&#xff1a;https://github.com/elasticsearch-dump/elasticsearch-dump) elasticdump 支持跨版本导出和导入数据&am…

【java进阶07:常用类】String类、包装类、日期类Date、数字类、随机数、枚举类型

String类 String类型的字符串存储原理 /*关于java JDK中内置的一个类&#xff1a;java.util.String1、String表示字符串类型&#xff0c;属于引用数据类型&#xff0c;不属于基本数据类型。2、在java中随便使用双引号括起来的都是String对象&#xff0c;例如&#xff1a;"…

零基础上手unity VR开发【配置PC端项目的实时调试】

&#x1f4cc;个人主页&#xff1a;个人主页 ​&#x1f9c0; 推荐专栏&#xff1a;Unity VR 开发成神之路 --【着重解决VR项目开发难&#xff0c;国内资料匮乏的问题。从零入门&#xff0c;一步一个脚印&#xff0c;带你搞定VR开发! &#x1f63b;&#x1f63b;】 &#x1f4d…

聊一聊如何截获 C# 程序产生的日志

一&#xff1a;背景 1.讲故事 前段时间分析了一个dump&#xff0c;一顿操作之后&#xff0c;我希望用外力来阻止程序内部对某一个com组件的调用&#xff0c;对&#xff0c;就是想借助外力实现&#xff0c;如果用 windbg 的话&#xff0c;可以说非常轻松&#xff0c;但现实情况…

当下互联网行业趋势,你顶得住吗?

持续三年的疫情导致经济形式大不如前&#xff0c;特别是互联网行业&#xff0c;不少员工面临着失业的压力&#xff0c;在如此恶劣的大环境下&#xff0c;计算机行业的我们应该如何生存&#xff1f;有一个很好的办法就是 —— 考证&#xff01;&#xff01;&#xff01;如今越来…

多线程与并发 - 常见的几种锁的实现方式

1、悲观锁 正如其名&#xff0c;它是指对数据修改时持保守态度&#xff0c;认为其他人也会修改数据。因此在操作数据时&#xff0c;会把数据锁住&#xff0c;直到操作完成。悲观锁大多数情况下依靠数据库的锁机制实现&#xff0c;以保证操作最大程度的独占性。如果加锁的时间过…

深度学习入门(6)误差反向传播基础---计算图与链式法则

在我的第三篇博文《深度学习入门&#xff08;3&#xff09;神经网络参数梯度的计算方式》中详细介绍了通过微分方式计算神经网络权重参数的梯度。但是数值微分的方式计算梯度效率较低。后续博文会介绍另外一种更加高效的梯度计算方式---误差的反向传播。 这篇文章介绍的是误差…

CorelDRAW2023最新版矢量设计软件

CorelDRAW2023最新版是我比较用的比较好的一款软件&#xff0c;因为其作为一款优秀的矢量设计软件&#xff0c;兼具功能和性能&#xff0c;它是由Corel公司出品的矢量设计工具&#xff0c;被广泛应用于排版印刷、矢量图形编辑、网页设计等行业。CDR软件的优势在于&#xff1a;易…

ROS2 机器人操作系统入门和安装以及如何使用 .NET 进行开发

本文是 ROS2 入门的第一课&#xff0c;简单介绍了 ROS 系统&#xff0c;并演示了 ROS2 系统在 Ubuntu 22.04 中的安装&#xff08;使用 gitee 和清华源&#xff09;以及其中错误的解决。最后对其优势进行总结&#xff0c;为什么选择 ROS。最后介绍简单 Demo 和如何使用 .NET 接…

ThingsBoard源码解析-规则引擎

描述 规则引擎是Thingsboard的核心部分&#xff0c;基于Actor编程模型&#xff0c;类似事件驱动&#xff1b; 每个actor都有自己的消息队列&#xff08;mailBox&#xff09;保存接收到的消息 actor可以创建actor actor可以将消息转发给其他actor 分析 Actor模型实现 系统…

戴尔科技集团通过多云数据保护和安全创新增强网络弹性

中国北京——2022年11月18日 Dell PowerProtect Data Manager软件更新和新一代备份一体机可帮助客户提高运维安全和网络弹性 戴尔多云数据保护解决方案利用内置的安全运维功能加速采用零信任原则 2022年全球数据保护指数(GDPI)调查结果公布 戴尔科技集团(NYSE:Dell)扩大其在数据…

OA系统,有效提升企业办公效率落实执行力

企业管理的成功将最终取决于企业的执行情况&#xff0c;只要有良好的经营管理&#xff0c;管理系统&#xff0c;一个好的领导者&#xff0c;充分调动员工的积极性&#xff0c;将能最大限度的管理执行力。 OA协同办公系统提供了工作流和协同工作互补结合。工作流程严格规定了工作…

PCB铺铜的优点与缺点

PCB设计铺铜是电路板设计的一个非常重要的环节。 什么是PCB铺铜&#xff0c;就是将PCB上无布线区域闲置的空间用固体铜填充。铺铜的意义在于减小地线阻抗&#xff0c;提高抗干扰能力;降低压降&#xff0c;提高电源效率&#xff0c;与地线相连&#xff0c;还可以减小环路面积。 …

基于蛙跳算法求解简单调度问题附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

python与Electron联合编程记录之九(Electron与Flask联合编程实现)

前面铺垫了这么多&#xff0c;这一节就要真正的实现Electron与python联合编程。这一节我通过加法器这个简单的例子来演示如何真正实现Electron和Flask联合编程。 1、安装Axios包 在终端工具选项卡中输入如下命令安装Axios包: npm i --save-dev axios2、项目结构 项目结构如下…

C语言源代码系列-管理系统之家庭财务小管家

往期文章分享点击跳转>《导航贴》- Unity手册&#xff0c;系统实战学习点击跳转>《导航贴》- Android手册&#xff0c;重温移动开发 &#x1f449;关于作者 众所周知&#xff0c;人生是一个漫长的流程&#xff0c;不断克服困难&#xff0c;不断反思前进的过程。在这个过…

COLMAP输出的文件类型(bin, txt)

默认情况下&#xff0c;COLMAP使用二进制文件格式(bin&#xff0c;机器可读&#xff0c;速度速)来存储稀疏模型。此外&#xff0c;COLMAP也可以将稀疏模型存储为文本文件(txt&#xff0c;人类可读&#xff0c;速度慢)。在这两种情况下&#xff0c;模型导出的信息被分为关于相机…

【吴恩达机器学习笔记】三、矩阵

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4e3;专栏定位&#xff1a;为学习吴恩达机器学习视频的同学提供的随堂笔记。 &#x1f4da;专栏简介&#xff1a;在这个专栏&#xff0c;我将整理吴恩达机器学习视频的所有内容的笔记&…

Cygwin安装

Cygwin是一个在Windows平台上运行的类UNIX模拟环境&#xff0c;在其提供的命令行界面中完美地兼容了Windows和Linux的命令行指令&#xff0c;安装和使用教程很容易百度到&#xff0c;可从官网下载安装包&#xff1a;Cygwin官网。安装步骤如下所示&#xff0c;也可自行百度安装方…