进程通信(10):Posix 共享内存区

POSIX.1提供了两种主要的方法来实现这一目标:

++1. 内存映射文件++

描述符获取:通过open函数打开一个文件,并获得其文件描述符。

映射过程:使用mmap函数将该文件映射到当前进程的地址空间。

适用场景:不仅可以用于父子进程间的共享,还可以用于无亲缘关系的进程之间。只要所有需要访问该共享内存区的进程都能够打开同一个文件,它们就可以共享由该文件映射而来的内存区域。

++2. 共享内存区对象++

描述符获取:通过shm_open函数打开或创建一个Posix IPC名字(可能是文件系统中的路径名),并返回一个描述符。

映射过程:同样使用mmap函数将这个描述符映射到当前进程的地址空间。

适用场景:特别适用于无亲缘关系的进程之间的共享内存通信。shm_open提供的接口允许创建专门用于共享内存的对象,而不必依赖实际文件。

区别:这两种技术的主要差异在于获取作为mmap参数之一的描述符的方式------是通过open还是shm_open。

共同点:两者都需要调用mmap来进行内存映射,并且都属于POSIX标准下的"内存区对象"范畴。

cpp 复制代码
+---------------------+       +-------------------------+
|                     |       |                         |
| 使用open和mmap      |       | 使用shm_open和mmap      |
|                     |       |                         |
| 1. open(file)       |       | 1. shm_open(name)       |
| 2. mmap(fd)         |       | 2. mmap(shm_fd)         |
|                     |       |                         |
+---------------------+       +-------------------------+
        |                             |
        v                             v
   父子进程间                   无亲缘关系进程间
   共享内存区                    共享内存区

Posix 共享内存区:shm_open 和 shm_unlink 函数

POSIX共享内存区通过两个关键步骤来实现无亲缘关系进程间的高效通信:

创建或打开共享内存区对象:使用shm_open函数。映射共享内存区到进程地址空间:使用mmap函数。

shm_open 函数

shm_open用于创建一个新的共享内存区对象或打开一个已存在的共享内存区对象。它返回一个文件描述符,该描述符随后可以传递给mmap进行内存映射。

cpp 复制代码
#include <sys/mman.h>
int shm_open(const char *name, int oflag, mode_t mode);

const char *name:指定共享内存区的名字。遵循特定命名规则(如以/开头的路径名)。

int oflag:访问模式标志,必须包含O_RDONLY(只读)或O_RDWR(读写),还可以包括以下可选标志:

O_CREAT:如果指定的共享内存区不存在,则创建之。

O_EXCL:与O_CREAT一起使用时,确保新创建的共享内存区是唯一的;若已存在则调用失败。

O_TRUNC:如果共享内存区已存在且指定了O_RDWR,则将其截短为零长度。

mode_t mode:权限位,仅当指定了O_CREAT时有效。指定新创建的共享内存区的权限。

返回值:成功时返回非负描述符。失败时返回-1并设置errno。

注意事项

mode参数在未指定O_CREAT时可以设为0。shm_open返回的描述符必须用于后续的mmap调用,作为映射共享内存区的基础。

++shm_unlink 函数++

shm_unlink用于删除共享内存区对象的名字,类似于文件系统中的unlink操作。它不会立即销毁底层的共享内存区对象,而是阻止新的引用被创建,直到所有现有引用都被关闭为止。

cpp 复制代码
#include <sys/mman.h>
int shm_unlink(const char *name);

const char *name:要删除名字的共享内存区对象的名字。

返回值:成功时返回0。失败时返回-1并设置errno。

删除名字后,现有的映射仍然有效,直到所有对该共享内存区的引用都关闭。这种行为与其他形式的unlink函数一致,例如文件系统的unlink、消息队列的mq_unlink和有名信号量的sem_unlink。

以下是创建和使用Posix共享内存区的基本步骤:

创建或打开共享内存区:

cpp 复制代码
int fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (fd == -1) {
    perror("shm_open failed");
    exit(EXIT_FAILURE);
}

设置共享内存区大小:

cpp 复制代码
ftruncate(fd, SIZE); // SIZE 是所需的共享内存区大小

映射共享内存区到进程地址空间:

cpp 复制代码
void *ptr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
    perror("mmap failed");
    close(fd);
    exit(EXIT_FAILURE);
}
close(fd); // 映射成功后关闭文件描述符

使用共享内存区:

取消映射并清理:

cpp 复制代码
munmap(ptr, SIZE);
shm_unlink("/my_shared_memory"); // 删除共享内存区的名字
ftruncate 和 fstat 函数在处理内存映射对象中的作用

ftruncate和fstat函数是处理普通文件和Posix共享内存区对象大小及属性的重要工具。

++ftruncate 函数++

ftruncate用于修改文件或共享内存区对象的大小。

cpp 复制代码
#include <unistd.h>
int ftruncate(int fd, off_t length);

int fd:文件描述符,指向要调整大小的文件或共享内存区对象。

off_t length:新的大小(以字节为单位)。

返回值:成功时返回0。失败时返回-1并设置errno。

++(1)对于普通文件++

如果文件当前大小大于指定的length,则超出部分的数据将被截断丢弃。如果文件当前大小小于length,理论上应扩展文件到新长度,但实际上POSIX并未明确规定这种情况下文件是否会被修改或其大小是否会增长。为了确保可移植性,通常的做法是先使用lseek移动到length - 1位置,然后写入一个字节的数据来扩展文件。幸运的是,几乎所有Unix实现都支持直接用ftruncate扩展文件大小。

++(2)对于共享内存区对象++

ftruncate明确地将共享内存区对象的大小设置为length字节。当创建一个新的共享内存区对象时,可以通过调用ftruncate来指定其初始大小。对于已存在的对象,可以使用ftruncate修改其大小。

操作Posix共享内存区的简单程序

为了展示如何操作Posix共享内存区,我们将开发四个简单的程序:shmcreate、shmunlink、shmwrite和shmread。这些程序分别用于创建、删除、写入和读取共享内存区对象。

shmcreate 程序

shmcreate程序以指定的名字和长度创建一个共享内存区对象,并将其映射到调用进程的地址空间中。

cpp 复制代码
#include "unpipc.h"
int main(int argc, char **argv) {
    int c, fd, flags;
    char *ptr;
    off_t length;
    mode_t file_mode = FILE_MODE; // 定义文件模式常量
    flags = O_RDWR | O_CREAT;
    // 解析命令行参数
    while ((c = getopt(argc, argv, "e")) != -1) {
        switch (c) {
            case 'e':
                flags |= O_EXCL;
                break;
            default:
                err_quit("usage: shmcreate [-e] <name> <length>");
        }
    }
    if (optind != argc - 2)
        err_quit("usage: shmcreate [-e] <name> <length>");
    length = atoi(argv[optind + 1]);
    fd = Shm_open(argv[optind], flags, file_mode);
    if (fd == -1)
        err_sys("Shm_open error");
    // 设置共享内存区对象的大小
    if (ftruncate(fd, length) == -1)
        err_sys("ftruncate error");
    // 映射共享内存区到当前进程地址空间
    ptr = Mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED)
        err_sys("mmap error");
    exit(0);
}

创建或打开一个名为<name>的共享内存区对象。如果指定了-e选项,则要求该对象必须不存在(即创建时独占)。使用ftruncate设置对象的大小为<length>字节。使用mmap将共享内存区映射到调用进程的地址空间中。

shmunlink程序从系统中删除一个共享内存区对象的名字,但不影响现有的映射。

cpp 复制代码
#include "unpipc.h"
int main(int argc, char **argv) {
    if (argc != 2)
        err_quit("usage: shmunlink <name>");
    if (Shm_unlink(argv[1]) == -1)
        err_sys("Shm_unlink error");
    exit(0);
}

删除名为<name>的共享内存区对象的名字。已有的引用仍然有效,直到所有引用都关闭为止。

shmwrite 程序

shmwrite程序往一个共享内存区对象中写入一个递增的模式:0, 1, 2, ..., 254, 255, 0, 1, ...。

cpp 复制代码
#include "unpipc.h"
int main(int argc, char **argv) {
    int fd;
    struct stat stat;
    unsigned char *ptr;
    if (argc != 2)
        err_quit("usage: shmwrite <name>");
    // 打开共享内存区对象,获取其大小信息并映射
    fd = Shm_open(argv[1], O_RDWR, FILE_MODE);
    if (fd == -1)
        err_sys("Shm_open error");
    if (fstat(fd, &stat) == -1)
        err_sys("fstat error");
    ptr = Mmap(NULL, stat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED)
        err_sys("mmap error");
    close(fd);
    // 写入模式
    for (int i = 0; i < stat.st_size; i++)
        ptr[i] = i % 256;
    exit(0);
}

打开名为<name>的共享内存区对象。获取对象的大小信息并通过mmap映射。写入递增模式的数据到共享内存区中。

shmread 程序

shmread程序验证由shmwrite写入的模式是否正确。

cpp 复制代码
#include "unpipc.h"
int main(int argc, char **argv) {
    int fd;
    struct stat stat;
    unsigned char c, *ptr;
    if (argc != 2)
        err_quit("usage: shmread <name>");
    // 打开共享内存区对象,获取其大小信息并映射(只读)
    fd = Shm_open(argv[1], O_RDONLY, FILE_MODE);
    if (fd == -1)
        err_sys("Shm_open error");
    if (fstat(fd, &stat) == -1)
        err_sys("fstat error");
    ptr = Mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED)
        err_sys("mmap error");
    close(fd);
    // 验证模式
    for (int i = 0; i < stat.st_size; i++) {
        c = ptr[i];
        if (c != (i % 256))
            err_ret("ptr[%d] = %d", i, c);
    }
    exit(0);
}

打开名为<name>的共享内存区对象,仅用于读取。获取对象的大小信息并通过mmap映射。验证共享内存区中的数据是否符合预期的递增模式。

使用Posix共享内存区和有名信号量实现计数器加1

我们将开发一个服务器程序和多个客户端程序,用于在一个共享内存区中维护一个计数器,并使用有名信号量进行同步。服务器程序创建并初始化共享内存区对象和信号量,而客户端程序则对该计数器执行加1操作。

cpp 复制代码
#include "unpipc.h"
struct shmstruct {
    int count;
};
int main(int argc, char **argv) {
    int fd;
    struct shmstruct *ptr;
    sem_t *mutex;
    if (argc != 3)
        err_quit("usage: server1 <shmname> <semname>");
    // 提防已存在的共享内存区对象
    Shm_unlink(Px_ipc_name(argv[1])); // 忽略失败情况
    // 创建共享内存区对象
    fd = Shm_open(Px_ipc_name(argv[1]), O_RDWR | O_CREAT | O_EXCL, FILE_MODE);
    if (fd == -1)
        err_sys("Shm_open error");
    // 设置共享内存区对象的大小
    if (ftruncate(fd, sizeof(struct shmstruct)) == -1)
        err_sys("ftruncate error");
    // 映射共享内存区到当前进程地址空间
    ptr = Mmap(NULL, sizeof(struct shmstruct), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED)
        err_sys("mmap error");
    close(fd); // 关闭描述符,映射仍然有效
    // 初始化计数器为0
    ptr->count = 0;
    // 提防已存在的有名信号量
    Sem_unlink(Px_ipc_name(argv[2])); // 忽略失败情况
    // 创建并初始化有名信号量
    mutex = Sem_open(Px_ipc_name(argv[2]), O_CREAT | O_EXCL, FILE_MODE, 1);
    if (mutex == SEM_FAILED)
        err_sys("Sem_open error");
    Sem_close(mutex); // 关闭信号量,但不销毁它
    exit(0);
}

创建或删除指定名称的共享内存区对象。将共享内存区对象设置为sizeof(struct shmstruct)大小,并将其映射到调用进程的地址空间中。初始化计数器为0。创建并初始化一个名为<semname>的有名信号量,初始值为1(即未锁定状态)。

client1程序:对共享计数器执行加1操作

client1程序打开现有的共享内存区对象和信号量,并对计数器执行一定次数的加1操作。

cpp 复制代码
#include "unpipc.h"
struct shmstruct {
    int count;
};
int main(int argc, char **argv) {
    int fd, i, nloop;
    pid_t pid;
    struct shmstruct *ptr;
    sem_t *mutex;
    if (argc != 4)
        err_quit("usage: client1 <shmname> <semname> <#loops>");
    nloop = atoi(argv[3]);
    // 打开现有的共享内存区对象
    fd = Shm_open(Px_ipc_name(argv[1]), O_RDWR, FILE_MODE);
    if (fd == -1)
        err_sys("Shm_open error");
    // 映射共享内存区到当前进程地址空间
    ptr = Mmap(NULL, sizeof(struct shmstruct), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED)
        err_sys("mmap error");
    close(fd); // 关闭描述符,映射仍然有效
    // 打开现有有名信号量
    mutex = Sem_open(Px_ipc_name(argv[2]), 0);
    if (mutex == SEM_FAILED)
        err_sys("Sem_open error");
    pid = getpid();
    // 对计数器执行加1操作
    for (i = 0; i < nloop; i++) {
        if (Sem_wait(mutex) == -1) // 获取锁
            err_sys("Sem_wait error");
        printf("pid %ld: %d\n", (long)pid, ptr->count++);
        if (Sem_post(mutex) == -1) // 释放锁
            err_sys("Sem_post error");
    }
    exit(0);
}

打开现有的共享内存区对象和有名信号量。对计数器执行<#loops>次加1操作,每次操作前获取信号量以确保互斥访问,操作完成后释放信号量。每次加1操作后打印当前PID和计数值。

相关推荐
vincent_woo2 小时前
再学安卓 - binder之ServiceManager
android·操作系统
x-cmd7 小时前
[241231] CachyOS 2024 年终总结:性能飞跃与社区繁荣 | ScyllaDB 宣布转向开源可用许可证
linux·开源·操作系统·database·开发·集群·arch
XLYcmy18 小时前
分布式练手:Client
c++·windows·分布式·网络安全·操作系统·c·实验源码
XLYcmy20 小时前
分布式练手:Server
c++·windows·分布式·网络安全·操作系统·c·实验源码
菠菠萝宝1 天前
【Go学习】-01-4-项目管理及协程
数据库·学习·golang·操作系统·软件工程·协程·os
love666666shen1 天前
【面试】后端开发面试中常见数据结构及应用场景、原理总结
数据结构·计算机网络·链表·操作系统··索引·后端开发
无双@3 天前
简单封装线程库 + 理解LWP和TID
linux·c++·操作系统·线程·进程·大作业
cloud___fly3 天前
零拷贝技术
linux·操作系统
矮油0_o3 天前
30天开发操作系统 第 10 天 -- 叠加处理
开发语言·汇编·数据结构·c++·算法·操作系统
kaixin_learn_qt_ing4 天前
Mac OS
操作系统