当前位置: 首页 > news >正文

【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> -> indexint taskIdx = rand() % tasknum;// 3. 任务发送给选择的进程sendTask(subs[subIdx], taskIdx);sleep(1);if(!forever){count--;if(count == 0) break;   }}// write quit -> read 0for(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); //writechar buffer[1024];while(true){std::cout << "Please Say# ";fgets(buffer, sizeof(buffer), stdin); // abcd\nif(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);//readchar 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.ccg++ -o $@ $^ -std=c++11
server:server.ccg++ -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 ipcShmServeripcShmClient:ipcShmClient.cppg++ $^ -o $@
ipcShmServer:ipcShmServer.cppg++ $^ -o $@.PHONY:clean
clean:rm -f ipcShmClient ipcShmServer .fifo

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

相关文章:

【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代码实现 四.…...

测试用例设计:提升测试覆盖率的策略与方法

测试用例设计&#xff1a;提升测试覆盖率的策略与方法 前言测试用例设计的原则提高测试覆盖率的方法测试类型的分析 测试用例设计的基本方法等价类划分边界值分析正交法判定表法因果图法 方法与策略方法策略 如何评价测试用例结论 前言 在软件开发过程中&#xff0c;测试用例设…...

【微服务】什么是Hystrix?一文带你入门Hystrix

文章目录 强烈推荐引言主要功能实现容错应用场景1. 远程服务调用2. 防止级联故障3. 网络延迟和超时管理4. 资源隔离5. 高并发场景6. 熔断与自动恢复7. 故障检测与监控 示例应用场景使用实例1. 引入依赖2. 创建 Hystrix 命令类3. 使用 Hystrix 命令4. 配置 Hystrix5. 实时监控集…...

AI学习指南机器学习篇-支持向量机超参数调优

AI学习指南机器学习篇-支持向量机超参数调优 在机器学习领域中&#xff0c;支持向量机&#xff08;Support Vector Machines&#xff0c;SVM&#xff09;是一种非常常用的监督学习模型。它通过寻找一个最优的超平面来进行分类和回归任务。然而&#xff0c;在实际应用中&#x…...

掉电安全文件系统分析

掉电安全FS 掉电安全的文件系统&#xff08;Power-Fail Safe File Systems&#xff09;被设计为在电源故障或系统崩溃的情况下仍能保持数据一致性的文件系统。这样的文件系统通常通过使用日志&#xff08;journaling&#xff09;或写时复制&#xff08;copy-on-write&#xff…...

React-Redux学习笔记(自用)

1. 环境搭建 插件安装&#xff1a;Redux Toolkit和react-redux npm i reduxjs/toolkit react-redux2、 store目录结构设计 集中状态管理的部分会单独创建一个store目录&#xff08;在src下&#xff09;应用通常会有很多个子模块&#xff0c;所以还会有个modules目录&#x…...

【机器学习】:线性回归模型学习路线

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…...

C++设计模式——Flyweight享元模式

一&#xff0c;享元模式简介 享元模式是一种结构型设计模式&#xff0c;它将每个对象中各自保存一份数据的方式改为多个对象共享同一份数据&#xff0c;该模式可以有效减少应用程序的内存占用。 享元模式的核心思想是共享和复用&#xff0c;通过设置共享资源来避免创建过多的实…...

Github 2024-06-19 开源项目日报 Top10

根据Github Trendings的统计,今日(2024-06-19统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量TypeScript项目3Rust项目2Go项目2JavaScript项目1Python项目1Dart项目1非开发语言项目1Ruby项目1HTML项目1项目化学习 创建周期:2538 天协议类…...

【ARM】如何通过Keil MDK查看芯片的硬件信息

【更多软件使用问题请点击亿道电子官方网站】 1、文档目标&#xff1a; 解决在开发过程中对于开发项目所使用的的芯片的参数查看的问题 2、问题场景&#xff1a; 在项目开发过程中&#xff0c;经常需要对于芯片的时钟、寄存器或者一些硬件参数需要进行确认。大多数情况下是需…...

elasticsearch的安装和配置

单节点安装与部署 我们通过docker进行安装 1.docker的安装 如果以及安装了docker就可以跳过这个步骤。 首先更新yum: yum update安装docker: yum install docker查看docker的版本&#xff1a; docker -v此时我们的docker就安装成功了。 2.创建网络 我们还需要部署kiban…...

华为云下Ubuntu20.04中Docker的部署

我想用Docker拉取splash&#xff0c;Docker目前已经无法使用&#xff08;镜像都在国外&#xff09;。这导致了 docker pull 命令的失败&#xff0c;原因是timeout。所以我们有必要将docker的源设置在国内&#xff0c;直接用国内的镜像。 1.在华为云下的Ubuntu20.04因为源的原因…...

1、C++编程中的基本运算 - 课件

一、基础知识 1、C程序的基本框架 // 预处理器指令&#xff0c;引入需要的头文件 #include <iostream> // 使用标准命名空间 using namespace std; // 主函数&#xff0c;程序的入口 int main() {// 局部变量声明// 程序逻辑代码// 返回值&#xff0c;表示程序正常结束…...

Java动态代理详解

文章目录 一、JDK动态代理1、关键类和接口2、实现步骤 二、CGLIB动态代理1、关键类2、实现步骤 三、总结 Java中的动态代理是一种设计模式&#xff0c;它允许在运行时创建代理对象&#xff0c;而不是在编译时。代理对象可以用来代理真实对象的方法调用。 Java中的动态代理主要…...

Python基础学习文档

一、Python入门 1.Python简介&#xff1a; Python是一种高级编程语言&#xff0c;用于多种应用&#xff0c;包括网站开发、数据科学、人工智能等。 Python具有语法简洁、易读性强、功能强大等特点。 2.安装Python ①访问Python官网&#xff08;https://www.python.org/&am…...

数据结构与算法笔记:基础篇 - 分治算法:谈一谈大规模计算框架MapReduce中的分治思想

概述 MapReduce 是 Google 大数据处理的三姐马车之一&#xff0c;另外两个事 GFS 和 Bigtable。它在倒排索引、PageRank 计算、网页分析等搜索引擎相关的技术中都有大量的应用。 尽管开发一个 MapReduce 看起来很高深。实际上&#xff0c;万变不离其宗&#xff0c;它的本质就…...

如何清除anaconda3缓存?

如果长期使用anaconda不清理缓存&#xff0c;会导致anaconda占用磁盘空间越来越多&#xff0c;甚至系统磁盘撑爆。 清除包缓存&#xff1a; 打开 Anaconda Prompt 或者命令行窗口。运行以下命令清除包缓存&#xff1a;conda clean --all这会清除所有的包缓存&#xff0c;释放磁…...

智慧校园发展趋势:2024年及未来教育科技展望

展望2024年及未来的教育科技领域&#xff0c;智慧校园的发展正引领着一场教育模式的深刻变革&#xff0c;其核心在于更深层次地融合技术与教育实践。随着人工智能技术的不断成熟&#xff0c;个性化学习将不再停留于表面&#xff0c;而是深入到每个学生的个性化需求之中。通过精…...

【Python机器学习系列】针对特定数据构建管道流水线进行机器学习预测(案例+源码)

这是我的第305篇原创文章。 一、引言 机器学习项目中有可以自动化的标准工作流程。在 Python scikit-learn 中&#xff0c;管道有助于明确定义和自动化这些工作流程。使用pipeline后&#xff0c;我们每一步的输出都会自动的作为下一个的输入。一套完整的机器学习应用流程如下&a…...

Python 学习 第三册 第12章 图的最优化问题

----用教授的方式学习。 目录 12.1图的最优化问题 12.1.1最短路径:深度优先搜索和广度优先搜索 12.1图的最优化问题 我们下面研究另一种最优化问题。假设你有一个航空公司航线的价格列表,其中包括美国任意两个城市之间的航班价格。假设有3个城市A、B和C,从A出发经过B到达…...

建筑工程乙级资质与工程质量控制体系的构建

1. 质量管理体系建立 ISO 9001认证&#xff1a;虽然不是直接要求&#xff0c;但许多乙级资质企业会选择通过ISO 9001质量管理体系认证&#xff0c;以标准化管理流程&#xff0c;提升质量管理水平。质量方针与目标&#xff1a;明确企业的质量方针&#xff0c;设定可量化、可追踪…...

kafka学习笔记07

Kafka高可用集群搭建节点需求规划 开放端口。 Kafka高可用集群之zookeeper集群搭建环境准备 删除之前的kafka和zookeeper。 重新进行环境部署&#xff1a; 我们解压我们的zookeeper: 编辑第一个zookeeper的配置文件: 我们重复类似的操作&#xff0c;创建三个zookeeper节点: 记…...

MQTTfx连接阿里云(详细版)

1、介绍 作为物联网开放平台&#xff0c;阿里云可谓是吸引大多数嵌入式爱好者的平台。物联网MQTT协议火热的今天&#xff0c;你使用过阿里云吗&#xff1f;本篇文章带你接触阿里云&#xff0c;实现MQTT通信。 我们在测试MQTT之前先了解下什么是MQTT协议。大家都知道它是一种发…...

Vue3使用provide和inject实现孙组件给爷组件传递数据

前言&#xff1a; 最近在研究gitHub中的一个项目并将与自己之前完成的项目进行结合&#xff0c;其中有一个功能是需要在孙组件将数据传递给爷组件&#xff0c;笔者研究后将使用总结如下&#xff1a; 具体步骤&#xff1a; 1.爷组件先定义一个空的函数传递给孙子 2.孙组件使…...

昇思25天学习打卡营第1天|基本介绍及快速入门

1.第一天学习总体复盘 1&#xff09;成功注册昇思大模型平台&#xff0c;并成功申请算力&#xff1b; 2)在jupyter环境下学习初学入门/初学教程的内容&#xff1b; 在基本介绍部分&#xff0c;快速撸了一边内容&#xff0c;有了一个基本的了解&#xff08;没理解到位的计划采用…...

C#.Net筑基-类型系统②常见类型

01、结构体类型Struct 结构体 struct 是一种用户自定义的值类型&#xff0c;常用于定义一些简单&#xff08;轻量&#xff09;的数据结构。对于一些局部使用的数据结构&#xff0c;优先使用结构体&#xff0c;效率要高很多。 可以有构造函数&#xff0c;也可以没有。因此初始…...

【人机交互 复习】第5章 交互式系统的需求

产品特性和用户个体差异引起的不同需求。 一、产品特性 1.功能不同 &#xff08;1&#xff09;智能冰箱&#xff1a;应能够提示黄油已用完 &#xff08;2&#xff09;字处理器&#xff1a;系统应支持多种格式 2.物理条件不同 &#xff08;1&#xff09;移动设备运行的系统应尽…...

知识的补充

目录 电容和电感的基本性质 高频电路中电容与电感的等效电路 阻抗与导纳 常用单位转换 电容和电感的基本性质 电容C是两个平板比较直&#xff0c;i也比较直&#xff0c;C的 i 随 u 的变化率变化&#xff0c;i 的相位超前。 电感L是个线圈比较弯曲&#xff0c;u也比较弯&…...

微信小程序请求服务器报ERR_CONNECTION_RESET

排查思路 1.域名是否配置或跳过 2.域名是否备案 3.证书是否有效 4.服务器中间件配置证书是否生效 5.服务器中间件转发配置是否生效 6.接口是否正常 本人遇到问题描述&#xff0c;通过浏览器访问本人网站&#xff0c;https&#xff0c;get请求可以通&#xff0c;小程序wx…...

SpringMVC:拦截Mybatis的mapper

我们在使用mybatis的时候会碰到一些公共添加时间&#xff0c;操作人员&#xff0c;更新时间、或者一些分页这个使我们如果要去添加每个对应的- service - dao - mapper - xml 这样就造成很多冗余代码&#xff0c;那这个时候我们就需要使用一些通用方法&#xff0c;统一就行修改…...

用eclipce做登录网站/站长之家官网入口

项目描述java web行业通用门户网站企业官网,后台可设置所有数据运行环境jdk7或jdk8tomcat7或tomat8mysqlIntelliJ IDEA或eclipse项目技术(必填)springspring mvcmybatisjqueryjsp...

网站建设优酷/网络营销渠道类型有哪些

在前段时间公司开发的一个项目中&#xff0c;需要使用多个进程监听同一个端口提高性能&#xff0c;这样的需求需要我们解决惊群问题。在早些时候&#xff0c;我们是不能在多个子进程中listen、bind同一个socket端口的。通常的做法会在主进程中对端口进行listen、bind&#xff0…...

上海做网站找哪家好/网站关键词快速排名软件

### 前言之前已经讲过了netty实现tcp客户端&#xff0c;大家有兴趣的话可以参考文章[Netty实现TCP客户端](http://www.demodashi.com/demo/17149.html)今天就让我们来看看netty实现tcp服务端吧。这里我封装了两个类TnServerHandler与TnServer,下面就来讲讲它们的使用吧。今天涉…...

武汉专业设计网站的公司/店铺推广方法

https://blog.csdn.net/morewindows/article/details/6684558 这个链接是快排讲解很好 以下是AC代码 题目链接&#xff1a;https://www.lintcode.com/problem/sort-integers-ii/description public class Solution {/*** param A: an integer array* return: nothing*/publi…...

网站推广优化教程/合肥网络推广优化公司

愿你如阳光&#xff0c;明媚不忧伤。 目録1. Java 中的日期时间2. MySQL 中的日期时间3. Oracle 中的日期时间4. 格林威治时间5. 世界标准时间6. 时区7. 夏令时8. 本地时间【每日一面】如何避免在公历闰年时出现日期转换错误或程序逻辑错误1. Java 中的日期时间 java.util.Date…...

精美网页源码网站/seo关键词优化技巧

在日常工作和生活中&#xff0c;我们常常看到这种现象&#xff1a;下属由于没有摆正自己的位置&#xff0c;弄得顶头上司尤其是那些心胸狭窄的上司和“武大郎”上司很不高兴&#xff0c;对此耿耿于怀。于是&#xff0c;上司处处给你“使绊子”&#xff0c;或不动声色的给你“穿…...