【Linux】进程通信

1. 整体学习思维导图

2. 理解进程通信概念

2.1 为什么要通信

  • 数据传输:一个进程需要将他的数据发送给另一个进程

  • 资源共享:多个进程使用并且共享一份资源

  • 通知事件:一个进程需要向另一个进程发送消息,如子进程结束时会通知父进程

  • 进程控制:父进程需要控制子进程的过程,时刻了解子进程的状态变化,如我们平时调试Debug过程

2.2 通信的种类

  • 管道

    • 匿名管道

    • 命名管道

  • System V IPC

    • SystemV消息队列

    • SystemV共享内存

    • SystemV信号量

  • POSIXIPC

    • 消息队列

    • 共享内存

    • 信号量

    • 互斥量

    • 条件变量

    • 读写锁

2.3 通信发展史

  • 管道

  • SystemV进程间通信

  • POSIX进程间通信

3. 管道通信

3.1 匿名管道

管道通信的发展是基于原本先有的技术所创建的,主要利用的是缓冲区技术概念实现,当然真正的管道文件是不需要刷新到磁盘上的!管道主要用于进程与进程之间的读写方式。

如:父进程以写的形式打开一个pipe文件,子进程读取pipe文件的内容进行拷贝,这种 父写/子读 的形式叫做单向通信,我们实现这种方式只需要父子进程在使用pipe时关闭对应不需要的 fd 即可!

3.1.1 创建一个匿名管道

不要文件路径,内存级,没有文件名,是匿名管道!

cpp 复制代码
/* 创建一个匿名管道 */
#include<unistd.h>
int pipe(int fd[2]);
/* 参数 */
fd:文件描述符数组,fd[0]表示读端,fd[1]表示写段
返回值:成功返回0,失败返回错误代码error

问题: 如果是父子进程之间通过这个匿名管道交互,我们怎么保证打开的是同一个管道呢?-->子进程会继承父文件表

3.1.2 demo代码测试

使用pipe,父写/子读,关闭对应不需要的端口,形成通信信道进行测试。

  • snprintf函数
cpp 复制代码
int snprintf(char *str, size_t size, const char *format, ...);
/* 以固定的格式写入到字符串中 */
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>

// child
void ChildRead(int crd)
{
    char buffer[1024];
    while (true)
    {
        ssize_t n = read(crd, buffer, sizeof(buffer));
        /* 如果读到0表示写端关闭 */
        if (n < 0)
        {
            perror("read fail");
            exit(3);
        }
        std::cout << "Father Say:" << buffer << std::endl;
        sleep(1);
    }
}

// father
void FatherWrite(int fwd)
{
    char buffer[1024];
    int fid = getpid();
    int cnt = 0;
    while (true)
    {
        snprintf(buffer, sizeof(buffer), "我是一个父进程, 我的pid:%d, 我要给子进程发送任务:%d", fid, cnt++);
        write(fwd, buffer, sizeof(buffer));
        sleep(1);
    }
}

int Test_Pipe()
{
    // fd[0]读端 / fd[1]写端
    int fd[2];
    int ret = pipe(fd);
    if (ret < 0)
    {
        perror("pipe error");
        exit(1);
    }
    std::cout << "fd[0]:" << fd[0] << std::endl;
    std::cout << "fd[1]:" << fd[1] << std::endl;

    // 创建子程序(子读/父写)
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork fail!");
        exit(2);
    }
    else if (id == 0)
    {
        // child
        // 关闭写端
        close(fd[1]);
        ChildRead(fd[0]);
        close(fd[0]);
        exit(3);
    }
    else
    {
        // father
        // 关闭读端
        close(fd[0]);
        FatherWrite(fd[1]);
        /* 进程等待 */
        waitpid(id, nullptr, 0);
        close(fd[1]);
        return 0;
    }
}

int main()
{
    Test_Pipe();
    return 0;
}

3.1.3 探究4种通信情况

  • 写慢/读快

这种情况下,读端会阻塞等待!

cpp 复制代码
/* 核心代码 */
// 写慢/读快
while (true)
{
    ssize_t n = read(crd, buffer, sizeof(buffer));
    if (n < 0)
    {
        perror("read fail");
        exit(3);
    }
    std::cout << "Father Say:" << buffer << std::endl;
    std::cout << "当前时刻" << std::time(nullptr) << std::endl;
}
    
// 写慢/读快
while (true)
{
    snprintf(buffer, sizeof(buffer), "我是一个父进程, 我的pid:%d, 我要给子进程发送任务:%d", fid, cnt++);
    write(fwd, buffer, sizeof(buffer));
    sleep(5);
}
  • 写快/读慢
cpp 复制代码
/* 核心代码 */
// 写快/读慢
while (true)
{
    ssize_t n = read(crd, buffer, sizeof(buffer));
    if (n < 0)
    {
        perror("read fail");
        exit(3);
    }
    std::cout << "Father Say:" << buffer << std::endl;
    sleep(10);
}

// 写快/读慢
while (true)
{
    snprintf(buffer, sizeof(buffer), "我是一个父进程, 我的pid:%d, 我要给子进程发送任务:%d", fid, cnt++);
    write(fwd, buffer, sizeof(buffer));
}
  • 写关/继续读

read->0, 表示文件结尾

  • 继续写/读关

写端在写入,OS不会做无意义的事情,OS->kill 写端进程发送异常信号[13] SIGPIPE

3.1.4 5种特性

  1. 匿名管道只能应用于有血缘关系的(父与子)

  2. 管道文件,自带同步机制

    1. 写的慢,读的快

    2. 写的快,读的慢

  3. 管道是面向字节流的

  4. 管道是单向通信的(属于半双工)

    1. 任何一个时刻,一发一收,半双工

    2. 任何一个时刻,可以同时收发,全双工

  5. 文件的生命周期是随进程的

3.1.5 匿名管道实现进程池

系统问题处理:
  • 我们进程池是基于父进程创建子进程和信道进行实现的,那么我们必然要对进程和信道占用的文件描述符进行等待和关闭,由于我们的子进程是按顺序申请建立信道,关闭对应的读写端口,我们又再次创建子进程,子进程是对父进程的一种拷贝,这就会带来一个问题:子进程将父进程的上次指向他哥哥进程的写端也拷贝了,但是没有关闭!

具体情况看图了解:

  • 处理方案

    • 方案一:关闭端口和等待子进程分开执行,先关闭写入端口防止子进程写入端口阻塞导致子进程不可以退出,关闭端口后对子进程进行等待回收!
  • 方案二:我们先关闭最后申请链接过多写入端口的子进程,使得信道的引用计数--,这种倒序关闭也可以解决问题!
  • 方案三:我们在申请一个子进程时,就去关闭他不必要的写入端所有窗口,这种方案保证每个子进程和父进程和我们预期设计的进程池一样一对多关系!

  • 我们在申请子进程去关闭写端会不会影响父进程正在进行的Insert新的信道?不会影响父进程,修改内容会发生写时拷贝!

Signal.hpp:

cpp 复制代码
#pragma once
#include <vector>
#include <functional>
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <ctime>

// 包装器 + lambda函数 设计Signal选项
std::function<void(void)> PrintLog = [](){ std::cout << "这是一个打印日志的任务" << std::endl; }; /* 打印日志 */
std::function<void(void)> DownLoad = [](){ std::cout << "这是一个下载的任务" << std::endl; }; /* 打印日志 */
std::function<void(void)> UpLoad = [](){ std::cout << "这是一个上传的任务" << std::endl; }; /* 打印日志 */


class SignalManage
{
    typedef std::function<void(void)> SL;
public:
    SignalManage()
    {
        /* 获取一个随机的种子 */
        srand(time(nullptr));
    }

    int SelectCode()
    {
        return rand() % _signal.size();
    }

    /* 选取并且执行一个任务 */
    void Execute(int code)
    {
        if(code >= 0 && code < _signal.size())
            _signal[code]();
    }

    void InsertToSl(SL sl)
    {
        _signal.emplace_back(sl);
    }


    ~SignalManage()
    {}
private:
    std::vector<SL> _signal;    /* 用于管理Signal */
};

ProcessPool.hpp:

cpp 复制代码
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__
#include "Signal.hpp"
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#define DEFAULTNUM 5

class Channel
{
public:
    Channel(int wfd, pid_t subid)
        : _wfd(wfd), _subid(subid)
    {
        /* 设置name */
        _name = "Channel-写入端口" + std::to_string(_wfd) + "控制的子进程ID:" + std::to_string(_subid);
    }

    void Close()
    {
        close(_wfd);
    }

    void Wait()
    {
        waitpid(_subid, nullptr, 0);
    }

    void Send(int code)
    {
        write(_wfd, &code, sizeof(code));
    }

    /* 主要函数 */

    /* 获取Channel信息 */
    int GetWfd() { return _wfd; }
    pid_t GetSubid() { return _subid; }
    std::string GetName() { return _name; }

    ~Channel()
    {}

private:
    int _wfd; /* 写端端口 */
    pid_t _subid;
    std::string _name;
};

class ChannelManage
{
public:
    ChannelManage()
        : _next(0)
    {}

    void InsertToCm(int wfd, pid_t subid)
    {
        _c.emplace_back(wfd, subid);
    }

    Channel &SelectChannel()
    {
        /* 轮询筛选 */
        auto &c = _c[_next];
        _next++;
        _next %= _c.size();
        return c;
    }

    /* 方案三: 关闭所有的写入信道 */
    void CloseAllWrite()
    {
        for (auto &c : _c)
        {
            c.Close();
            std::cout << "关闭写端" << c.GetName() << std::endl;
        }
    }

    /* 方案一: 分别处理写入端口和等待子进程 */
    void CloseWrite()
    {
        for (auto &c : _c)
        {
            c.Close();
            std::cout << "关闭写端" << c.GetName() << std::endl;   
        }
    }

    void CloseWait()
    {
        for (auto &c : _c)
        {
            c.Wait();
            std::cout << "回收进程" << c.GetName() << std::endl;
        }
    }

    /* 方案三 */
    void CloseWriteAndChild()
    {
        for (auto &c : _c)
        {
            c.Close();
            std::cout << "关闭写端" << c.GetName() << std::endl;
            c.Wait();
            std::cout << "回收进程" << c.GetName() << std::endl;
        }
    }


    /* 解决方法二 倒序关闭 */
    void R_CloseWriteAndChild()
    {
        for(int i = _c.size() - 1; i >= 0; --i)
        {
            _c[i].Close();
            std::cout << "关闭写端:" << _c[i].GetWfd() << std::endl;
            _c[i].Wait();
            std::cout << "回收进程:" << _c[i].GetSubid() << std::endl;
            std::cout << std::endl;
        }
    }

    void PrintName()
    {
        for (auto &c : _c)
            std::cout << c.GetName() << std::endl;
    }

    ~ChannelManage()
    {}

private:
    std::vector<Channel> _c;
    int _next; /* 用于轮询 */
};

class ProcessPool
{
public:
    ProcessPool()
        :_process_num(DEFAULTNUM)
    {
        _sm.InsertToSl(PrintLog);
        _sm.InsertToSl(DownLoad);
        _sm.InsertToSl(UpLoad);
    }

    void Work(int rfd)
    {
        while(true)
        {
            int code = 0;
            ssize_t n = read(rfd, &code, sizeof(code));
            if(n > 0)
            {
                if(n != sizeof(code))
                    continue;
                std::cout << "子进程:[PID->" << getpid() << "收到一个任务]" << std::endl;
                _sm.Execute(code); /* 执行任务 */
            }
            else if(n == 0) // read->end
            {
                std::cout << "子进程退出" << std::endl;
                break;
            }
            else
            {
                std::cout << "读取错误" << std::endl;
                break;
            }
        }
    }

    bool Start()
    {
        for (int i = 0; i < _process_num; ++i)
        {

            /* 1. 申请匿名通道 */
            int pipefd[2] = {0};
            int n = pipe(pipefd); /* pipefd[0]->读端 pipefd[1]->写端 */
            if (n < 0)
                return false;

            /* 2. 创建子程序 */
            pid_t subid = fork();
            if (subid < 0)
                return false;
            else if (subid == 0)
            {
                // child
                /* 方案三 */
                _cm.CloseAllWrite();
                /* 关闭写端 */
                close(pipefd[1]);
                /* 执行 */
                Work(pipefd[0]);
                /* 关闭读端 */
                close(pipefd[0]);
                exit(0);
            }
            else
            {
                // father
                /* 关闭读端 */
                close(pipefd[0]);
                _cm.InsertToCm(pipefd[1], subid);
            }
        }
        return true;
    }

    void Run()
    {
        /* 获取一个任务 */
        int taskcode = _sm.SelectCode();
        /* 选择一个信道 */
        auto& c = _cm.SelectChannel();
        std::cout << "选择了一个信道" << c.GetName() << std::endl;

        c.Send(taskcode);
        std::cout << "发送了一个信号码" << taskcode << std::endl;

    }

    void Debug()
    {
        _cm.PrintName();
    }

    void Stop()
    {
        /* 方案一 */
        /* 先关端口,再回收子进程 */
        // _cm.CloseWrite();
        // _cm.CloseWait();

        /* 方案二: 倒着关闭进程 */
        // _cm.R_CloseWriteAndChild();

        /* 方案三: 随意关闭*/

    }

    ~ProcessPool()
    {
    }

private:
    SignalManage _sm;
    ChannelManage _cm;
    int _process_num; /* 用于表示创建子进程的数量 */
};

#endif

main.cc:

cpp 复制代码
#include "ProcessPool.hpp"

int main()
{
    ProcessPool pp;
    pp.Start();
    pp.Debug();
    int cnt = 5;
    while(cnt--)
    {
        pp.Run();
        sleep(1);
        std::cout << std::endl;
    }
    pp.Stop();

    return 0;
}

3.2 命名管道

3.2.1 什么是命名管道

  • 前面我们了解了匿名管道。匿名管道是内存级的,因此没有名称,只能通过文件描述符进行访问,匿名管道只能解决有血缘关系进程之间的通信(如父子进程)。

  • 命名管道,显而易见是具有名称,也就是说他不是内存级,命名管道主要用于解决的就是两个毫不相干的进程之间通信的问题,并且存在血缘关系的进程也可以使用命名管道。

3.2.1 命名管道如何解决通信问题的

我们知道要想两个进程之间构成通信,必须让两个进程看到同一份资源。那么现在我们有着A/B两个进程,他们同时打开一份文件/path/file.txt不就可以看到同一份资源了吗,由于通过打开同一路径下的同一份文件(文件有路径,名字,路径存在唯一性,那么不就可以叫做命名管道了嘛)。

问题:

  • 我们知道在Linux打开一份文件,更改文件内容会进行刷新缓冲区到磁盘上,但我们通信是不需要刷新到磁盘上,怎么解决?

    • 创建一个特殊的文件类型FIFO,这种文件类型只能被打开,不需要刷新即可!
  • 那么当两个进程同时打开一份文件OS会将文件在内存加载两次吗?

    • 当然不会,FIFO文件的数据始终驻留内存无需加载,通过内核缓冲区直接传递,无需磁盘 IO。

    • 对于普通文件需要加载到内存,数据最终会持久化到磁盘,通过内存映射(mmap)实现共享。

3.2.2 创建一个命名管道文件

  • 指令
bash 复制代码
mkfifo filename
unlink filename
  • 函数接口
cpp 复制代码
int mkfifo(const char* filename, mode_t mode);
int unlink(const char* filename);
  • Filename: 名称

  • Mode: 权限, 最终权限 = 起始权限 & (~umask)

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
using namespace std;

int main()
{
    /* 创建一个命名管道 */
    int n = mkfifo("myfifo", 666);
    if(n < 0)
    {
        perror("mkfifo fail!");
        exit(EXIT_FAILURE);
    }
    return 0;
}

粘滞位常规标识为小写 ``t``(此时执行权限 ``x`` 存在),若执行权限未开启但仍设置了粘滞位,则显示为大写 ``T

3.2.3 设计不同进程服务端/客服端的通信

comm.hpp:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <unistd.h>
#include <fcntl.h>

#define PATH "./"
#define FILENAME "myfifo"

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

/* 创建pipe(FIFO文件) */
class CreateFIFO
{
public:
    void OpenForRead()
    {
        _fd = open(_pathname.c_str(), O_RDONLY);
        if (_fd < 0)
            ERR_EXIT("open_for_read fail");
        else
            std::cout << "open_for_read success!" << std::endl;
    }

    void OpenForWrite()
    {
        _fd = open(_pathname.c_str(), O_WRONLY);
        if (_fd < 0)
            ERR_EXIT("open_for_write fail");
        else
            std::cout << "open_for_write success!" << std::endl;
    }

    void Write()
    {
        int cnt = 1;
        pid_t id = getpid();
        while (true)
        {
            std::cout << "Please Enter#" << std::endl;
            std::getline(std::cin, _message);
            _message += (", message number: " + std::to_string(cnt++) + ", [" + std::to_string(id) + "]");
            write(_fd, _message.c_str(), _message.size());
        }
    }

    void Read()
    {
        while (true)
        {
            char buffer[1024];
            int n = read(_fd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                std::cout << "Client Say# " << buffer << std::endl;
            }
            else if (n == 0)
            {
                std::cout << "Client quit! me too!" << std::endl;
                break;
            }
            else
            {
                ERR_EXIT("read fail");
                break;
            }
        }
    }

    CreateFIFO(const std::string &path, const std::string &name, const mode_t &mode = 0666)
        : _mode(mode), _fd(-1)
    {
        _pathname = PATH + name;
    }

    void Create()
    {
        /* 创建FIFO文件 */
        umask(0);
        int n = mkfifo(_pathname.c_str(), _mode);
        if (n < 0)
            ERR_EXIT("mkfifo fail");
        else
            std::cout << "mkfifo success!" << std::endl;
    }

    void Close()
    {
        if (_fd > 0)
        {
            int m = close(_fd);
            if (m < 0)
                ERR_EXIT("Close fail");
            else
                std::cout << "Close success!" << std::endl;
        }
    }

    ~CreateFIFO()
    {
        int n = unlink(_pathname.c_str());
        if (n < 0)
            ERR_EXIT("unlink fail");
        else
            std::cout << "unlink success!" << std::endl;
    }

private:
    std::string _pathname;
    mode_t _mode;
    int _fd;
    std::string _message;
};

client.cc:

cpp 复制代码
#include "comm.hpp"

int main()
{
    CreateFIFO myfifo(PATH, FILENAME);
    myfifo.OpenForRead();
    myfifo.Read();
    myfifo.Close();
    return 0;
}

sever.cc:

cpp 复制代码
#include "comm.hpp"

int main()
{
    CreateFIFO myfifo(PATH, FILENAME);
    myfifo.Create();
    myfifo.OpenForWrite();
    myfifo.Write();
    myfifo.Close();
    return 0;
}

3.3 匿名管道/命名管道的区别

  • 匿名管道调用pipe函数创建并且打开,适用于有血缘关系的进程(父子进程)。

  • 命名管道调用mkfifo函数创建,使用open系统调用打开,适用于任意两个进程,无论是否存在关系。

  • 在4种通信情况和5种特性方面一致

4. System V 共享内存

4.1 什么是System V

  • System V 是一种标准,Linux系统内核支持了这个标准,并且基于这个标准实现了IPC(进程间通信)通信模块!

  • IPC 本质:让不同进程,看到同一份资源!

4.2 共享内存的原理

图解:

  • 共享内存的原理有点类似于动态库->映射方案,其目的就是为了让内存的数据块和进程之间产生联系

  • 一旦涉及对物理内存的使用离不开OS操作->系统调用

  • 取消关联关系后,OS需要释放内存

  • 可能同时存在多组进程进行共享内存的通信->导致物理内存中存在多块共享内存->需要对这些共享内存块进行管理->先描述,再组织->共享内存一个要有对应的描述共享内存内核结构体对象 + 物理内存的分配!

  • 进程和共享内存之间的关系 -> 内核数据结果之间的关系!

  • 共享内存的数据结构:

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 byDIPC */ 
    void shm_unused3; /* unused */
};  

4.3 使用共享内存接口,了解System V通信特性

  • shmget函数
cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
/* 用于创建一个共享内存 */
int shmget(key_t key, size_t size, int shmflg);
/* 
1. key: 这个共享内存段名字
2. size: 共享内存大小
3. shmflg: 由九个权限标志构成,它们的用法和创建⽂件时使用的mode模式标志是⼀样的
    取值为IPC_CREAT:共享内存,不存在,创建并返回;共享内存已存在,获取并返回。
    取值为IPC_CREAT | IPC_EXCL:共享内存,不存在,创建并返回;共享内存已存在,出错返回。
4. 返回值:成功返回⼀个非负整数,即该共享内存段的标识码;失败返回-1 
*/
  • ftok函数
cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
/* 用于生成一个key_t值 */
key_t ftok(const char *pathname, int proj_id);
/*
1. pathname: 路径名称,自取名称也可以
2. proj_id: 自己提供一个特殊的proj_id
3. 返回值:成功返回一个key,失败返回-1
*/
  • 创建一块共享内存
cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>

#define PATH "./"
#define PROJID 0x66
#define ERR_EXIT(m)     \
do                      \
{                       \
    perror(m);          \
    exit(EXIT_FAILURE); \
} while (0);

int main()
{
    key_t key = ftok(PATH, PROJID);
    int n = shmget(key, 4096, IPC_CREAT);
    if(n < 0)
    {
        ERR_EXIT("shmget fail");
    }
    return 0;
}
bash 复制代码
/* ipcs -m 查看共享内存 */
bash 复制代码
/* ipcrm -m [shmid] 删除共享内存 */ 
  • shmat函数
cpp 复制代码
/* 将共享内存段连接到进程地址空间 */
void* shmat(int shmid, const void* shmadder, int shmflg);
/*
shmid: 共享内存标识符
shmaddr:指定连接的地址,传nullptr表示由内核自动选择合适且未使用的地址映射共享内存段
shmflg:取值选项,可能是SHM_RND和SHM_RDONLY,传入0表示读写权限
返回值:成功返回一个指针,指向共享内存的一个节segment(虚拟地址),失败返回-1
*/
bash 复制代码
shmaddr为NULL,核心自动选择⼀个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。
公式:shmaddr - (shmaddr % SHMLBA)
shmflg = SHM_RDONLY,表示连接操作用来只读共享内存  
  • shmdt函数
cpp 复制代码
/* 将共享内存与当前进程脱离 */
int shmdt(const void* shmaddr);
/*
shmaddr:右shmat所返回的指针
返回值:成功返回0,失败返回-1
切断进程与共享内存直接的联系 != 删除共享内存段
*/
  • shmctl函数
cpp 复制代码
/* 控制共享内存 */
int shmctl(int shmid, int cmd, strucr shmid_ds* buf);
/*
shmid: 共享内存标识符
cmd:采取的指令操作选项
buf:指向一个保存着共享内存模式状态和访问权限的数据结构
返回值:成功返回0,失败返回-1
*/

底层结构体:

Shm.hpp:

cpp 复制代码
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string>
#include <unistd.h>
#include <cstring>
using namespace std;

#define SIZE 4096
#define PROJID 0x66
#define MODE 0666
#define CREATER "creater"
#define USER "user"
#define PATH "./"
#define ERR_EXIT(m)     \
do                      \
{                       \
    perror(m);          \
    exit(EXIT_FAILURE); \
} while (0);

class Shm
{
private:
    void ShmHeaper(int shmflg)
    {
        _shmid = shmget(_key, _size, shmflg);
        if(_shmid < 0)
            ERR_EXIT("shmget fail!");
    }

    void Create()
    {
        ShmHeaper(IPC_CREAT | IPC_EXCL | _mode);
    }

    void Get()
    {
        ShmHeaper(0);
    }

    void Connect()
    {
        _start_vadder = shmat(_shmid, nullptr, 0); /* nullptr表示让内核去寻找和连接的共享内存, 0表示读写权限全开 */
        if(_start_vadder == (void*) -1)
            ERR_EXIT("shmat fail!");
    }

    void Disconnect()
    {
        int n = shmdt(_start_vadder);
        if(n < 0)
            ERR_EXIT("shmdt fail!");
    }

    void Destroy()
    {
        int n = shmctl(_shmid, IPC_RMID, nullptr);
        if(n < 0)
            ERR_EXIT("shmctl fail!");
    }

public:
    Shm(const char* pathname, const char* usertype, int projid = PROJID, int size = SIZE, mode_t mode = MODE)
        :_pathname(pathname)
        ,_usertype(usertype)
        ,_size(size)
        ,_projid(projid)
        ,_mode(mode)
        ,_shmid(-1)
        ,_start_vadder(nullptr)
    {
        _key = ftok(_pathname.c_str(), _projid);
        if(_key < 0)
            ERR_EXIT("ftok fail!");
        if(_usertype == CREATER)
            Create();
        else if(_usertype == USER)
            Get();
        else
        {
            ERR_EXIT("usertype fail!");
        }
        Connect();
    }

    /* 禁用拷贝构造和赋值重载 */
    Shm(const Shm&) = delete;
    Shm& operator=(const Shm&) = delete;

    /* 获取共享内存虚拟地址以供使用 */
    void* GetShmadder()
    {
        return _start_vadder;
    }

    ~Shm()
    {
        /* creater */
        if(_usertype == CREATER)
            Destroy();
        /* creater / user */
        Disconnect();
    }

private:
    std::string _pathname; /* 路径名称->用于创建_key */
    std::string _usertype; /* 用户种类: 1.->创建者(创建回收共享内存) 2.->使用者(使用共享内存) */
    size_t _size; /* 共享内存的大小 */
    int _projid; /* 创建_key使用的数字,用于增加随机性 */
    mode_t _mode; /* 权限位->用于shmget时创建传入的权限 */
    int _shmid; /* 共享内存的标识符 */
    key_t _key;   /* _key-> 用于创建一个shmget, 使两个进程找到同一共享内存的标识 */
    void* _start_vadder; /* 虚拟地址->连接共享内存返回的虚拟地址,指向共享内存的一个段 */
};

client.cc

cpp 复制代码
#include "Shm.hpp"
int main() 
{
    // 使用者身份,连接共享内存
    Shm client_shm(PATH, USER);
    char* shm_buf = (char*)client_shm.GetShmadder();
    
    string input;
    while (true) 
    {
        cout << "client# ";
        getline(cin, input);
        // 将用户输入写入共享内存
        strcpy(shm_buf, input.c_str());
    }
    return 0;
}

sever.cc

cpp 复制代码
#include "Shm.hpp"

int main() 
{
    // 创建者身份,初始化共享内存
    Shm server_shm(PATH, CREATER);
    char* shm_buf = (char*)server_shm.GetShmadder();
    
    while (true) 
    {
        // 输出共享内存内容(模拟接收客户端输入)
        cout << shm_buf << endl;
        sleep(1); // 简单延时,避免忙等待
    }
    return 0;
}
  • 读写共享内存,没有系统调用,直接使用的是共享内存的虚拟地址通信。

  • 共享内存是进程间通信最快的

    • 一旦映射之后,读写都可以被对方看见

    • 不需要进行系统调用读取或者写入

  • 通信双方没有"同步机制",数据可能对方没写完就被读取,没有保护机制,会导致数据不一致!

5. System V 消息队列

我们知道IPC本质就是让不同进程看到同一份资源,前面我们可以使用共享内存实现,而消息队列正如其名,维护一个队列用于写入和读取消息!

  • 我们从图中可以发现,进程A和进程B都向消息队列放入了自己的消息,那么进程有怎么知道自己该拿那些消息呢?

    • 结论一:消息队列提供了一种,一个进程给另一个进程发送有类型数据块(用于区分A,B的消息)的方式。
cpp 复制代码
struct node
{
    int type;
    char buffer[SIZE];
    struct node* next;
    // ....
}
  • 我们可能出现多组进程使用消息队列的情况,那么消息队列是否需要管理起来?

    • 结论二:OS操作系统中一定有先描述,再组织消息队列的数据结构进行管理!
  • 这么多的消息队列,我们两个进程进行通信,怎么知道是否使用的是同一个消息队列?

    • 结论三:消息队列使用了共享内存同样的key设计进行区分消息队列的使用!

消息队列的生命周期和共享内存一致,都随内核!

cpp 复制代码
ipcs /* 查看共享内存,消息队列,信号量 资源 */
-m /* 共享内存 */
-q /* 消息队列 */
-s /* 信号量 */

5.1 消息队列的函数接口调用

  • 创建
cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
/* key -> ftok */
/* msgflg -> O_CREAT | O_EXCL */ 
  • 控制
cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
/* msqid -> 消息队列标识符 */
struct msqid_ds {
               struct ipc_perm msg_perm;     /* Ownership and permissions */
               time_t          msg_stime;    /* Time of last msgsnd(2) */
               time_t          msg_rtime;    /* Time of last msgrcv(2) */
               time_t          msg_ctime;    /* Time of last change */
               unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
               msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
               msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
               pid_t           msg_lspid;    /* PID of last msgsnd(2) */
               pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};
/* cmd -> IPC_RMID | IPC_SET | ... */
  • 发数据
cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
/
struct msgbuf {
               long mtype;       /* message type, must be > 0 */
               char mtext[1];    /* message data */
           };

6. System V 信号量

在谈信号量之前我们先引入一个话题:共享内存-->看到同一份资源(提供通信的前提),但是却又有两个缺点:

  1. 没有"保护机制",数据会不一致!

  2. 各自代码,访问了这个没有保护的公共资源!

6.1 保护机制

由上面的话题我们引入了保护机制,在了解保护机制前我们需要提前了解几个概念:

  • 多个进程能看到的同一份公共资源:共享资源

  • 被保护的起来的共享资源叫做 临界资源

  • 在进程中涉及到互斥资源的程序段叫做临界区(访问资源对应的代码)你写的代码=访问临界资源的代码(临界区)+不访问临界资源的代码(非临界区)

  • 保护机制有两种形态:互斥和同步

  1. 什么是互斥?

任何时刻,只允许一个进程访问资源,就叫做互斥!比如我们去ATM机取钱在外面会有一个防盗门,一次只允许一个人进去,只有当上个人执行完操作(存钱/取钱),下一个人才可以进去访问这台ATM!

  1. 保护机制保护谁?

是保护也是约束,保护临界区代码就是变相保护临界资源,限制临界区代码对临界资源的使用。

  1. 什么是同步?

多个进程(执行流)访问临界资源时具有一定的顺序性,就叫做同步!(如进程A被安排写入操作,直到进程A通信完毕再让进程B进行访问读取)

  1. 什么是原子性?

一件事要么做,要么不做,没有中间态。(使用ATM只有两种结果:取钱/存钱,我们不关心中间是否输错了密码,在原子性看来只有两态!)

6.2 信号量

6.2.1 信号量是什么?

  • 别名信号灯,本质是一个计数器,用来表面临界资源中,资源的多少!

  • 比如我们去电影院看一场电影,这个电影院的座位就是临界资源,信号量表示的就是有多少座位数量!

  • 看电影我们需要去售票处买票吧,而买票本质就是对资源的预定机制,所以想要访问临界资源需要预定,一旦预定这个资源就被分配给你了,不管了这场电影来不来看,这个座位都要为你的访问使用准备着,其他人不可以访问,进程也是如此!所有的进程访问资源前,先申请信号量,这就是预定进制。

  • 信号量本身也是共享资源,所以信号量具有原子性!

    • p操作,sem--

    • v操作,sem++

    • 通过PV操作来完成对资源的预定机制

  • 如果信号量只有0/1两态,我们叫做二元信号量;信号量有多个(电影院座位)就是多元信号量,可以多线程并发访问。

7. 内核组织IPC资源

共享内存,消息队列,信号量-> key区分唯一! ->OS中,共享内存,消息队列,信号量,被当做了同一种资源!也就是他们为什么都属于System V。

相关推荐
小麦嵌入式10 分钟前
Linux驱动开发实战(十一):GPIO子系统深度解析与RGB LED驱动实践
linux·c语言·驱动开发·stm32·嵌入式硬件·物联网·ubuntu
刘若水12 分钟前
Linux: 进程信号初识
linux·运维·服务器
chem41111 小时前
Conmon lisp Demo
服务器·数据库·lisp
渗透测试老鸟-九青1 小时前
面试经验分享 | 成都渗透测试工程师二面面经分享
服务器·经验分享·安全·web安全·面试·职场和发展·区块链
阳小江1 小时前
Docker知识点
运维·docker·容器
m0_555762901 小时前
QT 动态布局实现(待完善)
服务器·数据库·qt
极客柒2 小时前
RustDesk 开源远程桌面软件 (支持多端) + 中继服务器伺服器搭建 ( docker版本 ) 安装教程
服务器·docker·开源
只是橘色仍温柔2 小时前
xshell可以ssh连接,但vscode不行
运维·vscode·ssh
IT里的交易员2 小时前
【系统】换硬盘不换系统,使用WIN PE Ghost镜像给电脑无损扩容换硬盘
运维·电脑
共享家95272 小时前
深入剖析Linux常用命令,助力高效操作
linux·运维·服务器