[Linux]进程间通信--管道

news2025/1/19 11:28:10

[Linux]进程间通信–管道

文章目录

  • [Linux]进程间通信--管道
    • 进程间通信的目的
    • 实现进程间通信的原理
    • 匿名管道
      • 匿名管道的通信原理
      • 系统接口
      • 管道特性
      • 管道的协同场景
      • 管道的大小
    • 命名管道
      • 使用指令创建命名管道
      • 使用系统调用创建命名管道

进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程 。
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

实现进程间通信的原理

进程是具有独立性的,一个进程是无法看到另一个进程的代码和数据的,为了让进程间通信,要做的工作就是让不同的进程看到同一份“资源”。

任何进程通信手段需要解决的问题如下:

  • 让不同的进程看到同一份“资源”
  • 让一方进行读取,另一方进行写入

不同的进程间通信手段本质的区别就是让不同的进程看到同一份“资源”的方式不同。

匿名管道

匿名管道是一种以文件为媒介的通信方式,匿名管道是一个内存级别的文件,拥有和普通文件一样的缓冲区,但是操作系统不会将缓冲区刷新至外设,匿名管道虽然是文件,但是由于没有文件路径,进程是无法通过系统文件接口来操作的,因此匿名管道通常用于父子进程之间使用。

匿名管道的通信原理

由于匿名管道没有文件路径,进程是无法通过系统文件接口来操作的特性,匿名管道必须通过父进程创建,子进程继承父进程文件描述符表的方式,使得不同的进程看到同一个文件:

image-20230909104154011

由于匿名管道只支持单向通信,在使用匿名管道进行通信时,父进程必须分别以读方式和写方式打开管道文件,子进程继承了文件描述符表后,一方关闭读端,一方关闭写端进行通信。

注意: 如果父进程只以读方式或者写方式打开,子进程继承文件描述符表后,也是同样的方式,子进程自身无法打开该管道,因此导致无法通信。

系统接口

Linux系统提供了创建匿名管道的系统接口pipe:

//pipe所在的头文件和声明
#include <unistd.h>

int pipe(int pipefd[2]);
  • pipefd为输出型参数,用于接收以读方式和写方式打开管道的文件描述符。
  • pipefd[0]获取读端文件描述符,pipefd[1]获取写端文件描述符。
  • 成功返回0,失败返回-1,错误码被设置。

编写如下代码测试pipe接口:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <cstdlib>

using namespace std;

int main()
{
    //创建管道
    int pipefd[2] = { 0 };
    int n = pipe(pipefd);
    if (n < 0)//出错判断
    {
        cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
        exit(1); 
    }
    //创建子进程
    pid_t id = fork();
    assert(id != -1);//出错判断

    //进行通信 -- 父进程进行读取,子进程进行写入
    if (id == 0)
    {
        //子进程
        close(pipefd[0]);
        
        const string str = "hello world";
        int cnt = 1;
        char buffer[1024];
        while(1)
        {
            snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
            write(pipefd[1], buffer, strlen(buffer));//向管道写入数据
            sleep(1);
        }
        close(pipefd[1]);
        exit(0);
    }

    //父进程
    close(pipefd[1]);
    char buffer[1024];
    while(1)
    {
        read(pipefd[0], buffer, sizeof(buffer) - 1);//从管道读取数据
        cout << "我是父进程," << "child give me: " << buffer << endl;
    }
    close(pipefd[0]);
    return 0;
}

编译代码运行查看结果:

pipe1演示

从运行结果可以看出,建立管道后,父子进程就能够进行数据通信。

管道特性

  1. 单向通信,半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
  2. 管道的本质是文件,因此管道的生命周期随进程
  3. 管道通信,通常适用于具有“血缘关系的进程”,诸如父子进程、兄弟进程等
  4. 管道的数据是以字节流的形式传输的,读写次数的多数不是强相关的
  5. 具有一定的协同机制

管道的协同场景

场景一: 如果管道内部的数据被读端读取完了,写端不写入,读端就只能等待

编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <cstdlib>

using namespace std;

int main()
{
    //创建管道
    int pipefd[2] = { 0 };
    int n = pipe(pipefd);
    if (n < 0)
    {
        cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
        exit(1); 
    }
    //创建子进程
    pid_t id = fork();
    assert(id != -1);

    //进行通信 -- 父进程进行读取,子进程进行写入
    if (id == 0)
    {
        //子进程
        close(pipefd[0]);
        
        const string str = "hello world";
        int cnt = 1;
        char buffer[1024];
        while(1)
        {
            snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
            write(pipefd[1], buffer, strlen(buffer));
            sleep(100); // ---------  模拟写入暂停  --------- 
        }
        close(pipefd[1]);
        exit(0);
    }

    //父进程
    close(pipefd[1]);
    char buffer[1024];
    while(1)
    {
        read(pipefd[0], buffer, sizeof(buffer) - 1);
        cout << "我是父进程," << "child give me: " << buffer << endl;
    }
    close(pipefd[0]);
    return 0;
}

编译代码运行查看结果:

pipe2演示

场景二: 如果管道内部的数据被写端写满了,读端不读取,写端无法继续写入

编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <cstdlib>

using namespace std;

int main()
{
    //创建管道
    int pipefd[2] = { 0 };
    int n = pipe(pipefd);
    if (n < 0)
    {
        cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
        exit(1); 
    }
    //创建子进程
    pid_t id = fork();
    assert(id != -1);

    //进行通信 -- 父进程进行读取,子进程进行写入
    if (id == 0)
    {
        //子进程
        close(pipefd[0]);
        
        const string str = "hello world";
        int cnt = 1;
        char buffer[1024];
        while(1)
        {
            snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
            write(pipefd[1], buffer, strlen(buffer));
            printf("cnt: %d\n", cnt); // ---------  显示写入过程  --------- 
            //sleep(100);
        }
        close(pipefd[1]);
        exit(0);
    }

    //父进程
    close(pipefd[1]);
    char buffer[1024];
    while(1)
    {
        sleep(100); // ---------  模拟读取暂停  --------- 
        read(pipefd[0], buffer, sizeof(buffer) - 1);
        cout << "我是父进程," << "child give me: " << buffer << endl;
    }
    close(pipefd[0]);
    return 0;
}

编译代码运行查看结果:

pipe3演示

场景三: 写端关闭,读端读完了管道内部的数据时,再读就读到了文件的结尾。

编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <cstdlib>

using namespace std;

int main()
{
    //创建管道
    int pipefd[2] = { 0 };
    int n = pipe(pipefd);
    if (n < 0)
    {
        cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
        exit(1); 
    }
    //创建子进程
    pid_t id = fork();
    assert(id != -1);

    //进行通信 -- 父进程进行读取,子进程进行写入
    if (id == 0)
    {
        //子进程
        close(pipefd[0]);
        
        const string str = "hello world";
        int cnt = 1;
        char buffer[1024];
        while(1)
        {
            snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
            write(pipefd[1], buffer, strlen(buffer));
            printf("cnt: %d\n", cnt);
            sleep(1);
            if (cnt == 5) break; // ---------  写端关闭  --------- 
        }
        close(pipefd[1]);
        exit(0);
    }

    //父进程
    close(pipefd[1]);
    char buffer[1024];
    while(1)
    {
        int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            cout << "我是父进程," << "child give me: " << buffer << endl;
        }
        else if (n == 0)// ---------  判断读取到文件末尾  --------- 
        {
            cout << "读取完毕, 读到文件结尾" << endl;
            break;
        }
        else
        {
            cout << "读取出错" << endl;
            break;
        }
    }
    close(pipefd[0]);
    return 0;
}

编译代码运行查看结果:

pipe4演示

**场景四:**写端一直写,读端关闭,操作系统会给写端发送13号信号终止进程。

编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>

using namespace std;

int main()
{
    //创建管道
    int pipefd[2] = { 0 };
    int n = pipe(pipefd);
    if (n < 0)
    {
        cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
        exit(1); 
    }
    //创建子进程
    pid_t id = fork();
    assert(id != -1);

    //进行通信 -- 父进程进行读取,子进程进行写入
    if (id == 0)
    {
        //子进程
        close(pipefd[0]);
        
        const string str = "hello world";
        int cnt = 1;
        char buffer[1024];
        while(1)
        {
            snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
            write(pipefd[1], buffer, strlen(buffer));
            printf("cnt: %d\n", cnt);
            sleep(1);
        }
        close(pipefd[1]);
        exit(0);
    }

    //父进程
    close(pipefd[1]);
    char buffer[1024];
    while(1)
    {
        int cnt = 0;
        //sleep(100);
        int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            cout << "我是父进程," << "child give me: " << buffer << endl;
        }
        else if (n == 0)
        {
            cout << "读取完毕, 读到文件结尾" << endl;
            break;
        }
        else
        {
            cout << "读取出错" << endl;
            break;
        }
        //sleep(100);
        sleep(5);
        break;// ---------  读端关闭  --------- 
    }
    close(pipefd[0]);
    int status = 0;
    waitpid(id, &status, 0);
    cout << "signal: " << (status & 0x7F) << endl;// --------- 回收子进程获取退出信号  --------- 
    sleep(3);
    return 0;
}

编译代码运行查看结果:

pipe5演示

管道的大小

在Linux下,管道(Pipe)的大小受到操作系统的限制。具体来说,管道的大小由内核参数PIPE_BUF定义,通常是4096个字节。

  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

命名管道

命名管道同样是内存级的文件,和匿名管道的区别就是命名管道可以在指定路径下创建,并且命名可以指定,因此命名管道可以给任何两个不同的进程用于通信。

使用指令创建命名管道

Linux下使用mkfifo 指令就可以在指定路径下创建命名管道。

image-20230910162001046

命名管道同样和匿名管道一样满足管道的协同场景:

namepipe2演示

写端尝试打开管道文件,没有读端,写端就会卡在打开文件这一步骤。

namepipe1演示

右侧读端开始会等待写端写入,后续关闭右侧读端,左侧写端进程直接被终止。

使用系统调用创建命名管道

//mkfifo所在的头文件和声明
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • pathname参数 – 创建命名管道的路径
  • mode参数 – 创建命名管道的文件权限
  • 成功返回0,失败返回-1,错误码被设置。

为了测试mkfifo接口编写代码进行测试,首先设置文件结构如下:

image-20230910164247561

makefile文件内容如下:

.PHONY:all
all:client server

client:client.cc
	g++ -o $@ $^ -std=c++11

server:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -rf client server

common.hpp主要用于让两个进程获取管道路径,具体内容如下:

#include <iostream>
#include <string>

#define NUM 1024

const std::string pipename = "./namepipe"; //管道的路径和管道名

mode_t mode = 0666; //创建管道的文件权限

client.cc作为写端输入数据,具体内容如下:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <cassert>
#include "commn.hpp"

int main()
{
    // 打开管道文件
    int wfd = open(pipename.c_str(), O_WRONLY);
    if (wfd < 0)
    {
        std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
        exit(1);
    }

    //进行通信
    while(true)
    {
        char buffer[NUM];
        std::cout << "请输入内容:";
        fgets(buffer, sizeof(buffer), stdin);//获取用户输入
        buffer[strlen(buffer) - 1] = 0;

        if (strcasecmp(buffer, "quit") == 0) break;//用户输入quit退出进程

        ssize_t size = write(wfd, buffer, strlen(buffer));
        assert(size >= 0);
        (void)size;
    }

    close(wfd);
    return 0;
}

server.cc作为读端用于接收写端的输入并打印,具体内容如下:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include "commn.hpp"

int main()
{
    umask(0);
    // 创建管道文件
    int n = mkfifo(pipename.c_str(), mode);
    if (n < 0)
    {
        std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
        exit(1);
    }
    std::cout << "create fifo file success" << std::endl;

    // 以读方式打开管道文件
    int rfd = open(pipename.c_str(), O_RDONLY);
    if (rfd < 0)
    {
        std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
        exit(2);
    }

    // 进行通信
    while (true)
    {
        char buffer[NUM];

        ssize_t size = read(rfd, buffer, sizeof(buffer) - 1);
        buffer[size] = 0;
        if (size > 0)
        {
            std::cout << "client send me :" << buffer << std::endl;//输出接收的信息
        }
        else if (size == 0)
        {
            std::cout << "client quit, me too!" << std::endl;
            break;
        }
        else
        {
            std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
            break;
        }
    }

    close(rfd);
    unlink(pipename.c_str()); // 删除文件
    return 0;
}

编译代码运行查看结果:

namepipe3演示

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

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

相关文章

JL-A/41 JL-A/42 JL-A/43 集成电路电流继电器 过负荷或短路 JOSEF约瑟

JL-A、B集成电路电流继电器 JL-A/11 JL-A/31 JL-A/12 JL-A/32 JL-A/13 JL-A/33 JL-A/21 JL-A/22 JL-A/23 JL-A/34 JL-A/35 JL-B/41 JL-A/42 JL-B/43 JL-B/11 JL-B/31 JL-B/12 JL-B/32 JL-B/13 JL-B/33 JL-B/21 JL-B/22 JL-B/23 JL-B/34 JL-B/35 JL-B/41 JL-B/42 …

Android性能优化之应用瘦身(APK瘦身)

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、人工智能等&#xff0c;希望大家多多支持。 目录 一、导读二、概览2.1 apk组成 三、优化方向3.1 源代码3.1.1 代码混…

bootstrap-datepicker实现只能选择每一年的某一个月份

1、问题描述 最近碰到一个需求&#xff0c;要求日期控件选择的时候&#xff0c;只能选择某一年的1月份。 2、解决方法 使用setStartDate()和setEndDate()函数对日期选择框进行范围限制。 3、我的代码 【免费】bootstrap-datepicker实现只能选择每一年的某一个月份资源-CSDN文库…

JavaWeb_LeadNews_Day12-jenkins

JavaWeb_LeadNews_Day12-jenkins 后端项目部署多环境配置切换服务集成docker配置父工程项目构建构建微服务部署服务到远程服务器整体思路安装私有仓库jenkins插件部署服务准备工作部署服务 jenkins触发器来源Gitee 后端项目部署 多环境配置切换 在微服务中的bootstrap.yml中新…

Dajngo06_Template模板

Dajngo06_Template模板 6.1 Template模板概述 模板引擎是一种可以让开发者把服务端数据填充到html网页中完成渲染效果的技术 静态网页&#xff1a;页面上的数据都是写死的&#xff0c;万年不变 动态网页&#xff1a;页面上的数据是从后端动态获取的&#xff08;后端获取数据库…

二叉树详解(求二叉树的结点个数、深度、第k层的个数、遍历等)

二叉树&#xff0c;是一种特殊的树&#xff0c;特点是树的度小于等于2&#xff08;树的度是整个树的结点的度的最大值&#xff09;&#xff0c;由于该特性&#xff0c;构建二叉树的结点只有三个成员&#xff0c;结点的值和指向结点左、右子树的指针。 typedef int DateType; t…

长亭雷池社区版本安装与使用

0x01 雷池介绍 一款足够简单、足够好用、足够强的免费 WAF。基于业界领先的语义引擎检测技术&#xff0c;作为反向代理接入&#xff0c;保护你的网站不受黑客攻击。核心检测能力由智能语义分析算法驱动&#xff0c;专为社区而生&#xff0c;不让黑客越雷池半步。 官方网址&…

第一类曲线积分与二重积分在极坐标系下表示的区别

1.第一类曲线积分与二重积分在极坐标系下表示的区别 区别主要来源于一是曲线积分的积分区域为边界&#xff0c;而二重积分的积分区域为内部边界&#xff0c;二是极点位置选取的不同&#xff0c;二者共同造成在积分区域在极坐标下表示的不同&#xff0c;即 ρ \rho ρ是常量还是…

解决谷歌浏览器会http网站自动变成https的问题

不知道是不是升级的缘故&#xff0c;最近打开公司一个http网站&#xff0c;会自动跳去https&#xff0c;用了网上说的这个方案&#xff0c;如下&#xff1a; 但发现还不行&#xff0c;这时我尝试用点击地址栏左边那锁的那个图标&#xff0c;图如下&#xff1a; 然后点击网站设…

Pytest系列-数据驱动@pytest.mark.parametrize(7)

简介 unittest 和 pytest参数化对比&#xff1a; pytest与unittest的一个重要区别就是参数化&#xff0c;unittest框架使用的第三方库ddt来参数化的 而pytest框架&#xff1a; 前置/后置处理函数fixture&#xff0c;它有个参数params专门与request结合使用来传递参数&#x…

【javaSE】 枚举与枚举的使用

文章目录 &#x1f384;枚举的背景及定义⚾枚举特性总结&#xff1a; &#x1f332;枚举的使用&#x1f6a9;switch语句&#x1f6a9;常用方法&#x1f4cc;示例一&#x1f4cc;示例二 &#x1f38d;枚举优点缺点&#x1f334;枚举和反射&#x1f6a9;枚举是否可以通过反射&…

【基本数据结构 三】线性数据结构:栈

学习了数组和链表后,再来看看第三种线性表结构,也就是栈,栈和后边讲的队列一样是一种受限的线性表结构,正是因为其使用有限制,所以对于一些特定的需要操作可控的场合,受限的结构就非常有用。 栈的定义 我们平时放盘子的时候,都是从下往上一个一个放;取的时候,我们也…

矩阵系统全方位管理多平台1000多个账号,实现精准化运营获客!

全自动化视频综合处理工具&#xff01; ​ 普通的剪辑软件是不可能实现自动化&#xff0c;一个人一天制作3000条视频&#xff01;​必须要借助高效率的工具【呆头鹅批量剪辑软件】探店混剪系统&#xff0c;导入大量的素材&#xff0c;就能自动帮你批量处理&#xff0c;满…

第28章_瑞萨MCU零基础入门系列教程之基于面向对象的工程结构

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…

Django05_反向解析

Django05_反向解析 5.1 反向解析概述 随着功能的不断扩展&#xff0c;路由层的 url 发生变化&#xff0c;就需要去更改对应的视图层和模板层的 url&#xff0c;非常麻烦&#xff0c;不便维护。这个时候我们可以通过反向解析&#xff0c;将 url解析成对应的 试图函数 通过 path…

OSCP系列靶场-Esay-Vegeta1保姆级

OSCP系列靶场-Esay-Vegeta1保姆级 目录 OSCP系列靶场-Esay-Vegeta1保姆级总结准备工作信息收集-端口扫描目标开放端口收集目标端口对应服务探测 信息收集-端口测试22-SSH端口的信息收集22-SSH端口版本信息与MSF利用22-SSH协议支持的登录方式22-SSH手动登录尝试(无)22-SSH弱口令…

魔众携手ModStart上线全新模块市场,支持模板主题

ModStart模板主题 对于很多新手或者是缺乏经验的开发者来说&#xff0c;快速建站具有一定的难度&#xff0c;总是一件让人头疼的问题。 ModStart为开发者提供了一些模板主题供开发者选购使用&#xff0c;模块市场包含了丰富的模块&#xff0c;后台一键快速安装&#xff0c;让开…

Botowski:SEO友好的AI内容生成器

【产品介绍】 名称 Botowski 具体描述 Botowski是一个人工智能内容生成器&#xff0c;可以被撰稿人、企业主和其他人用来创建高质量的内容。 它可以创建各种主题的文章、博客文章&#xff0c;甚至散文。Botowski的设计是用户友好的;你所需要做…

Java笔记042-反射章节练习

反射章节练习 练习1&#xff1a;通过反射修改私有成员变量 定义PrivateTest类&#xff0c;有私有name属性&#xff0c;并且属性值为helloKitty提供getName的公有方法创建PrivateTest的类&#xff0c;利用Class类得到私有的name属性&#xff0c;修改私有的name属性值&#xff…

过拟合、欠拟合、泛化误差、训练误差

模型容量的影响&#xff1a; 泛化误差&#xff1a; 当训练的模型的容量过了最优点时&#xff0c;泛化误差反而升高&#xff0c;这是由于模型过于关注细节导致&#xff0c;模型也同时记住噪声&#xff1b;当拿来一个真的数据时&#xff0c;模型会被一些无关紧要的细节所干扰。 …