Linux系统编程 day06 进程间通信

进程间通信

  • 1. 进程间通信的概念
  • 2. 匿名管道pipe
  • 3. 命名管道FIFO
  • 4. 内存映射区

1. 进程间通信的概念

在Linux的环境下,进程地址空间是相互独立的,每个进程有着各自不同的用户地址空间。一个进程不能访问另一个进程中的内容,要进行数据交换必须要通过内核,在内核中开辟块缓冲区,一个进程将数据从用户空间拷贝到内核缓冲区中,另一个进程从内核缓冲区中将数据读走。内核提供的这种机制称为进程间通信(IPC)。

在进程间完成数据传输需要借助操作系统提供的特殊方法,比如文件、管道、FIFO、内存映射区、消息队列、信号、套接字等。如今常用的进程间通信主要有管道(最简单)、信号(开销最小)、内存映射区(无血缘关系)、本地套接字(最稳定)。

2. 匿名管道pipe

管道是一种最基本的进程间通信机制,也称为匿名管道,应用于有血缘关系的进程间进行通信。管道的本质是一块内核缓冲区,内部使用的环形队列实现,由两个文件描述符进行引用,其中一个表示读端,另一个表示写端。管道的数据从管道的写端流入管道,从读端流出。当两个进程都死亡的时候,管道也会自动消失。管道不管读端还是写端默认都是阻塞的。管道的默认缓冲区大小为4K,可以使用ulimit -a命令获取大小。

在这里插入图片描述

管道的数据一旦被读走,便不在管道中存在,不可以反复读取。管道的数据只能在一个方向上流动,如果需要实现双向流动则需要使用两个管道。匿名管道只能在有血缘关系的进程中使用。我们用pipe函数来创建管道。

在这里插入图片描述

pipe函数的原型如下:

       #include <unistd.h>int pipe(int pipefd[2]); // 创建管道

其中pipefd为管道写端和读端的文件描述符,其中pipefd[0]为管道读端的文件描述符,pipefd[1]为管道写端的文件描述符。当函数调用成功创建了管道返回0,失败则返回-1并设置errno。

在使用匿名管道进行通信的时候,一般是先用pipe函数创建管道,再使用fork函数创建子进程。这样父子进程就具有了相同的文件描述符,就会指向同一个管道。

在管道的通信中,读写数据也是使用readwrite。管道的示例代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>int main()
{// 创建管道//int pipe(int pipefd[2]);int fd[2];int ret = pipe(fd);if(ret < 0){perror("pipe error");return -1;}// 创建子进程pid_t pid = fork();if(pid < 0){perror("fork error");return -1;}else if(pid == 0){// 子进程关闭写端close(fd[1]);char buf[1024];memset(buf, 0x00, sizeof(buf));sleep(5);read(fd[0], buf, sizeof buf);printf("child: read over, pid = [%d], fpid = [%d], buf = [%s]\n", getpid(), getppid(), buf);}else{// 父进程关闭读端close(fd[0]);write(fd[1], "helloworld", strlen("helloworld"));printf("father: write over, pid = [%d], fpid = [%d]\n", getpid(), getppid());pid_t wpid = wait(NULL);printf("child [%d] is dead!\n", wpid);}return 0;
}

在shell中我们查询某一个进程的时候我们会使用ps -ef | grep --color=auto bash,这里的|就是管道。我们使用父进程进程去执行ps -ef命令,由于我们需要交给grep去作为输入,所以在父进程中需要将输出重定向到管道的写端。子进程执行grep的时候会从输入进行读取内容,因此我们需要将输入重定向到管道读端。重定向的时候需要使用dup2函数,需要执行命令则需要使用execlexeclp函数,示例程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>int main()
{// 创建管道int pipefd[2];int ret = pipe(pipefd);if(ret < 0){// 创建失败perror("pipe error");return -1;}// 创建子进程pid_t pid = fork();if(pid < 0){// 创建失败perror("fork error");return -1;}else if(pid == 0){// 子进程关闭写端close(pipefd[1]);// 将标准输入重定向到管道读端dup2(pipefd[0], STDIN_FILENO);execlp("grep", "grep", "--color=auto", "bash", NULL);perror("execlp error");}else {// 父进程关闭读端close(pipefd[0]);// 将标准输出重定向到管道写端dup2(pipefd[1], STDOUT_FILENO);execlp("ps", "ps", "-ef", NULL);perror("execlp error");}return 0;
}

当管道有数据的时候,read可以正常读,并返回读出来的字节数;当管道没有数据的时候,若写端全部关闭,则read函数解出阻塞状态,返回0,相当于读文件读到了尾部。若写端没有关闭,则read阻塞。

若读端全部关闭,进行写操作的时候则管道会破裂,进程终止,内核会给当前进程发送SIGPIPE信号。若读端没有完全关闭,缓冲区写满了则write会阻塞,缓冲区没有满则可以继续write

管道默认两端都是阻塞的,若要设置为非阻塞,则可以使用前面提过的fcntl函数。首先使用F_GETFL获取flags,然后在添加O_NONBLOCK使用F_SETFL设置即可。当读端设置为非阻塞状态的时候,会有以下四种情况:

  • 写端没有关闭,管道中没有数据可读,则read返回-1。
  • 写端没有关闭,管道中有数据可读,则read返回实际读到的字节数。
  • 写端已经关闭,管道中有数据可读,则read返回实际读到的字节数。
  • 写端已经关闭,管道中没有数据可读,则read返回0。

设置的流程为:

int flags = fcntl(fd[0], F_GETFL, 0);
flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);

除了前面使用的ulimit -a可以查看到管道缓冲区的大小之外,也可以使用fpathconf函数,该函数的原型为:

       #include <unistd.h>long fpathconf(int fd, int name); 

这个函数会根据name的参数设置返回文件描述符fd对应文件的配置内容。比如获取管道缓冲区则需要把name设置为_PC_PIPE_BUF。如下面查看管道的缓冲区大小。

printf("pipe size = [%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF));
printf("pipe size = [%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF));

更多的name参数设置可以使用man fpathconf查询帮助文档。

3. 命名管道FIFO

使用pipe管道只能实现有血缘关系的两个进程间的通信,那么对于两个没有血缘关系的进程又应该怎么实现呢?则需要这一节的命名管道FIFO

FIFO是Linux上基于文件类型的一种通信方式,文件类型为p,但是FIFO在磁盘上并没有数据块,文件大小为0,仅仅用于标识内核中的一条通道。进程可以通过read或者write函数去对这一条通道进行操作,也就是内核缓冲区,这样就实现了进程间的通信。

要使用命名管道,就得先创建命名管道文件,创建管道文件使用mkfifo函数,该函数的原型为:

       #include <sys/types.h>#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);

其中参数第一个pathname表示管道的路径名,mode表示权限,使用一个三位八进制数表示。创建成功函数返回0,创建失败则函数返回-1并设置errno

FIFO严格遵守先进先出的规则,也就是对于管道的读总是从管道的开始处返回数据,而对于管道的写则是添加到末尾。因此命名管道不支持使用lseek等对文件定位的操作。

使用FIFO完成进程间通信的示意图如下:

在这里插入图片描述

既然是两个进程,也就是说两个进程中的程序都需要打开管道,也就是需要找到文件。若写进程没有打开就去使用读进程,则可能因为管道的文件不存在而报错。因此我们在使用管道或者创建管道之前要先判断文件是否存在。这个可以使用access函数实现,该函数的原型为:

       #include <unistd.h>int access(const char *pathname, int mode);

该函数的第一个参数pathname是文件的路径,第二个参数mode表示要测试的模式,有四个参数可以传,分别为:

  • F_OK: 文件存在
  • R_OK:有读权限
  • W_OK:有写权限
  • X_OK:有执行权限

当有对应的权限或者文件存在的时候,该函数的返回值为0,若没有对应权限或者文件不存在则该函数返回-1

接下来我们实现两个无血缘关系之间的进程间通信。代码如下:

fifo_write.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>// FIFO 写进程
int main()
{// 创建FIFO文件// int access(const char *pathname, int mode);int ret = access("./myfifo", F_OK);if(ret < 0){// int mkfifo(const char *pathname, mode_t mode);ret = mkfifo("./myfifo", 0777);if(ret < 0){perror("mkfifo error");return -1;}}// 打开FIFOint fd = open("./myfifo", O_RDWR);if(fd < 0){perror("open error");return -1;}// 传输数据char buf[1024] = "hello world";write(fd, buf, strlen(buf));sleep(1);// 关闭FIFOclose(fd);return 0;
}

fifo_read.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>// FIFO 读进程端
int main()
{// 判断创建FIFOint ret = access("./myfifo", F_OK);if(ret < 0){int ret = mkfifo("./myfifo", 0777);if(ret < 0){perror("mkfifo error");return -1;}}// 打开FIFOint fd = open("./myfifo", O_RDWR);if(fd < 0){perror("open error");return -1;}// 接收数据char buf[1024];memset(buf, 0x00, sizeof buf);read(fd, buf, sizeof buf);printf("%s\n", buf);// 关闭FIFOclose(fd);return 0;
}

在上述代码中,只要运行,不管是读进程还是写进程先运行,只要写进程没有往管道中写数据,读进程都会阻塞在read处。直到写进程调用write函数往管道中写数据之后读进程才会读出来管道中的数据输出。

4. 内存映射区

内存映射区是将一个磁盘文件与内存空间的一个缓冲区向映射。当我们在缓冲区中读取数据的时候,就相当于在文件中读取相应的字节。若我们向缓冲区中写数据,则会把数据写入对应的文件之中。这样就可以在不使用read函数和write函数啊的情况下使用指针完成IO操作。

在这里插入图片描述

映射这个过程可以使用mmap函数实现。解除映射可以使用munmap实现。函数的原型以及参数如下:

       #include <sys/mman.h>// 函数作用: 建立存储映射区// 返回值: 成功:返回创建的映射区首地址//		  失败: 返回MAP_FAILED宏, 实际上就是(void *)-1// 参数: addr: 指定映射的起始地址,通常设为NULL,由系统指定//      length: 映射到内存的文件长度//       prot: 映射区的保护方式,最常用的有//                PROT_READ   				读//                PROT_WRITE  				写//                PROT_READ | PORT_WRITE    读写//       flags: 映射区的特性,可以设置以下//                MAP_SHARED   写入映射区的数据会写回文件,且允许//							   其它映射改文件的进程共享。//                MAP_PRIVATE  对映射区的写入操作会产生一个映射区的复//  						 	制,对此区域所做的修改不会写回原文件。//                MAP_ANONYMOUS  匿名映射区,需要结合MAP_SHARED使用。// 	      fd: 代表要映射的文件,由open函数返回的文件描述符//        offset: 以文件开始出的偏移量,必须是4K的整数倍,通常为0,表示//                从文件头开始映射void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);// 函数作用: 释放由mmap函数建立的存储映射区// 返回值: 成功返回0,失败返回-1并设置errno// 参数: addr: 调用mmap函数成功返回的映射区首地址//  	 length: 映射区的大小int munmap(void *addr, size_t length);

需要注意到的是,mmap函数不能开辟长度为0的存储映射区。在使用mmap函数创建映射区的过程中,隐含着一次对映射文件的读操作,将文件读取到映射区。当我们将flags设置为MAP_SHARED的时候,要求映射区的权限应该小于或者等于文件打开的权限,这是出于对映射区的保护。而对于MAP_PRIVATE,则是没有这个必要,因为mmap中的权限是对内存的限制。

在映射区建立之后,映射区的释放是与文件的关闭无关的,所以在映射区建立之后就可以关闭文件。在创建映射区的时候,使用mmap函数常常会出现总线错误,通常是因为共享文件存储空间大小引起的。所以创建映射区的时候出错的概率比较高,因此一定要检查函数的返回值,确保映射区建立成功了再进行后续的操作。使用munmap函数的时候,传入的地址一定要是mmap函数的返回值,一定不要改变指针的指向。其中函数的参数中offset也是需要注意的,不能随便指定,必须要是4K的整数倍才行。

下面给出一个关于关于有血缘关系的进程间的通信示例。

//使用mmap完成有血缘关系进程间的通信
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>int main()
{//使用mmap函数建立共享映射区//void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);int fd = open("test.log", O_RDWR);if(fd < 0){perror("open error");return -1;}int len = lseek(fd, 0, SEEK_END);void *addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if(addr == MAP_FAILED){perror("mmap error");return -1;}close(fd);//创建子进程pid_t pid = fork();if(pid < 0){perror("fork error");return -1;}else if(pid == 0){//子进程sleep(2);char *p = (char *)addr;printf("[%s]\n", p);}else{//父进程memcpy(addr, "hello world", strlen("hello world"));wait(NULL);}return 0;
}

共享存储映射区也可以用于没有血缘关系的两个进程。

写进程

// 使用mmap完成没有血缘关系的进程间的通信
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mman.h>// 写进程
int main()
{// 使用mmap函数建立共享映射区int fd = open("test.log", O_RDWR);if(fd < 0){perror("open error");return -1;}int len = lseek(fd, 0, SEEK_END);// 建立共享映射区void *addr = mmap(NULL, len, PROT_WRITE, MAP_SHARED, fd, 0);if(addr == MAP_FAILED){perror("mmap error");return -1;}close(fd);memcpy(addr, "Good morning", strlen("Good morning"));return 0;
}

读进程

// 使用mmap完成没有血缘关系的两个进程间的通信
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>// 读进程端
int main()
{// 使用mmap函数建立共享映射区int fd = open("test.log", O_RDONLY);if(fd < 0){perror("open error");return -1;}int len = lseek(fd, 0, SEEK_END);// 建立共享映射区void *addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);if(addr == MAP_FAILED){perror("mmap error");return -1;}char *p = (char *)addr;printf("[%s]\n", p);return 0;
}

上面的共享存储映射区都是有名字的,我们也可以创建匿名的共享存储映射区。匿名映射区是不需要使用文件去创建的,因此没有血缘关系的两个进程不能使用匿名映射区通信。在使用匿名共享映射区的时候,文件的描述符一般传为-1。关于匿名共享映射区的示例代码如下:

// 建立匿名共享映射区
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <fcntl.h>int main()
{void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);if(addr == MAP_FAILED){perror("mmap error");return -1;}pid_t pid = fork();if(pid < 0){perror("fork error");return -1;}else if(pid == 0){sleep(2);char *p = (char *)addr;printf("[%s]\n", p);}else{memset(addr, 0x00, 4096);memcpy(addr, "hello world", strlen("hello world"));wait(NULL);}return 0;
}

后续博客关于函数原型以及作用均以注释的形式写在代码中以便直观

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

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

相关文章

STM32F103C8T6第7天:

1. 智能小车&#xff1a;让小车动起来&#xff08;360.64&#xff09; 硬件接线 B-2A – PB0B-1A – PB1A-1B – PB2A-1A – PB10其余接线参考上官一号小车项目。 cubemx配置 代码&#xff08;28.smartCar_project1/MDK-ARM&#xff09; 2. 智能小车&#xff1a;串口控制小…

简要介绍Spring原生框架与Spring是轻量级框架的原因

&#x1f609;&#x1f609; 学习交流群&#xff1a; ✅✅1&#xff1a;这是孙哥suns给大家的福利&#xff01; ✨✨2&#xff1a;我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料 &#x1f96d;&#x1f96d;3&#xff1a;QQ群&#xff1a;583783…

别再让假的fiddler教程毒害你了,来看这套最全最新的fiddler全工具讲解

fiddler界面工具栏介绍 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; &#xff08;1&#xff09;WinConfig&#xff1a;windows 使用了一种叫做“AppContainer”的隔离技术&#xff0c;使得一些流量无法正常捕获&#xff0c;在 fiddler中点击 WinConfig…

Vue指令之v-html

在Vue中有很多特殊的标签属性&#xff0c;这些属性一般以’v’开头&#xff0c;用于在标签中实现特殊的功能。 例如&#xff0c;当Vue实例的data是一个inner html&#xff0c;我们想在网页上渲染这部分html&#xff0c;如果依然使用之前的{{ variable }}&#xff0c;则只会将i…

单片机复位电路

有时候我们的代码会跑飞,这个时候基本上是一切推到重来.”推倒重来”在计算机术语上称为复位.复位需要硬件的支持,复位电路就是在单片机的复位管脚上产生一个信号&#xff0c;俗称复位信号.这个信号需要持续一定的时间,单片机收到该信号之后就会复位,从头执行。 复位原理: 那么…

【视觉SLAM十四讲学习笔记】第三讲——四元数

专栏系列文章如下&#xff1a; 【视觉SLAM十四讲学习笔记】第一讲——SLAM介绍 【视觉SLAM十四讲学习笔记】第二讲——初识SLAM 【视觉SLAM十四讲学习笔记】第三讲——旋转矩阵 【视觉SLAM十四讲学习笔记】第三讲——Eigen库 【视觉SLAM十四讲学习笔记】第三讲——旋转向量和欧…

anaconda换源安装pytorch(附带bug解决办法)

1.安装anaconda 如何安装anaconda可以看这篇文章:如何安装anaconda 2.换源安装pytorch: 首先进入到pytorch官网&#xff0c;选对好参数之后复制命令进入到anaconda prompt即可: 然后进入自己的环境之后输入该命令(即conda install …)&#xff0c;则可以进行下载。下载完成…

【DevOps】SonarQube 指标解读

SonarQube 指标解读 1.BUG 评级计算方法&#xff08;可靠性&#xff09;2.漏洞评级计算方法&#xff08;安全性&#xff09;3.债务和坏味道4.覆盖率4.1 代码覆盖率4.2 分支覆盖率4.3 单元测试覆盖率 5.重复 1.BUG 评级计算方法&#xff08;可靠性&#xff09; ✅ A&#xff1a…

1128. 等价多米诺骨牌对的数量

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能&#xff0c;轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/number-of-equivalent-domino-pa…

Day12 qt QMianWindow,资源文件,对话框,布局方式,常用ui控件

QMianWindow 概述 QMainWindow 是一个为用户提供主窗口程序的类&#xff0c;包含一个菜单栏&#xff08; menu bar &#xff09;、多 个工具栏 (tool bars) 、多个铆接部件 (dock widgets) 、一个状态栏 (status bar) 及 一个中心部件 (central widget) 许多应用程序的基础…

postgresql-shared_buffers参数详解

shared_buffers 是 PostgreSQL 中一个非常关键的参数&#xff0c;用于配置服务器使用的共享内存缓冲区的大小。这些缓冲区用于存储数据页&#xff0c;以便数据库可以更快地访问磁盘上的数据。 这个参数在 PostgreSQL 的性能方面有着重要的影响。增加 shared_buffers 可以提高数…

【并发编程】ConcurrentHashMap底层结构和原理

&#x1f4eb;作者简介&#xff1a;小明Java问道之路&#xff0c;2022年度博客之星全国TOP3&#xff0c;专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化&#xff0c;文章内容兼具广度、深度、大厂技术方案&#xff0c;对待技术喜欢推理加验证&#xff0c;就职于…

wmvcore.dll丢失怎么办?解决电脑出现wmvcore.dll丢失问题5个方法

wmvcore.dll缺失5个解决方法与wmvcore.dll丢失原因及文件介绍 引言&#xff1a; 在日常使用电脑的过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中之一就是wmvcore.dll缺失。wmvcore.dll是Windows Media Video编码解码相关动态链接库文件之一&#xff0c;它对…

Linux 项目自动化构建工具:make/makefile

什么是 make make 是一个命令&#xff0c;他会在源文件的当前目录下寻找 makefile 或者 Makefile 文件执行这个文件中的代码。 makefile 文件的编写 我们先来见见猪跑&#xff0c;看看 make 怎么用的&#xff1a; 下面是 makefile 文件的内容&#xff1a; 这是 test.c 中的…

WPF创建进度条

使用wpf做一个原生的进度条&#xff0c;进度条上面有值&#xff0c;先看效果。 功能就是点击按钮&#xff0c;后台处理数据&#xff0c;前台显示处理数据的变化&#xff0c;当然还可以对进度条进行美化和关闭的操作&#xff0c;等待后台处理完毕数据&#xff0c;然后自动关闭。…

Python入职某新员工大量使用Lambda表达式,却被老员工喷是屎山

Python中Lambda表达式是一种简洁而强大的特性,其在开发中的使用优缺点明显,需要根据具体场景权衡取舍。 Lambda表达式的优点之一是它的紧凑语法,适用于一些短小而简单的函数。这种形式使得代码更为精炼,特别在一些函数式编程场景中,Lambda表达式可以提高代码的表达力。此外…

DMX512协议及对接口电路的分析

1、DMX512协议简介 DMX 是Digital MultipleX 的缩写&#xff0c;意为多路数字传输(具有512条信息的数字多路复用”)。DMX512控制协议是美国舞台灯光协会(usITT)于1990年发布的灯光控制器与灯具设备进行数据传输的工业标准&#xff0c;全称是USITTDMX512(1990); DMX512 在其物理…

福州大学《嵌入式系统综合设计》 实验八:FFMPEG视频编码

一、实验目的 掌握使用算能平台进行视频编码的流程&#xff0c;包括开发主机环境与云平台的配置&#xff0c;视频编码程序的编写与理解&#xff0c;代码的编译、运行以及学习使用码流分析工具分析视频压缩码流等。 二、实验内容 搭建实验开发环境&#xff0c;编译并运行编码…

Spring Boot 3.2.0 虚拟线程初体验 (部分装配解析)

写在前面 spring boot 3 已经提供了对虚拟线程的支持。 虚拟线程和平台线程主要区别在于&#xff0c;虚拟线程在运行周期内不依赖操作系统线程&#xff1a;它们与硬件脱钩&#xff0c;因此被称为 “虚拟”。这种解耦是由 JVM 提供的抽象层赋予的。 虚拟线程的运行成本远低于平…

组合设计模式

package com.jmj.pattern.combination;/*** 菜单组件&#xff0c;属于抽象根节点*/ public abstract class MenuComponent {//菜单组件的名称protected String name;//菜单组件的层级protected int level;//添加子菜单public void add(MenuComponent menuComponent) {throw new…