【Linux】SystemV版本下共享内存、消息队列、信号量

        进程间通信的本质是先让不同的进程看到同一份资源。

共享内存的原理

申请共享内存的原理:

释放共享内存的原理:

1.让进程地址空间与物理内存之间的映射去关联。

2.释放物理内存中的空间


上述的所有操作都是操作系统执行的,而不是进程。

操作系统也需要管理所有存在的共享内存——>先描述再组织。

编码实现

创建共享内存

shmget函数

【功能】用来创建共享内存

【原型】int shmget(key_t key, size_t size, int shmflg);

【参数】

  • key:这个共享内存段的名字,具备唯一性
  • size:共享内存大小
  • shmflg:一般使用IPC_CREAT 和 IPC_CREAT | IPC_EXCEL这两种方式,并且需要添加权限

【返回值】成功返回一个非负整数,即该共享内存段的标识码;失败返回-1.

谈谈key

  1. key是一个数字,这个数字在内核中具有唯一性,能够让不同的进程进程唯一性标识,以此才能判断共享内存是否存在。
  2. 第一个进程可以通过key创建共享内存,第二个之后的进程只需要使用同一个key,就可以使用同一份共享内存。
  3. 已经创建好一个共享内存,想要使用共享内存的进程需要找到key,key在共享内存的描述对象中。
  4. 第一次创建的时候,必须有一个key值,这个key值可以自己设定,也可以通过一个函数ftok创建。
  5. key在一定意义类似于路径,具备唯一性。

ftok函数是一套算法函数,将对应的pathname和proj_id进行数值计算,得到唯一的数值key。pathname和proj_id由用户自己来决定。

【注意】key值是不需要由系统默认生成的,否则其他进程无法获取到该key值,所以key值是每一个进程都约定的一个值,即每一个进程都知道的值。

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include "log.hpp"


using namespace std;

const int size = 4096;
// 通过ftok函数获取key获得参数
const string pathname = "/home/dabai";
const int proj_id = 0x6666;
Log log;

key_t getKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is: %d", key);
    return key;
}

int GetShareMem()
{
    key_t k = getKey();
    int shmid = shmget(k, size, IPC_CREAT | IPC_EXCL | 0666);
    if(shmid < 0)
    {
        log(Fatal, "create share memory error: %s", strerror(errno));
        exit(2);
    }
    log(Info, "create share memory success, shmid: %d", shmid);
    return shmid;
}

第一次执行:

【注意】key是在操作系统内标定唯一性的,而shmid只在该进程内用来表示资源的唯一性的。

第二次执行:

指令:ipcs -m

【功能】查看共享内存

【注意】共享内存的生命周期是随内的,进程退出后,如果不主动关闭共享内存,系统并不会主动关闭共享内存,共享内存会一直存在。除非内核重启或者用户关闭才可以关闭共享内存。

指令:ipcrm -m [shmid]

【功能】通过shmid值关闭共享内存

获取共享内存

调整上面的代码,实现获取共享内存:

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include "log.hpp"


using namespace std;

// 共享内存的大小一般建议是4096的整数倍
// 假如是4097字节,实际上操作系统给用户提供的是4096*2字节
const int size = 4097;
// 通过ftok函数获取key获得参数
const string pathname = "/home/dabai";
const int proj_id = 0x6666;
Log log;

key_t getKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is: %d", key);
    return key;
}

int GetShareMemHelper(int flag)
{
    key_t k = getKey();
    int shmid = shmget(k, size, flag);
    if(shmid < 0)
    {
        log(Fatal, "create share memory error: %s", strerror(errno));
        exit(2);
    }
    log(Info, "create share memory success, shmid: %d", shmid);
    return shmid;
}

int CreatShm()
{
    return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm()
{
    return GetShareMemHelper(IPC_CREAT);
}

【注意】共享内存的大小一般建议是4096的整数倍,假如是4097字节,实际上操作系统给用户提供的是4096*2字节。虽然系统显示的是4097字节,但是实际上操作系统分配内存是按照4kb来分配的,只是显示4097字节,底层是4096*2字节。

连接共享内存

shmat函数

【功能】将共享内存段连接到进程地址空间

【原型】void *shmat(int shmid, const void *shmaddr, int shmflg);

【参数】

  • shmid:共享标识
  • shmaddr:指定连接的地址
  • shmflg:可能取值是SHM_RND和SHM_RDONLY

【返回值】成功返回一个指针,执行共享内存第一个节;失败返回-1

    int shmid = CreatShm();
    log(Debug, "create shm done...");
    sleep(5);

    char* shmaddr = (char*)shmat(shmid, NULL, 0);
    log(Debug, "attach shm done...");
    sleep(5);

脱离连接共享内存

shmdt函数

【功能】将共享内存段与当前进程脱离

【原型】int shmdt(const void *shmaddr);

【参数】

  • shmaddr:由shmat所返回的指针

【返回值】成功返回0;失败返回-1

释放共享内存

shmctl函数

【功能】用于控制共享内存

【原型】int shmctl(int shmid, int cmd, struct shmid_ds *buf);

【参数】

  • shmid:由shmget返回的共享内存标识符
  • cmd:将要采取的动作(有三种可取值)
  • buf:指向一个保存着共享内存的模式状态和访问权限的数据结构

【返回值】成功返回0;失败返回-1.

使用共享内存进行通信

  • 对于接收方,一旦有数据写入到共享内存中,立马就可以看到。不要经过系统调用直接就可以看到数据。
  • 对于发送方,一旦存在共享内存,挂接到自己的地址空间中,就可以直接当作内存空间使用即可,不需要调用系统调用。

接收方代码简单示例:

    int shmid = CreatShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);

    // ipc code
    while(true)
    {
        cout << "client say: " << shmaddr << endl;
        sleep(1);
    }

    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, nullptr);

发送方代码简单示例:

    int shmid = GetShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);

    // ipc code
    while(true)
    {
        cout << "Please Enter@: ";
        fgets(shmaddr, sizeof(shmaddr), stdin);
    }

    shmdt(shmaddr);

共享内存的特性

  1. 共享内存没有同步互斥之类的保护机制
  2. 共享内存是所有的进程间通信间,速度最快的——拷贝少
  3. 共享内存内部的数据,由用户自己维护。

共享内存的属性

struct shmid_ds shmds;    
shmctl(shmid, IPC_STAT, &shmds);
cout << "shm size: " << shmds.shm_segsz << endl;
cout << "shm nattch: " << shmds.shm_nattch << endl;
cout << "shm key: " << shmds.shm_perm.__key << endl;
cout << "shm mode: " << shmds.shm_perm.mode << endl;

结合通道的共享内存代码实现

processa.cc文件

#include "comm.hpp"

extern Log log;

int main()
{
    Init init;
    int shmid = CreatShm();
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);

    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd < 0)
        exit(FIFO_OPEN_ERR);

    struct shmid_ds shmds;
    // ipc code
    while (true)
    {
        char c;
        ssize_t s = read(fd, &c, 1);
        if(s == 0) break;
        else if(s < 0) break;

        cout << "client say: " << shmaddr << endl;
        sleep(1);

        // shmctl(shmid, IPC_STAT, &shmds);
        // cout << "shm size: " << shmds.shm_segsz << endl;
        // cout << "shm nattch: " << shmds.shm_nattch << endl;
        // cout << "shm key: " << shmds.shm_perm.__key << endl;
        // cout << "shm mode: " << shmds.shm_perm.mode << endl;
    }

    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, nullptr);

    close(fd);

    return 0;
}

processb.cc文件

#include "comm.hpp"

int main()
{

    int shmid = GetShm();
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);

    int fd = open(FIFO_FILE, O_WRONLY);
    if (fd < 0)
        exit(FIFO_OPEN_ERR);

    // ipc code
    while (true)
    {
        cout << "Please Enter@: ";
        fgets(shmaddr, sizeof(shmaddr), stdin);

        write(fd, "c", 1); //通知对方
    }

    shmdt(shmaddr);

    close(fd);

    return 0;
}

makefile文件

.PHONY:all
all:processa processb

processa:processa.cc
	g++ -o $@ $^ -std=c++11
processb:processb.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f processa processb

comm.hpp文件

#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "log.hpp"


using namespace std;

// 共享内存的大小一般建议是4096的整数倍
// 假如是4097字节,实际上操作系统给用户提供的是4096*2字节
const int size = 4097;
// 通过ftok函数获取key获得参数
const string pathname = "/home/dabai";
const int proj_id = 0x6666;
Log log;

key_t getKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is: %d", key);
    return key;
}

int GetShareMemHelper(int flag)
{
    key_t k = getKey();
    int shmid = shmget(k, size, flag);
    if(shmid < 0)
    {
        log(Fatal, "create share memory error: %s", strerror(errno));
        exit(2);
    }
    log(Info, "create share memory success, shmid: %d", shmid);
    return shmid;
}

int CreatShm()
{
    return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm()
{
    return GetShareMemHelper(IPC_CREAT);
}

#define FIFO_FILE "./myfifo"
#define MODE 0664

enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

class Init
{
public:
    Init()
    {
        // Create a channel
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {
        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};


#endif

消息队列

消息队列会让两个进程以数据块的形式发送数据。

  1. 必须让不同进程看到同一个队列
  2. 允许不同的进程,向内核中发送带类型的数据库,放在消息队列中

创建消息队列

释放消息队列

发送消息与接收消息

消息队列的属性

Linux中消息队列相关指令

指令:ipcs -q

【功能】查看消息队列‘

指令:ipcrm -q [msqid]

【功能】删除一个消息队列

信号量

        当一个进程正在写入的时候(写入一部分),另外一个进程已经获取到这部分内容,导致进程双方的数据不完整,这种情况称为数据不一致问题。

数据不一致问题在管道中不存在,是因为管道中存在原子性和同步互斥的。

  1. 1进程和2进程看到同一份资源,共享资源,这份共享资源如果不加保护会出现数据不一致问题。
  2. 可以通过加锁方式来进行互斥访问,也就是说任何时刻只允许一个执行流访问共享资源,这种概念是互斥
  3. 共享的,且任何时刻只允许一个执行流访问的资源,称为临界资源。这个临界资源是操作系统维护的一块内存空间
  4. 执行流访问资源,也就是执行访问代码。访问临界资源的代码,被称为临界区。

【现象】多进程多线程在并发打印的时候,显示器上的消息可能会出现错乱、混乱、且与命令行混在一起的情况。

【解释】显示器也是共享资源,如果没有添加互斥保护机制的话,就会出现这种情况。。

信号量的原理

        信号量/信号灯的本质是一把计数器。类似于int cnt = n。用来描述临界资源中资源数量的多少。当存在一个执行流的时候,cnt--,申请资源,当cnt<=0的时候,资源就被申请完,执行流不能再继续申请资源了。

例如:看电影买票的本质是对资源的预定机制,存在卖电影票的计数器,每卖一张票,计数器就会-1,相当于放映厅的资源-1。当票数减少到0时,资源已经被申请完毕。

  1. 申请计数器成功,就表示具有访问资源的权限。
  2. 申请计数器资源,但并不是当前已经访问了对应的资源。申请资源的本质是对资源的预定机制。
  3. 计数器可以有效保证进入共享资源的执行流的数量。
  4. 每一个执行流想访问共享资源中的一部分内容时,不是直接访问,而是先申请计数器资源。

也就是说将计数器的概念称为信号量。

例如:当只有一个人能够进入电影院看电影,也就是说只有一个执行流能够访问临界资源,这种方式称为互斥。

我们将只能为1,0两态的计数器叫做二元信号量——本质就是一个锁。资源为1的本质就是将临界资源不要分成多块,而是当作一个整体,来进行整体申请,整体释放。

申请信号量的本质是对计数器进行--操作——即P操作,释放资源或者释放信号量的本质是对计数器进行++操作——即V操作。申请和释放(PV操作)可以说是原子的,即要么不做,要么就做完,不会存在“正在做”的概念。

创建信号量

删除释放信号量

信号量操作

信号量的属性

为什么信号量是进程间通信的一种呢?

  1. 通信不仅仅是通信数据,互相协同也是通信的一种。
  2. 要协同,本质也是通信,信号量首先要被所有的通信进程看到。

进程间通信的数据结构设计

SystemV中的属性都是被精心设计过的,都有着类似的结构体。

        在操作系统中,所有的IPC资源都是整合进操作系统的IPC模块中的。

不管是共享内存、消息队列还是信号量都有ipc_perm结构体,这些结构体都保存在一个结构体数组中,每一个结构体中的id值,例如shmid都是这个数组中的下标。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值