【Linux】system V共享内存与信号量

目录

一、前言

二、共享内存

1、基本原理

2、实现代码

2.1、创建共享内存

2.2、释放共享内存 

2.3、关联共享内存

2.4、与共享内存去关联

2.5、进程间通信

2.6、补充内容 

三、system V信号量

1、概念

2、信号量

3、相关接口

3.1、获取信号量

3.2、释放信号量 

3.3、信号量的PV接口

四、IPC资源管理方式


一、前言

 在上篇文章《管道》中,已经介绍了通过管道实现进程间通信的方式。本篇文章主要着重于 system V共享内存进行讲解。

 system V 是一套独立于操作系统外的标准,是一个专门为了通信设计出的内核模块,我们称之为 system V 的 IPC 通信机制。

 因为进程具有独立性,所以任何进程间通信的方式,首先要做的就是让不同的进程看到同一份资源。

二、共享内存

1、基本原理

 在物理内存中开辟一块空间,并在进程A和进程B的地址空间中分别通过页表与这一块空间建立映射关系,从而实现进程A和进程B共享一块内存。

 当进程通信结束后,只需要通过修改页表,取消掉进程A和进程B与共享内存的映射关系,并释放这块内存就可以了。

 操作系统中可能同时会有多对进程在通信。这就说明在任意时刻,可能有多个共享内存被用来进行通信。系统中有多个 shm 同时存在,就需要把他们根据先描述、再组织的方式管理起来。

 所以共享内存并不是只需要在内存中开辟空间就可以了,系统也要为了管理共享内存,构建描述共享内存的结构体struct shm。struct shm中存放共享内存的全部属性。共享内存 = 共享内存的内核数据结构 + 真正开辟的内存空间。

2、实现代码

2.1、创建共享内存

创建共享内存的接口:

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

 shmget 函数的参数列表中, key 是任意具有标定唯一性的数字。 size 表示所申请共享内存的大小。 shmflg 是创建共享内存的选项,常用以下两种选项:

  • IPC_CREAT:创建一个共享内存。如果共享内存不存在,就创建。如果已经存在,则获取已经存在的共享内存并返回。
  • IPC_EXCL:不能单独使用,一般都要配合IPC_CREAT使用。如果共享内存不存在,就创建。如果已经存在,则立刻出错返回。一旦创建成功,则对应的共享内存一定是最新的。

 如果想要对共享内存设置权限,则也可以在 shmflg 选项中,按位或 指定权限。

对于 key 值,我们一般使用 ftok 函数来设置。

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

 ftok 函数会结合参数列表中的路径字符串 pathname 与项目id proj_id ,形成一个重复概率非常低的key值。这样互相通信的两个进程就可以通过相同的参数,得到一个唯一的key值,从而找到同一块共享内存,进而实现进程间通信的前提:让不同的进程看到同一份资源。key本质是在内核中使用的。

 使用如下代码创建共享内存:

//comm.h

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
using namespace std;

#define PATHNAME "."
#define PROJID 0x0001

const int gsize = 4096;

key_t GetKey()
{
    key_t k = ftok(PATHNAME, PROJID);

    if(k == -1)
    {
        cerr << errno << " : " << strerror(errno) << endl;
        exit(1);
    }
    return k;
}

string toHex(int x)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}

int creatShmHelper(key_t k, int size, int flag)
{
    int shmid = shmget(k, gsize, flag);
    if(shmid == -1)
    {
        cerr << errno << " : " << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

int creatShm(key_t k, int size)
{
    return creatShmHelper(k, size, IPC_CREAT | IPC_EXCL);
}

int getShm(key_t k, int size)
{
    return creatShmHelper(k, size, IPC_CREAT);
}

#endif


//server.cc

#include "comm.hpp"

int main()
{
    //创建key
    key_t k = GetKey();
    cout << "server key: " << toHex(k) << endl;

    //创建共享内存
    int shmid = creatShm(k, gsize);
    cout << "server shmid: " << shmid << endl;
    return 0;
}


//client.cc

#include "comm.hpp"

int main()
{
    key_t k = GetKey();
    cout << "client key: " << toHex(k) << endl;

    int shmid = getShm(k, gsize);
    cout << "client shmid: " << shmid << endl;
    return 0;
}

编译运行,运行 server 程序,并且等待 server 进程结束后,再次运行 server 程序,会发现以下现象:

 原因是,共享内存的生命周期不随进程,随OS。进程结束后,所创建的共享内存不会被自动释放。

查看进程间通信内存资源的指令:

ipcs -m

 可以看到共享内存依然存在。

2.2、释放共享内存 

释放共享内存的指令:

ipcrm -m [shmid]

 此时就可以再次运行 server 程序创建共享内存了:

 共享内存除了使用指令释放外,还可以使用系统调用来释放

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

 shmctl 函数的参数列表中, shmid 指定共享内存。 cmd 表示想对共享内存做什么操作。 buf 可以获取该共享内存的属性,并存放在 buf 中。

 其中常用的 cmd 参数如下:

  • IPC_RMID:直接释放共享内存。
  • IPC_STAT:获取共享内存的属性。

1)IPC_STAT

获取共享内存的属性时,需要具备相应的权限,这里先设置一下:

 

编写代码: 

 编译运行:

 函数获取到了共享内存的属性。

2)IPC_RMID

 编译运行:

 进程结束,共享内存已经被释放。

2.3、关联共享内存

挂接共享内存的系统调用:

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

 shmat 函数的参数列表中, shmid 指定共享内存。 shmaddr 为共享内存挂接成功后得到的共享内存的虚拟地址的起始地址,一般设置为 NULL ,让系统自己挂接。 shmflg 为共享内存的选项,可以设置为只读属性,一般我们设置为 0 ,为读写属性。

新增代码:

 编译运行:

 观察到我们创建出的共享内存的 nattch 属性从 0 变为了 1 。这代表该共享内存的挂载数增加了。有几个进程与该共享内存相关联, nattch 就是几。

2.4、与共享内存去关联

与共享内存去关联的系统调用:

int shmdt(const void *shmaddr);

 shmdt 的函数参数列表中,ahmaddr shmat 函数的返回值,即共享内存挂载到虚拟内存中的起始地址。找到起始地址后,自然也就找到了共享内存的大小等属性,从而通过偏移量把虚拟地址对应的地址解关联。

新增代码:

2.5、进程间通信

为了使代码更加整洁,先把创建、关联、释放共享内存的代码封装成类:

#define SERVER 1
#define CLIENT 0
class Init
{
public:
    Init(int t):type(t)
    {
        key_t k = GetKey();
        if(type == SERVER)
            shmid = creatShm(k, gsize);
        else
            shmid = getShm(k, gsize);

        start = attachShm(shmid);
    }

    char* getStart() { return start; }

    ~Init()
    {
        detachShm(start);
        if(type == SERVER)
            delShm(shmid);
    }

private:
    char* start;
    int type;
    int shmid;
};

由于进程已经通过共享内存看到同一份资源了,接下来就借助共享内存实现进程间通信:

 获取共享内存的起始地址,并通过起始地址读写共享内存中的数据。

2.6、补充内容 

  •  关于共享内存的大小:共享内存的大小是以 PAGE 页(4KB)为单位分配的。即OS所分配的共享内存的大小一定使 4KB 的倍数。但是,OS给分配了这么多,并不代表进程就可以使用这么多。比如,进程申请了 4097 字节的共享内存,因为超出了 4KB 大小,OS给该进程分配了 8KB 大小的共享内存,但是该进程只能使用其中的 4097 个字节,其他字节使用不了。
  •  关于共享内存使用:可以看到上面的通信代码中并没有使用任何接口,这是因为一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程直接看到了,无需使用系统调用接口。
     因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信最快的。也因此,共享内存没有任何的保护机制。

三、system V信号量

1、概念

  • 互斥:任何一个时刻,都只允许一个执行流进行共享资源的访问。
  • 临界资源:任何一个时刻,都只允许一个执行流进行访问的共享资源。
  • 临界区:凡是访问临界资源的代码存放的地方,叫做临界区。对临界资源进行保护,实际上是对临界区进行保护,也是对代码进行保护。
  • 原子性:要么不做,要么做完,这种只有两种确定状态的属性,被称为原子性。

2、信号量

 信号量又被叫做信号灯,本质上是一个描述资源数量的计数器。

 任何一个执行流,想访问临界资源中的一个资源的时候,不能直接访问,而要先申请信号量资源。如果申请到了,信号量执行 "--" 操作,表示一个临界资源已经被占用,这个过程称为 P 操作。这是一个预定机制,只要申请成功,那么这个执行流就一定能够拿到一个子资源,在需要的时候就能够进入临界区,访问对应的临界资源。如果信号量为 0 ,就表示已经没有临界资源了,后面再申请的执行流会进入阻塞状态。当执行流访问临界资源结束后,信号量执行 "++" 操作,表示将对应的资源进行了归还,这个过程称为 V 操作。

 互斥功能本质就是将临界资源独立使用,即将信号量设置为 1

 因为进程在访问临界资源时,都要申请信号量,这就意味着所有的进程都得看到同一个信号量,即信号量本身也是一个共享资源。为了保护自身的安全,就需要信号量自己的 "++" 与 "--" 操作都是原子性的。这部分内容在后面讲进程信号时会着重讲解。

3、相关接口

3.1、获取信号量

获取信号量的接口:

int semget(key_t key, int nsems, int semflg);

 semget 函数的参数列表中, key 是一个具有唯一性的数字。 nsem 为申请信号量的个数(与一个信号量是几进行区分),称为信号量集。 semflg 是选项,常用的有 IPC_CREAT IPC_EXCL ,不再重复介绍。返回值是信号量标示符。

查看信号量指令:

ipcs -s

3.2、释放信号量 

 释放信号量的指令:

ipcrm -s [信号量shmid]

释放信号量的系统调用:

int semctl(int semid, int semnum, int cmd, ...);

 semctl 函数的参数列表中, semid 表示要对哪一个信号量集进行操作。 semnum 表示要对哪一个信号量进行操作。 cmd 是操作选项。 "...是可变参数,用来指定获取信号量的相关属性。

3.3、信号量的PV接口

int semop(int semid, struct sembuf *sops, unsigned nsops);

 semop 函数的参数列表中, semid 表示要对哪一个信号量集进行操作。 sops 是一个结构体指针,需要自己定义,结构体由如下部分构成:

 sem_num 表示哪一个信号量。 sem_op 表示进行什么操作(比如设置为 1 或 -1 ,表示加与减操作)。 sem_flg 是选项,设置为默认就可以。

 nsops  sem_num 相同,表示哪一个信号量。

四、IPC资源管理方式

 无论是共享内存、消息队列还是信号量,它们的结构体虽有很多不同,但是都在结构体第一个字段包含了另一个结构体 IPC_perm

共享内存:

 消息队列:

 信号量:

 在OS中,是以 ipc_id_ary 数组的方式来管理所有的 ipc 资源的。因为所有 ipc 资源结构体的起始字段的类型都一样,都是 ipc_perm 。其简化模型与简化原理是下面这样的:

 ipc_id_arr 是一个存储 struct ipc_perm* 类型数据的指针数组,因为每一个ipc资源的起始字段都是 struct ipc_perm* 类型,所以使用数组中的指针寻找到结构体起始地址,就相当于找到了整个结构体的地址,只需要找到起始地址后,把该指针强转成对应ipc资源的结构体类型就可以了。

 其中数组的下标就是IPC资源的返回值,也叫做标示符。之所以我们所看到的标示符很大,是因为标示符是递增的,数组下标是循环使用的,当数组越界后,会回到下标为 0 的位置,但是标示符不会清零。

 以上原理是对多态的应用。


 关于system V共享内存与信号量的相关内容就讲到这里,希望同学们多多支持,如果有不对的地方,欢迎大佬指正,谢谢!

猜你喜欢

转载自blog.csdn.net/weixin_74078718/article/details/130555468
今日推荐