操作系统:进程间通信 | 管道

news2025/1/16 14:02:21

目录

1.进程间通信介绍

1.1.简要介绍

1.2.进程间通信的目的

1.3.进程间通信的本质

2.管道

2.1.管道的通信原理

2.2.匿名管道 

2.3.命名管道 

2.4.基于匿名管道的进程池demo

2.4.1.进程池的相关引入

 2.4.2.整体框架的分析

2.4.3.代码的实现 


1.进程间通信介绍

1.1.简要介绍

进程间通信(Inter-Process Communication,简称IPC)是指在不同进程之间传播或交换信息

我们知道:进程之间是独立的,所以进程之间的进程间通信一定不是两个进程直接通信的,为了保证进程间的独立性和实现进程间通信,操作系统就设计了若干种进程间通信方式,来实现多进程之间的协同工作。 

1.2.进程间通信的目的

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

1.3.进程间通信的本质

进程间通信就是不同的进程通过 操作系统 这一个中间媒介,结合设计好的通信方式来进行通信的,本质上就是实现不同的进程能够访问到同一份资源,并从资源上获取信息。

  1. 比如存在进程A、B,其中A进程写入数据进入缓冲区,而B进程从这一块缓冲区中读取内容,这时A、B并没有之间接触,而是从中间商“缓冲区”处使得B进程获取信息。
  2. 同理,A、B进程可以从某一块缓冲区中读取数据。

2.管道

管道是一种基于文件系统的进程间通信方式,管道文件允许访问它的进程,通过它来对一块缓冲区的数据进行访问,通过进程对这一块缓冲区进行读写,来实现进程间数据的交换。  

2.1.管道的通信原理

生活中,我们见到的管道,当有流形成时,这个管道在某一个时刻或者时间段都是只允许单向流通的,比如水管中水的流动一般都是单向的,我们也没有见过管道发挥功能时,先向左流再从右往左流吧……

进程间的管道通信也是如此,一般来说:管道这种通信方式是单向流动的

因为进程具有独立性,所以进程是不能够直接进行通信的,比如A,B进行通信,只允许进行A--中间渠道--B或者B--中间渠道--A ,所以管道的通信原理就是作为中间渠道,在操作系统中,系统实现管道的功能是通过加载进内存的文件缓冲区实现的,并没有实际对管道文件进行操作,而是通过A/B往缓冲区读/写内容,然后B/A进行读/写……

另外管道的通信具有以下4种情形(规定)和三种特性:

四种情形:

  1. 正常情况下,如果管道中没有数据,也就是写端当前没有写入时,读端必须等待,直到写端提供数据。
  2. 如果管道中的数据写满时,如果需要继续写入,写端必须等待,直到读端读取完数据,写端才可以继续写入。
  3. 写端关闭时,读端直接接收到read()函数的返回值为0,表示读取结束,读到文件结尾。
  4. 读端关闭时,写端不会直接关闭,如果写端仍不断写入,操作系统会介入杀掉写端进程。

三种特性:

  1. 管道是单向通信的,是一种半双工通信
  2. 管道是面向字节流的,也就是对应C++IO流中的字符流,管道可以是整型流、字符流
  3. 管道的生命周期是伴随进程的,因为管道通信的本质就是通过文件系统在内存中开辟一块缓冲区,来间接实现进程间通信的

基于4种情形和三种特性,操作系统实现了两种管道通信方式:匿名管道和命名管道,前者只能用于具有血缘关系的进程,后者能用于所有进程……

2.2.匿名管道 

 C语言提供创建匿名管道的函数方法:

接下来我们通过匿名管道的测试来探究一下其原理: 

// 匿名管道的测试
void test1()
{
    // 设置管道的文件描述符数组
    int pipefd[2] = {0};
    // 将fd传入pipe接收返回值
    int n = pipe(pipefd);

    // 返回值为3,4表示占用了文件指针数组第3个、第4个文件
    cout << pipefd[0] <<" "<< pipefd[1] << endl;

    int pipefd1[2] = {0};
    int m = pipe(pipefd1);
    cout << pipefd1[0] << " " << pipefd1[1] << endl;
}

 通过这段代码的测试,我们发现除了0(stdin),1(stdout),2(stderr),我们在创造一个匿名管道时,会占用两个文件fd,在实际应用时,这两个文件分别负责读写功能,为什么需要这样设计呢?

首先我们要知道匿名管道只有拥有血缘关系的进程才可以使用的!!!

我们先从最简单的管道通信----父子进程通信出发:

如图这就是:匿名管道通信的原理,通过管道函数,在进程中开辟两个文件来实现读写功能,再通过进程的拷贝,实现对同一个文件的读写,最终各自释放一个读/写端,实现单向通信。

 下面是一个父子进程的匿名管道通信样例:

// 父子通过匿名管道通信demo
// 只要能把文件描述符继承下去,就能够实现匿名管道通信
// 也就是可以进行兄弟、爷孙进程的管道通信
// 没有任何继承体系的进程之间无法使用匿名管道
void test2()
{
    int pipefd[2] ={0};
    // 将fd传入pipe接收返回值
    int n = pipe(pipefd);

    // 父子进程关闭各自不使用的fd
    // 实现单向通信的管道
    pid_t id = fork();
    if(id < 0)
    {
        perror("error fork");
    }
    else if(id == 0)
    {
        // child
    
        // 关闭读的指向
        close(pipefd[0]);

        int count = 3;
        cout << "writing data into the buffer" << endl;
        while(count--)
        {
            char mesg[BUFFSIZE];
            cin >> mesg;
            // 通过系统接口 将写入的数据通过 写 的文件接口进入文件缓冲区中
            write(pipefd[1], mesg, strlen(mesg));
        }
        exit(0);
    }
    // father
        
    // 关闭写的指向
    
    close(pipefd[1]);
    char buffer[BUFFSIZE];
    while(true)
    {
        // 通过读接口把文件缓冲区的内容写入buffer中
        // 读取buffer大小减1预留 \0 字符
        ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            cout << "child wrote: " << buffer << " to father process " <<endl;
        }
    }

}

在这段代码demo中我们只实现了由子进程向缓冲区进行写功能,父进程向缓冲区进行读功能,那么我们能不能够实现双向通信呢?

答案是可以的,但是我们又要保证一个管道的流向是单向的,注意这里我们用的是“一个”,所以我们可以通过pipe函数再次创建一个管道文件,然后把子进程的写端关闭,父进程的读端关闭,再进行链接。即通过这个demo,加上逻辑相反的代码就可以实现了

2.3.命名管道 

我们在匿名管道的学习中,了解到它的可行性是通过具有血缘关系的进程会拷贝同一个file_struct结构体的指针,来实现读写文件指向同一块区域的。但是对于不具有血缘关系,也就是完全不相干的两个进程我们该如何通信呢?

这时我们可以通过命名管道,创建FIFO文件来实现在不相关的进程之间进行通信……

// 创建命名管道fifo
int n = mkfifo(文件名, 文件权限);

// 文件返回值
int r_open = open(文件名, 文件打开方式);

一般来说我们使用命名管道,首先先创建命名管道,然后两个不同的进程再通过系统调用接口通过不同的打开方式(读/写)来打开这个管道文件。

 下面我们用两个进程的交互来演示一下:

进程一:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#define FILENAME "fifo"
using namespace std;

int main()
{
    // 创建命名管道fifo
    int n = mkfifo(FILENAME, 0666);

    // 文件返回值
    int r_open = open(FILENAME, O_RDONLY);

    char buffer[1024];
    while (1)
    {

        ssize_t r_read = read(r_open, buffer, sizeof(buffer) - 1);
        if (r_read > 0)
        {
            buffer[r_read] = 0;
            cout << "recieve the message from client: " << buffer << endl;
        }
    }

    close(r_open);
}

 在这段代码中:

  1. 我们先创建了命名管道,接着通过只读方式打开文件
  2. 在死循环中,我们不断的读取打开文件返回的文件fd的内容,当r_read = 0时表示读端关闭,r_read > 0 时正常读取
  3. 读取后加载进我们设定好的buffer中,然后再打印出来

 进程二:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#define FILENAME "fifo"
using namespace std;


int main()
{
    int r_open = open(FILENAME, O_WRONLY);
    string message;
    while (1)
    {
        cout << "client send: ";
        getline(cin, message);
        ssize_t r_write = write(r_open, message.c_str(), message.size());
    }

    close(r_open);
}


 这段代码中:

  1. 我们在上一个进程中调用创建好的管道文件,获得相同的文件fd,这里因为我们默认创建两个进程都在当前目录下,这里的本质就是最终找到fifo这个文件
  2. 我们往fifo形成的缓冲区中写入数据,当我们完成写入时,对应的上一个进程就会打印相同的内容

XShell中的现象: 

这样我们就实现了两个互不相干的进程间的通信了…… 

2.4.基于匿名管道的进程池demo

2.4.1.进程池的相关引入

进程池是一种常见的多进程编程技术,用于优化资源使用和提高性能。它可以在程序启动时预先创建一定数量的进程,并将这些进程保存在池中以备后续使用。当有任务需要处理时,程序会从进程池中取出一个空闲的进程来处理任务,任务处理完毕后,该进程会被放回进程池中,等待下一个任务的到来。

如图我们通过父进程,创建五个子进程,在子进程创建的同时我们创建管道文件,进行父进程和子进程通过匿名管道的通信

当我们抽象出这一个模型图后,我们开始着手开辟5个管道和实现这个进程池……

// 创建5个子进程和实现5个管道
for (int i = 0; i < pipe_num; i++)
{
    // 1.定义并创建管道
    int pipefd[2];
    int n = pipe(pipefd);
    cout << "成功创建管道:" << i << endl;

    assert(n == 0);

    // 2.创建进程
    pid_t id = fork();
    assert(id != -1);

    // 3.构建单向信道
    if (id == 0)
    {

        // child
        // 子进程关闭当前的 写 端
        close(pipefd[1]);
        exit(0);
    }

    // father
    close(pipefd[0]);
}

 这段代码,我们循环5次,创建管道,并链接父子进程,但是这一段代码实际上在进行循环时会出现bug,具体如图:

按照这个思路:最终我们发现在创造了5个子进程之后,最后一个子进程对应的file_struct会继承4个父进程的写端!!!这个bug虽然不影响我们的通信,但是会影响我们后续对写端的回收,终止这个进程池,这在我们后面的代码模块有具体讲解!!!

 2.4.2.整体框架的分析

在上面部分内容,我们完成了进程池的创建,接下来就是代码对进程池逻辑的实现了,首先进程池通过父进程来管理5个子进程,当获取到任务时,首先通过父进程接收然后分配给子进程。接着子进程各自处理自己分配到的任务,任务完成后继续接收新的任务。

结合我们通过匿名管道来实现,我们初步设计成父进程作为写端通过匿名管道传输任务给子进程,然后子进程通过读端读取任务来进行任务的调用。那么我们就将整个框架设计为:

进程的创建 --- >管道的搭建--->管道间进程通信的管理--->任务内容的创建--->任务的发布--->子进程进行任务的处理--->资源释放

2.4.3.代码的实现 

这一部分主要是代码的实现,因为篇幅过长并且代码中注释较为详细,我们通过2.4.2.这个篇章在结合代码内容就能大概理解这个demo

work.h

#pragma once
#include<iostream>
#include<functional>
#include<vector>
#include<ctime>

using namespace std;

// using task_t function<void()>;
typedef function<void()> task_t;

void Download()
{
    cout << "执行下载任务" << " 通过子进程: "<< getpid() <<endl;
}

void PrintLog()
{
    cout << "执行打印日志任务" << " 通过子进程: "<< getpid()<< endl;
}

void PushStream()
{
    cout << "执行传输数据流任务" << " 通过子进程: "<< getpid()<< endl;
}

class Init
{
public:
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(PushStream);
        tasks.push_back(PrintLog);

        srand(time(nullptr) ^ getpid());
    }
    // 判断任务的可行性
    bool CheckSafe(int code)
    {
        if(code >= 0 && code < tasks.size()) return true;
        else return false;
    }
    void RunTask(int code)
    {
        // tasks数组中存放着可调用对象,通过()调用
        return tasks[code]();
    }
    int SelectTask()
    {
        return rand() % tasks.size();
    }

private:

    // 任务列表
    vector<task_t> tasks;
    // 任务码 (在代码中并没有用上)
    const int download_code = 0;
    const int print_code = 1;
    const int push_stream_code = 2;
};
// 定义全局对象
Init init;

main.cc:

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

using namespace std;
#define BUFFSIZE 1024
// a

const int pipe_num = 5;
// 判断是第几个管道
static int name_flag = 1;
class channel
{
public:
    channel(int fd, pid_t id)
        : _ctrlfd(fd), _workid(id)
    {
        _name = "channel-" + to_string(name_flag);
        name_flag++;
    }
    int GetFd() const
    {
        return _ctrlfd;
    }
    pid_t GetId() const
    {
        return _workid;
    }
    string GetName() const
    {
        return _name;
    }

private:
    int _ctrlfd;
    pid_t _workid;
    string _name;
};

void ChildWork()
{
    while (1)
    {
        int code = 0;
        // 子进程只读,当父进程没有写入指令时,子进程无法工作
        // 父进程写4个字节的数据 子进程读取4个字节
        ssize_t n = read(0, &code, sizeof(code));
        // 对应任务码

        if (n == sizeof(code))
        {
            // n值正常
            if (!init.CheckSafe(code))
                continue;
            init.RunTask(code);
        }
        else if (n == 0)
        {
            break;
        }
    }

    cout << "子进程已退出" << endl;
}

void CreatChannels(vector<channel> &channels)
{
    vector<int> fd_write;
    for (int i = 0; i < pipe_num; i++)
    {
        // 1.定义并创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        cout << "成功创建管道:" << i << endl;

        assert(n == 0);

        // 2.创建进程
        pid_t id = fork();
        assert(id != -1);

        // 3.构建单向信道
        if (id == 0)
        {
            // 对于子进程来说 只要出现拷贝了父进程的写
            // 就需要进行关闭,才能实现单向传输的管道
            if (!fd_write.empty())
            {
                for (size_t j = 0; j < fd_write.size(); j++)
                {
                    // 关闭我们插入数组内容
                    close(fd_write[j]);
                    cout << "process: " << getpid() << " close: " << fd_write[j] << endl;
                }
            }

            // child
            // 子进程关闭当前的 写 端
            close(pipefd[1]);
            // 重定向到标准输入
            dup2(pipefd[0], 0);
            ChildWork();
            exit(0);
        }

        // father
        close(pipefd[0]);

        // 存储写对应的下标相对值
        fd_write.push_back(pipefd[1]);

        // 传入这个 写 对应的下标文件给channel
        channels.push_back(channel(pipefd[1], id));
        // 测试父进程的写文件
        // cout<< pipefd[1] <<endl;
    }
    cout << "管道已全部创建,开始执行任务" << endl;
}
void SendCommand(const vector<channel> &channels, int flag = -1)
{

    int position = 0;
    while (1)
    {
        if (flag == 0)
        {
            break;
        }
        sleep(1);

        // 开始选择任务
        // 本质上就是获取任务码
        int command = init.SelectTask();

        // 分配进程
        channel c = channels[position++];
        position %= channels.size();

        cout << "send command: " << command << " in " << c.GetName() << " by father:" << getpid() << endl;
        // 发送任务
        write(c.GetFd(), &command, sizeof(command));

        flag--;
    }
    cout << "任务已完成" << endl;
}
void ReleaseChannel(const vector<channel> &channels)
{
    for (const auto &e : channels)
    {
        // 关掉 父进程开辟的写端,注意这里的子进程
        close(e.GetFd());
        pid_t rid = waitpid(e.GetId(), nullptr, 0);
    }
}

int main()
{

    vector<channel> channels;
    // 创建管道
    CreatChannels(channels);
    // 发送任务并执行
    SendCommand(channels, 5);
    // 解决子进程回收问题
    ReleaseChannel(channels);
}

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

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

相关文章

Etsy多账号关联怎么办?Etsy店铺防关联解决方法

Etsy虽然相对于其他跨境电商平台来说比较小众&#xff0c;但因为平台是以卖手工艺品为主的&#xff0c;所以成本较低&#xff0c;利润很高。许多跨境卖家都纷纷入驻&#xff0c;导致平台规则越发严格&#xff0c;操作不当就会封号&#xff0c;比如一个卖家操作多个账号会出现关…

kubernetes部署控制器Deployment

一、概念 在学习rc和rs控制器资源时&#xff0c;这两个资源都是控制pod的副本数量的&#xff0c;但是&#xff0c;他们两个有个缺点&#xff0c;就是在部署新版本pod或者回滚代码的时候&#xff0c;需要先apply资源清单&#xff0c;然后再删除现有pod&#xff0c;通过资源控制&…

赛氪网参与第61届中国高等教育博览会,助力产教融合与科教融汇

为深入贯彻党的二十大精神&#xff0c;落实立德树人根本任务&#xff0c;推动高等教育装备现代化&#xff0c;第61届中国高等教育博览会&#xff08;以下简称“高博会”&#xff09;于近日在福建省福州市隆重开幕。作为高等教育领域内的综合性品牌博览会&#xff0c;此次高博会…

达梦(DM)数据库表索引

达梦DM数据库表索引 表索引索引准则其他准则 创建索引显式地创建索引其他创建索引语句 使用索引重建索引删除索引 表索引 达梦数据库表索引相关内容比较多&#xff0c;常用的可能也就固定的一些&#xff0c;这里主要说一下常用的索引&#xff0c;从物理存储角度进行分类&#…

【声呐仿真】学习记录0.5-配置ssh远程连接docker、在docker中使用nvidia显卡

【声呐仿真】学习记录0.5-配置ssh远程连接docker、在docker中使用nvidia显卡 配置ssh远程连接docker1.端口映射2.配置ssh 在docker中使用nvidia显卡配置CUDA 注意&#xff1a;之前已经创建过容器的&#xff0c;需要打包成镜像&#xff0c;重新创建容器&#xff0c;因为要在创建…

# IDEA2019 如何打开 Run Dashboard 运行仪表面板

IDEA2019 如何打开 Run Dashboard 运行仪表面板 段子手168 1、依次点击 IDEA 上面工具栏 —> 【View】 视图。 —> 【Tool Windows】 工具。 —> 【Run Dashboard】 运行仪表面板。 2、如果 【Tool Windows 】工具包 没有 【Run Dashboard】 运行仪表面板 项 依次…

uniapp制作多选下拉框和富文本(短信页面)

实例 多选下拉框实现 http://t.csdnimg.cn/TNmcF 富文本实现 http://t.csdnimg.cn/Ei1iV

网络带宽相关

1.tcp重传率计算 watch -n 5 “cat /proc/net/snmp” 如下博客所讲 https://blog.csdn.net/michaelwoshi/article/details/121189743 2.iperf测试网络带宽 #客户端 #tcp iperf -c 服务端ip -P 4 -b 200M #udp iperf -c 服务端ip -u -P 4 -b 1000M -l 10K #服务端 iperf -s

OPTEE的GDB调试技术实战

【按语】&#xff1a;如果需要调试OPTEE&#xff0c;那么在远程调试配置中使用GDB可能会很有用。远程调试意味着GDB在您的PC上运行&#xff0c;它可以访问源代码&#xff0c;而被调试的程序在远程系统上运行(在本例中&#xff0c;在QEMU环境的OPTEE中)。本博客来探讨OPTEE的GDB…

React基础知识大汇总

函数组件和类组件 函数组件与类组件有什么区别呢&#xff1f; function getName(params:{name:string}){const count 0;return params.name -count; } getName({name:"test"}) getName({name:"哈哈哈"})getName是一个纯函数&#xff0c;不产生任何副作用…

算法竞赛相关问题总结记录

前言 日常在校生或者是工作之余的同学或多或少都会参加一些竞赛,参加竞赛一方面可以锻炼自己的理解与实践能力&#xff0c;也能够增加自己的生活费&#xff0c;竞赛中的一些方案也可以后续作为自己论文的base,甚至是横向课题的框架。在算法竞赛中算法的差别个人感觉差距都不大&…

区块链钱包开发指南: 探究区块链钱包开发涉及

区块链钱包是连接用户与区块链网络的重要工具&#xff0c;它们不仅提供了安全的存储和管理数字资产的功能&#xff0c;还允许用户进行交易和与区块链上的智能合约进行互动。本文将探究区块链钱包开发涉及的关键方面和技术要点。 1. 区块链钱包类型 区块链钱包可以分为以下几种…

Android Studio查看viewtree

前言&#xff1a;之前开发过程一直看的是手机上开发者选项中的显示布局边界&#xff0c;开关状态需要手动来回切换&#xff0c;今天偶然在Android Studio中弄出了布局树觉得挺方便的。

汽车纵染压制专用液压机比例阀放大器

汽车纵染压制专用液压机比例阀放大器是一种专门用于汽车纵梁拉伸工艺的设备&#xff0c;它也可以用于其他金属薄板的压制成型及校正工艺。该类型的液压机通常具备独立的动力机构和电气系统&#xff0c;采用PLC技术进行控制&#xff0c;以确保操作的准确性和稳定性。除了纵梁拉伸…

文心一言 VS 讯飞星火 VS chatgpt (243)-- 算法导论18.1 1题

一、B树中为什么不允许最小度数 t1&#xff1f;如果要写代码&#xff0c;请用go语言。 文心一言&#xff1a; 在B树中不允许最小度数 t1 的主要原因在于这样会导致树的结构退化为链表&#xff0c;失去了B树作为平衡多路搜索树的优势。当 t1 时&#xff0c;每个非根节点将只能…

openGauss学习笔记-269 openGauss性能调优-TPCC性能调优测试指导-数据库服务端及客户端绑核

文章目录 openGauss学习笔记-269 openGauss性能调优-TPCC性能调优测试指导-数据库服务端及客户端绑核269.1 安装openGauss数据库269.2 停止数据库269.3 使用gs_guc工具修改数据库端口、IP等269.4 使用gs_guc工具设置如下参数269.5 执行如下命令以绑核方式启动服务端数据库269.6…

在线教程丨刘强东数字人首秀交易额破5千万!用GeneFace++ 生成实时说话数字人

近日&#xff0c;京东创始人刘强东化身「采销东哥 AI 数字人」&#xff0c;在京东家电家居和超市的采销直播间开启了自己的直播首秀。此次直播活动观看人次超 2 千万&#xff0c;交易总额超 5 千万&#xff0c;充分彰显了AI 数字人在电商直播领域的巨大潜力。 「采销东哥」亮相…

SwiftUI 5.0(iOS 17.0)触摸反馈“震荡波”与触发器模式趣谈

概览 要想创作出一款精彩绝伦的 App&#xff0c;绚丽的界面和灵动的动画并不是唯一吸引用户的要素。有时我们还希望让用户真切的感受到操作引发的触觉反馈&#xff0c;直击使用者的灵魂。 所幸的是新版 SwiftUI 原生提供了实现触觉震动反馈的机制。在介绍它之后我们还将进一步…

prompt提示词:小红书爆款标题提示词,让AI 帮你生成吸睛的标题

目录 小红书爆款标题提示词效果展示&#xff1a;提示词&#xff1a; 小红书爆款标题提示词 一篇文章若缺少了吸引人的标题&#xff0c;就如同失去了灵魂的躯壳&#xff0c;失去了与读者心灵相通的桥梁&#xff0c;上次发表了一篇小红书爆款文案生成助手 提示词&#xff0c;大家…