实验五 进程通信_进程同步实验心得-程序员宅基地

技术标签: 运维  linux  操作系统  gnu  

一、实验目的:

掌握进程通信的基本原理、作用。了解LINUX 、Windows进程间通信的不同方法,如命名管道、文件映射等,掌握进程间通信的基本原理。掌握相关函数的使用方法。

二、实验环境:

一台运行Linux操作系统的计算机。

三、实验内容:

PART 1 LINUX 进程通信

1. 命名管道

(1)建立如下两个C 源文件,并编译链接成可执行文件,文件名可自己设置。

源文件:fifo_write.c

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#include <stdlib.h>

#include <stdio.h>

#include <signal.h>

#define MYFIFO "myfifo"

int main(int argc, char *argv[])

{

   int fd;      

   fd=open(MYFIFO,O_WRONLY);

   if (fd==-1)

   {   

     printf("open fifo file error.\n");

     exit(1);

   }

   while(1)

   {  char buff[128]={0};

      fgets(buff,128,stdin);

      if (strncmp(buff,"end",3)==0)

 break;

      write(fd,buff,strlen(buff));

   }   

   close(fd);

   exit(0);

}

源文件: fifo_read.c

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <signal.h>

#define MYFIFO "myfifo"

int main( )

{

   int fd;

   int nread;

   if (access(MYFIFO,F_OK)==-1)

   {

       if ((mkfifo(MYFIFO,0666)<0)&&(errno!=EEXIST))

          {        

               printf("Cannot create fifo file\n");

               exit(1);

  }

   }   

   fd=open(MYFIFO,O_RDONLY);

   if (fd==-1)

   {   

     printf("open fifo file error.\n");

     exit(1);

   }      

   while (1)

   {

     char buff[128]={0};

     int n=read(fd,buff,127);    

     if ( n==0 )

     {

       printf("对方已关闭!\n");

       break;

     }

     printf("read(n=%d):%s",n,buff);

   }

   close(fd);

   exit(0);

}

  1. 打开两个命令窗口,分别运行以上两个程序,观察分析程序运行结果。

  

  1. 在以上代码的基础上,增加如下功能:fifo_read.c程序中,输出"对方已关闭!"信息之前,输出一共从fifo_write.c程序中接收了多少行信息。

修改后的代码:

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <signal.h>

#include <unistd.h>

#define MYFIFO “myfifo”

int main( )

{

int fd;

int nread;

int count = 0; //增加的代码语句

if (access(MYFIFO,F_OK)== -1)

{

if ((mkfifo(MYFIFO,0666)<0)&&(errno!=EEXIST))

{

printf(“Cannot create fifo file\n”);

exit(1);

}

}

fd=open(MYFIFO,O_RDONLY);

if (fd == -1)

{

printf(“open fifo file error.\n”);

exit(1);

}

while (1)

{

char buff[128]={0};

int n=read(fd,buff,127);

if ( n==0 )

{

printf(“共从fifo_write.c程序中接收了%d行信息\n”,count);

printf(“对方已关闭!\n”);

break;

}

printf(“read(n=%d):%s”,n,buff);

count++; }

close(fd);

exit(0);

}

2. 共享内存

(1) 建立如下两个C 源文件,并编译链接成可执行文件,文件名可自己设置。

源文件 mem_keyboard.c

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#include <errno.h>

#include <string.h>

int main(int argc, const char *argv[])

{

    key_t key;

    int shmid;

    char *p = NULL;

    

    key=1234;  

    if(key < 0)

    {

        perror("fail ftok ");

        exit(1);

    }

    shmid = shmget(key,128,IPC_CREAT|0666); //创建/打开共享内存,返回id根据id映射

    

    p = (char *)shmat(shmid,NULL,0); //映射,返回地址,根据地址操作

    if( p == (char *)(-1) )

    {

        perror("shmat fail ");

        exit(1);

    }

    while(1) //接收到 quit 结束循环

    {

       //read 时p中 内容不会清空

        fgets(p,10,stdin);

        if(strstr(p,"quit") != NULL)

        {

            break;

        }

    }

    shmdt(p);  //解除映射

    shmctl(shmid,IPC_RMID,NULL); //删除

    return 0;

}

源文件 mem_print.c

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#include <errno.h>

#include <string.h>

#include <unistd.h>

int main(int argc, const char *argv[])

{

    key_t key;

    int shmid;

    char *p = NULL;

    key=1234;

    if(key < 0)

    {

        perror("fail ftok ");

        exit(1);

    }

    shmid = shmget(key,128,IPC_CREAT|0666);

    

    p = (char *)shmat(shmid,NULL,0);

    

    if( p == (char *)(-1) )

    {

        perror("shmat fail ");

        exit(1);

    }

    while(1)

    {

        sleep(1);

        printf("P:%s",p);

        if(strstr(p,"quit") != NULL)

        {

            break;

        }

    }

shmdt(p);

shmctl(shmid,IPC_RMID,NULL);

    return 0;

}

  1. 打开两个命令窗口,分别运行以上两个程序,观察分析程序运行结果。程序未退出时,另外打开一个命令窗口,用命令 ipcs 查看共享内存的信息。

  1. 以上程序只是实现简单的进程通信,未进行互斥控制。打开三个命令窗口,其中两个运行mem_keyboard程序,一个运行mem_print程序,观察程序运行结果,有什么问题。

答:输出结果被另外一个输入信息占据。

(4) 在以上程序的基础上,实现简单的互斥控制,mem_keyboard向共享内存写入数据之后,mem_print才能开始读,同样,mem_print将共享内存的数据读出之后,mem_keyboard才能写入下一行数据。

修改后的代码:

mem_keyboard.c:

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#include <errno.h>

#include <string.h>

int main(int argc, const char *argv[])

{

key_t key,key2;

int shmid,mutex;

char *p = NULL;

char *p2 = NULL;

key=1234;

key2 = 5678;

if(key < 0)

{

perror("fail ftok ");

exit(1);

}

shmid = shmget(key,128,IPC_CREAT|0666);

mutex = shmget(key2, 4, IPC_CREAT|0666);

p = (char *)shmat(shmid,NULL,0);

if( p == (char *)(-1) )

{

perror("shmat fail ");

exit(1);

}

p2 = (char *)shmat(mutex,NULL,0);

if( p2 == (char *)(-1) )

{

perror("shmat2 fail ");

exit(1);

}

*p2=0;

while(1) {

while(*p2!=0);

fgets(p,10,stdin);

*p2=1;

if(strstr(p,“quit”) != NULL)

{

break;

}

}

Shmdt(p);

shmdt(p2);

shmctl(shmid,IPC_RMID,NULL);

shmctl(mutex,IPC_RMID,NULL);

return 0;

}

mem_print.c:

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#include <errno.h>

#include <string.h>

#include <unistd.h>

int main(int argc, const char *argv[])

{

key_t key,key2;

int shmid,mutex;

char *p = NULL;

char *p2 = NULL;

key=1234;

key2 = 5678;

if(key < 0)

{

perror("fail ftok ");

exit(1);

}

shmid = shmget(key,128,IPC_CREAT|0666);

mutex = shmget(key2, 4, IPC_CREAT|0666);

p = (char *)shmat(shmid,NULL,0);

if( p == (char *)(-1) )

{

perror("shmat fail ");

exit(1);

}

p2 = (char *)shmat(mutex,NULL,0);

if( p2 == (char *)(-1) )

{

perror("shmat2 fail ");

exit(1);

}

while(1)

{

while(*p2!=1);

printf(“P:%s”,p);

*p2=0;

if(strstr(p,“quit”) != NULL)

{

break;

}

}

Shmdt(p);

shmdt(p2);

shmctl(shmid,IPC_RMID,NULL);

shmctl(mutex,IPC_RMID,NULL);

return 0;

}

PART 2 Windows进程通信

1. 共享内存文件映射方式

1)建立一个server.cpp文件,源代码为:

#include <iostream>

#include <windows.h>

#include <stdlib.h>

#include <time.h>

using namespace std;

int  main(int argc, char *argv[])

{

    int nRetCode = 0;

    char szBuffer[20] ;

    HANDLE hMapping = CreateFileMapping(NULL,NULL,PAGE_READWRITE,0,4096, "ShareMemory");

    LPVOID lpBase = MapViewOfFile(hMapping,FILE_MAP_WRITE|FILE_MAP_READ,0,0,0);

srand((unsigned)time(NULL));

    while(1)

{   for(int i=0;i<=18;i++)

       szBuffer[i]=rand()%26+65;

    szBuffer[19]='\0';

puts(szBuffer);

strcpy((char*)lpBase,szBuffer);

Sleep(1000);

    }

    Sleep(20000);

    UnmapViewOfFile(lpBase);

    CloseHandle(hMapping);

    return nRetCode;

}

编译生成server.exe文件。

2)建立一个client.cpp文件,源代码为:

#include <iostream>

#include <windows.h>

using namespace std;

int  main(int argc, char *argv[])

{  

  int nRetCode = 0;

  HANDLE hMapping = OpenFileMapping(FILE_MAP_ALL_ACCESS,NULL,"ShareMemory");

  if (hMapping)

    {

        wprintf(L"%s\r\n",L"Success");

        LPVOID lpBase = MapViewOfFile(hMapping,FILE_MAP_READ|FILE_MAP_WRITE,0,0,0);

        char szBuffer[20] = {0};        

        while(1)

        {

  strcpy(szBuffer,(char*)lpBase);  

          printf("%s\n",szBuffer);

  Sleep(1000);

}

        UnmapViewOfFile(lpBase);

        CloseHandle(hMapping);

    }

    else

    {

        wprintf(L"%s",L"OpenMapping Error");

    }

    return nRetCode;

}

编译生成client.exe文件。

  1. 先运行server.exe文件,再运行client.exe文件,分析运行结果。

  1. 实现上述程序的控制停止功能,可以两种方式停止发送字符串:一种是服务器端发送10个字符串后便结束程序;另一种是用户在服务器端输入字符即结束程序。

server.cpp修改代码

#include <iostream>

#include <windows.h>

#include <stdlib.h>

#include <time.h>

#include <conio.h>

using namespace std;

int  main(int argc, char *argv[]){

    int nRetCode = 0;

    char szBuffer[20] ;

char c[20];

HANDLE hMapping = CreateFileMapping(NULL,NULL,PAGE_READWRITE,0,4096,"ShareMemory");

LPVOID lpBase =MapViewOfFile(hMapping,FILE_MAP_WRITE|FILE_MAP_READ,0,0,0);

srand((unsigned)time(NULL));

 while(1){   

 for(int i=0;i<=18;i++)

 szBuffer[i]=rand()%26+65;

     szBuffer[19]='\0';

        puts(szBuffer);

        strcpy((char*)lpBase,szBuffer);

        Sleep(1000);

        if(kbhit())

          {

gets(c);

if(strlen(c)==10)

break;

}

    }

    Sleep(20000);

    UnmapViewOfFile(lpBase);

    CloseHandle(hMapping);

    return nRetCode;

}

5)将上述程序的功能改为:server端每次发送两个100之内的整数,client端实现将两个整数相加,并输出加法计算式。

server.cpp修改代码

#include <iostream>

#include <windows.h>

#include <stdlib.h>

#include <time.h>

using namespace std;

int  main(int argc, char *argv[])

{

    int nRetCode = 0;

char szBuffer[3] ;

HANDLE hMapping =CreateFileMapping(NULL,NULL,PAGE_READWRITE,0,4096, "ShareMemory");

LPVOID lpBase =MapViewOfFile(hMapping,FILE_MAP_WRITE|FILE_MAP_READ,0,0,0);

 srand((unsigned)time(NULL));

while(1)

 {   

 szBuffer[0]=rand()%100;

 szBuffer[1]=rand()%100;

 szBuffer[2]='\0';

 printf("%d\t%d\n",szBuffer[0],szBuffer[1]);

 strcpy((char*)lpBase,szBuffer);

 Sleep(1000);

 }

 Sleep(20000);

 UnmapViewOfFile(lpBase);

 CloseHandle(hMapping);

 return nRetCode;

}

client.exe修改代码:

#include <iostream>

#include <windows.h>

using namespace std;

int  main(int argc, char *argv[]){  

  int nRetCode = 0;

  HANDLE hMapping =OpenFileMapping(FILE_MAP_ALL_ACCESS,NULL,"ShareMemory");

  if (hMapping){

  wprintf(L"%s\r\n",L"Success");

LPVOID lpBase =MapViewOfFile(hMapping,FILE_MAP_READ|FILE_MAP_WRITE,0,0,0);

char szBuffer[20] = {0};   

while(1)

{

strcpy(szBuffer,(char*)lpBase);

printf("%d+%d=%d\n",szBuffer[0],szBuffer[1],szBuffer[0]+szBuffer[1]);

Sleep(1000);

}

UnmapViewOfFile(lpBase);

CloseHandle(hMapping);

}

else{

    wprintf(L"%s",L"OpenMapping Error");

    }

    return nRetCode;

}

2. 命名管道方式

1)建立一个server.cpp文件,源代码为:

#include <iostream>

#include <windows.h>

using namespace std;

int main(int argc, char* argv[])

{

    int nRetCode = 0;

    int err;

    BOOL rc;

    HANDLE hPipeHandle1;

    char lpName[] = "\\\\.\\pipe\\myPipe";

    char InBuffer[50] = "";

    char OutBuffer[50] = "";

    DWORD BytesRead,BytesWrite;

    hPipeHandle1 = CreateNamedPipe((LPCSTR)lpName,                                    PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED|WRITE_DAC,                                    PIPE_TYPE_MESSAGE|PIPE_READMODE_BYTE|PIPE_WAIT,                                    1,20,30,NMPWAIT_USE_DEFAULT_WAIT,                                    (LPSECURITY_ATTRIBUTES)NULL);

    if((hPipeHandle1 == INVALID_HANDLE_VALUE)||(hPipeHandle1 == NULL))

    {

        err = GetLastError();

        printf("Server Pipe Create Fail!err = %d\n",err);

        exit(1);

    }

    else printf("Server Pipe Create Success!\n");

    while(1)

    {

        rc = ConnectNamedPipe(hPipeHandle1,(LPOVERLAPPED)NULL);

        if(rc == 0)

        {

            err = GetLastError();

            printf("Server Pipe Connect Fail err = %d\n",err);

            exit(2);

        }

        else printf("Server Pipe Connect Success\n");

        strcpy(InBuffer,"");

        strcpy(OutBuffer,"");

        rc = ReadFile(hPipeHandle1,InBuffer,sizeof(InBuffer),&BytesRead,(LPOVERLAPPED)NULL);

        if(rc == 0 && BytesRead ==0)

        {

            err = GetLastError();

            printf("Server Read Pipe Fail!err = %d",err);

            exit(3);

        }

        else printf("Server Read Pipe Success!\nDATA from Client is %s\n",InBuffer);

        rc = strcmp(InBuffer,"end");

        if(rc == 0) break;

        printf("Please Input Data to Send\n");

        scanf("%s",OutBuffer);

        rc = WriteFile(hPipeHandle1,OutBuffer,sizeof(OutBuffer),&BytesWrite,(LPOVERLAPPED)NULL);

        if(rc == 0) printf("Server Write Pipe Success!\n");

        else printf("Server Write Pipe Success!\n");

        DisconnectNamedPipe(hPipeHandle1);

        rc = strcmp(OutBuffer,"end");

        if(rc == 0) break;

    }

return 0;

}

编译生成server.exe文件。

2)建立一个client.cpp文件,源代码为:

#include <iostream>

#include <windows.h>

using namespace std;    //命名空间

int main(int argc, char* argv[])

{

    int nRetCode = 0;    //整型类型的

    int err = 0;

    BOOL rc = 0;

    char lpName[] = "\\\\.\\pipe\\myPipe";

    char InBuffer[50] = "";

    char OutBuffer[50] = "";

    DWORD BytesRead;

    while(1)

    {

        strcpy(InBuffer,"");

        strcpy(OutBuffer,"");

        printf("Input Data Please!\n");

        scanf("%s",InBuffer);

        rc = strcmp(InBuffer,"end");

        if(rc == 0)

        {

            rc = CallNamedPipe(lpName,InBuffer,sizeof(InBuffer),OutBuffer,

                sizeof(OutBuffer),&BytesRead,NMPWAIT_USE_DEFAULT_WAIT);

            break;

        }

        rc = WaitNamedPipe(lpName,NMPWAIT_WAIT_FOREVER);

        if(rc == 0)

        {

            err = GetLastError();

            printf("Wait Pipe Fail!err = %d\n",err);

            exit(1);

        }

        else printf("Wait Pipe Success!\n");

        rc = CallNamedPipe(lpName,InBuffer,sizeof(InBuffer),OutBuffer,

                sizeof(OutBuffer),&BytesRead,NMPWAIT_USE_DEFAULT_WAIT);

        rc = strcmp(OutBuffer,"end");

        if(rc == 0) break;

        if(rc == 0)

        {

            err = GetLastError();

            printf("Pipe Call Fail!err = %d\n",err);

            exit(1);

        }

        else printf("Pipe Call Success!\nDATA from Server is %s\n",OutBuffer);

    }

    printf("Now Client to be End!\n");

    return nRetCode;

}

编译生成client.exe文件。

3) 先运行server.exe文件,再运行client.exe文件,分析运行结果。

四、心得体会:

通过本次实验了解了管道进程间通信形式,掌握利用管道进行进程通信的程序设计,管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道; 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统。数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。基本达到了本次实验的要求。另外,对于共享内存通信的工作机制也有了一定的了解,掌握线程与进程在组成成分上的差别,以及与其相适应的通讯方式和应用目标。也让我对管道及共享区的作用和用法,以及对操作系统中各进程之间的通信和协同作用等方面有了更深的了解。总之,本次实验自己收获了很多。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_63134982/article/details/125014902

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签