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

【Linux】进程间通信(万字详解)—— 匿名管道 | 命名管道 | System V | 共享内存

🌈欢迎来到Linux专栏~~进程通信


  • (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
  • 目前状态:大三非科班啃C++中
  • 🌍博客主页:张小姐的猫~江湖背景
  • 快上车🚘,握好方向盘跟我有一起打天下嘞!
  • 送给自己的一句鸡汤🤔:
  • 🔥真正的大师永远怀着一颗学徒的心
  • 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
  • 🎉🎉欢迎持续关注!
    在这里插入图片描述

请添加图片描述

文章目录

  • 🌈欢迎来到Linux专栏~~进程通信
    • 一. 进程间通信介绍
    • 二. 管道
      • 🌍匿名管道
        • 😎匿名管道原理
        • 😎创建匿名管道pipe
        • 😎demo代码
        • 😎匿名管道通信的4种情况
          • ✨读阻塞:写快,读慢
          • ✨写阻塞:写慢,读快
          • ✨写端关闭
          • ✨读端关闭
        • 😎管道的大小
      • 🌍命名管道
        • 🎨创建命名管道
        • 🎨基于命名管道通信
      • 🌍 pipe vs fifo
    • 三. System V标准下的进程间通信方式
      • 🌈共享内存
        • 💦共享内存的建立
          • 💛 创建共享内存
          • 💛 控制共享内存
          • 💛 挂接和去关联
          • 💛 shmid 和 key
        • 💦共享内存的进程间通信
        • 💦共享内存与管道进行对比
        • 💦共享内存归属谁
        • 💦共享内存的特征
      • 🌈消息队列(了解)
  • 📢写在最后

请添加图片描述

一. 进程间通信介绍

进程之间会存在特定的协同工作的场景:

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

进程间通信的本质就是,让不同的进程看到同一份资源

进程是具有独立性的。虚拟地址空间+页表 保证了进程运行的独立性(进程内核数据结构+进程代码和数据)

进程通信的前提,首先需要让不同的进程看到同一份“内存”(特定的结构组织)

  • 这块内存应该属于谁呢?为了维持进程独立性,它一定不属于进程A或B,它属于操作系统。

综上,进程间通信的前提就是:由OS参与,提供一份所有通信进程都能看到的公共资源

进程间通信的发展

  • 管道
    • 匿名管道pipe
    • 命名管道pipe
  • System V标准 进程间通信
    • System V 消息队列
    • System V 共享内存
    • System V 信号量
  • POSIX标准 进程间通信(多线程详谈)
    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

二. 管道

什么是管道?

  • 有入口,有出口,都是单向传输资源的(数据)

在这里插入图片描述

所以计算机领域设计者,设计了一种单向通信的方式 —— 管道

🌍匿名管道

众所周知,父子进程是两个独立进程,父子通信也是进程间通信的一种,基于父子间进程通信就是匿名管道。我们首先要对匿名管道有一个宏观的认识

父进程创建子进程,子进程需要以父进程为模板创建自己的files_struct ,而不是与父进程共用;但是struct file这个结构体就不会拷贝,因为打开文件也与创建进程无关(文件的数据不用拷贝)

  • 因为左边是进程相关数据结构,右边是文件相关结构

在这里插入图片描述

😎匿名管道原理

  1. 父进程创建管道,对同一文件分别以读&写方式打开

在这里插入图片描述

  1. 父进程fork创建子进程
    在这里插入图片描述

  2. 因为管道是一个只能单向通信的信道,父子进程需要关闭对应读写端,至于谁关闭谁,取决于通信方向。
    在这里插入图片描述

于是,通过子进程继承父进程资源的特性,双方进程看到了同一份资源。

😎创建匿名管道pipe

pipe谁调用就让以读写方式打开一个文件(内存级文件)

#include <unistd.h>
int pipe(int pipefd[2]);
  • 参数pipefd输出型参数!通过这个参数拿到两个打开的fd
  • 返回值:成功返回0;失败返回-1

数组pipefd用于返回两个指向管道读端和写端的文件描述符:

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

此处提取查一下要用到的函数

  • man2是获得系统(linux内核)调用的用法; man 3 是获得标准库(标准C语言库、glibc)函数的文档
//linux中用man可以查哦
#include <unistd.h>
pid_t fork(void);#include <unistd.h>
int close(int fd);#include <stdlib.h>
void exit(int status);

下面按照之前讲的原理进行逐一操作:①创建管道 ②父进程创建子进程 ③关闭对应的读写端,形成单向信道

#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <assert.h>using namespace std;int main()
{//1.创建管道int pipefd[2] = {0};int n = pipe(pipefd);  //失败返回-1assert(n != -1);  //只在debug下有效(void)n; //仅此证明n被使用过#ifdef DEBUGcout<< "pipefd[0]" << pipefd[0] << endl;  //3cout<< "pipefd[1]" << pipefd[1] << endl;  //4
#endif//2.创建子进程 pid_t id = fork();assert(id != -1);if(id == 0){//子进程//3. 构建单向通信的信道//3.1 子进程关闭写端[1]close(pipefd[1]);exit(0);}//父进程//父进程关闭读端[0]close(pipefd[0]);return 0;
}

在此基础上,我们就要进行通信了,实际上就是对某个文件进行写入,因为管道也是文件,下面提提前查看要用到的函数

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:
- 返回写入的字节数
- 零表示未写入任何内容,这里意味着对端进程关闭文件描述符#include <unistd.h>
unsigned int sleep(unsigned int seconds);

😎demo代码

简单实现了管道通信的demo版本:

#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <assert.h>
#include<sys/types.h>
#include<sys/wait.h>using namespace std;int main()
{//1.创建管道int pipefd[2] = {0};int n = pipe(pipefd);  //失败返回-1assert(n != -1);  //只在debug下有效(void)n; //仅此证明n被使用过#ifdef DEBUGcout<< "pipefd[0]" << pipefd[0] << endl;  //3cout<< "pipefd[1]" << pipefd[1] << endl;  //4
#endif//2.创建子进程 pid_t id = fork();assert(id != -1);if(id == 0){//子进程  - 读//3. 构建单向通信的信道//3.1 子进程关闭写端[1]close(pipefd[1]);char buffer[1024];while(1){size_t s = read(pipefd[0], buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;//因为read是系统调用,没有/0,此处给加上cout<<"child get a message["<< getpid() << "] 爸爸对你说" << buffer << endl;}}//close(pipefd[0]);exit(0);}//父进程 - 写//父进程关闭读端[0]close(pipefd[0]);string message = "我是父进程,我正在给你发消息";int count = 0; //计算发送次数char send_buffer[1024];while(true){//3.2构建一个变化的字符串snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",message.c_str(), getpid(), count);count++;//3.3写入write(pipefd[1], send_buffer, strlen(send_buffer));//此处strlen不能+1//3.4 故意sleepsleep(1);}pid_t ret = waitpid(id, nullptr, 0);assert(ret != -1);(void)ret;return 0;
}

此处有个问题:为什么不定义一个全局的buffer来进行通信呢?

  • 因为有写时拷贝的存在,无法更改通信!

上面的方法就是把数据交给管道,让对方通过管道进行读取

😎匿名管道通信的4种情况

之前父子进程同时向显示器中写入的时候,二者会互斥 —— 缺乏访问控制

而对于管道进行读取的时候,父进程如果写的慢,子进程就会等待读取 —— 这就是说明管道具有访问控制

✨读阻塞:写快,读慢

父进程疯狂的进行写入,子进程隔10秒才读取,子进程会把这10秒内父进程写入的所有数据都一次性的打印出来!

代码如非就是在父进程添加了打印conut,子进程sleep(10),可以自行的在demo代码上添加

在这里插入图片描述

父进程写了1220次,子进程一次就给你读完了,读写之间没有关系,这就叫做流式的服务
也就是管道是面向字节流的,也就是只有字节的概念,究竟读成什么样也无法保证,甚至可能读出乱码,所以父子进程通信也是需要制定协议的,但这个我们网络再细说。。

✨写阻塞:写慢,读快

管道没有数据的时候,读端必须等待:父进程每隔2秒才进行写入,子进程疯狂的读取

请添加图片描述

✨写端关闭

父进程写入10秒,后把写端fd关闭,读端会怎么样?

  • 写入的一方,fd没有关闭,如果有数据就读,没有数据就等
  • 写入的一方,fd关闭了,读取的一方,read会返回0,表示读到了文件结尾,退出读端
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <assert.h>
#include<sys/types.h>
#include<sys/wait.h>using namespace std;int main()
{//1.创建管道int pipefd[2] = {0};int n = pipe(pipefd);  //失败返回-1assert(n != -1);  //只在debug下有效(void)n; //仅此证明n被使用过#ifdef DEBUGcout<< "pipefd[0]" << pipefd[0] << endl;  //3cout<< "pipefd[1]" << pipefd[1] << endl;  //4
#endif//2.创建子进程 pid_t id = fork();assert(id != -1);if(id == 0){//子进程  - 读//3. 构建单向通信的信道//3.1 子进程关闭写端[1]close(pipefd[1]);char buffer[1024*8];while(1){//sleep(10);//20秒读一次//写入的一方,fd没有关闭,如果有数据就读,没有数据就等//写入的一方,fd关闭了,读取的一方,read会返回0,表示读到了文件结尾size_t s = read(pipefd[0], buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;//因为read是系统调用,没有/0,此处给加上cout<<"child get a message["<< getpid() << "] 爸爸对你说" << buffer << endl;}else if (s == 0){cout << "write quit(father), me quit!!!" <<endl;break;}}//close(pipefd[0]);exit(0);}//父进程 - 写//父进程关闭读端[0]close(pipefd[0]);string message = "我是父进程,我正在给你发消息";int count = 0; //计算发送次数char send_buffer[1024*8];while(true){//3.2构建一个变化的字符串snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",message.c_str(), getpid(), count);count++;//3.3写入write(pipefd[1], send_buffer, strlen(send_buffer));//此处strlen不能+1//3.4 故意sleepsleep(1);cout<< count <<endl;if(count == 5){cout<< "父进程写端退出" << endl;break;}}close(pipefd[1]);//关闭读端pid_t ret = waitpid(id, nullptr, 0);assert(ret != -1);(void)ret;return 0;
}

运行结果如下:

请添加图片描述

✨读端关闭

读端关闭,写端继续写入,直到OS终止写进程

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe创建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork创建子进程if (id == 0){//childclose(fd[0]); //子进程关闭读端//子进程向管道写入数据const char* msg = "hello father, I am child...";int count = 10;while (count--){write(fd[1], msg, strlen(msg));sleep(1);}close(fd[1]); //子进程写入完毕,关闭文件exit(0);}//fatherclose(fd[1]); //父进程关闭写端close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)int status = 0;waitpid(id, &status, 0);printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号return 0;
}

运行结果显示,子进程退出时收到的是13号信号

在这里插入图片描述
通过kill -l命令可以查看13对应的具体信号

在这里插入图片描述
由此可知,当发生情况四时,操作系统向子进程发送的是SIGPIPE信号将子进程终止的。

🐋总结上述的4中场景:

  • 写快,读慢,写满了不能再写了
  • 写慢,读快,管道没有数据的时候,读端必须等待
  • 写关,读取的一方,read会返回0,表示读到了文件结尾,退出读端
  • 读关,写继续写,OS终止写进程 ——

🧐由上总结出匿名管道的5个特点 ——

  1. 管道是一个单向通信的通信管道,是半双工通信的一种特殊情况
  2. 管道是用来进行具有血缘关系的进程进行进程间通信 —— 常用于父子通信
  3. 管道具有通过让进程间协同,提供了访问控制!
  4. 管道是 面向字节流 —— 协议(后面详谈)
  5. 管道是基于文件的,管道的声明周期是随进程的

😎管道的大小

管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,那么管道的最大容量是多少呢?

ps:原子性:要么做了,要么不做,没有中间状态

方法1 :man手册查询

在这里插入图片描述
然后我们可以使用uname -r命令,查看自己使用的Linux版本

在这里插入图片描述
我使用的是Linux 2.6.11之后的版本,因此管道的最大容量是65536字节

方法二:自行测试

也就是如果读端一直不读取,写端又不断的写入,当管道被写满后,写端进程就会被挂起。据此,我们可以写出以下代码来测试管道的最大容量。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe创建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork创建子进程if (id == 0){//child close(fd[0]); //子进程关闭读端char c = 'a';int count = 0;//子进程一直进行写入,一次写入一个字节while (1){write(fd[1], &c, 1);count++;printf("%d\n", count); //打印当前写入的字节数}close(fd[1]);exit(0);}//fatherclose(fd[1]); //父进程关闭写端//父进程不进行读取waitpid(id, NULL, 0);close(fd[0]);return 0;
}

写端进程最多写65536字节的数据就被操作系统挂起了,也就是说,我当前Linux版本中管道的最大容量是65536字节

在这里插入图片描述

🌍命名管道

为了解决匿名管道只能在父子之间通信,我们引入命名管道,可以在任意不相关进程进行通信

多个进程打开同一个文件,OS只会创建一个struct_file

在这里插入图片描述

命名管道就是一种特殊类型的文件(可以被打开,但不会将数据刷新进磁盘),两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。

命名管道就是通过唯一路径/文件名的方式定位唯一磁盘文件的

ps:命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像(所以有名字),但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。

🎨创建命名管道

💛 make FIFOs 在命令行上创建命名管道

mkfifo (named pipes)

FIFO:First In First Out 队列呀

在这里插入图片描述

来个小实验:
命令行上执行的命令echocat都是进程,所以这就是通过管道文件进行的进程间通信 ——

在这里插入图片描述
请添加图片描述
💛 那么如何用代码实现命名管道进程间通信的呢?

//查手册:man 3 mkfifo
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);
  • pathname:管道文件路径
  • mode:管道文件权限
  • 返回值:创建成功返回0;创建失败返回-1,并设置错误码

我touch了server.c和client.c,最终希望在serverclient两个进程之间相互通信,先写一个Makefile ——

.PHONY:all
all:client serverclient:client.cxxg++ -o $@ $^ -std=c++11
server:server.cxxg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f client server
  • Makefile自顶向下扫描,只会把第一个目标文件作为最终的目标文件。所以要一次性生成两个可执行程序,需要定义伪目标.PHONY: all,并添加依赖关系

🎨基于命名管道通信

comm.h

我们创建一个共用的头文件,这只是为了两个程序能有看到同一个资源的能力了

#ifndef _COMM_H_ //能避免头文件的重定义
#define _COMM_H_//hpp和.h的区别:.h里面只有声明,没有实现,而.hpp里声明实现都有,后者可以减少.cpp的数量#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>using namespace std;#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc";#endif

server.c

  1. 创建命名管道
  2. 读信息,并实现相应业务逻辑
#include "comm.hpp"int main()
{//1.创建管道文件if(mkfifo(ipcPath.c_str(), MODE) < 0){perror("mkfifo");exit(1);}//2. 正常的文件操作int fd = open(ipcPath.c_str(), O_RDONLY);if(fd < 0){perror("open");exit(2);}//3.编写正常的通信代码char buffer[SIZE];while(1){memset(buffer, '\0', sizeof(buffer));ssize_t s = read(fd, buffer, sizeof(buffer)-1);if(s > 0){cout << "client say >" << buffer << endl;}else if(s == 0){//说明写端关闭了cerr << "read end of file, client quit, server quit too" <<endl;}else{//读取失败perror("read");break;}}//4. 关闭文件close(fd);unlink(ipcPath.c_str());//通信完毕,删除文件return 0;
}

client.c
此时不需要再创建命名管道,只需要获取已打开的命名管道文件

  • 从键盘拿到了待发送数据
  • 发送数据,也就是向管道中写入
#include "comm.hpp"int main()
{//不需要创建fifo,只需获取即可int fd = open(ipcPath.c_str(), O_WRONLY);if(fd < 0){perror("open");exit(1);}//2.ipc通信string buffer;while(1){cout << "Place Enter Message:";std::getline(std::cin, buffer);write(fd, buffer.c_str(), sizeof(buffer));}//3.关闭close(fd);return 0;
}

效果展示:
一定要先运行服务端server创建命名管道,再运行客户端,实现了不相关进程通信 ——

请添加图片描述
如果我想让多个子进程来执行打印任务
在这里插入图片描述
当然我们就要调整一下server.c的业务逻辑:

#include "comm.hpp"
#include <sys/wait.h>static void getMessage(int fd)
{//3.编写正常的通信代码char buffer[SIZE];while(1){memset(buffer, '\0', sizeof(buffer));ssize_t s = read(fd, buffer, sizeof(buffer)-1);if(s > 0){cout << "[" << getpid() << "] " << "client say >" << buffer << endl;}else if(s == 0){//说明写端关闭了cerr << "[" << getpid() << "] " << "read end of file, client quit, server quit too" <<endl;}else{//读取失败perror("read");break;}}
}int main()
{//1.创建管道文件if(mkfifo(ipcPath.c_str(), MODE) < 0){perror("mkfifo");exit(1);}//log("创建管道文件成功", Debug) << "step 1" <<endl;//2. 正常的文件操作int fd = open(ipcPath.c_str(), O_RDONLY);if(fd < 0){perror("open");exit(2);}//log("打开管道文件成功", Debug) << "step 2" <<endl;int nums = 3;for(int i = 0; i < nums; i++){pid_t id = fork();if(id==0){//子进程getMessage(fd);exit(2);}}for(int i = 0; i < nums; i++){waitpid(-1, nullptr, 0);}//4. 关闭文件close(fd);//log("关闭管道文件成功", Debug) << "step 3" <<endl;unlink(ipcPath.c_str());//通信完毕,删除文件//log("删除管道文件成功", Debug) << "step 4" <<endl;return 0;
}

🌍 pipe vs fifo

为什么pipe叫做匿名管道和和fifo叫做命名管道?

  • 匿名管道文件属于内存级的文件,不需要名字,因为它是通过父子继承的方式看到同一份资源
  • 命名管道一定要有名字,从而使不相关进程通过唯一路径定位同一个文件

三. System V标准下的进程间通信方式

下面我们要学习System V标准,是在同一主机内的进程间通信方案,是站在OS层面,专门为进程间通信设计的方案。

进程通信的本质是先让不同进程看到同一份资源,System V提供了这三个主流方案 ——

  • 共享内存 - 传递数据
  • 消息队列(有点落伍) - 传递数据
  • 信号量 (多线程讲POSIX标准) - 实现进程同步&控制详谈

🌈共享内存

基于共享内存进行进程间通信原理 ——

  1. 首先在物理内存当中申请一块内存空间,将这块内存空间分别与各个进程各自的页表之间建立映射
  2. 进程虚拟地址空间当中开辟空间(共享内存)并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系
  3. 所以两个进程便看到了同一份物理内存,这块物理内存就叫做共享内存

在这里插入图片描述

💦共享内存的建立

共享内存提供者是操作系统OS,那么操作系统要不要管理共享内存呢? -> 先描述再组织

共享内存 = 共享内存块 + 对应的共享内存的内核数据结构来描述其属性

💛 创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);

参数:

  • key:为了使不同进程看到同一段共享内存,即让不同进程拿到同一个ID,需要由用户自己设定,但如何设定的与众不同好难啊,就要借助下面这个函数。
    在这里插入图片描述所以怎么样保证两个进程拿到同一个key值呢?

    #include <sys/types.h>
    #include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
    
    • pathname:自定义路径名
    • proj_id:自定义项目ID
    • 返回值:成功后,返回生成的key_t值。失败时返回1
  • szie共享内存的大小,建议是4KB的整数倍,因为共享内存在内核中申请的基本单位是页(内存页)。

  • shmflg标记位,这一看就是宏,都是只有一个比特位是1且相互不重复的数据,这样|在一起,就能传递多个标志位

    • IPC_CREAT:如果单独使用IPC_CREAT或者flg为0,如果创建共享内存时,底层已经存在,获取之;如果不存在,就创建之
    • IPC_EXCL单独使用没有意义,通常要搭配起来IPC_CREAT | IPC_EXCL,如果底层不存在,就创建,并返回;如果底层存在就出错返回。这样的意义在于 如果调用成功,得到的一定是一个全新的共享内存。

返回值:成功后,将返回有效的共享内存标识符。失败了,返回-1,并设置errno错误码。

💛 控制共享内存

手动查看与手动删除

ipcs -m 查看ipc资源,不带选项默认查看消息队列(-q)、共享内存(-m)、信号量(-s)
ipcrm -m + shmid //删除共享内存

system V IPC资源,生命周期随内核!所以我们要手动 / 自动删除,那怎么样自动删除呢?

💛 控制共享内存

#include <sys/ipc.h> 
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数:

  • cmd:设置IPC_RMID就行,IPC_RMID:即便是有进程和当下的shm挂接,依旧删除共享内存(强大)
  • buf:这就是描述共享内存的数据结构啊!
    在这里插入图片描述
    返回值:失败返回-1,成功返回0
💛 挂接和去关联

attach 挂接 ——

#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmaddr:挂接到什么位置,我们也不知道,给NULL,让操作系统来设置
  • shmflg: 给0

最重要的是返回值

  • 这个地址一定是虚拟地址,类似malloc返回申请到的起始地址
  • 失败返回-1,并设置错误码

detach 去关联 ——

int shmdt(const void *shmaddr);
  • shmaddr:shmat返回的地址

注意:去关联,不是释放共性内存,而是取消当前进程和共享内存的关系,本质是去掉进程和物理内存构建映射关系的页表项去掉

返回值:成功返回0,失败返回-1

💛 shmid 和 key

只有创建的时候用key,大部分用户访问共享内存,都用的是shmid(用户层)

💦共享内存的进程间通信

comm.h

#pragma one#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "log.hpp"using namespace std;//不推荐#define PATH_NAME "/home/ljj"
#define PROJ_ID 0x66

server.c

  1. 创建公共的key

  2. 创建共享内存 - 建议创建一个全新的共享内存:因为是通信的发起者
    带选项IPC_CREAT | IPC_EXCL若和系统中已经存在的ID冲突,则出错返回;
    注意到其中权限perm是0,那也可以设置一下

    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); 
    

    在这里插入图片描述

  3. 将指定的共享内存,挂接到自己的地址空间上

  4. 将指定的共享内存,从自己的地址空间去关联

  5. 删除共享内存

#include "comm.hpp"string TransToHex(key_t k)
{char buffer[32];snprintf(buffer, sizeof(buffer), "0x%x", k);return buffer;
}int main()
{//1.创建公共的key值key_t k = ftok(PATH_NAME, PROJ_ID);assert(k != -1);Log("create key done", Debug) << "server key : " << TransToHex(k) << endl;//2. 创建共享内存  - 建议创建一个全新的共享内存:因为是通信的发起者int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("shmget");exit(1);}Log("creat shm done", Debug) << "shmid : " << shmid << endl;sleep(10);//3.将指定的共享内存,挂接到自己的地址空间上char *shmaddr = (char*)shmat(shmid, nullptr, 0);Log("attach shm done", Debug) << "shmid : " << shmid << endl;sleep(10); //这里就是通信的代码//4.将指定的共享内存,从自己的地址空间去关联int n = shmdt(shmaddr);assert(n != -1);(void)n;Log("detach shm done", Debug) << "shmid : " << shmid << endl;sleep(10); //5.删除共享内存n = shmctl(shmid, IPC_RMID, nullptr);assert(n != -1);(void)n;Log("delete shm done", Debug) << "shmid : " << shmid << endl;return 0;
}

关于申请共享内存的大小size,我们说建议是4KB的整数倍,因为共享内存在内核中申请的基本单位是页(内存页),4KB。如果我申请4097Byte大小的空间,内核会向上取整给我4096* 2Byte,诶?那我监视到的↑怎么还是4097啊!虽然在底层申请到的是4096*2,但不会多给你,这样也可能引起错误~

client.c

  • 只需获取共享内存;不用删除
#include "comm.hpp"int main()
{key_t k = ftok(PATH_NAME, PROJ_ID);if(k < 0){Log("create key failed", Error) << "client key : " << k << endl;exit(1);}Log("create key done", Debug) << "client key : " << k << endl;//获取共享内存int shmid = shmget(k, SHM_SIZE, IPC_CREAT);if(shmid < 0){Log("create shm failed", Error) << "client key : " << k << endl;exit(2);}Log("attach shm success", Error) << "client key : " << k << endl;sleep(10);//挂接地址char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(shmaddr == nullptr){Log("attach shm failed", Error) << "client key : " << k << endl;exit(3);}Log("attach shm success", Error) << "client key : " << k << endl;sleep(10);//使用//去关联int n = shmdt(shmaddr);assert(n != -1);Log("datach shm success", Error) << "client key : " << k << endl;sleep(10);//你只管用,不需要删除共享内存return 0;
}

效果展示:
写一个命令行脚本来监视共享内存 ——

while :; do ipcs -m; echo "_________________________________________________________________"; sleep 1; done

注意观察nattch这个参数的变化:0->1->2->1->0

请添加图片描述

上面的框架都搭建好了之后,接下来就是通信部分:
1️⃣客户端不断向共享内存写入数据:

//client将共享内存看成一个char类型的buffer
char a = 'a';
for(; a <= 'z'; a++)
{//每一次都想共享内存shmaddr的起始地址snprintf(shmaddr, SHM_SIZE - 1,\"hello server, 我是其他进程, 我的pid: %d, inc: %c\n",\getpid(), a);sleep(2);
}

2️⃣服务端不断读取共享内存当中的数据并输出:

//将共享内存当成一个大字符串
for(;;)
{printf("%s\n", shmaddr);sleep(1);
}

结果如下:
在这里插入图片描述

ps:我们发现即使我们没有向server端发消息,server也是不断的在读取信息的

💦共享内存与管道进行对比

共享内存是所有进程间通信方式中最快的一种通信方式。

在这里插入图片描述
将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:

我们再来看看共享内存通信:

在这里插入图片描述
键盘写入shm,另一端可以直接获取到,哪里还需要什么拷贝?最多两次拷贝(键盘输入一次,输出到外设一次)

💦共享内存归属谁

共享内存的区域是在OS内核?还是在用户空间?

  • 用户空间!

其中文本、初始化数据区、未初始化数据区、堆、栈、环境变量、命令行参数、再 往上就是1GOS内核,其中剩余3G都是用户自己支配的

用户空间:不用经过系统调用,直接进行访问!

在这里插入图片描述

  • 所以双方进程如果要进行通信,直接进行内存级的读和写(减少了许多拷贝)

那为什么之前将的pipe和fifo都要通过read、write进行通信,为什么呢?

因为管道双方看到的资源都属于内核级的文件,我们无权直接进行访问,必须调用系统接口

💦共享内存的特征

  • 共享内存的生命周期随内核
  • 共享内存是所有进程中速度最快的,只需要经过页表映射,不需来回拷贝(不经过OS)
  • 共享内存没有提供访问控制,读写双方根本不知道对方的存在,会带来并发问题

🌈消息队列(了解)

严重过时:接口与文件不对应

创建消息队列,与创建共享内存极其相似:

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

删除消息队列:

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

我们可以通过key找到同一个共享内存。

我们发现共享内存、消息队列、信号量的 ——

  • 接口都类似
  • 数据结构的第一个结构类型struct ipc_perm是完全一致的!

我们由shmid申请到的都是01234… 大胆推测,在内核中,所有的ipc资源都是通过数组组织起来的。可是描述它们的结构体类型并不相同啊?但是~ System V标准的IPC资源,xxxid_ds结构体的第一个成员都是ipc_perm都是一样的。

📢写在最后

应该是我写过最长的一篇博客了
请添加图片描述

相关文章:

【Linux】进程间通信(万字详解)—— 匿名管道 | 命名管道 | System V | 共享内存

&#x1f308;欢迎来到Linux专栏~~进程通信 (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort目前状态&#xff1a;大三非科班啃C中&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自己的一句鸡汤…...

【Database-02】达梦数据库 - DM Manager管理工具安装

1、简介 DM Manager是达梦数据库自带的图形化界面管理工具&#xff0c;在安装达梦数据库的时候就会自动安装。 Linux环境&#xff0c;默认安装路径为&#xff1a;达梦安装目录/tool/manager&#xff0c;如果Linux是安装GUI&#xff0c;那么就可以直接启动使用。 实际大部分使…...

剑指 Offer 42. 连续子数组的最大和

剑指 Offer 42. 连续子数组的最大和 难度&#xff1a;easy\color{Green}{easy}easy 题目描述 输入一个整型数组&#xff0c;数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。 要求时间复杂度为O(n)。 示例1: 输入: nums [-2,1,-3,4,-1,2,1,-5,4] 输…...

双指针 (C/C++)

1. 双指针 双指针算法的核心思想&#xff1a;将暴力解法的时间复杂度&#xff0c;通常是O(N*N)&#xff0c;通过某种特殊的性质优化到O(N)。 做题思路&#xff1a;先想想暴力解法的思路&#xff0c;然后分析这道题的特殊性质&#xff0c;一般是单调性。然后得出双指针算法的思路…...

CVE-2023-23752 Joomla未授权访问漏洞分析

漏洞概要 Joomla 在海外使用较多&#xff0c;是一套使用 PHP 和 MySQL 开发的开源、跨平台的内容管理系统(CMS)。 Joomla 4.0.0 至 4.2.7 版本中的 ApiRouter.php#parseApiRoute 在处理用户的 Get 请求时未对请求参数有效过滤&#xff0c;导致攻击者可向 Joomla 服务端点发送包…...

单通道说话人语音分离——Conv-TasNet(Convolutional Time-domain audio separation Network)

单通道说话人语音分离——Conv-TasNet模型(Convolutional Time-domain audio separation Network) 参考文献&#xff1a;《Conv-TasNet: Surpassing Ideal Time-FrequencyMagnitude Masking for Speech Separation》 1.背景 在真实的声学环境中&#xff0c;鲁棒的语音处理通常…...

华为OD机试真题Python实现【环中最长子串】真题+解题思路+代码(20222023)

环中最长子串 题目 给你一个字符串s,首尾相连成一个环形, 请你在环中找出o字符出现了偶数次最长子字符串的长度. 备注: 1 <= s.lenth <= 5x10^5 s只包含小写英文字母 🔥🔥🔥🔥🔥👉👉👉👉👉👉 华为OD机试(Python)真题目录汇总 ## 输入 输入是…...

Netcat安装与使用(nc)

Netcat安装与使用1.Netcat简介1.1.Netcat安装1.1.1.安装整体流程1.1.1.1.安装依赖1.1.1.2.安装Netcat1.1.1.3.配置环境变量1.1.1.4.测试1.2.Netcat基本功能1.3.Netcat常用参数2.Netcat用法2.1.前期准备2.2.banner相关信息抓取2.3.端口扫描2.3.1.扫描指定端口2.3.2.扫描指定端口…...

蓝桥杯:聪明的猴子

题目链接&#xff1a;聪明的猴子https://www.lanqiao.cn/problems/862/learning/ 目录 题目描述 输入描述 输出描述 输入输出样例 运行限制 解题思路&#xff1a; 最小生成树 AC代码&#xff08;Java&#xff09;: 课后练习&#xff1a; 题目描述 在一个热带雨林中生存…...

Spring Boot应用如何快速接入Prometheus监控

1. Micrometer简介Micrometer为Java平台上的性能数据收集提供了一个通用的API&#xff0c;它提供了多种度量指标类型&#xff08;Timers、Guauges、Counters等&#xff09;&#xff0c;同时支持接入不同的监控系统&#xff0c;例如Influxdb、Graphite、Prometheus等。可以通过M…...

vscode远程调试python

目的 注意&#xff1a;这里我们想要实现的是&#xff1a;用vscode 使用remote ssh打开project&#xff0c;然后直接在project里面进行debug&#xff0c;而不需要 在本地vscode目录打开一样的project。 假设大家已经会使用remote ssh打开远程服务器的代码了&#xff0c;那么只…...

Spring Boot 框架 集成 Knife4j(内含源代码)

Spring Boot 框架 集成 Knife4j&#xff08;内含源代码&#xff09; 源代码下载链接地址&#xff1a;https://download.csdn.net/download/weixin_46411355/87480176 目录Spring Boot 框架 集成 Knife4j&#xff08;内含源代码&#xff09;源代码下载链接地址&#xff1a;[htt…...

什么蓝牙耳机适合打游戏?打游戏不延迟的蓝牙耳机

为了提升游戏体验&#xff0c;除了配置强悍的主机外&#xff0c;与之搭配蓝牙耳机等外设产品也尤为重要&#xff0c;今天就带大家来了解一下以下几款适合玩游戏&#xff0c;低延迟操作的蓝牙耳机。 第一款&#xff1a;南卡小音舱蓝牙耳机 参考价格&#xff1a;239元 推荐理由…...

【项目设计】高并发内存池(一)[项目介绍|内存池介绍|定长内存池的实现]

&#x1f387;C学习历程&#xff1a;入门 博客主页&#xff1a;一起去看日落吗持续分享博主的C学习历程博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 也许你现在做的事情&#xff0c;暂时看不到成果&#xff0c;但不要忘记&…...

初识MySQL下载与安装【快速掌握知识点】

目录 前言 MySQL版本 MySQL类型 MySQL官网有.zip和.msi两种安装形式&#xff1b; MySQL 下载 1、MySQL 属于 Oracle 旗下产品&#xff0c;进入Oracle官网下载 2、点击产品&#xff0c;找到MySQL 3、进入MySQL页面 4、点击Download&#xff08;下载&#xff09;&#x…...

如何终止一个线程

如何终止一个线程 是使用 thread.stop() 吗&#xff1f; public class ThreadDemo extends Thread{Overridepublic void run() {try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("this is demo thread :"Thre…...

上岸!选择你的隐私计算导师!

开放隐私计算 开放隐私计算开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神&#xff0c;专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播&#xff0c;愿成为中国 “隐私计算最后一公里的服务区”。183篇原创内容公众号…...

go gin学习记录5

有了前面几节的学习&#xff0c;如果做个简单的web服务端已经可以完成了。 这节来做一下优化。 我们实验了3种SQL写入的方法&#xff0c;但是发现每一种都需要在方法中去做数据库链接的操作&#xff0c;有些重复了。 所以&#xff0c;我们把这部分提取出来&#xff0c;数据库链…...

PyQt5数据库开发2 5.1 QSqlQueryModel

目录 一、Qt窗体设计 1. 新建Qt项目 2. 拷贝4-3的部分组件过来 3. 添加资源文件 4. 创建Action 5. 添加工具栏 6. 创建菜单项 7. 关闭Action的实现 8. 调整布局 8.1 调整两个groupbox的布局 8.3 为窗体设置全局布局 二、代码拷贝和删除 1. 新建项目目录 2. 编译…...

MySQL-redo log和undo log

什么是事务 事务是由数据库中一系列的访问和更新组成的逻辑执行单元 事务的逻辑单元中可以是一条SQL语句&#xff0c;也可以是一段SQL逻辑&#xff0c;这段逻辑要么全部执行成功&#xff0c;要么全部执行失败 举个最常见的例子&#xff0c;你早上出去买早餐&#xff0c;支付…...

阿里云ECS TOP性能提升超20%!KeenTune助力倚天+Alinux3达成开机即用的全栈性能调优 | 龙蜥技术

文/KeenTune SIG01阿里云 ECS 上售卖页新增“应用加速”功能2023年1月12日 阿里云 ECS 的售卖页有了一些新的变化&#xff0c;在用户选择倚天 Alinux3 新建实例时&#xff0c;多了一个新的选项“应用加速”。这个功能是 阿里云 ECS 基于 KeenTune 提供典型云场景的开机即用的全…...

华为OD机试真题Python实现【快递业务站】真题+解题思路+代码(20222023)

快递业务站 题目 快递业务范围有 N 个站点,A 站点与 B 站点可以中转快递,则认为 A-B 站可达, 如果 A-B 可达,B-C 可达,则 A-C 可达。 现在给 N 个站点编号 0、1、…n-1,用 s[i][j]表示 i-j 是否可达, s[i][j] = 1表示 i-j可达,s[i][j] = 0表示 i-j 不可达。 现用二维…...

【c语言】预处理

&#x1f680;write in front&#x1f680; &#x1f4dc;所属专栏&#xff1a;> c语言学习 &#x1f6f0;️博客主页&#xff1a;睿睿的博客主页 &#x1f6f0;️代码仓库&#xff1a;&#x1f389;VS2022_C语言仓库 &#x1f3a1;您的点赞、关注、收藏、评论&#xff0c;是…...

嵌入式常用知识

12、并发和并行的区别&#xff1f; 最本质的区别就是&#xff1a;并发是轮流处理多个任务&#xff0c;并行是同时处理多个任务。 你吃饭吃到一半&#xff0c;电话来了&#xff0c;你一直到吃完了以后才去接&#xff0c;这就说明你不支持并发也不支持并行。 你吃饭吃到一半&…...

和平精英五曜赐福返场,老款玛莎返场来了

和平精英五曜赐福返场&#xff0c;老款玛莎返场来了&#xff01;新款如何选择&#xff01; 关于返场的新消息&#xff0c;都说云南百收SEO解说消息不准&#xff0c;之前看过文章的应该会知道&#xff0c;全网只有云南百收SEO解说发了。玛莎返场&#xff0c;快喊你的阿姨来看&a…...

React从入门到精通二

React从入门到精通之购物车案例1. 购物车需求说明使用到的data list2. 项目code1. 购物车需求说明 list data展示到列表中每个item的通过按钮来控制购买的数据量删除按钮可以删除当前的itemTotal Price计算当前购物车的总的价格 使用到的data list const books [{id: 1,name…...

【likeshop多商户】电子面单商家直播上线啦~

likeshop多商户商城v2.2.0版本更新啦&#xff01; 新增功能&#xff1a; 商家直播 单子面单 优化&#xff1a; 个人中心优惠券数量统计优化 修复&#xff1a; 秒杀商品待审核时&#xff0c;下单价格计算错误 个人中心修改头像后地址保存错误 「商家直播」 提升品牌知名度…...

游戏化销售管理是什么?使用CRM系统进行有什么用?

对于企业销售来说&#xff0c;高薪酬也伴随着更高的压力与挑战。高强度的单一工作会让销售人员逐渐失去对工作的兴趣&#xff0c;导致销售状态缺少动力和激情&#xff0c;工作开展愈加困难。您可以通过CRM系统进行游戏化销售管理&#xff0c;让销售人员重新干劲满满。 游戏并不…...

Mysql 索引(三)—— 不同索引的创建方式(主键索引、普通索引、唯一键索引)

了解了主键索引的底层原理&#xff0c;主键索引其实就是根据主键字段建立相关的数据结构&#xff08;B树&#xff09;&#xff0c;此后在使用主键字段作为条件查询时&#xff0c;会直接根据主键查找B树的叶子结点。除了主键索引外&#xff0c;普通索引和唯一键索引也是如此&…...

秒懂算法 | 基于朴素贝叶斯算法的垃圾信息的识别

本文将带领大家亲手实现一个垃圾信息过滤的算法。 在正式讲解算法之前,最重要的是对整个任务有一个全面的认识,包括算法的输入和输出、可能会用到的技术,以及技术大致的流程。 本任务的目标是去识别一条短信是否为垃圾信息,即输入为一条文本信息,输出为二分类的分类结果。…...