进程间通信(下)

1. system V共享内存

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

1.1 共享内存示意图

我们来看看吧!下图就是shm的原理图。

所谓共享区,就是在内存中开辟的一块空间,然后将该内存空间挂接在进程的共享区中,这里的挂接就是将内存空间的实际地址由页表进行映射转为虚拟地址,将此虚拟地址存放入共享区(存放的还有该共享内存空间的其他相关信息,比如大小)。  

这里要注意,我们要如何确保需要通信的两个进程能够指向同一块共享内存区呢?

这就需要该空间具有唯一标识的标志。 

匿名管道是父子继承的方式,命名管道由路径进行唯一标识,那么共享内存区呢?

OS会依据唯一的key来对空间进行标识,后面函数篇详细讲解。 

在后面我们使用共享内存区的缩写shm。 

1.2 共享内存区的数据结构

OS当然会对shm进行管理,那就肯定有管理shm的结构体。

1.3 共享内存函数 

1.3.1 shmget函数

功能:用来创建共享内存
原型    int shmget(key_t key, size_t size, int shmflg);
参数
key: OS标识该内存空间唯一的标识符,由ftok函数得来。
size: 共享内存大小 < 这里有一个细节,shm的基本单位是4KB,如果你传入4097Bytes,那         么该shm的实际空间大小是8KB,但你只能使用4097字节,因此建议传入4KB的整数倍>
shmflg: 由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

             这里重点介绍IPC_CREAT与IPC_EXCL

             只传入前者表示有则返回shmid,传入前者|后者表示有则出错返回。
返回值成功返回一个非负整数,即该共享内存段的标识码;失败返回-1 

 1.3.2 ftok函数

功能:用来生成shmget的第一个参数key,使得操作系统可以开辟具有唯一标识的空间。

           实质是一个类似哈希函数的算法,在函数内部对传入的参数进行一系列运算,最后生             成一个具有唯一标识能力的码,该函数确保传入参数相同时,每次返回的key相同。
原型    key_t ftok(const char *pathname, int proj_id);
参数
pathname: 一个稳定的存在的路径,OS会使用该文件的相关信息生成唯一标识key
id: 通常是一个ASCLL码字符
返回值成功返回key值,失败返回-1

1.3.3 shmat函数 

功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1

        //通过该指针可以直接对内存空间进行操作

说明:

shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

 1.3.4 shmdt函数

功能:将共享内存段与当前进程脱离
原型    int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存 

1.3.5 shmctl 

功能:用于控制共享内存
原型    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

1.3.6 指令小集

ipcs 

这一指令可以查看当前所有进程间通信设施的状态信息 

 ipcs -m

仅查看进程间通信中以共享内存为设施的状态信息 

ipcrm -m  shmid 

删除该内存空间

小细节

这里我们在写下面代码验证的时候遇到了下面这种情况:这里的shm_sever里会创建shm。

上一个进程已经结束了,重新启动我们创建shm却失败了,查看以后发现上一个进程创建的shm还在。 

 当我们删除shmid为1的shm后,再次启动进程就成功创建了shmid为2的shm。

这说明,shm的生命周期并不向管道一样随进程,而是随内核的,如果我们不主动释放它,他会一直存在直到系统关闭。 

1.4 代码验证

common.hpp 

该文件中包含创建、销毁shm,挂接进程与解除关联关系的一系列方法。 

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

//在这里将获取shmid的参数使用宏定义,免去后面在外部接口处传入
#define defaultsize 4096
#define PATH "./"
#define Projid 'A'

//获取key值
int GetKey()
{
    return ftok(PATH, Projid);
}

//将key值转化为十六进制,与OS一致,方便查看
string ToHex(int key)
{
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "0X%x", key);
    return buffer;
}

//创建或获取shm的接口,后续会进行封装,sever创建,而client要获取
int CreatShmOrDie(int size, int shmflg)
{
    int key = GetKey();
    int shmid = shmget(key, size, shmflg);
    if (shmid < 0)
    {
        cerr << "shmget fail... errno" << errno << ",fail message: " << strerror(errno) << endl;
    }
    cout << "creat shm success..." << endl;
    return shmid;
}

//secver创建shm
int CreatShm(int size)
{
    return CreatShmOrDie(size, IPC_CREAT | IPC_EXCL | 0666);
}

//client获取shmid
int GetShm(int size)
{
    return CreatShmOrDie(size, IPC_CREAT | 0666);
}

//销毁shm
void DelShm(int shmid)
{
    int ret = shmctl(shmid, IPC_RMID, 0);
    if (ret == -1)
    {
        cerr << "Delete shm fail... errno" << errno << ",fail message: " << strerror(errno) << endl;
    }
    cout << "Delete shm success..." << endl;
}

//进程与shm建立关联关系
void *SetRelationship(int shmid)
{
    void *ptr = shmat(shmid, nullptr, 0);
    if ((long long int)ptr == -1)
    {
        cerr << "process and shm set relationship fail... errno" << errno << ",fail message: " << strerror(errno) << endl;
        return nullptr;
    }
    cout << "process and shm set relationship success..." << endl;
    return ptr;
}

//解除进程与shm的关联关系
void RemoveRelationship(void *addr)
{
    int ret = shmdt(addr);
    if (ret == -1)
    {
        cerr << "remove relationship fail... errno" << errno << ",fail message: " << strerror(errno) << endl;
    }
    else
    {
        cout << "remove relationship success..." << endl;
    }
}
fifo.hpp 

这一文件是命名管道的一部分,由于shm通信并不具备同步互斥机制,shm的通信是借助信号量来保护的。这里简单起见,我们借用管道通信的保护机制来保护shm,具体是如何实现的详见代码注释。 

#pragma once
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;

#define PAtH "./fifo.txt"
#define MODE 0666
class FIFO // 管理fifo的创建与销毁
{
public:
    FIFO(string path)
        : _path(path)
    {
        int n = mkfifo(_path.c_str(), MODE); // 创建,成功返回0
        if (n == 0)
        {
            cout << "name_pipe creat success..." << endl;
        }
        else
        {
            cerr << "name_pipe creat fail... errno: " << errno << ",errstring: " << strerror(errno) << endl;
        }
    }
    ~FIFO()
    {
        int n = unlink(_path.c_str()); // 销毁,成功返回0
        if (n == 0)
        {
            cout << "name_pipe unlink success..." << endl;
        }
        else
        {
            cerr << "name_pipe unlink fail... errno: " << errno << ",errstring: " << strerror(errno) << endl;
            // 标准错误输出
        }
    }

private:
    string _path;
};

//控制对管道的操作(读和写),借助管道的同步机制保护shm
class Sync
{
public:
    Sync()
        : rfd(-1), wfd(-1)
    {
    }
    void OpenRead()//打开管道的读端
    {
        rfd = open(PAtH, O_RDONLY);
        if (rfd == -1)
        {
            cerr << "rfd open O_RDONLY fail... errno: " << errno << ",errstring: " << strerror(errno) << endl;
        }
        else
        {
            cout << "rfd open success" << "rfd=" << rfd << endl;
        }
    }
    void OpenWrite()//打开管道的写端
    {
        wfd = open(PAtH, O_WRONLY);
        if (wfd == -1)
        {
            cerr << "wfd  open O_WRONLY fail... errno: " << errno << ",errstring: " << strerror(errno) << endl;
        }
        else
        {
            cout << "wfd open success" << "wfd=" << wfd << endl;
        }
    }
    bool wake()//读端读,当读取结束或失败返回false
    {
        int c = 0;

        int n = read(rfd, &c, sizeof(c));
        if (n == sizeof(c))
            return true;
        if (n == 0)
            return false;
        return false;
    }
    bool wakeup()//写端写,当写入结束或失败返回false
    {
        int c = 0;

        int n = write(wfd, &c, sizeof(c));

        if (n == sizeof(c))
            return true;
        return false;
    }

private:
    int rfd;
    int wfd;
};
shm_sever.cc 

sever端对shm的内容进行读取,借用管道的同步机制,只要管道在读取,sever就对shm读取,一旦管道读取结束,sever对shm的读取也结束,进入下一阶段。

#include "common.hpp"
#include "fifo.hpp"
int main()
{
    // 检查获取key值是否出错
    int key = GetKey();
    cout << ToHex(key) << endl;

    // 创建shm,如果有报错的那种
    int shmid = CreatShm(defaultsize);
    cout << shmid << endl;

    // 挂接shm
    char *buffer = (char *)SetRelationship(shmid);

    // 进程间通信  这里借用管道的同步互斥机制,当写端终止写入或关闭,读端读到0,
    FIFO fifo(PAtH);
    Sync syn;
    syn.OpenRead();
    while (1)
    {
        if (!syn.wake()) // 读端读到0,退出循环
            break;
        cout << "from client message: " << buffer << endl;
        sleep(1);
    }

    // 解除挂接
    RemoveRelationship(buffer);
    // 销毁shm
    DelShm(shmid);
    return 0;
}
 shm_client.cc

client端对shm进行写入,借用管道的同步机制,只要管道在写入,client就对shm写入,一旦管道写入结束,client对shm的写入也结束,进入下一阶段。

#include "common.hpp"
#include "fifo.hpp"
int main()
{
    // 检查获取key值是否出错
    int key = GetKey();
    cout << ToHex(key) << endl;

    // 获取shmid
    int shmid = GetShm(defaultsize);
    cout << shmid << endl;

    // 进程与shm挂接
    char *buffer = (char *)SetRelationship(shmid);

    // 进程间通信  这里借用管道的同步互斥机制,当写端终止写入或关闭,读端读到0,
    Sync syn;
    syn.OpenWrite();
    sleep(10);

    for (char c = 'A'; c < 'H'; c++)
    {
        buffer[c - 'A'] = c;
        sleep(1);
        !syn.wakeup();//当读端不再读,OS会强制关闭该管道并释放信号杀死该进程
    }
    // 写端不再写,读端会返回0

    // 解除挂接
    RemoveRelationship(buffer);
    return 0;
}

当我们终止sever与client的任意一端,管道机制都会收到,进而影响shm。 

  我们由sever端负责创建与销毁shm和管道,接收信息的任务,client端只需要发送信息即可。

下图nattch没有0-1的过程是因为该信息是每秒进行打印,而我们在两端并未进行等待,0-1,1-0的过程瞬间就已经完成。 

1.5 小结

注意:共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除

1.5.1 shm的优点 (部分)

这一进程通信方式速度是最快的,因为这一通信方式无需向管道那样使用系统调用,read与write系统调用的本质是拷贝函数,将数据在用户空间与内核空间之间传输。而shm相对而言非常快,两个进程访问同一内存空间,一个进程对该空间进行更改,另一个进程马上就能看到。 

1.5.2 shm的缺点 (部分)

这一进程通信方式没有同步与互斥机制的保护,也就是说,存在写端还没写完,读端就已经把已写的部分数据进行读取了, 这会造成通信间双方数据不一致的问题。

共享内存的操作是非进程安全的,多个进程同时对共享内存读写是有可能会造成数据的交叉写入或读取,造成数据混乱

2. system V消息队列

2.1 什么是消息队列? 

消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
特性方面
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核 

消息队列也是一种单向通信,每个消息队列都是单向的,且消息是按顺序存储的。

当发送方发送消息,就会将该消息插入到消息队列的尾,接收方将会依次接收消息队列中的数据。

2.2 功能函数--msgget(创建/获取消息队列) 

 

2.3 功能函数--msgctl(控制消息队列)

 

2.4 功能函数--msgsnd(向队列中发送消息)

 

2.5 功能函数--msgrcv(接收队列中的消息) 

 

3. system V信号量 

什么是信号量呢?当我们访问gpt,会得到以下结论。

 

看完gpt的回答,我们有了一下结论:

1. 信号量的存在是为了实现同步与互斥机制 

2. 信号量本身是一种类似计数器的存在,其标示还有多少资源可用。

3. 通过pv操作可以对信号量进行操作,用以限制进程对资源的访问。

4. pv操作必须是原子的(要么还没开始,要么已经结束),即不会被中断的。

可是我们心里的疑惑却更多了,同步与互斥机制又是什么?进程是互相独立的,信号量是如何被不同进程看见的?临界区又是什么?等等诸多问题在我们心头浮现,我们来看看吧。

3.1 同步与互斥

同步是指访问资源时,具有一定的顺序性(即协调进程与线程动作,使其在执行时具有顺序性),以避免竞态条件(不同进程访问同一资源时,其执行不具有顺序性,会出现多个进程争抢同一份资源)。

互斥是指同一份资源在同一时刻只能由一个进程或线程进行访问。

由此可以看到,互斥是实现同步的一种手段,为了实现同步,需要使用互斥对资源进行控制。

3.2 信号量是如何实现的 

信号量有两种:

1. 二进制信号量(只有0和1)本质就是互斥

2. 计数信号量(可以大于1) 由程序员来限制进入该资源的进程数,合理分配资源。

信号量的实现 

 信号量要想实现进程间的同步机制,信号量的状态就必须能够被不同进程看见,因此信号量本质也是一个公共资源,从这方面看,信号量本身就是一种进程间通信的方式。

信号量需要支持++、--,同时这一操作不能被中断,因此只是简单的int类型数据可做不到,其实现非常复杂。我们只需要知道信号量是一块可以被不同进程看见的,具备计数功能的资源即可。

3.3 临界资源与临界区 

所谓临界资源就是一次只能被同一进程访问的资源,临界区即代码中访问共享资源的那段代码。

而如何访问共享资源呢?无非是申请资源,访问资源,释放资源,这一部分由程序员主导。因此,对共享资源的保护实质上就是程序员对临界区的保护。 

程序员所做的工作

 

3.4 信号量部分的功能函数 

既然本质也是进程间通信的一种方式,自然也不会少了信号量的系统调用。

 

这里我们看到,函数中出现了一个新的词,信号量集,那么这是什么呢?

我们试想,倘若一份共享资源内共有十二份临界资源,只使用一个信号量对其进行控制是否有些不太合适,因此就有了信号量集的概念。

 

4. 进程间通信总结 

我们学习了共享内存,消息队列,信号量之后会发现他们具有很多共性,甚至其系统调用都十分相似。

这是OS为了对IPC资源进行统一管理特意做的设计,在内核中,所有管理IPC资源的结构体其第一个成员都是kern_ipc_term。 

在内核中,对IPC资源的管理就变成了对数组的增删查改,这是为什么呢? 

 因为在内核中有一个kern_ipc_term*的指针数组,存放着各个IPC资源结构体的第一个成员变量ipc_term结构体。借此可以直接访问ipc_term结构体的内容,同时可以对该指针进行强转访问IPC资源结构体的成员。

 

这一实现是不是与c++的多态很像。

 

那我们又如何得知该IPC是什么类型呢?

 

perm里的mode就显示了其作用。

 

  • 24
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值