目录

一、进程间通信概念

二、进程间通信的发展

三、进程间通信的分类

四、管道

4.1 什么是管道

4.2 匿名管道

4.2 基于匿名管道设计进程池

4.3 命名管道

4.4 用命名管道实现server&client通信

五、system V共享内存

5.1 system V共享内存的引入

5.2 共享内存的原理

5.3 共享内存函数

5.4 使用共享内存的步骤

5.5 基于共享内存的进程间通信示例

5.6 共享内存的特点

5.7 共享内存数据结构

六、简述system V消息队列和system V信号量

6.1 system V消息队列

6.2 system V信号量

七、回顾共享内存数据结构


一、进程间通信概念

进程虽然具有独立性,但是进程和进程之间是可能进行协作的。协作的前提是进程之间可以传递信息,即需要进程间通信。

Linux中进程间通信(Inter-Process Communication,IPC)是指为了协调进程之间的行为,不同进程之间进行信息交换和资源共享的机制。

进程间通信的目的包括:

  • 数据传输:允许一个进程将数据发送给另一个进程。
  • 资源共享:允许多个进程访问相同的资源,如文件、内存区域等。
  • 通知事件:一个进程可以向另一个或一组进程发送消息,通知它(它们)某个事件的发生,如进程终止时通知父进程。
  • 进程控制:允许一个进程完全控制另一个进程的执行。例如调试进程需要拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
    (“陷入”通常指的是程序的执行被操作系统或其他进程强制暂停,以便处理系统调用或硬件中断。)

进程间通信如何实现呢?之前讲到进程具有独立性,那么A进程的数据要交给B进程,不能直接把A进程的数据直接给B进程,因为A进程访问B进程的内存区域把数据拷贝进去,或者B进程访问A进程的内存区域把数据拷贝出来,这两种都不行,会破坏进程的独立性。所以就需要进程通信时的中间媒介。这样既能保持进程的独立性也能实现进程间通信。因此进程间通信的本质就是让不同的进程看到OS中的同一份资源,从而实现数据的传递和共享。(该资源不能由A/B进程提供,但是能由A/B进程申请)

二、进程间通信的发展

进程间通信的发展经历了以下几个阶段:

  • 管道:包括匿名管道(pipe)和命名管道(FIFO)。匿名管道只能用于具有亲缘关系的进程间通信,而命名管道可以用于不具有亲缘关系的进程间通信。
  • System V进程间通信:包括System V消息队列、System V共享内存、System V信号量等。这些机制提供了更为复杂的IPC功能,支持多种形式的通信和同步。
  • POSIX进程间通信:包括POSIX消息队列、POSIX共享内存、POSIX信号量、互斥量、条件变量、读写锁等。POSIX IPC提供了与System V IPC类似的功能,但具有更好的可移植性。

三、进程间通信的分类

Linux中的进程间通信可以分为以下几类:

  • 管道:
    匿名管道:用于具有亲缘关系的进程间通信。
    命名管道:用于不具有亲缘关系的进程间通信。
  • System V IPC:
    消息队列:用于进程间传递消息。
    共享内存:用于进程间共享内存区域。
    信号量:用于进程间同步和互斥。
  • POSIX IPC:
    消息队列:与System V消息队列类似。
    共享内存:与System V共享内存类似。
    信号量:与System V信号量类似。
    互斥量:用于进程间同步。
    条件变量:用于进程间同步。
    读写锁:用于进程间同步。

四、管道

4.1 什么是管道

管道(Pipe)是Unix系统中用于进程间通信的一种机制,它允许一个进程的输出直接作为另一个进程的输入。管道是一种单向的通信通道,数据只能从管道的一端流向另一端。

回顾文件系统:
【Linux】文件描述符和重定向-CSDN博客    【Linux】文件系统和软硬链接-CSDN博客

如何做到让不同的进程看到了同一个管道文件?

进程是具有独立性的,一个进程的数据,另一个数据是无法直接拿到的。就连父子进程也会因为修改数据而触发写时拷贝。所以不能通过数据传递(这里指命名的变量),而是使用其他方式。

可执行程序加载到内存时,要创建task_struct,其中包含指向files_struct结构体的指针,在该结构体中有一个fd_array指针数组。当加载一个文件到内存时,会创建struct file ,结构体中会包含文件的inode、方法集、文件缓冲区。并将自己链入到fd_array中。

在上层用户使用某个方法向磁盘写入数据时,会打开文件、得到fd、找到struct inode、文件缓冲区、通过方法集的方法将数据刷新到磁盘。

创建子进程,父进程的task_struct 、flies_struct 都要给子进程拷贝一份(flies_struct属于进程部分的数据),flies_struct是浅拷贝,直接拷贝里面的指针。因此父子进程的fd_array[]指向相同的file。struct file不需要重新拷贝,此时不同的进程看到OS中的同一份资源,父进程只需要向自己的文件缓冲区中写入数据,子进程就可以通过它的文件描述符得到该数据。

打开普通文件就要有路径,最终数据刷新到磁盘上。父进程想要给子进程发消息,如果通过这种把数据写到缓冲区里,再写到磁盘中的方式,效率就太低下了(一般文件缓冲区的数据都要刷新到磁盘)。现在就需要这个文件是一个纯内存级的文件,不需要在磁盘中存在,甚至不需要名字,只要保证父子进程能访问到它即可。这种文件就叫做管道文件,所以管道文件也是纯内存级的文件,不需要向磁盘刷新。不需要名字也不需要路径,所以也叫匿名管道

管道文件有一个特点:实现了资源共享之后,只允许单向通信。

这种单向传递的通信特征很像日常生活中的管道,所以起名叫做管道。例如家里自来水永远都是自来水公司到家里。

在Unix系统中,管道通常通过命令行中的管道符号('|')来创建。例如在命令行中输入以下命令时:

command1 | command2

命令'command1'的输出会被重定向到管道中,而命令'command2'的输入会从管道中读取。这样,'command1'的输出就会成为'command2'的输入,实现了两个进程之间的数据传递。

除了命令行中的管道,Unix系统还提供了两种类型的管道:匿名管道和命名管道。

4.2 匿名管道

匿名管道(pipe)是在命令行中自动创建的,用于具有亲缘关系的进程间通信,如父进程和子进程。它有一个管道文件描述符,分别对应读端和写端。
匿名管道在创建后不能被其他进程打开。

#include <unistd.h>
功能:创建一匿名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

注:fd是输出型参数,返回读写端对应的fd,用来关掉读/写端。

匿名管道的原理:(这里实现父写子读)

  1. 把一个文件按读方式和写方式打开。
  2. 创建子进程时,子进程直接拷贝父进程的文件描述符表。
  3. 父进程关闭fd[0],留下写端,子进程关闭fd[1],留下读端。
  4. 就形成了单向通信的管道通路。

1. 为什么最开始时把一个文件按读方式和写方式打开?
因为只保留读端或写端,创建子进程时不能保留单向信道。保留读端和写端,子进程也有读端和写端,再进行适当的关闭。就可以实现单向信道,父进程读子进程写,或者父进程写子进程读。

2. 同一个进程把文件分别进行读打开和写打开,在内存里,文件的内容和属性会存在几份?

只用存在一份。这是因为文件的内容和属性(如权限、所有者、大小、创建和修改时间等)都存储在文件的 inode 结构体中,而 inode 结构体在文件系统中被唯一标识。

3. 同一个进程把文件分别进行读打开和写打开,需要几个struct file结构体?

需要两个struct file结构体。struct file内部有一个字段f_pos ,表示当前的操作位置,相当于文件内部的偏移量。文件读和写打开时,它读位置和写位置不一样。同一个进程把文件分别进行读打开和写打开,需要创建两个struct file结构体,一个用来读取、一个用来写入。只不过这两个struct file结构体会指向同样的一个inode、同一个方法集、同一个缓冲区。

4. 进程结束时,文件会被直接关闭吗?

不会。创建子进程时,由于files_struct是浅拷贝,所以指向相同的struct file结构体。形成管道时父子进程关闭各自的读/写端。struct file 中有一个引用计数的字段f_count,用于跟踪有多少个进程正在使用这个文件。当进程打开该文件时,f_count 会增加;当进程关闭文件时,f_count 会减少。所以进程关闭读/写端的实质是把文件描述符表内部指向struct file 的指针清空,然后依次将引用计数f_count--,此时进程就认为把文件关了,但最后文件是否关闭是由操作系统决定的,要判断f_count是否减到0。最终,是否关闭文件由操作系统决定,它会在所有引用计数减到0时释放与文件相关的资源。

5. 引用计数f_count和硬链接数的不同

硬链接是在磁盘中用来统计有多少文件名和我的文件inode产生映射关系的;但是上面的引用计数f_count是用来记述内核数据结构struct file被多少进程文件描述符表指向的。两者虽然都是引用计数,但引用的场景不同

现让父进程创建一个管道文件,进行父读子写,即父进程关闭写端,子进程关闭读端 

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

#define MAX_SIZE 1024

int main()
{
    int pipefd[2] = {0};
    int ret = pipe(pipefd);
    assert(ret == 0); //防止编译器告警,意料之中的错误用assert,意料之外的错误用if
    (void)ret;
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }

    if(id == 0)
    {
        //子进程写
        close(pipefd[0]);//关闭读端
        int n = 5;
        while(n--)
        {
            char buffer[MAX_SIZE];
            snprintf(buffer, sizeof(buffer),"child progress,pid: %d, n: %d\n",getpid(),n);
            write(pipefd[1], buffer,strlen(buffer));
            sleep(1);
        }
        exit(0);
    }
    else
    {
        //父进程读
        close(pipefd[1]);//关闭写端
        char buffer2[MAX_SIZE];
        while(true)
        {
            ssize_t n = read(pipefd[0],buffer2,sizeof(buffer2)-1);
            if(n > 0)
            {
                buffer2[n] = 0;
                std::cout << getpid() << ", child words: "<<buffer2 << std::endl;
            }
            else 
            {
                break;
            }
        }
    }

    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id)
    {
        std::cout << "wait success" << std::endl;
    }
    return 0;
}

注:

  1. 系统调用的接口是C语言的,为了更好地适应某些极端场景,可以使用C语言的接口,例如示例中使用了snprintf接口。
  2. sizeof()-1是为了传递字符串时预留一个\0,虽然大部分场景也会预留\0,甚至字符串截断也会预留\0,但在某些场景还是要sizeof()-1。例如read,它不知道传进来的是二进制还是字符串还是其它类型。\0结尾是字符串的标准,读写文件没有义务在数据后面预留\0,所以需要我们自己预留维护。

 a. 管道的4种情况

  1. 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据)。
  2. 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)。
  3. 写端关闭,读端继续读取,它将读到管道中的所有数据,直到read返回值为0, 表示读到文件结尾。
  4. 读端关闭,写端写入时,OS会直接杀掉写端进程,通过向目标进程发送SIGPIPE(13)信号,终止写端进程。

 b. 管道的5种特性

  1. 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信。(父子、爷孙...)
  2. 匿名管道,默认给读写端要提供同步机制 --- 了解现象:读端和写端是顺序进行的,它们之间不会同时进行。
  3. 面向字节流  --- 现象:不关心数据的格式,只关心数据的大小和顺序,按字节一次性将数据获取。管道可以传输任何类型的数据
  4. 管道的生命周期是随进程的。当创建管道的进程结束时,管道也随之消失。
  5. 管道是单向通信的,半双工通信的一种特殊情况

补充:如果 read 成功读取数据,它会返回实际读取的字节数。如果 read 调用失败,它将返回 -1 并设置 errno 以指示错误。如果到达文件末尾,read 将返回 0。

例如命令: sleep 1000 | sleep 2000 | sleep 3000
操作系统创建了3个进程,两个管道。

使用管道之后,原本向标准输出输出的内容将重定向到管道文件中。原本从标准输入获得的内容将重定向到从管道文件中获取。

4.2 基于匿名管道设计进程池

进程池的概念:

        一个进程可以创建很多进程,通过管道与每个进程相连。正常情况,如果管道没有数据了,读端必须等待,直到有数据为止。这样就可以通过对特定的管道传输数据实现唤醒特定的进程。

        创建进程会消耗时间和空间资源,如果要处理一个任务要等到任务来到时再处理,进行创建进程、分配资源,这样就有些耽误时间,如果提前把进程创建好,等任务来到时让已经创建好的进程完成任务,这样就可以节省创建进程的时间。这些提前创建好的进程就叫做进程池。

补充内存池的概念:

        调用系统调用是有成本的。调用自己的函数也有成本,所以才有了宏函数、内联函数。调用系统调用时操作系统会做很多事情,比如申请内存,如果内存不足,操作系统就要执行内存管理算法协调内存,释放、调整、置换挂起等等。一次性申请100MB内存比申请十次10MB内存效率更高。在C++标准模板库(STL)中,有一个参数为内存配置器,它是一个模板类,用于指定用于存储容器元素的内存管理策略。它定义了如何分配内存、如何构造新元素、如何释放内存以及如何管理内存池等。在申请内存时它会额外多申请一部分,这样在需要扩容时就可以减少系统调用,这种多申请内存的方法就叫做内存池。

模拟实现进程池

Task.hpp如下:

#pragma once

#include <iostream>
#include <vector>
#include <unistd.h>
#include <functional>
#include <ctime>

typedef std::function<void()> task_t;
void Download()
{
    std::cout << "我是一个下载任务"
              << " 处理者: " << getpid() << std::endl;
}

void PrintLog()
{
    std::cout << "我是一个打印日志的任务"
              << " 处理者: " << getpid() << std::endl;
}

void PushVideoStream()
{
    std::cout << "这是一个推送视频流的任务"
              << " 处理者: " << getpid() << std::endl;
}

class Init
{
public:
    // 任务码
    const static int g_download_code = 0;
    const static int g_printlog_code = 1;
    const static int g_push_videostream_code = 2;
    // 任务集合
    std::vector<task_t> tasks;

public:
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(PrintLog);
        tasks.push_back(PushVideoStream);

        srand(time(nullptr) ^ getpid());
    }

    // 检查任务码
    bool CheckCode(int code)
    {
        if (code >= 0 && code < tasks.size())
            return true;
        else
            return false;
    }

    // 运行任务
    void RunTask(int code)
    {
        return tasks[code]();
    }

    // 随机选择任务
    int SelectTask()
    {
        return rand() % tasks.size();
    }
    
    // 描述任务码对应的任务名称
    std::string ToDesc(int code)
    {
        switch (code)
        {
        case g_download_code:
            return "Download";
        case g_printlog_code:
            return "PrintLog";
        case g_push_videostream_code:
            return "PushVideoStream";
        default:
            return "Unknow";
        }
    }
};
Init init;

ProcessPool.cc如下:

#include <iostream>
#include <unistd.h>
#include <string>
#include <cassert>
#include <vector>
#include "Task.hpp"
#include <sys/types.h>
#include <sys/wait.h>

static int number = 0; // 管道的编号
const int count = 5;   // 子进程和管道个数

// 用来确定有哪些任务
class Channel
{
public:
    Channel(int fd, pid_t workerid)
        : _fd(fd), _workerid(workerid)
    {
        _name = "channel: " + std::to_string(number++);
    }

public:
    // 管道fd  子进程pid  管道名
    int _fd;
    pid_t _workerid;
    std::string _name;
};

void Work()
{
    while (true)
    {
        int code = 0;                             // 用来规定buffer,读取必须是4个字节,得到任务码
        ssize_t n = read(0, &code, sizeof(code)); // 已经完成输入重定向
        // read读到数据长度n必须等于sizeof(code)
        if (n == sizeof(code)) // 读到正确的code
        {
            if (!init.CheckCode(code)) // 不合法直接continue
                continue;
            init.RunTask(code); // 合法,执行任务,相当于init.tasks[code]()
        }
        else if (n == 0) // 写端关闭,读端继续读取,它将读到管道中的所有数据,直到read返回值为0
        {
            break;
        }
        else
        {
        }
    }
    std::cout << "child quit" << std::endl;
}

void PrintFd(const std::vector<int> &fds)
{
    std::cout << getpid() << " close fds: ";
    for (auto fd : fds)
    {
        std::cout << fd << " ";
    }
    std::cout << std::endl;
}

// 传参形式:
// 1. 输入参数:const &
// 2. 输出参数:*
// 3. 输入输出参数:&
void CreatChannel(std::vector<Channel> *c)
{
    // bug
    // 父进程在不断创建管道时,创建第一个进程,父进程的信道写端已经在文件描述符里,
    // 再创建第二个管道和进程时,除了建立正常的通信信道以外,上一个信道在父进程的写端也会被下一个进程继承,
    // 再创建第三个管道和进程时,这个子进程的文件描述符表将包含指向三个信道。
    // 一直创建管道和进程,只有最后一个创建的管道只有一个写端指向,其它的管道都有多个写端指向。
    // 所以回收时要关闭全部信道写端再wait,如果close和wait同时进行,关闭信道写端从上往下关,关闭后还有无数个进程指向该信道,引用计数不为0,管道不释放,read读不到0,也就阻塞了

    std::vector<int> old;
    for (int i = 0; i < count; i++)
    {
        // 1. 定义并创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 2. 创建进程
        pid_t id = fork();
        assert(id != -1);

        // 3. 构建单向信道
        if (id == 0) // 子进程
        {
            if (!old.empty())
            {
                for (auto fd : old)
                {
                    close(fd); // 把不属于自己的管道的写端关闭
                }
                PrintFd(old);
            }
            close(pipefd[1]);
            dup2(pipefd[0], 0); // 使用dup2后就不用给Work传参了,只用从标准输入拿数据即可
            Work();
            exit(0); // 会自动关闭自己打开的所有的fd
        }

        // 父进程
        close(pipefd[0]);
        c->push_back(Channel(pipefd[1], id)); // 之后对信道的增删查改就变成了对该vector的增删查改
        old.push_back(pipefd[1]);             // 记录父进程的管道写端
    }
}

void SendCommand(const std::vector<Channel> &c, bool flag, int num = -1)
{
    int pos = 0;
    while (true)
    {
        // 1. 选择任务,得到任务码,4字节
        int taskcode = init.SelectTask();

        // 2. 选择信道(进程),轮询或随机,较为平均地将任务给进程,要考虑子进程完成任务的负载均衡
        const auto &channel = c[pos++];
        pos %= c.size();

        // debug 查看任务发送给谁了
        std::cout << "send taskcode " << init.ToDesc(taskcode) << "[" << taskcode << "]"
                  << " in "
                  << channel._name << " worker is : " << channel._workerid << std::endl;

        // 3. 发送任务
        write(channel._fd, &taskcode, sizeof(taskcode));
        // 4. 判断是否退出
        if (!flag)
        {
            num--;
            if (num <= 0)
                break;
        }
        sleep(1);
    }
    std::cout << "SendCommand done..." << std::endl;
}

void ReleaseChannel(const std::vector<Channel> &c)
{
    // 父进程退出了,与信道写端对应的文件描述符自动关闭
    // 写端关闭,读端继续读取,它将读到管道中的所有数据,直到read返回值为0
    for (const auto &channel : c)
    {
        close(channel._fd);
        waitpid(channel._workerid, nullptr, 0);
    }
    // for (const auto &channel : c)
    // {
    //     pid_t rid = waitpid(channel._workerid, nullptr, 0);
    //     if (rid == channel._workerid)
    //     {
    //         std::cout << "wait child: " << channel._workerid << " success" << std::endl;
    //     }
    // }

    // 还有一种方法,不用使用old关闭不属于自己的写端:倒状回收
    // int pos = c.size();
    // for (; pos >= 0; pos--)
    // {
    //     close(c[pos]._fd);
    //     waitpid(c[pos]._workerid, nullptr, 0);
    // }
}

int main()
{
    std::vector<Channel> channels;
    // 创建信道、创建进程
    CreatChannel(&channels);

    // 向不同的管道发送不同任务
    const bool g_always_loop = true;
    // SendCommand(channels,g_always_loop);
    SendCommand(channels, !g_always_loop, 10);

    // 回收资源,子进程退出、释放管道
    ReleaseChannel(channels);
    return 0;
}

4.3 命名管道

命名管道(也称为FIFO)在Linux中是一种特殊的文件类型,它允许不同进程之间通过一个命名的管道进行通信。命名管道在文件系统中有一个可见的名称,可以像普通文件一样访问,但它们的操作方式与匿名管道不同。

命名管道是通过系统调用'mkfifo'创建的,可以用于不具有亲缘关系/毫不相关的进程进行进程间通信。它是一个文件,通常具有特定的扩展名(如'.fifo'  点表示匿名文件),但它实际上并不是文件系统中的普通文件,而是一个特殊的文件。

创建命名管道

  • 命名管道可以从命令行上创建:
    $ mkfifo filename
  • 命名管道也可以从程序里创建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname是命名管道的路径名。
mode是设置命名管道的权限模式,与open函数的mode参数类似。注意与umask的运算
成功返回0,失败返回-1。

命名管道文件是创建出来的磁盘级的符号,实际在进行数据通信时,由于该文件是管道文件,被打开时数据也不会向磁盘刷新。命名管道文件有路径和文件名,因为路径是具有唯一性的,所以,我们可以使用路径+文件名,来唯一的让不同进程看到同一份资源!

创建名为filename的命名管道,使用ll命令,发现命名管道文件类型为p,即管道文件。

匿名管道与命名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。(原理和特征一样)

4.4 用命名管道实现server&client通信

文件:comm.h   client.cc   server.cc   Makefile

Makefile如下:

.PHONY:all
all:clientPipe serverPipe

clientPipe:client.cc
	g++ -o $@ $^ -std=c++11

serverPipe:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f clientPipe serverPipe

comm.h如下:

#pragma once

#define FILENAME "fifo"

client.cc如下:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include "comm.h"
#include <fcntl.h>
#include <cstring>
#include <unistd.h>
#include <string>

int main()
{
    // 打开命名管道
    int fifo_wfd = open(FILENAME, O_WRONLY);
    if (fifo_wfd < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        exit(0);
    }
    std::cout << "open fifo success-------write" << std::endl;

    // 向管道写入数据
    std::string message;
    while (true)
    {
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);
        ssize_t num = write(fifo_wfd, message.c_str(), message.size());
        if (num < 0)
        {
            std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
            break;
        }
    }
    close(fifo_wfd);
    std::cout << "close fifo success..." << std::endl;

    return 0;
}

server.cc 如下:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include "comm.h"
#include <fcntl.h>
#include <cstring>
#include <unistd.h>

// 创建命名管道
bool MakeFifo()
{
    int n = mkfifo(FILENAME, 0666);
    if (n < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return false;
    }
    std::cout << "mkfifo success-------read" << std::endl;
    return true;
}
int main()
{
Start:
    // 不管有没有管道,直接打开命名管道,有管道就会返回fifo_rfd
    int fifo_rfd = open(FILENAME, O_RDONLY);
    if (fifo_rfd < 0)//没有管道就创建,然后再次打开
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        if(MakeFifo()) goto Start;
        else return 1;
    }
    std::cout << "open fifo success-------read" << std::endl;

    // version 1  命名管道创建后再运行serverPipe会提示管道文件已存在
    //  // 创建命名管道
    //  int n = mkfifo(FILENAME, 0666);
    //  if (n < 0)
    //  {
    //      std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
    //      exit(0);
    //  }
    //  std::cout << "mkfifo success-------read" << std::endl;

    // // 打开命名管道
    // int fifo_rfd = open(FILENAME, O_RDONLY);
    // if (fifo_rfd < 0)
    // {
    //     std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
    //     exit(0);
    // }
    // std::cout << "open fifo success-------read" << std::endl;

    // 从管道读数据
    char buffer[1024];
    while (true)
    {
        ssize_t num = read(fifo_rfd, buffer, sizeof(buffer) - 1);
        if (num > 0)
        {
            buffer[num] = 0; // 或等于'\0'
            std::cout << "Client say: " << buffer << std::endl;
        }
        else if (num == 0)
        {
            std::cout << "client quit, server quit too!" << std::endl;
            break;
        }
    }
    close(fifo_rfd);
    std::cout << "close fifo success..." << std::endl;

    return 0;
}

五、system V共享内存

5.1 system V共享内存的引入

管道不是为了通信而专门设置的一套方案,而是为了通信复用了之前的代码。而实际上OS在通信时场景很多,只有一种通信方式是不够的,因此,操作系统提供了多种IPC机制,包括但不限于:

  • 管道(Pipe)和命名管道(FIFO):用于单向数据流通信。
  • 消息队列(Message Queue):允许一个或多个进程向队列中写入消息,其他进程则可以读取队列中的消息。
  • 信号量(Semaphore):用于同步进程间的访问共享资源。
  • 共享内存(Shared Memory):允许多个进程共享一段内存区域,是最快的IPC方式,因为它不需要数据复制。
  • 套接字(Socket):提供了在网络上的不同主机间进行通信的能力,也可以用于同一主机上的不同进程间通信。

System V共享内存是操作系统中提供的一种IPC机制,它允许不同的进程访问同一块内存区域,从而实现数据共享。

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

5.2 共享内存的原理

  • 共享内存允许多个进程共享一段内存区域,而且共享内存段是物理内存中的一部分。
  • 在物理内存新增共享内存段时,要对共享内存段先描述再组织,即使用struct shmid_ds描述了共享内存段的属性,如大小、访问权限、创建者信息等。通过链表进行对共享内存段的管理。
  • 共享内存的创建是进程发起的。每个进程在Linux内核中都有一个task_struct结构来表示,这个结构包含了进程的所有信息,其中包括它的地址空间。地址空间被分为多个部分,包括代码段、数据段、堆、栈、共享区等。
  • 每个进程都有自己的页表,通过页表可以将虚拟地址翻译成物理地址。在新增共享内存时,要在页表中进行映射,共享内存被映射到进程地址空间的共享区中,并向上层返回所在共享区的起始地址,使得进程可以通过地址空间,像访问自己的内存一样访问共享内存。
  • 在使用System V共享内存时,每个共享内存段都有一个唯一的键(key),用于在进程间标识和访问共享内存段。内核使用这个键来查找或创建对应的struct shmid_ds。

OS中会存在很多进程,这些进行都有可能申请和使用共享内存,OS一定会允许系统中同时存在多个共享内存。共享内存,也要被操作系统管理,管理的方法就是先描述再组织,即上面讲到的struct shmid_ds结构体。但是上面的步骤只是一个进程创建共享内存,那么如何保证第二个之后的参与通信的进程,看到的就是同一个共享内存呢?

注意,进程不能直接给另一个进程直接传值,因为如果这样就说明已经能通信了,就不需要共享内存来传递消息了。所以进程不能将key传给另一个进程。方法:提前进行约定,让使用同一块共享内存的进程使用相同的key,这个key可以用户自己定义,也可以使用库方法,只要保证key唯一即可。

5.3 共享内存函数

shmget函数:既能创建也能获取

shmget函数用于创建一个新的共享内存段或者获取一个已经存在的共享内存段的标识符。

原型:

        #include <sys/ipc.h>
        #include <sys/shm.h>

        int shmget(key_t key, size_t size, int shmflg);

参数:

  • key:一个键值,用于唯一标识共享内存段。在创建共享内存时就将key加载到其中。
  • size:共享内存的大小。
  • shmflg:一个标志位,用于控制共享内存的创建和访问权限。(使用方法类似open的flags标志位)

返回值:

        成功时,返回共享内存段的标识符shmid;失败时,返回-1。

补充:

1. shmflg参数中常用的标志位

  • IPC_CREAT:如果这个标志位被设置,并且共享内存段不存在,那么shmget函数会创建一个新的共享内存段。如果共享内存段已经存在,shmget会返回已存在的共享内存段的标识符。
  • IPC_EXCL:这个标志位必须与IPC_CREAT标志位一起使用。如果IPC_CREAT和IPC_EXCL都被设置,并且共享内存段不存在,shmget函数会创建一个新的共享内存段。如果共享内存段已经存在,shmget函数会失败,并返回-1。用来保证共享内存段是新创建的。
  • mode:这个值通常作为shmflg参数的低位部分,它表示共享内存段的权限模式。例如0666。
  • 示例:int shmid = shmget(11223344, 4096, IPC_CREAT | 0666);

2. ftok函数来生成一个键值

  • #include <sys/types.h>
    #include <sys/ipc.h>
    key_t ftok(const char *pathname, int proj_id);
  • 用户可以通过ftok函数来生成一个键值,这个键值通常基于一个路径名和一个项目ID。ftok函数返回一个整数,这个整数就是用于shmget函数的key参数。
  • 示例:
    key_t key;
    key = ftok("path/to/file", 1); // "path/to/file"是文件路径,1是项目ID
    int shmid = shmget(key, 4096, IPC_CREAT | 0666);
  • 因为用户定义的key不容易保证唯一性,所以使用ftok函数获取key。(相同的参数相同的算法,最终得到相同的值)

注意:

  1. key和shmid的区别
    key是操作系统用来区分共享内存段的,shmid是用户用来进行对共享内存段的操作的。下面的shmat、shmctl都是使用shmid来对指定的共享内存段操作。包括命令行指令也是通过shmid进行操作。
  2.  共享内存(IPC资源)的生命周期是随内核的!共享内存需要用户主动释放,除非重启OS
    ipcs -m shmid 命令,查看有多少共享内存
    ipcrm -m 命令,删除指定的共享内存

shmat函数:at->attach建立关联

shmat函数用于将共享内存段连接到进程的地址空间。

原型:

        #include <sys/types.h>
        #include <sys/shm.h>

        void *shmat(int shmid, const void *shmaddr, int shmflg);

参数:

  • shmid:共享内存段的标识符。
  • shmaddr:指定连接的地址,如果为NULL,内核将自动选择地址。
  • shmflg:连接标志,可以指定读写权限等。

返回值:
        成功时,返回指向共享内存的指针,映射到地址空间的起始虚拟地址;失败时,返回-1。

说明:

  • shmaddr为NULL,核心自动选择一个地址
  • shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
  • shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
  • shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

shmdt函数:dt->detach去关联

shmdt函数用于将共享内存段与当前进程的地址空间脱离,即解除映射。

原型:

        #include <sys/types.h>
        #include <sys/shm.h>

        int shmdt(const void *shmaddr);

参数:

  • shmaddr:由shmat返回的指针。

返回值:
        成功时,返回0;失败时,返回-1。

注意:将共享内存段与当前进程脱离不等于删除共享内存段。只是将页表中与共享内存段的映射清空。

什么时候删除共享内存?

struct shmid_ds中有shm_nattch字段,它是一个引用计数器,表示有多少个进程正在使用这个共享内存段。当一个进程使用shmat函数将共享内存段映射到自己的地址空间时,shm_nattch的值会增加;当进程使用shmdt函数将共享内存段从自己的地址空间脱离时,shm_nattch的值会减少。

当shm_nattch的值降至0时,意味着没有进程在使用这个共享内存段。在这种情况下,内核会考虑删除共享内存段,但还需要满足其他条件,比如共享内存段没有被其他进程以只读方式映射。只有当所有使用该共享内存段的进程都调用了shmdt函数后,操作系统才会删除共享内存段。

shmctl函数:ctl->control 

shmctl函数用于控制共享内存,如删除共享内存段、改变共享内存的权限等。

原型:

        #include <sys/ipc.h>
        #include <sys/shm.h>

        int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数:

  • shmid:共享内存段的标识符。
  • cmd:将要采取的动作,如删除(IPC_RMID)、改变权限(IPC_SET)等。
    IPC_STAT:获取共享内存段的当前状态,并将其存储在buf指向的struct shmid_ds结构体中。
    IPC_SET:设置共享内存段的当前状态,并从buf指向的struct shmid_ds结构体中读取信息。
    IPC_RMID:删除共享内存段,释放系统资源。(remove id 或remove immediately)
  • buf:指向一个'struct shmid_ds'结构体,该结构体包含共享内存的属性信息。

返回值:
        成功时,返回0;失败时,返回-1。

5.4 使用共享内存的步骤

  1. 生成key:通过ftok函数来生成一个键值,基于一个路径名和一个项目ID。
  2. 创建共享内存段:使用shmget函数,指定key和共享内存的大小及其他属性来创建一个新的共享内存段或者获取一个已经存在的共享内存段的标识符。内核会创建一个struct shmid_ds来描述这个共享内存段,并在文件系统中创建一个对应的特殊文件。
  3. 映射共享内存段:使用shmat函数,将共享内存段映射到进程的地址空间中。内核会更新进程的页表,将共享内存的虚拟地址映射到物理内存的页面。
  4. 访问共享内存:进程可以使用指针操作来读取和写入共享内存中的数据。当进程访问共享内存时,它的页表会将虚拟地址翻译成物理地址,从而访问共享内存的物理页面。
  5. 解除映射:当进程完成共享内存的使用后,应该使用shmdt函数来解除映射。内核会更新进程的页表,取消共享内存的虚拟地址到物理地址的映射。
  6. 删除共享内存段:如果共享内存不再需要,可以使用shmctl函数来标记删除。内核会删除对应的struct shmid_ds,并在文件系统中删除对应的特殊文件。

5.5 基于共享内存的进程间通信示例

文件:comm.hpp   client.cc   server.cc   Makefile

Makefile如下:

.PHONY:all
all:clientPipe serverPipe

clientPipe:client.cc
	g++ -o $@ $^ -std=c++11

serverPipe:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f clientPipe serverPipe

comm.hpp如下:

#pragma once

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


const char *pathname = "/home/zzx/2024/0604/shm";
const int projectID = 1111;    // 项目ID
const int Size = 4096;         // 文件大小
const char *filename = "fifo"; // 命名管道

key_t GetKey()
{
    return ftok(pathname, projectID);
}

// int CreateShm(key_t key)
// {
//     int shmid = shmget(key, Size, IPC_CREAT | 0666);
//     if(shmid < 0)
//     {
//         std::cerr << "errno" << errno << ",errnostring: " << strerror(errno) << endl;
//         exit(2);
//     }
//     return shmid;
// }

int __CreateOrGetShm(key_t key, int flag)
{
    int shmid = shmget(key, Size, flag);
    if (shmid < 0)
    {
        std::cerr << "errno" << errno << ",errnostring: " << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(key_t key)
{
    return __CreateOrGetShm(key, IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm(key_t key)
{
    return __CreateOrGetShm(key, IPC_CREAT /*0也可以*/);
}

bool MakeFifo()
{
    int n = mkfifo(filename, 0666);
    if (n < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return false;
    }
    std::cout << "mkfifo success... read" << std::endl;
    return true;
}

client.cc如下:

#include "comm.hpp"

int main()
{
    // 使用共享内存
    key_t key = GetKey();
    int shmid = GetShm(key);
    std::cout << "GetShm success --- client" << std::endl;
    char* shmaddr = (char*)shmat(shmid, nullptr,0);
    std::cout << "attach success --- client" << std::endl;
    int fd = open(filename,O_WRONLY);
    char c = 'a';
    while (c < 'z')
    {
        shmaddr[c-'a'] = c;
        std::cout << "write: " << shmaddr << std::endl;
        sleep(1);

        int code = 1;//只是通知作用,用来同步
        write(fd,&code,sizeof(code));
        c++;
    }
    shmdt(shmaddr);
    close(fd);
    return 0;
}

server.cc如下:

#include "comm.hpp"

class Init
{
public:
    Init()
    {
        // 创建管道文件,复用同步机制
        bool r = MakeFifo();
        if (!r)
            return;

        // 创建共享内存
        key_t key = GetKey();
        shmid = CreateShm(key); // 封装了底层接口,其它函数也可以这样实现,在此不作实现
        std::cout << "CreateShm success --- server" << std::endl;

        // 与进程地址空间进行关联
        shmaddr = (char *)shmat(shmid, nullptr, 0);
        std::cout << "shmat success --- server" << std::endl;
        fd = open(filename, O_RDONLY);
    }

    ~Init()
    {
        // 与进程地址空间去关联
        shmdt(shmaddr);
        std::cout << "shmdt success --- server" << std::endl;

        // 删除共享内存
        shmctl(shmid, IPC_RMID, nullptr);
        std::cout << "shmctl success --- server" << std::endl;
    }

public:
    int fd;
    int shmid;
    char *shmaddr;
};

int main()
{
    Init init;
    while (true)
    {
        int code = 0;
        ssize_t n = read(init.fd, &code, sizeof(code));
        if (n > 0)
        {
            std::cout << "共享内存的内容:" << init.shmaddr << std::endl;
        }
        else if (n == 0)
        {
            break;
        }
    }

    return 0;
}

5.6 共享内存的特点

  1. 共享内存的通信方式,不会提供同步机制,共享内存是直接裸露给所有的使用者的,一定要注意共享内存的使用安全问题。
  2. 共享内存是所有进程间通信,速度最快的。
  3. 共享内存可以提供较大的空间

共享内存通信速度快是因为它减少了数据拷贝次数。在使用管道传递数据时要先创建管道,然后不同端向管道写入或读取数据,调用write或read等系统调用。在计算机中,凡是数据迁移,都是对数据的拷贝。用户通过进程A将数据写到管道,进程B从管道读出数据写入显示器,用户把数据传给进程A,进程B把数据打印到显示器文件也都用到了拷贝,拷贝也有代价。

使用共享内存,用户把数据传给进程A,就直接传到了共享内存中,数据一旦进入共享内存,进程B立即就能知道(因为没有同步机制),进程B直接共享区数据传给显示器,中间就至少减少两次系统调用(write, read)。

简而言之,在传统的IPC机制中,如管道,数据需要经过以下步骤:

  • 用户空间到内核空间:用户通过系统调用(如write)将数据从用户空间拷贝到内核空间。
  • 内核空间到内核空间:数据在内核空间之间传递,可能需要通过网络堆栈、文件系统等。
  • 内核空间到用户空间:数据从内核空间拷贝到用户空间,通过系统调用(如read)被进程读取

这个过程涉及了多次数据拷贝,并且每次拷贝都会带来一定的开销。

相比之下,共享内存通信的过程是这样的:

  • 用户空间到共享内存:用户进程将数据写入共享内存。
  • 共享内存到用户空间:另一个进程从共享内存中读取数据。

在这个过程中,只有两次数据拷贝

5.7 共享内存数据结构

上面讲到的shmid_ds结构体,包括buf参数也使用一个指向shmid_ds结构的指针,shmid_ds结构体在<sys/shm.h>中定义如下:

struct shmid_ds {
    struct ipc_perm shm_perm;    /* Ownership and permissions */
    size_t          shm_segsz;   /* Size of segment (bytes) */
    time_t          shm_atime;   /* Last attach time */
    time_t          shm_dtime;   /* Last detach time */
    time_t          shm_ctime;   /* Last change time */
    pid_t           shm_cpid;    /* PID of creator */
    pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
    shmatt_t        shm_nattch;  /* No. of current attaches */
    ...
};

ipc_perm结构定义如下(突出显示的字段可以使用IPC_SET设置):

struct ipc_perm {
    key_t          __key;    /* Key supplied to shmget(2) */
    uid_t          uid;      /* Effective UID of owner */
    gid_t          gid;      /* Effective GID of owner */
    uid_t          cuid;     /* Effective UID of creator */
    gid_t          cgid;     /* Effective GID of creator */
    unsigned short mode;     /* Permissions + SHM_DEST and
                              SHM_LOCKED flags */
    unsigned short __seq;    /* Sequence number */
};

从中可以看到shmid_ds结构体的首元素是一个结构体ipc_perm,它包含创建共享内存段时提供的键值。

要想了解shmid_ds和ipc_perm就要介绍一下system V消息队列和system V信号量

六、简述system V消息队列和system V信号量

6.1 system V消息队列

消息队列的特性:

  1. 消息队列提供了一个从一个进程向另外一个进程发送一个数据块的方法。这个数据块也叫消息。
  2. 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。接收者进程可以指定它只接收特定类型的消息。这允许不同的消息可以同时存在于队列中,而不需要接收者知道队列中有哪些类型的消息。例如进程A要求进程B能看到,类型就设置为B。
  3. 每个消息队列都有一个唯一的标识符msqid,用于在系统中标识和访问该队列。
  4. 与共享内存段类似,消息队列也可以通过键来标识,用于在系统中唯一标识消息队列。
  5. 与System V的其他IPC资源一样,消息队列需要显式地删除,否则不会自动清除,除非重启,所以system V 消息队列资源的生命周期随内核。
  6. 系统中可以同时存在多个消息队列,消息队列在内核中管理,也要先描述,再组织,因此消息队列=队列+队列的属性。

System V消息队列函数:

msgget:创建或获取一个消息队列标识符。
原型:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>
       int msgget(key_t key, int msgflg);
参数:

  •   key:用于标识消息队列的键值,可以是一个已存在的键值或者通过ftok函数生成的键值。
  •   msgflg:标志位,用于控制消息队列的创建和访问权限。

返回值:成功时返回消息队列标识符,失败时返回-1。
注意:msgflg参数可以设置权限标志,如IPC_CREAT(创建消息队列)、IPC_EXCL(创建时检查消息队列是否存在)等。用法与System V共享内存shmget函数的shmflg参数相同。

msgctl:控制消息队列。
原型:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>
       int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:

  •   msqid:消息队列标识符。
  •   cmd:操作命令,如IPC_STAT(获取消息队列状态)、IPC_SET(设置消息队列属性)、IPC_RMID(删除消息队列)等。
  •   buf:指向struct msqid_ds的指针,用于存储消息队列的状态信息。

返回值:成功时返回0,失败时返回-1。

msgsnd:向消息队列发送消息。
原型:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>
       int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:

  •   msqid:消息队列标识符。
  •   msgp:指向消息的指针。
  •   msgsz:消息的大小。
  •   msgflg:标志位,用于控制发送操作的行为。

返回值:成功时返回0或消息大小,失败时返回-1。

msgrcv:从消息队列接收消息。
原型:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>
       ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:

  •   msqid:消息队列标识符。
  •   msgp:指向接收消息缓冲区的指针。
  •   msgsz:接收缓冲区的大小。
  •   msgtyp:接收消息的类型值。
  •   msgflg:标志位,用于控制接收操作的行为。

返回值:成功时返回接收到的消息大小,失败时返回-1。

msgsnd和msgrcv函数的msgflg参数可以设置阻塞标志,如MSG_EXCEPT(接收除指定类型外的消息)、MSG_NOERROR(如果接收消息失败,返回-1而不是设置错误码)等。

msqid_ds数据结构定义如下:

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) */
};

ipc_perm结构定义如下:

struct ipc_perm {
    key_t          __key;       /* Key supplied to msgget(2) */
    uid_t          uid;         /* Effective UID of owner */
    gid_t          gid;         /* Effective GID of owner */
    uid_t          cuid;        /* Effective UID of creator */
    gid_t          cgid;        /* Effective GID of creator */
    unsigned short mode;        /* Permissions */
    unsigned short __seq;       /* Sequence number */
};

6.2 system V信号量

System V信号量函数也有semget、semctl、semop函数,在此不讲述。它们用法也和共享内存、消息队列类似,因为都是system V系列的。

semid_ds结构体定义如下:

struct semid_ds {
    struct ipc_perm sem_perm;  /* Ownership and permissions */
    time_t          sem_otime; /* Last semop time */
    time_t          sem_ctime; /* Last change time */
    unsigned long   sem_nsems; /* No. of semaphores in set */
};

ipc_perm定义如下:

struct ipc_perm {
    key_t          __key; /* Key supplied to semget(2) */
    uid_t          uid;   /* Effective UID of owner */
    gid_t          gid;   /* Effective GID of owner */
    uid_t          cuid;  /* Effective UID of creator */
    gid_t          cgid;  /* Effective GID of creator */
    unsigned short mode;  /* Permissions */
    unsigned short __seq; /* Sequence number */
};

 信号量的本质是一组计数器。信号量主要用于同步和互斥。

为了让进程间能够通信,就要让多个进程看到同一份资源,这份资源称为公共资源,使用公共资源就可能导致并发访问、数据不一致问题,例如读的时候另一个进程读、读的时候另一个进行写、写的时候。。。所以就需要在一个进程使用资源的时候,将这份资源保护起来,所有进程按顺序使用,这就是互斥和同步。

互斥:任何一个时刻只允许一个执行流(进程)访问公共资源,(加锁实现的)

同步:多个执行流执行时,按照一定的顺序执行。

临界资源:被保护起来的公共资源。(不是临界资源的就是非临界资源)

临界区:访问该临界区的代码。(维护临界资源就是维护临界区)

原子性:只有两态,要么没做,要么做完。

比如在电影院买票,电影院和内部座位就是多人共享的资源 --- 公共资源(可能被拆分为多份资源)。我们买票的本质:是对资源的预订机制。可以看成,电影院有一个计数器用来表示公共资源的个数。别人买票时要先看计数器内还有没有剩余的座位,有的话就分配,计数器--,没有就让那人等着。

如果公共资源没有被拆分只有一份,用二元信号量int sem =1表示互斥锁来完成互斥功能,在临界区前面和后面加上维护代码,检测sem是否有剩余,如果有剩余就允许继续临界区的代码、sem--,没有剩余就继续等待,直至有一个临界区完成并sem++。其实这个信号量也可以看作一个结构体,里面有一个计数器和一个等待队列,没有剩余就将进程放入等待队列中,知道有一个sem++,就执行等待队列的下一个进程。

信号量:表示资源数目的计数器,每一个执行流想访问公共资源内的某一份资源,不应该让执行流直接访问,而是先申请信号量资源,其实就是先对信号量计数器进行--操作。本质上,只要--成功,完成了对资源的预订机制,如果申请不成功,执行流被挂起阻塞。

七、回顾共享内存数据结构

在看到共享内存、消息队列和信号量的数据结构后,发现它们都使用了ipc_perm结构体,而且都是位于对应数据结构的第一个,这是因为在底层中,在系统层面有一个类型为kern_ipc_perm *p[0]的柔性指针数组,通过该数组管理所有的IPC资源。例如创建一个共享内存的数据结构shmid_ds,在柔性指针数组中加上对应的ipc_perm结构体的地址,将来对shmid_ds进行管理时,由于ipc_perm结构体是shmid_ds第一个元素,所以只需要对它进行类型转换,就可以变成shmid_ds的地址,就可以对它的数据成员进行操作。例如(shmid_ds *)p[1] 、 (msqid_ds*)p[2]。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部