Linux C语言 41-进程间通信IPC之共享内存

Linux C语言 41-进程间通信IPC之共享内存

本节关键字:C语言 进程间通信 共享内存 shared memory

相关库函数:shmget、shmat、shmdt、shmctl

什么是共享内存?

共享内存(Shared Memory)指两个或多个进程共享一个给定的存储区。

共享内存的特点

  • 共享内存是最快的只用System V IPC,因为进程是直接对内存进行读写;
  • 因为多个进程可以同时操作,所以需要进程同步;
  • 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问;
  • 共享内存被创建成功后需要手动释放,否则将持续到内核重新引导。

共享内存相关库函数

创建或链接一个共享内存

当创建新的共享内存段时,其内容被初始化为零,并且其相关的数据结构shmid_ds(参见shmctl(2))被初始化如下:

  • shm_perm.uid 和shm_perm.uid 被设置为调用进程的有效用户ID。
  • shm_perm.cgid 和shm_perm_gid 被设置为调用进程的有效组ID。
  • shm_perm.mode 的最低有效9位被设置为shmflg 的最低有效的9位。
  • shm_setsz 设置为size 的值。
  • shm_lpid、shm_natch、shm_time和shm_dtime 设置为0。
  • shm_time 设置为当前时间。

如果共享内存段已经存在,则会验证权限,并检查它的销毁标记是否有效。

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
key_t ftok(const char *pathname, int proj_id); // 前文消息队列中已经介绍过,这里就不展开介绍了
int shmget(key_t key, size_t size, int shmflg);
/**
@brief 分配共享内存段。shmget()返回与参数key的值相关联的共享内存段的标识符。如果key的值为IPC_PRIVATE或key不是IPC_PRIVATE,不存在与该密钥对应的共享内存段,并且在shmflg中指定了IPC_CREAT,则会创建一个新的共享内存分段,其大小等于四舍五入到PAGE_SIZE的倍数。如果shmflg同时指定了IPC_CREAT和IPC_EXCL,并且key的共享内存段已经存在,那么shmget()将失败,errno设置为EEXIST。(这类似于组合O_CREAT | O_EXCL对open(2)的效果。)
@param key 进程间通信的键值(密钥),是ftok()的返回值
@param size 该共享内存的字节长度
@param shmflg 表示函数的行为及共享内存的权限,取值如下:
	IPC_CREAT    如果不存在就创建
	IPC_EXCL    如果存在则返回失败
	mode_flags(最低有效9位),指定授予所有者、组和世界的权限。这些位具有与open(2)的模式自变量相同的格式和含义。目前,系统未使用执行权限。
	SHM_HUGETLB 自Linux 2.6起)使用"巨大的页面"分配段。有关更多信息,请参阅内核源文件Documentation/vm/hugetlbpage.txt。
	SHM_NORESERVE (自Linux 2.6.15起)此标志的作用与mmap(2)MAP_NORESERVE标志相同。不要为该段保留交换空间。当保留交换空间时,可以保证可以修改段。当交换空间没有保留时,如果没有可用的物理内存,则可能会在写入时获得SIGSEGV。另请参阅proc(5)中文件/proc/sys/vm/overcommit_memory的讨论。
@return 成功返回有效的贡献内存标识符shmid,失败返回-1,并设置错误码errno

错误码errno的类型:
EACCES        用户没有访问共享内存段的权限,也没有CAP_IPC_OWNER功能。
EEXIST        指定了IPC_CREAT | IPC_EXCL,并且该段存在。
EINVAL        要创建一个新的段,其大小<SHMMIN或size>SHMMAX,或者不创建新的段、存在一个具有给定键的段,但大小大于该段的大小。
ENFILE        已达到打开文件总数的系统限制。
ENOENT        给定key不存在分段,并且未指定IPC_CREAT。
ENOMEM        无法为段开销分配内存。
ENOSPC        已获取所有可能的共享内存ID(SHMMNI),或者分配所请求大小的段将导致系统超过共享内存的系统范围限制(SHMALL)。
EPERM         指定了SHM_HUGETLB标志,但调用方没有特权(不具有CAP_IPC_LOCK功能)。

NOTES
IPC_PRIVATE不是标志字段,而是key_t类型。如果这个特殊值用于key,则系统调用将忽略shmflg的除最低有效9位之外的所有内容,并创建一个新的共享内存段。
以下对共享内存段资源的限制会影响shmget()调用:
SHMALL        系统范围内共享内存页的最大值(在Linux上,可以通过/proc/sys/kernel/SHMALL读取和修改此限制)。
SHMMAX        共享内存段的最大大小(以字节为单位):取决于策略(在Linux上,可以通过/proc/sys/kernel/SHMMAX读取和修改此限制)。
SHMMIN        共享内存段的最小大小(以字节为单位):取决于实现(当前为1字节,但PAGE_size是有效的最小大小)。
SHMMNI        系统范围内共享内存段的最大数量:取决于实现(目前为4096个,在Linux 2.3.99之前为128个;在Linux上,可以通过/proc/sys/kernel/SHMMNI读取和修改此限制)。
该实现对每个进程的共享内存段的最大数量(SHMSEG)没有特定限制。
*/
将共享内存映射到进程内存

一个成功的shmat()调用将更新与共享内存段相关联的shmid_ds结构的成员(请参见shmctl(2)),如下所示:

  • shm_time 设置为当前时间。
  • shm_lpid 设置为调用进程的进程ID。
  • shm_natch 增加1。
c 复制代码
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
/**
@brief shmat()将shmid标识的共享内存段附加到调用进程的地址空间。附加地址由shmaddr根据以下条件之一指定:
    如果shaddr为NULL,系统会选择一个合适的(未使用的)地址来附加段。
    如果shaddr不为NULL,并且在shmflg中指定了SHM_RND,则附加发生在等于shaddr的地址处,四舍五入到SHMLBA的最近倍数。否则,shaddr必须是发生附加的页面对齐地址。
    如果在shmflg中指定了SHM_RDONLY,则附加该段进行读取,并且进程必须具有该段的读取权限。否则,将附加段进行读写操作,并且进程必须具有该段的读写权限。不存在只写共享内存段的概念。
@param shmid 共享内存的标识符,是shmget()的成功返回值
@param shmaddr 共享内存映射地址(为NULL则系统自动指定),一般使用NULL
@param shmflg 共享内存端的访问权限和映射条件(一般设置为0),具体取值如下:
    0 共享内存具有可读可写权限
    SHM_RDONLY 只读
    SHM_RND shmaddr非空时才有效
@return 成功返回附加的共享内存段的地址;失败返回-1,并设置错误码errno

错误码errno的类型:
EACCES        调用进程不具有所请求的附加类型所需的权限,也不具有CAP_IPC_OWNER功能。
EIDRM         shmid指向一个已删除的标识符。
EINVAL        无效的shmid值、未对齐(即未对齐页面且未指定SHM_RND)或无效的shaddr值,或者无法在shaddr处附加段,或者指定了SHM_REMAP且shaddr为NULL。
ENOMEM        无法为描述符或页表分配内存。
*/

#### 解除共享内存的映射
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
/**
@brief shmdt()从调用进程的地址空间中分离位于shaddr指定地址的共享内存段。
要分离的段当前必须附加等于附加shmat()调用返回值的shaddr。在成功调用shmdt()时,系统会更新与共享内存段相关联的shmid_ds结构的成员,如下所示:
    shm_dtime 设置为当前时间。
    shm_lpid  设置为调用进程的进程ID。
    shm_natch 递减一。如果它变为0并且该段被标记为删除,则该段被删除。
@param shmaddr 共享内存的参数地址
@return 成功返回0,失败返回-1,并设置错误码errno

错误码errno的类型:
EINVAL        shaddr上没有附加共享内存段;或者,shaddr没有在页面边界上对齐。
*/
控制共享内存
c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

struct ipc_perm 
{
    key_t          __key;    // shmget(2)提供的密钥
    uid_t          uid;      // 所有者的有效UID
    gid_t          gid;      // 所有者的有效GID
    uid_t          cuid;     // 创建者的有效UID
    gid_t          cgid;     // 创建者的有效GID
    unsigned short mode;     // 权限+SHM_DEST和SHM_LOCKED标志
    unsigned short __seq;    // 序列号码
};

struct shmid_ds 
{
    struct ipc_perm shm_perm;    // 所有权和权限
    size_t          shm_segsz;   // 段大小(字节)
    time_t          shm_atime;   // 上次连接时间
    time_t          shm_dtime;   // 上次分离时间
    time_t          shm_ctime;   // 上次修改时间
    pid_t           shm_cpid;    // 创建者进程PID
    pid_t           shm_lpid;    // 上次执行shmat(2)或shmdt(2)的进程PID
    shmatt_t        shm_nattch;  // 当前附件数量
    ...
};

struct  shminfo 
{
    unsigned long shmmax; // 共享内存最大分段长度
    unsigned long shmmin; // 共享内存最小分段长度,总为1
    unsigned long shmmni; // 共享内存最大分段数
    unsigned long shmseg; // 进程可以附加的最大段数;内核中未使用
    unsigned long shmall; // 系统范围内共享内存的最大页数
    
    // shmmni、shmmax和shmall设置可以通过相同名称的/proc文件进行更改;有关详细信息,请参见proc(5)。
};

struct shm_info
{
    int           used_ids;           // # of currently existing segments
    unsigned long shm_tot;            // Total number of shared memory pages
    unsigned long shm_rss;            // # of resident shared memory pages
    unsigned long shm_swp;            // # of swapped shared memory pages
    unsigned long swap_attempts;      // Unused since Linux 2.4
    unsigned long swap_successes;     // Unused since Linux 2.4
};

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/**
@brief 获取和设置消息队列的属性
@param shmid 共享内存的标识符
@param cmd 函数功能的控制,取值如下:
    IPC_STAT        将信息从与shmid关联的内核数据结构复制到buf指向的shmid_ds结构中。调用方必须具有对共享内存段的读取权限。
    IPC_SET         将buf指向的shmid_ds结构的一些成员的值写入与该共享内存段相关联的内核数据结构,同时更新其shm_time成员。可以更改以下字段:shm_perm.uid、shm_perm_gid和(shm_per.mode的最低有效9位)。调用进程的有效uid必须与共享内存段的所有者(shm_erm.uid)或创建者(shm_term.cuid)匹配,或者调用方必须具有特权。
    IPC_RMID        标记要销毁的段。只有在最后一个进程将该段分离后(即,当关联结构shmid_ds的shm_natch成员为零时),该段才会被实际销毁。调用方必须是所有者或创建者,或者具有特权。如果某个段已标记为要销毁,则将设置IPC_STAT检索到的相关数据结构中的SHM_perm.mode字段的(非标准)SHM_DEST标志。调用方必须确保一个段最终被销毁;否则,其在中出现故障的页面将保留在内存或交换中。
    IPC_INFO        (特定于Linux)返回有关buf指向的结构中的系统范围共享内存限制和参数的信息。如果定义了_GNU_SOURCE功能测试宏,则该结构的类型为shminfo(因此,需要强制转换),在<sys/shm.h>中定义: struct shminfo;(结构内容及注释在上方展示)
    SHM_INFO        (特定于Linux)返回一个shm_info结构,其字段包含有关共享内存所消耗的系统资源的信息。如果定义了_GNU_SOURCE功能测试宏,则在<sys/shm.h>中定义此结构:struct shm_info;(结构内容及注释在上方展示)
    SHM_STAT        (特定于Linux)返回与IPC_STAT相同的shmid_ds结构。但是,shmid参数不是段标识符,而是内核内部数组的索引,该数组维护系统上所有共享内存段的信息。


调用方可以阻止或允许使用以下cmd值交换共享内存段:
     SHM_LOCK        (特定于Linux)防止交换共享内存段。调用方必须在启用锁定后所需的任何页面中出错。如果某个段已被锁定,则将设置IPC_STAT检索到的相关数据结构中的SHM_perm.mode字段的(非标准)SHM_locked标志。
     SHM_UNLOCK      (特定于Linux)解锁分段,允许将其交换出去。

在2.6.10之前的内核中,只有特权进程可以使用SHM_LOCK和SHM_UNLOCK。由于内核2.6.10,如果非特权进程的有效UID与段的所有者或创建者UID匹配,并且(对于SHM_LOCK)要锁定的内存量在RLIMIT_MEMLOCK资源限制范围内,则该进程可以使用这些操作(请参见setrlimit(2))。

@param buf  shmid_ds 数据类型的地址,用来存放或修改共享内存的属性。
@return 成功的IPC_INFO或SHM_INFO操作返回内核内部数组中使用次数最多的条目的索引,该数组记录了有关所有共享内存段的信息。(此信息可与重复的SHM_STAT操作一起使用,以获得有关系统上所有共享内存段的信息。)成功的SHM_TAT操作将返回共享内存段(其索引在shmid中给出)的标识符。其他操作成功后返回0。失败返回-1,并设置错误码errno

错误码errno的类型:
EACCES        IPC_STAT或SHM_STAT被请求,并且SHM_perm.mode不允许对shmid进行读取访问,并且调用进程不具有CAP_IPC_OWNER功能。
EFAULT        参数cmd的值为IPC_SET或IPC_STAT,但buf指向的地址不可访问。
EIDRM         shmid指向一个已删除的标识符。
EINVAL        shmid不是有效的标识符,或者cmd不是有效命令。或者:对于SHM_STAT操作,shmid中指定的索引值引用了当前未使用的数组槽。
ENOMEM        (在2.6.9之后的内核中),指定了SHM_LOCK,并且要锁定的段的大小意味着锁定的共享内存段中的总字节数将超过调用进程的真实用户ID的限制。此限制由RLIMIT_MEMLOCK软资源限制定义(请参阅setrlimit(2))。
EOVERFLOW    尝试IPC_STAT,但GID或UID值太大,无法存储在buf指向的结构中。
EPERM        尝试IPC_SET或IPC_RMID,并且调用进程的有效用户ID不是创建者(在shm_perm.cuid中找到)或所有者(在shr_perm.uid中找到)的ID,并且进程没有特权(Linux:不具有CAP_SYS_ADMIN功能)。或者(在2.6.9之前的内核中),指定了SHM_LOCK或SHM_UNLOCK,但进程没有特权(Linux:没有CAP_IPC_LOCK功能)。(从Linux 2.6.9开始,如果RLIMIT_MEMLOCK为0并且调用方没有特权,也可能发生此错误。)

NOTES
ipcs(8)程序使用IPC_INFO、SHM_STAT和SHM_INFO操作来提供有关所分配资源的信息。将来,这些文件可能会被修改或移动到/proc文件系统接口。Linux允许进程使用shmctl(IPC_RMID)附加(shmat(2))已标记为删除的共享内存段。此功能在其他Unix实现中不可用;可移植应用程序应避免依赖它。
结构shmid_ds中的各种字段在Linux 2.2下被键入为短字段,而在Linux 2.4下则变为长字段。为了利用这一点,在glibc-2.1.91或更高版本下重新编译就足够了。(内核通过cmd中的IPC_64标志来区分新旧调用。)
*/

共享内存例程

c 复制代码
/**
 * 共享内存使用例程,创建、连接、写、读、断开连接、删除
 * 目前未增加线程安全措施,后期可以增加互斥锁等
 */

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

#define SHM_FULL            1
#define SHM_LINES_MAX       10
#define SHM_LINE_LEN_MAX    128

typedef struct 
{
    int  w_idx, full;
    char msg[SHM_LINES_MAX][SHM_LINE_LEN_MAX];
    int  len[SHM_LINES_MAX];
} SHM;

void shmwrite(SHM *shm, const char *msg, size_t len);
char *shmread(SHM *shm);

int main() 
{
    key_t shmid;
    SHM *shmaddr = NULL;
    char message[SHM_LINE_LEN_MAX];
    
    // 创建一个新的共享内存段,也可自行指定key值(将IPC_PRIVARE换成其它值,例如:6565656)
    shmid = shmget(IPC_PRIVATE, sizeof(SHM), IPC_CREAT | 0666);
    if (shmid == -1) 
    {
        perror("shmget");
        exit(EXIT_FAILURE);
    }
    
    // 连接到共享内存段
    shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) 
    {
        perror("shmat");
        exit(EXIT_FAILURE);
    }
    
    printf("shared memory: shmid[%d], address[%p]\n", shmid, shmaddr);
    
    // 对共享内存进行读写等操作...
    while (shmaddr->full != SHM_FULL)
    {
        printf("please input your message: ");
        bzero(message, sizeof(message));
        scanf("\n%[^\n]", message); //注意尝试思考和直接使用 %s的区别哦^!^
        
        shmwrite(shmaddr, message, strlen(message));
        
        printf("reading shared memory data...\n");
        shmread(shmaddr);
        
        // 此过程中可以在新开命令窗口中使用 ipcs -m 查看新建共享内存的相关信息
    }
    
    printf("shared memory will be deleted in 5 seconds\n");
    sleep(5);
    
    // 从共享内存分离
    if (shmdt(shmaddr) != 0) 
    {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }
    
    // 删除共享内存段
    if (shmctl(shmid, IPC_RMID, NULL) != 0) 
    {
        perror("shmctl");
        exit(EXIT_FAILURE);
    }
    
    // 此时再使用 ipcs -m 查看创建的共享内存已经不存在了
    printf("shared memory deleted: shmid[%d], address[%p]\n", shmid, shmaddr);
    return 0;
}

void shmwrite(SHM *shm, const char *msg, size_t len)
{
    if (shm->full == SHM_FULL)
        return;
    
    strncpy(shm->msg[shm->w_idx], msg, len);
    shm->len[shm->w_idx] = len;
    shm->w_idx++;
    
    if (shm->w_idx == SHM_LINES_MAX)
        shm->full = SHM_FULL;
}

char *shmread(SHM *shm)
{
    int i, count;
    if (shm->w_idx == 0)
    {
        printf("share memory is empty\n");
        return;
    }
    
    count = shm->w_idx;
    for (i=0; i<count; i++)
    {
        printf("[%d]: %s\n", i, shm->msg[i]);
    }
    
    return shm->msg[count];
}

提示:先做内容框架梳理,后期进行完善补充!

相关推荐
JiMoKuangXiangQu3 分钟前
Linux 内存管理:页表管理简析
linux·mmu·内存管理·页表管理
WG_174 分钟前
Linux:缓冲区_glibc封装
linux·运维·服务器
番知了7 分钟前
Ubuntu 22.04 常用命令清单
linux·运维·ubuntu
学IT的周星星8 分钟前
java常见面试题
java·开发语言
Macbethad8 分钟前
Linux网关应用技术报告
网络
旺仔Sec9 分钟前
2026年河北省职业院校技能大赛“网络系统管理”(高职组)网络构建样题
运维·服务器·网络
wjs202418 分钟前
XPath 运算符
开发语言
Mr.朱鹏21 分钟前
大模型入门学习路径(Java开发者版)上
java·开发语言·spring boot·spring·大模型·llm·transformer
黎雁·泠崖24 分钟前
C 语言指针进阶教程:const 修饰、野指针规避与传址调用
c语言·开发语言
lsx20240628 分钟前
ASP TextStream
开发语言