跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Linux 进程间通信进阶:管道与共享内存详解

Linux 进程间通信主要包含管道和共享内存。匿名管道用于父子进程单向通信,命名管道允许无关进程交互。共享内存通过内核映射实现高速数据共享,但需处理同步与权限。文中详细阐述了 shmget、shmat、shmdt、shmctl 等系统调用的使用,结合 C++ 代码演示了进程池构建与共享内存读写流程,强调了 key 值生成、内存对齐及资源清理的重要性。

暖阳发布于 2026/3/30更新于 2026/10/2072 浏览
Linux 进程间通信进阶:管道与共享内存详解

Linux 进程间通信进阶

1. 准备阶段:进程间通信的概念

1.1 是什么(本质前提)

进程间通信指的是两个或者多个进程进行信息相互传递的过程。

我们知道:

  • 进程是具有独立性的!
  • 进程 = 内核数据结构 + 代码和数据

一个进程想把自己的数据,发送给另一个进程,至少在目前是一件比较困难的事情!

那父进程的全局变量,子进程可以看到不算嘛?不算进程间通信——

  • 只能父进程~>子进程不是相互的(信息传递)
  • 不能持续,后续更改了,子进程也就看不到了!

一个进程挂掉不会影响另外的进程,就算是父子进程也一样!

1.2 为什么

为什么要进行进程间通信,实际上就是要回答进程间通信的目的!

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

1.3 怎么办

进程间通信的本质前提:先让不同的进程,看到同一份资源!

我们后续进行进程间通信的时候,大部分时候,都是想办法看到同一份资源!

要让不同的进程看到同一份资源,OS 必然要提供系统调用!

1.4 思维导图

原理思维导图


2. 进程间通信

2.1 进程间通信的定制标准:System V

相对于网络的标准来说,系统的标准就没那么严格了。

在我们的日常生活当中,其实也是有很多标准的,这些标准也不是一开始就可以决定的,有一个发展的过程。

2.2 进程间通信的发展

发展图


3. 管道的特点和情况总结

3.1 五种特点

我们总结一下管道的五大特点:

  • 1、管道在设计之初,只允许进行单项数据通信。因此,管道特点之一:基于文件的,单向数据通信。
  • 2、管道只能用来让'具有血缘关系'进程,进行进程间通信,常用于父子进程之间,进行进程间通信。
  • 3、管道的本质是文件,一般文件,如果打开它的进程退出了,那么文件也会被系统自动关闭!打开的文件的生命周期随进程(进程退出,生命周期结束)。
  • 4、管道是自己内部实现了:进程间的同步。
  • 5、管道是面向字节流的!~~> 读写次数不匹配。

3.2 四种情况

  • 场景 1:写端很慢,读端很快,以慢的节奏来 ---- 父进程,等待数据就绪,即等待子进程写入
  • 场景 2:写端很快,读端很慢,读端就会把写端写入的数据一次都能读上来,全部读上去!
  • 场景 3:写端不写,close(wfd),读端会怎么办?
  • 场景 4:写端一直在写,读端不读 &&(并且)close(rfd)。

3.3 思维导图

管道思维导图


4. 进程池

4.1 有一个小 Bug

alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/2_12$ ll total 24 
drwxrwxr-x 6 alice alice 4096 Feb 13 15:35 ./ 
drwxrwxr-x 27 alice alice 4096 Feb 12 10:42 ../ 
drwxrwxr-x 2 alice alice 4096 Feb 12 12:56 1.test/ 
drwxrwxr-x 2 alice alice 4096 Feb 12 13:23 2.test/ 
drwxrwxr-x 2 alice alice 4096 Feb 13 21:10 3.process_pool/ 
drwxrwxr-x 2 alice alice 4096 Feb 13 15:35 4.my_process_pool/
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/2_12$ cd 3.process_pool
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/2_12/3.process_pool$ make
g++ -o process_pool process_pool.cc -std=c++14
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/2_12/3.process_pool$ ll total 100 
drwxrwxr-x 2 alice alice 4096 Mar 10 17:43 ./ 
drwxrwxr-x 6 alice alice 4096 Feb 13 15:35 ../ 
-rw-rw-r-- 1 alice alice 95 Feb 13 20:52 Makefile 
-rwxrwxr-x 1 alice alice 80936 Mar 10 17:43 process_pool* 
-rw-rw-r-- 1 alice alice 7258 Feb 14 00:26 process_pool.cc
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/2_12/3.process_pool$ ./process_pool
Usage: ./process_pool process_number alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/2_12/3.process_pool$

我们怎么体现进程间通信的协同?

下面这个函数是子进程的入口函数——

任务是什么?我们以任务码的形式体现——

由此我们形成一张任务表。

至此,有一个任务清单,一个数组,数组下标就是任务。

通道轮询式的被遍历了——

用 Close() 关闭写端文件描述符,用 Wait() 等待子进程、回收僵尸进程——

修改一下代码,运行一下,我们发现这里有一个 Bug!

我们只退出了一个子进程,并没有退出所有的子进程(这里有 10 个),我们查看一下之前写的代码,发现问题就处在这里——

我们只要加一段代码就行——

再运行一下,通道被关闭成功,子进程全部回收——

4.2 进程池:以创建 10 个子进程为例

进程池示例


5. 命名管道

5.1 客户端不需要建立管道

客户端无需建立

5.2 打开和关闭管道之间,客户端可以进行通信

通信窗口

5.3 不需要写 C 语言的\0

无需 null

5.4 毫不相干的两个进程的父进程可能不一样

无关进程

5.5 目前完成的服务端和客户端代码

5.5.1 服务器端 server.cc
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FIFO_NAME "fifo"

int main() {
    // 创建 FIFO(如果已存在则忽略错误)
    if (mkfifo(FIFO_NAME, 0666) == -1) {
        perror("mkfifo");
        // 如果文件已存在,可以继续,但最好确认是 FIFO
    }
    printf("Server waiting for client...\n");
    int fd = open(FIFO_NAME, O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(1);
    }
    printf("Client connected.\n");
    char buf[1024];
    ssize_t n;
    while ((n = read(fd, buf, sizeof(buf)-1)) > 0) {
        buf[n] = '\0';
        printf("client say# %s", buf);
        // 假设消息自带换行,否则可加换行
        fflush(stdout);
    }
    if (n == -1) perror("read");
    close(fd);
    unlink(FIFO_NAME); // 可选:删除 FIFO
    return 0;
}
5.5.2 客户端 client.cc
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FIFO_NAME "fifo"

int main() {
    printf("Please Enter@");
    fflush(stdout); // 确保提示立即显示
    char msg[1024];
    if (fgets(msg, sizeof(msg), stdin) == NULL) {
        perror("fgets");
        exit(1);
    }
    // 以只写方式打开 FIFO(会阻塞直到有读者)
    int fd = open(FIFO_NAME, O_WRONLY);
    if (fd == -1) {
        perror("open");
        exit(1);
    }
    write(fd, msg, strlen(msg));
    close(fd);
    return 0;
}
5.5.3 Makefile 得这样写
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
5.5.4 运行:服务端~>客户端

像这样一来就可以了——

先打开 ./server,再打开 ./client——

也就是说先打开服务器,再打开客户端。 这样就可以完成毫不相干的进程之间的单向通信了!

通信完成

5.6 最终呈现的服务端和客户端代码

5.6.1 服务器端 server.cc
#include <iostream>
#include <cstdio> // 混编
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "comm.h"

int main() {
    std::cout << "open begin" << std::endl;
    // int wfd = open(fifoname.c_str(),O_WRONLY);
    int wfd = open("fifo", O_WRONLY);
    if (wfd < 0) {
        perror("open");
        return 1;
    }
    std::cout << "open end" << std::endl;
    std::string outstring;
    while (true) {
        std::cout << "Please Enter@ ";
        std::cin >> outstring;
        write(wfd, outstring.c_str(), outstring.size());
        // 要不要写\0?不需要写!
    }
    close(wfd);
    return 0;
}
5.6.2 客户端 client.cc
#include <iostream>
#include <cstdio> // 混编
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "comm.h"

int main() {
    // 1.创建管道文件
    umask(0); // 关闭系统的权限,用我们指定的权限
    int n = mkfifo(fifoname.c_str(), 0666); // 系统调用 mkfifo
    if (n < 0) {
        if (errno != EEXIST) {
            perror("mkfifo");
            return 1;
        }
        // 文件已存在,忽略错误,继续执行
    }
    // 2.打开管道文件
    std::cout << "open begin" << std::endl;
    int rfd = open(fifoname.c_str(), O_RDONLY);
    if (rfd < 0) {
        perror("open");
        return 2;
    }
    std::cout << "open end" << std::endl;
    char inbuffer[1024];
    // 3、进行通信
    while (true) {
        ssize_t n = read(rfd, inbuffer, sizeof(inbuffer)-1);
        if (n > 0) {
            inbuffer[n] = 0;
            std::cout << "client say# " << inbuffer << std::endl;
        } else if (n == 0) { // 写端关闭了
            break;
        } else {
            perror("read");
            break;
        }
    }
    // 4.关闭
    close(rfd);
    // 5.删除管道文件
    unlink(fifoname.c_str());
    return 0;
}
5.6.3 运行:服务端~>客户端

运行截图

5.7 命名管道的本质

命名管道的本质其实就是一种符号,让不同的进程找到同一个文件,只需要存在一个 inode 就可以,不用刷新到磁盘。有缓冲区就行,做刷新时看到是以 p 为开头的就不会刷新,等另外的进程来读取。

5.8 命名管道打开的细节问题

在使用命名管道的时候,不用担心第一次读的时候读不到东西,在首次打开的时候读端会在 open 阻塞。在 open 的时候就已经进行了让读写同步。server 端变成写端也是一样的,都要保持这个同步的。

5.9 命名管道思维导图

如下图所示——

5.9.1 匿名管道(进程池)+(开了个头)命名管道

命名管道思维导图

5.10 管道的应用场景

  • 匿名管道——竖划线。
  • 命名管道——应用场景相对比较少,举俩例子,一个是实现类似于两个进程互传文件的逻辑,还有一个就如下图所示。

应用场景

5.10.1 命名管道实现文件拷贝

我们也可以把这个例子换成跟网络和数据库关联,并且大家发现这个结构,有点像进程池的,命名管道也可以用来实现进程池。 实现类似于两个进程互传文件的逻辑——

文件拷贝

5.10.2 定标准

技术上的问题解决了,下一个矛盾就是怎么让更多人用。 怎么解决这个问题呢,定标准! 朴素点的理解:函数是什么,返回值是什么,结构化的字段怎么定义等等。管你是什么系统,都遵守这个标准。用户不管用的啥都只用学一套就行了。

  • 定标准——先不着急设计进程间通信的技术,先定标准 社会层面上,只需要提交一份标准,用户也只需要学习一套通信标准即可。
5.10.3 System V 标准
  • System V 标准。
    • 共享内存:用来进行大块数据通信
    • 消息队列:
    • 信号量:可以用来实现锁,多进程互斥和同步等

这三种通信技术上在使用层面已经过时了!但是共享内存还是需要讲一下的!信号量(编码工作)会交待原理。消息队列就不讲了——标准在很多地方很类似。 但是现在比较过时,现在也包装的比较好了。现在都可以网络通了,我们就讲讲共享内存就行;信号量只讲原理,线程还会再讲讲;消息队列基本就不讲了。

System V


6. 共享内存

共享内存有一个非常重要的东西!我们后面细嗦!

6.1 原理

6.1.1 原理思维导图
  • 虚拟地址对物理内存进行读写
  • 页表和虚拟地址进行映射

让不同的进程看到同一份资源——动态库就是这样共享的!

原理图

6.1.2 原理理论

我们在物理内存中如果也能开辟一块内存空间 整个物理内存的起始地址和大小我就知道了同时映射到进程 A 的共享区,得到一个内存块的起始虚拟地址,我们就可以直接通过虚拟地址对这块内存进行读写了所以进程 B 也可以啊,我也搞一块区域,建议共享内存和虚拟地址空间的映射,都得到了各自的虚拟地址,再加上偏移量就可以访问内存中的任何的区域。我们肯定能做到,动态库不就是共享区这样嘛。我们通过地址空间映射,让不同的进程看到了同一个内存块,这种技术就叫共享内存!

6.1.3 共享内存与进程地址空间布局
argu,environ 栈 共享内存、内存映射和 共享库位于此处 为堆扩展保留 堆 未初始化数据(bss) 初始化数据 文本(程序代码) 

原图如下所示——

地址布局

文本(程序代码):存放可执行代码,只读。初始化数据:已初始化的全局变量和静态变量。未初始化数据(bss):未初始化的全局变量和静态变量,在程序执行前会被清零。堆:用于动态内存分配(如 malloc),向高地址增长。为堆扩展保留:堆和共享区之间的空闲区域,供堆向上增长。共享内存、内存映射和共享库位于此处:即内存映射区,用于映射共享库、文件映射、匿名映射等,通常从高地址向低地址增长(或固定位置)。栈:存放局部变量、函数调用信息,向低地址增长。argu, environ:命令行参数和环境变量,位于栈的高地址顶部

堆和栈相对而生,动态库映射到共享区就可以被多个进程共享。

堆和栈相对生长(堆向上,栈向下),而动态库被映射到共享内存区,这样它们在物理内存中只需加载一份,多个进程的页表可以映射到同一物理页,从而节省内存并实现代码共享。

6.1.4 问题
1、整个过程是谁做的?

操作系统 OS,那是谁让 OS 做的呢,操作系统必然会提供系统调用,我们程序员就可以调用系统调用——所以是用户让操作系统做的!

2、共享区这个东西用户可以直接访问嘛?用户可以不需要使用系统调用来读写共享内存(指针)
  • 用户用指针就可以直接访问了! 共享区这个不属于内核空间,是属于用户空间的。所以我们用户随便拿个指针就可以直接访问了!意味着我们也可以不需要使用系统调用来读写 shm(共享内存)。我们之前使用动态库也可以没系统调用。
  • 总结:创建和'删除'shm 需要系统调用,使用 shm 不需要 (类似 malloc())。 我们创建管道的时候用了系统调用,使用也用了,这也是和共享内存的差别。用户空间最有代表的就是用户可以直接访问。

用户空间

  • 用户空间有 3G,这些用户都可以直接访问!

3G 空间

访问库函数也是直接访问使用

我们访问库函数也是虚拟地址结合.GOT 表就可以直接访问使用。

去关联
  • 如果我们不想用这个共享内存了怎么办,我们要先去掉页表的映射关系,这个过程叫做去关联。
6.1.5 思路打开——突破点

需要通信时,不同的进程都想用共享内存通信啊!

  • 内存的存在是可以同时存在多份的 共享内存有不同状态的,删除,创建啥的,操作系统必然要管理共享内存,怎么管理呢,先描述再组织——老生常谈了。 一定会存在一种共享内存的结构体,描述共享内存的信息等等——

结构体

肯定有链接信息,我们后面有很多的共享内存结构体对象,就可以用链表啥的组织管理起来了,思路都很类似了,我们后面会介绍的(底层用的数组管理)。

  • 不申请内存块都可以——虚拟地址空间的延迟申请。
  • 底层用数组把结构体变量统一管理起来
  • 在内核当中应该有一个管理共享内存的结构体。 我们谈到共享内存想到的首先不是一个内存块而是内核中有一个描述共享内存的结构体。 把对操作系统的管理转换成对数据结构的增删查改。
  • 操作系统是数据结构和算法的集合——数据结构决定算法。 操作系统说到底就是一个数据结构和算法的结合,我们现阶段可以把操作系统想成一个大型数据结构的集合。

6.2 准备工作:写代码 + 原理

准备工作

6.2.1 .hpp 和 header only
6.2.1.1 概念区分

概念区分

6.2.2 使用.hpp 的原因:写代码工作量小,文件数量少
  • 为啥我们 cpp 要头源文件分离呢? 为了方便打包成库。
  • .hpp 我们就想写在一起就可以用.hpp,但是这种无法打包成库了。 编译型语言头源分离——方便把头文件打包成库——有历史原因。 .hpp 也是源文件,只不过把头文件声明、源文件可以在一个文件里面写了。 .hpp 头源混合——不能打包成库了。 联系闭源项目和开源项目。 开源软件 / 项目,把动静态库 + XXX 打包给你。
  • header only:开源的方式,不头源分离了,减少文件量。 写代码工作量小,文件数量少,文件以后多起来可能会有 20 多个,如果头源分离的话就得有 40 多个了,很容易搞混。 header only。我们后面会很多时候选择这种做法,节省时间,文件更少点方便看。 只需要有一边负责创建并删除共享内存,另一边获取就可以了,不能两边都创建,乱套了无法进行 IPC 了。
6.2.3 shmget

shmget 创建成功了就会返回一个共享内存的标识符,也可以叫句柄,但是跟文件描述符可没有关系联系。这也是这种技术会被边缘化的原因之一,要是能跟文件关联上多好 shmget 怎么知道 shm 存在还是不存在呢?所以共享内存一定要有一个标识 shm 唯一性的标识符!在哪里?在它的结构体里一定有一个唯一标识符的。需要用户设置唯一值,作为 shm 在内核中的唯一值,我们叫做 key 值,这点有点违反我们的直觉

Key 值

Key 值 2

6.3 共享内存:代码书写

以面向对象的形式——

面向对象

  • 不能两个都创建共享内存——这样不能建立进程间通信的信道。

不能双建

6.3.1 设计问题:这里设计成文件描述符才行
  • 共享内存,两个参数问题(共享内存部分最重要的内容)

参数 这两个选项参数分别是:

选项

  • 两个选项的应用场景

场景

场景 2

6.3.2 键值:key

共享内存的结构体里面一定存在一个标识共享内存唯一性的标识符。

  • 这个标识符叫做键值(key)

Key

  • 为什么用 key 作为唯一的标识符?

Why Key

6.3.3 为什么要用这种方式设置 key 值?

设置 Key 原则上这个键值可以随便写——保证这个键值在系统中唯一——保证不了你就改。

  • 这种由用户自己随便设置方式有点挫

挫 在系统当中有一个接口——

接口

  • ftok本质上是一个算法

ftok

6.3.4 怎么办?

说实话这种方式也挺离谱的,我们有时候也难保持一致还要改,所以我们建议使用系统中的一个函数 ftok,提供一个工程路径 (有效的就行),和一个人工写的随便的项目 id。严格讲这个不属于系统调用,其实是一个算法。把路径标识指定文件的 inode number 和项目 ID 结合生成一个唯一值。

ftok 用法 这个 key_t 其实就是一个整数。

6.3.5 约定 key 值 VS 返回值 id

ID vs Key

6.3.6 新指令:ipcs -m

ipcs

ipcs 2

ipcs 3

  • OS 不删除不释放这个共享内存,这个共享内存就还在——除非关机重启!

持久性

  • (1)用系统调用删除

系统调用删除

  • (2)用指令删除

指令删除 ipcrm 也是 C 语言写的——key 值是删除不了的(默认是删除不了的,有的可以,但是最普遍的是删除不了的)——要用 id 删除。

ipcrm

6.3.7 权限设置

权限

权限 2

6.3.8 删除共享内存:shmctl
  • 现在我们正式来看删除共享内存的系统调用——shmctl

shmctl

shmctl 2 控制共享内存——

控制

  • shmctl 的参数:op

Op 前面设置了 sleep(5);,共享内存自动删除——

自动删除

6.3.9 获取共享内存
  • 创建和获取只有选项不一样!

创建获取 暴露和不暴露的代码——

暴露 下面这种不是正确的做法——

不正确

  • 键值有点挫,一个十进制一个十六进制
  • 共享内存的权限需要设置一下 我们前面说过:'进程问通信的本质前提:先要让不同的进程,看到同一份资源'。 下图中,两个人看到了同一份共享内存——

同一份 我们此时发现:shmid的值是在线性递增的——

递增

  • 获取共享内存的属性

属性

  • 把共享内存挂接到指定的虚拟地址空间

挂接

挂接 2

操作系统自己也可以调用系统调用,这个挂接的函数第二个参数我们不管,暂时做不到。第三个权限问题,我们也直接设置成 0 就可以了,不用管了。 注意一下返回值,失败返回 -1(强转成 void*),成功给你一个地址,有点像 malloc 的返回值,不过一个在堆上一个在共享区。我如果不想通信我甚至能当 malloc 用,或者给库的来试试看。

malloc

malloc 2

malloc 3

6.3.10 查看结构体

结构体 shmid_ds结构体——

shmid_ds

  • 用户曾经设置的键值 key 已经保存在内核中了

Key 保存

Key 保存 2

Key 保存 3

6.3.11 挂接函数

挂接函数

挂接函数 2

挂接函数 3

6.4 代码演示(挂接)

Makefile
all:Reader Writer
Reader:Reader.cc
	g++ -o$@ $^ -std=c++11
Writer:Writer.cc
	g++ -o$@ $^ -std=c++11
.PHONY:clean
clean: rm-f Reader Writer
Reader.cc
#include "Shm.hpp"
#include <iostream>
#include <string>
#include "unistd.h"
// Writer -> shm -> Reader
int main(){
    // 1.在内核中创建共享内存
    Shm shm; 
    shm.Create(); // 这一步才是创建
    sleep(3);
    shm.Attach(); 
    shm.Debug(); 
    shm.GetShmAttr();
    sleep(5); 
    shm.Delete();
    return 0;
}
Shm.hpp
#ifndef __SHM_HPP
#define __SHM_HPP
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/shm.h>
#include <string>
const std::string proj_name = "/home"; // 工作路径,可以随机,最好是系统存在的路径
const int proj_id = 0x6666; // 项目 ID
const int g_size = 4096; // g_size:global size
// 转成十六进制
static std::string ToHex(int data){
    char hex[64];
    snprintf(hex,sizeof(hex),"0x%x",data);
    return hex;
}
class Shm{
public:
    // 构造函数和析构函数
    Shm(int size = g_size):_shmid(-1),_size(size),_key(0){}
    ~Shm(){}
private:
    key_t GetKey(){
        _key = ftok(proj_name.c_str(),proj_id);
        if(_key <0){perror("ftok");}
        return _key;
    }
    bool CreateCoreHelper(int flags){
        // 1.获取 key 值
        key_t k = GetKey();
        // 2.创建共享内存
        _shmid = shmget(k,_size,flags);
        if(_shmid <0){perror("shmget");return false;}
        return true;
    }
public:
    // 1.创建
    bool Create(){return CreateCoreHelper(IPC_CREAT | IPC_EXCL |0666);}
    // 2.获取共享内存
    bool Get(){return CreateCoreHelper(IPC_CREAT);// return CreateCoreHelper(0);}
    // 3.删除共享内存
    bool Delete(){
        int n = shmctl(_shmid,IPC_RMID,NULL);
        return n <0?false:true;
    }
    // 4.保存共享内存属性
    void GetShmAttr(){
        struct shmid_ds ds;
        int n = shmctl(_shmid,IPC_STAT,&ds);
        if(n <0){perror("shmctl");return;}
        std::cout <<"pid: "<<getpid()<< std::endl;// 当前进程的 PID - 显示调用此函数的进程 ID
        // 创建共享内存的进程 PID - shm_cpid 是创建该共享内存段的进程 ID
        std::cout << ds.shm_cpid << std::endl;
        // 共享内存段的大小(字节) - shm_segsz 表示共享内存的大小,单位是字节
        std::cout << ds.shm_segsz << std::endl;
        // 共享内存的 key 值(十六进制)
        // - shm_perm.__key 是共享内存的键值,通过 ToHex 函数转换为十六进制显示
        std::cout <<ToHex(ds.shm_perm.__key)<< std::endl;
    }
    // 挂接
    void* Attach(){return shmat(_shmid,nullptr,0);}
    // Debug
    void Debug(){
        std::cout <<"key: "<<ToHex(_key)<< std::endl;
        std::cout <<"shmid: "<< _shmid << std::endl;
    }
private:
    key_t _key;
    int _shmid;
    int _size;
};
#endif
Writer.cc
#include "Shm.hpp"
#include <iostream>
#include "string"
int main(){
    Shm shm;
    // 这里不能创建共享内存,创建了就不能建立进程间通信的信道了
    shm.Get(); 
    shm.Debug();
    return 0;
}
运行
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ll total 64 
drwxrwxr-x 2 alice alice 4096 Mar 13 19:00 ./ 
drwxrwxr-x 29 alice alice 4096 Mar 12 22:20 ../ 
-rw-rw-r-- 1 alice alice 143 Mar 12 23:04 Makefile 
-rwxrwxr-x 1 alice alice 17712 Mar 13 19:00 Reader* 
-rw-rw-r-- 1 alice alice 350 Mar 12 22:28 Reader.cc 
-rw-rw-r-- 1 alice alice 2233 Mar 13 18:58 Shm.hpp 
-rwxrwxr-x 1 alice alice 18008 Mar 13 19:00 Writer* 
-rw-rw-r-- 1 alice alice 225 Mar 12 22:30 Writer.cc
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Reader
shmget: File exists
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcrm -m7
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcrm -m
ipcrm: option requires an argument -- 'm'
Try 'ipcrm --help'for more information.
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipc -m ^[[A
Command 'ipc' not found, but there are 25 similar ones.
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcrm -m^C
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Writer
key: 0x66020002
shmid: 8
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
0x66020002  8          alice     040960              alice
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -l
------ Messages Limits --------
max queues system wide =32000
max size of message (bytes)=8192
default max size of queue (bytes)=16384
------ Shared Memory Limits --------
max number of segments =4096
max seg size (kbytes)=18014398509465599
max total shared memory (kbytes)=18446744073709551612
min seg size (bytes)=1
------ Semaphore Limits --------
max number of arrays =32000
max semaphores per array =32000
max semaphores system wide =1024000000
max ops per semop call =500
semaphore max value =32767
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
0x66020002  8          alice     040960              alice
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Writer
key: 0x66020002
shmid: 8
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
0x66020002  8          alice     040960              alice
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
0x66020002  8          alice     040960              alice
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
0x66020002  8          alice     040960              alice
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcrm -M 0x66020002
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ make clean
rm-f Reader Writer
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ make
g++ -o Reader Reader.cc -std=c++11
g++ -o Writer Writer.cc -std=c++11
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Writer
key: 0x66020002
shmid: 9
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Reader
^C
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Writer
key: 0x66020002
shmid: 10
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Writer
key: 0x66020002
shmid: 10
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Reader
shmget: File exists
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Reader
shmget: File exists
^[[A^C
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcs -m
------ Shared Memory Segments --------
key         shmid      owner     perms   bytes   nattch  status
0x66020002  10         alice     66640960              alice
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ make clean
rm-f Reader Writer
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ make
g++ -o Reader Reader.cc -std=c++11
g++ -o Writer Writer.cc -std=c++11
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Reader
shmget: File exists
key: 0x66020002
shmid: -1
shmctl: Invalid argument
^C
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcrm -M 0x66020002
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Reader
key: 0x66020002
shmid: 11
pid: 180681018068104096
0x66020002
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Writer
key: 0x66020002
shmid: 12
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ipcrm -M 0x66020002
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Writer
key: 0x66020002
shmid: 13
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ ./Reader
shmget: File exists
key: 0x66020002
shmid: -1
shmctl: Invalid argument
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ man3 shmget
No manual entry for shmget in section 3
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ man2 shmget
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ man2 ftok
No manual entry for ftok in section 2
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$ man3 ftok
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/3_12_shm$

6.5 共享内存思维导图 1

思维导图

6.6 代码演示(共享内存收尾)

Makefile
all:Reader Writer
Reader:Reader.cc
	g++ -o$@ $^ -std=c++11
Writer:Writer.cc
	g++ -o$@ $^ -std=c++11
.PHONY:clean
clean: rm-f Reader Writer
Reader.cc
#include "Shm.hpp"
#include <iostream>
#include <string>
#include "unistd.h"
// 信号
#include <signal.h>
// Writer -> shm -> Reader
int main(){
    // 1.在内核中创建共享内存
    Shm shm; 
    shm.Create(); // 这一步才是创建
    sleep(100);
    char* addr =(char*)shm.Attach();
    buffer_t *shm_addr =(buffer_t*)addr;
    int old = shm_addr->count;
    while(true){
        if(old != shm_addr->count){
            std::cout <<"count : "<< shm_addr->count << std::endl;
            std::cout <<"data : "<< shm_addr->buffer << std::endl;
            old = shm_addr->count;
        }
        usleep(50000);
        if(shm_addr->count >=26)break;
    }
    // sleep(3);
    // shm.Attach();
    // shm.Debug();
    // shm.GetShmAttr();
    // sleep(5);
    shm.Detach(); 
    shm.Delete();
    return 0;
}
Shm.hpp
#ifndef __SHM_HPP
#define __SHM_HPP
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/shm.h>
#include <string>
const std::string proj_name = "/home"; // 工作路径,可以随机,最好是系统存在的路径
const int proj_id = 0x6666; // 项目 ID
const int g_size = 4096; // g_size:global size
// 转成十六进制
static std::string ToHex(int data){
    char hex[64];
    snprintf(hex,sizeof(hex),"0x%x",data);
    return hex;
}
class Shm{
public:
    // 构造函数和析构函数
    Shm(int size = g_size):_shmid(-1),_size(size),_key(0){}
    ~Shm(){}
private:
    key_t GetKey(){
        _key = ftok(proj_name.c_str(),proj_id);
        if(_key <0){perror("ftok");}
        return _key;
    }
    bool CreateCoreHelper(int flags){
        // 1.获取 key 值
        key_t k = GetKey();
        // 2.创建共享内存
        _shmid = shmget(k,_size,flags);
        if(_shmid <0){perror("shmget");return false;}
        return true;
    }
public:
    // 1.创建
    bool Create(){return CreateCoreHelper(IPC_CREAT | IPC_EXCL |0666);}
    // 2.获取共享内存
    bool Get(){return CreateCoreHelper(IPC_CREAT);// return CreateCoreHelper(0);}
    // 3.删除共享内存
    bool Delete(){
        int n = shmctl(_shmid,IPC_RMID,NULL);
        return n <0?false:true;
    }
    // 4.保存共享内存属性
    void GetShmAttr(){
        struct shmid_ds ds;
        int n = shmctl(_shmid,IPC_STAT,&ds);
        if(n <0){perror("shmctl");return;}
        std::cout <<"pid: "<<getpid()<< std::endl;// 当前进程的 PID - 显示调用此函数的进程 ID
        // 创建共享内存的进程 PID - shm_cpid 是创建该共享内存段的进程 ID
        std::cout << ds.shm_cpid << std::endl;
        // 共享内存段的大小(字节) - shm_segsz 表示共享内存的大小,单位是字节
        std::cout << ds.shm_segsz << std::endl;
        // 共享内存的 key 值(十六进制)
        // - shm_perm.__key 是共享内存的键值,通过 ToHex 函数转换为十六进制显示
        std::cout <<ToHex(ds.shm_perm.__key)<< std::endl;
    }
    // 挂接
    void* Attach(){return shmat(_shmid,nullptr,0);}
    // Detach:去关联
    void Detach(){
        int n = shmdt(_start); // 返回值自己设置
        (void)n;
    }
    // Debug
    void Debug(){
        std::cout <<"key: "<<ToHex(_key)<< std::endl;
        std::cout <<"shmid: "<< _shmid << std::endl;
    }
private:
    key_t _key;
    int _shmid;
    int _size;
    void*_start;
};
// 重定义
typedef struct data{
    int count;
    char buffer[26*2];
}buffer_t;
#endif
Writer.cc
#include "Shm.hpp"
#include <iostream>
#include <string>
#include <string.h>
Shm shm;
class Init// 定义结构体
{
public:
    Init(){
        shm.Get();
        addr =(char*)shm.Attach();
        std::cout <<"addr: "<<ToHex((long long)addr)<< std::endl;
    }
    // 析构函数
    ~Init(){
        shm.Detach();
    }
    char* Addr(){return addr;}
public:
    char* addr;
};
Init init;
int main(){
    std::cout <<"test Begin..."<< std::endl;
    buffer_t *shm =(buffer_t*)init.Addr();
    shm->count =0;
    memset(shm->buffer,0,4096);
    char ch ='A';
    for(int i =0;i <26*2;i +=2,ch++){
        // ? shm->buffer[i]= ch;
        // ?usleep(2000000);
        shm->buffer[i +1]= ch;
        usleep(7000000);
        // ? shm->count++;
        usleep(7000000);
        sleep(1);
    }
    return 0;
}

6.7 共享内存补充

6.7.1 回顾

共享内存映射到虚拟地址空间的时候,由于虚拟地址空间使用情况不同,两个进程的这个地址一样或者不一样都很正常,反正都在堆栈之间。

因为创建共享内存的过程是 OS 创建的,所以 OS 必然要提供系统调用。 系统设置 key 的无法和目标进程进行进程间通信,变成了'鸡生蛋'问题,所以得由程序员约定 key 值。 key 值只在内核中使用。 共享内存可以挂接到虚拟地址空间里。 共享内存的生命周期随内核,不是随进程!

  • 系统调用删除
  • 系统指令删除

共享内存是可以同时存在多份的!需要先描述再组织,OS 要管理共享内存。

6.7.2 验证 addr 在堆栈之间

上文中已经获取了共享内存属性、把共享内存挂接到虚拟地址空间,挂接到虚拟地址空间之后会返回一个虚拟地址空间的起始地址——我们可以打印查看一下 addr(起始地址),看看地址被映射到了什么地方。

验证 创建共享内存成功,attach——

Attach 这里 addr 是有精度损失的!addr 的类型是 int,我们转成 long long:

精度

我们运行一下,发现:

运行

我们也可以去验证一下,看看这个 addr 的地址是不是在堆栈之间。

6.7.3 删除共享内存之前一般是要去关联

去关联

  • dt(delete attach):能够去关联

dt

这样我们就把共享内存创建、去关联、删除的系统封装做好了。

作为使用共享内存的一方,只需要获取、关联、去关联(从虚拟地址空间剥离下去):

流程

而读取的一方:

读取

监控开起来,我们可以观察挂接从无到有、从有到无的变化,至此我们就可以完成对共享内存的管理。

我们使用共享内存的过程中,通信了吗?

  • 我们没有通信!

我们之前大量的时间都花在让不同的进程看到同一份资源! 为什么这么麻烦呢?因为进程具有独立性!所以要做很多准备工作!

  • 共享内存、消息队列、信号量都是如此,这就是进程间通信的特点,通信就是一会儿的事,但是准备工作很多。

我们可以把共享内存做得更好玩一点,写端向往里写'AA''BB'CC'这样的内容,读端可以让它 while(true) 一直读取共享内存,或者也可以做得更好玩一点 比如头四个字节作为约定,从 4 字节的位置往后写(前 4 字节写对数):

  • 放 Shm.hpp,这样大家都能够看到了

Shm.hpp

  • 双方基于一个相同的结构体变量共享,进行消息互传
  • 把共享内存当成了一个结构体。

共享内存,共享结构体变量,用指针强转成我们想要的数据类型,buffer_t(typedef 出来的数据类型)——对共享内存可以全部进行定义,设置成 4096:

定义

  • 好像打印有问题,没有打印 ZZ

打印

6.7.4 我们在使用共享内存的时候。有没有使用系统调用?
6.7.4.1 结论 1
  • 结论 1:答案是没有,因为共享内存直接映射到了我们进程自己的用户空间中,用户可以用指针直接访问!
6.7.4.2 结论 2

一方直接写入的数据,另一方是直接看到了!!!

  • 下面这里就能看到,我们直接用指针进行了访问!

指针

共享内存直接属于映射方!双方都会看到同一份资源! 以前 A 和 B 通信要进行两次拷贝,先拷贝给内核,再拷贝给 B,这里只要进行一次拷贝!

  • 结论 2:共享内存是进程间通信中速度最快的,没有之一! 不需要使用系统调用,而且还可以减少拷贝次数!速度是进程间通信中速度最快的! 把内存块映射到地址空间,对方立马就看到了。

  • 模拟一下(今天还做不到)让这三个结构体变量同时

模拟

我们修改一下代码,效果不够明显,我们多复制几台机器,搞两个(多个)写端——

多写端

上下对应,明显感觉到有多个写端的时候,另一个写端写的数据被覆盖或者清空:

覆盖

出现了覆盖和清空问题,我们可由此得出结论 3.

6.7.4.3 结论 3
  • 结论 3:共享内存没有自带保护机制(不是说正在写/读的时候就不能读写了),任何挂接到地址空间的进程都可以随时访问共享内存! 读端可能在写端只写了一半就把数据取走了——共享内存没有自带保护机制! 怎么保护呢?通过信号量来保护。 真正的实现共享内存保护得利用我们的信号量。
6.7.5 共享内存的大小设置

大小设置建议设置为 4096 的整数倍。

  • 如果设置成非 4096 的整数倍呢?

非整数倍

底层操作系统是按 4096 对齐向上申请的,换言之设置 4097,有 4095 的空间被浪费了。 万一访问越界了,但是因为申请了 4096*2,出错了也不知道,你会不会怪操作系统?

  • 写的是 4097,实际申请的是 4096*2

目录

  1. Linux 进程间通信进阶
  2. 1. 准备阶段:进程间通信的概念
  3. 1.1 是什么(本质前提)
  4. 1.2 为什么
  5. 1.3 怎么办
  6. 1.4 思维导图
  7. 2. 进程间通信
  8. 2.1 进程间通信的定制标准:System V
  9. 2.2 进程间通信的发展
  10. 3. 管道的特点和情况总结
  11. 3.1 五种特点
  12. 3.2 四种情况
  13. 3.3 思维导图
  14. 4. 进程池
  15. 4.1 有一个小 Bug
  16. 4.2 进程池:以创建 10 个子进程为例
  17. 5. 命名管道
  18. 5.1 客户端不需要建立管道
  19. 5.2 打开和关闭管道之间,客户端可以进行通信
  20. 5.3 不需要写 C 语言的\0
  21. 5.4 毫不相干的两个进程的父进程可能不一样
  22. 5.5 目前完成的服务端和客户端代码
  23. 5.5.1 服务器端 server.cc
  24. 5.5.2 客户端 client.cc
  25. 5.5.3 Makefile 得这样写
  26. 5.5.4 运行:服务端~>客户端
  27. 5.6 最终呈现的服务端和客户端代码
  28. 5.6.1 服务器端 server.cc
  29. 5.6.2 客户端 client.cc
  30. 5.6.3 运行:服务端~>客户端
  31. 5.7 命名管道的本质
  32. 5.8 命名管道打开的细节问题
  33. 5.9 命名管道思维导图
  34. 5.9.1 匿名管道(进程池)+(开了个头)命名管道
  35. 5.10 管道的应用场景
  36. 5.10.1 命名管道实现文件拷贝
  37. 5.10.2 定标准
  38. 5.10.3 System V 标准
  39. 6. 共享内存
  40. 6.1 原理
  41. 6.1.1 原理思维导图
  42. 6.1.2 原理理论
  43. 6.1.3 共享内存与进程地址空间布局
  44. 6.1.4 问题
  45. 1、整个过程是谁做的?
  46. 2、共享区这个东西用户可以直接访问嘛?用户可以不需要使用系统调用来读写共享内存(指针)
  47. 访问库函数也是直接访问使用
  48. 去关联
  49. 6.1.5 思路打开——突破点
  50. 6.2 准备工作:写代码 + 原理
  51. 6.2.1 .hpp 和 header only
  52. 6.2.1.1 概念区分
  53. 6.2.2 使用.hpp 的原因:写代码工作量小,文件数量少
  54. 6.2.3 shmget
  55. 6.3 共享内存:代码书写
  56. 6.3.1 设计问题:这里设计成文件描述符才行
  57. 6.3.2 键值:key
  58. 6.3.3 为什么要用这种方式设置 key 值?
  59. 6.3.4 怎么办?
  60. 6.3.5 约定 key 值 VS 返回值 id
  61. 6.3.6 新指令:ipcs -m
  62. 6.3.7 权限设置
  63. 6.3.8 删除共享内存:shmctl
  64. 6.3.9 获取共享内存
  65. 6.3.10 查看结构体
  66. 6.3.11 挂接函数
  67. 6.4 代码演示(挂接)
  68. Makefile
  69. Reader.cc
  70. Shm.hpp
  71. Writer.cc
  72. 运行
  73. 6.5 共享内存思维导图 1
  74. 6.6 代码演示(共享内存收尾)
  75. Makefile
  76. Reader.cc
  77. Shm.hpp
  78. Writer.cc
  79. 6.7 共享内存补充
  80. 6.7.1 回顾
  81. 6.7.2 验证 addr 在堆栈之间
  82. 6.7.3 删除共享内存之前一般是要去关联
  83. 6.7.4 我们在使用共享内存的时候。有没有使用系统调用?
  84. 6.7.4.1 结论 1
  85. 6.7.4.2 结论 2
  86. 6.7.4.3 结论 3
  87. 6.7.5 共享内存的大小设置

更多推荐文章

查看全部
  • Web 前端基础:HTML、CSS 与 JavaScript 核心知识梳理
  • ROS2 机械臂控制(MoveIt2)入门与实战
  • Docker 零基础入门:核心概念与使用价值
  • RS485 收发器在 FPGA 中的应用及注意事项
  • FPGA 实现高速数字信号处理的技术原理与实践
  • OpenClaw 接入飞书机器人教程
  • MambaYOLO: 一种基于状态空间模型的简单目标检测基线
  • AIGC 试用 47:AI 辅助软件开发流程实践
  • Lumerical Python API 实战:从数据自动化到光子逆向设计
  • Mistral 发布开源多模态模型 Pixtral Large,Le Chat 助手新增多项功能
  • Linux 命令行核心指令:Git 提交、时间管理、权限控制与热键技巧
  • 存储过程开发模板与异常处理规范
  • 基于 Windows 环境搭建 OpenClaw 部署指南
  • GitHub 十大必装 Claude Skill 推荐
  • OpenClaw 高级配置与云端本地协同实战
  • 数据结构:二叉树经典习题讲解
  • Python 包管理工具 uv 全面指南:安装、命令与性能优化
  • SAP Web Service 基本配置与调用实战
  • Java RSA 私钥 PKCS1 与 PKCS8 格式互转及对象构建
  • 基于Apache Curator框架的ZooKeeper使用详解

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online