Linux 进程间通信--system V 共享内存

Linux 进程间通信--system V 共享内存

七月 20, 2025 次阅读

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

shared_momery

下面来介绍一下共享内存函数

函数讲解

shmget 函数

功能描述

shmget函数用于创建或获取一个共享内存段。共享内存是进程间通信(IPC)的一种方式,允许多个进程访问同一块内存区域,从而实现高效的数据共享。

函数原型

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

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

参数说明

  • key: 共享内存段的标识键值

    • 可以是由ftok()生成的键值,也可以是常量IPC_PRIVATE(表示创建私有共享内存)
    • 不同进程通过相同的key值访问同一个共享内存
  • size: 请求的共享内存段大小(以字节为单位)

    • 如果是创建新共享内存,必须指定size
    • 如果是获取已存在的共享内存,size可以设为0
  • shmflg: 权限标志和创建选项的组合

    • 权限标志:与文件权限类似,如0644(八进制)
    • 创建选项:
      • IPC_CREAT: 如果共享内存不存在则创建,否则获取已存在的
      • IPC_EXCL: 与IPC_CREAT一起使用时,如果共享内存已存在则返回错误
      • IPC_NOWAIT: 如果内存段需要等待(如交换空间不足),不等待直接返回错误

返回值

  • 成功: 返回共享内存段的标识符(非负整数)
  • 失败: 返回-1,并设置errno指示错误原因

使用示例

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

#define SHM_SIZE 1024  // 1KB共享内存

int main() {
    key_t key = ftok("shmfile", 65);  // 生成key
    
    // 创建共享内存(如果已存在则失败)
    int shmid = shmget(key, SHM_SIZE, 0666|IPC_CREAT|IPC_EXCL);
    if (shmid == -1) {
        perror("shmget failed");
        return 1;
    }
    
    printf("Shared memory created with ID: %d\n", shmid);
    return 0;
}

注意事项

  1. 使用IPC_PRIVATE作为key时,总是创建新的共享内存段
  2. 共享内存创建后不会自动初始化,内容是不确定的
  3. 需要配合shmat()shmdt()函数来连接和分离共享内存
  4. 使用完毕后应使用shmctl()删除共享内存段
  5. 在多进程环境中需要考虑同步问题(如使用信号量)

shmat 函数

功能描述

shmat函数(Shared Memory Attach)用于将共享内存段连接到调用进程的地址空间中,使进程能够访问该共享内存区域。

函数原型

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

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

参数说明

  • shmid: 共享内存标识符,由shmget函数返回的值
  • shmaddr: 指定共享内存连接到进程地址空间的地址
    • 为NULL时,系统自动选择合适的地址(最常用方式)
    • 非NULL时,尝试在指定地址连接(需要特殊权限)
  • shmflg: 连接选项标志
    • SHM_RDONLY: 以只读方式连接共享内存
    • SHM_RND: 当shmaddr非NULL时,将连接地址向下舍入到SHMLBA(低边界地址)的倍数
    • 0: 默认读写权限

返回值

  • 成功: 返回指向共享内存段的指针(连接地址)
  • 失败: 返回(void *)-1,并设置errno指示错误原因

详细说明

  1. 自动地址选择
    shmaddr参数为NULL时,系统会自动选择一个合适的、未使用的地址来映射共享内存段。这是最常用、最安全的方式。

  2. 指定地址连接
    shmaddr非NULL且未设置SHM_RND标志时,系统会尝试在指定的确切地址连接共享内存段。这需要该地址可用且满足对齐要求,通常需要特殊权限。

  3. 地址舍入
    shmaddr非NULL且设置了SHM_RND标志时,实际连接地址会按照公式调整:
    实际地址 = shmaddr - (shmaddr % SHMLBA)
    其中SHMLBA是”段低边界地址”(Segment Low Boundary Address),通常是内存页大小的倍数。

  4. 只读模式
    设置SHM_RDONLY标志时,进程只能读取共享内存内容,不能修改。其他进程可能仍具有读写权限(取决于创建时的权限设置)。

使用示例

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

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("shmfile", 65);
    int shmid = shmget(key, SHM_SIZE, 0666|IPC_CREAT);
    
    // 连接共享内存(让系统自动选择地址)
    char *str = (char*) shmat(shmid, NULL, 0);
    if (str == (char*)-1) {
        perror("shmat failed");
        return 1;
    }
    
    printf("Shared memory attached at address: %p\n", str);
    
    // 使用共享内存...
    sprintf(str, "Hello Shared Memory");
    
    // 分离共享内存
    shmdt(str);
    return 0;
}

注意事项

  1. 连接后的共享内存就像普通内存一样使用,但要注意进程间的同步问题
  2. 使用完毕后应调用shmdt函数分离共享内存
  3. 多次连接同一共享内存段会返回不同的地址
  4. 共享内存的删除(shmctl)在所有进程都分离后才会实际生效
  5. 在多线程/多进程环境中访问共享内存时,必须实现适当的同步机制(如信号量)

shmctl 函数

功能描述

shmctl(Shared Memory Control)用于控制共享内存段,包括查询状态、修改权限或删除共享内存。


函数原型

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

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

参数说明

参数 说明
shmid 共享内存标识符,由 shmget 返回
cmd 控制命令,决定操作类型(见下表)
buf 指向 struct shmid_ds 的指针,用于存储或设置共享内存信息

cmd 命令取值及作用

命令 说明
IPC_STAT 获取共享内存的当前状态,并存入 buf 指向的结构体
IPC_SET 修改共享内存的权限、属主等属性(需权限)
IPC_RMID 标记删除 共享内存段(在所有进程 shmdt 后真正释放)

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno(如 EPERM 权限不足、EINVAL 无效参数等)

struct shmid_ds 结构体(部分关键字段)

struct shmid_ds {
    struct ipc_perm shm_perm;  // 权限信息
    size_t          shm_segsz; // 共享内存大小
    pid_t           shm_cpid;  // 创建者PID
    pid_t           shm_lpid;  // 最后操作进程PID
    time_t          shm_atime; // 最后访问时间
    time_t          shm_dtime; // 最后分离时间
    time_t          shm_ctime; // 最后修改时间
    // ...
};

使用示例

1. 查询共享内存状态(IPC_STAT
struct shmid_ds shm_info;
if (shmctl(shmid, IPC_STAT, &shm_info) == -1) {
    perror("shmctl IPC_STAT failed");
    exit(1);
}
printf("Shared Memory Size: %zu\n", shm_info.shm_segsz);
2. 修改共享内存权限(IPC_SET
struct shmid_ds shm_info;
shmctl(shmid, IPC_STAT, &shm_info);  // 先获取当前状态
shm_info.shm_perm.mode = 0640;       // 修改权限
if (shmctl(shmid, IPC_SET, &shm_info) == -1) {
    perror("shmctl IPC_SET failed");
    exit(1);
}
3. 删除共享内存(IPC_RMID
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
    perror("shmctl IPC_RMID failed");
    exit(1);
}
printf("Shared memory marked for deletion.\n");

注意IPC_RMID 只是标记删除,实际释放需所有进程 shmdt


注意事项

  1. IPC_RMID 不会立即删除共享内存,而是等所有进程 shmdt 后才真正释放。
  2. IPC_SET 需要权限(如 root 或共享内存创建者)。
  3. shmctl + IPC_STAT 可用于监控共享内存使用情况(如检查最后访问时间)。
  4. 多进程共享内存时,需额外同步机制(如信号量)避免竞争

总结

操作 命令 用途
查询状态 IPC_STAT 获取共享内存信息
修改权限 IPC_SET 调整属主、权限等
删除内存 IPC_RMID 标记删除(需进程分离后生效)

shmctl 是共享内存管理的核心函数,通常与 shmgetshmatshmdt 配合使用。

指令讲解

查看当前所有共享内存

1. ipcs 命令

ipcs -m

选项说明:

  • -m: 只显示共享内存段
  • -a: 显示所有IPC信息(共享内存、消息队列、信号量)
  • -l: 显示系统限制
  • -p: 显示创建者和最后操作者的PID
  • -t: 显示时间信息

示例输出:

╭─ljx@VM-16-15-debian ~/linux_review/fifo
╰─➤  ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 0          root       600        80         2
0x00000000 1          root       600        16384      2
0x00000000 2          root       600        280        2
0xffffffff 3          ljx        666        4096       0

2. 查看更详细的信息

╭─ljx@VM-16-15-debian ~/linux_review/fifo
╰─➤  ipcs -m -i 3

Shared memory Segment shmid=3
uid=1003        gid=1003        cuid=1003       cgid=1003
mode=0666       access_perms=0666
bytes=4096      lpid=3988199    cpid=3988199    nattch=0
att_time=Sun Jul 20 21:49:15 2025
det_time=Sun Jul 20 21:49:26 2025
change_time=Sun Jul 20 21:49:15 2025

其中<shmid>是共享内存ID

删除共享内存

1. ipcrm 命令

ipcrm -m <shmid>

选项说明:

  • -m <shmid>: 删除指定的共享内存段
  • -M <shmkey>: 删除指定key值的共享内存段
  • -a: 删除当前用户拥有的所有IPC资源(慎用!)

示例:

╭─ljx@VM-16-15-debian ~/linux_review/fifo
╰─➤  ipcrm -m 3
╭─ljx@VM-16-15-debian ~/linux_review/fifo
╰─➤  ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 0          root       600        80         2
0x00000000 1          root       600        16384      2
0x00000000 2          root       600        280        2

2. 通过程序删除

在C程序中可以使用shmctl函数:

shmctl(shmid, IPC_RMID, NULL);

注意事项

  1. 删除共享内存需要相应权限(通常是创建者或root用户)
  2. 如果共享内存仍被进程附加(nattch>0),删除会被标记但不会立即生效
  3. 使用ipcs -m可以查看共享内存是否已被成功删除
  4. 对于残留的共享内存,可能需要root权限才能删除

综合使用示例

利用共享内存实现进程间通信

// common.hpp
#ifndef _COMMON_HPP_
#define _COMMON_HPP_ 1

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstring>

#define SHM_SIZE 4096

const char *pathname = "/home/ljx/linux_review/shared_memory";
const int proj_id = 669988;

enum global_error{
    shmid_error = 1,
    ftok_error,
};

// 创建共享内存
int shared_mem(int mod)
{
    key_t key = ftok(pathname, proj_id);
    if(key == -1)
    {
        perror("common.hpp: ftok failed");
        exit(global_error::ftok_error);
    }
    int shmid = shmget(key, SHM_SIZE, mod);
    if(shmid == -1)
    {
        perror("common.hpp: shmget failed");
        exit(global_error::shmid_error);
    }
    return shmid;
}

int create_shared_mem()
{
    return shared_mem(0666 | IPC_CREAT | IPC_EXCL);
}

int get_shared_mem()
{
    return shared_mem(IPC_CREAT);
}

#endif

服务端实现

#include "common.hpp"

int main()
{
    int shmid = create_shared_mem();
    // 将共享内存段连接到进程地址空间
    char * addr = (char*)shmat(shmid, nullptr, 0);
    while(true)
    {
        if(strcmp(addr, "quit") == 0) break;
        std::cout << "Client say: " << addr << std::endl;
        sleep(1);
    }
    shmdt(addr);
    shmctl(shmid, IPC_RMID, nullptr);
    return 0;
}

客户端实现

#include "common.hpp"

int main()
{
    int shmid = get_shared_mem();
    char *addr = (char *)shmat(shmid, nullptr, 0);
    while(true)
    {
        std::cout << "Please Enter: ";
        fgets(addr, SHM_SIZE, stdin);
        addr[strcspn(addr, "\n")] = '\0'; 
        if(strcmp(addr, "quit") == 0)
        {
            std::cout << "Bye!" << std::endl;
            break;
        }
    }
    shmdt(addr);
    return 0;
}

但是直接实现存在进程读取不同步问题,因此我们需要利用同步机制,我们可以简单使用命名管道的同步机制来简单实现一下同步:

// common.hpp
#ifndef _COMMON_HPP_
#define _COMMON_HPP_ 1

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SHM_SIZE 4096

const char *pathname = "/home/ljx/linux_review/shared_memory";
const int proj_id = 669988;
const char *file_name = "myfifo";
const int mode = 0664;
const int MAX_SIZE = 1024;

enum global_error{
    shmid_error = 1,
    ftok_error,
    mkfifo_error,
    unlink_error,
    write_error,
    read_error,
    open_error,
};

// 创建共享内存
int shared_mem(int mod)
{
    key_t key = ftok(pathname, proj_id);
    if(key == -1)
    {
        perror("common.hpp: ftok failed");
        exit(global_error::ftok_error);
    }
    int shmid = shmget(key, SHM_SIZE, mod);
    if(shmid == -1)
    {
        perror("common.hpp: shmget failed");
        exit(global_error::shmid_error);
    }
    return shmid;
}

int create_shared_mem()
{
    return shared_mem(0666 | IPC_CREAT | IPC_EXCL);
}

int get_shared_mem()
{
    return shared_mem(IPC_CREAT);
}

// fifo
class InitPipe{
public:
    InitPipe()
    {
        if(mkfifo(file_name, mode) == -1)
        {
            perror("InitPipe(): mkfifo error");
            exit(global_error::mkfifo_error);
        }
    }
    ~InitPipe()
    {
        if(unlink(file_name) == -1)
        {
            perror("~InitPipe(): unlink error");
            exit(global_error::unlink_error);
        }
    }
};

#endif

// server.cc
#include "common.hpp"

int main()
{
    int shmid = create_shared_mem();
    // 将共享内存段连接到进程地址空间
    char * addr = (char*)shmat(shmid, nullptr, 0);
    // 命名管道实现同步
    InitPipe init;
    int fd = open(file_name, O_RDONLY);
    if(fd == -1)
    {
        perror("server.o: open error");
        exit(global_error::open_error);
    }

    char ch;
    while(true)
    {
        read(fd, &ch, 1);
        if(strcmp(addr, "quit") == 0) break;
        std::cout << "Client say: " << addr << std::endl;
        sleep(1);
    }
    shmdt(addr);
    shmctl(shmid, IPC_RMID, nullptr);
    close(fd);
    return 0;
}

// client.cc
#include "common.hpp"

int main()
{
    int shmid = get_shared_mem();
    char *addr = (char *)shmat(shmid, nullptr, 0);
    // 命名管道实现同步
    int fd = open(file_name, O_WRONLY);

    while(true)
    {
        std::cout << "Please Enter: ";
        fgets(addr, SHM_SIZE, stdin);
        addr[strcspn(addr, "\n")] = '\0'; 
        if(strcmp(addr, "quit") == 0)
        {
            std::cout << "Bye!" << std::endl;
            break;
        }
        write(fd, "c", 1);
    }
    shmdt(addr);
    close(fd);
    return 0;
}

测试结果如下:

# client
╭─ljx@VM-16-15-debian ~/linux_review/shared_memory
╰─➤  ./client.o                                                                                                                      130 ↵
Please Enter: 123
Please Enter: 456
Please Enter: quit
Bye!
# server
╭─ljx@VM-16-15-debian ~/linux_review/shared_memory  
╰─➤  ./server.o
Client say: 123
Client say: 456

可以看到,我们利用命名管道间接实现了进程同步