【Linux】进程间通信上 (1.5万字详解)

目录

一.进程间通信介绍

1.1进程间通信的目的

 1.2初步认识进程间通信

1.3进程间通信的种类

二.匿名管道

2.1何为管道

 2.1实现原理

2.3进一步探寻匿名管道

2.4编码实现匿名管道通信

2.5管道读写特点

 2.6基于管道的进程池设计

 三.命名管道

3.1实现原理

3.2代码实现

 四.共享内存

4.1共享内存的原理

4.2接口介绍

4.3命令行操作共享内存

4.4代码实现


hello,大家好呀。今天我们来学习关于进程间通信的内容,我们知道操作系统中会同时存在多个进程,这些进程有可能会共同完成一个任务,所以就需要通信。本文将讲解几种常见的进程间通信的方式。相信大家已经迫不及待的想要学习了,那我们就开始啦!

本节重点:进程间通信介绍,管道,消息队列,共享内存,信号量

一.进程间通信介绍

1.1进程间通信的目的

  • 数据传输:数据一个进程需要将它的数据发送给另一个进程数据传输:
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另个进程的所有陷入和异常,并能够及时知道它的状态改变

其中,重要的目的是:数据传输,通知事件,进程控制。

 1.2初步认识进程间通信

在没有正式接触通信之前,我们就应该知道:进程具有独立性,今天我们需要完成进程间相互通信,成本一定不低。

进程间通信的场景应该是这样的:一个进程将数据放入一块固定的区域中,另外一个进程从这块区域中读取数据。所以这块区域对通信双方来说,应该是一个公共资源。所以这块区域一定不能让两个进程提供(无论是哪个进程提供,都不会让另一个进程看到,因为进程具有独立性);这块区域只能由操作系统提供

所以,通信的本质是什么?

  • 操作系统必须直接或者间接的为通信双方提供可以交换数据的"内存空间"。
  • 要通信的进程,必须看到一份公共资源。 
  • 不同的通信种类:本质就是上面的公共资源是操作系统的哪一个模块提供的。

总结来说,要完成通信,必须做好两件事情:

  1.  让通信进程双方看到同一份资源(我们其实学的就是这部分内容)。
  2. 通信。

1.3进程间通信的种类

 根据操作系统给我们提供的公共资源属于操作系统中的哪一部分,前辈大佬们设计出了不同的通信方式。

管道通信:由文件系统提供

  •  匿名管道pipe
  •  命名管道

System V IPC:聚焦在本机通信

  •  System V 消息队列
  •  System V 共享内存
  •  System V 信号量

POSIX IPC:实现跨主机通信

  •  消息队列
  •  共享内存
  •  信号量
  •  互斥量
  •  条件变量
  •  读写锁

二.匿名管道

2.1何为管道

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

 2.1实现原理

匿名管道是基于文件系统来实现的。

我们在学习文件系统时学到:一个进程会默认打开3个文件描述符,0号指向标准输入流,1号指向标准输出流,2号指向标准错误流。我们使用的文件描述符一般从3号开始。

如果对一个文件分别以“r”和“w”的形式打开,操作系统会分别为其分配不同的文件描述符来指向这个文件。一个文件仅有一个缓冲区。所以不管是对该文件进行读还是写操作,数据都会经过该缓冲区。 

我们使用fork函数创建子进程时,操作系统会为子进程拷贝一份父进程的PCB,程序地址空间,列表等等。

但是父子进程会共用一个文件描述符数组吗? 不会,因为父子进程可能会打开不同的文件,为了确保独立性,并让父进程可以操作文件,操作系统会为子进程创建一个独立的文件描述符数组。

如果父子进程同时打开一个文件,这个文件就可以当做父子进程双方的共享资源,如果父子进程想要通信的话,就可以利用该文件进行通信(因为这个文件对父子进程来说都是可见的区域)。父子进程分别以读的方式和写的方式打开这个文件。一个进程向这个文件缓冲区中写入,另一个进程就可以从这个文件缓冲区中读取数据。这就是匿名管道的实现原理。采取匿名管道的方式通信利用的公共资源就是文件。

我们将操作系统提供的这个供进程间通信的文件就做管道文件。

 

:刚刚,我们有提到:管道通信依据的是struct file结构体给文件提供的缓冲区来进行通信。为什么不让缓冲区内的数据刷新到磁盘上,然后再从磁盘中读取数据呢?这种方式可以吗? 

 答:这种方式是可以的,但是没有必要。因为这种通信太慢了。

 管道通信依赖的仅仅是struct file结构体中的内核级的缓冲区。我们知道:一个文件被加载到内存,首先就是要为其创建struct file结构体,那操作系统可不可以为一个根本不存在的文件在内存中创建struct file结构体呢?可以。所以管道文件实际上是一个内存文件,要么这个文件根本不存在,要么即使存在,也不管新它在磁盘中的位置。

问:如何让父子进程看到同一个文件呢?

答:父进程打开文件,然后fork创建子进程,子进程继承文件描述符表,文件描述符中指向同一个文件的struct file结构体地址,所以父子进程看到同一个文件。这种看到同一个文件的方式不需要文件名的参与,所以这个这种管道又被称为匿名管道

2.3进一步探寻匿名管道

总结来说,创建管道的过程是:

  1. 分别让父进程以读和写的方式打开同一个文件。
  2. 用fork创建子进程。
  3. 一般而言,管道传输数据一般都是单向传输。所以根据传输方向的需要,关闭没有用的文件描述符。 

:为什么让父进程分别以读和写的方式打开同一个文件。

:为了满足通信,通信双方会分别以读和写的方式打开同一个文件。父进程分别以读和写的方式打开同一个文件,子进程通过继承也会以读和写的方式打开同一个文件,这样一来,父子进程就可以选择数据传输的方向。

:管道进行数据传输为什么是单项的?

:这种通信方式之所以被命名为管道,是因为它符合现实生活中管道进行单向资源传输的特点。

设计出双向的管道在技术上是可行的,但也以为会更加麻烦,会添加更多的标定信息。如果我们想进行双向传输数据的话,我们可以创建两个管道来解决问题。

2.4编码实现匿名管道通信

目前,匿名管道用来父子进程间通信。 

pipe函数

pipe()函数可用于创建一个管道,以实现进程间的通信。
pipe()函数的定义如下:

#include<unistd.h>

/* @param fd,经参数fd返回的两个文件描述符
 * fd[0]为读而打开,fd[1]为写而打开
 * fd[1]的输出是fd[0]的输入
 * @return 若成功,返回0;若出错,返回-1并设置errno
 */
int pipe(int fd[2]);

pipe函数定义中的fd参数是一个大小为2的数组类型指针。为输出型参数。

通过pipe函数创建的这两个文件描述符fd[0]和fd[1]分别构成管道的两端,往fd[1]写入的数据可以从fd[0]读出,并且fd[1]一端只能进行写操作,fd[0]一端只能进行读操作,不能反过来使用。要实现双向数据传输,可以使用两个管道。

默认情况下,这一对文件描述符都是阻塞的。此时,如果我们用read系统调用来读取一个空的管道,则read将被阻塞,直到管道内有数据可读;如果我们用write系统调用往一个满的管道中写数据,则write也将被阻塞,直到管道内有足够的空闲空间可用(read读取数据后管道中将清除读走的数据)。当然,用户可以将fd[0]和fd[1]设置为非阻塞的。

写一段小的测试代码:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <cassert>
#include<cstring>
int main()
{
    // 创建管道
    int fd[2];
    int n = pipe(fd);
    assert(n == 0);
    // fd[0]为读端
    // fd[1]为写端

    // 创建子进程
    pid_t fds = fork();
    assert(fds >= 0);
    const char *msg = "我是子进程,我正在给你发消息";
    int cnt = 0;
    if (fds == n)
    {
        // 子进程
        // 关闭文件描述符
        // 子进程进行写入
        while (1)
        {
            cnt++;
            close(fd[0]);
            char buffer[1024];
            snprintf(buffer, sizeof buffer, "子进程->父进程:%d[%s]", cnt, msg);
            write(fd[1], buffer, strlen(buffer));
            sleep(1);
        }
        exit(1);
    }
    close(fd[1]);
    char readbuffer[1024];
    while(1)
    {
        read(fd[0],readbuffer,sizeof readbuffer-1);
        std::cout<<readbuffer<<std::endl;
        sleep(1);
    }
  
    int m = waitpid(fds, nullptr, 0);
    assert(m = fds);
}

整个过程是严格按照我们刚刚的步骤进行的,运行一下:

 

整个过程非常流畅。

2.5管道读写特点

情况1:

管道写的快,读的慢。现在我们让子进程一直在写,父进程每隔5秒钟读一次,我们还是使用上面的测试代码:

综合打印结果,我们发现:读端从管道中读取数据时,当管道中数据足够多时, 读端会将缓冲区读满。所以读端就会一次性读取1023个字节的数据。

总结:读端读取数据,如果管道中数据足够多时,读端就会读满设定的缓冲区。如果管道中数据不够填满给读端准备的缓冲区时,读端就会一次性的把所有数据给读完。

 情况2:

 写端写入数据的速度非常慢,每10秒钟写入一条数据,读端一直读取。

在写端休眠的10秒中,读端一直阻塞在read函数那里,等待写端将数据写入。

结论:当管道中没有数据时,且写端没有关闭写文件描述符时,读端会一直阻塞等待,直到写端有数据写入。

情况3

写端正常写入,读端每10秒钟读取一次数据。当管道被写满时,写端在做什么?

管道瞬间被写满 ,然后写段会阻塞在那里,等待读端读取数据。

总结:当管道被写满时,写端会阻塞等待读端将数据读取。

 情况4

读端正常读取,写端在写入过程中突然将写文件描述符关闭

总结:当写端不再写入,并且关闭了pipe,那么读端将会把管道内的内容读完,最后就会读到返回值为0,表示读取结束,类似于读到了文件的结尾 

情况5

写端正常写入,但是读端正常读取过程中突然将读文件描述符关闭。

 

我们发现:当读端关闭读文件描述符的同时,写文件描述符也同时被关闭了。这是因为没有进程从管道读取数据了 ,所以往管道中写入的数据就是没有利用价值的,操作系统不会出现这种毫无价值的写入。

总结当读端不再进行读取操作,并且关闭自己的文件描述符fd,此时的写就没有意义了。那么OS就会通过信号13(SIGPIPE)的方式直接终止写端的进程

如何证明写进程是被13号信号杀死的呢?由于子进程退出后,父进程可以通过进程等待查到子进程的退出信息。所以我们:

所以,的确是操作系统向子进程发送13号信号,来终止写进程的。 


根据管道的几种特殊读写的情况,也间接创造出了管道的5个特征。

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。同样,兄弟进程,爷孙进程都可以利用管道进行通信。
  • 管道提供流式服务。管道并不关系管道传输的是什么数据。
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道 

 2.6基于管道的进程池设计

目标:父进程通过管道控制子进程。

实现原理

如图所示:创建若干子进程和管道,父子进程之间通过管道进行链接,父进程写入数据,子进程读取数据。然后子进程做特定的操作。

 如下代码:

#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define MakeSeed() srand((unsigned long)time(nullptr) ^ getpid() ^ 0x171237 ^ rand() % 1234)

#define PROCSS_NUM 10

///子进程要完成的某种任务 -- 模拟一下/
// 函数指针 类型
typedef void (*func_t)();

void downLoadTask()
{
    std::cout << getpid() << ": 下载任务\n"
              << std::endl;
    sleep(1);
}

void ioTask()
{
    std::cout << getpid() << ": IO任务\n"
              << std::endl;
    sleep(1);
}

void flushTask()
{
    std::cout << getpid() << ": 刷新任务\n"
              << std::endl;
    sleep(1);
}

void loadTaskFunc(std::vector<func_t> *out)
{
    assert(out);
    out->push_back(downLoadTask);
    out->push_back(ioTask);
    out->push_back(flushTask);
}

/下面的代码是一个多进程程序//
class subEp // Endpoint
{
public:
    subEp(pid_t subId, int writeFd)
        : subId_(subId), writeFd_(writeFd)
    {
        char nameBuffer[1024];
        snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", num++, subId_, writeFd_);
        name_ = nameBuffer;
    }

public:
    static int num;
    std::string name_;
    pid_t subId_;
    int writeFd_;
};

int subEp::num = 0;

int recvTask(int readFd)
{
    int code = 0;
    ssize_t s = read(readFd, &code, sizeof code);
    if(s == 4) return code;
    else if(s <= 0) return -1;
    else return 0;
}

void sendTask(const subEp &process, int taskNum)
{
    std::cout << "send task num: " << taskNum << " send to -> " << process.name_ << std::endl;
    int n = write(process.writeFd_, &taskNum, sizeof(taskNum));
    assert(n == sizeof(int));
    (void)n;
}

void createSubProcess(std::vector<subEp> *subs, std::vector<func_t> &funcMap)
{
    std::vector<int> deleteFd;
    for (int i = 0; i < PROCSS_NUM; i++)
    {
        int fds[2];
        int n = pipe(fds);
        assert(n == 0);
        (void)n;
        // 父进程打开的文件,是会被子进程共享的
        // 你试着多想几轮
        pid_t id = fork();
        if (id == 0)
        {
            for(int i = 0; i < deleteFd.size(); i++) close(deleteFd[i]);
            // 子进程, 进行处理任务
            close(fds[1]);
            while (true)
            {
                // 1. 获取命令码,如果没有发送,我们子进程应该阻塞
                int commandCode = recvTask(fds[0]);
                // 2. 完成任务
                if (commandCode >= 0 && commandCode < funcMap.size())
                    funcMap[commandCode]();
                else if(commandCode == -1) break;
            }
            exit(0);
        }
        close(fds[0]);
        subEp sub(id, fds[1]);
        subs->push_back(sub);
        deleteFd.push_back(fds[1]);
    }
}

void loadBlanceContrl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int count)
{
    int processnum = subs.size();
    int tasknum = funcMap.size();
    bool forever = (count == 0 ? true : false);

    while (true)
    {
        // 1. 选择一个子进程 --> std::vector<subEp> -> index - 随机数
        int subIdx = rand() % processnum;
        // 2. 选择一个任务 --> std::vector<func_t> -> index
        int taskIdx = rand() % tasknum;
        // 3. 任务发送给选择的进程
        sendTask(subs[subIdx], taskIdx);
        sleep(1);
        if(!forever)
        {
            count--;
            if(count == 0) break;   
        }
    }
    // write quit -> read 0
    for(int i = 0; i < processnum; i++) close(subs[i].writeFd_); // waitpid();
}

    
void waitProcess(std::vector<subEp> processes)
{
    int processnum = processes.size();
    for(int i = 0; i < processnum; i++)
    {
        waitpid(processes[i].subId_, nullptr, 0);
        std::cout << "wait sub process success ...: " << processes[i].subId_ << std::endl;
    }
}

int main()
{
    MakeSeed();
    // 1. 建立子进程并建立和子进程通信的信道, 有bug的,但是不影响我们后面编写
    // 1.1 加载方发表
    std::vector<func_t> funcMap;
    loadTaskFunc(&funcMap);
    // 1.2 创建子进程,并且维护好父子通信信道
    std::vector<subEp> subs;
    createSubProcess(&subs, funcMap);

    // 2. 走到这里就是父进程, 控制子进程,负载均衡的向子进程发送命令码
    int taskCnt = 3; // 0: 永远进行
    loadBlanceContrl(subs, funcMap, taskCnt);

    // 3. 回收子进程信息
    waitProcess(subs);

    return 0;
}

 三.命名管道

匿名管道通信仅仅适用于有血缘关系的进程间的通信,有较大的局限性。有没有一种能用于没有血缘关系的进程间的通信呢?有,命名管道。 

创建命名管道文件的操作:mkfifo +filename

示例演示:

我们可以发现它的文件类型前面以P开头,当大家看到P开头的,会能想到什么?在之前我给大家在讲我们Linux基础命令的时候说过一个话题叫做文件类型:以 - 开头普通文件、以D开头为目录文件、以L开头为链接文件L开头的叫做软链接、这里以P开头叫做管道文件,这时候在磁盘上存在了一个管道文件。


 

【解释说明】  

在我们的理解中把它写到文件当中,此时就相当于当我一敲回车,echo对应的这个东西就会变成进程;
然后,执行我们向显示器当中打印,经过重定向,它最终不向显示器文件打印,而向管道文件中打印,所以底层作为重定向是没问题的;
紧接着我们就尝试去写了,但当前呢它卡在这里的,什么都没做,我们再看一下当前这个管道文件里,当前显示的是零,好像没有写入啊;
这是因为管道文件有种特殊特性,虽然在磁盘当中创建了这个 fifo,但它仅仅是一种符号,那么对于这种符号呢,将来你向这个文件里写入的消息,并没有或者并不会刷新落实到磁盘上,而是只帮我们在这里直接 echo,然后写入管道文件当中,但是管道文件当前是内存级的,所以你的大小没有变。


接下来,我们来试一下重定向:

3.1实现原理

我们在磁盘中创建并命名一个文件,这个文件是真是存在在磁盘的某个路径下的。所以任意进程都可以打开这个文件。如果系统中有两个想要通信的进程,这个文件对双方进程来说就是公共资源。

 一个进程向该文件中写入数据,另一进程从该软件中读取数据,进程双方就可以达到通信的目的。但是要通信的数据不会真的刷新到文件中,还是利用文件的缓冲区来进行通信的。所以你查询该文件,总会发现这个文件的大小一直是0。

:要通信的两个进程如何找到同一个文件呢?

:命名文件一定是真实存在于磁盘的某个目录下的,也就是说会有具体的路径,所以通信进程双方可以使用路径和文件名相结合的方式找到同一个文件。路径+文件名=唯一性

:两个进程同时打开同一个文件,操作系统会为该文件创建两个struct file结构体吗?

:不会,操作系统在计算机里是一个精打细算的角色,尤其是内存这种有限且非常重要的资源。

同一个文件的struct file结构体内部的数据应该是一样的,既然如此,操作系统为什么还要花费资源去维护另外一块空间呢?

如图所示:

:为什么不进行文件数据的刷新工作?

:没有必要,我们想要的仅仅是读取数据而已,数据在缓冲区内依旧可以完成数据的写入和读取。理论上可以从将数据刷新到磁盘,然后再从磁盘中将数据读取出来,但这样做,进程间通信的成本就会大大增加,因为磁盘属于外设,将数据从内存中写入外设是很慢的。 

3.2代码实现

我们创建4个文件:

  • client.cc :充当文件的写入端,为一个进程
  • server.cc:充当文件的读取端,为一个进程
  • comm.hpp完成创建管道文件的工作
  • makefile:编译

client.cc

#include "comm.hpp"

// 你可不可以把刚刚写的改成命名管道呢!
int main()
{
    std::cout << "client begin" << std::endl;
    int wfd = open(NAMED_PIPE, O_WRONLY);
    std::cout << "client end" << std::endl;
    if(wfd < 0) exit(1); 

    //write
    char buffer[1024];
    while(true)
    {
        std::cout << "Please Say# ";
        fgets(buffer, sizeof(buffer), stdin); // abcd\n
        if(strlen(buffer) > 0) buffer[strlen(buffer)-1] = 0;
        ssize_t n = write(wfd, buffer, strlen(buffer));
        assert(n == strlen(buffer));
        (void)n;
    }

    close(wfd);
    return 0;
}

server.cc

#include "comm.hpp"

int main()
{
    bool r = createFifo(NAMED_PIPE);
    assert(r);
    (void)r;

    std::cout << "server begin" << std::endl;
    int rfd = open(NAMED_PIPE, O_RDONLY);
    std::cout << "server end" << std::endl;
    if(rfd < 0) exit(1);

    //read
    char buffer[1024];
    while(true)
    {
        ssize_t s = read(rfd, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "client->server# " << buffer << std::endl;
        }
        else if(s == 0)
        {
            std::cout << "client quit, me too!" << std::endl;
            break;
        }
        else
        {
            std::cout << "err string: " << strerror(errno) << std::endl;
            break;
        }
    }

    close(rfd);

    // sleep(10);
    removeFifo(NAMED_PIPE);
    return 0;
}

comm.hpp 

#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<cerrno>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

#define NAMED_PIPE "/home/user/exercise/my_pipe/named_pipe/name_pipe"
bool createFiFo(const std::string &path)
{
    umask(0);
    int n=mkfifo(path.c_str(),0600);
    if(n==0)
    {
        return true;
    }
    else
    {
        std::cout<<"errno:"<<errno<<"err string"<<strerror(errno)<<std::endl;

    }
}
void deleteFifo(const std::string &path)
{
    int n=unlink(path.c_str());
    assert(n==0);
     (void)n;
}

makefile 

.PHONY:all
all:client server
client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f client server

 四.共享内存

写在最前面:共享内存虽然作为进程间通信的一种方式,但是在实际工作中,使用的次数缺很少,具体原因我会在讲解中说明。这次,我们打破以往的讲解顺序:先讲原理,然后写代码,最后是概念。

4.1共享内存的原理

依上图,我简单讲解一下:通过学习管道,我们知道两个进程要实现通信,必须看到同一块资源。其中,我们在内存中申请的这块空间就可以充当进程双方通信的资源,这块内存就叫做共享内存。

我们需要知道:

  • 操作系统中一定会存在很多共享的内存,这些共享内存空间要不要管理?如何被管理?我们后面说。
  •  共享内存是一种通信方式,如果愿意,所有的内存都可以使用共享内存进行通信。

4.2接口介绍

 操作系统为了方便我们使用共享内存,对外提供了一批接口。

shmget:在内存中申请一块指定大小的共享内存空间

参数介绍

①:key

我们提到操作系统中一定会存在多个共享内存,所以一定要有一个数据来标定这个共享内存的唯一性,key的作用便是标定这个唯一性,未来key值要被写入到共享内存相关属性集中的。

我们可以使用函数ftok来获取这个key的值。这个值是多少一点都不重要,能进行唯一性标识最重要。就像我们的身份证号,具体是多少对警察👮‍♀️来说无关紧要,重要的是它只属于你一个人。

有两个参数

  1. pathname是一个路径,必须是真是存在且可以被访问的路径。
  2. proj_id为任意一个不为0的数字。 

成功的话,返回计算的key_t值,失败返回-1。

这个函数的作用是将用户传入的路径和数字通过算法得到一个值。并且传入的参数不同,得到的结果也一定不同。 在使用共享内存时,进程双方要想访问同一块共享内存,必须传入相同的路径和数字,通过ftok得到同一个返回值,然后将返回值传入shmget中,才能访问到同一块共享内存。

 ②size:

在内存中要开辟的共享内存空间大小,单位为字节。

③proj_id:

该参数用于确定创建共享内存的选项。使用二进制标志位通过位图的形式传给该函数。

选项有两个:

  •  IPC_CREAT:共享内存不存在,就创建;如果存在,获取之。
  • IPC_EXCL:这个选项不可以单独使用,必须结合IPC_CREAT使用。如果共享内存已经存在,出错返回。也就是说,如果创建成功,给用户返回的一定是一块新的共享内存。

返回值:

 程序员使用该返回值来对该共享内存进行相关的操作。这个返回值在作用上和open函数的返回值一样。但这两个返回值之间是相互割裂的,所以这就造成在后期学习网络时,我们很少使用共享内存来进行通信。

 🌿再谈key值

key值和shmget的返回值有什么区别呢?

问:大家在学C语言时,使用malloc申请一块堆空间时,要传入要申请的堆空间的大小,为什么使用free要释放空间时,只需传入堆空间的起始地址即可? 系统怎么知道我要释放多大的空间呢?

答:假如我们申请4KB的空间,系统会为我们分配超过4KB的空间,多出来的空间要维护这块空间,包括这块堆空间的起始地址,大小,权限等信息。这就是先描述,再组织。共享内存也是如此

所以,我们申请一块共享内存空间,我们不能简单的认为操作系统仅仅为我们在内存中申请了一块空间。共享内存=共享内存块+共享内存的属性信息 所以,操作系统为每块共享内存都创建了一个对应的结构体,用来保存共享内存相关信息。所以,对共享内存的管理就变为对相应结构体的管理。


有一次,张三请李四去吃饭,张三提前去酒店定了包间,因为包间说话方便嘛!吃饭之前,张三通过微信把包间号发给了李四,比如:好再来酒店,6号包间。然后张三就早早的在包间里等着李四,李四找到了对应的包间,一看张三在等着呢。两人见面习惯性的含蓄了一番,然后就吃了起来。

我问:假如张三定的是6号包间,李四会问张三为什么要定6号包间吗?

不会,因为数字的作用仅仅是用来标识这个房间的唯一性,数值毫无意义。


我们提到每一个共享内存都有相应的数据块用来保存这个内存的属性信息。然后我们又说key值毫无意义,可以用来标识唯一性即可。

我们通过ftok函数得到key值,当我们通过shmget函数申请共享内存时,将key值传入,这是key值就被当作属性的一部分被设置到了共享内存相应的数据块中。等到再有进程拿着相同的key值申请内存时,系统就会遍历系统内所有的共享内存的数据块,然后将自己的key值和数据块中的key值进行对比。一句话:key值的作用在于标识这个共享内存,等待着其他进程通过这个key值来找到这块内存来进行通信。

 :如何理解shmget的返回值shmid和key值的关系呢?这两个值是什么关系呢?

:我们在学习文件系统时,操作系统内核中是通过inode编号来区分文件的。而我们对文件进行操作是利用文件描述符fd。shmid和key值的关系就好似fd和inode的关系,shmid暴露给上层供程序员进行操作,而底层标识一个共享内存使用的却是key值。前辈大佬们使用两套规则来标识共享内存,其用意是实现底层和上层的解耦,而且方便上层操作,两套规则之间不会相互干扰。


接下来:我们认识其他几个接口

 🍃shmctl :对共享内存进行销毁

该接口本身用于控制共享内存,可用于销毁。 

shmid不再介绍,cmd传入IPC_RMID,buf传nullptr。 

成功返回0,失败返回-1。 

🍃shmat:共享内存和进程地址空间的链接

创建共享内存后还不能直接使用,需要找到内存地址后才能使用,即连接。 

shmid即shmget返回值。

shmaddr用于确定将共享内存挂在进程虚拟地址哪个位置,一般填nullptr即可代表让内核自己确定位置。

shmflg用于确定挂接方式,一般填0

连接成功返回共享内存在进程中的起始地址,失败返回-1。

🍃hmdts—分离

当使用完毕后,需要分离挂接的共享内存。 

 

shmaddr与shmat的相同,为共享内存在进程中地址位置,一般填nullptr。

分离成功返回0,失败返回-1。 

4.3命令行操作共享内存

查看共享内存

 ipcs -m 

共享内存的生命周期是随操作系统的,也就是除非计算机关机,如果我们不使用函数或者指令的方式对空间进行释放的话,共享内存的空间会一直存在。

删除共享内存空间: ipcs -m +shmid

4.4代码实现

 现在我们已经把准备工作全部做完了,可以写完整的代码了。

 我们创建4个文件即可,分别为makefile,comm.hpp,shm_client,ipcShmServer.cpp:。

comm.hpp

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <unistd.h>
#include <cassert>

#define SHM_SIZE 4096
#define PATH_NAME ".fifo"
#define PROJ_ID 0x14

#define FIFO_FILE ".fifo"

// 创建命名管道文件
void CreatFifo() {
    umask(0);
    if(mkfifo(FIFO_FILE, 0666) < 0)
    {
        std::cerr << strerror(errno) << std::endl;
        exit(-1);
    }
}

#define READER O_RDONLY
#define WRITER O_WRONLY

// 以一定的方式打开管道文件
int Open(const std::string &filename, int flags)
{
    return open(filename.c_str(), flags);
}

// 用于服务端, 等待读取管道文件数据, 即读取信号
int Wait(int fd) {
    uint32_t value = 0;
    ssize_t res = read(fd, &value, sizeof(value));
    
    return res;
}

// 用于客户端, 向管道中写入数据, 即写入信号
void Signal(int fd) {
    uint32_t cmd = 1;
    write(fd, &cmd, sizeof(cmd));
}

// 关闭管道文件, 删除管道文件
void Close(int fd, const std::string& filename) {
    close(fd);
    unlink(filename.c_str());
}

ipcShmServer.cpp: 

// ipcShmServer 服务端代码, 即 接收端
// 需要创建、删除共享内存块
// 需要创建、删除命名管道
#include "common.hpp"
using std::cout;
using std::endl;
using std::cerr;

int main() {
    // 0. 创建命名管道
    CreatFifo();
    int fd = Open(FIFO_FILE, READER);       // 只读打开命名管道
    assert(fd >= 0);

    // 1. 创建共享内存块
    int key = ftok(PATH_NAME, PROJ_ID);
    if(key == -1) {
        cerr << "ftok error. " << strerror(errno) << endl;
        exit(1);
    }

    cout << "Create share memory begin. " << endl;
    int shmId = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if(shmId == -1) {
        cerr << "shmget error" << endl;
        exit(2);
    }
    cout << "Creat share memory success, key: " << key << " , shmId: " << shmId << endl;

    // 2. 连接共享内存块
    sleep(2);
    char* str = (char*)shmat(shmId, nullptr, 0);
    if(str == (void*)-1) {
        cerr << "shmat error" << endl;
        exit(3);
    }
    cout << "Attach share memory success. \n" << endl;

    // 3. 使用共享内存块
    while(true) {
        if (Wait(fd) <= 0)
            break;              // 如果从管道读取数据失败, 或管道文件关闭, 则退出循环
        
        cout << str;
        sleep(1);
    }
    cout << "\nThe server has finished using shared memory. " << endl;

    sleep(1);
    // 3. 分离共享内存块
    int resDt = shmdt(str);
    if(resDt == -1) {
        cerr << "shmdt error" << endl;
        exit(4);
    }
    cout << "Detach share memory success. \n" << endl;

    // 4. 删除共享内存块
    int res = shmctl(shmId, IPC_RMID, nullptr);
    if(res == -1) {
        cerr << "shmget error" << endl;
        exit(5);
    }
    cout << "Delete share memory success. " << endl;

    // 5. 删除管道文件
    Close(fd, FIFO_FILE);
    cout << "Delete FIFO success. " << endl;

    return 0;
}

ipcShmClient.cpp: 

// ipcShmClient 客户端代码, 即 发送端
// 不参与共享内存块的创建与删除
// 不参与命名管道的创建与删除
#include "common.hpp"
using std::cout;
using std::endl;
using std::cerr;

int main() {
    // 0. 打开命名管道
    int fd = Open(FIFO_FILE, WRITER);

    // 1. 获取共享内存块
    int key = ftok(PATH_NAME, PROJ_ID);
    if(key == -1) {
        cerr << "ftok error. " << strerror(errno) << endl;
        exit(1);
    }
    cout << "Get share memory begin. " << endl;
    sleep(1);
    int shmId = shmget(key, SHM_SIZE, IPC_CREAT);
    if(shmId == -1) {
        cerr << "shmget error" << endl;
        exit(2);
    }
    cout << "Creat share memory success, key: " << key << " , shmId: " << shmId << endl;

    // 2. 连接共享内存块
    sleep(2);
    char* str = (char*)shmat(shmId, nullptr, 0);
    if(str == (void*)-1) {
        cerr << "shmat error" << endl;
        exit(3);
    }
    cout << "Attach share memory success. " << endl;

    // 3. 使用共享内存块
    while (true) {
        printf("Please Enter $ ");
        fflush(stdout);
        ssize_t res = read(0, str, SHM_SIZE);       // 从标准输入读取数据写入到 共享内存(str) 中
        if(res > 0) {
            str[res] = '\0';
        }

        Signal(fd);     // 向命名管道写入信号
    }
    cout << "\nThe client has finished using shared memory. " << endl;

    // 3. 分离共享内存块
    int res = shmdt(str);
    if(res == -1) {
        cerr << "shmdt error" << endl;
        exit(4);
    }
    cout << "Detach share memory success. " << endl;

    return 0;
}

makefile 

.PHONY:all
all:ipcShmClient ipcShmServer

ipcShmClient:ipcShmClient.cpp
	g++ $^ -o $@
ipcShmServer:ipcShmServer.cpp
	g++ $^ -o $@

.PHONY:clean
clean:
	rm -f ipcShmClient ipcShmServer .fifo

本篇到这里就结束了,如何您觉得内容还可以的话,给作者点一个免费的关注呀!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/725885.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

精打细算做好“节水账”,宏电“灌区哨兵”助力灌区量水监测

节水优先&#xff0c;量水而行。量水监测是高标准农田生产灌溉水资源监测的重要部分&#xff0c;用于解决大面积农业灌溉条件下节点多、距离长、灌区水情自动在线监测的难题&#xff0c;有效实现灌区水资源统一管理、优化配置&#xff0c;提高灌溉效率。 根据灌区所在地域和规模…

获得淘宝app商品详情原数据API接口|商品价格详情页面优惠券主图

item_get_app&#xff1a;通过商品id获取商品详情页数据 注册账号获取API测试地址 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地址中&#xf…

docker ce的使用介绍

docker docker17.03以后 docker ce&#xff0c;社区免费版&#xff0c;vscode的docker插件使用的该版本&#xff08;默认windows只支持windows容器&#xff0c;linux支持linux容器&#xff09;docker ee&#xff0c;企业版本 docker17.03以前 docker toolbox&#xff0c;基于…

【计算机网络】[第4章 网络层][自用]

1 概述 (1)因特网使用的TCP/IP协议体系(四层)的网际层,提供的是无连接、不可靠的数据报服务; (2)ATM、帧中继、X.25的OSI体系(七层)中的网络层,提供的是面向连接的、可靠的虚电路服务。 (3)路由选择分两种: 一种是由用户or管理员人工进行配置(只适用于规…

idea 创建properties文件,解决乱码

设置properties文件编码 点击file->Settings File Encodings->设置utf-8 重新创建.properties文件才生效

Vue64-引入第三方的样式文件

xxx.vue文件里面的样式&#xff0c;一般都是程序员亲自写的&#xff0c;不要在里面复制进第三方的样式代码&#xff0c;不方便后期的维护&#xff01;&#xff01;&#xff01; 第三方的样式&#xff0c;建议作为CSS文件&#xff0c;引入。 一、方式一&#xff1a;第三方的CSS…

C++ Windows Hook使用

GitHub - microsoft/Detours: Detours is a software package for monitoring and instrumenting API calls on Windows. It is distributed in source code form. /*挂载钩子 setdll /d:C:\Users\g\source\repos\LotTest\Release\lotDll.dll C:\Users\g\source\repos\LotTest…

MPLS TE简介

定义 MPLS TE&#xff08;MPLS Traffic Engineering&#xff09;&#xff0c;即MPLS流量工程。MPLS流量工程通过建立基于一定约束条件的LSP隧道&#xff0c;并将流量引入到这些隧道中进行转发&#xff0c;使网络流量按照指定的路径进行传输&#xff0c;达到流量工程的目的。 …

Redis变慢了?之三

Redis变慢了&#xff1f;之三 Redis变慢了fork耗时优化方案 AOFAOF策略对性能影响 最后 Redis变慢了 Redis变慢上一篇文章地址&#xff1a;Redis变慢了&#xff1f;之二 这篇文章继续Redis变慢情况的分析。 fork耗时 在 Redis 中&#xff0c;fork 是一个非常重要的操作&…

已成功见刊检索的国际学术会议论文海报展示(2)

【先投稿先送审】第四届计算机、物联网与控制工程国际学术会议&#xff08;CITCE 2024) 大会官网&#xff1a;www.citce.org 时间地点&#xff1a;2024年11月1-3日&#xff0c;中国-武汉 收录检索&#xff1a;EI Compendex&#xff0c;Scopus 主办单位&#xff1a;四川师范…

C# WPF入门学习主线篇(二十二)—— 样式(Styles)的定义和应用

C# WPF入门学习主线篇&#xff08;二十二&#xff09;—— 样式&#xff08;Styles&#xff09;的定义和应用 欢迎来到C# WPF入门学习系列的第二十二篇。本篇文章将详细介绍WPF中的样式&#xff08;Styles&#xff09;的定义和应用。样式在WPF中起到重要作用&#xff0c;通过样…

idea创建Mavenweb项目-提供Servlet 2024idea无法创建解决方式

idea社区版需要自己配置服务器需要去下载插件Tomcat Server 对服务器设置 idea无法创建servlet的问题是2023与2024版本idea做出了改动需要自己手动配置servlet文件 在设置中搜索template也就是模板&#xff0c;然后选择文件和代码板块&#xff0c;点击左上角的添加新的模板 名…

【ARM】MDK在debug模式下断点的类型

【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 了解不同情况下&#xff0c;设置的断点的类型是什么。 2、 问题场景 在debug模式下&#xff0c;经常通过断点去调试代码。但是对于断点的类型不了解&#xff0c;不清楚断点为什么会被进入。不了解在不同语句或者情…

推荐一款功能强大的显示器!

最近在写项目开发文档&#xff0c;经常需要几个界面来回切换&#xff0c;真的深刻感受到了一台外接显示器对一名程序员来说有多重要了&#xff0c;画功能流程图的时候嫌弃自己的笔记本屏幕不够大&#xff0c;看代码的时候又在想要是有个旋转屏就好了&#xff0c;来回切换界面的…

Mac数据如何恢复?3 款最佳 Mac 恢复软件

如果您认为 Mac 上已删除的文件永远丢失了&#xff0c;那您就大错特错了&#xff01;实际上&#xff0c;即使您清空了 Mac 上的垃圾箱&#xff0c;也有许多解决方案可以帮助您恢复已删除的文件。最好的解决方案之一是 Mac 恢复删除软件。最好的Mac 恢复删除应用程序可以轻松准确…

微信多开器

由于微信的限制&#xff0c;我们平时只能登录一个微信&#xff0c;要登录多个微信一般需要多台手机&#xff0c;很显然这种方法很费手机&#xff01;&#xff01;一个微信多开神器可以给你省下好几台手机钱&#xff0c;抓紧拉下来放手机里落灰http://www.xbydon.online/?p132 …

「51媒体」活动会议,展览展会,直播曝光的一种方法

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 我们在做活动会议&#xff0c;或者参加展览展会&#xff0c;需要进行直播的时候&#xff0c;可以通过一键同步多个媒体平台的方法&#xff0c;来扩大曝光&#xff0c;比如一场直播我们可…

JDK8和JDK17共存以及切换

1. 下载和安装 1.1下载安装JDK17 下载地址: https://www.oracle.com/cn/java/technologies/downloads/ 1.2安装jdk17 点击下一步 此处可以修改安装的目标位置,修改安装位置后,点击下一步 接下来及安装完成 注意事项 此时通过cmd命令java –version你会发现竟然出现了…

B 站开源专为角色扮演优化的模型

是 B 站开源的一系列的大语言模型&#xff0c;其中的 chat 版本训练时引入了互联网社区语料&#xff0c;趣味性明显增强; character 版本则为角色扮演做了优化。 现在&#xff0c;已经可以在 HF Space 上免费使用 Chat 和 角色扮演啦

【解决】法启动此程序,因为计算机中丢失vcruntime140_1.dll,尝试重新安装此程序以解决此问题【包括安装mysql在内的】

缺少vcruntime140_1.dll解决此问题的第一步找到该文件,有些dll修复工具是收费的&#xff0c;因此下面介绍几种比较简单有效而且免费的解决办法 方法1&#xff1a;重新安装Visual C Redistributable Packages 上面的安装包解决win7&#xff0c;8&#xff0c;10&#xff0c;11的…