Linux多进程和多线程(二)-进程间通信-管道用法

news2024/11/25 17:18:03
  • 进程间通信
    • 关于多进程的通信
    • 管道
    • 无名管道(匿名管道)
    • 创建无名管道
    • 示例:创建子进程,父进程通过管道向子进程发送消息
    • 无名管道(匿名管道) 的特点
  • 有名管道(命名管道)
    • 创建有名管道需要调⽤ mkfifo() 函数
    • 示例:创建两个没有关联关系的进程,通过有名管道通信
  • 注意:
    • 缺点
    • 优点
  • 关于判断管道是否存在,可以使用errno
    • errno

进程间通信

关于多进程的通信

  • linux 下的进程通信⼿段基本上是从 Unix 平台上的进程通信⼿段继承⽽来的。
  • 每个进程都有⾃⼰独⽴的地址空间, 当两个不同进程需要进⾏交互时, 就需要使⽤进程间通讯
  • 进程间通讯分为单个计算机的进程间通讯与局域⽹的计算机的进程间通讯
  • 进程间通讯⽅式有 管道, 信号, 消息队列, 共享内存,⽹络

管道

  • 管道分为 ⽆名管道(匿名管道) 与 有名管道
    • ⽆名管道⽤于⽗⼦进程之间通讯
    • 有名管道⽤于任意进程之间通讯

管道的本质是在内存建⽴⼀段缓冲区,由操作系统内核来负责创建与管理, 具体通讯模型如下:

在这里插入图片描述

无名管道(匿名管道)

- 无名管道属于单向通讯
- ⽆名管道只能⽤于 ⽗⼦进程通讯
- ⽆名管道发送端叫做 写端, 接收端叫做 读端
- ⽆名管道读端与写端抽象成两个⽂件进⾏操作,在⽆名管道创建成功之后,则会返回读端与写端的⽂件描述符

创建无名管道

  • 创建⽆名管道需要系统调用 pipe()
  • pipe() 函数原型如下:

在这里插入图片描述

#include <unistd.h>

int pipe(int pipefd[2]);
  • pipefd[2] 是一个数组,数组的两个元素分别代表读端和写端的⽂件描述符
  • pipefd[0] 代表读端,pipefd[1] 代表写端
  • 函数成功返回 0,否则返回 -1 并设置 errno 变量

示例:创建子进程,父进程通过管道向子进程发送消息

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    //创建子进程,父进程通过管道向子进程发送消息
    pid_t cpid;//子进程ID
    int ret;//返回值
    int pipefd[2];//管道文件描述符

    ret = pipe(pipefd);//创建管道,内核会将文件描述符号放入pipefd数组中
    if (ret < 0) {//创建失败
        perror("pipe");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1

    }
    cpid = fork();//创建子进程
    if (cpid < 0) {//创建失败
        perror("fork");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1

    }else if (cpid == 0) {//子进程

        ssize_t rbytes;//此变量在父进程不存在,在子进程中使用 //ssize_t是read函数的返回类型,表示读取的字节数
        close(pipefd[1]);//关闭写端,子进程只读

        //操作管道
        char buf[1024];
        printf("子进程开始等待父进程发送消息...\n");
        rbytes = read(pipefd[0], buf, sizeof(buf));//从管道中读取数据,当管道没有数据,没有相应的进程往管道写,这里会阻塞
        if (rbytes < 0) {//读取失败
            perror("read");//打印错误信息
            close(pipefd[0]);//关闭读端
            exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
        }
        //读到了数据
        printf("子进程收到消息: %s\n", buf);//打印接收到的消息
        //操作结束


        close(pipefd[0]);//最后关闭子进程的读端,避免阻塞
    }else if (cpid > 0) {//父进程
        ssize_t wbytes;//此变量在父进程中使用,在子进程不存在
        close(pipefd[0]);//关闭读端,父进程只写

        //操作管道
        char buf[1024];
        strcpy(buf, "Hello, child!");//发送的消息
        wbytes = write(pipefd[1], buf, strlen(buf));
        if (wbytes < 0) {//发送失败
            perror("write");//打印错误信息
            wait(NULL);//此时写错误要,等待子进程结束
            close(pipefd[1]);//关闭写端
            exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
        }
        //发送成功
        printf("父进程发送消息: %s\n", buf);//打印发送的消息
        //操作结束

        close(pipefd[1]);//最后关闭父进程的写端,
        wait(NULL);//正常等待子进程结束

    }
    return 0;
}

无名管道(匿名管道) 的特点

当管道为空,读管道会阻塞读进程

当管道的写端被关闭了,从管道中读取剩余的数据后 会返回0,表示管道已经被关闭

在写入管道时,确保不会超过管道的容量, PIPE_BUF 4096;//管道缓冲区大小

  • 当写入的数据达到PIPE_BUF时,write函数会阻塞,直到管道中有足够的空间以原子的方式完成方式

  • 当写入的数据大于PIPE_BUF时,write函数会尽可能多的传输数据,充满这个管道

管道的大小是有限的,不能让父/子进程同时对管道进行读/写操作,否则会导致数据混乱

当一个进程试图向一个管道写入数据,但是没有任何进程拥有该管道的打开着的读取描述符,内核向写入进程发送一个SIGPIPE信号

有名管道(命名管道)

有名管道是在 ⽂件系统中可⻅的⽂件, 但是不占⽤磁盘空间, 仍然在内存中, 可以通过 mkfifo 命令创建有名管道

有名管道与⽆名管道⼀样,在应⽤层是基于⽂件接口进⾏操作

有名管道⽤于 任意进程之间的通讯, 当管道为空时, 读进程会阻塞.

在文件系统中:
文件名以 "p"开头,说明是管道文件,他占用的是内存空间, 并不占用磁盘空间
在这里插入图片描述

创建有名管道需要调⽤ mkfifo() 函数

函数头文件:

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

函数原型:

int mkfifo(const char *pathname, mode_t mode);

参数:

  • pathname: 管道文件路径名
  • mode: 管道文件的权限, 如 0666

函数返回值:
成功返回0, 失败返回-1, 并设置errno变量

示例:创建两个没有关联关系的进程,通过有名管道通信

  • 读的进程代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
/*
 * 无名管道(匿名管道) 和 有名管道(命名管道)
 * */

/*
 * 无名管道(匿名管道)
 */
//创建子进程,父进程通过管道向子进程发送消息
int main1() {

    pid_t cpid;//子进程ID
    int ret;//返回值
    int pipefd[2];//管道文件描述符
    //PIPE_BUF ;//管道缓冲区大小 4096
    ret = pipe(pipefd);//创建管道,内核会将文件描述符号放入pipefd数组中
    if (ret < 0) {//创建失败
        perror("pipe");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1

    }
    cpid = fork();//创建子进程
    if (cpid < 0) {//创建失败
        perror("fork");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1

    }else if (cpid == 0) {//子进程

        ssize_t rbytes;//此变量在父进程不存在,在子进程中使用 //ssize_t是read函数的返回类型,表示读取的字节数
        close(pipefd[1]);//关闭写端,子进程只读

        //操作管道
        char buf[1024];
        printf("子进程开始等待父进程发送消息...\n");
        rbytes = read(pipefd[0], buf, sizeof(buf));//从管道中读取数据,当管道没有数据,没有相应的进程往管道写,这里会阻塞
        if (rbytes < 0) {//读取失败
            perror("read");//打印错误信息
            close(pipefd[0]);//关闭读端
            exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
        }
        //读到了数据
        printf("子进程收到消息: %s\n", buf);//打印接收到的消息
        //操作结束


        close(pipefd[0]);//最后关闭子进程的读端,避免阻塞
    }else if (cpid > 0) {//父进程
        ssize_t wbytes;//此变量在父进程中使用,在子进程不存在
        close(pipefd[0]);//关闭读端,父进程只写

        //操作管道
        char buf[1024];
        strcpy(buf, "Hello, child!");//发送的消息
        wbytes = write(pipefd[1], buf, strlen(buf));
        if (wbytes < 0) {//发送失败
            perror("write");//打印错误信息
            wait(NULL);//此时写错误要,等待子进程结束
            close(pipefd[1]);//关闭写端
            exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
        }
        //发送成功
        printf("父进程发送消息: %s\n", buf);//打印发送的消息
        //操作结束

        close(pipefd[1]);//最后关闭父进程的写端,
        wait(NULL);//正常等待子进程结束

    }
    return 0;
}


/*
 * 有名管道(命名管道)
 */
//创建两个没有关联关系的进程,通过有名管道通信
//下方是读的进程代码,写的进程代码在另外一个文件Process2中
#define PATHNAME "/home/gopher/ClionWork/fifo_test"
int main(){
    int  ret;//创建有名管道返回值
    int fd;//打开文件返回值
    ssize_t rbytes;//读取文件返回值
    char buf[1024]={0};//读取文件缓冲区

    ret= mkfifo(PATHNAME, 0666);//创建有名管道

//  如果管道存在了,下面代码判断条件会报错
//    if (ret < 0) {//创建失败
//        perror("mkfifo");//打印错误信息
//        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
//    }

    //使用新的判断条件保证管道存在,不会报错
    //关于  errno  文章末尾有更多介绍 他是一个全局变量,用来存放系统调用的错误信息,它用于指示最近一次系统调用或库函数调用中发生的错误类型。
    //errno 等于 ( EEXIST = 17 表示文件存在 EEXIST 是 POSIX 标准中的一个宏) 表示管道已经存在
    if (ret < 0) { // 创建失败
        if (errno == EEXIST) {
            // 管道已经存在
            printf("管道已存在,继续执行程序。\n");
        } else {
            // 打印错误信息
            perror("mkfifo");
            // 退出程序
            exit(EXIT_FAILURE);
        }
    } else {
        printf("管道创建成功。\n");
    }


    fd = open(PATHNAME, O_RDWR);//打开有名管道
    if (fd < 0) {//打开失败
        perror("open");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
    }
    //开始读
    printf("进程等待读消息...\n");
    rbytes = read(fd, buf, sizeof(buf));
    if (rbytes < 0) {//读取失败
        perror("read");//打印错误信息
        close(fd);//关闭有名管道
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
    }
    printf("进程收到消息: %s\n", buf);//打印接收到的消息
    //读结束

    close(fd);//关闭有名管道
    return 0;
}

  • 写的进程代码
/*
 * 有名管道(命名管道)
 */
//创建两个没有关联关系的进程,通过有名管道通信
//这是写的进程
#define PATHNAME "/home/gopher/ClionWork/fifo_test"
int main(){
//    int  ret;//创建有名管道返回值
    int fd;//打开文件返回值
    ssize_t wbytes;//读取文件返回值
    char buf[]={"hello world"};//读取文件缓冲区

//读的进程已经创建了
//    ret= mkfifo(PATHNAME, 0666);//创建有名管道
//    if (ret < 0) {//创建失败
//        perror("mkfifo");//打印错误信息
//        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
//    }

    fd = open(PATHNAME, O_WRONLY);//打开有名管道
    if (fd < 0) {//打开失败
        perror("open");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
    }
    //写的进程
    printf("进程写入消息...\n");
    wbytes = write(fd, buf, strlen(buf));//写入消息到有名管道
    if (wbytes < 0) {//读取失败
        perror("read");//打印错误信息
        close(fd);//关闭有名管道
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
    }

    close(fd);//关闭有名管道
    return 0;
}

注意:

如果有名管道的⼀端以只读⽅式打开,它会阻塞到另⼀端以写的⽅式 (只写,读写)

如果有名管道的⼀端以只写⽅式打开,它会阻塞到另⼀端以读的⽅式 (只读,读写)

如果有名管道的⼀端以读写⽅式打开,它不会阻塞,因为读写方式既支持读取又支持写入,满足了管道通信的要求。

有名管道在不同情况下的阻塞特性。当有名管道的一端以只读或只写模式打开时,会等待另一端的对应模式(写或读)来完成通信,从而进行阻塞。而当管道的一端以读写模式打开时,由于该模式同时支持读取和写入,所以不会出现阻塞现象。

确保在合适的场景下选择合适的文件打开模式,避免不必要的阻塞,从而提高程序运行的效率和稳定性。

缺点

  • 打开时需要读写一起进行,否则会阻塞,管道大小是PIPE_BUF 4096字节
  • 半双工的工作模式,如果和多个进程通信,需要创建多个管道

半双工(Half-Duplex)通信是一种数据传输模式,允许设备在同一个信道上进行双向通信,但不能同时进行。

这意味着同一时间内,数据只能在一个方向上传输,而另一个方向则需要等待。

举个例子,对讲机就是典型的半双工设备。当一个人在发言时,另一方只能接收,无法同时发言。要进行双向交流,双方需要轮流按下发话键来切换发送和接收状态。

半双工通信的优点包括:

节省带宽:因为信道资源是共享的,减少了频带的使用。

简单实现:相对于全双工,半双工的实现原理和机制更为简单,有助于降低设备成本。

缺点则主要在于效率较低,因为在同一个时间内只能单方向传输数据,可能导致通信延迟。

优点

  • 可以实现任意进程间的通信,操作起来和文件操作一样

关于判断管道是否存在,可以使用errno

errno

使用他要引入errno.h

errno 是一个全局变量,在几乎所有的 C 语言和 POSIX 标准的编程环境中都存在。

它用于指示最近一次系统调用或库函数调用中发生的错误类型。

即,通过检查 errno 的值,程序员可以确定错误发生的原因并进行相应的处理。

每次系统调用或库函数调用失败时,会设置 errno 的值。
不同的错误类型由不同的错误码标示,
例如,EEXIST 表示试图创建一个已经存在的文件或目录,ENOMEM 表示内存不足等等。

在 POSIX 的环境中,以下是一些常用的错误码及其说明:
所有错误码都定义在 errno-base.h 头文件中。

EEXIST - 文件已存在。

ENOENT - 文件或目录不存在。

ENOMEM - 内存不足。

EACCES - 权限被拒绝。

EINVAL - 无效的参数。

EBUSY - 设备或资源正忙。

EAGAIN - 资源暂时不可用。

EPIPE - 管道已关闭。

EINTR - 系统调用被中断。

EIO - 输入/输出错误。

EISDIR - 试图打开一个目录。

EFBIG - 文件太大。

EFBIG - 文件太大。

EOVERFLOW - 值过大。

会设置 errno 的值。
不同的错误类型由不同的错误码标示,
例如,EEXIST 表示试图创建一个已经存在的文件或目录,ENOMEM 表示内存不足等等。

在 POSIX 的环境中,以下是一些常用的错误码及其说明:
所有错误码都定义在 errno-base.h 头文件中。

EEXIST - 文件已存在。

ENOENT - 文件或目录不存在。

ENOMEM - 内存不足。

EACCES - 权限被拒绝。

EINVAL - 无效的参数。

EBUSY - 设备或资源正忙。

EAGAIN - 资源暂时不可用。

EPIPE - 管道已关闭。

EINTR - 系统调用被中断。

EIO - 输入/输出错误。

EISDIR - 试图打开一个目录。

EFBIG - 文件太大。

EFBIG - 文件太大。

EOVERFLOW - 值过大。

errno 提供了一种标准化的方式来报告和处理错误,使得程序可靠和健壮。

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

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

相关文章

烧结刚玉砂轮片 磨具用晶谷低温陶瓷结合剂玻璃粉

晶谷CBN 砂轮磨具用低温陶瓷结合剂玻璃粉的一些特点如下&#xff1a; - 软化点&#xff1a;通常为450~650度&#xff1b; - 膨胀系数&#xff1a;50~12010-7&#xff1b; - 粒径&#xff1a;300~3000目&#xff08;可按要求订做&#xff09;&#xff1b; - 外观颜色&#xff…

6. 较全的Open3D点云数据处理(python)

注意&#xff1a;以下内容来自博客爆肝5万字❤️Open3D 点云数据处理基础&#xff08;Python版&#xff09;_python 点云 焊缝-CSDN博客&#xff0c;这篇博客写的全且详细&#xff0c;在这里是为了记笔记方便查看&#xff0c;并非抄袭。 1.点云的读写 代码如下&#xff1a; …

使用Perplexity打造产品的27种方式

ChatGPT和Perplexity等聊天机器人正迅速成为产品经理的首选助手。以下是一份全面的指南&#xff0c;介绍PM如何在日常工作中使用Perplexity&#xff0c;该指南基于300多份回复和30次电话后的总结。 理解并制定增长战略&#xff1a;例如&#xff0c;解释增长会计的基本原理&…

什么是 Elasticsearch 数据预热?

引言&#xff1a;在现代的信息检索和数据分析领域&#xff0c;Elasticsearch 已经成为一个广泛应用的分布式搜索和分析引擎。作为开源项目的一部分&#xff0c;Elasticsearch 提供了强大的实时搜索和分析能力&#xff0c;使得处理大规模数据变得更加高效和可靠。然而&#xff0…

MySQL高级-MVCC-undo log 版本链

文章目录 1、undo log2、undo log 版本链2.1、然后&#xff0c;有四个并发事务同时在访问这张表。2.1.1、修改id为30记录&#xff0c;age改为32.1.2、修改id为30记录&#xff0c;name改为A32.1.3、修改id为30记录&#xff0c;age改为10 2.2、总结 1、undo log 回滚日志&#xf…

运行vue : 无法加载文件 C:\Program Files\nodejs\node_global\vue.ps1,因为在此系统上禁止运行脚本。

报错背景: 重装了win10系统,然后准备安装Vue,这个时候我已经安装好了node.js和npm,输入node -v和npm -v都有正确输出,但是每次输入npm install -g vue/cli 安装的时候,就会报错. 大家安装node.js的时候最好就是默认路径(C:\Program Files\nodejs),别去修改不然很多报错.(个人…

Ubuntu网络管理命令:netstat

安装Ubuntu桌面系统&#xff08;虚拟机&#xff09;_虚拟机安装ubuntu桌面版-CSDN博客 顾名思义&#xff0c;netstat命令不是用来配置网络的&#xff0c;而是用来查看各种网络信息的&#xff0c;包括网络连接、路由表以及网络接口的各种统计数据等。 netstat命令的基本语法如…

3-linux命令行与基本命令

目录 什么是shell linux命令 命令组成 几个简单的命令 linux文件系统导航 什么是shell linux学习路径&#xff1a;学习shell→配置和环境→见任务和主要工具→编写shell脚本 shell是一个接收由键盘输入的命令&#xff0c;并将其传递给操作系统来执行的程序。几乎所有…

Coursera耶鲁大学金融课程:Financial Markets 笔记Week 03

Financial Markets 本文是学习 https://www.coursera.org/learn/financial-markets-global这门课的学习笔记 这门课的老师是耶鲁大学的Robert Shiller https://en.wikipedia.org/wiki/Robert_J._Shiller Robert James Shiller (born March 29, 1946)[4] is an American econom…

cookie 的获取过程

#第一次请求过程 浏览器第一次发送请求时,不会携带任何cookie信息 服务器接收到请求之后&#xff0c;发现请求中没有任何cookie信息 服务器生成和设置一个cookie.并将此cookie设置通过set_cookie的首部字段保存在响应报文中返回给浏 览器 浏览器接收到这个响应报文之后,发现里…

windows 本地ES 7.11.0 版本集群搭建

1.先下载JDK &#xff0c;建议下载JDK11. 2.下载ES Windows安装包 7.11.0下载 3.下载完成后&#xff0c;在本地解压三份&#xff0c;分别取名 node1,node2,node3 4.若启动一直报端口占用&#xff0c;这修改 每个文件夹下bin/elasticsearch.bat文件&#xff0c;没有则跳过 在…

电商平台数据爬取经验分享

一、引言 在电商领域&#xff0c;数据的重要性不言而喻。无论是市场趋势分析、竞争对手研究&#xff0c;还是用户行为洞察&#xff0c;都离不开数据的支持。而数据爬虫作为获取这些数据的重要工具&#xff0c;其技术的掌握和运用对于电商平台来说至关重要。本文将结合个人实际…

Stateflow快速入门系列(-):构造并运行 Stateflow 图

Stateflow 提供了一种图形语言&#xff0c;包括状态转移图、流程图、状态转移表和真值表。您可以使用 Stateflow 来说明 MATLAB 算法和 Simulink 模型如何响应输入信号、事件和基于时间的条件。 Stateflow 使您能够设计和开发监控、任务调度、故障管理、通信协议、用户界面和混…

昇思MindSpore学习笔记7--函数式自动微分

摘要&#xff1a; 介绍了昇思MindSpore神经网络训练反向传播算法中函数式自动微分的使用方法和步骤。包括构造计算函数和神经网络、grad获得微分函数&#xff0c;以及如何处理停止渐变、获取辅助数据等内容。 一、概念要点 神经网络训练主要使用反向传播算法&#xff1a; 准备…

学习平台推荐_菜鸟教程官网

网址&#xff1a; 菜鸟教程 - 学的不仅是技术&#xff0c;更是梦想&#xff01;菜鸟教程(www.runoob.com)提供了编程的基础技术教程, 介绍了HTML、CSS、Javascript、Python&#xff0c;Java&#xff0c;Ruby&#xff0c;C&#xff0c;PHP , MySQL等各种编程语言的基础知识。 同…

[数据集][目标检测]猪只状态吃喝睡站检测数据集VOC+YOLO格式530张4类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;530 标注数量(xml文件个数)&#xff1a;530 标注数量(txt文件个数)&#xff1a;530 标注类别…

【操作系统期末速成】EP06 | 学习笔记(基于五道口一只鸭)

文章目录 一、前言&#x1f680;&#x1f680;&#x1f680;二、正文&#xff1a;☀️☀️☀️2.1 考点十四&#xff1a;同步互斥的基本概念2.2 考点十五&#xff1a;实现临界区互斥的基本方法2.3 考点十六&#xff1a;信号量的含义及常用信号量 一、前言&#x1f680;&#x1…

土体中应力的计算

土中的应力的计算 非水面以下土体中应力的计算&#xff1a;水面以下的土中的应力计算 参考视频&#xff1a; https://www.bilibili.com/video/BV1Rh411J72h/?spm_id_from333.788&vd_source02b2bad477a153eaeb9c48cbbedaf8df 非水面以下土体中应力的计算&#xff1a; 按成…

深入理解 Spring MVC:原理与架构解析

文章目录 前言一、MVC二、Spring MVC三、Spring MVC 工作流程四、小结推荐阅读 前言 Spring MVC 是一种基于 Java 的 Web 应用开发框架&#xff0c;它通过模型-视图-控制器&#xff08;Model-View-Controller, MVC&#xff09;的设计模式来组织和管理 Web 应用程序。本文将深入…

【Uniapp微信小程序】图片左右分割/分割线切割图片/图片批量分割线切割

特别说明&#xff1a;本文章因业务组件功能&#xff0c;不完全开放/暂vip可见&#xff0c;有需要者留言找博主&#xff01; ps&#xff1a;注意&#xff01;&#xff01;本效果为图片分割切割&#xff01;&#xff01;不是文档切割&#xff01;&#xff01;图片仅供参考&#x…