Linux进程间通信(IPC)——system V

目录

[system V 共享内存](#system V 共享内存)

原理

shmget系统调用

代码实现

shmctl系统调用

shmat系统调用

shmdt系统调用

完整代码:

共享内存特点(优缺点):

完善同步与互斥的共享内存通信(管道实现):

共享内存的内核结构

[system V 消息队列(了解)](#system V 消息队列(了解))

系统调用

[system V 信号量](#system V 信号量)

IPC资源的组织方式


上篇:Linux进程间通信(IPC)------管道-CSDN博客https://blog.csdn.net/suimingtao/article/details/156070951?

system机制的进程间通信有三种:共享内存、消息队列、信号量。它们是由内核提供的通信方法

system V 共享内存

通过让不同进程看到同一个内存块的方式,就称为共享内存

原理

通常来讲,因为进程中的地址都是虚拟地址 (进程地址空间),因此A进程申请的空间并不会和B进程申请的空间造成冲突,也就是平常所说的进程独立性

但如果先让操作系统在物理内存申请一块空间 ,再将该空间通过页表映射到进程的地址空间(关联),不同的进程就能看到同一份资源了,映射了这块内存的进程之间就可以进行通信了。当通信结束后,再取消进程和这块内存的映射关系(去关联)并释放内存

这块由操作系统在物理内存申请的空间就是共享内存,将共享内存映射到进程的地址空间的过程称为进程和共享内存关联,当不通信时,通过去关联来解除关系

共享内存是专门设计出来用于进程间通信(IPC)的,它不能通过例如malloc、new创建出来,因为malloc、new出来的地址是虚拟地址无法让其他进程在物理内存中找到

共享内存是一种通信方式,例如有两个进程在通过共享内存的方式进行通信,同时其他进程也可以通过共享内存的方式进行通信,所以OS中肯定会存在很多的共享内存

shmget系统调用

创建和获取共享内存都通过shmget系统调用完成

shmflg:类似于open的flag参数,可选的参数用位图表示可以同时使用多个参数 ,该位置可以用IPC_CREATIPC_EXCL两个参数

  • IPC_CREAT表示如果不存在就创建,如果存在就获取
  • IPC_EXCL 不能单独使用,如果要使用就必须和I PC_CREAT一起使用,即IPC_CREAT | IPC_EXCL,表示如果不存在就创建,如果存在就出错返回,也就是说如果创建成功,一定是一个新的共享内存
  • 除此之外,若是创建共享内存,还需要加上该共享内存段的权限 ,例如IPC_CREAT | IPC_EXCL | 0666

除此之外,若该位置为0,即为默认行为,和 IPC_CREAT 行为一致

size:指定创建的共享内存段的大小,该位置最好写4096的整数倍 。因为共享内存的单位是page,一般来说,一个page为4096字节 ,若写的其他值,例如5000,OS也会为该共享内存分配两个page,即8192个字节的空间,即会向上取整到page的整数倍。

此时若用 ipcs -m 查看,虽然size部分显示的是4097,但OS不会限制我们使用超过4097的数据,我们可以使用全部的8192个字节的空间

在C语言阶段的malloc和free中,我们申请了特定字节数的空间后,free的传参只有该空间的起始地址,是怎么知道我们要释放多少字节的空间的?

当调用malloc(size)时,实际分配的空间比请求的size要大这多出来的空间用于存储分配的大小、权限等信息,这块空间存储的数据叫做cookie数据.

同时,因为malloc的底层依旧是操作系统申请的空间 ,所以它也遵循先描述,再组织,因此这块多出的空间实际就是为创建结构体所分配的空间

cpp 复制代码
typedef struct {
    size_t size;  // 存储分配的大小
    // 其他元数据(如用于完整性检查的magic值)
} header_t;

共享内存最终也是操作系统申请的空间,所以它也需要被管理,因此根据先描述,再组织,创建共享内存时也需要多申请一些空间用于创建结构体,该结构体存的就是该块共享内存的容量、权限等等信息。

所以:共享内存 = 物理内存块 + 共享内存的相关属性

返回值:若成功,返回该共享内存的标识符,若失败,返回-1。未来想对共享内存做操作时,就用该标识符进行操作。该返回值和open返回的fd一样,也是数组下标,只不过它在不同的操作系统中起始位置不一样

key:要想实现进程间通信,就需要保证两个进程看到同一份资源。 ++匿名管道通过继承的方式保证,命名管道通过唯一路径的方式保证++ ,而在共享内存中,则是通过唯一标识key来保证它的作用只是保证唯一性,因此值是多少并不重要。 要想知道key,需要用到ftok函数

该函数通过传一个路径和一个数字,生成一个唯一的key

只要我们两个进程传进的 pathname 、 proj_id 同样,那么返回的key也一样,一个进程用该key创建共享内存,另一个进程用key获取共享内存,就可以看到同一份资源了

现在我们知道,共享内存靠key来保证在系统中的唯一性,只要另一个进程也看到key,就可以找到这段唯一的共享内存

那么,key在哪呢?当通过shmget创建共享内存时,就是将参数key存到了创建共享内存时创建的结构体中,当shmget获取共享内存时,遍历共享内存所对应属性的key和带来的key是否相等

也就是说,key是通过shmget设置进共享内存属性中的,用来标识该共享内存在内核的唯一性

shmid和key都是用于标识唯一性的,那它们两个之间有什么区别呢?

shmid vs key 就类似于 fd vs inode ,两者都用于标识唯一性,本质目的是为了解耦,这样即使C++的底层代码发生变动(key和inode改变),也不会影响到上层的shmid和fd

代码实现

和命名管道的代码实现一样,我们用server.cpp作为创建共享内存的一端,client作为获取共享内存的一端,两端通用的头文件、宏、函数就放在command.hpp中

command.hpp:

cpp 复制代码
#ifndef __COMM_HPP__//防止重复包含头文件(第一次执行时,没有定义该宏,会定义该宏,第二次因为定义了该宏,就不会执行了)
#define __COMM_HPP__

#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#define PATHNAME "."//这里就举例为设置ftok的路径为当前路径
#define PROJ_ID 0x114//ftok的项目ID这里也是随便设的
#define MAX_SIZE 4096//指定创建的共享内存段的大小
using namespace std;

key_t GetKey()//获取key
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key == -1)//若Key为-1,代表获取key失败,则终止进程
    {
        cerr << "ftok错误 --> " << errno << ":" << strerror(errno) << endl;//cerr为标准错误,它不需要经过缓冲区,会直接显示在屏幕上,更快
        exit(1);
    }
    return key;
}

int ShmHelper(key_t key,int shmflg)//通用的创建/获取共享内存
{
    int shmid = shmget(key,MAX_SIZE,shmflg);
    if(shmid == -1)//若返回-1,则创建/获取失败,则终止进程
    {
        cerr << "shmget错误 --> " << errno << ":" << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(key_t key)//创建共享内存(若成功,必是一个全新的共享内存)
{
    return ShmHelper(key,IPC_CREAT | IPC_EXCL | 0600);//设为只有拥有者可以读写
}

int GetShm(key_t key)//获取共享内存
{
    return ShmHelper(key,IPC_CREAT);
}

#endif 

server.cpp:

cpp 复制代码
#include "command.hpp"
int main()
{
    key_t key = GetKey();//获取key
    int shmid = CreateShm(key);//创建全新的共享内存段

    cout << "key:" << key << "\nshmid:" << shmid << endl;

    return 0;
}

client.cpp:

cpp 复制代码
#include "command.hpp"
int main()
{
    key_t key = GetKey();//获取key
    int shmid = GetShm(key);//获取共享内存段

    cout << "key:" << key << "\nshmid:" << shmid << endl;
    return 0;
}

运行会发现,它们两进程确实看到了同一份共享内存,但当我们再次重复运行server端时,会报错文件已存在

在管道中,只要进程退出,它的文件描述符会自动释放,即管道文件的生命周期是随进程的。

但共享内存的生命周期并不是随进程的,而是随操作系统的(不止共享内存,只要是system V版本的进程间通信,例如消息队列和信号量,它们的生命周期也都是随操作系统的)

要想查看ststem V版本的进程间通信资源状态,可以用ipcs命令

从上往下,依次是消息队列,共享内存段,信号量

若只想查看其中某个,按照从上往下的顺序可以分别用**-q,-m,-s**选项,这里就以查看共享内存为例

可以看到,即使进程关闭,创建出来的共享内存依然存在,++owner为创建出该共享内存段的用户,perms为该共享内存段的权限,bytes为该共享内存段的大小,nattch为与该共享内存段连接的进程个数++

删除共享内存也有对应的接口,但此时进程已经关闭,需要先从命令行删除该共享内存

在命令行中删除ipc资源可以用ipcrm 命令,若想删共享内存,那就是ipcrm -m ,消息队列和信号量同理。此时要删的是共享内存,因此在后面加上要删除的共享内存的shmid (为什么不是用key呢?key是在内核中标识唯一性的shmid是在应用层标识唯一性的,既然这指令,那一定是在应用层)

shmctl系统调用

要想在进程中删除共享内存,就要用到shmctl系统调用

  • shmid即为想操作的共享内存ID
  • cmd为操作的类型,可选参数有IPC_STATIPC_SETIPC_RMIDIPC_INFOSHM_INFOSHM_STATSHM_LOCKSHM_UNLOCK 。要想删除共享内存段,就需要用到IPC_RMID(rm为删除的意思),将共享内存立即移除。除此之外,IPC_STAT可以获取制定共享内存段的信息,将信息传到buf中
  • buf可以当作一个输出型参数,若我们的cmd是IPC_STAT,就可以在buf中传一个struct shmid_ds类型的结构体,将指定共享内存段的信息都写入进去。该结构体是操作系统暴露给用户的共享内存的一部分数据结构,操作系统管理共享内存的结构比它复杂得多
cpp 复制代码
struct shmid_ds 
{
    struct ipc_perm shm_perm;    /* 所有权和权限 */
    size_t          shm_segsz;   /* 段的大小(字节) */
    time_t          shm_atime;   /* 最后附加时间(进程调用 shmat 的时间) */
    time_t          shm_dtime;   /* 最后分离时间(进程调用 shmdt 的时间) */
    time_t          shm_ctime;   /* 最后修改时间(如权限或大小变更) */
    pid_t           shm_cpid;    /* 创建者的进程 ID */
    pid_t           shm_lpid;    /* 最后操作的进程 ID(shmat/shmdt 调用者) */
    shmatt_t        shm_nattch;  /* 当前附加进程数 */
    ...
};
struct ipc_perm 
{
    key_t          __key;    /* 传入 shmget(2) 的键值 */
    uid_t          uid;      /* 所有者的有效用户 ID */
    gid_t          gid;      /* 所有者的有效组 ID */
    uid_t          cuid;     /* 创建者的有效用户 ID */
    gid_t          cgid;     /* 创建者的有效组 ID */
    unsigned short mode;     /* 权限位(如 0666) + SHM_DEST 和 SHM_LOCKED 标志 */
    unsigned short __seq;    /* 序列号(用于 IPC 内部管理) */
};

但如果我们的cmd是IPC_RMID,则不需要传buf,设为nullptr即可

该函数若失败,返回值为-1

cpp 复制代码
void DelShm(int shmid)//删除共享内存
{
    if(shmctl(shmid,IPC_RMID,nullptr) == -1)
        cerr << "shmctl错误 --> " << errno << ":" << strerror(errno) << endl;
}

现在我们将server端加上删除共享内存的部分,并在删除前sleep5秒代表使用,就可以看到共享内存在ipcs -m中的变化

cpp 复制代码
#include "command.hpp"
int main()
{
    key_t key = GetKey();//获取key
    int shmid = CreateShm(key);//创建全新的共享内存段

    cout << "key:" << key << "\nshmid:" << shmid << endl;

    sleep(5);

    DelShm(shmid);
    return 0;
}

运行的前5秒:

进程结束后:

shmat系统调用

现在我们只是将共享内存创建出来了,还不能使用。在此之前,需要现将共享内存与进程关联起来,就需要用到shmat(at为attach)系统调用,它可以将指定的共享内存段与该进程的进程地址空间连接起来

  • shmid即要连接的共享内存id
  • shmaddr为想要映射到的地址空间,一般来说不需要设定,即传nullptr,除非你真的有需要指定映射地址的需求
  • shmflg的设置跟读写权限有关,一般设置为0,表示读写权限都有

它的返回值就是该共享内存映射到进程地址空间的起始地址,若连接失败,返回-1

cpp 复制代码
void* AttachShm(int shmid)//连接共享内存
{
    void* addr = shmat(shmid,nullptr,0);
    if((long long)addr == -1LL)
    {
        cerr << "shmat错误 --> " << errno << ":" << strerror(errno) << endl;
        DelShm(shmid);
        exit(4);
    }
    return addr;
}

再在server端和client端加入连接共享内存的代码

server.cpp:

cpp 复制代码
#include "command.hpp"
int main()
{
    key_t key = GetKey();//获取key
    int shmid = CreateShm(key);//创建全新的共享内存段
    cout << "key:" << key << "\nshmid:" << shmid << endl;
    sleep(5);
    void* addr = AttachShm(shmid);//连接共享内存
    cout << "共享内存起始地址:" << addr << endl;

    sleep(5);
    DelShm(shmid);//删除共享内存
    return 0;
}

client.cpp:

cpp 复制代码
#include "command.hpp"
int main()
{
    key_t key = GetKey();//获取key
    int shmid = GetShm(key);//获取共享内存段

    cout << "key:" << key << "\nshmid:" << shmid << endl;
    sleep(5);
    void* addr = AttachShm(shmid);//连接共享内存

    cout << "共享内存起始地址:" << addr << endl;
    sleep(5);
    return 0;
}

都运行起来后,该共享内存的连接数先是0

再是1

再是2

再是1,此时是client端结束运行了

再删除共享内存

shmdt系统调用

但是在共享内存与进程关联时直接删除不太好,可以先去关联,即修改页表,回收进程地址空间中的虚拟地址,当nattch为0时再删除

去关联共享内存就需要用到shmdt系统调用(dt为detach的意思)

参数为要去关联的共享内存的起始地址,即shmat的返回值。成功返回0,失败返回-1

cpp 复制代码
void DetachShm(void* addr)//去关联共享内存
{
    if(shmdt(addr) == -1)
        cerr << "shmdt错误: " << errno << strerror(errno) << endl;
}

完善server和client:

cpp 复制代码
//server.cpp:
#include "command.hpp"
int main()
{
    key_t key = GetKey();//获取key
    int shmid = CreateShm(key);//创建全新的共享内存段
    void* addr = AttachShm(shmid);//关联共享内存

    //通信

    DetachShm(addr);//去关联共享内存
    DelShm(shmid);//删除共享内存
    return 0;
}

//client.cpp:
#include "command.hpp"
int main()
{
    key_t key = GetKey();//获取key
    int shmid = GetShm(key);//获取共享内存段
    void* addr = AttachShm(shmid);//关联共享内存

    //通信

    DetachShm(addr);//去关联
    return 0;
}

到现在为止,就完成了通信前的所有准备工作,可以正式开始通信了

共享内存没有专门通信的接口,也不需要,它可以直接当作char*字符串使用

完整代码:

cpp 复制代码
//server.cpp:
#include "command.hpp"
int main()
{
    key_t key = GetKey();//获取key
    int shmid = CreateShm(key);//创建全新的共享内存段
    void* addr = AttachShm(shmid);//关联共享内存

    //通信
    while(true)
    {
        //共享内存可以当作char*字符串使用,因此可以直接用%s输出
        printf("client: %s\n",addr);
        sleep(1);
    }

    DetachShm(addr);//去关联共享内存
    DelShm(shmid);//删除共享内存
    return 0;
}

//client.cpp:
#include "command.hpp"
int main()
{
    key_t key = GetKey();//获取key
    int shmid = GetShm(key);//获取共享内存段
    char* addr = (char*)AttachShm(shmid);//关联共享内存,强转为char*方便后面用snprintf通信

    //通信过程
    int pid = getpid();
    int cnt = 1;
    while(true)
    {
        //共享内存可以直接当作char*类型的字符串使用,因此这里可以直接用snprintf写入
        snprintf(addr,MAX_SIZE,"你好server,我是client,这是我发的第%d条消息[mypid:%d]",cnt++,pid);
        sleep(1);
    }

    DetachShm(addr);//去关联
    return 0;
}

command.cpp:

cpp 复制代码
#ifndef __COMM_HPP__//防止重复包含头文件(第一次执行时,没有定义该宏,会定义该宏,第二次因为定义了该宏,就不会执行了)
#define __COMM_HPP__

#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#define PATHNAME "."//这里就举例为设置ftok的路径为当前路径
#define PROJ_ID 0x114//ftok的项目ID这里也是随便设的
#define MAX_SIZE 4096//指定创建的共享内存段的大小
using namespace std;

key_t GetKey()//获取key
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key == -1)//若Key为-1,代表获取key失败,则终止进程
    {
        cerr << "ftok错误 --> " << errno << ":" << strerror(errno) << endl;//cerr为标准错误,它不需要经过缓冲区,会直接显示在屏幕上,更快
        exit(1);
    }
    return key;
}

int ShmHelper(key_t key,int shmflg)//通用的创建/获取共享内存
{
    int shmid = shmget(key,MAX_SIZE,shmflg);
    if(shmid == -1)//若返回-1,则创建/获取失败,则终止进程
    {
        cerr << "shmget错误 --> " << errno << ":" << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(key_t key)//创建共享内存(若成功,必是一个全新的共享内存)
{
    return ShmHelper(key,IPC_CREAT | IPC_EXCL | 0600);//设为只有拥有者可以读写
}

int GetShm(key_t key)//获取共享内存
{
    return ShmHelper(key,IPC_CREAT);
}

void DelShm(int shmid)//删除共享内存
{
    if(shmctl(shmid,IPC_RMID,nullptr) == -1)
        cerr << "shmctl错误 --> " << errno << ":" << strerror(errno) << endl;
}

void* AttachShm(int shmid)//关联共享内存
{
    void* addr = shmat(shmid,nullptr,0);
    if((long long)addr == -1LL)
    {
        cerr << "shmat错误 --> " << errno << ":" << strerror(errno) << endl;
        DelShm(shmid);
        exit(4);
    }
    return addr;
}

void DetachShm(void* addr)//去关联共享内存
{
    if(shmdt(addr) == -1)
        cerr << "shmdt错误: " << errno << strerror(errno) << endl;
}

#endif 

前三行为空是因为client还没启动

共享内存特点(优缺点):

共享内存的生命周期随OS,这一点上面已经说过了,就不赘述了

优点:共享内存是最快的IPC方式。它能大大减少数据拷贝的次数

在用别的IPC方式时,例如管道,写端需要先将数据拷贝到buffer[]中再将buffer拷贝到管道中 ,读端再将管道中的数据拷贝到buffer中再将buffer中的数据拷贝到屏幕上,共4次拷贝

但在共享内存中,写端可以将数据直接拷贝到共享内存中 ,读端再将共享内存的数据拷贝到显示器上,共2次拷贝

键盘输入的数据会先存到输入缓冲区 中,再传给buffer[]/共享内存;同理,输出到显示器的数据也会先存到输出缓冲区,再给显示器。若把这两次拷贝加上,管道共4+2=6次拷贝,共享内存共2+2=4次拷贝

缺点:不会进行同步与互斥操作,没有对数据做任何保护

当前程序是写端每隔一秒发送一次,读端每隔一秒接收一次,若此时将写端改为每隔5秒发送一次

会发现每5秒读端读取的都是一条消息,若是管道,当写端没有写入时,读端会阻塞等待

并且当我们只启动了server端时,也会一直输出,不会像管道一样等待写端启动

因此可以再将代码完善一下,实现只有当写端写入时,通知读端读取,否则让server阻塞等待

这里可以利用管道的同步与互斥。写端每次向共享内存写入后,就向管道中发送一条消息;读端只有当从管道中读到消息后,再去共享内存中读取

完善同步与互斥的共享内存通信(管道实现):

cpp 复制代码
//server.cpp:
#include "command.hpp"
int main()
{
    bool flg = CreatePipe();//创建命名管道
    assert(flg);
    (void)flg;
    int fd = open(PIPE_PATH,O_RDONLY);//只读打开命名管道
    if(fd == -1)
    {
        cerr << "open错误 --> " << errno << ":" << strerror(errno) << endl;
        exit(6);
    }

    key_t key = GetKey();//获取key
    int shmid = CreateShm(key);//创建全新的共享内存段
    void* addr = AttachShm(shmid);//关联共享内存

    //通信
    while(true)
    {
        //管道
        if(PipeRead(fd,shmid) == false)//只有当写端向管道发送信息后,才允许server从共享内存中读取
            break;//若写端退出,则结束server端
        //共享内存可以当作char*字符串使用,因此可以直接用%s输出
        printf("client: %s\n",addr);
        sleep(1);
    }
    RemovePipe();
    DetachShm(addr);//去关联共享内存
    DelShm(shmid);//删除共享内存
    return 0;
}

//client.cpp:
#include "command.hpp"
int main()
{
    int fd = open(PIPE_PATH,O_WRONLY);//只写打开管道文件
    if(fd == -1)
    {
        cerr << "open错误 --> " << errno << ":" << strerror(errno) << endl;
        exit(5);
    }
    sleep(1);//这里是为了让server先运行,将共享内存创建工作做完,否则因为管道有同步功能,只有双方都打开管道文件时才会继续运行,会导致client端试图创建共享内存

    key_t key = GetKey();//获取key
    int shmid = GetShm(key);//获取共享内存段
    char* addr = (char*)AttachShm(shmid);//关联共享内存,强转为char*方便后面用snprintf通信

    //通信过程
    int pid = getpid();
    int cnt = 1;
    while(true)
    {
        //共享内存可以直接当作char*类型的字符串使用,因此这里可以直接用snprintf写入
        snprintf(addr,MAX_SIZE,"你好server,我是client,这是我发的第%d条消息[mypid:%d]",cnt++,pid);
        //管道
        write(fd,"1",sizeof(1));//每次向共享内存中写入,都会向管道发送一条消息
        sleep(5);//写端每5秒写一次,读端每1秒读一次
    }

    DetachShm(addr);//去关联
    return 0;
}

comand.hpp:

cpp 复制代码
#ifndef __COMM_HPP__//防止重复包含头文件(第一次执行时,没有定义该宏,会定义该宏,第二次因为定义了该宏,就不会执行了)
#define __COMM_HPP__

#include <iostream>
#include <cstring>
#include <cassert>
#include <cerrno>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATHNAME "."//这里就举例为设置ftok的路径为当前路径
#define PIPE_PATH "/tmp/mypipe"
#define PROJ_ID 0x114//ftok的项目ID这里也是随便设的
#define MAX_SIZE 4096//指定创建的共享内存段的大小
using namespace std;

key_t GetKey()//获取key
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key == -1)//若Key为-1,代表获取key失败,则终止进程
    {
        cerr << "ftok错误 --> " << errno << ":" << strerror(errno) << endl;//cerr为标准错误,它不需要经过缓冲区,会直接显示在屏幕上,更快
        exit(1);
    }
    return key;
}

int ShmHelper(key_t key,int shmflg)//通用的创建/获取共享内存
{
    int shmid = shmget(key,MAX_SIZE,shmflg);
    if(shmid == -1)//若返回-1,则创建/获取失败,则终止进程
    {
        cerr << "shmget错误 --> " << errno << ":" << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(key_t key)//创建共享内存(若成功,必是一个全新的共享内存)
{
    return ShmHelper(key,IPC_CREAT | 0600);//设为只有拥有者可以读写
}

int GetShm(key_t key)//获取共享内存
{
    return ShmHelper(key,IPC_CREAT);
}

void DelShm(int shmid)//删除共享内存
{
    if(shmctl(shmid,IPC_RMID,nullptr) == -1)
        cerr << "shmctl错误 --> " << errno << ":" << strerror(errno) << endl;
}

void* AttachShm(int shmid)//关联共享内存
{
    void* addr = shmat(shmid,nullptr,0);
    if((long long)addr == -1LL)
    {
        cerr << "shmat错误 --> " << errno << ":" << strerror(errno) << endl;
        DelShm(shmid);
        exit(4);
    }
    return addr;
}

void DetachShm(void* addr)//去关联共享内存
{
    if(shmdt(addr) == -1)
        cerr << "shmdt错误: " << errno << strerror(errno) << endl;
}

bool CreatePipe()//创建命名管道
{
    int fd = mkfifo(PIPE_PATH,0600);
    if(fd == -1)
    {
        cerr << "mkfifo错误 --> " << errno << ":" << strerror(errno) << endl;
        return true;
    }
    return true;
}

void RemovePipe()//删除命名管道
{
    int n = unlink(PIPE_PATH);
    assert(n == 0);
    (void)n;
}

bool PipeRead(int fd,int shmid)//读端的管道验证
{
    char buf[4];
    int n = read(fd,buf,sizeof(buf));
    if(n == 0)
    {
        cout << "写端退出" << endl;
        return false;//若写端退出,则返回false,让写端一起退出
    }
    if(n < 0)
    {
        cerr << "read错误 --> " << errno << ":" << strerror(errno) << endl;
        RemovePipe();
        DelShm(shmid);
        exit(7);
    }
    return true;
}

#endif 

再运行时,只有当读写全开时,才会开始读取,并且即使写端每5秒写一次,读端每1秒读一次,也不会读取陈旧信息,且写端关闭时读端会自动退出

共享内存的内核结构

在介绍shmctl系统调用时,简单说了一下它的第三个参数shmid_ds结构体

cpp 复制代码
struct shmid_ds 
{
    struct ipc_perm shm_perm;    /* 所有权和权限 */
    size_t          shm_segsz;   /* 段的大小(字节) */
    time_t          shm_atime;   /* 最后附加时间(进程调用 shmat 的时间) */
    time_t          shm_dtime;   /* 最后分离时间(进程调用 shmdt 的时间) */
    time_t          shm_ctime;   /* 最后修改时间(如权限或大小变更) */
    pid_t           shm_cpid;    /* 创建者的进程 ID */
    pid_t           shm_lpid;    /* 最后操作的进程 ID(shmat/shmdt 调用者) */
    shmatt_t        shm_nattch;  /* 当前附加进程数 */
    ...
};
struct ipc_perm 
{
    key_t          __key;    /* 传入 shmget(2) 的键值 */
    uid_t          uid;      /* 所有者的有效用户 ID */
    gid_t          gid;      /* 所有者的有效组 ID */
    uid_t          cuid;     /* 创建者的有效用户 ID */
    gid_t          cgid;     /* 创建者的有效组 ID */
    unsigned short mode;     /* 权限位(如 0666) + SHM_DEST 和 SHM_LOCKED 标志 */
    unsigned short __seq;    /* 序列号(用于 IPC 内部管理) */
};

现在我们就试着读取一下共享内存的各种属性

cpp 复制代码
#include "command.hpp"
int main()
{
    key_t key = GetKey();//获取key
    int shmid = CreateShm(key);//创建全新的共享内存段
    void* addr = AttachShm(shmid);//关联共享内存

    struct shmid_ds buffer;//输出型参数
    shmctl(shmid,IPC_STAT,&buffer);
    cout << "段大小:" << buffer.shm_segsz << endl << "key值:" << buffer.shm_perm.__key << endl;

    DetachShm(addr);//去关联共享内存
    DelShm(shmid);//删除共享内存
    return 0;
}

system V 消息队列(了解)

消息队列是操作系统提供的内核级队列它允许两端同时进行读写

消息队列中的数据是一个个的节点,每个节点是一个结构体,但我们只需要关注该节点的两个字段:

cpp 复制代码
struct
{
    //....
    int type;//节点类型
    char buffer[];//发送的数据
    //....
}

通过type字段,可以知道该节点是谁发送的 ,例如一端发送的节点type为1,另一端发送的节点type为0,那么就可以知道哪些节点是某一端应该接收的

系统调用

想要创建或获取消息队列,需要用到 msgget系统调用

  • 第一个参数key相信大家并不陌生,并且作用也和大家想的一样
  • 第二个参数msgflg也有些眼熟,因为共享内存有shmflg,并且它们的用法也一致,都只有IPC_CREATIPC_EXCL 两个参数

返回值即返回类似于shmid的msqid

想要删除消息队列,需要用到 msgctl系统调用

  • msqid即为消息队列的编号,和shmid作用一样
  • cmd的参数为想对消息队列做的操作,最常用的也是 IPC_RMID (删除),除此之外,若设置IPC_STAT ,也可以通过给第三个参数buf传一个msqid_ds结构体当作输出型参数获取消息队列的信息
cpp 复制代码
struct msqid_ds {
    struct ipc_perm msg_perm;     /* 所有权和权限 */
    time_t          msg_stime;    /* 最后一次 msgsnd(2) 的时间 */
    time_t          msg_rtime;    /* 最后一次 msgrcv(2) 的时间 */
    time_t          msg_ctime;    /* 最后一次修改的时间 */
    unsigned long   __msg_cbytes; /* 队列中当前字节数(非标准扩展) */
    msgqnum_t       msg_qnum;     /* 队列中当前消息数 */
    msglen_t        msg_qbytes;   /* 队列允许的最大字节数 */
    pid_t           msg_lspid;    /* 最后一次 msgsnd(2) 的进程 PID */
    pid_t           msg_lrpid;    /* 最后一次 msgrcv(2) 的进程 PID */
};
struct ipc_perm {
    key_t          __key;       /* 提供给 msgget(2) 的键值 */
    uid_t          uid;         /* 所有者的有效用户 ID (UID) */
    gid_t          gid;         /* 所有者的有效组 ID (GID) */
    uid_t          cuid;        /* 创建者的有效用户 ID (UID) */
    gid_t          cgid;        /* 创建者的有效组 ID (GID) */
    unsigned short mode;        /* 访问权限(如读/写权限标志) */
    unsigned short __seq;       /* 内部序列号 */
};

消息队列发送数据(写入)的接口为 msgsnd 系统调用(snd为send的意思)

  • msqid即消息队列编号
  • msqp即为发送的数据块,该数据块是一个结构体,里面包含了数据块的类型和数据
cpp 复制代码
struct msgbuf 
{
    long mtype;       /* message type, must be > 0 */
    char mtext[1];    /* message data */
};

msgsz为数据块的大小,msgflg一般设为0

与发送数据相对应的就是接收数据(读取),为msgrcv系统调用

只是比发送数据的接口多了一个数据块类型参数msgtyp,可以指定接收什么类型的数据

system V 信号量

信号量本质上是一个计数器,用来表示公共资源中,资源数量的多少(公共资源即为可以被多个进程同时访问的资源)

在之前的共享内存中,它没有对数据的保护 ,就可能会造成数据不一致(同一份数据在不同地方的存储或表示不一致,导致不同用户或系统看到的结果不同,例如多个用户同时修改一份数据)

被保护起来的资源被称为临界资源访问临界资源的代码部分被称为临界区其他代码部分被称为非临界区

要想将公共资源保护起来,就需要同步或互斥 ,而信号量就可以实现。互斥:当有两个进程想访问同一份公共资源时,只允许一个人去访问,只有当一个进程访问结束后,才允许另一个进程访问

若一个操作只有两态,这种情况称为原子性。例如转账系统,要么不转,要转就必须成功,如果转到一半,已经将自己的钱扣出去了,但因为网络问题对方没有收到钱,就必须将钱再加回来,

信号量就可以让多进程实现原子性的同步与互斥

虽然信号量本质是一个计数器,但并不是随便一个计数器都能被多个进程所看到。要想让多个进程看到同一个计数器,就需要先让多个进程看到同一份资源,因此信号量也被划分到了进程间通信的范畴

例如,在电影院中,并不是只要坐在座位上,这个座位就是你的,而是需要通过买票,票上有自己的座位号,当你买票成功的那一刻,这个座位就已经属于你了,即使你不去看这场电影,这个座位也不会有人坐。因此电影买票的本质是对电影院中的座位的预订机制

并且,若一个电影院只有100个座位,对应的有100张票,那么绝对不能售出第101张票

因此可以定义一个计数器int count = 100; 每次有人买票成功后,将count--; 当count == 0 时,就不能再售票了,

共享资源分为两种情况:1.作为一个整体去使用 2.划分为一个一个的资源子部分

例如管道就是作为整体去使用的例子,当有进程在访问管道时,别的进程就不能访问,只有当该进程访问结束后才可以访问

但有时需要多个进程并行访问,A访问该共享资源的1部分,B访问该共享资源的2部分,这时就是将公共资源划分为资源子部分去使用,电影院就是很好的例子,将整个放映厅想象成一块共享资源,每个座位就是一个子部分,人相当于进程,每个进程想要访问资源子部分都需要预订,预订就需要通过信号量来完成

cpp 复制代码
int sem = 40;
//所有进程想访问公共资源,必须先将信号量--   P操作
sem--;
//当访问结束后,必须将信号量++     V操作
sem++;

进程访问信号量让它--的操作叫做P操作

访问结束后让信号量++的操作叫做V操作

由于信号量也是公共资源,它也需要保证自己的安全,因此它的++、--操作也是原子性

++若一个信号量的初始值是1,就代表这块资源是被整体使用,否则就是被划分为子部分使用++

创建/获取信号量,需要用到semget系统调用

  • key的作用和共享内存、消息队列中的key一样
  • nsems为想要创建的信号量的个数,该系统调用允许一次性创建多个信号量,用数组下标的方式表示
  • semflg,和前面介绍的一样,都是只有IPC_CREATIPC_EXCL两个选项

返回值是semid,信号量编号

如果想删除信号量,就要用到semctl系统调用

  • semid即为信号量编号
  • semnum,如果在创建信号量时创建了多个信号量,该参数就是用于填数组下标的,如果只创建了一个,就写0
  • cmd即为想对信号量做的操作,和共享内存、消息队列同样,IPC_RMID 为删除。如果是IPC_STAT,即为获取信号量的信息,需要填第四个参数

第四个参数是可变参数,它需要传一个semnu联合体的实例 ,注意这里和前面的共享内存和消息队列就有些许不一样了,这里传的是一个实例,不是指针 ,而这个semnu联合体里,有一个指针变量,类似于共享内存、消息队列的结构体指针参数

cpp 复制代码
// 联合体semun用于semctl系统调用的参数传递
union semun {
    int              val;    /* 用于SETVAL命令设置信号量值 */
    struct semid_ds *buf;    /* 用于IPC_STAT/IPC_SET命令的缓冲区指针 */
    unsigned short  *array;  /* 用于GETALL/SETALL命令的数组指针 */
    struct seminfo  *__buf;  /* 用于IPC_INFO命令的缓冲区指针 
                               (Linux特有功能) */
};

// 信号量集合状态结构体
struct semid_ds {
    struct ipc_perm sem_perm;  /* 信号量集的权限和所有权信息 */
    time_t          sem_otime; /* 最后一次semop操作的时间戳 */
    time_t          sem_ctime; /* 最后一次状态修改的时间戳 */
    unsigned long   sem_nsems; /* 信号量集中包含的信号量数量 */
};

// IPC对象权限结构体
struct ipc_perm {
    key_t          __key;     /* 通过semget(2)传入的键值 */
    uid_t          uid;       /* 所有者的有效用户ID */
    gid_t          gid;       /* 所有者的有效组ID */
    uid_t          cuid;      /* 创建者的有效用户ID */
    gid_t          cgid;      /* 创建者的有效组ID */
    unsigned short mode;      /* 权限标志位(低9位有效) */
    unsigned short __seq;     /* 序列号(用于内部管理) */
};

若想获取信息,可以如下定义:

cpp 复制代码
union semnu s;
struct semid_ds sd;
s.buf = &sd;//创建一个semnu变量,指向一个semid_ds变量
semctl(semid,0, IPC_STAT,s);
cout << s.buf->sem_nsems;//查看信息

要想对信号量进行PV操作(++、--),就要用到semop系统调用(op为option的意思)

  • semid即消息量编号
  • sops是一个sembuf类型的结构体指针:
cpp 复制代码
unsigned short sem_num; /* 信号量编号 /
short sem_op; / 信号量操作 /
short sem_flg; / 操作标志 */
  • sem_num,若一次创建了多个信号量,该位置就是操作信号量的下标,若只创建了一个信号量,则为0
  • sem_op,一般有两个值,-1代表--操作,即P操作,1代表++操作,即V操作
  • sem_flg一般设为0
  • nsops为信号量的个数,该系统调用允许同时对多个信号量进行PV操作,如果是多个信号量,sops就是一个数组,每个元素说明了每个信号量要进行的操作

IPC资源的组织方式

现在再回顾共享内存、消息队列、信号量这三个IPC方式,会发现它们三者的接口都高度相似 ,并且它们暴露给用户的内核结构体也高度相似,它们的结构体中的第一个元素都是struct ipc_perm类型

实际上,操作系统会维护一个指针数组struct ipc_perm *perms[],当要创建共享内存/消息队列/信号量时,就会把它的struct ipc_perm类型字段的地址存入该数组

并且,在C语言中,结构体第一个成员的地址和结构体对象本身的地址,在值上来说是相等的(意义不一样)

那么如果将来需要访问myshm中的资源,就可以直接将该数组的值强转成对应类型,就可以访问该资源了

cpp 复制代码
(struct shmid_ds*)perms[0] ->【字段】;

这种思路和C++的多态很像,当然,是爸爸像儿子: )

相关推荐
蓝队云计算2 小时前
深耕本土,安全稳定——云南云服务器为何首推蓝队云
运维·服务器·安全·云服务器·蓝队云
人生苦短,菜的抠脚2 小时前
RK628 Linux 内核驱动开发指南
linux·驱动开发
代码AC不AC2 小时前
【Linux】命名管道
linux·命名管道
陌上花开缓缓归以2 小时前
linux boot 烧写纪要以及内存相关分析
linux·服务器·网络
yy_xzz2 小时前
【Linux开发】 04 Linux UDP 网络编程
linux·网络·udp
带鱼吃猫2 小时前
C++11 核心特性解析(一):从初始化列表到移动语义,解锁高效对象构造
开发语言·c++
123过去2 小时前
mdb-sql使用教程
linux·网络·数据库·sql
m0_694845572 小时前
Docker 从入门到实践教程:docker_practice 完整学习指南
运维·服务器·docker·容器·云计算·github
郝学胜-神的一滴2 小时前
冷却时间下的任务调度最优解:从原理到实现
数据结构·c++·算法·面试