Linux操作系统进程间通信

一、进程间通信的背景

进程是具有独立性的,即使是父子进程之间,父进程的数据子进程能够看得见,但这是在没有发生写时拷贝的前提下,一旦发生了写时拷贝,父子进程之间的数据是不能相互看见的,这是因为进程是具有独立性的。正是因为进程具有独立性,所以进程之间想要交互数据,成本是非常高的。所以我们要有支持进程间通信的方法。除此之外,如果我们想要多进程协同完成一件事情,也需要实现进程间通信。

进程间通信的方式一般有管道通信、System V标准的进程间通信、POSIX标准的进程间通信。进程之间实现通信的的前提是我们要让不同的进程看到同一份资源。这个同一份资源可以是文件、内存块等等,只有不同的进程看到了同一份资源,才可以实现一个进程向资源内写入内容,其它进程可以从资源内读取内容获取信息,从而完成通信。不同的资源种类,决定了不同的进程间通信方式。

二、管道

1.管道的原理

管道本质上就是一个内存级文件。当我们创建一个进程的时候,操作系统会维护一个task_struct结构以及files_struct结构,files_struct结构里有一个结构体数组struct file*fd_array[],其中保存着该进程打开的文件的地址。我们用该进程创建并打开一个管道文件,然后创建一个子进程。当子进程被创建成功的时候,会将父进程的task_struct结构和files_struct结构拷贝下来。因此,父进程打开的文件子进程也可以找得到。此时的父进程打开的文件就是能够被子进程看到的同一份资源,我们可以在管道文件中实现进程间的通信,这就是管道的原理。

在这里插入图片描述

2.管道的特点

  1. 管道是用来传输数据的文件,这份文件可以被多个进程同时看到。
  2. 管道是半双工的,数据只能从一个方向流动。即进行通信的两个进程只能由一个进程向管道写入内容,由另一个进程从管道中读取内容。不能两个进程同时向管道写入和读取内容。
  3. 一般而言,进程退出,管道释放,所以管道的生命周期是随进程的。
  4. 匿名管道的限制是只能用于具有共同祖先的进程(即具有亲缘关系的进程)之间进行通信;如果我们想让两个没有亲缘关系的进程之间进行通信,需要用到命名管道。
  5. 管道是自带同步机制的,它会自带访问控制,当管道满了的时候,写端进程不能再写入数据,必须阻塞式等待读端进程读取走数据才可以接着写入;当管道空了的时候,读端进程不能再读取数据,必须阻塞式等待写端进程写入数据才可以接着读取。
  6. 管道是面向字节流的。首先管道是一块固定大小的缓冲区,管道中先写入的字符一定是先被读取的。其次,管道内的内容是没有格式边界的,需要我们使用管道的用户来规定内容的边界。比如说如果我们没有规定格式边界,写端进程一直在写入数据但读端进程暂时就是不读取,等到写端进程写入完毕以后,读端进程就会一次性地从头到尾将数据全部读取。同样的如果我们没有规定格式边界,但是写端进程写入之后读端进程也在读取数据,那么写端进程就会向管道内一个字节一个字节地写入数据,读端进程就会从管道内一个字节一个字节地读取数据。所以我们可以规定管道的格式边界,比如我们控制写端进程在写入的时候,调用write接口的时候规定每次写入的大小是sizeof()多少,比如每次写入sizeof(int)大小的数据,那么读端进程每次也会读取sizeof(int)大小的数据。

3.匿名管道

使用匿名管道进行进程间通信限制在具有亲缘关系的进程之间,所以使用匿名管道的步骤一般分为下面几步:

  1. 首先由父进程创建匿名管道文件,该文件的读端和写端文件都会被打开。
  2. 父进程创建子进程,子进程继承了父进程的匿名管道文件。
  3. 根据需求,在父子进程中分别将读端文件和写端文件关闭,以此来满足只有一个进程进行写入操作,另一个进程进行读取操作。

创建匿名管道需要用到的系统调用接口是pipe接口:该接口会默认打开读端文件和写端文件,需要我们将数组pipefd传递进去用来获取读端文件和写端文件的文件描述符。pipefd[0]是读端文件的文件描述符,pipefd[1]是写端文件的文件描述符。如果匿名管道文件创建成功则返回值为0,创建失败则返回值为-1.

在这里插入图片描述

我们以一个简单的例子来演示一下匿名管道的使用方式:

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define NUM 1024

using namespace std;

int main()
{
    // 1.创建匿名管道
    int pipefd[2]; // 用来获取匿名管道的读写端文件描述符
    // 如果匿名管道文件创建失败
    if (pipe(pipefd) != 0)
    {
        cerr << "pipe error" << endl;
        return 1;
    }

    // 2.创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        // 我们让子进程进行读取操作,所以就要关闭写端文件
        close(pipefd[1]);

        // 子进程开始执行读取操作
        while (true)
        {
            char buf[NUM];
            memset(buf, 0, sizeof(buf));
            ssize_t readRes = read(pipefd[0], buf, sizeof(buf) - 1);
            // 读取成功,打印读取到的内容
            if (readRes > 0)
            {
                buf[readRes] = '\0';
                cout << buf << endl;
            }
            // 父进程关闭写端文件,停止读取
            else if (readRes == 0)
            {
                cout << "父进程退出了,子进程也可以退出了" << endl;
                break;
            }
            // 读取失败
            else
            {
                cerr << "read error" << endl;
                return 4;
            }
        }

        // 子进程读取完毕,关闭读端文件
        close(pipefd[0]);
        cout << "子进程读取完毕,可以退出了" << endl;
        exit(0);
    }
    else if (id > 0)
    {
        // 父进程
        // 我们让父进程进行写入操作,所以就要关闭读端文件
        close(pipefd[0]);

        // 父进程开始执行写入操作
        string msg = "你好子进程,我是父进程!";
        int cnt = 0;
        while (cnt < 5)
        {
            ssize_t writeRes = write(pipefd[1], msg.c_str(), msg.size());
            // 写入失败
            if (writeRes < 0)
            {
                cerr << "write error" << endl;
                return 3;
            }
            cnt++;
            sleep(1);
        }

        // 父进程写入完毕,关闭写端文件
        close(pipefd[1]);
        cout << "父进程写入完毕,可以退出了" << endl;
    }
    else
    {
        cerr << "fork error" << endl;
        return 2;
    }

    // 父进程最后需要回收子进程
    pid_t waitRes = waitpid(id, nullptr, 0);
    // 等待失败
    if (waitRes != id)
    {
        cerr << "wait error" << endl;
        return 5;
    }
    return 0;
}

运行程序结果如图:

在这里插入图片描述

1. 当父进程关闭了写端文件的时候,子进程是如何得知父进程关闭了的?
由于文件引用计数的存在,所以子进程可以得知父进程是否关闭了写端文件。

2. 上面的代码中,父进程在写入操作时每次写入都会sleep上1s,而子进程并没有设置sleep,为什么子进程读取内容的时候也会跟着sleep上1s呢?
当管道内部没有数据的时候,读端进程就必须阻塞等待;当管道内部数据被写满的时候,写端进程也必须阻塞等待。

除了父进程向子进程传递简单的信息,我们还可以利用这个方法来让父进程控制子进程执行任务,例如下面这份代码:我们写一个任务集合,将不同的任务加载进vector类型的任务集合中(由于没有具体的业务,所以就简单地输出一下语句当作是一个任务),父进程随机生成任务编码,将这个任务编码传递给子进程,子进程获取到这个任务编码以后再到任务集合中执行相应的任务。

#include <iostream>
#include <vector>
#include <unordered_map>
#include <cassert>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

// 3
using namespace std;

typedef void (*functor)();

vector<functor> functors; // 任务集合

unordered_map<uint32_t, string> info; // 任务信息

void func1()
{
    cout << "这是任务一,执行该任务的进程pid:[" << getpid() << "],执行时间:[" << time(nullptr) << "]" << endl;
}

void func2()
{
    cout << "这是任务二,执行该任务的进程pid:[" << getpid() << "],执行时间:[" << time(nullptr) << "]" << endl;
}

void func3()
{
    cout << "这是任务三,执行该任务的进程pid:[" << getpid() << "],执行时间:[" << time(nullptr) << "]" << endl;
}

void loadFunctor()
{
    info.insert({functors.size(), "任务一"});
    functors.push_back(func1);

    info.insert({functors.size(), "任务二"});
    functors.push_back(func2);

    info.insert({functors.size(), "任务三"});
    functors.push_back(func3);
}

int main()
{
    // 加载任务
    loadFunctor();
    // 创建管道
    int pipefd[2];
    // 创建管道失败
    if (pipe(pipefd) != 0)
    {
        cerr << "pipe error" << endl;
        return 1;
    }

    // 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        // 子进程获取任务指令并执行,关闭写端文件
        close(pipefd[1]);

        // 执行任务
        while (true)
        {
            uint32_t operatorType = 0;
            ssize_t readRes = read(pipefd[0], &operatorType, sizeof(uint32_t));
            // 读取到文件末尾,代表父进程关闭了写端文件
            if (readRes == 0)
            {
                cout << "父进程关闭了写端文件,我也可以退出了" << endl;
                break;
            }
            // 断言,必须让读取到的大小等于uint32_t的大小,这才是我们想要读取到的内容
            assert(readRes == sizeof(uint32_t));
            // 只有在debug模式下才会有断言,在release模式下没有断言
            // 所以这里对readRes进行强转,是为了防止在release模式下没有断言
            // readRes出现了只有定义没有使用的情况,这种情况会有warning
            (void)readRes;

            if (operatorType < functors.size())
            {
                functors[operatorType]();
            }
            else
            {
                cerr << "operatorType error,operatorType:" << operatorType << endl;
            }
        }

        // 任务执行完毕,子进程关闭读端文件并退出
        close(pipefd[0]);
        exit(0);
    }
    else if (id > 0)
    {
        // 生成随机种子
        srand((long long)time(nullptr));
        // 父进程
        // 父进程指派任务给子进程,关闭读端文件
        close(pipefd[0]);

        // 指派任务
        int sum = functors.size();
        int cnt = 20;
        while (cnt--)
        {
            // 生成任务码
            uint32_t commandCode = rand() % sum;
            cout << "父进程指派任务完成,任务是" << info[commandCode] << "任务编号是:" << cnt + 1 << endl;
            write(pipefd[1], &commandCode, sizeof(uint32_t));
            sleep(1);
        }

        // 指派任务结束,父进程关闭写端文件
        close(pipefd[1]);
        // 父进程回收子进程
        pid_t waitRes = waitpid(id, nullptr, 0);
        // 等待失败
        if (waitRes < 0)
        {
            cerr << "wait error" << endl;
            return 3;
        }
    }
    else
    {
        cerr << "fork error" << endl;
        return 2;
    }

    return 0;
}

我们上面举的两个例子都是单个进程向单个进程进行通信,从而实现单个进程控制单个进程的业务场景,其简单的结构图如下图所示:

在这里插入图片描述

下面我们再模拟实现一下单个进程向进程池(即多个进程)分配业务的场景。对多个进程进行控制,也就是与多个进程进行通信,我们只需要创建多个管道,让每一个管道对应一个进程,这样就可以通过向指定的管道里写入信息来达到控制多个进程的目的。

在这里插入图片描述

#include <iostream>
#include <vector>
#include <unordered_map>
#include <string>
#include <ctime>
#include <cassert>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

typedef void (*functor)();
vector<functor> functors; // 任务集合

unordered_map<uint32_t, string> info; // 任务信息 {任务编号,任务描述}

typedef pair<uint32_t, uint32_t> elem; // {进程pid,写端文件fd}
// 父进程要对子进程分派任务,必须拿到子进程对应的pid以及对应的写端文件fd
vector<elem> assignMap;

int processNum = 5;// 创建子进程的数量

void func1()
{
    cout << "这是任务一,执行该任务的进程pid:[" << getpid() << "],执行时间:[" << time(nullptr) << "]" << endl;
}

void func2()
{
    cout << "这是任务二,执行该任务的进程pid:[" << getpid() << "],执行时间:[" << time(nullptr) << "]" << endl;
}

void func3()
{
    cout << "这是任务三,执行该任务的进程pid:[" << getpid() << "],执行时间:[" << time(nullptr) << "]" << endl;
}

// 加载任务
void loadFunctors()
{
    info.insert({functors.size(), "任务一"});
    functors.push_back(func1);

    info.insert({functors.size(), "任务二"});
    functors.push_back(func2);

    info.insert({functors.size(), "任务三"});
    functors.push_back(func3);
}

// 子进程工作核心代码
void childWork(int blockFd)
{
    cout << "进程[" << getpid() << "] 开始执行任务" << endl;
    while (true)
    {
        uint32_t operatorCode = 0; // 任务码,标定哪一个具体的任务
        // 从管道文件中读取任务码
        ssize_t readRes = read(blockFd, &operatorCode, sizeof(uint32_t));
        // 读到文件末尾,即父进程关闭了写端文件,子进程也退出了
        if (readRes == 0)
        {
            break;
        }
        assert(readRes == sizeof(uint32_t));
        (void)readRes;

        // 执行对应的任务
        if (operatorCode < functors.size())
        {
            functors[operatorCode]();
            cout << endl;
        }
    }
    cout << "进程[" << getpid() << "] 结束执行任务" << endl;

}

// 父进程工作核心代码
void parentWork(vector<elem> processFds, int taskSum)
{
    // 生成随机数种子
    srand((long long)time(nullptr));
    while (taskSum--)
    {
        uint32_t processCode = rand() % processNum; // 进程码,指定哪一个进程
        uint32_t taskCode = rand() % functors.size();// 任务码,指定哪一个任务
        write(processFds[processCode].second, &taskCode, sizeof(uint32_t));

        cout << "父进程指派任务 -> " << info[taskCode] << " 给进程[" << processFds[processCode].first << "]" << endl;
        sleep(1);
    }
}

int main()
{
    // 1. 加载任务
    loadFunctors();

    // 2. 创建多个子进程
    for (int i = 0; i < processNum; i++)
    {
        // 创建管道
        int pipeFd[2] = {0};
        pipe(pipeFd);

        // 创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            // 子进程
            // 子进程读取信息,关闭写端文件
            close(pipeFd[1]);

            // 子进程执行任务
            childWork(pipeFd[0]);

            // 子进程执行完毕以后直接退出,能走到下面代码的一定是父进程,不需要做判断
            exit(0);
        }
        // 父进程
        // 父进程写入信息,关闭读端文件
        close(pipeFd[0]);
        elem e(id, pipeFd[1]);
        assignMap.push_back(e);
    }

    // 父进程指派任务
    parentWork(assignMap, 5);

    // 父进程关闭写端文件
    for (int i = 0; i < processNum; i++)
    {
        // 关闭写端文件
        close(assignMap[i].second);
    }
    for (int i = 0; i < processNum; i++)
    {
        // 回收子进程
        if (waitpid(assignMap[i].first, nullptr, 0) > 0)
        {
            cout << "回收子进程[" << assignMap[i].first << "]成功" << endl;
        }
    }
    
    return 0;
}

4.命名管道

命名管道和匿名管道的特征几乎一致,不同的地方在于匿名管道只能够让父子进程或者是兄弟进程之间通信,而命名管道是让两个毫无亲缘关系的进程通信。

和匿名管道一样,命名管道的使用首先得要创建一个命名管道文件。我们用 mkfifo 指令创建命名管道文件,输入指令man mkfifo查看一下 mkfifo 指令的介绍:

在这里插入图片描述

这是命令行的指令创建的命名管道,我们在写代码的时候需要使用操作系统为我们提供的 mkfifo 接口来创建命名管道:

mkfifo:

  1. 形参:
    (1)const char *pathname:指定在什么路径下创建命名管道文件
    (2)mode_t mode:指定命名管道文件的权限
  2. 返回值:如果创建命名管道文件成功,则返回0,否则返回-1

在这里插入图片描述

下面我们模拟一个简单的业务场景来演示一下命名管道通信的使用方法:
我们写一个客户端和一个服务端,让服务端来创建命名管道,客户端以写的方式打开命名管道文件,服务端以读的方式打开命名管道文件,客户端向命名管道文件写入数据,服务端从命名管道文件读取数据,从而实现客户端与服务端的通信。

comm.h文件:

#pragma once
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

using namespace std;

#define IPC_NAME "/home/hkx/study23.02.06/linux_blog/fifo/.fifo"

client.cc文件:

#include "comm.h"

#define NUM 1024

int main()
{
    // 以只写的方式打开命名管道文件
    int pipeFd = open(IPC_NAME, O_WRONLY);

    // 向命名管道文件写入数据
    char line[NUM];
    while(true)
    {
        printf ("请输入你的消息 #");
        fflush(stdout);   
        // 从命令行读取数据
        if (fgets(line, sizeof(line), stdin) != nullptr)
        {
            // 消除\n
            line[strlen(line) - 1] = '\0';
            // 读取到的数据再写入到命名管道文件当中
            write(pipeFd, line, strlen(line));
        }
        else
        {
            break;
        }
    }

    // 关闭命名管道文件
    close(pipeFd);
    cout << "客户端退出了" << endl;
    return 0;
}

server.cc文件:

#include "comm.h"

#define NUM 1024

int main()
{
    // 设置系统默认权限为0
    umask(0);
    // 创建命名管道文件失败
    if (mkfifo(IPC_NAME, 0600) != 0)
    {
        cerr << "mkfifo error" << endl;
        return 1;
    }

    // 以只读的方式打开命名管道文件
    int pipeFd = open(IPC_NAME, O_RDONLY);

    // 读取命名管道文件内容
    char buffer[NUM];// 存储读取到的内容
    while (true)
    {
        ssize_t readRes = read(pipeFd, buffer, sizeof(buffer) - 1);
        if (readRes == 0)
        {
            cout << "客户端退出了,服务端也退出了" << endl;
            break;
        }
        else if (readRes > 0)
        {
            buffer[readRes] = '\0';
            cout << "客户端 -> 服务端 #" << buffer << endl;
        }
        else
        {
            cerr << "read error" << endl;
            return 2;
        }

    }

    // 关闭命名管道文件
    close(pipeFd);
    cout << "服务端退出了" << endl;
    unlink(IPC_NAME);
    return 0;
}

三、System V共享内存

System V是一套正常进行通信时的标准,进程间通信的本质是要让不同的进程能够看到同一份资源,共享内存的原理就是在物理内存上创建一个共享内存能让不同的进程都可以访问这块共享内存。每一个进程都有进程地址空间和页表,页表维护的是进程地址空间和物理内存之间的映射关系。共享内存机制的通信方式,首先要在物理内存上创建一块共享内存,然后通过不同进程的页表将这块内存的地址映射到对应进程的共享区中,这样每个进程都能拿到物理内存上的这一块共享内存,也就是不同的进程可以看到同一份资源,从而可以实现共享内存式的进程间通信。

在这里插入图片描述

操作系统提供的共享内存的这一套接口使用成本是比较高的,有很多细节需要讲清楚才能掌握使用,所以我们详细地介绍一下共享内存的接口。

1.shmget函数

shmget函数是用来创建一个共享内存的,它需要传入三个参数,分别是 key_t keysize_t sizeint shmflg。下面首先介绍一下这三个参数的含义以及使用方法。

在这里插入图片描述

size_t size:
这个参数是用来设置共享内存的空间大小的,这个参数建议设置为页的整数倍,一页的大小是4KB,也就是建议设置成4KB的整数倍。原因是假设我们的内存是4GB的大小,一页的大小是4KB那么4GB的内存就等于1048576页,也就是2^20页,所以操作系统是将内存看作一个一个的页,操作系统会为一个页维护一个数据结构 struct page{} ,然后将这些页组织起来成为一个页数组 struct page mem[2^20],最终操作系统对内存的管理就变成了对页数组的管理。所以我们向物理内存中申请共享内存最好是以页为单位。

int shmflg:
在物理内存中申请共享内存会有两种情况:如果该共享内存不存在、如果该共享内存存在。shmflg这个参数需要用户传递选项,由用户来规定在创建共享内存时如果该共享内存存在要怎么做,如果该共享内存不存在又要怎么做。它的常见选项有以下两个:

  1. IPC_CREAT: 创建共享内存时,如果该共享内存已经存在就获取之,如果该共享内存不存在就创建之。
  2. IPC_EXCL: 这个选项不单独使用,必须和IPC_CREAT配合使用(位图结构,按位或即可配合使用),创建共享内存时,如果该共享内存不存在就创建之,如果该共享内存已经存在就出错返回。
    IPC_EXECL可以保证如果用shmget函数创建共享内存成功了,那么该共享内存一定是一个全新的共享内存。

key_t ket:
操作系统中可能存在上百个进程,也就可能存在很多的进程之间需要通信,因此可能会存在很多个共享内存。这么多个共享内存,操作系统当然需要将它们管理起来。操作系统管理共享内存的核心也是先描述再组织。所以操作系统会维护共享内存的结构。

这就是操作系统维护的共享内存结构,在 struct shmid_ds{} 中有一个结构是 struct ipc_perm shm_perm ,是共享内存里与权限相关的信息。

在这里插入图片描述

我们在来看一下 struct ipc_perm{} 这个结构,该结构里有一个变量 key_t _key ,该变量后面的描述说这是由shmget函数提供的key值。这个key值就是shmget函数接口中需要传入的参数 key_t ket ,它标定了共享内存在内核中的唯一值。

在这里插入图片描述

这个key值是由用户提供的而不是由操作系统生成的,原因是如果key值是由操作系统生成的,那么我们一个进程调用shmget函数以后获取到了这个key值,它是没有办法让其它进程也获得该key值得。由于key值是标识共享内存的唯一性的,所以如果我们想让通信的两个进程看到同一份共享内存,只需要让他们拥有同一个key值即可。

理论上来说key值的设定我们可以自己给值,但需要注意的是不能与操作系统中已有的共享内存的key值起冲突。所以方便起见,操作系统为我们提供了ftok接口来生成key值:我们只需要传递对应的文件路径和项目id(项目id可以自定义设置,一般在0-255之间就够了),它会根据文件路径找到对应的文件,拿到该文件的inode(因为每个文件的inode具有唯一性)和我们传入的项目id进行组合,生成一个具有唯一性的key值。

在这里插入图片描述

返回值:
shmget函数如果创建共享内存成功则返回一个合法的共享内存标识符,否则返回-1。

shmget所有的参数以及返回值我们都介绍完了,下面用代码演示一下shmget的具体使用方法:我们创建comm.hpp文件来编写生成key值的函数,创建ShmServer.cc文件和ShmClient.cc文件分别调用该函数生成key值,然后由ShmServer.cc文件充当创建共享内存的角色。

comm.hpp文件:

#pragma once 

#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "Log.hpp"

#define IPC_PATHNAME "/home/hkx/study23.02.07/linux_blog"
#define PROC_NAME 0x15
#define MEM_SiZE 4096 // 4096字节=4kb

key_t creatKey()
{
    // 创建key值
    key_t key = ftok(IPC_PATHNAME, PROC_NAME);
    // 创建失败
    if (key < 0)
    {
        Log() << "ftor : " << strerror(errno) << std::endl;
        exit(1);
    }

    return key;
}

Log.hpp文件:

#pragma once

#include <iostream>
#include <time.h>

// 打印信息,方便查看
std::ostream& Log()
{
    std::cout << "For Debug | " << "time : " << time(nullptr) << " | ";
    return std::cout;
}

ShmServer.cc文件:

#include "comm.hpp"
#include "Log.hpp"

using namespace std;

const int flag = IPC_CREAT | IPC_EXCL;

// 充当创建共享内存的角色
int main()
{
    // 创建key值
    key_t key = creatKey();
    Log() << "key : " << key << endl;

    // 创建共享内存
    int shmid = shmget(key, MEM_SiZE, flag);
    
    // 创建共享内存失败
    if (shmid < 0)
    {
        Log() << "shmget : " << strerror(errno) << endl;
        return 2;
    }
    
    // 创建共享内存成功
    Log() << "create shm success, shmid : " << shmid << endl; 
    return 0;
}

ShmClient.cc文件:

#include "comm.hpp"
#include "Log.hpp"

using namespace std;

int main()
{
    // 创建key值
    key_t key = creatKey();
    Log() << "key : " << key << endl;
    return 0;
}

我们运行程序查看一下结果,首先运行ShmServer程序,再运行ShmClient程序,我们可以看到,两个进程确实获取到了同一份key值,并且ShmServer进程也成功地创建了共享内存。

在这里插入图片描述

但是当我们程序运行完毕退出了以后,我们再运行一次ShmServer程序创建共享内存时,它会报错,会说该共享内存已经存在了,无法创建成功。这就说明共享内存不像文件一样随着进程退出就退出,当进程运行完毕退出以后共享内存还会存在,共享内存的生命周期是随操作系统内核的。所以我们需要显式地删除共享内存,否则的话只能让内核(操作系统)重启来清空共享内存。

在这里插入图片描述

我们可以在操作系统中查看当前用户创建的共享内存都有哪些,输入指令ipcs -m查看共享内存:

在这里插入图片描述

我们可以输入指令ipcrm -m shmid删除对应的共享内存,shmid就填入想要删除的shmid值即可,因为shmid值在用户层面是唯一的,它是在用户层面标定共享内存的唯一性,不用key值来指定删除是因为key值是在内核层面标定共享内存的唯一性。

在这里插入图片描述

2.shmctl函数

shmctl是一个控制共享内存的接口,它可以控制删除共享内存(就不用在命令行删除那么麻烦了,可以直接写代码删除共享内存)、设置共享内存属性以及获取共享内存的属性。它需要传递三个参数,分别是 int shmid ,指定要操作的共享内存的shmid,int cmdstruct shmid_ds * buf 。下面详细介绍一下后面两个参数。

在这里插入图片描述
int cmd:
这个参数传递的是命令选项:

  1. IPC_STAT:这个命令选项是用来获取共享内存信息的,共享内存的信息保存在 struct shmid_ds {} 这个结构中,shmctl函数的第三个参数 struct shmid_ds *buf 可以用来获取内核中的共享内存的信息。
  2. IPC_SET:这个命令选项是用来设置共享内存信息的,我们可以定义变量 struct shmid_ds *buf 来写入共享内存的信息,再将这个变量通过shmctl函数传递进去设置内核中的共享内存的信息。
  3. IPC_RMID:这个命令选项可以删除共享内存,如果使用这个选项的话,第三个参数可以设置为nullptr。

3.shmat函数和shmdt函数

创建好了共享内存以后,进程间通信使用共享内存,就需要使用shmat函数将进程与共享内存挂接起来。相反,当不再进行通信的时候,就需要使用shmdt函数将进程与共享内存取消挂接。

在这里插入图片描述

shmat:

  1. 形参:
    (1)int shmid:指定需要挂接到哪一个共享内存
    (2)const void *shmaddr:如果有特殊需要的话,这个参数可以指定在进程地址空间上的什么地址挂接共享内存,无特殊需要一般默认设置为nullptr
    (3)int shmflg:设置挂接的方式,一般设置为0默认为以读写方式挂接
  2. 返回值:如果挂接成功则返回挂接的这块共享内存的地址,否则返回-1

shmdt:

  1. 形参:
    (1)const void *shmaddr:要取消关联的共享内存的地址
  2. 返回值:如果取消成功则返回0,否则返回-1

4.共享内存的使用

上面我们介绍的都是如何创建共享内存以及如何让不同的进程关联共享内存,说到底就是上面做的工作都是实现让不同的进程看到同一份共享内存资源。下面我们就可以使用共享内存进行进程间通信。我们让ShmServer.cc文件创建共享内存并且往共享内存写入数据,让ShmClient.cc文件循环读取共享内存的数据。

comm.hpp文件:

#pragma once 

#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "Log.hpp"

#define IPC_PATHNAME "/home/hkx/study23.02.07/linux_blog"
#define PROC_NAME 0x15
#define MEM_SiZE 4096 // 4096字节=4kb

key_t creatKey()
{
    // 创建key值
    key_t key = ftok(IPC_PATHNAME, PROC_NAME);
    // 创建失败
    if (key < 0)
    {
        Log() << "ftor : " << strerror(errno) << std::endl;
        exit(1);
    }

    return key;
}

Log.hpp文件:

#pragma once

#include <iostream>
#include <time.h>

// 打印信息,方便查看
std::ostream& Log()
{
    std::cout << "For Debug | " << "time : " << time(nullptr) << " | ";
    return std::cout;
}

ShmServer.cc文件:

#include "comm.hpp"
#include "Log.hpp"

using namespace std;

const int flag = IPC_CREAT | IPC_EXCL;

// 充当创建共享内存的角色
int main()
{
    // 创建key值
    key_t key = creatKey();
    Log() << "key : " << key << endl;

    // 创建共享内存
    // 我们必须设置权限否则默认权限为0不能进行读写操作
    int shmid = shmget(key, MEM_SiZE, flag | 0666);
    // 创建共享内存失败
    if (shmid < 0)
    {
        Log() << "shmget : " << strerror(errno) << endl;
        return 2;
    }
    // 创建共享内存成功
    Log() << "create shm success, shmid : " << shmid << endl;
    sleep(10);// 方便查看结果

    // 关联共享内存
    char* str = (char*)shmat(shmid, nullptr, 0);

    // 使用共享内存
    int cnt = 0;
    while(cnt < 26)
    {
        str[cnt] = 'A' + cnt;
        cnt++;
        str[cnt] = '\0';
        sleep(1);
    }

    // 取消关联共享内存
    shmdt(str); 

    // 删除共享内存
    shmctl(shmid, IPC_RMID, nullptr);
    return 0;
}

ShmClient.cc文件:

#include "comm.hpp"
#include "Log.hpp"

using namespace std;

int main()
{
    // 创建key值
    key_t key = creatKey();
    Log() << "key : " << key << endl;

    // 获取共享内存
    int shmid = shmget(key, MEM_SiZE, IPC_CREAT);
    // 创建共享内存失败
    if (shmid < 0)
    {
        Log() << "shmget : " << strerror(errno) << endl;
        return 2;
    }
    cout << shmid << endl;

    // 关联共享内存
    char* str = (char*)shmat(shmid, nullptr, 0);

    // 使用共享内存
    while (true)
    {
        printf("%s\n", str);
        sleep(1);
    }

    // 取消关联共享内存
    shmdt(str); 
    return 0;
}

当我们的程序运行起来以后,ShmServer是向共享内存写入的一方,ShmClient是从共享内存读取的一方,当ShmServer进程还没有往共享内存写入内容时,ShmClient进程不会阻塞式地等待写入,它会直接读取,当共享内存为空的时候默认读到的就是空串。这是因为
共享内存本身没有任何的访问控制,它是被双方直接看到,属于双方各自的用户空间,可以直接通信。因此,共享内存是所有进程间通信方式中通信速度最快的。

我们在使用shmget函数时说到创建和使用一个共享有如果存在和如果不存在两种情况,那么进程是如何判断该共享内存存不存在的呢?
想要判断共享内存存不存在,首先得要有方法来标识共享内存的唯一性,key值就是在内核层面标识共享内存唯一性的。接着用户在使用shmat函数创建一个全新的共享内存的时候,用户自己设定的key值来标定这一块全新的共享内存,当有另一个进程要使用这个共享内存的时候,就可以使用shmat函数通过同样的key值来比对是否存在该共享内存。

5.用管道实现具有访问控制的共享内存

共享内存由于本身的特性它是不具有访问控制的,读端进程不会阻塞式地等待写端进程写入数据,即使共享内存为空读端进程也会读取。管道是具有访问控制的,所以我们可以实现一份代码,利用管道的特性让共享内存具有访问控制。

我们在ShmServer.cc文件中创建命名管道文件,进程ShmServer作为向共享内存写入的一端,所以当向共享内存写入完毕以后,再向命名管道写入一个信号代表写端进程写入完毕了,ShmClient进程作为读端进程在读取共享内存的数据之前,先读取命名管道的信号,由于命名管道是有访问控制的,所以如果写端进程没有发送信号过来,就意味着写端进程还没有向共享内存写入数据,此时读端进程就会阻塞式等待读取命名管道的内容,当读取到信号以后再从共享内存读取信息,这样就可以利用管道的访问控制从而实现共享内存的访问控制。

comm.hpp文件:

#pragma once 

#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"

#define IPC_PATHNAME "/home/hkx/study23.02.07/linux_blog"
#define PROC_NAME 0x15
#define MEM_SiZE 4096 // 4096字节=4kb
#define FIFO_PATHNAME ".fifo"
#define WRITE O_WRONLY
#define READ O_RDONLY

key_t createKey()
{
    // 创建key值
    key_t key = ftok(IPC_PATHNAME, PROC_NAME);
    // 创建失败
    if (key < 0)
    {
        Log() << "ftor : " << strerror(errno) << std::endl;
        exit(1);
    }

    return key;
}

// 创建命名管道文件
void createFIFO()
{
    umask(0);
    if (mkfifo(FIFO_PATHNAME, 0666) < 0)
    {
        Log() << "mkfifo : " << strerror(errno) << std::endl;
        exit(3);
    }
}

// 打开FIFO文件
int openFIFO(const std::string &pathName, int flags)
{
    return open(pathName.c_str(), flags);
}

// 读端进程等待
ssize_t readerWait(int fd)
{
    uint32_t value = 0;
    ssize_t readRes = read(fd, &value, sizeof(uint32_t));
    return readRes;
} 

// 写端进程发送写入信号
void writerSignal(int fd)
{
    uint32_t cmd = 1;
    write(fd, &cmd, sizeof(uint32_t));
}

// 关闭命名管道文件
void closeFIFO(int fd, const char* filename)
{
    close(fd);
    unlink(filename);
}

Log.hpp文件:

#pragma once

#include <iostream>
#include <time.h>

// 打印信息,方便查看
std::ostream& Log()
{
    std::cout << "For Debug | " << "time : " << time(nullptr) << " | ";
    return std::cout;
}

ShmServer.cc文件:

#include "comm.hpp"
#include "Log.hpp"

using namespace std;

const int flag = IPC_CREAT | IPC_EXCL;

// 充当创建共享内存的角色
int main()
{
    // 创建key值
    key_t key = createKey();
    Log() << "key : " << key << endl;

    cout << "before shmget" << endl;
    // 创建共享内存
    // 我们必须设置权限否则默认权限为0不能进行读写操作
    int shmid = shmget(key, MEM_SiZE, flag | 0666);

    cout << "after shmid" << endl;
    // 创建共享内存失败
    if (shmid < 0)
    {
        Log() << "shmget : " << strerror(errno) << endl;
        return 2;
    }
    // 创建共享内存成功
    Log() << "create shm success, shmid : " << shmid << endl;

    // 创建命名管道文件
    createFIFO();
    cout << "after createFIFO" << endl;

    // 以只写的方式打开命名管道文件
    int fd = openFIFO(FIFO_PATHNAME, WRITE);
    cout << "after openFIFO" << endl;
    if (fd < 0)
    {
        Log() << "openFIFO error" << endl;
    }

    // 关联共享内存
    char *str = (char *)shmat(shmid, nullptr, 0);

    // 使用共享内存
    while (true)
    {
        printf("请输入需要发送的信息 #");
        fflush(stdout);

        // 从标准输入文件读取内容到共享内存中
        ssize_t readRes = read(0, str, MEM_SiZE);
        if (readRes > 0)
        {
            str[readRes] = '\0';
        }

        // 写端进程向共享内存写入信息完毕,可以发信号给读端进程进行读取
        writerSignal(fd);
    }

    // 取消关联共享内存
    shmdt(str);

    // 删除共享内存
    shmctl(shmid, IPC_RMID, nullptr);

    // 删除命名管道
    closeFIFO(fd, FIFO_PATHNAME);
    return 0;
}

ShmClient.cc文件:

#include "comm.hpp"
#include "Log.hpp"

using namespace std;

int main()
{
    // 以只读的方式打开命名管道
    int fd = openFIFO(FIFO_PATHNAME, READ);
    if (fd < 0)
    {
        Log() << "openFIFO error" << endl;
    }

    // 创建key值
    key_t key = createKey();
    Log() << "key : " << key << endl;

    // 获取共享内存
    int shmid = shmget(key, MEM_SiZE, IPC_CREAT);
    // 获取共享内存失败
    if (shmid < 0)
    {
        Log() << "shmget : " << strerror(errno) << endl;
        return 2;
    }
    Log() << "shmget success" << endl;

    // 关联共享内存
    char* str = (char*)shmat(shmid, nullptr, 0);
    Log() << "shmat success" << endl;

    // 使用共享内存
    while (true)
    {
        // 阻塞式等待写端进程写入数据
        if(readerWait(fd) <= 0)
        {
            cout << readerWait << endl;
            break;
        }
        printf("%s\n", str);
        sleep(1);
    }

    // 取消关联共享内存
    shmdt(str); 
    Log() << "shmat success" << endl;
    return 0;
}
  • 13
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值