目录
[1. 进程间通信的介绍](#1. 进程间通信的介绍)
[1.1 概念](#1.1 概念)
[1.2 目的](#1.2 目的)
[1.3 进程间通信的本质](#1.3 进程间通信的本质)
[1.4 进程间通信的分类](#1.4 进程间通信的分类)
[2. 管道](#2. 管道)
[2.1 概念](#2.1 概念)
[2.2 匿名管道](#2.2 匿名管道)
[2.2.1 原理](#2.2.1 原理)
[2.2.2 pipe函数](#2.2.2 pipe函数)
[2.2.3 匿名管道使用步骤](#2.2.3 匿名管道使用步骤)
[2.2.4 管道读写规则](#2.2.4 管道读写规则)
[2.2.5 管道的特点](#2.2.5 管道的特点)
[2.2.6 管道的四种特殊情况](#2.2.6 管道的四种特殊情况)
[2.2.7 管道的大小](#2.2.7 管道的大小)
[2.3 命名管道](#2.3 命名管道)
[2.3.1 原理](#2.3.1 原理)
[2.3.2 使用命令创建命名管道](#2.3.2 使用命令创建命名管道)
[2.3.3 使用函数创建命名管道](#2.3.3 使用函数创建命名管道)
[2.3.4 命名管道的打开规则](#2.3.4 命名管道的打开规则)
[2.3.5 用命名管道实现server&client通信](#2.3.5 用命名管道实现server&client通信)
[2.3.6 用命名管道实现派发计算任务](#2.3.6 用命名管道实现派发计算任务)
[2.3.7 用命名管道实现进程遥控](#2.3.7 用命名管道实现进程遥控)
[2.3.8 用命名管道实现文件拷贝](#2.3.8 用命名管道实现文件拷贝)
[2.3.9 命名管道和匿名管道的区别](#2.3.9 命名管道和匿名管道的区别)
[3. system V共享内存](#3. system V共享内存)
[3.1 共享内存的基本原理](#3.1 共享内存的基本原理)
[3.2 共享内存数据结构](#3.2 共享内存数据结构)
[3.3 共享内存的建立与释放](#3.3 共享内存的建立与释放)
[3.4 共享内存的创建](#3.4 共享内存的创建)
[3.5 共享内存的释放](#3.5 共享内存的释放)
[3.6 共享内存的关联](#3.6 共享内存的关联)
[3.7 共享内存的去关联](#3.7 共享内存的去关联)
[3.8 用共享内存实现server&client通信](#3.8 用共享内存实现server&client通信)
[3.9 共享内存与管道进行对比](#3.9 共享内存与管道进行对比)
1. 进程间通信的介绍
1.1 概念
进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
1.2 目的
1、数据传输: 一个进程需要将它的数据发送给另一个进程。
2、资源共享: 多个进程之间共享同样的资源。
3、通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。
4、进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.3 进程间通信的本质
进程间通信的本质就是,让不同的进程看到同一份资源。
由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。
各个进程之间若想实现通信,一定要借助第三方资源 ,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。

因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。
1.4 进程间通信的分类

2. 管道
2.1 概念
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个"管道"。
例如,统计我们当前使用云服务器上的登录用户个数。

其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到"管道"当中,wc进程再通过标准输入从"管道"当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。
tip: who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l用于统计当前的行数。
2.2 匿名管道
2.2.1 原理
匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。

注意:
1、这里父子进程看到的同一份文件资源是由操作系统来维护 的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
2、管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。
2.2.2 pipe函数
pipe函数用于创建匿名管道,pip函数的函数原型如下:
cpp
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:

pipe函数调用成功时返回0,调用失败时返回-1。
2.2.3 匿名管道使用步骤
在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
step1:父进程调用pipe函数创建管道。

step2:父进程创建子进程。

step3:父进程关闭写端,子进程关闭读端。

注意:
1、管道只能够进行单向通信 ,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
2、从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。
我们可以站在文件描述符的角度再来看看这三个步骤:
step1:父进程调用pipe函数创建管道。

step2:父进程创建子进程。

这里其实就是发生了浅拷贝,子进程将父进程的文件描述符表拷贝了一份儿 ,但是指针的指向并没有改变,这样就能保证父子进程打开的是同一个文件。
step3:父进程关闭写端,子进程关闭读端。

例如,在以下代码当中,子进程向匿名管道当中写入10行数据,父进程从匿名管道当中将数据读出。
cpp
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
int fd[2]={0};
//创建匿名管道
if(pipe(fd)<0)
{
perror("pipe");
return 1;
}
pid_t id=fork();
if(id==0)
{
//子进程:关闭读端
close(fd[0]);
//子进程向管道写入
const char* str="Hello Father,I am child";
int count=10;
while(count--)
{
write(fd[1],str,strlen(str));
sleep(1);
}
//子进程写入完毕,关闭文件
close(fd[1]);
exit(0);
}
//父进程
close(fd[1]);//父进程关闭写端
char buff[64];
while(1)
{
ssize_t s=read(fd[0],buff,sizeof(buff));
if(s>0)
{
buff[s]='\0';
printf("child send to father:%s\n",buff);
}
else if(s==0)
{
printf("end of file\n");
break;
}
else
{
printf("read fail\n");
break;
}
}
close(fd[0]);
waitpid(id,NULL,0);
return 0;
}

2.2.4 管道读写规则
pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:
cpp
int pipe2(int pipefd[2], int flags);
pipe2函数的第二个参数用于设置选项。
1、当没有数据可读时:
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
2、当管道满的时候:
O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。
3、如果所有管道写端对应的文件描述符被关闭,则read返回0。
4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
5、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
2.2.5 管道的特点
1、管道内部自带同步与互斥机制。
我们将一次只允许一个进程使用的资源,称为临界资源。
管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。
临界资源是需要被保护的,若是我们不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。
为了避免这些问题,内核会对管道操作进行同步与互斥:
同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
2、管道的生命周期随进程。
管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。
这里我们可以了联想到我们之前学过的0\1\2文件描述符,我们在程序中一般不会主动去关闭它们,其实在进程退出的时候,系统自动将它们关闭了。
3、管道提供的是流式服务。
对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:
流式服务: 数据没有明确的分割,不分一定的报文段。
数据报服务: 数据有明确的分割,拿数据按报文段拿。
4、管道是半双工通信的。
单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。简单来说:任何一个时刻,一个发,一个收 。
全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。简单来说:任何一个时刻,可以同时收发。
管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。

2.2.6 管道的四种特殊情况
1、写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
2、读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
3、写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后(read会读到文件末尾),就会继续执行该进程之后的代码逻辑,而不会被挂起。
4、读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
相关解释:
1、其中前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
2、第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
3、第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,OS不会做没有意义的事,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。
我们可以通过代码查看第四种情况子进程会收到几号信号:
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int fd[2] = {0};
// 创建匿名管道
if (pipe(fd) < 0)
{
perror("pipe");
return 1;
}
pid_t id = fork();
if (id == 0)
{
// 子进程:关闭读端
close(fd[0]);
// 子进程向管道写入
const char *str = "Hello Father,I am child";
int count = 10;
while (count--)
{
write(fd[1], str, strlen(str));
sleep(1);
}
// 子进程写入完毕,关闭文件
close(fd[1]);
exit(0);
}
// 父进程
close(fd[1]); // 父进程关闭写端
close(fd[0]); // 父进程直接关闭读端(子进程会被系统杀掉)
int status = 0;
waitpid(id, &status, 0);
printf("子进程收到信号:%d\n", status & 0x7F);
return 0;
}

2.2.7 管道的大小
管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,那么管道的最大容量是多少呢?
方法一:使用man手册
根据man手册,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11往后,管道的最大容量是65536字节。
然后我们可以使用uname -r命令,查看自己使用的Linux版本。

方法二:使用ulimit命令
我们还可以使用ulimit -a命令,查看当前资源限制的设定。

根据上图,管道最大容量是512*8=4096bytes,这与我们上面man查出来的不一致,那么下面我们自己利用代码测试一下。
方法三:自行测试
前面说到,若是读端进程一直不读取管道当中的数据,写端进程一直向管道写入数据,当管道被写满后,写端进程就会被挂起。据此,我们可以写出以下代码来测试管道的最大容量。
cpp
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int fd[2] = {0};
if (pipe(fd) < 0)
{ // 使用pipe创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork(); // 使用fork创建子进程
if (id == 0)
{
// child
close(fd[0]); // 子进程关闭读端
char c = 'a';
int count = 0;
// 子进程一直进行写入,一次写入一个字节
while (1)
{
write(fd[1], &c, 1);
count++;
printf("%d\n", count); // 打印当前写入的字节数
}
close(fd[1]);
exit(0);
}
// father
close(fd[1]); // 父进程关闭写端
// 父进程不进行读取
waitpid(id, NULL, 0);
close(fd[0]);
return 0;
}

大家可以看到,进程阻塞了,卡在65536这里,说明管道最大容量是65536bytes(64KB),这是在ubuntu22.04下管道的大小,不同的系统可能不一样。
2.3 命名管道
2.3.1 原理
匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。
命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
注意:
1、普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
2、命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
2.3.2 使用命令创建命名管道

使用这个命名管道文件,就能实现两个进程之间的通信了。
我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。
现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
bash
cp@hcss-ecs-348a:~/test1$ while :; do echo "Hello fifo";sleep 1; done > fifo

这时我们如果直接关闭读端,那么写端进程将被系统杀掉。


这里大家可以明显地看到系统报错,"Connection closed",说明通信中断了。
2.3.3 使用函数创建命名管道
在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:
cpp
int mkfifo(const char *pathname, mode_t mode);
mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。
- 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
- 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。这里和我们之前学习的文件创建类似(管道本来也就是文件)。
mkfifo函数的返回值。
- 命名管道创建成功,返回0。
- 命名管道创建失败,返回-1。
示例:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FILE_NAME "Myfifo"
int main()
{
umask(0); //将文件默认掩码设置为0
if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
perror("mkfifo");
return 1;
}
//创建成功
return 0;
}

2.3.4 命名管道的打开规则
1、如果当前打开操作是为读而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
O_NONBLOCK enable:立刻返回成功。
2、如果当前打开操作是为写而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。
2.3.5 用命名管道实现server&client通信
共用头文件的代码如下:
cpp
//comm.h
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
对于如何让客户端和服务端使用同一个命名管道文件,这里我们可以让客户端和服务端包含同一个头文件,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。
服务端代码(server.c)如下:
cpp
#include "comm.h"
int main()
{
umask(0); // 设置权限掩码
// 使用mkfifo创建命名管道
if (mkfifo(FILE_NAME, 0666) < 0)
{
perror("mkfifo");
return 1;
}
int fd = open(FILE_NAME, O_RDONLY); // 以读的方式打开管道文件
if (fd < 0)
{
perror("open");
return 2;
}
char msg[128];
while (1)
{
// 每次读之前清空msg
msg[0] = '\0';
// 从命名管道中读取信息
ssize_t s = read(fd, msg, sizeof(msg) - 1);//减1给\0留位置
if (s > 0)
{
msg[s] = '\0';
printf("client:%s\n", msg);
}
else if (s == 0)
{
printf("client quit\n");
break;
}
else
{
printf("read error\n");
break;
}
}
close(fd);//通信完成,关闭管道文件
return 0;
}
实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。
客户端代码(client.c)如下:
cpp
#include"comm.h"
int main()
{
int fd=open(FILE_NAME,O_WRONLY);//以写的方式打开管道文件
if(fd<0)
{
perror("open");
return 1;
}
char msg[128];
while(1)
{
msg[0]='\0';//每次读清空msg
printf("请输入信息:");
fflush(stdout);
ssize_t s=read(0,msg,sizeof(msg)-1);
if(s>0)
{
msg[s-1]='\0';//把\n的位置改成0
//将信息写入命名管道
write(fd,msg,strlen(msg));
}
}
close(fd);
return 0;
}
而对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。
代码编写完毕后,先将服务端进程运行起来,之后我们就能在客户端看到这个已经被创建的命名管道文件。

下面我们在运行客户端,在客户端输入消息,服务端将看见用户端发来的消息;


大家观察上面的图,这样我们就实现了进程间的通信,当我们客户端终止进程,服务端也会跟着退出。
当客户端和服务端运行起来时,我们还可以通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同。
也就证明了,命名管道是可以实现两个毫不相关进程之间的通信的。

服务端和客户端之间的退出关系
当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。


当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。


通信是在内存当中进行的
若是我们只让客户端向管道写入数据,而服务端不从管道读取数据,那么这个管道文件的大小会不会发生变化呢?
cpp
//server.c
#include "comm.h"
int main()
{
umask(0); //将文件默认掩码设置为0
if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
perror("mkfifo");
return 1;
}
int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
if (fd < 0){
perror("open");
return 2;
}
while (1){
//服务端不读取管道信息
}
close(fd); //通信完毕,关闭命名管道文件
return 0;
}


这里大家发现,管道文件的大小为0,尽管服务端不读取管道当中的数据,但是管道当中的数据并没有被刷新到磁盘,也就说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的,这也就与我们前面的结论逻辑自洽了。
上面我们是分开实现了客户端和服务端,这里其实我们可以封装一个类,即"命名管道"类,这样代码的可读性就更强,也更加系统化。
comm.hpp
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATH "."
#define FILENAME "fifo"
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
class Namedfifo
{
public:
Namedfifo(const std::string &path, const std::string &name)
: _path(path), _name(name)
{
_fifoname = path + "/" + name;
umask(0);
// 创建管道
int n = mkfifo(_fifoname.c_str(), 0666);
if (n < 0)
{
ERR_EXIT("mkfifo");
}
else
{
std::cout << "mkfifo success" << std::endl;
}
}
~Namedfifo()
{
// 删除管道文件
int n = unlink(_fifoname.c_str());
if (n < 0)
{
ERR_EXIT("unlink");
}
else
{
std::cout << "remove success" << std::endl;
}
}
private:
std::string _path;
std::string _name;
std::string _fifoname;
};
class Fileoper
{
public:
Fileoper(const std::string &path, const std::string &name)
: _path(path), _name(name), _fd(-1)
{
_fifoname = path + "/" + name;
}
void OpenforRead()
{
_fd=open(_fifoname.c_str(),O_RDONLY);
if(_fd<0)
{
ERR_EXIT("open");
}
std::cout<<"open fifo success"<<std::endl;
}
void OpenforWrite()
{
_fd=open(_fifoname.c_str(),O_WRONLY);
if(_fd<0)
{
ERR_EXIT("open");
}
std::cout<<"open fifo success"<<std::endl;
}
void Write()
{
//写入操作
std::string messsage;
int cnt=1;
pid_t id=getpid();
while(true)
{
std::cout<<"请输入";
std::getline(std::cin,messsage);
messsage+=(", message number:"+std::to_string(cnt++)+",["+std::to_string(id)+"]");
write(_fd,messsage.c_str(),messsage.size());
}
}
void Read()
{
//读取操作
while(true)
{
char buffer[1024];
int number=read(_fd,buffer,sizeof(buffer)-1);
if(number>0)
{
buffer[number]=0;
std::cout<<"Client say:"<<buffer<<std::endl;
}
else if(number==0)
{
std::cout<<"client quit"<<std::endl;
break;
}
else
{
std::cout<<"read error"<<std::endl;
break;
}
}
}
void Close()
{
if(_fd>0) close(_fd);
}
~Fileoper()
{
}
private:
std::string _path;
std::string _name;
std::string _fifoname;
int _fd;
};
这里需要说明一下,.hpp为后缀的文件表示头源文件混编的,我们可以直接在这里实现对应的方法。
cpp
#include"comm.hpp"
int main()
{
//创建管道文件
Namedfifo fifo(PATH,FILENAME);
//文件操作
Fileoper readerfile(PATH,FILENAME);
readerfile.OpenforRead();
readerfile.Read();
readerfile.Close();
return 0;
}
cpp
#include"comm.hpp"
int main()
{
Fileoper writerfile(PATH,FILENAME);
writerfile.OpenforWrite();
writerfile.Write();
writerfile.Close();
return 0;
}
Makefile
bash
.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 -f client server
大家可以发现,我们封装以后,客户端和服务端的代码都变得非常简洁,我们可以很高效地完成通信操作(直接调用类中的接口就即可)。
2.3.6 用命名管道实现派发计算任务
需要注意的是两个进程之间的通信,并不是简单的发送字符串而已,服务端是会对客户端发送过来的信息进行某些处理的。
这里我们以客户端向服务端派发计算任务为例,客户端通过管道向服务端发送双操作数的计算请求,服务端接收到客户端的信息后需要计算出相应的结果。
这里我们无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。
cpp
#include "comm.h"
int main()
{
umask(0);
if (mkfifo(FILE_NAME, 0666) < 0) // 使用mkfifo创建命名管道
{
perror("mkfifo");
return 1;
}
int fd = open(FILE_NAME, O_RDONLY);
if (fd < 0)
{
perror("open");
return 2;
}
char msg[128];
while (1)
{
msg[0] = '\0'; // 每次读之前清空msg
// 从命名管道中读取信息
ssize_t s = read(fd, msg, sizeof(msg) - 1);
if (s > 0)
{
msg[s] = '\0'; // 设置'\0',便于输出
printf("client:%s\n", msg);
// 服务端处理计算任务
char *lable = "+-*/%";
char *p = msg;
int flag = 0;
while (*p)
{
switch (*p)
{
case '+':
flag = 0;
break;
case '-':
flag = 1;
break;
case '*':
flag = 2;
case '/':
flag = 3;
break;
case '%':
flag = 4;
break;
}
p++;
}
char *data1 = strtok(msg, "+-*/%");
char *data2 = strtok(NULL, "+-*/%");
int num1 = atoi(data1);
int num2 = atoi(data2);
int ret = 0;
switch (flag)
{
case 0:
ret = num1 + num2;
break;
case 1:
ret = num1 - num2;
break;
case 2:
ret = num1 * num2;
break;
case 3:
ret = num1 / num2;
break;
case 4:
ret = num1 % num2;
break;
}
printf("%d %c %d = %d\n",num1,lable[flag],num2,ret);
}
else if(s==0)
{
printf("client quit\n");
break;
}
else
{
printf("rean error\n");
break;
}
}
close(fd);
return 0;
}
此时服务端接收到客户端的信息后,需要进行的处理动作就不是将其打印到显示器了,而是需要将信息经过进一步的处理,从而得到相应的结果。


2.3.7 用命名管道实现进程遥控
们可以通过一个进程来控制另一个进程的行为,比如我们从客户端输入命令到管道当中,再让服务端将管道当中的命令读取出来并执行。
下面我们只实现了让服务端执行不带选项的命令,若是想让服务端执行带选项的命令,可以对管道当中获取的命令进行解析处理。
这里的实现非常简单,只需让服务端从管道当中读取命令后创建子进程,然后再进行进程程序替换即可。
这里也无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。
cpp
#include "comm.h"
int main()
{
umask(0);
if (mkfifo(FILE_NAME, 0666) < 0)
{
perror("mkfifo");
return 1;
}
int fd = open(FILE_NAME, O_RDONLY);
if (fd < 0)
{
perror("open");
return 2;
}
char msg[128];
while (1)
{
msg[0] = '\0';
// 从命名管道读取数据
ssize_t s = read(fd, msg, sizeof(msg) - 1);
if (s > 0)
{
msg[s] = '\0';
printf("client:%s\n", msg);
if (fork() == 0)
{
// 子进程
execlp(msg, msg, NULL); // 进行进程替换
exit(1);
}
waitpid(-1, NULL, 0); // 等待子进程
}
else if (s == 0)
{
printf("client quit\n");
break;
}
else
{
printf("read error\n");
break;
}
}
return 0;
}


2.3.8 用命名管道实现文件拷贝
需要拷贝的文件是text.txt,该文件当中的内容如下:

我们要做的是,在客户端将text.txt 文件发送给服务端,在服务端创建一个text_bat.txt 文件,并将从管道获取到的数据写入text_bat.txt 文件当中,至此便实现了text.txt文件的拷贝。

下面我们来实现具体代码:
其中服务端需要做的就是,创建命名管道并以读的方式打开该命名管道,再创建一个名为text_bat.txt 的文件,之后需要做的就是将从管道当中读取到的数据写入到text_bat.txt文件当中即可。
服务端代码如下:
cpp
#include"comm.h"
int main()
{
umask(0);
if(mkfifo(FILE_NAME,0666)<0)
{
perror("mkfifo");
return 1;
}
int fd=open(FILE_NAME,O_RDONLY);
if(fd<0)
{
perror("open");
return 2;
}
//创建文件text_bat.txt,并以写的方式打开该文件
int fout=open("text_bat.txt",O_WRONLY,0666);
if(fout<0)
{
perror("write");
return 3;
}
char msg[128];
while(1)
{
msg[0]='\0';//每次读之前将msg清空
//从命名管道中读取信息
ssize_t s=read(fd,msg,sizeof(msg)-1);
if(s>0)
{
write(fout,msg,s);//将读到的信息写入text_bat.txt文件中
}
else if(s==0)
{
printf("client quit\n");
break;
}
else
{
printf("read error\n");
break;
}
}
close(fd);//通信完毕,关闭管道文件
close(fout);//数据写入完毕,关闭text_bat.txt文件
return 0;
}
而客户端需要做的就是,以写的方式 打开这个已经存在的命名管道文件 ,再以读的方式 打开text.txt 文件,之后需要做的就是将text.txt文件当中的数据读取出来并写入管道当中即可。
客户端的代码如下:
cpp
#include "comm.h"
int main()
{
int fd = open(FILE_NAME, O_WRONLY); // 以写的方式打开管道文件
if (fd < 0)
{
perror("open");
return 1;
}
int fin = open("text.txt", O_RDONLY);
if (fin < 0)
{
perror("open");
return 2;
}
char msg[128];
while (1)
{
// 从text.txt中读取数据
ssize_t s = read(fin, msg, sizeof(msg));
if (s > 0)
{
write(fd, msg, s); // 将数据写到命名管道中
}
else if (s == 0)
{
printf("read end of file\n");
break;
}
else
{
printf("read error\n");
break;
}
}
close(fd); // 通信完毕,关闭管道文件
close(fin); // 数据读取完毕,关闭text.txt文件
return 0;
}
编写完代码后,先运行服务端,再运行客户端,一瞬间这两个进程就相继运行结束了。



程序运行完毕后,我们查看text_bat.txt文件,发现其中的内容就是我们一开始创建的文件内容,至此我们就完成了拷贝工作。
使用管道实现文件的拷贝有什么意义?
因为这里是使用管道在本地进行的文件拷贝,所以看似没什么意义,但我们若是将这里的管道想象成"网络",将客户端想象成"Windows Xshell",再将服务端想象成"centos服务器"。
那我们此时实现的就是文件上传的功能,若是将方向反过来,那么实现的就是文件下载的功能。

2.3.9 命名管道和匿名管道的区别
1、匿名管道由pipe函数创建并打开。
2、命名管道由mkfifo函数创建,由open函数打开。
3、FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。
3. system V共享内存
3.1 共享内存的基本原理
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。

注意:
这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。
3.2 共享内存数据结构
在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理(先描述,再组织),所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。
共享内存的数据结构如下:
cpp
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值 ,这个key值用于标识系统中共享内存的唯一性。
可以看到上面共享内存数据结构的第一个成员是**shm_perm
,shm_perm
** 是一个**ipc_perm
**类型的结构体变量,每个共享内存的key值存储在shm_perm
这个结构体变量当中,其中ipc_perm
结构体的定义如下:
cpp
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
3.3 共享内存的建立与释放
共享内存的建立大致包括以下两个过程:
- 在物理内存当中申请共享内存空间。
- 将申请到的共享内存挂接到地址空间,即建立映射关系。
共享内存的释放大致包括以下两个过程:
- 将共享内存与地址空间去关联,即取消映射关系。
- 释放共享内存空间,即将物理内存归还给系统
3.4 共享内存的创建
创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:
cpp
int shmget(key_t key, size_t size, int shmflg);
shmget函数的参数说明:
第一个参数key,表示待创建共享内存在系统当中的唯一标识。
第二个参数size,表示待创建共享内存的大小。
第三个参数shmflg,表示创建共享内存的方式。
shmget函数的返回值说明:
shmget调用成功,返回一个有效的共享内存标识符 (用户层标识符)。
shmget调用失败,返回-1。
注意: 我们把具有标定某种资源能力的东西叫做句柄 ,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作。
传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取;
cpp
key_t ftok(const char *pathname, int proj_id);
ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。
需要注意的是,pathname所指定的文件必须存在且可存取。
注意:
1、使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
2、需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。
传入shmget函数的第三个参数shmflg,常用的组合方式有以下两种:

使用组合IPC_CREAT,一定会获得一个共享内存的句柄,但无法确认该共享内存是否是新建的共享内存。
使用组合IPC_CREAT | IPC_EXCL,只有shmget函数调用成功时才会获得共享内存的句柄,并且该共享内存一定是新建的共享内存。
至此我们就可以使用ftok和shmget函数创建一块共享内存了,创建后我们可以将共享内存的key值和句柄进行打印,以便观察,代码如下:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/test.c"
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096
int main()
{
key_t key=ftok(PATHNAME,PROJ_ID);//获取key值
if(key<0)
{
perror("ftok");
return 1;
}
int shm = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
if(shm<0)
{
perror("shmget");
return 2;
}
printf("key:%x\n",key);
printf("shm:%d\n",shm);
return 0;
}
该代码编写完毕运行后,我们可以看到输出的key值和句柄值:

Linux当中,我们可以使用ipcs
命令查看有关进程间通信设施的信息。

单独使用ipcs
命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
- -q:列出消息队列相关信息。
- -m:列出共享内存相关信息。
- -s:列出信号量相关信息。
例如,携带-m选项查看共享内存相关信息:

此时,根据ipcs
命令的查看结果和我们的输出结果可以确认,共享内存已经创建成功了。
ipcs
命令输出的每列信息的含义如下:

注意: key是在内核层面 上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于fd和FILE*之间的的关系。
注意:
共享内存的大小是4KB(4096字节)的整数倍,如果我们申请4097,那么系统会向上取整,为我们申请4096*2,但是我们只能用4097个字节。
3.5 共享内存的释放
通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。
实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
使用命令释放共享内存资源
我们可以使用**ipcrm -m shmid
命令**释放指定id的共享内存资源。

注意: 指定删除时使用的是共享内存的用户层id,即列表当中的shmid。
使用程序释放共享内存资源
控制共享内存我们需要用shmctl函数,shmctl函数的函数原型如下:
cpp
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl函数的参数说明:
第一个参数shmid,表示所控制共享内存的用户级标识符。
第二个参数cmd,表示具体的控制动作。
第三个参数buf,用于获取或设置所控制共享内存的数据结构。
shmctl函数的返回值说明:
shmctl调用成功,返回0。
shmctl调用失败,返回-1。
其中,作为shmctl函数的第二个参数传入的常用的选项有以下三个:

例如,在以下代码当中,共享内存被创建,3秒后程序自动移除共享内存,再过3秒程序就会自动退出。
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/test.c"
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096
int main()
{
key_t key=ftok(PATHNAME,PROJ_ID);//获取key值
if(key<0)
{
perror("ftok");
return 1;
}
int shm = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
if(shm<0)
{
perror("shmget");
return 2;
}
printf("key:%x\n",key);//打印key值
printf("shm:%d\n",shm);//打印句柄
sleep(3);
shmctl(shm,IPC_RMID,NULL);//释放共享内存
sleep(3);
return 0;
}
我们可以打开监控脚本,来观察共享内存的变化。
bash
cp@hcss-ecs-348a:~/test1$ while :; do ipcs -m;echo "###################################";sleep 1;done

3.6 共享内存的关联
将共享内存连接到进程地址空间我们需要用shmat函数,shmat函数的函数原型如下:
cpp
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmat函数的参数说明:
第一个参数shmid,表示待关联共享内存的用户级标识符。
第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL ,表示让内核自己决定一个合适的地址位置。
第三个参数shmflg,表示关联共享内存时设置的某些属性。
shmat函数的返回值说明:
shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
shmat调用失败,返回(void*)-1。
其中,作为shmat函数的第三个参数传入的常用的选项有以下三个:

其实这个函数就类似于我们之前学的malloc函数,申请空间,然后返回起始地址。
这时我们可以尝试使用shmat函数对共享内存进行关联:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/test.c"
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096
int main()
{
key_t key = ftok(PATHNAME, PROJ_ID); // 获取key值
if (key < 0)
{
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);
if (shm < 0)
{
perror("shmget");
return 2;
}
printf("key:%x\n", key); // 打印key值
printf("shm:%d\n", shm); // 打印句柄
sleep(2);
char* mem=shmat(shm,NULL,0);//关联共享内存
if(mem==(void*)-1)
{
perror("shmat");
return 3;
}
printf("attend end\n");
sleep(2);
shmctl(shm,IPC_RMID,NULL);//释放共享内存
return 0;
}
代码运行后发现关联失败,主要原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此server进程没有权限关联该共享内存。


我们应该在使用shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/test.c"
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096
int main()
{
umask(0);
key_t key = ftok(PATHNAME, PROJ_ID); // 获取key值
if (key < 0)
{
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shm < 0)
{
perror("shmget");
return 2;
}
printf("key:%x\n", key); // 打印key值
printf("shm:%d\n", shm); // 打印句柄
sleep(2);
char *mem = shmat(shm, NULL, 0); // 关联共享内存
if (mem == (void *)-1)
{
perror("shmat");
return 3;
}
printf("attend end\n");
sleep(2);
shmctl(shm, IPC_RMID, NULL); // 释放共享内存
return 0;
}
我们只需要在创建共享内存时带上权限即可,这里需要先将系统的权限掩码置为0。
此时再运行程序,即可发现关联该共享内存的进程数由0变成了1,而共享内存的权限显示也不再是0,而是我们设置的666权限。

3.7 共享内存的去关联
取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:
cpp
int shmdt(const void *shmaddr);
shmdt函数的参数说明:
待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。
shmdt函数的返回值说明:
shmdt调用成功,返回0。
shmdt调用失败,返回-1。
现在我们就能够取消共享内存与进程之间的关联了。
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/test.c"
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096
int main()
{
umask(0);
key_t key = ftok(PATHNAME, PROJ_ID); // 获取key值
if (key < 0)
{
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shm < 0)
{
perror("shmget");
return 2;
}
printf("key:%x\n", key); // 打印key值
printf("shm:%d\n", shm); // 打印句柄
sleep(2);
char *mem = shmat(shm, NULL, 0); // 关联共享内存
if (mem == (void *)-1)
{
perror("shmat");
return 3;
}
printf("attend end\n");
sleep(2);
printf("detach begin\n");
sleep(2);
shmdt(mem);//共享内存去关联
printf("detach end\n");
sleep(2);
shmctl(shm, IPC_RMID, NULL); // 释放共享内存
return 0;
}
运行程序,通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联。

注意: 将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系,比如上面的代码,我们最后还需要调用系统调用主动释放共享内存。
3.8 用共享内存实现server&client通信
在知道了共享内存的创建、关联、去关联以及释放后,现在可以尝试让两个进程通过共享内存进行通信了。在让两个进程进行通信之前,我们可以先测试一下这两个进程能否成功挂接到同一个共享内存上。
为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一种key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行挂接。
这里我们可以将这些需要共用的信息放入一个头文件当中,服务端和客户端共用这个头文件即可。
共用头文件的代码如下:
cpp
//comm.h
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/shm/server.c" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小
服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,之后进入死循环,便于观察服务端是否挂接成功。
服务端代码如下:
cpp
//server.c
#include"comm.h"
int main()
{
umask(0);
key_t key=ftok(PATHNAME,PROJ_ID);//获取key值
if(key<0)
{
perror("ftok");
return 1;
}
int shm=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);//创建共享内存
if(shm<0)
{
perror("shmget");
return 2;
}
printf("key:%x\n",key);//打印key值
printf("shm:%d\n",shm);//打印用户层id
char* mem=shmat(shm,NULL,0);//关联共享内存
while(1){}
shmdt(mem);//共享内存去关联
shmctl(shm,IPC_RMID,NULL);//释放共享内存
return 0;
}
客户端只需要直接和服务端创建的共享内存进行关联即可,之后也进入死循环,便于观察客户端是否挂接成功。
客户端代码如下:
cpp
//client.c
#include"comm.h"
int main()
{
key_t key=ftok(PATHNAME,PROJ_ID);//获取与server端相同的key值
if(key<0)
{
perror("ftok");
return 1;
}
int shm=shmget(key,SIZE,IPC_CREAT);//获取与server端创建的共享内存的用户层id
if(shm<0)
{
perror("shmget");
return 2;
}
printf("key:%x\n",key);
printf("shm:%d\n",shm);
char* mem=shmat(shm,NULL,0);//关联共享内存
while(1){}
shmdt(mem);//共享内存去关联
return 0;
}
先后运行服务端和客户端后,通过监控脚本可以看到服务端和客户端所关联的是同一个共享内存,共享内存关联的进程数也是2,表示服务端和客户端挂接共享内存成功。



此时我们就可以让服务端和客户端进行通信了,这里以简单的发送字符串为例。
客户端不断向共享内存写入数据:
cpp
//客户端不断向共享内存写入数据
int i = 0;
while (1){
mem[i] = 'A' + i;
i++;
mem[i] = '\0';
sleep(1);
}
服务端不断读取共享内存当中的数据并输出:
cpp
//服务端不断读取共享内存当中的数据并输出
while (1){
printf("client# %s\n", mem);
sleep(1);
}
此时先运行服务端创建共享内存,当我们运行客户端时服务端就开始不断输出数据,说明服务端和客户端是能够正常通信的。


谈到这里,大家有没有发现一个问题,我们在对共享内存做读写操作时,并没有调用系统调用,反观上面命名管道,我们读写时直接使用了read和write等系统调用的接口,那么这是什么原因?
实际上答案与共享内存的映射位置有关,共享内存经过页表映射到进程地址空间的共享区,而共享区是属于用户的,可以让用户直接使用。
共享内存的优缺点
优点:
1、映射之后,读写直接被对方看到。
2、不需要进行系统调用来获取或者写入内容。
3、是速度最快的进程间通信方式。
缺点:
1、通信双方没有"同步机制"。
2、导致数据不一致问题,没有保护机制。(两个进程各跑各的,不存在谁等谁,如果写端正在写入,而读端没等写端写完就直接读了,那么可能读上来的数据不全,导致数据不一致问题)。
3.9 共享内存与管道进行对比
当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。
实际上,共享内存是所有进程间通信方式中最快的一种通信方式。

从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:
1、服务端将信息从输入文件复制到服务端的临时缓冲区中。
2、将服务端临时缓冲区的信息复制到管道中。
3、客户端将信息从管道复制到客户端的缓冲区中。
4、将客户端临时缓冲区的信息复制到输出文件中。
我们再来看看共享内存通信:

从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:
1、从输入文件到共享内存。
2、从共享内存到输出文件。
所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少,并且不需要调用系统调用来进行读写操作。