【Linux系统】命名管道与共享内存
前言:
上文我们讲到了匿名管道【Linux系统】匿名管道以及进程池的简单实现-CSDN博客
本文我们来讲一讲命名管道与共享内存
命名管道
上面我们讲到,匿名管道只能用于有血缘关系(尤其父子)的进程进行通信!但如果我们想让没有关系的进程进行通信,该怎么办呢?命名管道就是答案!
进程间通信的本质是让不同的进程看到同一份资源!命名管道也是一样!
1.命名管道原理
1.命名管道与匿名管道一样,本质上都是文件!
2.命名管道不同与匿名管道,命名管道是有名字、有路径的!
3.如下图,创建命名管道只会返回一个fd。并且当多个进程打开同一个文件时,系统并不会将其加载多次。
4.命名管道同匿名管道一样,其缓冲区不会刷新到磁盘中!
5.如何保证多个进程打开的是同一个命名管道?路径!路径是唯一的!
2.命名管道的特性
命名管道的特性与匿名管道基本一样!唯一区别就是:命名管道可用于不相关进程间的通信!
5种特性:
命名管道,可用于不相关的进程通信 |
命名管道文件,自带同步机制:包含5种通信情况! |
命名管道的面向字节流的 |
命名管道是单向通信的!(属于半双工的特殊情况。半双工:任何时候一个发,一个收。全双工:任何时候,可以同时收发) |
命名管道的生命周期是由管道文件是否被删除决定的! |
5种通信情况:
只要有一方没有打开管道文件,另一方就会阻塞在open处!直到都打开了管道文件,才会向下继续执行! |
写慢,读快:读端阻塞,等待写端 |
写快,读慢:管道缓冲区写满了,就要阻塞等待读端 |
写关闭,读继续:一直读取,知道读到完,返回0,表示读取到文件末尾 |
写继续,读关闭:无意义操作!OS会自动杀掉写端进程(通过信号:13 SIGPIPE杀掉) |
3.命名管道的接口
指令方面
//创建命名管道
mkfifo 管道名//删除命名管道
rm 管道名
unlink 管道名
yc@hyc-alicloud:~$ mkfifo t1hyc@hyc-alicloud:~$ ls -l
total 8
prw-rw-r-- 1 hyc hyc 0 Aug 22 12:01 t1hyc@hyc-alicloud:~$ unlink t1
hyc@hyc-alicloud:~$ ls -l
total 8
我们可以看到,创建的管道文件第一个字母为:p!这代表管道文件!
代码方面
创建命名管道:
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);pathname:FIFO 文件路径(如 /tmp/myfifo),进程通过该路径访问管道。
mode:文件权限(如 0666 表示读写权限,需结合进程的 umask 计算实际权限)。
返回值:成功返回 0,失败返回-1
打开命名管道:
#include <fcntl.h>
int open(const char *pathname, int flags);flags:打开模式,需指定 O_RDONLY(只读,读端)或 O_WRONLY(只写,写端)
返回值:成功返回fd,失败返回-1
删除命名管道:
#include <unistd.h>
int unlink(const char *pathname);FIFO文件被删除后,已打开的进程仍可继续使用对应资源,直到所有进程关闭文件描述符后,资源才彻底释放
4.利用命名管道实现通信
值得一提的,命名管道的同步机制是:只要有一方没有打开管道文件,另一方就会阻塞在open处!直到都打开了管道文件,才会向下继续执行!
//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获取共享内存:
#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);key:共享内存的唯一标识(通过 ftok 函数生成)
size:共享内存段的大小(字节),创建新段时必须指定,获取已有段时可设为 0shmflg:标志位(获取)IPC_CREAT:若不存在则创建新段,若存在则打开这个共享内存,并返回(创建)IPC_EXCL:与 IPC_CREAT 配合使用(单独使用没有意义),若指定要创建的共享内存已经存在则返回错误,否则创建(想要给出权限:如0666)成功:返回共享内存段标识符(shmid,非负整数);
失败:返回 -1,并设置 errno
#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 表示错误原因
让物理内存地址与虚拟地址进行映射:
void *shmat(int shmid, const void *shmaddr, int shmflg);shmid:shmget返回的共享内存 ID
shmaddr:指定映射到进程地址空间的起始地址。通常设为NULL,由内核自动分配
shmflg:映射选项,如SHM_RDONLY(只读映射,默认是读写)返回值:成功返回映射后的内存起始地址(void*),失败返回(void*)-1(设置errno)
解除映射关系:
int shmdt(const void *shmaddr);参数:shmaddr为shmat返回的共享内存起始地址
返回值:成功返回0,失败返回-1(设置errno)
删除or查询状态:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);shmid:共享内存 IDcmd:控制命令,常用:
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了!只要对ftok传入相同的函数,就可以得到相同的key,从而找到相同的内存段! |
共享内存的生命周期是随内核的!如果不显示的删除,那么就算进程退出了,共享内存仍然存在! |
共享内存大小:必须是4KB(4096)的整数! |
同步机制:
不同于管道,共享内存本身是没有同步机制的! |
共享内存属于用户空间,用户可以直接访问! 那其优点就是:速度快,映射之后可以直接看到资源,并可以直接读取!没有限制的 |
但缺点就是,没有同步机制,这会导致数据不被保护! 比如:写数据写到一半,就被读取走了!(管道调用系统调用,会被内核保护起来。而共享内存是没有内核保护的) |
5.利用共享内存实现进程间通信
//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();
}