【Linux】一文吃透嵌入式 Linux 共享内存:原理、优化与避坑指南

目录

1.  概述

2.  System V IPC

2.1  shmget()函数——创建或打开共享内存

2.2  shmctl()函数——删除共享内存

2.3  shmat()函数——将共享内存段附加到进程的地址空间

2.4  shmdt()函数——进程分离共享内存段

3.  POSIX IPC

3.1  shm_open()函数——创建或打开一个共享内存对

3.2  shm_unlink()函数——删除共享内存

3.3  truncate()和ftruncate()——将文件缩放到指定大小

3.4  mmap()函数——将一组设备或者文件映射到内存地址

3.5  总结使用


1.  概述

        共享内存是嵌入式 Linux 系统中一种非常重要且高效的进程间通信机制。它允许多个不相关的进程访问同一块物理内存区域,从而实现大规模数据的快速共享。

        其核心思想是,在物理内存中开辟一块区域,映射到多个进程各自的虚拟地址空间。这样,一个进程写入的数据,另一个进程立刻就能看到,无需任何形式的数据拷贝。

         共享内存是存在于内核级别的一种资源,在shell中可以使用ipcs命令来查看当前系统 IPC 中的状态,在文件系统/proc 目录下有对其描述的相应文件:

        共享内存的实现方式有两种标准:System V IPC 和 POSIX IPC。

2.  System V IPC

        这是比较传统但广泛使用的方法。关键步骤:

  • ftok:生成一个唯一的键值。
  • shmget:根据键值创建或获取共享内存段。可以指定大小和权限。
  • shmat:将共享内存段“附加”到当前进程的地址空间,返回其虚拟地址。
  • 读写操作:通过 shm_addr 指针像操作普通内存一样进行读写。
  • shmdt:进程分离共享内存段。
  • shmctl:用于控制共享内存段,例如删除它(IPC_RMID)。

2.1  shmget()函数——创建或打开共享内存

        创建或打开一块共享内存区。

        表头文件:

#include<sys/shm.h>

        函数定义:

/* Get shared memory segment.  */
extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW;
  • key:用于唯一标识共享内存段的键值;其有三种创建方式:
    ①使用 IPC_PRIVATE(推荐用于亲缘进程)
    ②使用 ftok() 生成键值(推荐用于无亲缘进程)
    ③使用固定整数值
  • size:请求的共享内存段大小(字节),如果是创建新段,必须指定 size > 0,如果是获取已存在的段,size 被忽略(但必须 ≤ 已存在段的大小),实际分配的大小会被向上取整到系统页大小的整数倍;
  • shmflg:控制共享内存的创建和访问权限,通过位或 | 组合,其可选标志位如下。
标志位值(十六进制)说明
IPC_CREAT0x200创建新段或获取已存在段
IPC_EXCL0x400与 IPC_CREAT 配合,确保创建新段
SHM_HUGETLB0x4000使用大页内存
SHM_NORESERVE0x1000不预留交换空间
SHM_HUGE_2MB(21 << 26)2MB 大页
SHM_HUGE_1GB(30 << 26)1GB 大页
S_IRUSR0x100用户读权限
S_IWUSR0x080用户写权限
S_IRGRP0x020组读权限
S_IWGRP0x010组写权限
S_IROTH0x004其他用户读权限
S_IWOTH0x002其他用户写权限

        函数的返回值,如果成功则返回内存IP,如果失败则返回-1。

        下面我们来使用shmget()函数来创建一块共享内存,这里我们使用IPC_PRIVATE进行创建:

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

#define BUFF 4096

int main(int argc, char const *argv[])
{
    system("ipcs -m");//查看当前共享内存

    //共享内存标识符
    int shm_id;
    shm_id = shmget(IPC_PRIVATE,BUFF,IPC_CREAT | 0666);

    if(shm_id < 0)//创建共享内存失败
    {
        perror("shmget");
        exit(1);
    }

    printf("成功创建共享内存:%d\n",shm_id);
    system("ipcs -m");//查看当前共享内存

    return 0;
}

        编写Makefile:

cc := gcc

shmget_test : shmget_test.c
	-$(cc) -o $@ $^
	-./$@
	-rm ./$@

        可以看到创建了一个4096的共享内存:

        对于使用ftok()生成键值,部分代码如下:

#include <sys/ipc.h>

key_t key = ftok("/some/existing/file", 'A');
shm_id = shmget(key, BUFF, IPC_CREAT | 0666);

        对于使用固定数值(需要确保选择的键值不会与其他应用冲突):

#define MY_SHM_KEY 0x1234
// 或
#define MY_SHM_KEY 5678

shm_id = shmget(MY_SHM_KEY, BUFF, IPC_CREAT | 0666);
方法优点缺点适用场景
IPC_PRIVATE永不冲突,简单安全只能亲缘进程使用父子进程通信
ftok()标准方法,无亲缘关系可用依赖文件存在性任意进程通信
固定值完全控制,性能好可能冲突,需要协调内部系统,测试

2.2  shmctl()函数——删除共享内存

        共享内存与消息队列以及信号量相同,在使用完毕后都应该进行释放,另外,当调用fork(函数创建子进程时,子进程会继承父进程已绑定的共享内存;当调用exec函数更改子进程功能以及调用exit()函数时,子进程中都会解除与共享内存的映射关系,因此在必要时仍应使用shmctl0函数对共享内存进行删除。

        表头文件:

#include <sys/shm.h>

        函数定义:

/* The following System V style IPC functions implement a shared memory
   facility.  The definition is found in XPG4.2.  */

/* Shared memory control operation.  */
#ifndef __USE_TIME_BITS64
extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW;
#else
  • shmid:共享内存标识符,指定要操作的共享内存段,其来源由 shmget() 函数返回的正整数;
  • cmd:控制命令,指定要执行的操作类型;
  • buf:缓冲区指针,根据不同的 cmd,此参数有不同的用途。

        其中对于cmd的一些类型:

命令参数类型作用返回值
IPC_STATstruct shmid_ds *获取段状态0/-1
IPC_SETstruct shmid_ds *设置段参数0/-1
IPC_RMIDNULL标记删除段0/-1
IPC_INFOstruct shminfo *获取系统限制最大索引/-1
SHM_INFOstruct shm_info *获取使用统计最大索引/-1
SHM_STATstruct shmid_ds *通过索引获取信息shmid/-1
SHM_LOCKNULL锁定内存0/-1
SHM_UNLOCKNULL解除锁定0/-1

        我们创建在删除:

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

int main() 
{
    int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
    
    printf("创建共享内存: %d\n", shmid);
    system("ipcs -m");
    
    // 删除共享内存
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }
    
    printf("已标记删除共享内存\n");
    system("ipcs -m");
    
    return 0;
}

        可以看到上面创建了32829,下面给删除了:

        我们也可以直接输入相应的键值进行删除,如我们想要删除上方的32811,直接输进去即可:

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

int main() 
{
    system("ipcs -m");
    
    // 删除共享内存
    if (shmctl(32811, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }
    
    printf("已标记删除共享内存\n");
    system("ipcs -m");
    
    return 0;
}

2.3  shmat()函数——将共享内存段附加到进程的地址空间

        将共享内存段“附加”到当前进程的地址空间,返回其虚拟地址。

        表头文件:

#include <sys/shm.h>

        函数定义:

/* Attach shared memory segment.  */
extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg)
  • shmid:共享内存段标识符,必须是已存在的共享内存段 ID,由 shmget() 函数返回的有效标识符;
  • shmaddr:指定期望的附加地址,NULL(推荐)让内核自动选择可用地址,或者尝试在指定地址附加;
  • shmflg:控制附加行为的标志位,常用标志位如下。
标志说明
00默认,读写方式附加
SHM_RDONLY010000只读方式附加
SHM_RND020000自动对齐 shmaddr
SHM_REMAP040000强制重新映射(Linux 扩展)
SHM_EXEC0100000内存可执行(Linux 扩展)

        其返回值,若是成功则返回实际引入的地址,如果失败,则返回-1。

        编写代码:

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

int main() 
{
    int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
    
    printf("创建共享内存: %d\n", shmid);
    system("ipcs -m");

    char* shm_addr = shmat(shmid, NULL, 0);
    if (shm_addr == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }
    printf("共享内存附加成功 at: %p\n", shm_addr);
    system("ipcs -m");
    
    // 删除共享内存
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }
    
    printf("已标记删除共享内存\n");
    system("ipcs -m");
    
    return 0;
}

        我们首先成功创建 shmid=65554,其连接数为0,状态正常,而后成功附加到地址0x7dd226f2d000,此时的连接数从0变为1,当我们删除65554后,其状态变为"目标",连接数仍为1(因为进程还未分离),当整个工程结束,我们通过 ipcs 查看可以发现已经删除过了:

2.4  shmdt()函数——进程分离共享内存段

        头文件:

#include <sys/shm.h>

        函数原型:

/* Detach shared memory segment.  */
extern int shmdt (const void *__shmaddr) __THROW;

        shmaddr:要分离的共享内存地址指针,必须是 shmat() 函数返回的有效地址,必须是当前进程已附加的共享内存地址。

        成功返回 0,失败返回-1。

        基础用法,往共享内存中写入数据:

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

int main() {
    int shmid;
    char *shm_addr;
    
    // 创建共享内存
    shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }
    
    printf("创建共享内存成功: shmid=%d\n", shmid);
    
    // 附加共享内存
    shm_addr = shmat(shmid, NULL, 0);
    if (shm_addr == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }
    
    printf("共享内存附加成功 at: %p\n", shm_addr);
    
    // 使用共享内存
    strcpy(shm_addr, "Hello, Shared Memory!");
    printf("写入的数据: %s\n", shm_addr);
    
    // 分离共享内存 - 核心调用
    if (shmdt(shm_addr) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }
    
    printf("共享内存分离成功\n");
    
    // 删除共享内存段
    shmctl(shmid, IPC_RMID, NULL);
    
    return 0;
}

3.  POSIX IPC

        这是更现代、符合 POSIX 标准的方法,推荐在新项目中使用,关键步骤:

  • shm_open:使用一个名字(如 /my_shm)来创建或打开一个共享内存对象。它返回一个文件描述符。
  • ftruncate:设置共享内存对象的大小。
  • mmap:将共享内存对象映射到进程的地址空间。
  • 读写操作:通过 shm_addr 指针进行。
  • munmap:解除内存映射。
  • close:关闭文件描述符。
  • shm_unlink:删除共享内存对象(当所有进程都关闭后,内核会释放资源)。

3.1  shm_open()函数——创建或打开一个共享内存对

        头文件:

#include <sys/mman.h>

        函数原型:

/* Open shared memory segment.  */
extern int shm_open (const char *__name, int __oflag, mode_t __mode);
  • name:共享内存对象的名字,其名字长度有限制(通常最多 255 字符),格式应该以 / 开头,如 "/my_shm";
  • oflag:打开标志,控制创建和打开行为;
O_RDONLY    // 只读
O_RDWR      // 读写
O_CREAT     // 如果不存在则创建
O_EXCL      // 与 O_CREAT 一起使用,如果已存在则失败
O_TRUNC     // 如果已存在,将其截断为0长度
  • mode:权限模式(当创建新对象时使用),八进制权限,类似文件权限。

3.2  shm_unlink()函数——删除共享内存

        删除一个先前由 shm_open() 创建的命名共享内存对象。尽管这个函数被称为“unlink”,但它并没有真正删除共享内存段本身,而是移除了与共享内存对象关联的名称,使得通过该名称无法再打开共享内存。当所有已打开该共享内存段的进程关闭它们的描述符后,系统才会真正释放共享内存资源。

        头文件:

#include <sys/mman.h>

        函数原型:

/* Remove shared memory segment.  */
extern int shm_unlink (const char *__name);
  • name: 要删除的共享内存对象名称

3.3  truncate()和ftruncate()——将文件缩放到指定大小

        truncate和ftruncate都可以将文件缩放到指定大小,二者的行为类似:如果文件被缩小,截断部分的数据丢失,如果文件空间被放大,扩展的部分均为\0字符。缩放前后文件的偏移量不会更改。缩放成功返回0,失败返回-1。

        不同的是,前者需要指定路径,而后者需要提供文件描述符;ftruncate缩放的文件描述符可以是通过shm_open()开启的内存对象,而truncate缩放的文件必须是文件系统已存在文件,若文件不存在或没有权限则会失败。

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

/**
 * 将指定文件扩展或截取到指定大小
 * 
 * char *path: 文件名 指定存在的文件即可 不需要打开
 * off_t length: 指定长度 单位字节
 * return: int 成功 0
 *             失败 -1
 */
int truncate(const char *path, off_t length);
/**
 *  将指定文件描述符扩展或截取到指定大小
 * 
 * int fd: 文件描述符 需要打开并且有写权限
 * off_t length: 指定长度 单位字节
 * return: int 成功 0
 *             失败 -1
 */
int ftruncate(int fd, off_t length);

3.4  mmap()函数——将一组设备或者文件映射到内存地址

        mmap系统调用可以将一组设备或者文件映射到内存地址,我们在内存中寻址就相当于在读取这个文件指定地址的数据。父进程在创建一个内存共享对象并将其映射到内存区后,子进程可以正常读写该内存区,并且父进程也能看到更改。使用man 2 mmap查看该系统调用声明:

#include <sys/mman.h>

/**
 * 将文件映射到内存区域,进程可以直接对内存区域进行读写操作,就像操作普通内存一样,但实际上是对文件或设备进行读写,从而实现高效的 I/O 操作
 * 
 * void *addr: 指向期望映射的内存起始地址的指针,通常设为 NULL,让系统选择合适的地址
 * size_t length: 要映射的内存区域的长度,以字节为单位
 * int prot: 内存映射区域的保护标志,可以是以下标志的组合
 *          (1) PROT_READ: 允许读取映射区域
 *          (2) PROT_WRITE: 允许写入映射区域
 *          (3) PROT_EXEC: 允许执行映射区域
 *          (4) PROT_NONE: 页面不可访问
 * int flags:映射选项标志
 *          (1) MAP_SHARED: 映射区域是共享的,对映射区域的修改会影响文件和其他映射到同一区域的进程(一般使用共享)
 *          (2) MAP_PRIVATE: 映射区域是私有的,对映射区域的修改不会影响原始文件,对文件的修改会被暂时保存在一个私有副本中
 *          (3) MAP_ANONYMOUS: 创建一个匿名映射,不与任何文件关联
 *          (4) MAP_FIXED: 强制映射到指定的地址,如果不允许映射,将返回错误
 * int fd: 文件描述符,用于指定要映射的文件或设备,如果是匿名映射,则传入无效的文件描述符(例如-1)
 * off_t offset: 从文件开头的偏移量,映射开始的位置
 * return void*: (1) 成功时,返回映射区域的起始地址,可以像操作普通内存那样使用这个地址进行读写
 *               (2) 如果出错,返回 (void *) -1,并且设置 errno 变量来表示错误原因
 */
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
/**
 * 用于取消之前通过 mmap() 函数建立的内存映射关系
 * 
 * void *addr: 这是指向之前通过 mmap() 映射的内存区域的起始地址的指针,这个地址必须是有效的,并且必须是 mmap() 返回的有效映射地址
 * size_t length: 这是要解除映射的内存区域的大小(以字节为单位),它必须与之前通过 mmap() 映射的大小一致
 * return: int 成功 0
 *             失败 -1
 */
int munmap(void *addr, size_t length);

3.5  总结使用

        简单来说就是父进程读取子进程在共享内存写入的数据:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>

int main()
{
    char *share;
    pid_t pid;
    char shmName[100] = {0};
    
    sprintf(shmName, "/letter%d", getpid());
    
    // 共享内存对象的文件标识符
    int fd;
    fd = shm_open(shmName, O_CREAT | O_RDWR, 0644);
    if (fd < 0)
    {
        perror("共享内存对象开启失败!\n");
        exit(EXIT_FAILURE);
    }
    
    // 将该区域扩充为100字节长度
    ftruncate(fd, 100);
    
    // 以读写方式映射该区域到内存,并开启父子共享标签 偏移量选择0从头开始
    share = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    
    // 注意:不是p == NULL 映射失败返回的是((void *) -1)
    if (share == MAP_FAILED)
    { 
        perror("共享内存对象映射到内存失败!\n");
        exit(EXIT_FAILURE);
    }
    
    // 映射区建立完毕,关闭读取连接 注意不是删除
    close(fd);
    
    // 创建子进程
    pid = fork(); 
    
    if (pid == 0)
    {
        // 子进程写入数据作为回信 
        strcpy(share, "我收到信了!\n");
        printf("子进程%d完成回信!\n", getpid());
    }
    else
    {
        // 等待回信
        sleep(1);
        printf("父进程%d看到子进程%d回信的内容: %s", getpid(), pid, share);
        
        // 等到子进程运行结束
        wait(NULL);
        
        // 释放映射区
        int ret = munmap(share, 100); 
        if (ret == -1)
        {
            perror("munmap");
            exit(EXIT_FAILURE);
        }
    }
    
    // 删除共享内存对象
    shm_unlink(shmName);
    
    return 0;
}

嵌入式Linux_时光の尘的博客-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时光の尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值