【Linux】进程间通信(二)

目录

  • [三、system V共享内存](#三、system V共享内存)
    • [3.1 共享内存的原理](#3.1 共享内存的原理)
    • [3.2 共享内存函数](#3.2 共享内存函数)
      • [3.2.1 ftok函数](#3.2.1 ftok函数)
      • [3.2.2 shmget函数](#3.2.2 shmget函数)
      • [3.2.3 shmat函数](#3.2.3 shmat函数)
      • [3.2.4 shmdt函数](#3.2.4 shmdt函数)
      • [3.2.5 shmctl函数](#3.2.5 shmctl函数)
    • [3.3 共享内存的使用](#3.3 共享内存的使用)
      • [3.3.1 ftok函数与shmget函数的使用](#3.3.1 ftok函数与shmget函数的使用)
      • [3.3.2 shmat函数的使用](#3.3.2 shmat函数的使用)
      • [3.3.3 shmdt函数的使用](#3.3.3 shmdt函数的使用)
      • [3.3.4 shmctl函数的使用](#3.3.4 shmctl函数的使用)
        • [3.3.4.1 删除共享内存](#3.3.4.1 删除共享内存)
        • [3.3.4.2 获取共享内存中的属性](#3.3.4.2 获取共享内存中的属性)
      • [3.3.5 进行通信](#3.3.5 进行通信)
    • [3.4 共享内存总结](#3.4 共享内存总结)
  • [四、system V消息队列(略讲)](#四、system V消息队列(略讲))
    • [4.1 消息队列的原理](#4.1 消息队列的原理)
    • [4.2 消息队列函数](#4.2 消息队列函数)
      • [4.2.1 msgget 函数](#4.2.1 msgget 函数)
      • [4.2.2 msgsnd函数](#4.2.2 msgsnd函数)
      • [4.2.3 msgrcv函数](#4.2.3 msgrcv函数)
      • [4.2.4 msgctl函数](#4.2.4 msgctl函数)
  • [五、system V信号量](#五、system V信号量)
    • [5.1 预备知识](#5.1 预备知识)
    • [5.2 信号量的原理](#5.2 信号量的原理)
    • [5.2 信号量函数](#5.2 信号量函数)
      • [5.3.1 semget函数](#5.3.1 semget函数)
      • [5.3.2 semctl函数](#5.3.2 semctl函数)
      • [5.3.3 semop函数](#5.3.3 semop函数)
  • 六、内核是如何看待IPC资源的
  • 结尾

前面那篇文章讲述了进程间通信的目的、理解、发展及分类和管道相关的知识,本篇文章将讲述system V共享内存、消息队列、信号量以及内核是如何看待IPC资源的。进程间通信(一)


三、system V共享内存

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

3.1 共享内存的原理

进程间通信的前提是让不同的进程看到同一份资源,这份资源必须由操作系统提供。

一个进程想要进行进程间通信,首先先在物理内存中申请一段内存,然后在进程的进程地址空间中的共享区中找一段区间,然后通过页表将物理内存与进程地址空间映射起来,最后再将共享区中找到的区间的启示地址返回给上层,这时候这个进程就看到了一块系统级别的内存资源。然后第二个进程想跟第一个进程进行进程间通信,就在它自己的进程的进程地址空间中的共享区中找一段区间,然后通过页表将物理内存与进程地址空间映射起来。这样两个毫不相关的进程就看到了同一份资源。

那么如果我们想删除共享内存呢?首先我们就需要将进程中与共享内存相关的地址在页表中删除,那么共享区中与共享内存相关的地址就全部失效,这里也可以帮助我们理解一下之前的知识,我们知道使用malloc、new等申请空间的时候是在进程地址空间中申请的,实际上申请空间的时候是在页表上申请的,只要在页表中将虚拟地址进行初始化,然后把整个虚拟地址的起始地址返回给上层,那么对应进程地址空间也被开辟好了。然后将共享内存在内存中释放,就完成了共享内存的删除。

理解

操作系统需要对共享内存进行管理:

操作系统中有一对进程在进行进程间通信,那么也有可能有多对进程想进行进程间通信,那么操作系统中一定运行多个共享内存被创建,并且多个共享内存可以同时存在,那么这个每个共享内存的使用情况操作系统需要知道吧,所以操作系统需要对共享内存进行管理,也就是先描述再组织,当创建共享内存的时候,操作系统会为其创建对应的结构体对象,操作系统还可以创建很多共享内存,每个共享内存都为其创建对应的结构体,再将所有的结构体对象以链表的形式组织起来,那么操作系统对共享内存的管理就转化为了对链表的管理了。

由于操作系统中有多个共享内存,那么当一对进程需要进程间通信时,它们如何保证它们指向的是同一个共享内存的呢?那么就需要要求共享内存具有唯一标识,这就由延伸出两个问题:

  1. 这个标识怎么来 (用户自己生成传给共享内存)
  2. 怎么将标识交给另一个进程(通过约定的方式)

这两个问题的详细讲解和更多的原理会在使用中讲到。


3.2 共享内存函数

3.2.1 ftok函数

cpp 复制代码
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

功能:ftok函数是Linux系统中用于生成唯一键值(key)的函数

参数

  • pathname:指向系统中的一个现有文件或目录的路径名。这个文件或目录必须是有效的,为了项目的可读性,通常这个文件或目录是与项目是有关系的。
  • proj_id:项目标识符,通常为一个字符或整数。

返回值

  • 成功时,ftok函数返回一个key_t类型的唯一键值。
  • 失败时,返回-1,并设置errno来指示错误。

3.2.2 shmget函数

cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>	
int shmget(key_t key, size_t size, int shmflg);

功能:用于创建/获取共享内存

参数

  • key : 这是一个用于标识共享内存段的键,你可以给他任意值,但是可能会导致它更容易与其他的key冲突,所以这个键规定使用 ftok 函数生成的。
  • size:指定共享内存的大小,建议是4096的倍数,即使你给的大小不是4096的倍数,他在底层也会以4096的倍数进行创建,但是它只会返回你所需要的大小,多出来的部分你也不能使用。
  • shmflg :是一组标志位,通常是以下值的组合:
    • IPC_CREAT: 如果共享内存段不存在,则创建它,如果存在就直接返回共享内存的起始地址。
    • IPC_EXCL:它不能单独被使用,与 IPC_CREAT 一起使用时,如果共享内存段不存在,则创建共享内存,如果共享内存段已存在,出错返回。
    • 权限标志(如 0666),这些标志设置共享内存段的访问权限,类似于文件的权限设置。

返回值

  • 成功时,shmget 返回一个与共享内存段关联的标识符(shm_id)。
  • 失败时,返回 -1 并设置 errno 以指示错误类型。

3.2.3 shmat函数

cpp 复制代码
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

功能:shmat函数是Linux系统中用于将共享内存区对象映射到调用进程的地址空间的函数。

参数

  • shmid:由shmget函数返回的共享内存标识符。

  • shmaddr:指定共享内存区对象在调用进程地址空间中的连接位置。由于我们对进程地址空间的使用并不熟悉,通常这里使用NULL让操作系统来帮我们选择连接的位置。

  • shmflg:标志参数,用于控制连接行为。

    • 如果指定了SHM_RDONLY标志,则以只读方式连接共享内存。
    • 否则,以读写方式连接共享内存。

返回值

  • 成功时,shmat函数返回共享内存区对象在调用进程地址空间中的实际连接地址。
  • 失败时,返回(void *)-1,并设置errno来指示错误。

3.2.4 shmdt函数

cpp 复制代码
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);

功能:shmdt 函数是 Linux系统中用于断开共享内存段与调用进程的地址空间之间的连接的函数。

参数

  • shmaddr:指向先前由 shmat 函数返回的共享内存区对象在调用进程地址空间中的连接地址。

返回值

  • 成功时,shmdt 函数返回 0。
  • 失败时,返回 -1,并设置 errno 来指示错误。

注意:将共享内存段与当前进程脱离不等于删除共享内存段


3.2.5 shmctl函数

cpp 复制代码
#include <sys/types.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

功能:shmctl 函数是 Linux系统中用于控制共享内存段的一个函数。它允许进程对共享内存段进行各种控制操作,如获取状态、修改权限、删除共享内存段等。

参数

  • shmid:由 shmget 函数返回的共享内存标识符。

  • cmd:指定要执行的控制命令。常见的命令包括:

    • IPC_STAT:获取共享内存段的状态,将共享内存的 shmid_ds 结构复制到 buf 中。
    • IPC_SET:设置共享内存段的某些属性,将 buf 中的信息拷贝到内核空间以设置 IPC 内核对象。
    • IPC_RMID:删除共享内存段。此时,buf 参数被忽略,可以传 NULL。
  • buf:指向 shmid_ds 结构体的指针,用于存储或接收共享内存段的状态信息。当 cmd 为 IPC_STAT 时,buf 用于接收返回值;当 cmd 为 IPC_SET 时,buf 用于传递值并设置内核对象。

返回值

  • 成功时,shmctl 函数返回 0。
  • 失败时,返回 -1,并设置 errno 来指示错误。

3.3 共享内存的使用

进程间通信需要两个进程,我这里创建两个源文件,一个server.cpp代表服务端,client.cpp代表客户端。

3.3.1 ftok函数与shmget函数的使用

shmget函数的参数中有一个参数是key,它是为了创建共享内存时,用来标记共享内存的,为了减少冲突,操作系统中使用了一个特殊的算法来设计了一个ftok函数。

这里我创建一个comm.hpp文件,用于编写服务端和客户端都需要的函数和数据,我们将pathname和proj_id写入到.hpp文件中,使两个源文件都使用.hpp文件,那么两个源文件使用同样的算法,同样的pathname和同样的proj_id,形成的key值一定是相同的,那么两个进程最终就会指向同一个共享内存,这样就完成了进程间通信的前提:不同的进程看到同一份资源。

下面我们就使用ftok函数返回的key作为shmget的参数来创建共享内存,这里我使用共享内存不存在就创建,存在就出错返回的方式来创建,观察下面代码和运行结果,我们发现第一次创建共享内存时,创建成功并输出了shmid的值,我们可以使用ipcs -m的指令来查看操作系统中共享内存的属性,我们发现共享内存确实创建成功了,当我们第二次创建时却发现创建失败了,并且这时候进程也已经退出了,所以这里可以得出一个结论就是:共享内存的生命周期并不跟随进程,而是跟随内核,除非用户使用指令或是函数将共享内存删除。system V下的所有IPC资源的生命周期都跟随内核

上面我们查看共享内存属性的时候,发现有两个标识符:key和shmid。

  • key:不能在应用层使用,它只能用于内核中标记共享内存。
  • shmid:在应用层操作共享内存时使用shmid。

所以我们使用函数或是指令删除时,就需要用到shmid。

这里我先使用指令来删除共享内存ipcrm -m shmid

我们看到共享内存的属性中还有权限和共享内存的大小,这两个属性都是可以在创建的时候给共享内存设置的。这里设置共享内存大小时建议是4096的倍数,即使你给的大小不是4096的倍数,他在底层也会以4096的倍数进行创建,但是它只会返回你所需要的大小,多出来的部分你也不能使用。


3.3.2 shmat函数的使用

shmat函数能够将共享内存区对象映射到调用进程的地址空间中。我们看到共享内存中有一个属性叫做nattch,这个数据就是有多少个进程的地址空间与该共享内存相互映射。

观察下面代码、进程运行结果和查看内存空间属性的脚本结果,首先操作系统中没有共享内存,运行进程后出现了一个共享内存,但是与没有地址空间与之相互映射,它的nattch为0,过了三秒以后,进使用shmat函数将它的地址空间与地址空间相互映射,共享内存的nattch变为了1,再过三秒后进程退出,共享内存的nattch又变回了0。


3.3.3 shmdt函数的使用

shmdt 函数能够断开共享内存段与调用进程的地址空间之间的连接。它的参数就是shmat的返回值。

观察下面代码、进程运行结果和查看内存空间属性的脚本结果,首先操作系统中没有共享内存,运行进程后出现了一个共享内存,但是与没有地址空间与之相互映射,它的nattch为0,过了三秒以后,进程使用shmat函数将它的地址空间与地址空间相互映射,共享内存的nattch变为了1,再过三秒后进程使用shmdt函数断开地址空间与共享内存的映射关系,共享内存的nattch又变回了0,再过三秒后进程退出。


3.3.4 shmctl函数的使用

shmctl函数能够控制共享内存段。它允许进程对共享内存段进行各种控制操作,如获取状态、修改权限、删除共享内存段等。

3.3.4.1 删除共享内存

我这里使用函数的删除功能,观察下面代码、进程运行结果和查看内存空间属性的脚本结果,首先操作系统中没有共享内存,运行进程后出现了一个共享内存,但是与没有地址空间与之相互映射,它的nattch为0,过了三秒以后,进使用shmat函数将它的地址空间与地址空间相互映射,共享内存的nattch变为了1,再过三秒后进程使用shmdt函数断开地址空间与共享内存的映射关系,共享内存的nattch又变回了0,再过三秒删除共享内存,我们确实也没查到刚刚的共享内存,再过三秒后进程退出。


3.3.4.2 获取共享内存中的属性

通过查询shmctl函数,我们可以将shmid_ds对象作为shmctl的参数,当调用完函数后,shmid_ds对象中就存储着部分共享内存的属性,我们还发现shmid_ds结构体中还存储着另一个ipc_perm结构体。我们可以将结构体中的属性打印出来,看看与指令查找出来的共享内存属性是否一致。

通过上面进程的运行结果和指令的运行结果,我们发现shmid_ds对象中确实存储的是共享内存的属性,既然能获取到共享内存的属性,就代表我们上面说的是正确的,操作系统中为共享内存维护了一个结构体对象用来存储共享内存的属性的。共享内存 = 共享内存空间 + 共享内存属性。


3.3.5 进行通信

server端在创建共享内存时,就已经将约定好的key放入到了共享内存中了,client端只需要通过约定的key就可以找到对应的共享内存了。

观察下面代码、进程运行结果和查看内存空间属性的脚本结果,首先操作系统中没有共享内存,运行server端后出现了一个共享内存,但是与没有地址空间与之相互映射,它的nattch为0,然后运行client端,client端使用shmat函数将它的地址空间与地址空间相互映射,共享内存的nattch变为了1,过了几秒以后,server端使用shmat函数将它的地址空间与地址空间相互映射,共享内存的nattch变为了2,再过几秒后client端使用shmdt函数断开地址空间与共享内存的映射关系,共享内存的nattch又变回了1,server端使用shmdt函数断开地址空间与共享内存的映射关系,共享内存的nattch又变回了0,再过几秒后进程退出。


上面所写的这些代码并没有让两个进程进行通信,我们写了这么多代码也只是为两个进程看到同一份资源,所以进程间通信无论是共享内存还是管道,它本质的叫法是:为两个进程通信做准备,让两个进程看到同一份资源

那么这里是如何让两个进程看到同一份资源的呢?

根据两个进程相同且唯一的key看到同一份资源的,而命名管道是通过路径+文件名来使两个进程看到同一份资源的,路径+文件名也具有唯一性,共享内存和命名管道都是通过唯一性使两个进程看到同一份资源的。

为什么key是由用户生成的而不让操作系统生成呢?

假设是由操作系统生成,那么server端进行创建共享内存后返回shmid就可以直接使用了,但是client端想找到对应的共享内存时,操作系统内有很多共享内存,在系统层面上key只有操作系统和创建该共享内存的进程知道,而想要其他进程根本不可能知道它的key,那么client端就不知道对应的共享内存是哪个了。这里让用户生成key的根本原因就是让用户的不同进程看到同一个key。


这里我们将使用server端和client端进行通信。共享内存是支持让两个进程进行同时通信的,它们通信的方式取决于你的代码是如何编写的。

这里我们让client端3秒向共享内存中写入一次,让server端1秒从共享内存中读一次,先运行server端,过几秒在运行client端,观察下面的代码和运行结果,我们发现在client端还没有运行起来的时候,server端就已经开始从共享内存中读取数据了,当运行client端后,client端写入了一次,server端就读取了3次。得出结论:共享内存的通信方式不会提供同步机制,共享内存是直接裸露给使用者的,所以一定要注意共享内存的使用安全。



我们上面学习了管道,管道是提供了同步的机制的,这里我们通过管道来为共享内存添加同步机制。

我们在server端中添加了创建命名管道和向管道中读取的步骤,在client端中添加了向管道中写入的步骤。

运行server端时,创建命名管道,创建命名空间并与地址空间进行映射,在进程想读取共享内存之前,我们添加向管道中读取的操作,只有从管道中读到信息才证明了client端向共享内存中写入了数据,server端才能读取共享内存中的数据。

运行client端时,找到并使自己的进程地址空间与对应的共享内存映射,然后向共享内存中写入数据,当向共享内存中写入完毕后,再向管道中写入数据,代表client端已经写入数据,server端可以从共享内存中读取。

从下面的运行结果来看,确实为共享内存添加了同步机制,但是server端的读取却并没有真正的将数据读走,而是将数据拷贝出去了,如果想实现真正意义上的读走,可以在共享内存的开头添加管理字段,这里就不做讲解了。

cpp 复制代码
// comm.hpp
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <errno.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

const char *pathname = "/home/chineseperson04/CSDN/IPC/shared_memory";
int proj_id = 9;
const char *filename = "fifo";

const int size = 4096;

int GetKey()
{
    int key = ftok(pathname, proj_id);
    if (key == -1)
    {
        cout << "errno:" << errno << " strerror:" << strerror(errno) << endl;
        exit(2);
    }
    return key;
}

string ToHex(int key)
{
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "0x%x", key);
    return buffer;
}

int ShmHelper(int key, int flag)
{
    int shmid = shmget(key, size, flag);
    if (shmid == -1)
    {
        cout << "errno:" << errno << " strerror:" << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(int key)
{
    return ShmHelper(key, IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm(int key)
{
    return ShmHelper(key, IPC_CREAT);
}

// 创建命名管道
bool MakeFifo()
{
    int n = mkfifo(filename, 0666);
    if (n < 0)
    {
        cerr << "errno" << errno << "strerror" << strerror(errno) << endl;
        return false;
    }
    cout << "create fifo success..." << endl;
    return true;
}
cpp 复制代码
// server.cpp
#include <iostream>
#include "comm.hpp"

using namespace std;

int main()
{
    // 创建管道失败则直接退出
    if (!MakeFifo())
        return 1;
    int key = GetKey();
    // cout << "key:" << ToHex(key) << endl;

    int shmid = CreateShm(key);
    // cout << "shmid:" << shmid << endl;
    // sleep(3);

    char *s = (char *)shmat(shmid, nullptr, 0);
    cout << "开始将shm映射到进程地址空间中..." << endl;
    // sleep(3);

    int fd = open(filename, O_RDONLY);

    while (true)
    {
        int code = 0;
        // 如果client端没有写入,这里read就会一直等待
        // 直到client端写入后,向管道中发送提示后再进行读取
        ssize_t n = read(fd, &code, sizeof(code));
        if (n == 4)
        {
            cout << "共享内存中的内容:";
            cout << s << endl;
        }
        // 如果n == 0,代表client端已经关闭,这里也直接退出
        else if (n == 0)
        {
            return 2;
        }
    }

    shmdt(s);
    cout << "开始将shm从进程地址空间中移除..." << endl;
    // sleep(3);

    shmctl(shmid, IPC_RMID, nullptr);
    cout << "开始将shm从操作系统中删除" << endl;
    // sleep(3);

    close(fd);
    // unlink可以删除指定路径下的文件,这里删除管道文件
    unlink(filename);

    return 0;
}
cpp 复制代码
// client.cpp
#include <iostream>
#include "comm.hpp"
#include <sys/ipc.h>

using namespace std;

int main()
{
    int key = GetKey();
    int shmid = GetShm(key);

    char *s = (char *)shmat(shmid, NULL, 0);
    cout << "attach shm done" << endl;
    // sleep(5);

    int fd = open(filename, O_WRONLY);
    char c = 'a';

    for (; c <= 'z'; c++)
    {
        s[c - 'a'] = c;
        cout << "write " << c << " done" << endl;

        // 通知对方
        int code = 0;
        write(fd, &code, sizeof(code));
        sleep(3);
    }

    shmdt(s);
    cout << "detach shm done" << endl;

    close(fd);

    return 0;
}

3.4 共享内存总结

  1. 共享内存的通信方式不会提供同步机制,共享内存是直接裸露给使用者的,所以一定要注意共享内存的使用安全
  2. 共享内存可以提供较大的空间。
  3. 共享内存是所有进程间通信中最快的。

如何理解共享内存是所有进程间通信中最快的呢?

例如共享内存与管道,共享内存相比于管道减少数据进程通信时的拷贝,以下面一个具体的例子为例,从键盘中输入数据,通过A、B进程将数据打印到显示屏中。我们需要知道的一个知识:凡是数据迁移都是拷贝。

通过管道则需要4次拷贝,这还是不算上输入输出函数中缓冲区的结果,将数据拷贝到进程A的用户缓冲区,将进程A用户缓冲区的数据拷贝到管道中,从管道中将数据拷贝到进程B的用户缓冲区中,再将进程B用户缓冲区中是数据拷贝到显示器中。

通过共享内存则需要2次,将数据拷贝到共享内存中,再将共享内存中的数据拷贝到显示器中。

根据这个例子来看共享内存至少减少了两次拷贝。需要注意:减少拷贝的次数,一定要在确定的场景中才能被确定。


四、system V消息队列(略讲)

4.1 消息队列的原理

消息队列与共享内存的原理十分相似,无非就是变为在物理内存中申请一个队列,消息队列的底层是链表结构,消息队列的头部(msg_first)和尾部(msg_last)指针用于维护这个链表结构。使用者必须在每个进程中定义一个结构体,里面必须有一个字段用来使消息队列区分是哪个进程写入的,方便另一个进程进行读取。

操作系统中会有许多消息队列,操作系统就需要对消息队列进行管理,先描述再组织,当创建消息队列的时候,操作系统会为其创建对应的结构体对象,操作系统还可以创建很多消息队列,每个消息队列都为其创建对应的结构体,再将所有的结构体对象以链表的形式组织起来,那么操作系统对消息队列的管理就转化为了对链表的管理了。消息队列=队列+队列属性。通过对消息队列结构体的查看,我们发现消息队列中的第一个属性与共享内存中的第一个属性相同都是一个结构体ipc_perm。


4.2 消息队列函数

4.2.1 msgget 函数

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

功能:msgget 函数用于创建一个新的消息队列或获取一个现有消息队列的标识符。

参数

  • key : 这是一个用于标识消息队列的键,你可以给他任意值,但是可能会导致它更容易与其他的key冲突,所以这个键规定使用 ftok 函数生成的。
  • shmflg :是一组标志位,通常是以下值的组合:
    • IPC_CREAT: 如果消息队列不存在,则创建它,如果存在就直接返回消息队列的起始地址。
    • IPC_EXCL:它不能单独被使用,与 IPC_CREAT 一起使用时,如果消息队列不存在,则创建消息队列,如果消息队列已存在,出错返回。
    • 权限标志(如 0666),这些标志设置消息队列的访问权限,类似于文件的权限设置。

返回值

  • 成功时,shmget 返回一个与消息队列关联的标识符(shm_id)。
  • 失败时,返回 -1 并设置 errno 以指示错误类型。

4.2.2 msgsnd函数

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

功能:用于向消息队列发送消息。

参数

  • msqid:由 msgget 返回的消息队列标识符。
  • msgp :指向要发送的消息的指针。消息结构必须以 long int 类型成员变量开始,用于指定消息类型。
  • msgsz:消息的长度(不包括消息类型成员变量的长度)。
  • msgflg:控制消息队列满或达到系统限制时的行为,可以包含 IPC_NOWAIT 标志。

返回值:成功时返回 0,失败时返回 -1 并设置 errno。


4.2.3 msgrcv函数

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

功能:用于从消息队列接收消息。

参数

  • msqid:由 msgget 返回的消息队列标识符。
  • msgp:指向用于接收消息的缓冲区的指针。
  • msgsz:缓冲区的长度。
  • msgtyp:用于指定接收消息的类型。
  • msgflg:控制消息队列中没有相应类型的消息时的行为,可以包含 IPC_NOWAIT 和 MSG_NOERROR 标志。

返回值

  • 成功时返回接收到的消息长度。
  • 失败时返回 -1 并设置 errno。

4.2.4 msgctl函数

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

功能:用于控制消息队列,如获取状态、设置属性或删除消息队列。

参数

  • msqid:由 msgget 返回的消息队列标识符。
  • cmd:指定要执行的操作,可以是 IPC_STAT、IPC_SET 或 IPC_RMID。
  • buf:指向 msqid_ds 结构的指针,用于存储或设置消息队列的属性。

返回值

  • 成功时返回 0。
  • 失败时返回 -1 并设置 errno。

五、system V信号量

5.1 预备知识

我们知道进程间通信的前提是不同的执行流(进程/线程)看到同一份资源,这份资源我们称之为公共资源,若该公共资源被多个执行流同时写入或同时读取,会导致数据不一致的问题,所以我们需要将这个公共资源保护起来,可以通过同步和互斥的方式将公共资源保护起来。保护资源无法就是用户和操作系统进行保护,操作系统保护公共资源,例如:匿名/命名管道和消息队列,用户保护公共资源,例如:共享内存。

  • 公共资源:多个执行流看到的同一份资源。
  • 同步:多个执行流执行时,按照一定的顺序执行。
  • 互斥:任何一个时刻只允许一个执行流访问资源。
  • 临界资源:被保护起来的公共资源,反之则是非临界资源
  • 临界区: 访问该临界资源的代码,反之则是非临界区。
  • 原子性:代表状态只有两种,要么做完,要么没做,它是没有中间态的

我们常说的维护临界资源,其实就是在维护临界区。


5.2 信号量的原理

这里我以一个生活中的例子开始讲解信号量。

不知道大家有没有去电影院看过电影,在电影院中每一场电影都有很多的座位,这个座位是否是你的取决于你买到了这个位置的票还是你坐在了这个座位上?在现实中,只要你买到票了这个座位就属于你了,除非有没素质的人强制坐了你的位置,这时候你去找管理人员,这个位置依旧是你的。电影院和内部的座位就是多个人共享的资源,在现实生活中我们称之为公共资源,我们刚刚讲过只要买到了票,这个位置就属于你,即使你没有去,它也需要将这个位置为你保留下来。我们买票的本质就是对资源的预定机制。假设电影院中有100个座位,那么它就不可能卖出101张票,通过一个计数器就可以做到,每卖出去一张票计数器就减一,当计算器为0后就代表票卖完了。这里的计数器就代表了公共资源的个数,一个公共资源(电影院)可以被拆分成为多分资源(座位)去使用。这种用来衡量公共资源数目,用来达到对公共资源进行分配目的的计数器我们称之为信号量。

信号量表示资源数目的计数器,每一个执行流想要访问公共资源中的某一份资源之前,需要先申请信号量资源,就是对信号量计数器进行减减操作。本质上只要减减成功,就完成了资源的预定。如果是申请不成功,这个执行流就要被挂起阻塞。

资源可以申请,同样也可以释放,我们说的维护临界资源,也就是在维护临界区,就是在临界区之前加上申请信号量资源,申请成功信号量计数器减减,代表申请资源成功,在临界区之后让信号量计数器加加,代表资源释放。

如果是公共资源只有一份,我们就可以将信号量计数器设置为1,在未来计数器就只能为0或是1,任何情况下就只有一个执行流可以访问这个资源,这就完成了一个互斥功能,我们将这样只有两态的信号量称之为二元信号量,在未来可以实现互斥锁,完成互斥功能

一个公共资源可以单独使用,那么信号量计数器就为1,一个公共资源也可以被分为n份,那么信号量计数器就是n,这样就可以使最多n个执行流使用公共资源时,只要每个执行流使用的资源是不同份就可以做到并发访问。

细节问题

  1. 申请共享资源之前需要先访问信号量资源,那么每个执行流就必须先看到同一份信号量资源,所以信号量资源就必须由操作系统提供,它也属于IPC体系。所以进程间通信并不只是传输数据为目的,让不同进程看到同一个计数器达到协同也是进程间通信的目的。
  2. 创建信号量的目的就是为了保护公共资源,可是信号量本质上就是公共资源,由于信号量访问方式并不复杂,只有加加和减减操作,它在内部中实现时,加加减减都是原子的,所以多个执行流在申请信号量资源时,并不会出现问题,我们将申请信号量(原子的减减)称之为P操作,释放信号量(原子的加加)称之为V操作。
  3. 如何理解申请信号量失败后,进程被阻塞挂起呢?我们可以简单的理解为单个信号量结构体中有一个计数器和等待队列(task_struct* queue),申请失败后就将进程从运行队列中放到等待队列中,申请成功后再将进程放回到运行队列中。

操作系统中会有信号量,操作系统就需要对信号量进行管理,先描述再组织,当创建信号量的时候,操作系统会为其创建对应的结构体对象,操作系统还可以创建很多信号量,每个信号量都为其创建对应的结构体,再将所有的结构体对象以链表的形式组织起来,那么操作系统对信号量的管理就转化为了对链表的管理了。通过信号量属性的查看,我们发现信号量的第一个成员也是一个ipc_perm结构体对象。

更多信号量的知识会在后面的多线程中讲到。


5.2 信号量函数

5.3.1 semget函数

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

功能: 创建一个新的信号量集或访问一个已存在的信号量集。

参数

  • key : 这是一个用于标识信号量集的键,你可以给他任意值,但是可能会导致它更容易与其他的key冲突,所以这个键规定使用 ftok 函数生成的。
  • nsems: 信号量集中信号量的数量。
  • shmflg :是一组标志位,通常是以下值的组合:
    • IPC_CREAT: 如果信号量集不存在,则创建它,如果存在就直接返回信号量集的起始地址。
    • IPC_EXCL:它不能单独被使用,与 IPC_CREAT 一起使用时,如果信号量集不存在,则创建信号量集,如果信号量集已存在,出错返回。
    • 权限标志(如 0666),这些标志设置信号量集的访问权限,类似于文件的权限设置。

返回值:

  • 成功时返回信号量集的标识符(semid)。
  • 失败时返回-1并设置errno。

5.3.2 semctl函数

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);

功能: 对信号量集执行各种控制操作,如初始化信号量、获取信号量值、设置信号量值等。

参数:

  • semid: 信号量集的标识符。
  • semnum: 信号量集中要操作的信号量的索引。
  • cmd: 要执行的控制操作。
  • ...: 根据cmd的不同,可能需要额外的参数。

返回值:根据cmd的不同,返回值可能不同。通常,成功时返回0或正数,失败时返回-1并设置errno。


5.3.3 semop函数

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);

功能: 对信号量集中的信号量进行P(等待)和V(信号)操作。

参数:

  • semid: 信号量集的标识符。
  • sops: 指向sembuf结构数组的指针,每个结构指定一个信号量及其要执行的操作。
  • nsops: sops数组中的元素数量。

sembuf结构通常定义如下:

cpp 复制代码
struct sembuf {
    unsigned short sem_num;  // 信号量在信号量集中的索引
    short sem_op;           // 要执行的操作(正数表示V操作,负数表示P操作,0表示获取信号量的当前值)
    short sem_flg;          // 操作标志(通常为0或IPC_NOWAIT)
};

返回值:

  • 成功时返回0。
  • 失败时返回-1并设置errno。

六、内核是如何看待IPC资源的

system V版本下所有描述IPC资源的结构体都叫做xxxid_ds,并且他们的第一个成员都是结构体ipc_perm,这些结构都是用户级别的,也是操作系统给用户暴露出来的属性。

内核是如何看待IPC资源的:

  1. IPC资源是被单独设计出来的
  2. 内核中维护IPC资源的方式

首先操作系统层面能够找到一个结构体ipc_lds,这个结构体中有一个成员entries指向结构体ipc_id_array,结构体ipc_id_array有一个成员记录数组元素,还有一个变长数组,数组中存储的都是korn_ipc_perm结构体的指针。在描述共享内存、消息队列和信号量的结构体中,它们的第一个成员都是korn_ipc_perm结构体,我们知道结构体中第一个成员的地址就是结构体的地址 ,ipc_id_array结构体中存储着所以IPC资源的结构体中的第一个成员的地址,那么操作系统就可以根据这些地址找到所以的IPC资源的结构体,所以操作系统可以通过该数组管理所有的IPC资源,虽然数组中指针只有资格访问IPC资源的结构体中前面一部分的数据,但是操作系统知道指针指向的是哪一种IPC资源,通过对类型的强制类型转换,就可以访问到IPC资源的结构体中所有的数据了。

通过上面的解释,我们发现这种方式很像多态,kern_ipc_perm就是基类,其他IPC资源的结构体都是子类,初始化子类对象时,会先将基类部分先初始化。


结尾

如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。

希望大家以后也能和我一起进步!!🌹🌹

如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹

相关推荐
帅大大的架构之路6 分钟前
Could not resolve host: mirrorlist.centos.org
linux·运维·centos
xuehaowang7 分钟前
Ubuntu20.04中EasyConnect启动报错
linux·运维·服务器
AI+程序员在路上31 分钟前
OpenCV轮廓相关操作API (C++)
c++·人工智能·opencv
ZPC821039 分钟前
MoveItConfigsBuilder 配置机器人的完整示例
c++·人工智能·机器人
凉秋girl1 小时前
JVM vs JDK vs JRE
linux·运维·服务器
m0_725958991 小时前
day36 C++对C的扩充
c++
麻瓜老宋1 小时前
【手搓一个脚本语言】七、用C语言抽象语法树AST实现一个可交互运行的表达式计算器
c语言·开发语言·数据结构·算法
侬本多情。1 小时前
复杂园区网基本分支的构建
运维·服务器·网络
一丝晨光1 小时前
如何很快将文件转换成另外一种编码格式?编码?按指定编码格式编译?如何检测文件编码格式?Java .class文件编码和JVM运行期内存编码?
java·c++·python·visual studio·unicode·ansi·utf8
pumpkin845141 小时前
Windows上使用VSCode开发linux C++程序
linux·windows·vscode