【Linux】详解IPC:共享内存

目录

IPC

共享内存

1.理解

2.运用

1. 创建 ipc - shmget

2. 创建 key - ftok

⭕shmid vs key

3. 连接 - shmat

4. 脱离 - shmdt

5. 控制/删除 - shmctl

总结

代码示例

3.实验

comm.hpp

processa.cc

processb.cc

log.hpp

makefile

测试

4.思考


IPC

进程间通信 IPC有多种实现方式,包括但不限于:

  1. 管道(Pipes):包括匿名管道和命名管道(FIFOs)。匿名管道只能在具有亲缘关系的进程之间使用,而命名管道可以用于任意进程间的通信。借助文件
  2. 共享内存(Shared Memory):允许两个或多个进程访问同一块内存区域,从而实现快速的数据交换。借助物理内存映射
  3. 消息队列(Message Queues):允许进程之间通过消息队列传递数据,消息可以是任意类型的数据,包括文本和二进制数据。借助物理内存映射
  4. 信号量(Semaphores):用于进程间的同步和互斥访问共享资源。信号量可以是二进制信号量(用于互斥访问)或计数信号量(用于资源的计数控制)。
  5. 套接字(Sockets):通常用于网络通信,但在某些操作系统中也用于进程间通信。
  6. 文件映射(Memory Mapped Files):允许进程通过内存地址来访问文件,从而实现进程间数据的共享。

每种IPC机制都有其优缺点,选择合适的IPC机制取决于具体的应用需求和场景。

本篇文章来学习systemv 共享内存:

  1. 系统调用接口
  2. 使用共享内存
  3. 了解它的属性

共享内存

1.理解

进程间通信的本质是:先不同的进程,看到同一份资源

共享内存区是最快的IPC形式

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

注意:共享内存没有进行访问控制(同步与互斥)

如果要释放共享内存:1. 去关联 2. 释放共享内存

上面的操作都是进程 直接 做吗?不是。直接由操作系统来做

需求方 --系统调用--> 执行方

操作系统如何管理共享内存呢?

先描述,再组织内核结构体描述共享内存

你怎么保证让不同的进程看到同一共享内存呢?你怎么知道这个共享内存存在还是不存在呢?

设置了标识 key

2.运用

1. 创建 ipc - shmget

man shmget

相当于传的就是:地址,大小,操作

功能:
创建一个新的共享内存段,或者获取一个已存在的共享内存段。

原型:

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

参数:

  • key:共享内存段的标识符。用于命名共享内存段,在服务器和客户端之间共享。
  • size:共享内存段的大小,建议是页大小(一般是4096字节)的整数倍。
  • shmflg权限标志和控制标志,可以组合使用:(如何实现操作?)
    • IPC_CREAT如果共享内存段不存在,则创建它;如果存在,则返回其标识符。
    • IPC_CREAT | IPC_EXCL如果共享内存段不存在则创建它;如果已存在,则返回错误。
    • 权限标志(如文件权限)给出访问权限(如 0666 表示用户、组、其他都可读写)。

返回值:
成功返回一个非负整数,即共享内存段的标识码(shmid);失败返回-1。

测试:

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

int main() {
    key_t key = 1234; // 任意选定一个key
    int shmid;

    // 创建共享内存段
    shmid = shmget(key, 4096, 0644 | IPC_CREAT);
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }

    printf("共享内存创建成功,ID: %d\n", shmid);
    return 0;
}

2. 创建 key - ftok

man ftok

功能:
用文件路径和项目ID创建一个System V IPC key。

原型:

key_t ftok(const char *pathname, int proj_id);

参数:

  • pathname路径名,指向一个存在且可访问的文件。
  • proj_id项目ID,通常为小整数。

返回值:
成功返回生成的 key;失败返回 -1。

测试:

#include <stdio.h>
#include <sys/ipc.h>
#include <stdlib.h>

int main() {
    key_t key;

    // 使用 ftok 生成一个唯一的key
    key = ftok("shmfile", 'R');
    if (key == -1) {
        perror("ftok failed");
        exit(1);
    }

    printf("ftok 生成的key: %d\n", key);
    return 0;
}

🏷️谈谈 key:

  1. key 是一个数字。这个数字是几,不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识
  2. 第一个进程可以通过 key 创建共享内存,第二个之后的进程,只要拿着同一个 key 就可以和第一个共享内存
  3. 对于一个已经创建好的共享内存,key 在哪?key 在共享内存的描述对象中!(保存信物)
  4. key--类似(理念)--路径--唯一

第一次创建的时候,必须有一个 key 了. 怎么有?

  • ftok 是一套算法,pathname 和 proj_id 进行了数值计算即可,由用户自己指定!

key 为什么要用户来生成?自己设完之后还有可能出现冲突

  • 用户约定的,原理定义时提到过(命名管道:同路径下同一个文件名=路径+文件名),借用 ftok 并不会冲突

共享内存被删除后,则其他线程直接无法通信?

  • 错误共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除
⭕shmid vs key
  • key 保证了操作系统内的唯一性,shmid 只在你的进程内,用来表示资源的唯一性
  • 只有 shmget 时候用key,大部分情况用户访问共享内存,都用的是shmid

3. 连接 - shmat

功能:
将共享内存段连接到进程地址空间。

原型:

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

参数:

  • shmid:共享内存段标识符。
  • shmaddr:连接地址,若为NULL,系统自动选择地址;若为非NULL,按提供的地址连接。
  • shmflg
    • 0:默认连接方式。
    • SHM_RDONLY:只读连接。
    • SHM_RND:将地址向下取整为某个整数倍。

返回值:
成功返回指向共享内存段的指针;失败返回 -1。

存取返回值,调用实现写入

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <string.h>

int main() {
    key_t key = 1234;
    int shmid;
    char *data;

    // 获取共享内存段
    shmid = shmget(key, 4096, 0644 | IPC_CREAT);
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }

    // 将共享内存段连接到当前进程
    data = (char *)shmat(shmid, NULL, 0);
    if (data == -1)) {
        perror("shmat failed");
        exit(1);
    }

    // 写入数据
    strncpy(data, "Hello, Shared Memory!", 4096);
    printf("数据已写入共享内存: %s\n", data);

    return 0;
}

4. 脱离 - shmdt

功能:
将共享内存段与当前进程脱离。

原型:

int shmdt(const void *shmaddr);

参数:

  • shmaddr:由 shmat 返回的指向共享内存的指针。

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

经典格式:获取,修改,查看共享内存

5. 控制/删除 - shmctl

功能:
控制共享内存段。

原型:

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

参数:

  • shmid:共享内存段标识符,由 shmget 返回。内核当中获取共享内存的属性
  • cmd
    • IPC_STAT:获取共享内存段的当前关联值。
    • IPC_SET:设置共享内存段的当前关联值(需要足够权限)。
    • IPC_RMID:删除共享内存段。
  • buf:指向 shmid_ds 结构的指针。struct shmid_ds buf;创建一下

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

总结

  • ftok 测试代码生成一个唯一的key。
  • shmget 测试代码创建一个共享内存段
  • shmat 测试代码将共享内存段连接到当前进程并写入数据。
  • shmdt 测试代码将共享内存段与当前进程脱离
  • shmctl 测试代码获取共享内存段的状态,并最终删除它

代码示例

下面是一个简单的示例展示如何创建和使用共享内存:

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <string.h>
#define SHM_SIZE 4096
int main()
{
    key_t key;
    int shmid;
    char* data;
    struct shmid_ds buf;
    //ftok
    printf("make key\n");
    key=ftok("/home/lvy",'R');
    if(key==-1){
        perror("ftok failed");
        exit(1);
    }
    printf("ftok 生成的 key:%d\n",key);
    //creat. shmget
    shmid=shmget(key,SHM_SIZE,0644 | IPC_CREAT);
    if(shmid==-1){
        perror("shmget failed");
        exit(1);
    }
    printf("shmget,ID:%d\n",shmid);
    //shmat
    data=(char*)shmat(shmid,NULL,0);
    if (data == (char *)(-1)) {
        perror("shmat failed");
        exit(1);
    }
    printf("共享内存段连接成功。\n");
    //write
    strncpy(data, "Hello, Shared Memory!", SHM_SIZE);
    printf("数据已写入共享内存: %s\n", data);
    //shmdt
    // 将共享内存段与当前进程脱离
    printf("将共享内存段与当前进程脱离...\n");
    if (shmdt(data) == -1) {
        perror("shmdt failed");
        exit(1);
    }
    printf("共享内存已脱离。\n");

    // shmctl
    printf("获取共享内存段的状态...\n");
    if(shmctl(shmid,IPC_STAT,&buf)==-1){
        perror("shmctl (IPC_STAT) failed");
        exit(1);
    }
    //打印结构中内容
    printf("共享内存大小: %zu\n", buf.shm_segsz);
    printf("最后访问时间: %ld\n", buf.shm_atime);
    printf("最后分离时间: %ld\n", buf.shm_dtime);

    //shmctl
    printf("删除共享内存段...\n");
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl (IPC_RMID) failed");
        exit(1);
    }
    printf("共享内存段已删除。\n");
    return 0;
}

展示了如何使用 shmgetshmat、和 shmdt 来创建、连接、和脱离共享内存段,以及使用 ftok 生成唯一的 key。

3.实验

tip: 之前讲过堆栈空间的优劣,在这共享内存也会多申请的一部分空间叫 cookie

上面有没有通信呢?没有

下面来进行通信,直接用。一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可!不需要调用系统调用

我们来尝试在小项目中使用共享内存,思路:5 步操作存储 shmaddr(=shmat return) 后,对共享地址 read 和 write~

comm.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "log.hpp"

using namespace std;
Log log;
// 共享内存的大小一般建议是4096的整数倍
// 4097,实际上操作系统给你的是4096*2的大小
const int size = 4096; 
const string pathname="/home/lvy";
const int proj_id = 0x6666;
key_t GetKey()
{
    key_t k=ftok(pathname.c_str(),proj_id);
    if(k<0)
    {
         log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is : 0x%x", k);
    return k;
}

int GetShareMemHelper(int flag)
{
    key_t k = GetKey();
    int shmid = shmget(k, size, flag);
    if(shmid < 0)
    {
        log(Fatal, "create share memory error: %s", strerror(errno));
        exit(2);
    }
    log(Info, "create share memory success, shmid: %d", shmid);

    return shmid;
}

int CreateShm()
{
    return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm()
{
    return GetShareMemHelper(IPC_CREAT); 
}

#define FIFO_FILE "./myfifo"
#define MODE 0664

enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

class Init
{
public:
    Init()
    {
        // 创建管道
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {

        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};
#endif

processa.cc

tip: 共享内存的大小一般建议是 4096 的整数倍

4097,实际上操作系统给的是 4096*2 的大小,但是多的并不能用

#include "comm.hpp"

extern Log log;

int main()
{
    Init init;
    int shmid = CreateShm();
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);

    // ipc code 在这里!!
    // 一旦有人把数据写入到共享内存,其实我们立马能看到了!!
    // 不需要经过系统调用,直接就能看到数据了!

    int fd = open(FIFO_FILE, O_RDONLY); // 等待写入方打开之后,自己才会打开文件,向后执行, open 阻塞了!
    if (fd < 0)
    {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }
    struct shmid_ds shmds;
    while(true)
    {
        char c;
        ssize_t s = read(fd, &c, 1);
        if(s == 0) break;
        else if(s < 0) break;

        cout << "client say@ " << shmaddr << endl; //直接访问共享内存
        sleep(1);

        shmctl(shmid, IPC_STAT, &shmds);
        cout << "shm size: " << shmds.shm_segsz << endl;
        cout << "shm nattch: " << shmds.shm_nattch << endl;
        printf("shm key: 0x%x\n",  shmds.shm_perm.__key);
        cout << "shm mode: " << shmds.shm_perm.mode << endl;
    }

    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, nullptr);

    close(fd);
    return 0;
}

processb.cc

#include "comm.hpp"

int main()
{
    int shmid = GetShm();
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);

    int fd = open(FIFO_FILE, O_WRONLY); // 等待写入方打开之后,自己才会打开文件,向后执行, open 阻塞了!
    if (fd < 0)
    {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }
    // 一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可!
    // 不需要调用系统调用
    // ipc code
    while(true)
    {
        cout << "Please Enter@ ";
        fgets(shmaddr, 4096, stdin);

        write(fd, "c", 1); // 通知对方
    }

    shmdt(shmaddr);

    close(fd);
    return 0;
}

log.hpp

调用之前写好了的

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    void logmessage(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        // 打印日志
        printLog(level, logtxt);
    }
    void printLog(int level, const std::string &logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
        if (fd < 0) {
            perror("open");
            return;
        }
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
        printOneFile(filename, logtxt);
    }

    ~Log() {}

    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        // 打印日志
        printLog(level, logtxt);
    }

private:
    int printMethod;
    std::string path;
};

int main() {
    Log logger;

    // 设置日志输出方法
    logger.Enable(Screen);
    logger.logmessage(Info, "This is an information message.");
    logger(Warning, "This is a warning message.");

    // 切换到单文件输出方法
    logger.Enable(Onefile);
    logger(Debug, "This is a debug message written to a file.");

    // 切换到按级别分类输出方法
    logger.Enable(Classfile);
    logger(Fatal, "This is a fatal error message written to separate class files.");

    return 0;
}

makefile

.PHONY:all
all:processa processb

processa:processa.cc
   g++ -o $@ $^ -g -std=c++11
processb:processb.cc
   g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
   rm -f processa processb

测试

4.思考

共享内存的特性--扩展代码

  1. 共享内存没有同步互斥之类的保护机制,因为双方不知道对方
  2. 共享内存是所有进程间通信中,速度最快的!--拷贝少
  3. 共享内存内部的数据,由用户自己维护

想看看共享内存的属性!获取内核属性 man shmctl

对于没有同步机制,我们可像上面实验一样,借用管道来通知对方,来实现对方不输入就进行的等待

ipc 指令的使用

ipcs -m shmid #实现查看ipc资源
ipcrm   #删除共享内存

复习:

  1. cp 的高级用法

cp ../../XXX . #来实现拷贝到当前路径下

  1. 位段(Bitfields)和大小端(Big-Endian vs Little-Endian)以及处理器架构(如x86和x64)是有关联的,但它们之间的关系并不是直接的。

位段(Bitfields):位段是C语言中用于在结构体或联合中指定数据成员的位大小的一种方式。它们允许您指定哪些位应该用于存储特定的数据,从而节省内存空间。位段的布局取决于编译器和目标平台的字节序。

字节序(Byte Ordering):字节序描述了多字节数据类型(如整数、浮点数等)在内存中的存储方式。有两种常见的字节序:

  • 大端字节序(Big-Endian):在这种字节序中,最高有效字节(Most Significant Byte)位于内存地址的最小位置。
  • 小端字节序(Little-Endian):在这种字节序中,最低有效字节(Least Significant Byte)位于内存地址的最小位置。

处理器架构:不同的处理器架构默认支持不同的字节序。例如,x86架构通常使用小端字节序,而ARM架构则通常使用大端字节序。x64是x86架构的64位扩展,它也支持小端字节序。

  1. 缺页中断和报错

在计算机操作系统中,缺页中断(Page Fault)和越界 是两种不同的概念,它们在不同的上下文中发生。

  1. 缺页中断
    • 定义:缺页中断是在虚拟内存系统中发生的一种异常,当程序试图访问一个不存在的内存页时触发。
    • 原因:可能是因为程序试图访问一个未分配或已释放的内存页(如何识别出?这些信息os结构体都有存),或者是因为程序试图访问一个受保护的内存区域。
    • 处理:操作系统会处理缺页中断,它会检查页面是否已经在物理内存中,如果是,则重新加载页面;如果不在,则会从磁盘加载页面到物理内存中,并更新内存管理表(如页表)。
  1. 越界(Out of Bounds)
    • 定义:越界是指程序尝试访问内存地址范围之外的内存区域。
    • 原因:越界通常是由于程序错误地处理了内存地址,例如,访问数组的越界元素,或者访问了一个不存在的内存地址。
    • 处理:越界通常由程序本身处理,例如,在数组访问中,如果访问的索引超出了数组的大小,程序会触发一个越界错误。越界错误不会由操作系统处理,而是由程序的错误处理机制处理。
      区别
  • 触发条件:缺页中断是由于尝试访问不存在的内存页,而越界是由于尝试访问内存地址范围之外的内存区域。
  • 处理机制:缺页中断由操作系统处理,而越界错误由程序本身处理。
  • 后果:缺页中断可能导致程序暂停或重新启动,而越界错误可能导致程序崩溃或执行不正确。
    在实际编程中,程序员需要确保处理越界错误,以防止程序崩溃或产生不正确的结果。操作系统则负责处理缺页中断,以保持系统的稳定性和性能。

不存在的内存页和内存地址之外的内存区是怎么区分的?

  • 映射关系:每个内存地址都映射到一个内存页。当程序访问一个内存地址时,操作系统会通过页表查找对应的内存页。
  • 访问单位:在编程中,内存地址是操作的单位,但在内存管理中,内存页是分配和管理的单位。
  • 虚拟内存与物理内存的桥梁:内存页在虚拟内存和物理内存之间架起了一座桥梁,它允许操作系统将虚拟内存的地址空间映射到物理内存或磁盘上的交换文件。
  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值