[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第1张图片

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量

    • 进程间通信
      • 进程间通信目的
    • 管道
      • 匿名管道
        • pipe with fd
        • pipe in kernel
      • 管道读写规则
        • 匿名信道四种情况
        • 管道**三推六问**
      • 命名管道
        • 创建命名管道
          • 命令行创建
          • C语言创建
        • 命名管道的打开规则
        • 测试用例1
          • 测试用例2
      • pipe小结
    • System V IPC共享内存
      • 共享内存
      • 共享内存函数
      • 共享内存的创建
          • shmflag宏
          • size
          • ftok
        • 测试用例:创建共享内存
      • 共享内存的释放
          • 程序指令增删改共享内存,利用shmctl
      • 共享内存的关联
        • shmat(attach)
        • shmaddr
        • 测试用例
      • 共享内存的去关联
        • 使用共享地址通信
        • 共享内存数据结构
      • **重要的查看共享内存的命令**
      • 共享内存V.S.管道
    • System V 消息队列
      • 消息队列数据结构
      • msgget
      • msgctl
      • msgsnd
      • msgrcv
      • msqid_ds
    • system V信号量
      • semid_ds
      • semget
      • 进程互斥

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第2张图片

进程间通信

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第3张图片

一个独立的进程不受其他进程执行的影响,而一个协作的进程可以受到其他正在执行的进程的影响。尽管人们可以认为这些独立运行的进程将非常有效地执行,但实际上,在许多情况下,可以利用协作性质来提高计算速度、便利性和模块化。进程间通信(IPC)是一种允许进程相互通信并同步它们的动作的机制。这些进程之间的通信可以看作是它们之间的一种合作方式。

进程通信的意义就是让多个进程协同完成某种任务

进程间通信目的

为什么要进程通信?

数据传输:一个进程需要将它的数据发送给另一个进程

资源共享:多个进程之间共享同样的资源。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

如何做到进程之间通信?

  1. 进程运行的时候是具有独立性的
  2. 进程间通信一定要借助第三方资源(OS)
  3. 通信的本质是“数据的拷贝”

⚡️ 如何完成“数据的拷贝”?

因为两个进程是有独立性的,所以直接让A给到B是不可能的,以为A不应该看到B及进程所在的空间,所以我们采取进程A->数据拷贝给OS->OS数据拷贝给进程B

操作系统一定要提供一段内存区域,能够被双方进程看到,进程间通信的本质是,让不同的进程看到同一份资源(内存,文件内核缓冲)

在我们的生活中打电话、发邮件、用QQ聊天都叫做通信,通信的本质是传递信息。但是传递信息这种说法并不是很直观,站在程序员的角度,通信的本质其实叫做传递数据。

管道

管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第4张图片

测试用例:

who | wc -l

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第5张图片

注意: 管道只能单向通信

匿名管道

匿名管道最典型的特征是供具有血缘关系(常见于父子)的进程进行进程间通信。

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第6张图片

#include 
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

pipe属于输出型参数:调这个函数是为了得到某些返回值

数组元素 含义
pipefd[0] 管道读端的文件描述符
pipefd[1] 管道写端的文件描述符

下面来具体看一下管道的过程:这里之所以要使用fork是为了让两个进程看到同一份数据,这种通信形式就是父子进程通信

为什么要后面再关?

一开始要让子进程继承父进程的信息,之后由于管道只能是单向的,所以说要关闭,一个关读,一个关写,fd[0](想象成读的嘴)一般保存的是读端,fd[1](想象的是一支笔)保存的是写端

利用fork创建管道

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第7张图片

  1. 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
  2. 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。

测试用例:

#include 
#include 
#include 
#include 
#include 
#include 

// child->write, fahter->read
int main()
{
    int fd[2] = {0};
    if (pipe(fd) < 0)
    {
        perror("pipe!");
        return 1;
    }

    pid_t id = fork();
    if (id == 0)
    {
        // child
        close(fd[0]);
        const char *msg = "hello father, I am child!";
         int count=10;
        while(count){
            write(fd[1],msg,strlen(msg));
            count--;
            sleep(1);
        }
        close(fd[1]);
        exit(0);
    }

    close(fd[1]);

    char buff[64];
    while (1)
    {
        ssize_t s = read(fd[0], buff, sizeof(buff));
        if (s > 0)
        {
            buff[s] = '\0';//前提是已经知道了是字符串,所以这里在结尾加了一个'\0'
            printf("child send to father# %s\n", buff);
        }
        else if (s == 0)
        {
            printf("read file end!\n");
            break;
        }
        else
        {
            printf("read error!\n");
            break;
        }
        close(fd[0]);
        break;
    }

    sleep(3);
    // father
    int status = 0;
    waitpid(id, &status, 0);

    printf("child quit!: sig: %d\n", status & 0x7F);

    return 0;
}

image-20221005104934099

父子进程通信可不可以创建全局缓冲区来完成通信,因为进程有独立性,进程对全局缓冲区做的修改,另一方是看不到了,注意进程在通信的时候用的是操作系统的空间,不是进程A或B的空间

pipe with fd

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第8张图片

pipe in kernel

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第9张图片

管道读写规则

pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:

int pipe2(int pipefd[2], int flags);

pipe2函数的第二个参数用于设置选项。

当没有数据可读时:(读条件)
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

当管道满的时候:(写条件)
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

匿名信道四种情况

当管道里面没有数据或者管道没有空间的时候,对应进程就会被挂起

  1. 读端不读或读的慢,写端要等读端
  2. 读端关闭,写端一直写,写方被操作系统杀掉,写入没有意义(代码没有运行完,直接退出,会收到信号,收到SIGPIPE信号直接终止)
  3. 写端不写或者写的慢,读端要等写端
  4. 写端关闭,读端会读完管道内的数据然后再读,会读到0,表示读到文件结尾

其中前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。

第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。

第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。

管道三推六问

进程同步和互斥

临界资源是父亲和孩子都能看到的资源

在多执行流的时候:正是因为我们在通信的时候,需要两个进程看到同一份资源,可是看到同一份资源不加任何保护机制就有可能一份资源会同时读写,交叉读写,导致产生数据不一致,这就产生了进程的同步和互斥,我们的管道内部已经自动提供了互斥与同步的机制

互斥:任何时候只能有一个人正在使用某一个资源

如果写端关闭了,读端如何知晓并退出?反之如果读端退出了,那么写端怎么做?

如果写端关闭,读端就会read返回值0,代表文件结束

while (1)
{
    ssize_t s = read(fd[0], buff, sizeof(buff));//ssize_t是有符号整数
    if (s > 0)
    {
        buff[s] = '\0';
        printf("child send to father# %s\n", buff);
    }
    else if (s == 0)
    {
        printf("read file end!\n");
        break;
    }
    else
    {
        printf("read error!\n");
        break;
    }
    close(fd[0]);
    break;
}

如果读端关闭,操作系统不会允许进程浪费资源的,因为写已经没有意义了,直接写结束

如果打开的文件的进程退出了,文件也会被释放掉(管道的生命周期随进程变化

管道是提供流式服务的,管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

  • 流式服务: 数据没有明确的分割,不分一定的报文段。
  • 数据报服务: 数据有明确的分割,拿数据按报文段拿。

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第10张图片

匿名通道,适合具有血缘关系的进程进行进程间通信,常用于父子

补充:

  1. 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
  2. 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
  3. 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。

管道的大小

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第11张图片

命名管道

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。为了解决匿名管道只能在父子进程间通信的缺陷,引入了命名管道。

其性质除了能让任意进程间通信外,与匿名管道基本一致,即创建一个文件,一个进程往文件中写数据,一个进程读数据,且不让文件内容刷新到磁盘上,从而实现任意进程间的通信。

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件

创建命名管道
命令行创建

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:现在我们的管道是有名字的了

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第12张图片

$ mkfifo filename
C语言创建

命名管道也可以从程序里创建,相关函数有:

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第13张图片

int mkfifo(const char *filename,mode_t mode);

mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。

  • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。

  • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)

mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。

例如,将mode设置为0666,则命名管道文件创建出来的权限如下:

在这里插入图片描述

若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。

mkfifo函数的返回值。

  • 命名管道创建成功,返回0。
  • 命名管道创建失败,返回-1。

创建示例:

#include 
#include 
#include 

#define FILE_NAME "myfifo"

int main()
{
    if (mkfifo(FILE_NAME, 0644) < 0)
    {
        perror("mkfifo");
        return 1;
    }
}
命名管道的打开规则

1、如果当前打开操作是为读而打开FIFO时。

O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
O_NONBLOCK enable:立刻返回成功。
2、如果当前打开操作是为写而打开FIFO时。

O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。

测试用例1

下面测试一个管道client写,server可以读到

///
///common.h
///
#pragma once

#include 
#include 
#include 
#include 
#include 
#include 

#define FILE_NAME "myfifo"

///
///server.c
///
#include"commmon.h"
int main()
{
    //创建管道
    if (mkfifo(FILE_NAME, 0644) < 0)
    {
        perror("mkfifo");
        return 1;
    }
	//打开管道只读
    int fd = open(FILE_NAME, O_RDONLY);
    if (fd < 0)
    {
        perror("open errror!\n");
        return 2;
    }

    char msg[128];
    while (1)
    {
        msg[0] = 0;
        ssize_t s = read(fd, msg, sizeof(msg) - 1);
        if (s > 0)
        {//继续读
            msg[s] = 0;        
            printf("client# %s\n", msg); //10+20
        }
        else if (s == 0)
        {//写完了
            printf("client quit!\n");
            break;
        }
        else
        {
            printf("read error!\n");
            break;
        }
    }
    
///
///client.c
///
#include "comm.h"

int main()
{
    int fd = open(FILE_NAME, O_WRONLY);
    if (fd < 0)
    {
        printf("open error!\n");
        return 1;
    }

    char msg[128];
    while (1)
    {
        msg[0] = 0;//置为空字符串
        printf("Please Enter# ");
        fflush(stdout);
        ssize_t s = read(0, msg, sizeof(msg));
        if(s>0){
            msg[s]=0;//'\0'结尾
            write(fd,msg,strlen(msg));
        }
    }
    close(fd);
    return 0;
}

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第14张图片

当客户端和服务端运行起来时,我们还可以通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同。也就证明了,命名管道是可以实现两个毫不相关进程之间的通信的。

测试用例2

通过client读取file.txt,然后管道传给server,然后server读取,写入到file.txt.bak完成管道的读取

///
///server.c
///
int main()
{
    if (mkfifo(FILE_NAME, 0644) < 0)
    {
        perror("mkfifo");
        return 1;
    }

    int fd = open(FILE_NAME, O_RDONLY);
    if (fd < 0)
    {
        perror("open errror!\n");
        return 2;
    }

    int out = open("file-bak.txt", O_CREAT | O_WRONLY, 0644);
    if (out < 0)
    {
        perror("open errror!\n");
        return 2;
    }
    
    char msg[128];
    while (1)
    {
        msg[0] = 0;
        ssize_t s = read(fd, msg, sizeof(msg) - 1);
        if (s > 0)
        {//管道写到文件中
            write(out, msg, s);
        }
        else if (s == 0)
        {
            printf("client quit!\n");
            break;
        }
        else
        {
            printf("read error!\n");
            break;
        }
    }

    close(fd);
    close(out);
    return 0;
}
///
///client.c
///
int main()
{
    int fd = open(FILE_NAME, O_WRONLY);
    if (fd < 0)
    {
        printf("open error!\n");
        return 1;
    }

    int in = open("file.txt", O_RDONLY);
    if (in < 0)
    {
        printf("open error!\n");
        return 1;
    }
    char msg[128];
    while (1)
    {
        msg[0] = 0;
        // dup2(in, 0);
        // ssize_t s = read(0, msg, sizeof(msg));
        ssize_t s = read(in, msg, sizeof(msg));

        if (s == sizeof(msg ))
        {//如果期望读的和写的实际长度相等的话就读完
            write(fd, msg, s);
        }
        else if (s < sizeof(msg))
        {
            write(fd, msg, s);//读不完就EOF
            printf("read end of file!\n");
            break;
        }
        else
        {
            break;
        }
    }

    close(in);
    close(fd);
    return 0;
}

pipe小结

下面的管道是匿名的还是命名的?

cat file.txt | gerp "你好"

这里的管道的功能是输出file.txt中输出有关你好的语句,这里的管道是匿名管道

sleep 1000 |sleep 2000 |sleep 3000

这里的管道是命名的还是匿名的呢?

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第15张图片

我们发现这三者是兄弟关系,这是我们发现他有匿名管道的特征,但是我们还是不能随便判断,因为命名管道也是可以有血缘关系的,记住这些命令行上的一般都是匿名

有趣的一张图片

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第16张图片

From https://twitter.com/b0rk/status/982996819830624256/photo/1

System V IPC共享内存

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第17张图片

之前的管道通信是基于文件的,OS没有做过多的设计工作,而systemV这个进程间的通信:是OS特地设计的方式,不过本质是进层间的通信都是想要让不同的进程看到同一份资源

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

共享内存是一种程序进程可以比使用常规操作系统服务读取和写入更快地交换数据的方法。例如,客户端进程可能有数据要传递给服务器进程,服务器进程将修改这些数据并返回给客户端。通常,这需要客户端写入输出文件(使用缓冲区),然后服务器将该文件作为输入从缓冲区读取到它自己的工作空间。使用共享内存的指定区域,可以使两个进程直接访问数据,而无需使用系统服务。要将数据放入共享内存中,客户端在检查信号量值后访问共享内存,写入数据,然后重置信号量以向服务器(定期检查共享内存以查找可能的输入)发出数据正在等待的信号。反过来,服务器进程将数据写回共享内存区域,使用信号量指示数据已准备好被读取。

共享内存

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第18张图片

怎么共享内存?

  1. 申请共享内存(物理内存已经开辟好)

  2. 现在需要共享内存挂接到地址空间(建立映射关系)

    1. 映射的本质是改变修改页表,虚拟地址空间中开辟空间,这些用的是系统接口,完成开辟空间,建立映射,开辟虚拟地址,返回给用户,这些都是系统做的,所以说系统可以修改映射让不同的进程看到同一份资源,狭义上说就是共享内存
  3. 取消关联共享内存(修改链表,取消映射关系)

  4. 释放关联内存(内存还给系统),删除shm

共享内存函数

再通信双方中,一定有一个创建shm,一个获取

多个共享内存

系统可能存在多个共享内存

OS需要管理多个共享内存,怎么管理?先描述再组织

为共享内存维护响应的数据结构,如何保证共享内存是唯一的?保证不同的进程看到的是同一个shm?共享内存需要通过一个标识来保证内存的唯一性,下面的key值就是唯一的这个参数,key会被存进数据结构中

共享内存的创建

功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);

shmget函数的参数说明:

第一个参数key,表示待创建共享内存在系统当中的唯一标识。
第二个参数size,表示待创建共享内存的大小。
第三个参数shmflg,表示创建共享内存的方式。

shmget函数的返回值说明:

shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
shmget调用失败,返回-1。

shmflag宏
组合方式 作用
IPC_CREAT 如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则直接返回该共享内存的句柄
IPC_CREAT | IPC_EXCL 如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则出错返回

也就是说:

  • 使用IPC_CREAT,一定会获得一个共享内存的句柄,但无法确认该共享内存是否是新建的共享内存。
  • 使用组合IPC_CREAT | IPC_EXCL,只有shmget函数调用成功时才会获得共享内存的句柄,并且该共享内存一定是新建的共享内存。

同时我们也可以在这里 | 上我们的权限(mode)如 0666

size

这里的size通常我们设置为4096,因为我们知道一个page的PAGE_SIZE是4096 bytes,也就是一页数据

当我们申请了4KB的大小的话,相当于我们的共享内存是页对齐的,如果我们随便写的话,就会导致空间浪费,操作系统每次会分配一页大小的整数倍,虽然我们用的时候只能申请多少给用多少,也就是说我们申请了4097KB,操作系统实际上给了我们9192大小的内存,但是我们只能使用4097大小

ftok

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第19张图片

函数原型:key_t ftok(const char *pathname, int proj_id)
函数功能: 对给定的文件名(pathname)进行检测, 若文件不存在返回-1, 反之将proj_id的低8位与其生成system V IPC key

ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。

注意:

  1. 使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
  2. 需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。

注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于fd和FILE*之间的的关系。

测试用例:创建共享内存
int main()
{
    //PATHNAME定义在头文件:标识着当前文件的路径
	//PROJ_ID 这里 #define PROJ_ID 0x6666
    key_t k = ftok(PATHNAME, PROJ_ID);
    if (k < 0)
    {
        printf("ftok error!\n");
        return 1;
    }
    printf("%x\n", k);
    //需要全新的共享内存
    int shm = shmget(k, SIZE, IPC_CREAT | IPC_EXCL);
    if (shm < 0)
    {
        perror("shmget");
        return 2;
    }
    return 0;
}

下面我们查看一下我们有没有开辟共享内存成功

共享内存的释放

通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。

这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。

此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。

#####系统指令如何删除共享内存?

ipcrm -m [shmid]

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第20张图片

程序指令增删改共享内存,利用shmctl
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmctl函数的参数说明:

第一个参数shmid,表示所控制共享内存的用户级标识符。
第二个参数cmd,表示具体的控制动作。
第三个参数buf,用于获取或设置所控制共享内存的数据结构。

shmctl函数的返回值说明:

shmctl调用成功,返回0。
shmctl调用失败,返回-1。

其中,作为shmctl函数的第二个参数传入的常用的选项有以下三个:

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第21张图片

在以下代码当中,共享内存被创建,两秒后程序自动移除共享内存,再过两秒程序就会自动退出。

#include 
#include 
#include 
#include 
#include 

#define PATHNAME "/home/allen/server.cpp" //路径名

#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

int main()
{
	key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
	if (key < 0){
		perror("ftok");
		return 1;
	}
	int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
	if (shm < 0){
		perror("shmget");
		return 2;
	}
	printf("key: %x\n", key); //打印key值
	printf("shm: %d\n", shm); //打印句柄

	sleep(2);
	shmctl(shm, IPC_RMID, NULL); //释放共享内存
	sleep(2);
	return 0;
}

共享内存的关联

shmat(attach)
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);

shmat函数的参数说明:

第一个参数shmid,表示待关联共享内存的用户级标识符。
第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
第三个参数shmflg,表示关联共享内存时设置的某些属性。

shmat函数的返回值说明:

shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
shmat调用失败,返回(void*)-1。

其中,作为shmat函数的第三个参数传入的常用的选项有以下三个:

选项 作用
SHM_RDONLY 关联共享内存后只进行读取操作
SHM_RND 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA)
0 默认为读写权限

该函数的重点是理解返回值,和理解挂接操作

还是在上面的创建的基础上,我们进行挂接操作

 char *mem = shmat(shmid, NULL, 0);

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第22张图片

指针shmaddr如果为NULL,则由系统attatch到第一个可用的地址上;若指针shmaddr不为NULL且指定SHM_RND,则此段链接到shmaddr所指的地址上;如果shmaddr非零且指定SHM_RND,则此段链接到shmaddr - (shmaddr mod SHMLBA)所表示的地址上。

shmaddr

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

测试用例
#include 
#include 
#include 
#include 
#include 

#define PATHNAME "/home/allen/server.cpp" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

int main()
{
	key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
	if (key < 0){
		perror("ftok");
		return 1;
	}
	int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
	if (shm < 0){
		perror("shmget");
		return 2;
	}
	printf("key: %x\n", key); //打印key值
	printf("shm: %d\n", shm); //打印句柄

	printf("attach begin!\n");
	sleep(2);
	char* mem = shmat(shm, NULL, 0); //关联共享内存
	if (mem == (void*)-1){
		perror("shmat");
		return 1;
	}
	printf("attach end!\n");
	sleep(2);
	
	shmctl(shm, IPC_RMID, NULL); //释放共享内存
	return 0;
}

代码运行后发现关联失败,主要原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此server进程没有权限关联该共享内存。

我们应该在使用shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。

int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存

共享内存的去关联

取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:

功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);

shmdt函数的参数说明:

  • 待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。

shmdt函数的返回值说明:

  • shmdt调用成功,返回0。
  • shmdt调用失败,返回-1。

在关联完之后我们需要去关联,还是上面的例子,使用起来还是比较方便的

    shmdt(mem);
使用共享地址通信

在熟悉了创建,释放,挂接等操作之后,基本就是一个完整的共享内存操作了,那么接下来我们要去使用共享内存,使用共享内存的方式就是通过在挂接之后,取消挂接之前的位置里面,同过挂接返回获得的虚拟地址来操作,访问内存,就可以了

我们有两个端,一个server一个client,现在有了共享内存,我们如何来让他们使用同一个共享内存呢?

只要我们保证了PATHNAME和PROJ_ID,我们就可以获得同一块共享内存

管道和共享内存的使用

当我们使用管道的时候,我们常常会使用write,read之类的系统接口,类似的数据我们需要拷贝两次,先拷给操作系统,然后才是进程B,而且还需要刷新缓冲区,注意还是单向的

而现在我们使用共享内存的时候实际上我们没有使用OS接口,甚至一定意义上我们可以理解为我们使用的都是用户层的操作,而且我们的写入可以直接往共享内存中写,这说明我们可以直接节省拷贝,因此说共享内存的效率是最高的,同时面对同一个共享内存的话,两端是可以同时读写的

共享内存的特点2(缺点)

管道提供了互斥和同步操作,但是共享内存是没有的,这是一个缺点,共享内存存在严重的干扰,需要锁机制来保证实现一致

fork创建子进程是不是子进程页拿到了共享内存?

好像还是只能拷贝一份代码,获取,挂接,通过接口获取

测试用例

/
client.c
/
int main()
{
    key_t k = ftok(PATHNAME, PROJ_ID);
    if (k < 0)
    {
        perror("ftok");
        return 1;
    }

    printf("%x\n", k);

    int shmid = shmget(k, SIZE, IPC_CREAT);
    if (shmid < 0)
    {
        perror("shmget");
        return 2;
    }
    char *mem = shmat(shmid, NULL, 0);

    // TODO
    int i = 0;
    while (1)
    {
        mem[i] = 'A' + i;
        sleep(1);
        i++;
        mem[i] = '\0';
    }
    shmdt(mem);

    return 0;
}

/
server.c
/
//server.c
#include "comm.h"

int main()
{
	key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
	if (key < 0){
		perror("ftok");
		return 1;
	}

	int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存
	if (shm < 0){
		perror("shmget");
		return 2;
	}
	
	printf("key: %x\n", key); //打印key值
	printf("shm: %d\n", shm); //打印共享内存用户层id

	char* mem = shmat(shm, NULL, 0); //关联共享内存

	while (1){
		//不进行操作
	}

	shmdt(mem); //共享内存去关联

	shmctl(shm, IPC_RMID, NULL); //释放共享内存
	return 0;
}

为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一种key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行挂接。这里我们可以将这些需要共用的信息放入一个头文件当中,服务端和客户端共用这个头文件即可。

//comm.h
#include 
#include 
#include 
#include 
#include 
#include 

#define PATHNAME "/home/allen/server.c" //路径名

#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,之后进入死循环,便于观察服务端是否挂接成功。

客户端只需要直接和服务端创建的共享内存进行关联即可,之后也进入死循环,便于观察客户端是否挂接成功。

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第23张图片

共享内存数据结构

shmid_ds结构体

当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。

可以看到上面共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:

/* One shmid data structure for each shared memory segment in the system. */
struct shmid_ds {
    struct ipc_perm shm_perm;        /* operation perms */   系统权限
    int     shm_segsz;               /* size of segment (bytes) */
    time_t  shm_atime;               /* last attach time */
    time_t  shm_dtime;               /* last detach time */
    time_t  shm_ctime;               /* last change time */
    unsigned short  shm_cpid;        /* pid of creator */
    unsigned short  shm_lpid;        /* pid of last operator */
    short   shm_nattch;              /* no. of current attaches */

    /* the following are private */

    unsigned short   shm_npages;     /* size of segment (pages) */
    unsigned long   *shm_pages;      /* array of ptrs to frames -> SHMMAX */ 
    struct vm_area_struct *attaches; /* descriptors for attaches */
};
//ipc_perm 结构定义于中,原型如下:
struct ipc_perm
{
key_t        key;                        调用shmget()时给出的关键字
uid_t           uid;                      /*共享内存所有者的有效用户ID */
gid_t          gid;                       /* 共享内存所有者所属组的有效组ID*/
uid_t          cuid;                    /* 共享内存创建 者的有效用户ID*/
gid_t         cgid;                   /* 共享内存创建者所属组的有效组ID*/
unsigned short   mode;    /* Permissions + SHM_DEST和SHM_LOCKED标志*/
unsignedshort    seq;          /* 序列号*/
};

为什么我们删除共享内存的时候推荐使用shmid而不是key?

在内核层面上,多个进程之间,区分共享内存的唯一性方式就是key

而在用户层面,在进程内部,区分一个IPC资源的使用shmid

重要的查看共享内存的命令

Linux当中,我们可以使用ipcs命令查看有关进程间通信设施的信息。

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第24张图片

ipcs -m

运行server.c得到

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第25张图片

单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:

  • -q:列出消息队列相关信息。
  • -m:列出共享内存相关信息。
  • -s:列出信号量相关信息。

此时,根据ipcs命令的查看结果和我们的输出结果可以确认,共享内存已经创建成功了。

ipcs命令输出的每列信息的含义如下:

标题 含义
key 系统区别各个共享内存的唯一标识
shmid 共享内存的用户层id(句柄)
owner 共享内存的拥有者
perms 共享内存的权限
bytes 共享内存的大小
nattch 关联共享内存的进程数
status 共享内存的状态

共享内存V.S.管道

当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。

使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:

  1. 服务端将信息从输入文件复制到服务端的临时缓冲区中。
  2. 将服务端临时缓冲区的信息复制到管道中。
  3. 客户端将信息从管道复制到客户端的缓冲区中。
  4. 将客户端临时缓冲区的信息复制到输出文件中。

使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:

  1. 从输入文件到共享内存。
  2. 从共享内存到输出文件。

所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。

但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。

共享内存和管道一样是会自动释放吗?

我们发现虽然我们的server.c会退出,但是共享内存还一直在,而管道确实会随着进程退出而消失,这说明共享内存的生命周期是内核的,所以说进程不主动删除或者用命令删除的话,共享内存一直存在直到关机重启

System V 消息队列

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第26张图片

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法

  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值

  • IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

借助消息队列,系统的不同部分可相互通信并异步执行处理操作。消息队列提供一个临时存储消息的轻量级缓冲区,以及允许软件组件连接到队列以发送和接收消息的终端节点。这些消息通常较小,可以是请求、恢复、错误消息或明文信息等。要发送消息时,一个名为“创建器”的组件会将消息添加到队列。消息将存储在队列中,直至名为“处理器”的另一组件检索该消息并执行相关操作。

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第27张图片

消息队列在实际应用中包括如下四个场景:

  • 应用耦合:多应用间通过消息队列对同一消息进行处理,避免调用接口失败导致整个过程失败;
  • 异步处理:多应用对消息队列中同一消息进行处理,应用间并发处理消息,相比串行处理,减少处理时间;
  • 限流削峰:广泛应用于秒杀或抢购活动中,避免流量过大导致应用系统挂掉的情况;
  • 消息驱动的系统:系统分为消息队列、消息生产者、消息消费者,生产者负责产生消息,消费者(可能有多个)负责对消息进行处理;

消息队列数据结构

struct msqid_ds {
	struct ipc_perm msg_perm;
	struct msg *msg_first;      /* first message on queue,unused  */
	struct msg *msg_last;       /* last message in queue,unused */
	__kernel_time_t msg_stime;  /* last msgsnd time */
	__kernel_time_t msg_rtime;  /* last msgrcv time */
	__kernel_time_t msg_ctime;  /* last change time */
	unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */
	unsigned long  msg_lqbytes; /* ditto */
	unsigned short msg_cbytes;  /* current number of bytes on queue */
	unsigned short msg_qnum;    /* number of messages in queue */
	unsigned short msg_qbytes;  /* max number of bytes on queue */
	__kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */
	__kernel_ipc_pid_t msg_lrpid;   /* last receive pid */
};

可以看到消息队列数据结构的第一个成员是msg_perm,它和shm_perm是同一个类型的结构体变量,ipc_perm结构体的定义如下:

struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

msgget

#define _XOPEN_SOURCE
#include 

int msgget(key_t key, int msgflg);

Description

The msgget() function returns the message queue identifier associated with the argument key.

A message queue identifier, associated message queue and data structure (see ) are created for the argument key if one of the following is true:

  • The argument key is equal to IPC_PRIVATE
  • The argument key does not already have a message queue identifier associated with it, and the flag IPC_CREAT is on in msgflg.

Returned value

If successful, msgget() returns a nonnegative integer, namely a message queue identifier.

If unsuccessful, msgget() returns -1 and sets errno to one of the following values:Error Code

When msgflg equals 0, the following applies:

  • If a message queue identifier has already been created with key earlier, and the calling process of this msgget() has read and/or write permissions to it, then msgget() returns the associated message queue identifier.
  • If a message queue identifier has already been created with key earlier, and the calling process of this msgget() does not have read and/or write permissions to it, then msgget() returns-1 and sets errno to EACCES.
  • If a message queue identifier has not been created with key earlier, then msgget() returns -1 and sets errno to ENOENT.

msgctl

#define _XOPEN_SOURCE
#include 

int  msgctl(int msgid, int cmd, struct msqid_ds *buf);

Description

The msgctl() function provides message control operations as specified by cmd.

Returned value

If successful, msgctl() returns 0.

If unsuccessful, msgctl() returns -1 and sets errno to one of the following values:Error Code

msgsnd

#define _XOPEN_SOURCE
#include 

int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);

Description

The msgsnd() function is used to send a message to the queue associated with the message queue identifier specified by msgid.

The argument msgp points to a user-defined buffer that must contain first a field of type long int that will specify the type of the message, and then a data portion that will hold the data bytes of the message. The structure below is an example of what this user-defined buffer should look like:

struct message 
{
    long int   mtype;    Message type
    int        mtext[n];  Message text
}

Returned value

If successful, msgsnd() returns 0.

If unsuccessful, no message is sent, msgsnd() returns -1, and sets errno to one of the following values:Error Code

msgrcv

#define _XOPEN_SOURCE
#include 

int msgrcv(int msgid, void *msgp, size_t msgsz, long int msgtyp, int msgflg);
#define _XOPEN_SOURCE 500
#include 

ssize_t msgrcv(int msgid, void *msgp, size_t msgsz, long int msgtyp, int msgflg);

Description

The msgrcv() function reads a message from the queue associated with the message queue identifier specified by msgid and places it in the user-defined buffer pointed to by msgp.

The argument msgp points to a user-defined buffer that must contain first a field of type long int that will specify the type of the message, and then a data portion that will hold the data bytes of the message. The structure below is an example of what this user-defined buffer should look like:

struct message
{
     long int   mtype;    Message type
     int        mtext[n]; Message text
}

Returned value

If successful, msgrcv() returns a value equal to the number of bytes actually placed into the mtext field of the user-defined buffer pointed to by msgp. A value of zero indicates that only the mtype field was received from the message queue.

If unsuccessful, msgrcv() returns -1 and sets errno to one of the following values: Error Code

msqid_ds

struct msqid_ds
  {
    struct msqid_ds {
    struct ipc_perm msg_perm;
    struct msg *msg_first;      /* first message on queue,unused  */
    struct msg *msg_last;       /* last message in queue,unused */
    __kernel_time_t msg_stime;  /* last msgsnd time */
    __kernel_time_t msg_rtime;  /* last msgrcv time */
    __kernel_time_t msg_ctime;  /* last change time */
    unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */
    unsigned long  msg_lqbytes; /* ditto */
    unsigned short msg_cbytes;  /* current number of bytes on queue */
    unsigned short msg_qnum;    /* number of messages in queue */
    unsigned short msg_qbytes;  /* max number of bytes on queue */
    __kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */
    __kernel_ipc_pid_t msg_lrpid;   /* last receive pid */
};

system V信号量

信号量的使用成本是最高的,接口太复杂,多线程接口更合适,后期专门使用和细讲POSIX的信号量

semid_ds

semget

#define _XOPEN_SOURCE
#include 

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

Description

The semget() function returns the semaphore identifier associated with key.

A semaphore identifier is created with a semid_ds data structure, see , associated with nsems semaphores when any of the following is true:

  • Argument key has a value of IPC_PRIVATE
  • Argument key is not associated with a semaphore ID and (semflg & IPC_CREAT) is non zero.

When a semaphore set associated with argument key already exists, setting IPC_EXCL and IPC_CREAT in argument semflg will force semget() to fail.

When a semid_ds data structure is created the following anonymous data structure is created for each semaphore in the set:

unsigned short int semval Semaphore value
pid_t sempid Process ID of last operation
unsigned sort int semcnt Number of processes waiting for semval to become greater than current value
unsigned short int semzcnt Number of processes waiting for semval to become zero

Returned value

If successful, semget() returns a nonnegative semaphore identifier.

If unsuccessful, semget() returns -1 and sets errno to one of the following values:Error Code

进程互斥

进程间通信通过共享资源来实现,这虽然解决了通信的问题,但是也引入了新的问题,那就是通信进程间共用的临界资源,若是不对临界资源进行保护,就可能产生各个进程从临界资源获取的数据不一致等问题。

保护临界资源的本质是保护临界区,我们把进程代码中访问临界资源的代码称之为临界区,信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量。

信号量本质是一个计数器,在二元信号量中,信号量的个数为1(相当于将临界资源看成一整块),二元信号量本质解决了临界资源的互斥问题,以下面的伪代码进行解释:

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第28张图片

根据以上代码,当进程A申请访问共享内存资源时,如果此时sem为1(sem代表当前信号量个数),则进程A申请资源成功,此时需要将sem减减,然后进程A就可以对共享内存进行一系列操作,但是在进程A在访问共享内存时,若是进程B申请访问该共享内存资源,此时sem就为0了,那么这时进程B会被挂起,直到进程A访问共享内存结束后将sem加加,此时才会将进程B唤起,然后进程B再对该共享内存进行访问操作。

在这种情况下,无论什么时候都只会有一个进程在对同一份共享内存进行访问操作,也就解决了临界资源的互斥问题。

实际上,代码中计数器sem减减的操作就叫做P操作,而计数器加加的操作就叫做V操作,P操作就是申请信号量,而V操作就是释放信号量。

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第29张图片

在进程中涉及到互斥资源的程序段叫临界区

[Linxu-进程间通信] 匿名管道&命名管道&共享内存&消息队列&信号量_第30张图片

你可能感兴趣的:(请回答Linux,linux,共享内存,SystemV,管道,c++)