目录
????????16.5 共享内存实现server&client
在Linux系统中,进程间通信(Inter-Process Communication,IPC)是不同进程之间进行数据交换和同步的一种机制
进程间通信(IPC)是由于在计算机系统中,多个进程可能同时运行,而这些进程可能需要共享信息、协同工作或者进行数据交换,以下是一些常见的原因,解释了为什么需要进程间通信:
- 资源共享: 进程间通信允许多个进程共享系统资源,如文件、设备、内存等。这种共享资源的方式使得系统可以更高效地利用资源,避免了每个进程都维护一份独立的资源拷贝。
- 并发执行: 在多任务操作系统中,多个进程可能同时运行。通过进程间通信,这些进程可以同步执行,共享信息,以便更好地协调工作。
- 模块化设计: 大型软件系统通常被分解成多个模块或组件。这些模块可能运行在独立的进程中,通过IPC进行通信,使得系统的设计更具模块化和可维护性。
- 数据传递: 进程间通信提供了一种机制,允许进程之间传递数据,从而使得它们能够相互交流和共享信息。这对于实现分布式计算、网络通信等场景是至关重要的。?
管道:
System V IPC: 这是一组在System V风格的UNIX操作系统中引入的进程间通信机制。
POSIX IPC: 这是一组遵循POSIX标准的IPC机制,可在不同的操作系统上使用。
其实,在设计 IPC 方式时,其中的两个关键点就是共享与通信,这两个概念是进程间通信的基础,不同的通信方式只是在实现这两个概念时采用了不同的技术手段.
共享:?共享其实是IPC设计的一个难点,因为进程在设计之初就具有独立性,共享的方式多种多样,其中一种常见的方式是共享内存。通过共享内存,多个进程可以直接读写同一块内存区域,实现了高效的数据共享。此外,共享还可以通过共享文件等方式实现.共享的本质就是让不同的进程看见同一份资源
通信:通信是另一个 IPC 中不可或缺的概念,通信机制使得进程能够感知彼此的状态、协调行动,并确保数据的正确传递。通信机制不仅仅是信息传递,还包括了进程同步的概念。通过信号量、互斥量、条件变量等机制,可以实现对共享资源的安全访问,防止多个进程同时对同一资源进行写操作,确保数据的一致性
管道想必大家都不陌生,它是Unix中最古老的进程间通信的形式,允许一个进程的输出成为另一个进程的输入。在操作系统中,管道是一种特殊的文件,常用于在父进程和子进程之间或者在两个独立的进程之间传递数据
$ command1 | command2
在命令行中,我们常常用管道连接多个命令,形成了一个数据处理的流水线
一般而言,管道可以分为两类,分别是匿名管道和命名管道:
匿名管道(Anonymous Pipe):
pipe
系统调用创建。命名管道(Named Pipe):
mkfifo
系统调用创建,也称为FIFO文件。而我们常用的 , |(管道符号)用于连接两个命令,其实就是创建一个匿名管道,使得 command1 的输出被传输到?command2 的输入
在我们之前讲过文件描述符在PCB中的实际结构,这里我们需要补充一点:
在fork创建新进程式,父进程会将自己的文件描述符表一并复制到新的子进程中.出于这样的原因我们就可以通过在父进程中指定两个不同的fd指向同一份文件,其中一个代表读端,另一个代表写端,再调用fork()就实现了让两个进程共享同一份资源的工作
具体流程我们可以参考下图:文件描述符在
?
而想要实现指定两个fd,分别代表读端与写端的话,我们可以调用pipe函数来实现.
这里我们可以先使用man手册来查看pipe
这里我们可以看到pipe函数的原型:
int pipe(int pipefd[2])
参数:其参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
数组元素 | 含义 |
---|---|
pipefd[0] | 管道读端的文件描述符 |
pipefd[1] | 管道写端的文件描述符 |
返回值:pipe函数调用成功时返回0,调用失败时返回-1
注意:管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
这里我们简单实现一个父子进程通过匿名管道通信的程序:
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <cstdio>
#define buffer_size 1024 * 8
using namespace std;
int main()
{
int pipefd[2] = {0};
if (pipe(pipefd))
{
perror("pipe");
}
pid_t id = fork();
if (id == -1)
{
perror("fork");
}
if (id == 0)
{
// 子进程读
close(pipefd[1]);
// 写入的一方,fd没有关闭,如果有数据,就读,没有数据就等
// 写入的一方,fd关闭, 读取的一方,read会返回0,表示读到了文件的结尾!
char get_buffer[buffer_size];
const string message = "我是子进程,我收到了消息";
while(1)
{
ssize_t s =read(pipefd[0],get_buffer,buffer_size-1);
if(s>0)
{
get_buffer[s]=0;
cout <<message <<'['<< getpid() << "]" << get_buffer << endl;
}
else
{
cout << "writer quit(father), me quit!!!" << endl;
break;
}
}
}
else
{
// 父进程写
close(pipefd[0]);
const string message = "我是父进程,我在给你发消息";
char send_buffer[buffer_size];
int count = 0;
while (1)
{
snprintf(send_buffer, sizeof send_buffer, "%d:%s|%d", getpid(), message.c_str(), count++);
write(pipefd[1], send_buffer, strlen(send_buffer));
if (count == 5)
{
cout << "writer quit(father)" << endl;
break;
}
sleep(1);
}
close(pipefd[1]);
pid_t ret = waitpid(id, nullptr, 0);
}
return 0;
}
运行结果如下:
?单工通信(Simplex Communication):
半双工通信(Half Duplex Communication):
全双工通信(Full Duplex Communication):
匿名管道是一种半双工通信方式,它允许数据在一个方向上传输,但不能同时进行双向传输。如果需要实现双方之间的双向通信,通常需要建立两个管道
例如,可以通过以下方式创建两个管道:
#include <unistd.h>
int main() {
int fd1[2]; // 父进程写,子进程读
int fd2[2]; // 父进程读,子进程写
// 创建第一个管道
if (pipe(fd1) == -1) {
// 处理错误
}
// 创建第二个管道
if (pipe(fd2) == -1) {
// 处理错误
}
// 进行fork,创建子进程
pid_t pid = fork();
if (pid == -1) {
// 处理错误
} else if (pid == 0) {
// 子进程,关闭不需要的管道端
close(fd1[1]); // 关闭写入端
close(fd2[0]); // 关闭读取端
// 子进程可以使用fd1[0]读取,使用fd2[1]写入
} else {
// 父进程,关闭不需要的管道端
close(fd1[0]); // 关闭读取端
close(fd2[1]); // 关闭写入端
// 父进程可以使用fd1[1]写入,使用fd2[0]读取
}
// 其他逻辑...
return 0;
}
管道内部自带同步与互斥机制:
管道的生命周期随进程:
管道提供的是流式服务:
管道是半双工通信的:
- 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被阻塞挂起,直到管道里面有数据后,读端进程才会被唤醒。
- 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被阻塞挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
- 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
- 读端进程将读端关闭,而写端进程还在一直向管道写入数据,没有进程读取,那么写入的数据就没有意义,那么操作系统会将写端进程杀掉。
匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,而命名管道可以实现两个毫不相关进程之间的通信
命名管道在磁盘上有一个简单的映像,通过一个特定的文件名标识。这种映像使得进程可以通过文件系统的方式访问命名管道,而不需要直接操作内存
相较于使用普通文件进行通信,命名管道提供了更安全的通信机制。由于管道是一种特殊的文件,而且其映像大小永远为0,因此通信数据不会被刷新到磁盘,减少了对数据的持久性存储,有助于提高通信的安全性
我们可以使用mkfifo命令创建一个命名管道?
mkfifo pipe
这里的p类型,指的就是我们的命名管道文件类型
在此基础上我们可以使用这个命名管道文件实现两个进程之间的通信:我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。
while :; do echo "pipe test" ;sleep 1; done >namedpipe
使用cat命令的读进程主动退出,另一个写进程就被杀掉了;?因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。(当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉)
mkfifo的函数原型
int mkfifo(const char *pathname, mode_t mode)
参数:
pathname 是一个包含文件路径的字符串,指定了要创建的命名管道的名称
mode 是一个表示权限的参数,指定了创建的文件的访问权限,类似于 chmod 函数
返回值:
如果 mkfifo 成功创建了命名管道,则返回值为 0
如果 mkfifo 失败,返回值为 -1,并且 errno 会被设置为指示具体错误的整数值
在我们创建好一个命名管道后,我们需要使用之前在提到open()来打开,这里在打开文件时,我们需要考虑open的第二个参数flags的影响
在默认情况下,命名管道(FIFO)的打开是阻塞的
读操作:
- O_NONBLOCK 未启用(阻塞): 如果当前打开操作是为读而打开 FIFO,而 O_NONBLOCK 未启用,那么读操作将一直阻塞,直到有相应的进程为写而打开该 FIFO。
- O_NONBLOCK 启用: 如果 O_NONBLOCK 启用,读操作将立即返回成功。如果 FIFO 中没有数据可读,返回的读操作结果可能是空。
写操作:
- O_NONBLOCK 未启用(阻塞): 如果当前打开操作是为写而打开 FIFO,而 O_NONBLOCK 未启用,那么写操作将一直阻塞,直到有相应的进程为读而打开该 FIFO。
- O_NONBLOCK 启用: 如果 O_NONBLOCK 启用,写操作将立即返回失败,错误码为 ENXIO。这是因为没有相应的进程为读而打开该 FIFO。?
演示如下:?
#include <fcntl.h>
#include <unistd.h>
int main() {
const char *fifoPath = "/tmp/myfifo";
// 打开命名管道,使用 O_NONBLOCK 标志启用非阻塞模式
int fd = open(fifoPath, O_RDONLY | O_NONBLOCK);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 在这里可以进行读操作,不会阻塞
close(fd);
return 0;
}
?除此之外,我们还使用 fcntl 函数在运行时更改属性:
#include <fcntl.h>
#include <unistd.h>
int main() {
const char *fifoPath = "/tmp/myfifo";
// 打开命名管道
int fd = open(fifoPath, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 使用 fcntl 函数启用非阻塞模式
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl");
close(fd);
exit(EXIT_FAILURE);
}
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) == -1) {
perror("fcntl");
close(fd);
exit(EXIT_FAILURE);
}
// 在这里可以进行读操作,不会阻塞
close(fd);
return 0;
}
实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了
#include "Common.hpp"
#include <sys/wait.h>
static void getMessage(int fd)
{
char buffer[SIZE];
while (true)
{
memset(buffer, '\0', sizeof(buffer));
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
cout <<"[" << getpid() << "] "<< "client say> " << buffer << endl;
}
else if (s == 0)
{
// end of file
cerr <<"[" << getpid() << "] " << "read end of file, clien quit, server quit too!" << endl;
break;
}
else
{
// read error
perror("read");
break;
}
}
}
int main()
{
// 1. 创建管道文件
if (mkfifo(ipcPath.c_str(), MODE) < 0)
{
perror("mkfifo");
exit(1);
}
Log("创建管道文件成功", Debug) << " step 1" << endl;
// 2. 正常的文件操作
int fd = open(ipcPath.c_str(), O_RDONLY);
if (fd < 0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功", Debug) << " step 2" << endl;
int nums = 3;
for (int i = 0; i < nums; i++)
{
pid_t id = fork();
if (id == 0)
{
// 3. 编写正常的通信代码了
getMessage(fd);
exit(1);
}
}
for(int i = 0; i < nums; i++)
{
waitpid(-1, nullptr, 0);
}
// 4. 关闭文件
close(fd);
Log("关闭管道文件成功", Debug) << " step 3" << endl;
unlink(ipcPath.c_str()); // 通信完毕,就删除文件
Log("删除管道文件成功", Debug) << " step 4" << endl;
return 0;
}
而对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。
客户端的代码如下:
#include "Common.hpp"
int main()
{
// 1. 获取管道文件
int fd = open(ipcPath.c_str(), O_WRONLY);
if(fd < 0)
{
perror("open");
exit(1);
}
// 2. ipc过程
string buffer;
while(true)
{
cout << "Please Enter Message Line :> ";
std::getline(std::cin, buffer);
write(fd, buffer.c_str(), buffer.size());
}
// 3. 关闭
close(fd);
return 0;
}
对于如何让客户端和服务端使用同一个命名管道文件,这里我们可以让客户端和服务端包含同一个头文件,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。
共用头文件的代码如下:
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
using namespace std;
#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc";
我们再定义了一个简单的日志系统,其中包括不同级别的日志输出,如Debug、Notice、Warning和Error ,具体实现如下:
#pragma once
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
最后我们先将服务器程序运行起来,再运行客服端程序,看一下最后的效果:
?
最后我们可以看到客服端输入的文字在服务器上都成功显示,并且当客服端退出时,服务器也完成了关闭和删除管道文件的工作.?
前面我们提到过进程间通信为了实现资源共享,有共享内存与共享文件两种常见的解决思路,其中我们上面讲到的匿名管道和命名管道都是基于共享文件而得到的解决方案,而这里我们将要谈到的System V共享内存则顾名思义是基于共享内存的一种解决方案.
System V共享内存是一种IPC机制,属于System V IPC的一部分。它允许不同的进程共享同一块物理内存区域,以实现数据的高效传递。相较于其他IPC方式,System V共享内存提供了直接的内存访问,因此在性能上具有优势。
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存
在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。
共享内存的内核数据结构如下:位于/usr/include/linux/shm.h
struct shmid_ds {
struct ipc_perm shm_perm; /* 操作权限 */
int shm_segsz; /* 段的大小(字节) */
__kernel_time_t shm_atime; /* 上次附加时间 */
__kernel_time_t shm_dtime; /* 上次分离时间 */
__kernel_time_t shm_ctime; /* 上次更改时间 */
__kernel_ipc_pid_t shm_cpid; /* 创建者的进程ID */
__kernel_ipc_pid_t shm_lpid; /* 上一次操作的进程ID */
unsigned short shm_nattch; /* 当前的附加次数 */
unsigned short shm_unused; /* 兼容性 */
void *shm_unused2; /* 同上 - DIPC使用 */
void *shm_unused3; /* 未使用 */
};
当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。
可以看到上面共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:位于/usr/include/linux/ipc.h
struct ipc_perm {
__kernel_key_t key; /* 键值 */
__kernel_uid_t uid; /* 所有者的用户ID */
__kernel_gid_t gid; /* 所有者的组ID */
__kernel_uid_t cuid; /* 创建者的用户ID */
__kernel_gid_t cgid; /* 创建者的组ID */
__kernel_mode_t mode; /* 权限掩码 */
unsigned short seq; /* 序列号 */
};
使用 shmget 函数创建或获取一个已存在的共享内存区域。需要指定内存大小和一些标志位。
int shmget(key_t key, size_t size, int shmflg)
参数:?
返回值:?
注意:
对于shmflg,在这里只说两个IPC_CREAT 和 IPC_EXCL
而key相当于唯一标识符ID,需要用户自己填入。理论来讲,用户可以随便填什么值,具体是几并不重要,重要的是它和其他key不一样。但难免会填写的值与其他的key冲突,所以我们一般使用ftok()函数获取key
ftok 是一个函数,通常用于生成一个用于标识 System V IPC 对象的键值(key)。它的原型如下:
key_t ftok(const char *pathname, int proj_id)
参数:
pathname
:一个指向存在的文件的路径名的指针,可以是任何有效的路径。proj_id
:一个用户定义的整数,用于在特定路径下创建唯一的 IPC 键。返回值:
errno
来指示错误原因。这里我们演示一下内存申请
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0X777
#define SIZE 4096
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL);
if(shmid < 0)
{
perror("shmget");
return 2;
}
printf("key: %u, shmid: %d\n", key, shmid);
return 0;
}
运行程序:?
第一次执行之后,成功打印了key和shimd,我们发现shimd默认是从0开始的
后面程序执行为什么会打印"shmget:file exists"呢?
此时说明共享内存已经被创建出来了。
通过指令ipcs -m可以查看被创建出来的共享内存
单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
ipcs命令输出的每列信息的含义如下:
标题 | 含义 |
---|---|
key | 系统区别各个共享内存的唯一标识 |
shmid | 共享内存的用户层id(句柄) |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
使用 shmat 函数将共享内存映射到进程的地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg)
参数:?
返回值:
下面是在加上shmat后的代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0X777
#define SIZE 4096
int main() {
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0) {
perror("ftok");
return 1;
}
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);
if (shmid < 0) {
perror("shmget");
return 2;
}
printf("key: %u, shmid: %d\n", key, shmid);
// Attach the shared memory segment
void *shm_addr = shmat(shmid, NULL, 0);
if (shm_addr == (void *)-1) {
perror("shmat");
return 3;
}
// Use the shared memory...
// Detach the shared memory segment
if (shmdt(shm_addr) == -1) {
perror("shmdt");
return 4;
}
return 0;
}
运行后:
原来:?
代码运行后发现关联失败,主要原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,因此进程没有权限关联该共享内存。我们应该在使用shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666)
使用 shmdt 函数解除共享内存与进程地址空间的映射
int shmdt(const void *shmaddr)
参数:
返回值:
通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。
使用ipcrm -m shmid命令释放指定id的共享内存资源:
使用 shmctl?函数删除共享内存:
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数:?
返回值:
代码和命名管道实现server&client类似
实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个内存空间,之后服务端就可以从该内存空间当中读取客户端发来的通信信息了
#include "comm.hpp"
Init init;
string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer, sizeof buffer, "0x%x", k);
return buffer;
}
int main()
{
// 1. 创建公共的Key值
key_t k = ftok(PATH_NAME, PROJ_ID);
assert(k != -1);
Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;
// 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); //
if (shmid == -1)
{
perror("shmget");
exit(1);
}
Log("create shm done", Debug) << " shmid : " << shmid << endl;
// sleep(10);
// 3. 将指定的共享内存,挂接到自己的地址空间
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
Log("attach shm done", Debug) << " shmid : " << shmid << endl;
// sleep(10);
int fd = OpenFIFO(FIFO_NAME, READ);
for (;;)
{
Wait(fd);
// 临界区
printf("%s\n", shmaddr);
if (strcmp(shmaddr, "quit") == 0)
break;
// sleep(1);
}
// 4. 将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmaddr);
assert(n != -1);
(void)n;
Log("detach shm done", Debug) << " shmid : " << shmid << endl;
// sleep(10);
// 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm done", Debug) << " shmid : " << shmid << endl;
CloseFifo(fd);
return 0;
}
客户端的代码如下:
#include "comm.hpp"
int main()
{
Log("child pid is : ", Debug) << getpid() << endl;
key_t k = ftok(PATH_NAME, PROJ_ID);
if (k < 0)
{
Log("create key failed", Error) << " client key : " << k << endl;
exit(1);
}
Log("create key done", Debug) << " client key : " << k << endl;
int shmid = shmget(k, SHM_SIZE, 0);
if (shmid < 0)
{
Log("create shm failed", Error) << " client key : " << k << endl;
exit(2);
}
Log("create shm success", Error) << " client key : " << k << endl;
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if (shmaddr == nullptr)
{
Log("attach shm failed", Error) << " client key : " << k << endl;
exit(3);
}
Log("attach shm success", Error) << " client key : " << k << endl;
int fd = OpenFIFO(FIFO_NAME, WRITE);
while (true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
if (s > 0)
{
shmaddr[s - 1] = 0;
Signal(fd);
if (strcmp(shmaddr, "quit") == 0)
break;
}
}
CloseFifo(fd);
int n = shmdt(shmaddr);
assert(n != -1);
Log("detach shm success", Error) << " client key : " << k << endl;
return 0;
}
?对于如何让客户端和服务端使用同内存空间,这里我们可以让客户端和服务端包含同一个头文件,该头文件当中提供这个共用PROJ_ID和PATH_NAME,这样客户端和服务端通过ftok才会生成同样的值,也就能实现看到同一处内存资源
共用头文件的代码如下:注意此处的PATH_NAME改成自己的地址
#pragma once
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
using namespace std;
#define PATH_NAME "/home/Chris" //改成自己的地址
#define PROJ_ID 0x66
#define SHM_SIZE 4096 // 共享内存的大小,最好是页(PAGE: 4096)的整数倍
#define FIFO_NAME "./fifo"
class Init
{
public:
Init()
{
umask(0);
int n = mkfifo(FIFO_NAME, 0666);
assert(n != 0);
(void)n;
Log("create fifo success", Notice) << "\n";
}
~Init()
{
unlink(FIFO_NAME);
Log("remove fifo success", Notice) << "\n";
}
};
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(std::string pathname, int flags)
{
int fd = open(pathname.c_str(), flags);
assert(fd >= 0);
return fd;
}
void Wait(int fd)
{
Log("等待中....", Notice) << "\n";
uint32_t temp = 0;
ssize_t s = read(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
}
void Signal(int fd)
{
uint32_t temp = 1;
ssize_t s = write(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
Log("唤醒中....", Notice) << "\n";
}
void CloseFifo(int fd)
{
close(fd);
}
我们再定义了一个简单的日志系统,其中包括不同级别的日志输出,如Debug、Notice、Warning和Error ,具体实现如下:
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
通信速度比较:
数据拷贝过程:
同步与互斥机制:
优点和缺点:
总体而言,共享内存在通信速度和拷贝次数上具有优势,但缺乏同步与互斥机制,而管道提供了这些机制但相对慢一些。选择使用哪种通信方式通常取决于具体的应用场景和需求。