【Linux】进程间通信(四):System V标准(共享内存、消息队列、信息量)

一,System V 标准
Linux中支持了System V标准的进程管理与通信,使得该标准下的通信模块接口的设计、原理、和使用方式相似。下面要介绍的三个进程间方式就属于System V 标准。
一,共享内存
1. 原理介绍
原理:
在物理内存上申请一块空间,然后将这块空间映射到不同进程的虚拟地址中。【在共享内存的结构体shmid_ds中会有引用计数记录这块内存被多少进程使用】
于是,各个进程就可以通过页表的映射访问到这块共享内存
共享内存的特点:
最快的IPC形式。
因为:⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递就不再涉及到内核(即:映射成功后,进程不再通过执行内核的系统调⽤来传递彼此的数据,就像malloc一样,申请完就是一块空间)
不具有同步机制
因为,一旦映射成功以后,就不依赖物理内存和内核,每个进程各干各的,不像管道一样会等待另一端。【就会导致读写混乱】
生命周期随内核,不用指令删就一直存在,除非操作系统重启
2. 共享内存数据结构
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void shm_unused2; /* ditto - used by DIPC */
void shm_unused3; /* unused */
};
2. 接口介绍
2.1 shmget
用于获取共享内存
函数原型:int shmget(key_t key, size_t size, int shmflg)
key:用户提供,仅用于获取共享内存时,内核通过key来找对应的共享内存(这个key具有唯一性)
key的生成用ftok函数:
函数原型:key_t ftok(const char *pathname, int proj_id)
传入一个字符串和一个整数,生成一个key(可以随便给,但是为了代码可读性,一般字符串传入路径)
size:要开辟的共享内存的大小
注意:申请的共享内存,内核开的物理内存大小是向上 4KB 取整的,但是给用户的大小是按用户实习要多少决定的(比如申请 4097,内核物理内存开 4096 * 2,但是给用户还是4097,即:虚拟地址只映射4097大小的空间)
shmflg:标记位。两种传参方法:
IPC_CREAT:获取共享内存,不存在就创建
IPC_EXCL | IPC_CREAT:获取全新的共享内存。如果已经存在就报错(系统就是通过传入的key来判断共享内存是不是全新)
0xxx:还要传创建共享内存时的权限【共享内存也类似文件】
返回值:返回共享内存标识符shmid【这才是后续用户用来标识共享内存的】
2.2 shmat
用于将共享内存段映射到进程地址空间
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg)
shmid:共享内存标识符
shmaddr:指定映射的虚拟地址,一般设置为NULL 表示由系统自动分配地址。(因为我们用户也不知道虚拟地址的使用情况)
shmflg:设置映射地址的权限(一般也用 默认设置 0)
0:默认读写权限。进程可以读取并修改共享内存中的数据。
SHM_RDONLY:只读模式。进程只能读取共享内存
返回值:返回指向共享内存段在当前进程中的起始虚拟地址
2.4 shmdt
用于取消映射,仅减少引用计数
原型:int shmdt(const void *shmaddr)
shmaddr:指向共享内存段在当前进程中映射地址的指针(由 shmat 返回)。
2.3 shmctl
用于控制共享内存(我们删除共享内存也用这个)
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf)
shmid:共享内存标识符。
cmd:操作命令,有以下几种取值。
IPC_RMID:删除这片共享内存。(此时buf参数设置成 NULL)
IPC_STAT:得到共享内存的状态,把共享内存的 shmid_ds 结构复制到 buf 中。
IPC_SET:改变内核共享内存的状态,把 buf 所指的 shmid_ds 结构中的 uid、gid、mode 复制到共享内存的 shmid_ds 结构内。
buf:用户层的共享内存管理结构体。需要我们自己创建struct shmid_ds结构体,用来存储内核结构体的信息。
因为,在 Linux 系统中,用户空间程序无法直接访问内核内存,包括共享内存的管理结构体 struct shmid_ds。必须通过 系统调用(如 shmctl(),用IPC_STAT) 将内核中的数据复制到用户空间的结构体中。
返回值:返回共享内存的标识符(删除:成功返回 0,失败:-1)
2.4 命令行级操作
查看:ipcs -m
删除:ipcrm -m <shmid>
创建:ipcmk -m<size>
3. 使用示例
当共享内存创建并且映射好以后,这篇区域就已经属于用户了,用户访问这篇区域的时候就不需要再调用系统调用。
// comm.h
#ifndef _COMM_
#define _COMM_
# include <stdio.h>
# include <sys/types.h>
# include <sys/ipc.h>
# include <sys/shm.h>
#define SIZE 1024
// ftok 参数
# define PATHNAME "."
# define PROJ_ID 0x6666
// 功能上:用户端:写,服务端:读。
// 服务端创建,用户端获取,服务端删除
// 对系统调用封装实现
int CreateShm(int size);
int DestroyShm(int shmid);
int GetShm(int size);
int CloseShm(void* shmaddr);
#endif // 条件编译,确保头文件只被包含一次
// comm.cpp
#include "comm.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
using namespace std;
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
int CreateShm(int size)
{
    key_t key = ftok(PATHNAME, PROJ_ID);
    int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0)
        ERR_EXIT("shmget");
    cout << "创建共享内存成功" << endl;
    return shmid;
}
int DestroyShm(int shmid)
{
    int ret = shmctl(shmid, IPC_RMID, NULL);
    if(ret < 0)
        perror("shmctl");
    cout << "删除成功"  << endl;
    return ret;
}
int CloseShm(void* shmaddr)
{
    int ret = shmdt(shmaddr);
    if(ret < 0)
        perror("shmdt");
    cout << "关闭成功"  << endl;
    return ret;
}
int GetShm(int size)
{
    key_t key = ftok(PATHNAME, PROJ_ID);
    int shmid = shmget(key, size, IPC_CREAT);
    if (shmid < 0)
        ERR_EXIT("shmget");
    cout << "获取共享内存成功" << endl;
    return shmid;
}
// client.cpp
#include "comm.h"
#include <unistd.h>
int main()
{
    int shmid = GetShm(SIZE);
    char * ptr = (char*)shmat(shmid, nullptr, 0); // 我们把返回的地址当一个字符串指针
    for(int i = 0; i < 10; i+=2)
    {
        *(ptr + i) = 'A' + i;
        *(ptr + i + 1) = 'A' + i;
        sleep(1);
    }
    CloseShm(ptr); // 关闭共享内存(引用计数 - 1)
    return 0;
}
// server.cpp
#include "comm.h"
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
    int shmid = CreateShm(SIZE);
    char* ptr = (char*)shmat(shmid, nullptr, 0);
    for(int i = 0; i < 26; i++)
    {
        cout << ptr << endl; // 直接当字符串打印
        sleep(1);
    }
    CloseShm(ptr);
    DestroyShm(shmid);
    return 0;
}
运行:
两个进程是不具有同步性的,我们可以感受一下
先启动server.exe:
server.exe自己干自己的,即使没有内容写入
共享内存被创建,nattch引用计数为 1
再运行client.exe:
nattch变为2
并且开始server.exe打印信息
要解决上面的不同步问题:
信号量(这个是重点,但是 System V 版本的信号量不是重点)
加中间层命名管道(这东西同步)来传“信号”实现暂停和环形另一个进程,达到同步效果
二,消息队列和信号量(非重点)
消息队列和信号量也是System V 标准中提供“共享资源”,实现通信的其他IPC方法,但是非重点(System V 标准的 设计不好 / 用的少了)
如果想了解,可以看这篇文章:【Linux】进程间通信4——system V消息队列,信号量【其中包括对同步与互斥 以及 系统对IPC资源的组织管理】,写的很好!
但是我对里面一些内容进行总结和补充:
原子性:行为是两态的(不做 / 做完,没有中间态)
临界资源:被保护起来的共享资源
同步:多个执行流,访问临界资源的时候,具有⼀定的顺序性(就比如要等待前一个完成)
互斥:任何时刻,只允许一个执行流访问共享资源
临界区:每个进程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程进入
所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护【防止多个进程同时对代码共享资源进行修改,限制代码行为】
保护方法:加锁
锁也是共享资源,所以锁本身也需要被保护。在设计锁时,会把申请锁的行为设置成原子性的。(为了保护锁)
信号量:
消息队列和信号量的生命周期也是随内核的,申请的IPC资源必须删除,否则不会自动清除,除非重启操作系统
我们将一大片共享内存分成多个不同的区域(资源)(按共性内存分),信号量就是记录其中可用资源的数量(可进入进程)的计数器。
左图:资源整体使用,信号量只有 0 / 1 :这种情况下就是互斥,每次只有一个进程能访问该资源
信号量本质是对资源的预订机制
申请资源,计数器–,P操作
释放资源,计数器++,V操作
如果当前信号量不够了,则申请资源的进程就会被加入到信号量结构体的等待队列中(即:进程先阻塞)
IPC资源的组织管理:
当共享内存、消息队列、信息量三者的key相同时,就会冲突
三种资源虽然都有各自的结构体,但是他们的第一个成员都是kern_ipc_perm(kern_ipc_perm就存储了他们的key值,同时会记录所处在的结构体的资源类型)
而所有kern_ipc_perm会被组织到ipc_id_ary这个数组里面(也就是这个数组里面存着指向kern_ipc_perm的指针,即:指向对应的资源的结构体的头部)
相当于kern_ipc_perm是基类,其余三种资源的结构体都是子类
————————————————
                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/tan_run/article/details/148104931
阅读剩余
THE END