前言:
上文我们讲到了匿名管道【Linux系统】匿名管道以及进程池的简单实现-CSDN博客
本文我们来讲一讲命名管道与共享内存
命名管道
上面我们讲到,匿名管道只能用于有血缘关系(尤其父子)的进程进行通信!但如果我们想让没有关系的进程进行通信,该怎么办呢?命名管道就是答案!
进程间通信的本质是让不同的进程看到同一份资源!命名管道也是一样!
1.命名管道原理
1.命名管道与匿名管道一样,本质上都是文件!
2.命名管道不同与匿名管道,命名管道是有名字、有路径的!
3.如下图,创建命名管道只会返回一个fd。并且当多个进程打开同一个文件时,系统并不会将其加载多次。
4.命名管道同匿名管道一样,其缓冲区不会刷新到磁盘中!
5.如何保证多个进程打开的是同一个命名管道?路径!路径是唯一的!

2.命名管道的特性
命名管道的特性与匿名管道基本一样!唯一区别就是:命名管道可用于不相关进程间的通信!
5种特性:
|-------------------------------------------------------------|
| 命名管道,可用于不相关的进程通信 |
| 命名管道文件,自带同步机制:包含5种通信情况! |
| 命名管道的面向字节流的 |
| 命名管道是单向通信的!(属于半双工的特殊情况。半双工:任何时候一个发,一个收。全双工:任何时候,可以同时收发) |
| 命名管道的生命周期是由管道文件是否被删除决定的! |
5种通信情况:
|------------------------------------------------------|
| 只要有一方没有打开管道文件,另一方就会阻塞在open处!直到都打开了管道文件,才会向下继续执行! |
| 写慢,读快:读端阻塞,等待写端 |
| 写快,读慢:管道缓冲区写满了,就要阻塞等待读端 |
| 写关闭,读继续:一直读取,知道读到完,返回0,表示读取到文件末尾 |
| 写继续,读关闭:无意义操作!OS会自动杀掉写端进程(通过信号:13 SIGPIPE杀掉) |
3.命名管道的接口
指令方面
bash
//创建命名管道
mkfifo 管道名
//删除命名管道
rm 管道名
unlink 管道名
bash
yc@hyc-alicloud:~$ mkfifo t1
hyc@hyc-alicloud:~$ ls -l
total 8
prw-rw-r-- 1 hyc hyc 0 Aug 22 12:01 t1
hyc@hyc-alicloud:~$ unlink t1
hyc@hyc-alicloud:~$ ls -l
total 8
我们可以看到,创建的管道文件第一个字母为:p!这代表管道文件!
代码方面
创建命名管道:
cpp
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname:FIFO 文件路径(如 /tmp/myfifo),进程通过该路径访问管道。
mode:文件权限(如 0666 表示读写权限,需结合进程的 umask 计算实际权限)。
返回值:成功返回 0,失败返回-1
打开命名管道:
cpp
#include <fcntl.h>
int open(const char *pathname, int flags);
flags:打开模式,需指定 O_RDONLY(只读,读端)或 O_WRONLY(只写,写端)
返回值:成功返回fd,失败返回-1
删除命名管道:
cpp
#include <unistd.h>
int unlink(const char *pathname);
FIFO文件被删除后,已打开的进程仍可继续使用对应资源,直到所有进程关闭文件描述符后,资源才彻底释放
4.利用命名管道实现通信
值得一提的,命名管道的同步机制是:只要有一方没有打开管道文件,另一方就会阻塞在open处!直到都打开了管道文件,才会向下继续执行!
cpp
//comm.hpp
#pragma once
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
using namespace std;
// 目标:实现client与service的通信
#define EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
class NameFifo
{
public:
NameFifo(string path, string name)
: _path(path), _name(name)
{
_fd = -1;
_PATH = _path + "/" + _name;
}
// 创建管道
void Create()
{
int n = mkfifo(_PATH.c_str(), 0666);
if (n < 0)
{
EXIT("mkfifo");
}
cout << "命名管道创建成功!\n";
}
// 打开管道
void OpenForRead()
{
_fd = open(_PATH.c_str(), O_RDONLY);
if (_fd < 0)
{
EXIT("open");
}
cout << "读端打开成功!\n";
}
void OpenForWrite()
{
_fd = open(_PATH.c_str(), O_WRONLY);
if (_fd < 0)
{
EXIT("open");
}
cout << "写端打开成功!\n";
}
// 读取数据
void Read()
{
char buffer[1024];
int n = read(_fd, buffer, sizeof(buffer) - 1);
buffer[n] = 0;
printf("接收到数据:%s\n", buffer);
}
// 写数据
void Write()
{
string msg;
cout << "请输入内容:\n";
cin >> msg;
write(_fd, msg.c_str(), msg.size());
}
// 关闭管道
void Close()
{
close(_fd);
unlink(_PATH.c_str());
cout << "管道:" << _fd << "关闭并删除!\n";
}
private:
string _path;
string _name;
string _PATH;
int _fd;
};
//service.cc
#include "comm.hpp"
int main()
{
NameFifo nf(".", "myfifo");
nf.Create();
nf.OpenForWrite();
nf.Write();
nf.Close();
}
//client.cc
#include "comm.hpp"
int main()
{
//service已经创建了管道,这里不用再创建了!
NameFifo nf(".", "myfifo");
nf.OpenForRead();
nf.Read();
nf.Close();
}

system V共享内存
system V共享内存也是进程间通信的一重要方式!
1.system V
system V是Linux系统中的一种标准。它规定了系统调用接口的设计,共享内存正是满足了这一标准。
2.共享内存的原理
如图,顾名思义共享内存就是将相同的内存空间,通过页表映射到不同的进程中去,达到不同进程访问同一个数据的效果(既IPC)
1.想要完成上面的操作系统通过操作系统提供的系统调用来实现!
2.取消内存与进程之间的映射关系,OS会自动的释放共享内存
3.一个操作系统必然存在多个共享内存供给多个进程使用,所以OS一定会去管理共享内存,至于如何管理,我们后面说!
3.共享内存接口
创建or获取共享内存:
cpp
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key:共享内存的唯一标识(通过 ftok 函数生成)
size:共享内存段的大小(字节),创建新段时必须指定,获取已有段时可设为 0
shmflg:标志位
(获取)IPC_CREAT:若不存在则创建新段,若存在则打开这个共享内存,并返回
(创建)IPC_EXCL:与 IPC_CREAT 配合使用(单独使用没有意义),若指定要创建的共享内存已经存在则返回错误,否则创建(想要给出权限:如0666)
成功:返回共享内存段标识符(shmid,非负整数);
失败:返回 -1,并设置 errno
cpp
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
pathname:指向一个已存在的文件路径的字符串,ftok 会使用该文件的 inode 编号 和 设备编号 作为生成键值
proj_id:一个 8 位的非 0 整数用于区分同一文件对应的不同 IPC 对象,范围为1~255
成功:返回一个 key_t 类型的键值(非负整数)
失败:返回 -1,并设置 errno 表示错误原因
让物理内存地址与虚拟地址进行映射:
cpp
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:shmget返回的共享内存 ID
shmaddr:指定映射到进程地址空间的起始地址。通常设为NULL,由内核自动分配
shmflg:映射选项,如SHM_RDONLY(只读映射,默认是读写)
返回值:成功返回映射后的内存起始地址(void*),失败返回(void*)-1(设置errno)
解除映射关系:
cpp
int shmdt(const void *shmaddr);
参数:shmaddr为shmat返回的共享内存起始地址
返回值:成功返回0,失败返回-1(设置errno)
删除or查询状态:
cpp
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存 ID
cmd:控制命令,常用:
IPC_RMID:标记共享内存段为待删除(所有进程分离后实际删除)
IPC_STAT:获取共享内存属性,存储到buf指向的struct shmid_ds结构中
IPC_SET:修改共享内存属性(需进程有足够权限)
buf:指向struct shmid_ds的指针(用于IPC_STAT/IPC_SET),IPC_RMID时可设为NULL
返回值:成功返回0,失败返回-1(设置errno)
4.共享内存特性
|-----------------------------------------------------------------------------------------------------------------------------------------|
| 在上面的接口中,我们会发现共享内存中存在两个标示符:唯一标识符key、内存段标识符shmid |
| 理解:
key****是用户层 "告诉内核要找哪个内存段" 的标识,shmid
是内核 "告诉用户层如何操作这个内存段" 的句柄。两者的交互是 "用户层用key换shmid,再用shmid操作内存段"。简而言之,查找时用key,操作时用shmid!!! |
| 如何保证不同的进程访问的是同一个内存呢?那当然是key了!只要对ftok传入相同的函数,就可以得到相同的key,从而找到相同的内存段! |
| 共享内存的生命周期是随内核的!如果不显示的删除,那么就算进程退出了,共享内存仍然存在! |
| 共享内存大小:必须是4KB(4096)的整数! |
同步机制:
|------------------------------------------------------------------------------|
| 不同于管道,共享内存本身是没有同步机制的! |
| 共享内存属于用户空间,用户可以直接访问! 那其优点就是:速度快,映射之后可以直接看到资源,并可以直接读取!没有限制的 |
| 但缺点就是,没有同步机制,这会导致数据不被保护! 比如:写数据写到一半,就被读取走了!(管道调用系统调用,会被内核保护起来。而共享内存是没有内核保护的) |
5.利用共享内存实现进程间通信
cpp
//Shm.hpp
#pragma once
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <iostream>
using namespace std;
// 目标:利用共享内存,实现service和client的通信
#define SIZE 4096
#define gmode 0666
#define EXIT(m) \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}
class Shm
{
public:
Shm(string &pathname, int &projid)
{
_key = ftok(pathname.c_str(), projid);
}
// 创建共享内存
void Creat()
{
umask(0);
_shmid = shmget(_key, SIZE, IPC_CREAT | IPC_EXCL | gmode);
if (_shmid < 0)
{
EXIT("shmget");
}
cout << "创建共享内存成功!\n";
}
// 获取共享内存
void Get()
{
_shmid = shmget(_key, SIZE, IPC_CREAT);
if (_shmid < 0)
{
EXIT("shmget");
}
cout << "获取共享内存成功!\n";
}
// 映射共享内存至虚拟空间
void Attach()
{
_start_mem = shmat(_shmid, NULL, 0);
if ((long long)_start_mem < 0)
{
EXIT("shmat");
}
cout << "映射成功!\n";
}
void Destroy()
{
UnAttach();
int n = shmctl(_shmid, IPC_RMID, NULL);
if (n < 0)
{
EXIT("shmctl");
}
cout << "删除共享内存成功!\n";
}
// 获取开始地址
void *Start()
{
return _start_mem;
}
private:
// 解除映射
void UnAttach()
{
int n = shmdt(_start_mem);
if (n < 0)
{
EXIT("shmdt");
}
cout << "解除映射成功!\n";
}
key_t _key;
int _shmid;
void *_start_mem;
};
//service.cc
#include "Shm.hpp"
#include <unistd.h>
int main()
{
string pathname = ".";
int projid = 0x66;
Shm shm(pathname, projid);
shm.Creat();
shm.Attach();
// 写入数据
char *arr = (char *)shm.Start();
for (int i = 'a'; i <= 'z'; i++)
{
arr[i - 'a'] = i;
sleep(1);
}
shm.Destroy();
}
//client.cc
#include "Shm.hpp"
#include <unistd.h>
int main()
{
string pathname = ".";
int projid = 0x66;
Shm shm(pathname, projid);
shm.Get();
shm.Attach();
// 读取数据
while (1)
{
printf("%s\n", (char *)shm.Start());
sleep(1);
}
shm.Destroy();
}
