Linux —— 进程间通信 - system V进程间通信 - 共享内存(2)

目录

[1. 共享内存的属性:](#1. 共享内存的属性:)

[2. 共享内存的特征总结:](#2. 共享内存的特征总结:)

[3. system V消息队列](#3. system V消息队列)

消息队列的原理:

[4. system V信号量](#4. system V信号量)

[4.1 并发编程,概念铺垫](#4.1 并发编程,概念铺垫)

[4.2 信号量](#4.2 信号量)

[4.2.1 概念理解](#4.2.1 概念理解)

[4.2.2 为什么要有信号量](#4.2.2 为什么要有信号量)

[4.2.3 信号量的操作](#4.2.3 信号量的操作)

[4.2.4 结论](#4.2.4 结论)

[4.2.5 那么OS是如何管理所有的IPC资源的呢?](#4.2.5 那么OS是如何管理所有的IPC资源的呢?)


1. 共享内存的属性:

设计一个接口,来获取共享内存的属性:

IPC_STAT:获取共享内存的状态。可以将内核相关的属性拷贝到指定的结构体(struct shmid_ds *buf)当中。

cpp 复制代码
//Shm.hpp
    void PrintAttr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        if(n < 0)
        {
            perror("shmctl");
            return;
        }

        //打印输出shm的相关属性
        printf("key:0X%x\n",ds.shm_perm.__key);
        printf("size:%ld\n",ds.shm_segsz);
        printf("atime:%lu\n",ds.shm_atime);
        printf("nattach:%ld\n",ds.shm_nattch);
    }
cpp 复制代码
// server.cpp

#include "Shm.hpp"

int main()
{
    SharedMemory shm;
    shm.Create();
    // sleep(5);

    shm.Attach();
    // sleep(5);

    shm.PrintAttr();
    sleep(3);

    //使用  ---- 一次读一个字符
    while(true)
    {
        char c;
        shm.PopChar(&c);

        printf("server get char:%c\n",c);

        sleep(1);
    }

    shm.Detach();
    // sleep(5);

    shm.RemoveShm();
    // sleep(5);
    return 0;
}
cpp 复制代码
//client.cpp

#include "Shm.hpp"

int main()
{
    SharedMemory shm;
    shm.Get(); // 获取已经存在的共享内存
    // sleep(5);

    shm.Attach();
    // sleep(5);

    // 使用 --- 一次放一个字符
    char c = 'A';
    for (; c <= 'Z'; c++)
    {
       shm.AddChar(c);
       sleep(1);
    }

    shm.Detach();
    // sleep(5);

    // sleep(5);
    return 0;
}

运行结果:

证明:在系统中,共享内存也是要被系统管理的,共享内存内部也是要有描述共享内存相关的内核数据结构的。正是因为有描述共享内存属性的数据结构,所以才可以通过shmctl这样的接口去获取共享内存的相关的属性。

2. 共享内存的特征总结:

根据共享内存的第三个特点,所以对共享内存进行读写时是很容易形成并发问题的!!后续会提到信号量就可以实现共享内存的同步或互斥相关的保护了。以及后面的多线程会专门介绍同步和互斥。现在的阶段,我们用进程池来解决,进程池是通过管道来控制指定的进程的。

但是实际上是不会这样做的,仅仅是为了理解,让进程执行产生一定的顺序性。管道为空,读端就不能读,就必须让写端去写。管道满了的话,写端就不能写,就让读端去读。这种顺序性就叫做进程/执行流之间的同步。

cpp 复制代码
// Comm.hpp
#pragma once
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cassert>
#include <cstdio>
#include <ctime>
#include <cstring>
#include <iostream>
using namespace std;
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
    "Debug",
    "Notice",
    "Warning",
    "Error"};
std::ostream &Log(std::string message, int level)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << "
        | " << message;
        return std::cout;
}
#define PATH_NAME "/home/sy"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 // 共享内存的大小,最好是页(PAGE:4096)的整数倍
#define FIFO_NAME "./fifo"
class Init
{
public:
    Init()
    {
        umask(0);
        int n = mkfifo(FIFO_NAME, 0666);
        assert(n == 0);
        (void)n;
        Log("create fifo success", Notice) << "\n";
    }
    ~Init()
    {
        unlink(FIFO_NAME);
        Log("remove fifo success", Notice) << "\n";
    }
};
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(std::string pathname, int flags)
{
    int fd = open(pathname.c_str(), flags);
    assert(fd >= 0);
    return fd;
}
void CloseFifo(int fd)
{
    close(fd);
}
//对方不写,也就卡在这里
void Wait(int fd)
{
    Log("等待中....", Notice) << "\n";
    uint32_t temp = 0;
    ssize_t s = read(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
}
void Signal(int fd)
{
    uint32_t temp = 1;
    ssize_t s = write(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
    Log("唤醒中....", Notice) << "\n";
}
string TransToHex(key_t k)
{
    char buffer[32];
    snprintf(buffer, sizeof buffer, "0x%x", k);
    return buffer;
}
cpp 复制代码
// ShmServer.cc
#include "Comm.hpp"
Init init;
int main()
{
    // 1. 创建公共的key值
    key_t k = ftok(PATH_NAME, PROJ_ID);
    assert(k != -1);
    Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;
    // 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid == -1)
    {
        perror("shmget");
        exit(1);
    }
    Log("create shm done", Debug) << " shmid : " << shmid << endl;
    // 3. 将指定的共享内存,挂接到自己的地址空间
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    Log("attach shm done", Debug) << " shmid : " << shmid << endl;
    // 4. 访问控制
    int fd = OpenFIFO(FIFO_NAME, O_RDONLY);
    while (true)
    {
        // 阻塞
        Wait(fd);
        // 临界区
        printf("%s\n", shmaddr);
        if (strcmp(shmaddr, "quit") == 0)
            break;
    }
    CloseFifo(fd);
    // 5. 将指定的共享内存,从⾃⼰的地址空间中去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    (void)n;
    Log("detach shm done", Debug) << " shmid : " << shmid << endl;
    // 6. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
    n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
    Log("delete shm done", Debug) << " shmid : " << shmid << endl;
    return 0;
}
cpp 复制代码
// ShmClient.cc
#include "Comm.hpp"
int main()
{
    // 1. 创建公共的key值
    key_t k = ftok(PATH_NAME, PROJ_ID);
    if (k < 0)
    {
        Log("create key failed", Error) << " client key : " << TransToHex(k)
                                        << endl;
        exit(1);
    }
    Log("create key done", Debug) << " client key : " << TransToHex(k) << endl;
    // 2. 获取共享内存
    int shmid = shmget(k, SHM_SIZE, 0);
    if (shmid < 0)
    {
        Log("create shm failed", Error) << " client key : " << TransToHex(k)
                                        << endl;
        exit(2);
    }
    Log("create shm success", Error) << " client key : " << TransToHex(k) << endl;
    // 3. 挂接共享内存
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    if (shmaddr == nullptr)
    {
        Log("attach shm failed", Error) << " client key : " << TransToHex(k)
                                        << endl;
        exit(3);
    }
    Log("attach shm success", Error) << " client key : " << TransToHex(k) << endl;
    // 4. 写
    int fd = OpenFIFO(FIFO_NAME, O_WRONLY);
    while (true)
    {
        ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
        if (s > 0)
        {
            shmaddr[s - 1] = 0;
            Signal(fd);
            if (strcmp(shmaddr, "quit") == 0)
                break;
        }
    }
    CloseFifo(fd);
    // 5. 去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    Log("detach shm success", Error) << " client key : " << TransToHex(k) << endl;
    return 0;
}

3. system V消息队列

消息队列的原理:

获取消息队列的命令:msgget
删除消息队列了:msgctl

查看消息队列的命令:ipcs -q
删除消息队列的 命令:ipcrm -q msgid

但是,消息队列毕竟不是共享内存,毕竟提供的是内核级队列,毕竟还是要通过系统调用往里面写,从上面读。

消息队列的发和收:msgsed 和 msgrcv

所以要用消息队列来进行通信时,是需要你自己要定义一个结构体:struct msgbuf

4. system V信号量

4.1 并发编程,概念铺垫

  • 多个执行流(进程),能看到的同一份公共资源:共享资源**【并发访问共享资源,导致数据访问不一致!!】**

匿名管道、命名管道、共享内存、消息队列都是共享资源,资源共享了,但是没有对资源进行一定的保护,就好比共享内存,没有对共享内存进行保护,甚至有两个进程对共享内存同时写,没有将这两个进程的写入操作没有分开的话,让他们自由发挥的话,各自写,就极有可能出现前面写着,后面将刚写的数据给覆盖了,最终二者的消息互相覆盖。还有就是一方正在写,未写完,另一方就来读了,读到的内容只有一部分。这种情况因为并发访问公共资源导致的数据不一致的问题。

为什么会有共享资源呢?因为进程要通信。为什么进程要通信??因为多进程要协作。所以思路是:

因为多进程间要协作 -> 所以进程要通信 -> 进程要通信,就必须得进程看到同一份资源 -> 如果没有保护好 -> 所以就会造成数据不一致问题。管的好的,例如管道。

  • 被保护起来的共享资源叫做临界资源
  • 常见的保护的方式:互斥和同步
  • 任何时刻,只允许一个执行流访问资源,叫做互斥
  • 多个执行流,访问临界资源的时候,具有一定的顺序性,叫做同步

例子:食堂排序打饭;管道写满了就得等,让读的一端进行;管道读空了,让写的一端进行,让读的一端等待,具有一定的顺序性,都是同步的体现。

  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源
  • 在进程中涉及到互斥资源的程序段叫临界区。你写的代码 = 访问临界资源的代码(临界区) + 不访问临界资源的代码(非临界区)

访问临界资源的代码(临界区):管道的创建、删除;共享内存的创建、连接、去关联、移除

不访问临界资源的代码(非临界区):for循环、sleep、printf

  • 所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护(也就是对临界区进行保护)

4.2 信号量

4.2.1 概念理解

在现实生活中,很多公共资源被整体使用,也有很多公共资源被局部使用:

特性方面:

  • IPC资源必须删除,否则不会自动删除,除非重启,所以system V IPC资源的声明周期随内核

理解方面:

  • 信号量:描述临界资源,资源数目的计数器

作用方面:

  • 保护临界区

本质方面:

  • 信号量是对资源的预订机制

操作方面:

  • 申请资源,计数器--,P操作
  • 释放资源,计数器++,V操作

4.2.2 为什么要有信号量

  • 信号量是用来保护共享资源的 --- 共享内存

4.2.3 信号量的操作

信号量的创建:semget
信号量的删除:semctl
信号量的初始化:semctl

查看信号量的命令:ipcs -s
删除信号量的命令:ipcrm -s semid
获得所有的IPC资源命令:ipcs

删除指定的IPC资源命令:icprm -m/-s/-q shmid

4.2.4 结论

所有的system V资源,生命周期,全部都随内核!!
所有的system V资源,都要被OS管理起来!!

systemV OS是专门设计的进程间通信的模块!!

4.2.5 那么OS是如何管理所有的IPC资源的呢?

那么柔性数组的场景是什么呢?

系统内部IPC资源本来就不是固定的,进程间通信的使用场景增多,有的使用消息队列、共享内存、信号量,资源越来越多,一定是变化的,如果单独设计了一个通信模块,用一种固定长度的数组来表示的话,定少了怕不够,定多了怕浪费。所以将IPC资源设计成了柔性数组,意味着IPC资源的数组可以随时扩容。

信号量、消息队列、共享内存的底层实现:

信号量:

消息队列:

共享内存:

好啦,到这里,进程间通信(IPC)的内容就全部分享完啦!🎉

如果你在阅读过程中发现了任何错误或写得不够清楚的地方,欢迎随时指出,我们一起交流进步~

下一篇见!👋

相关推荐
烛衔溟1 小时前
TypeScript 类实现接口
linux·ubuntu·typescript
小则又沐风a1 小时前
深入了解进程概念 第二章
java·linux·服务器·前端
CCPC不拿奖不改名1 小时前
PostgreSQL数据库部署linux服务器流程
linux·服务器·数据库·windows·python·docker·postgresql
lzh200409191 小时前
手搓一个简易 Linux 进程池:巩固进程知识
linux·c++
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_60:(表单与按钮技能测试实战)
服务器·前端·javascript·数据库·ui·html
xiaoye-duck1 小时前
《Linux系统编程》Linux基础开发工具 (一):软件包管理器yum/apt,编辑器Vim,编译器GCC/G++
linux
William.csj1 小时前
Linux——服务器后台运行程序指南(包含 Python 与 .sh 脚本实战)
linux·服务器·python
秦歌6661 小时前
Agent Skills详解
服务器·前端·数据库
杨云龙UP2 小时前
MySQL主库高峰期备份引发504故障:从库手动切换接管 + 主从恢复同步 + Docker版DB2重启实战_2026-05-17
linux·运维·数据库·mysql·docker·容器·centos