[操作系统] 进程间通信:system V共享内存

文章目录

System V 是UNIX操作系统的一个版本,它定义了一系列的标准,包括进程间通信(IPC)的标准。Linux操作系统的内核实现了 <font style="color:rgb(6, 6, 7);">System V</font> 定义的IPC标准,并为此专门设计了一个模块来处理进程间的通信。进程间通信(IPC)的核心目的是允许不同的进程能够访问和操作同一份资源。这样,进程之间就可以通过共享资源来交换信息。不同的IPC机制可能在接口和原理上有相似之处,使得开发者可以更容易地理解和使用这些机制。


共享内存(Shared Memory)是进程间通信(IPC)的一种机制,它允许两个或多个进程访问同一块内存空间。共享内存是<font style="color:rgb(6, 6, 7);">System V</font> IPC标准的一部分,它提供了一种高效的进程间数据交换方式。

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

内存共享示意图

这是进程地址空间示意图,其中一部分为共享区,会存储共享内存、内存映射、共享库等。

这个区域如何使用呢?对于共享内存来说:

进程的共享区中存储的地址实际上是虚拟地址。当需要使用共享内存时,整个过程可以更清晰地描述如下:

  1. 数据加载到物理内存:首先,系统会将需要共享的数据加载到一块物理内存区域中,这块物理内存将成为多个进程共享的基础。
  2. 虚拟地址的建立:每个需要访问共享数据的进程会在自己的虚拟地址空间中开辟一个区域(通常称为共享区),并为该区域分配一个虚拟地址。
  3. 页表映射:通过进程的页表,这个虚拟地址会被映射到物理内存中那块存放共享数据的具体地址上。
  4. 实现数据共享:由于不同进程的虚拟地址通过各自的页表指向同一块物理内存区域,多个进程就能通过自己的虚拟地址访问相同的共享数据。

这种机制使得进程能够高效地共享内存中的数据,避免了不必要的数据复制,从而提升了系统的性能和资源利用效率。

共享内存通常有一个引用计数(Reference Count),记录当前有多少进程正在使用这块共享内存。共享内存会在引用计数为0时释放:

  • 只有当所有进程都解除对共享内存的映射(即引用计数降为0)时,共享内存才会被标记为可销毁。
  • 在Linux中,共享内存段由shmget创建时分配一个标识符(shmid),并通过shmctl系统调用管理。如果引用计数为0,系统会根据共享内存的设置决定是否立即销毁。

同一时刻可能会存在多组进程,都在使用不同的共享内存来进行通信。也就是说在操作系统中会有多个共享内存同时存在,那么按照以往的经验以及操作系统的习惯,这些共享内存是否需要管理呢?

和之前一样,先描述,再组织!

前面所提的引用计数当然也就是在管理共享内存的数据结构中进行维护。

共享内存的管理代码

cpp 复制代码
struct shmid_ds {
    struct ipc_perm shm_perm;    /* 操作权限 */
    int shm_segsz;              /* 共享内存段的大小(字节) */
    __kernel_time_t shm_atime;  /* 最后一次附加时间 */
    __kernel_time_t shm_dtime;  /* 最后一次分离时间 */
    __kernel_time_t shm_ctime;  /* 最后一次更改时间 */
    __kernel_ipc_pid_t shm_cpid;/* 创建者的进程 ID */
    __kernel_ipc_pid_t shm_lpid;/* 最后操作者的进程 ID */
    unsigned short shm_nattch;  /* 当前附加的进程数量 */
    unsigned short shm_unused;  /* 兼容性字段 */
    void *shm_unused2;          /* 兼容性字段(DIPC 使用) */
    void *shm_unused3;          /* 未使用字段 */
};

这个结构包含 11 个字段,每个字段都有特定的用途。让我们逐一讲解。


字段解析

  1. struct ipc_perm shm_perm - 操作权限
  • 作用:定义共享内存段的访问权限和所有权。
  • 详情:shm_perm 是一个嵌套的 struct ipc_perm 结构,包含创建者的用户 ID (uid)、组 ID (gid) 和权限模式(类似于文件权限,例如 0666)。它确保只有授权的进程可以访问或修改共享内存。
  • 这是 System V IPC 的通用权限机制,适用于共享内存、消息队列和信号量。
  1. int shm_segsz - 共享内存段大小
  • 作用:记录共享内存段的大小(单位:字节)。
  • 详情:在调用 shmget 创建共享内存时指定这个值,进程可以通过它知道可用内存的大小。
  1. __kernel_time_t shm_atime - 最后附加时间
  • 作用:记录最近一次进程附加(attach)到共享内存的时间。
  • 详情:当进程调用 shmat 附加共享内存时,这个字段更新为当前时间戳。
  1. __kernel_time_t shm_dtime - 最后分离时间
  • 作用:记录最近一次进程分离(detach)共享内存的时间。
  • 详情:调用 shmdt 时更新,反映共享内存的活跃状态。
  1. __kernel_time_t shm_ctime - 最后更改时间
  • 作用:记录共享内存属性的最后修改时间。
  • 详情:通过 shmctl 修改权限或其他属性时更新。
  1. __kernel_ipc_pid_t shm_cpid - 创建者 PID
  • 作用:保存创建共享内存的进程 ID。
  • 详情:由 shmget 调用时记录,用于追踪共享内存的所有权。
  1. __kernel_ipc_pid_t shm_lpid - 最后操作者 PID
  • 作用:记录最后操作共享内存的进程 ID。
  • 详情:包括附加(shmat)、分离(shmdt)或修改(shmctl)等操作。
  1. unsigned short shm_nattch - 当前附加进程数量
  • 作用:统计当前附加到共享内存的进程数。
  • 详情:这是引用计数的关键字段,进程附加时加 1,分离时减 1。当值为 0 时,共享内存可能被销毁(取决于标志)。
  1. unsigned short shm_unused - 兼容性字段
  • 作用:保留字段,未在现代 Linux 中使用。
  • 详情:早期可能有用途,现仅为兼容性保留。
  1. void *shm_unused2 - 兼容性字段(DIPC 使用)
  • 作用:同样是保留字段,可能在分布式 IPC (DIPC) 中使用。
  • 详情:标准 Linux 内核未使用。
  1. void *shm_unused3 - 未使用字段
  • 作用:完全未使用,可能为未来扩展保留。
  • 详情:对齐或占位用途。

shmget 函数

功能 :创建共享内存段
原型

c 复制代码
int shmget(key_t key, size_t size, int shmflg);

参数

  • key:不同进程shm来进行通信,作为共享内存段的唯一标识(键值),通过用户层来传入,但是只是对于内核 来作为唯一标识,用户层使用该函数的返回值int作为唯一表示。
  • size:共享内存大小(单位:字节)。
  • shmflg:权限标志组合,常见取值:
    • IPC_CREAT:若共享内存不存在则创建,否则获取已存在的共享内存。
    • IPC_CREAT | IPC_EXCL:若共享内存不存在则创建,若已存在则报错(确保唯一性),也就是说只要成功返回就一定是一个全新的共享内存。

返回值

  • 成功:返回共享内存段的标识符(非负整数)。
  • 失败:返回 -1

**size**的细节处理:

在内核中,共享内存在创建的时候它的大小必须是以4KB(4096)的整数倍为单位。

如果我们给size传入4097的话,它会向上取整,也就是开辟4096*2,但是在实际上显示的时候仍然显示4097,因为你指定的使用空间是4097,但是底层仍然是按照单位开辟空间。

原理

例如现在需要两个进程共享一块内存。

先创建共享内存

对于使用shmget函数时要传入的key值,使用ftok函数来得到要传入的key

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>

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

功能:

<font style="color:black;">ftok</font> 函数通过一个现有的文件路径名(<font style="color:black;">pathname</font>)和一个项目标识符(<font style="color:black;">proj_id</font>)生成一个唯一的 <font style="color:black;">key_t</font> 值。这个键值可以用作 IPC 对象(如共享内存)的标识符,确保不同进程能够访问相同的 IPC 资源。


参数:

  1. <font style="color:black;">pathname</font>
    • 类型:<font style="color:black;">const char *</font>
    • 描述:指向一个现有文件路径的字符串,通常是一个可访问的文件的绝对路径。
    • 要求:文件必须存在且当前进程有权限访问,否则函数会失败。
  2. <font style="color:black;">proj_id</font>
    • 类型:<font style="color:black;">int</font>
    • 描述:一个非零的项目标识符,通常取值范围为 1 到 255(低 8 位有效)。
    • 作用:与 <font style="color:black;">pathname</font> 结合,增加键值的唯一性。

返回值:

  • 成功:返回一个 key_t 类型的非负整数,作为 IPC 对象的键值
  • 失败:返回 -1,并设置 errno 以指示错误原因(例如,文件不存在或无权限)。

工作原理:

<font style="color:black;">ftok</font> 函数通过以下步骤生成键值:

  1. 获取 <font style="color:black;">pathname</font> 对应的文件信息(如索引节点号<font style="color:black;"> </font><font style="color:black;">inode</font> 和设备号 <font style="color:black;">dev</font>)。
  2. 将文件的 <font style="color:black;">inode</font> 号、设备号与 <font style="color:black;">proj_id</font> 的低 8 位组合起来,生成一个唯一的 <font style="color:black;">key_t</font> 值。
  3. 返回生成的键值。

生成的键值并不是完全随机的,而是基于文件系统信息和 <font style="color:black;">proj_id</font> 的确定性结果。因此:

  • 如果两个进程使用相同的 <font style="color:black;">pathname</font><font style="color:black;">proj_id</font>,它们会得到相同的 <font style="color:black;">key_t</font><font style="color:black;"> </font>值。
  • 如果 <font style="color:black;">pathname</font><font style="color:black;">proj_id</font> 不同,通常会生成不同的键值。

如何保证两个不同的进程拿到的是一块共享内存

当通过ftok得到key值并传入shmget后,即可得到一块共享内存,shmget函数的返回值就是用户层面上对于共享内存的唯一表示符。key值是对于操作系统而言的唯一标识符。

当想让两个不同的进程拿到同一块共享内存,比如有A进程和B进程,它们现在想要共享shmget创建的这一块共享内存。

A进程把key传给操作系统内核,然后操作系统使用这个key设置为开辟的共享内存(物理内存)的唯一值,之后B进程链接该共享内存时会辨别传入

<font style="color:black;">shmctl</font> 函数

<font style="color:black;">shmctl</font> 是 System V IPC 中用于控制共享内存的核心函数。它提供了对共享内存段的多种操作,包括获取状态、修改属性和删除内存段等。

函数定义

  • 头文件:
c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
  • 原型:
c 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

功能

<font style="color:black;">shmctl</font> 用于控制共享内存段的状态和行为。通过不同的命令(cmd),它可以:

  • 获取共享内存的当前状态。
  • 修改共享内存的属性(如权限)。
  • 标记共享内存段为待删除。

参数

  1. <font style="color:black;">shmid</font>
    • 类型:<font style="color:black;">int</font>
    • 描述:共享内存的标识符,由 <font style="color:black;">shmget</font><font style="color:black;"> </font>函数返回。
    • 作用:指定要操作的共享内存段。
  2. <font style="color:black;">cmd</font>
    • 类型:<font style="color:black;">int</font>
    • 描述:指定要执行的动作,可取以下三个值:
      • <font style="color:black;">IPC_STAT</font>:获取共享内存的状态,将信息存储到 <font style="color:black;">buf</font><font style="color:black;"> </font>指向的 <font style="color:black;">struct shmid_ds</font> 结构体中。
      • <font style="color:black;">IPC_SET</font>:设置共享内存的属性,从 <font style="color:black;">buf</font><font style="color:black;"> </font>指向的 <font style="color:black;">struct shmid_ds</font> 中读取新值。
      • <font style="color:black;">IPC_RMID</font>:标记共享内存段为待删除,当所有进程脱离后销毁。
    • 作用:决定 shmctl 的具体行为。
  3. <font style="color:black;">buf</font>
    • 类型:<font style="color:black;">struct shmid_ds *</font>
    • 描述:指向一个 <font style="color:black;">struct shmid_ds</font> 结构体的指针,用于保存或设置共享内存的状态和访问权限。
    • 细节:
      • 对于 <font style="color:black;">IPC_STAT</font><font style="color:black;">buf</font><font style="color:black;"> </font>被填充为共享内存的当前状态。
      • 对于 <font style="color:black;">IPC_SET</font><font style="color:black;">buf</font><font style="color:black;"> </font>提供要设置的新属性值。
      • 对于 <font style="color:black;">IPC_RMID</font>,此参数通常设为 <font style="color:black;">NULL</font>(不使用)。

返回值

  • 成功:返回 0。
  • 失败:返回 -1,并设置 errno 以指示错误原因(如无效的 shmid 或权限不足)。

<font style="color:black;">IPC_RMID</font>参数

当我们在程序中使用shmget后随着进程退出后,会发现开辟的共享内存并没有释放:

c 复制代码
const char *path = ".";
key_t key = ftok(path, 'a');  // 使用单字节 proj_id(如 'a')
if (key == -1) 
{
    std::cerr << "ftok failed: " << strerror(errno) << std::endl;
    return 1;
}

// 创建共享内存(权限 0600:用户可读可写)
int shmid = shmget(key, 1000, IPC_CREAT | IPC_EXCL | 0600);
if (shmid == -1) 
{
    std::cerr << "shmget failed: " << strerror(errno) << std::endl;
    return 1;
}

使用ipcs -m命令查询当前共享内存段信息:

得到结论:共享内存资源,生命周期跟随内核!

也就是说当操作系统重启后该共享内存就会释放。

但是我们可以通过其他方法进行释放共享内存:

  • bashipcs -m进行查询当前共享内存,ipcrm shmid来删除制定shmid的共享内存。
    • 为什么使用的是shmid而不是key?因为key是给内核用来区分共享内存唯一性的,所以使用shmid进行管理共享内存。
  • 程序中使用对应函数进行释放:int shmctl(int shmid, int cmd, struct shmid_ds *buf);cmd使用<font style="color:black;">IPC_RMID</font>
c 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cerrno>
#include <cstring>
#include <unistd.h>

int main() {
    const char *path = ".";
    key_t key = ftok(path, 'a');  // 使用单字节 proj_id(如 'a')
    if (key == -1) {
        std::cerr << "ftok failed: " << strerror(errno) << std::endl;
        return 1;
    }

    // 创建共享内存(权限 0600:用户可读可写)
    int shmid = shmget(key, 1000, IPC_CREAT | IPC_EXCL | 0600);
    if (shmid == -1) {
        std::cerr << "shmget failed: " << strerror(errno) << std::endl;
        return 1;
    }

    sleep(10);

    // 程序退出前删除共享内存
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        std::cerr << "shmctl failed: " << strerror(errno) << std::endl;
        return 1;
    }

    return 0;
}

程序运行中(sleep阻挡释放共享区):

shmctl(shmid, IPC_RMID, NULL)释放shmid的共享内存:

shmat函数

shmat用于将共享内存挂接到进程的虚拟地址空间,sham-at,at对应的就是attach这个单词。

为什么需要 shmat:因为共享内存只是"存在"还不够,进程需要一个具体的虚拟地址来访问它,而 shmat 完成了这个映射过程,<font style="color:black;">shmget</font> 是"造房子",<font style="color:black;">shmat</font><font style="color:black;"></font>是"开门入住"。

c 复制代码
NAME
    shmat, shmdt - System V shared memory operations

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

   void *shmat(int shmid, const void *shmaddr, int shmflg);
  • **shmid****:**共享内存的标识符,也就是shmget的返回值,不是key,key是给系统使用的。
  • **shmaddr****:**虚拟地址,使用固定的虚拟地址进行挂接,默认时填入NULL
  • **shmflg****:**为挂接在虚拟内存设置权限,两个可能取值是SHM_RNDSHM_RDONLY,使用默认设置的话传入0。
  • 返回值:成功 返回一个指针,指向共享内存的第一个节(section),也就是指向挂接的那个共享内存区域在当前进程虚拟地址空间的起始位置失败返回-1。

如何理解返回值,我们得到这个起始的虚拟地址有什么用?

  • 在学习C语言时,malloc函数就是用来在堆上开辟空间,然后返回一个指针,这个指针也是指向开辟空间在虚拟地址空间中的的起始位置,只不过shmat是在共享区进行挂接,是用户可访问修改的(除非指定权限),而堆栈是不可写的。

无同步机制

同步机制是什么

同步机制(Synchronization Mechanism)是多进程或多线程编程中用来协调多个执行单元(进程或线程)访问共享资源的一种方法。它的核心目标是避免数据竞争(data race)和不一致性(inconsistency),确保共享资源(如共享内存、文件、变量等)在并发访问时能够保持正确性和一致性。

为什么需要同步机制?

  • 数据竞争:多个进程同时写入共享内存,可能导致数据被覆盖或部分写入。
  • 例如:server 正在写入 "Hello",刚写到 "Hel" 时,client 读取,可能只读到 "Hel"。
  • 不一致性:读写顺序不可控,可能导致逻辑错误。
  • 例如:client 读取数据时,server 还没写完,读到的可能是未初始化的垃圾数据。
  • 死锁或竞争条件:如果多个进程互相等待对方完成,可能会陷入死锁。

模拟演示非同步效果

使用封装的shm类进行模拟:

写:

cpp 复制代码
FileOper writerfile(PATH, FILENAME);
writerfile.OpenForWrite();

Shm shm(pathname, projid, USER);
char *mem = (char *)shm.VirtualAddr();
// 我们读写共享内存,有没有使用系统调用??没有!!
int index = 0;
for (char c = 'A'; c <= 'B'; c++, index += 2)
{
    // 才是向共享内存写入
    sleep(1);
    mem[index] = c;
    mem[index + 1] = c;
    sleep(1);
    mem[index+2] = 0;

    writerfile.Wakeup();
}

读:

cpp 复制代码
Shm shm(pathname, projid, CREATER);
// sleep(5);
shm.Attr();


NamedFifo fifo(PATH, FILENAME);

// 文件操作了
FileOper readerfile(PATH, FILENAME);
readerfile.OpenForRead();

char *mem = (char *)shm.VirtualAddr();
// 我们读写共享内存,有没有使用系统调用??也没有!!
while (true)
{
    if (readerfile.Wait())
    {
        printf("%s\n", mem); // 每次读取共享内存区上的全部内容
    }
    else
        break;
}

可以发现在往共享内存写入和读取的时候直接使用C语言提供的函数进行操作,而不是系统调用。并且还没有写入完整的内容就已经被一直读取了,这就是因为共享内存没有所谓的"同步机制",没有内核文件缓冲区那样的中间层。但是这样的方式使得共享内存是进程间通信最快的方式

  • 在映射之后,读写可以直接被对方看到,直接在内存上读写。
  • 不需要使用系统调用获取或者写入内容。

因为共享区属于用户空间,可以让用户直接使用!

如何提供保护机制

共享内存本身不提供访问控制或保护机制。所有映射了这块内存的进程都可以自由读写,没有限制,这就是缺点。因此程序员需要自己实现保护机制。

如使用管道机制的阻塞来手动同步:

<font style="color:black;">server</font>端运行时,阻塞等待。

以此方法来作为有控制性的读取和输入的保证。

shmdt

cpp 复制代码
shmdt函数 
    功能:将共享内存段与当前进程脱离 
原型 
    int shmdt(const void *shmaddr); 
参数 
    shmaddr: 由shmat所返回的指针,指向共享区内虚拟地址的指针
    返回值:成功返回0;失败返回-1 
    注意:将共享内存段与当前进程脱离,不等于删除共享内存段

演示代码:

cpp 复制代码
// _start_mem 为shmat返回值,即共享区对应虚拟地址起始位置
int n = shmdt(_start_mem);
if (n == 0)
{
    printf("detach success\n");
}

再谈描述共享内存的数据结构

struct shmid_ds

struct shmid_ds即为描述共享内存的数据结构,每一个由shmget获取的共享内存都由struct shmid_ds组织管理:

cpp 复制代码
/* 描述共享内存段的数据结构 */
struct shmid_ds
  {
    struct ipc_perm shm_perm;   /* 操作权限结构 */
    size_t shm_segsz;           /* 段的大小(以字节为单位) */
    __time_t shm_atime;         /* 最后一次 shmat() 的时间 */
#ifndef __x86_64__
    unsigned long int __unused1; /* 未使用的字段1(仅在非x86_64架构中) */
#endif
    __time_t shm_dtime;         /* 最后一次 shmdt() 的时间 */
#ifndef __x86_64__
    unsigned long int __unused2; /* 未使用的字段2(仅在非x86_64架构中) */
#endif
    __time_t shm_ctime;         /* 最后一次通过 shmctl() 更改的时间 */
#ifndef __x86_64__
    unsigned long int __unused3; /* 未使用的字段3(仅在非x86_64架构中) */
#endif
    __pid_t shm_cpid;           /* 创建者的进程ID */
    __pid_t shm_lpid;           /* 最后一次共享内存操作的进程ID */
    shmatt_t shm_nattch;        /* 当前附着的数量 */
  };

struct ipc_perm

**struct ipc_perm**为操作权限结构 :

cpp 复制代码
/* 用于向 IPC 操作传递权限信息的数据结构 */
struct ipc_perm
  {
    __key_t __key;              /* 键值 */
    __uid_t uid;                /* 所有者的用户ID */
    __gid_t gid;                /* 所有者的组ID */
    __uid_t cuid;               /* 创建者的用户ID */
    __gid_t cgid;               /* 创建者的组ID */
    unsigned short int mode;    /* 读/写权限 */
    unsigned short int __pad1;  /* 填充字段1 */
    unsigned short int __seq;   /* 序列号 */
    unsigned short int __pad2;  /* 填充字段2 */
    __syscall_ulong_t __unused1; /* 未使用的字段1 */
    __syscall_ulong_t __unused2; /* 未使用的字段2 */
  };

通过struct ipc_perm即可得到_key值等信息。

实力代码:

cpp 复制代码
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds); // ds:输出型参数
printf("shm_segsz: %ld\n", ds.shm_segsz);
printf("key: 0x%x\n", ds.shm_perm.__key);

得以验证_key即为使用shmget时传入的key

相关推荐
tjsoft29 分钟前
asm汇编字符串操作
linux·运维·汇编
xyd陈宇阳36 分钟前
linux入门三:Linux 编辑器
linux·运维·编辑器
_长银2 小时前
Ubuntu-搭建nifi服务
linux·运维·ubuntu
江沉晚呤时2 小时前
深入探析C#设计模式:访问者模式(Visitor Pattern)的原理与应用
java·服务器·开发语言·数据库·.netcore
m0_521118232 小时前
Ubuntu在桌面缺少图标
linux·ubuntu
tpoog2 小时前
[MySQL] 表的增删查改(查询是重点)
linux·数据库·mysql·算法·贪心算法
hero_th2 小时前
ssh 免密登录服务器(vscode +ssh 免密登录)
服务器·vscode·ssh
陌上花开缓缓归以2 小时前
linux RCU技术
linux
阳光九叶草LXGZXJ3 小时前
达梦数据库-学习-18-ODBC数据源配置(Linux)
linux·运维·数据库·sql·学习·oracle
运维之美@3 小时前
达梦数据库迁移问题总结
运维·服务器·数据库