SystemV标准的Linux进程间通信共享内存

Posted MangataTS

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SystemV标准的Linux进程间通信共享内存相关的知识,希望对你有一定的参考价值。

文章目录

一、systemV 标准

  • systemV是一种用于在操作系统层面上进行进程间通信的标准
  • systemV标准给用户提供了系统调用接口,只要我们使用它所提供的系统调用就可以完成进程间的通信;
  • systemV主要提供了三种操作:
    • 1.共享内存
    • 2.消息队列
    • 3.信号量

其中 共享内存消息队列 是用于进程间通信的,而 信号量 是保证同步和互斥的

这一篇要讲解的便是 共享内存 方面的一些知识

二、内存共享原理

通过调用系统的函数将两个虚拟的地址映射到同一块物理地址上,使得两个不同的进程能够共同访问到同一份资源,并实现通信操作

这个图就非常的形象,我们能发现每一个进程都有 进程块地址空间,并且会有一个对应的页表管理虚拟地址到物理地址的映射。那么如何通信呢?其实很显然,例如这里的 进程A 往这个虚拟地址写入数据,然后系统会将这个地址转到一个真实的物理地址,然后往这个物理地址写入(有一点重定向的感觉),进程B 就从这个地址读取数据,这样就实现了进程间的通信,那么这个时候问题来了,怎么保证数据写入读出的原子性,因为可能 进程A 写到一半,然后 进程B 就开始读,最后读的数据就不完整,这个就是我们后面要谈的 信号量 来实现同步与互斥

三、共享内存优缺点

3.1 优点

  • 效率高,整个通信过程中进程可以对共享内存直接读写(会产生两次拷贝操作),不需要额外的拷贝,像管道和消息队列等通信方式,则需要在内核和用户空间进行四次数据拷贝,而共享内存只会拷贝两次
  • 在共享内存进行通讯的时候会一直保持共享区域,知道整个通信完毕,这样的话数据内容会一直在共享内存中,共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

3.2缺点

  • 共享内存没有同步机制,我们在进行进程间通信的时候往往需要借助其他的手段(信号量)来进行进程间通信的协作操作
  • 由于共享内存的操作是实时写入的,如果读端进程没有读取,而写端进程不断的写入就会让数据不断更新,想要查看未读之后的第一条信息需要采取一些额外的手段

四、查看系统共享内存

4.1 查看系统中的ICP对象

ipcs -m

当然你可以可以通过

ipcs -a

查看全部 IPC 对象

4.2 删除IPC对象

  • 删除指定的消息队列:ipcrm -q MSG_ID 或者 ipcrm -Q msg_key
  • 删除指定的共享内存:ipcrm -m SHM_ID 或者 ipcrm -M shm_key
  • 删除指定的信号量:ipcrm -s SEM_ID 或者 ipcrm -S sem_key

五、ftok创建IPC节点

系统在简历IPC通信(共享内存、消息队列)的时候必须指定一个ID值,而这个值就是通过调用ftok函数得到的

5.1 头文件及函数原型

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

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

5.2 ftok解析

首先说明这个函数是为了获取一个IPC的标识号

通过参数的名字和类型我们大概知道第一个pathname应该是传一个地址,第二个proj_id大概是传一个数,其实前者只需要随便传入一个真实存在的系统路径地址这个也称作为文档地址,例如/home,而后者呢只需要传入一个8bit范围内的值就好了(一般选在一百以内就差不多了),在一般的UNIX实现中,是将文档的索引节点号取出,前面加上子序号得到key_t的返回值。而这个返回值就是一个唯一表示IPC 的数字,但是要注意的是这里如果ftok的返回值是 − 1 -1 1 那么就说明构建失败

例如我这里

key_t k1 = ftok("/home",10);
if(k1 == -1) 
    return NULL;

六、shmget创建共享内存

6.1 头文件及函数原型

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

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

6.2 shmget解析

首先说明这个函数是为了获取或创建共享对象的ID号

  • 第一个参数key 其实就是我们上面通过ftok 得到的,是标识系统的唯一IPC资源
  • 第二个参数 size 是需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是 4 k 4k 4k 字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍,不过这里不需要过多担心,因为系统会自动帮我们向上取整 4 k 4k 4k字节的倍数的。
  • 第三个参数shmflg是一个操作选项,有两个选项:
    • IPC_CREAT(0),创建一个共享内存,如果已经存在则返回共享内存;IPC_EXCL (单独使用没有意义)
    • IPC_CREAT|IPC_EXCL(如果调用成功,一定会得到一个全新的共享内存):如果不存在共享内存,就创建;反之,返回出错

这里第三个参数的操作选项:

选项作用
IPC_CREAT如果 key 对应的共享内存不存在,则创建
IPC_EXCL如果该 key 对应的共享内存已存在,则报错
SHM_HUGETLB使用“大页面”来分配共享内存
SHM_NORESERVE不在交换分区中为这块共享内存保留空间
mode共享内存的访问权限(八进制,如 0644 0644 0644

注意的是这里的mode 参数和前面的 起来就好了

这个函数有一个返回值是int类型,是描述共享内存的标识符,如果返回值是 − 1 -1 1 那么就说明申请失败了
例如我这里:

int shm_id = shmget(k1,sizeof(task_infos), IPC_CREAT | 0644);
if(shm_id == -1) 
    return NULL;

七、shmat 挂载共享内存

7.1 头文件及函数原型

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

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

7.2 shmat解析

首先说明这个函数做的事情是将虚拟地址映射到物理地址上,前面我们已经获取了一系列的参数

  • 第一个参数 shmid 就是我们上面shmget函数得到的一个共享内存对象号,或者说是共享存储段的标识符
  • 第二个参数 shmaddr 是我们挂载的地址,当然这里如果设为NUILL系统会自动为你找到一个第一个能够使用的空间
  • 第三个参数 shmflg 是我们挂载的时候的方式一般默认为 0 0 0 即可

关于第三个参数有这三个选项:

选项作用
SHM_RDONLY以只读方式映射共享内存
SHM_REMAP重新映射,此时 shmaddr 不能为 NULL
SHM_RND自动选择比 shmaddr 小的最大页对齐地址

这里如果成功的话会返回一个地址,否则会返回一个NULL

例如:

void *res = shmat(shm_id,NULL,0);
return res;

八、shmdt 取关联共享内存

当一个进程不需要共享内存的时候,就需要去关联。该函数并不删除所指定的共享内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程。

8.1 头文件及函数原型

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

int shmdt(const void *shmaddr);

8.2 shmdt解析

这个其实没啥好讲的,这个函数看名字也能知道是和shmat配合使用,用于解除共享内存的映射的,并不会直接删除

用法也是非常简单,只有一个void *的参数

九、shmctl 销毁共享内存

9.1 头文件及函数原型

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

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

9.2 shmctl解析

这个函数是用于设置共享内存的相关属性,例如释放共享内存等

  • 第一个参数传入的是共享内存的ID
  • 第二个参数传入的是行为
  • 第三个参数传入的是属性信息结构体指针,一般设置为NULL

其中第二个参数可以有很多选项:

字段效果
IPC_STAT获取属性信息,放置到 buf
IPC_SET设置属性信息为 buf 指向的内容
IPC_RMID将共享内存标记为“即将被删除”状态
IPC_INFO获得关于共享内存的系统限制值信息
SHM_INFO获得系统为共享内存消耗的资源信息
SHM_STATIPC_STAT,但 shmid 为该 SHM 在内核中记录所有 SHM 信息的数组的下标,因此通过迭代所有的 下标可以获得系统中所有 SHM 的相关信息
SHM_LOCK禁止系统将该 SHM 交换至 swap 分区
SHM_UNLOCK允许系统将该 SHM 交换至 swap 分区

第三个参数的结构体大概长这个样子:

struct shmid_ds 
    struct ipc_perm shm_perm;       
    int     shm_segsz;              
    time_t  shm_atime;              
    time_t  shm_dtime;              
    time_t  shm_ctime;              
    unsigned short  shm_cpid;       
    unsigned short  shm_lpid;       
    short   shm_nattch;             
    unsigned short   shm_npages;    
    unsigned long   *shm_pages;     
    struct vm_area_struct *attaches;
;

成功后将会返回 1 1 1 失败后返回 − 1 -1 1

注:

  1. IPC_STAT 获得的属性信息被存放在以下结构体中:
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; /* 最后一次映射或解除映射者PID */
    shmatt_t shm_nattch;/* 映射该SHM 的进程个数*/
;

其中权限信息结构体如下:

    struct ipc_perm
    
        key_t __key; /* 该SHM 的键值key */
        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; /* 序列号*/
    ;
  1. 当使用IPC_RMID 后,上述结构体struct ipc_perm 中的成员 mode 将可以检测出
    SHM_DEST,但 SHM 并不会被真正删除,要等到shm_nattch 等于 0 0 0 时才会被真正删除。
    IPC_RMID 只是为删除做准备,而不是立即删除。

  2. 当使用IPC_INFO 时,需要定义一个如下结构体来获取系统关于共享内存的限制值
    信息,并且将这个结构体指针强制类型转化为第三个参数的类型。

struct shminfo

    unsigned long shmmax; /* 一块SHM 的尺寸最大值*/
    unsigned long shmmin; /* 一块SHM 的尺寸最小值(永远为1) */
    unsigned long shmmni; /* 系统中SHM 对象个数最大值*/
    unsigned long shmseg; /* 一个进程能映射的SHM 个数最大值*/
    unsigned long shmall; /* 系统中SHM 使用的内存页数最大值*/
;
  1. 使用选项SHM_INFO 时,必须保证宏_GNU_SOURCE 有效。获得的相关信息被存放在如下结构体当中:
struct shm_info

    int used_ids; /* 当前存在的SHM 个数*/
    unsigned long shm_tot; /* 所有SHM 占用的内存页总数*/
    unsigned long shm_rss; /* 当前正在使用的SHM 内存页个数*/
    unsigned long shm_swp; /* 被置入交换分区的SHM 个数*/
    unsigned long swap_attempts; /* 已废弃*/
    unsigned long swap_successes; /* 已废弃*/
;
  1. 注意:选项SHM_LOCK 不是锁定读写权限,而是锁定 SHM 能否与 swap 分区发生交换。一个 SHM 被交换至 swap 分区后如果被设置了 SHM_LOCK ,那么任何访问这个 SHM的进程都将会遇到页错误。进程可以通过IPC_STAT 后得到的 mode 来检测 SHM_LOCKED信息。

十、流程&代码

从上面的讲解中我们大概也能猜到

  1. 首先我们要通过ftok() 申请到一个 ipc 标识符
  2. 然后将这个标识符和我们需要共享的空间大小以及方式放入shmget()获取共享内存的ID
  3. 然后通过shmat()将共享内存挂载(映射)到真实物理地址上,实现进程间的通信
  4. 在通信完成后我们通过shmdt()可以解除共享内存的映射,通过shmctl()实现

我们会发现这个过程是重复的,对于进程A他会做些这操作,进程B也会做这些操作,于是我们将一些共性的操作写在一个.c.h文件中,到时候对于A或者B的话直接#include就好了,这里的话方便后续项目的介绍,我们就称为task.htask.c好了

这里附上代码:
task.h

//
// Created by Mangata on 2022/5/9.
//
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#ifndef WORKSPACE_TASK_H
#define WORKSPACE_TASK_H

#define MAX_SIZE 5
#define KEY_1 "/home"
#define KEY_2 10

typedef struct task_msg
    double temp;
    char label[32];
task_msg;

typedef struct task_infos
    int num;
    task_msg data[MAX_SIZE];
task_infos;

extern void *init_shm(void);
extern void detach_shm(void *p);

#endif //WORKSPACE_TASK_H

task.c

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

void* init_shm(void)
    key_t k1;
    int shm_id;
    void *res;
    k1 = ftok(KEY_1,KEY_2);
    if(k1 == -1) 
        return NULL;
    
    shm_id = shmget(k1,sizeof(task_infos), IPC_CREAT | 0644);
    if(shm_id == -1) 
        return NULL;
    
    res = shmat(shm_id,NULL,0);
    return res;


void detach_shm(void *p)
    shmdt(p);

由于代码很短,并且上面也都做了分析,所以就没加注释了,我也相信以读者的能力也能轻松看懂

以上是关于SystemV标准的Linux进程间通信共享内存的主要内容,如果未能解决你的问题,请参考以下文章

Linux进程IPC浅析[进程间通信SystemV共享内存]

Linux进程IPC浅析[进程间通信SystemV共享内存]

linux里的ipc是啥意思

Linux-进程间通信

Linux-进程间通信

Linux-进程间通信