【Linux系统】命名管道与共享内存

前言:

上文我们讲到了匿名管道【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();
}