跳到主要内容
Linux 进程间通信详解:管道、共享内存与 System V IPC 机制 | 极客日志
C++ 算法
Linux 进程间通信详解:管道、共享内存与 System V IPC 机制 综述由AI生成 Linux 进程间通信(IPC)涵盖管道、System V 共享内存、消息队列及信号量等核心机制。管道支持单向字节流传输,分为匿名与命名两种形式;System V 共享内存通过物理内存映射实现高效数据共享,配合信号量解决同步问题。文章深入解析了各类 IPC 的内核数据结构、系统调用接口及代码实现,阐述了内核如何通过命名空间和权限模型组织管理 IPC 资源,为并发编程提供底层支撑。
活在当下 发布于 2026/3/16 更新于 2026/4/30 18 浏览一、进程间通信介绍
1. 什么是进程间通信
进程间通信 (Inter-Process Communication, IPC) 是指在不同进程之间传播或交换信息的技术方法。由于操作系统中的进程通常拥有独立的地址空间,一个进程不能直接访问另一个进程的变量或数据结构,因此需要专门的机制来实现进程间的数据共享和通信。
进程间通信的本质:是让不同的进程先看到同一份资源(内存),然后才有通信的条件。
2. 进程间通信的发展和分类(简单介绍)
(1)早期 IPC:管道 (Pipe) 是最早的 IPC 机制之一。
匿名管道 (无名管道):单向通信,只能用于有亲缘关系的进程 (如父子进程),通过 pipe() 系统调用创建。
命名管道 (FIFO):有名称的管道文件,可用于无亲缘关系的进程间通信,通过 mkfifo() 创建。
(2)System V IPC
三种主要机制:消息队列、信号量、共享内存。
使用键值 (Key) 来标识 IPC 对象,需要显式删除 IPC 对象,否则会一直存在于系统中。
权限控制通过类似文件权限的机制。
(3)POSIX IPC:对 System V IPC 的改进和标准化
主要包括以下组件:消息队列、共享内存、信号量、互斥量、条件变量、读写锁。
(4)发展对比
特性 管道 System V IPC POSIX IPC 标准化 Unix 传统 System V Unix POSIX 标准 对象标识 文件描述符 (匿名管道) 键值 (Key) 文件系统路径名 生命周期 随进程结束 需显式删除 可配置为随进程结束 访问控制 文件权限 IPC 权限 文件权限 跨平台性 有限 有限 较好 性能 中等 高 (特别是共享内存) 高
3. 进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程。
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
二、管道
1. 什么是管道?
管道(Pipe)是 Unix 系统最古老的进程间通信(IPC)方式,其核心思想是:将一个进程的输出数据流(stdout)直接连接到另一个进程的输入数据流(stdin),形成一个单向的、基于字节流(无消息边界,二进制传输)的通信通道(内核级)。
2. 匿名管道
(1)核心特点
单向通信:数据只能从写端流向读端(半双工)
仅限亲缘进程:通过 fork() 创建的父子/兄弟进程间使用
内存级通信:由内核管理缓冲区,不依赖磁盘文件
随进程销毁:当所有相关进程终止时,管道自动释放
(2)创建匿名管道 #include <unistd.h>
int pipe (int pipefd[2 ]) ;
参数:pipefd[2] 表示一个长度为 2 的整型数组,用于存储管道的两个文件描述符:
pipefd[0]:管道的读端(用于从管道读取数据)。
pipefd[1]:管道的写端(用于向管道写入数据)。
注:调用 pipe 创建管道不要文件路径,没有文件名。是内存级的,被 OS 单独设计,称之为匿名管道。
(3)fork 共享管道原理 单个进程中的管道几乎没有任何用处,通常,进程会先调用 pipe(),接着调用 fork(),从而创建父进程到子进程的 IPC 通道。
我们怎么保证两个进程打开的是同一个管道文件?fork 之后,子进程会继承父进程的管道文件描述符,父子进程通过 fd 访问同一管道,内核确保数据同步和生命周期管理。
fork 之后做什么取决于我们想要的数据流的方向。对于父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程关闭写端(fd[1])。
(4)内核角度 - 管道本质 管道在内核中通过 inode 表示,不同于磁盘文件的 inode,它关联的是内存中的 pipe_inode_info(管道核心)。
管道是通过 文件描述符→file→inode→pipe_inode_info→数据页 的链式关系实现的,多个进程通过不同层级的共享(file 结构独立,但底层 pipe_inode_info 共享)完成通信。
匿名管道的'匿名性'体现在其 inode 不关联文件系统,仅存于内存。
(5)管道代码样例 创建一个管道,用于父子进程间单向通信,子进程每隔 1 秒向管道写入数据,父进程从管道读取数据并打印。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <string.h>
void ChildWrite (int wfd) {
char buffer[1024 ];
int cnt = 0 ;
while (true ) {
snprintf (buffer, sizeof (buffer), "I am child, pid: %d, cnt: %d" , getpid (), cnt++);
write (wfd, buffer, strlen (buffer));
sleep (1 );
}
}
void FatherRead (int rfd) {
char buffer[1024 ];
while (true ) {
buffer[0 ] = 0 ;
size_t n = read (rfd, buffer, sizeof (buffer)-1 );
if (n > 0 ) {
buffer[n] = 0 ;
std::cout << "Child say: " << buffer << std::endl;
}
}
}
int main () {
int fds[2 ] = {0 };
int n = pipe (fds);
if (n < 0 ) {
std::cerr << "pipe error!" << std::endl;
return -1 ;
}
std::cout << "fds[0]: " << fds[0 ] << std::endl;
std::cout << "fds[1]: " << fds[1 ] << std::endl;
pid_t id = fork();
if (id == 0 ) {
close (fds[0 ]);
ChildWrite (fds[1 ]);
close (fds[1 ]);
exit (0 );
}
close (fds[1 ]);
FatherRead (fds[0 ]);
waitpid (id, nullptr , 0 );
close (fds[0 ]);
return 0 ;
}
(6)管道文件的特点
• 管道通常由父进程创建,然后调用 fork(),使父子进程共享管道的文件描述符。
• 非亲缘关系的进程无法直接使用管道通信(但可以通过其他方式,如命名管道 FIFO,后面讲)。
• 管道是字节流(stream),没有消息边界,数据以字节序列的形式传输。
• 不同于消息队列(如 msgqueue),不会自动分隔消息,需要应用层自行处理(如用 \n 分隔)。
• 管道的生命周期依赖于进程,当所有引用该管道的进程都关闭文件描述符后,管道会被内核回收。
• 如果父进程先退出,子进程仍可继续使用管道,但若所有进程都关闭管道,数据会丢失。
• 同步 (Synchronization):
当管道空时,读端 read() 会阻塞,直到有数据写入。
当管道满时(默认缓冲区大小通常为 64KB),写端 write() 会阻塞,直到有空间可写。
• 互斥 (Mutual Exclusion):
内核保证多个进程同时读写管道时不会发生数据竞争(read 和 write 是原子的)。
例如:如果两个进程同时写管道,内核会确保数据不会交错(一次 write() 不会被另一个 write() 打断)。如果两个进程同时读管道,内核会确保数据不会被重复读取(每个 read() 获取不同的数据)。
• 半双工(Half-Duplex):数据只能单向传输(要么父写子读,要么子写父读)。
• 如果需要双向通信(全双工),必须建立两个管道
• 在 Linux 中,管道的默认缓冲区大小通常是 64KB (PIPE_BUF,定义在 <limits.h>)。
• 如果写入的数据超过 PIPE_BUF,write() 可能会部分写入或阻塞,取决于是否设置 O_NONBLOCK。
情况 读端 read() 写端 write() 管道空 阻塞 (直到有数据)正常写入 管道满 正常读取 阻塞 (直到有空间)所有写端关闭 read() 返回 0(EOF)- 所有读端关闭 - SIGPIPE
(7)基于匿名管道 --- 进程池 这是一个基于 C++ 实现的简单进程池系统,主要用于管理多个子进程并通过管道进行任务分发。
① Task.hpp ● TaskManager 类(任务管理器):管理可执行任务
• 关键特性:
使用函数指针数组存储任务
支持任务注册 (Register)
随机选择任务 (Code)
执行指定任务 (Execute)
• 任务类型:typedef void (*task_t)() 定义的无参数无返回值函数
#pragma once
#include <iostream>
#include <vector>
#include <ctime>
typedef void (*task_t ) () ;
void PrintLog () { std::cout << "我是一个打印日志的任务" << std::endl; }
void DownLoad () { std::cout << "我是一个下载的任务" << std::endl; }
void Upload () { std::cout << "我是一个上传的任务" << std::endl; }
class TaskManager {
private :
std::vector<task_t > _tasks;
public :
TaskManager () { srand (time (nullptr )); }
void Register (task_t t) { _tasks.push_back (t); }
int Code () { return rand () % _tasks.size (); }
void Execute (int code) { if (code >= 0 && code < _tasks.size ()) { _tasks[code](); } }
~TaskManager () {}
};
② ProcessPool.hpp ● Channel 类(通道/管道):封装了父子进程间的单向通信管道
• 关键成员:
_wfd:管道写端文件描述符
_subid:子进程 PID
_name:通道名称(用于标识)
● ChannelManager 类(通道管理器):集中管理所有 Channel 对象
• 关键特性:
使用 vector 存储 Channel 对象
采用轮询 (round-robin) 方式选择 Channel
● ProcessPool 类(进程池):主管理类,整合上述组件
• 工作流程:
初始化时注册任务
启动时创建指定数量的子进程
通过 Run() 方法分发任务
#pragma once
#include <iostream>
#include <vector>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include <sys/wait.h>
#include "Task.hpp"
class Channel {
private :
int _wfd;
pid_t _subid;
std::string _name;
public :
Channel (int wfd, int subid) : _wfd(wfd), _subid(subid) { _name = "channel- " + std::to_string (wfd) + " - " + std::to_string (subid); }
int Fd () { return _wfd; }
pid_t Subid () { return _subid; }
std::string Name () { return _name; }
void Send (int code) { int n = write (_wfd, &code, sizeof (code)); (void )n; }
void Close () { close (_wfd); }
void Wait () { pid_t rid = waitpid (_subid, nullptr , 0 ); (void )rid; }
~Channel () {};
};
class ChannelManager {
private :
std::vector<Channel> _channels;
int _next;
public :
ChannelManager () : _next(0 ) {};
void InsertChannel (int wfd, pid_t subid) { _channels.emplace_back (wfd, subid); }
void PrintChannel () { for (auto &channel : _channels) { std::cout << channel.Name () << std::endl; } }
Channel &Select () { auto &c = _channels[_next]; _next++; _next %= _channels.size (); return c; }
void StopSubProcess () { for (auto &channel : _channels) { channel.Close (); std::cout << "关闭:" << channel.Name () << std::endl; } }
void WaitSubProcess () { for (auto &channel : _channels) { channel.Wait (); std::cout << "回收:" << channel.Name () << std::endl; } }
~ChannelManager () {};
};
const int defaultnum = 5 ;
class ProcessPool {
private :
ChannelManager _cm;
int _process_num;
TaskManager _tm;
public :
ProcessPool (int num) : _process_num(num) {
_tm.Register (PrintLog); _tm.Register (DownLoad); _tm.Register (Upload);
}
void Work (int rfd) {
while (true ) {
int code = 0 ;
ssize_t n = read (rfd, &code, sizeof (code));
if (n > 0 ) {
if (n != sizeof (code)) { continue ; }
std::cout << "子进程 [" << getpid () << "] 收到一个任务码:" << code << std::endl;
_tm.Execute (code);
} else if (n == 0 ) { break ; }
else { break ; }
}
}
bool Start () {
for (int i = 0 ; i < _process_num; i++) {
int pipefd[2 ] = {0 };
int n = pipe (pipefd);
if (n < 0 ) return false ;
pid_t subid = fork();
if (subid < 0 ) return false ;
else if (subid == 0 ) {
close (pipefd[1 ]);
Work (pipefd[0 ]);
close (pipefd[0 ]);
exit (0 );
} else {
close (pipefd[0 ]);
_cm.InsertChannel (pipefd[1 ], subid);
}
}
return true ;
}
void Run () {
int taskcode = _tm.Code ();
auto &c = _cm.Select ();
std::cout << "选择一个子进程:" << c.Name () << std::endl;
c.Send (taskcode);
std::cout << "发送了一个任务码:" << taskcode << std::endl;
}
void Stop () {
_cm.StopSubProcess ();
_cm.WaitSubProcess ();
}
~ProcessPool () {};
};
③ Main.cc
• 初始化阶段:创建进程池对象,注册任务函数;调用 Start() 方法:创建管道和子进程
• 任务分发阶段:主进程通过 Run() 方法:随机选择一个任务码,轮询选择一个子进程,通过管道发送任务码。
• 任务执行阶段:子进程读取管道中的任务码,调用 TaskManager 执行对应任务。
• 终止阶段:关闭所有管道写端,等待子进程退出。
#include "ProcessPool.hpp"
int main () {
ProcessPool pp (defaultnum) ;
pp.Start ();
int cnt = 5 ;
while (cnt--) { pp.Run (); sleep (1 ); }
pp.Stop ();
return 0 ;
}
④ Makefile process_pool:Main.cc
g++ -o $@ $^
.PHONY:clean
clean:
rm -f process_pool
⑤ BUG --- 文件描述符的继承和泄漏 如果我们用整合后的 StopAndWait() 代替 WaitSubProcess() 和 StopAndWait() 呢。会发生什么?
void StopAndWait () {
for (auto &channel : _channels) {
channel.Close ();
std::cout << "关闭:" << channel.Name () << std::endl;
channel.Wait ();
std::cout << "回收:" << channel.Name () << std::endl;
}
}
1、文件描述符继承:每次 fork() 时,子进程会复制父进程的文件描述符表,包括之前创建的管道描述符。
2、描述符泄漏:父进程虽然关闭了当前管道的写端(pipefd[1]),但之前子进程继承的冗余描述符未被关闭。子进程的 read() 可能阻塞在其他未关闭的管道描述符上。
3、waitpid() 阻塞:由于子进程仍有未关闭的描述符,它的 read() 可能未检测到 EOF,导致子进程无法退出。
方案 1:在执行 StopAndWait() 时,倒着关闭文件描述符表。
void StopAndWait () {
for (int i = _channels.size () - 1 ; i >= 0 ; i--) {
_channels[i].Close ();
std::cout << "关闭:" << _channels[i].Name () << std::endl;
_channels[i].Wait ();
std::cout << "回收:" << _channels[i].Name () << std::endl;
}
}
方案 2:在子进程中关闭无关描述符,实现一个父进程指向所有管道。
void CloseAll () {
for (auto &channel : _channels) { channel.Close (); }
}
3. 命名管道 命名管道(Named Pipe),也称为 FIFO(First In First Out)文件,是一种特殊的文件类型,用于在不相关进程(无父子关系)之间进行通信(特性)。与匿名管道(pipe())不同,命名管道在文件系统中有一个可见的路径名,任何进程只要知道该路径,都可以访问它。
所以说 FIFO 是通过打开同一个路径下同一个文件 (管道文件),看到同一份资源,从而实现通信的。
(1)创建命名管道
① 命令行创建 $ mkfifo fifo
$ ll total 8
drwxrwxr-x 2 zyt zyt 4096 May 22 16:24 ./
drwxrwxr-x 27 zyt zyt 4096 May 22 16:19 ../
prw-rw-r-- 1 zyt zyt 0 May 22 16:24 fifo|
命名管道(FIFO)是一种 同步通信机制,写入操作(>)会 阻塞,直到另一端有进程打开管道并开始读取。如果没有任何进程在读取管道,写入操作会一直等待。
$ echo "hello fifo" >fifo
$ cat < fifo
hello fifo
② 系统调用函数 #include <sys/types.h>
#include <sys/stat.h>
int mkfifo (const char *pathname, mode_t mode) ;
pathname:要创建的 FIFO 文件的路径名;mode:文件权限模式
成功时返回 0,失败时返回 -1 并设置 errno
(2)实例
① 示例:简单实现一个客户端与服务端的通信 #include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <iostream>
#include <unistd.h>
#include "comm.hpp"
int main () {
umask (0 );
int n = mkfifo (FIFO_FILE, 0666 );
if (n < 0 ) { std::cerr << "mkfifo error" << std::endl; return 1 ; }
std::cout << "mkfifo success" << std::endl;
int fd = open (FIFO_FILE, O_RDONLY);
if (fd < 0 ) { std::cerr << "open fifo error" << std::endl; return 2 ; }
std::cout << "open fifo success" << std::endl;
while (true ) {
char buffer[1024 ];
int number = read (fd, buffer, sizeof (buffer)-1 );
if (number > 0 ) { buffer[number] = 0 ; std::cout << "client say: " << buffer << std::endl; }
else if (number == 0 ) { std::cout << "client quit ? " << number << std::endl; break ; }
else { std::cerr << "read error" << std::endl; break ; }
}
int m = unlink (FIFO_FILE);
if (m == 0 ) { std::cout << "remove fifo success" << std::endl; }
return 0 ;
}
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <iostream>
#include <unistd.h>
#include "comm.hpp"
int main () {
int fd = open (FIFO_FILE, O_WRONLY);
if (fd < 0 ) { std::cerr << "open fifo error" << std::endl; return 1 ; }
std::cout << "open fifo success" << std::endl;
std::string message;
int cnt = 1 ;
pid_t id = getpid ();
while (true ) {
std::cout << "Please Enter# " ;
std::getline (std::cin, message);
message += ", message number " + std::to_string (cnt++) + ", [" + std::to_string (id) + "]" ;
write (fd, message.c_str (), message.size ());
}
close (fd);
return 0 ;
}
.PHONY :all
all : server client
server:server.cc
g++ -o $@ $^ -std=c++11
client:client.cc
g++ -o $@ $^ -std=c++11
.PHONY :clean
clean:
rm -f client server
② 优化:用类封装
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_FILE "fifo"
#define PATH "."
#define FILENAME "fifo"
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0)
struct Namedfifo {
public :
Namedfifo (const std::string path, const std::string name) : _path(path), _name(name) {
_fifoname = _path + "/" + _name;
umask (0 );
int n = mkfifo (_fifoname.c_str (), 0666 );
if (n < 0 ) ERR_EXIT ("mkfifo" );
else std::cout << "mkfifo success" << std::endl;
}
~Namedfifo () {
int m = unlink (_fifoname.c_str ());
if (m == 0 ) std::cout << "remove fifo success" << std::endl;
else ERR_EXIT ("unlink" );
}
private :
std::string _path, _name, _fifoname;
};
class FileOper {
public :
FileOper (const std::string &path, const std::string &name) : _path(path), _name(name), _fd(-1 ) {
_fifoname = _path + "/" + _name;
}
void OpenForRead () { _fd = open (_fifoname.c_str (), O_RDONLY); if (_fd < 0 ) ERR_EXIT ("open" ); }
void OpenForWrite () { _fd = open (_fifoname.c_str (), O_WRONLY); if (_fd < 0 ) ERR_EXIT ("open" ); }
void Write () {
std::string message; int cnt = 1 ; pid_t id = getpid ();
while (true ) {
std::cout << "Please Enter# " ; std::getline (std::cin, message);
message += ", message number " + std::to_string (cnt++) + ", [" + std::to_string (id) + "]" ;
write (_fd, message.c_str (), message.size ());
}
}
void Read () {
while (true ) {
char buffer[1024 ];
int number = read (_fd, buffer, sizeof (buffer) - 1 );
if (number > 0 ) { buffer[number] = 0 ; std::cout << "client say: " << buffer << std::endl; }
else if (number == 0 ) { std::cout << "client quit ? " << number << std::endl; break ; }
else ERR_EXIT ("read" );
}
}
void Close () { if (_fd > 0 ) close (_fd); }
~FileOper () {}
private :
std::string _path, _name, _fifoname; int _fd;
};
三、System V System V IPC 是 Unix/Linux 系统中一种经典的进程间通信(IPC)标准,与 POSIX IPC 共同构成传统 UNIX 系统的两大 IPC 通信体系。System V IPC 包含以下三种核心机制(共享内存,信号量,消息队列),均在内核中维护全局唯一的标识符和数据结构。
1. system V 共享内存 共享内存允许两个或多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种 IPC。
映射之后读写直接被对方看到。
不需要进行系统调用获取或写入内容,以指针地址的方式。
(1)原理
① 物理内存共享
内核分配一块物理内存:通过系统调用(如 shmget())在内核中申请一块物理内存区域,称为共享内存段。
• 该区域独立于任何进程,由内核统一管理。
• 生命周期持续到显式释放(shmctl(IPC_RMID))或系统重启。
② 虚拟地址映射
进程通过 shmat() 将共享内存段映射到自己的虚拟地址空间:
• 不同进程的映射地址可以不同(如进程 A 映射到 0x4000,进程 B 映射到 0x5000)。
• 通过 页表 将虚拟地址转换为同一物理地址,实现共享。
③ 直接访问
进程通过指针操作共享内存,无需系统调用:
• 写入数据后,其他进程立即可见(无延迟)。
• 性能接近直接访问本地内存。
④ 工作流程
(2)内核数据结构 共享内存的机制依赖于内核数据结构和物理内存的紧密结合。
① struct shmid_ds(用户可见的元信息) struct shmid_ds {
struct ipc_perm shm_perm ;
int shm_segsz;
__kernel_time_t shm_atime;
__kernel_time_t shm_dtime;
__kernel_time_t shm_ctime;
__kernel_ipc_pid_t shm_cpid;
__kernel_ipc_pid_t shm_lpid;
unsigned short shm_nattch;
};
② struct shmid_kernel(内核实际使用的扩展结构) struct shmid_kernel {
struct shmid_ds u ;
struct file *shm_file ;
unsigned long shm_nattch;
};
③ struct file 与物理页帧 shm_file->f_mapping:指向共享内存的物理页帧集合(struct address_space)。
物理页:通过 struct page 数组表示,每个页帧对应实际的物理内存。
④ 操作系统如何跟踪共享内存的使用状态? 操作系统通过内核数据和引用计数机制精确跟踪共享内存是否被进程使用。
• shm_nattch 是核心计数器,记录有多少进程通过 shmat() 映射了该共享内存段。
• shm_perm.mode:包含标志位 IPC_RMID,标记共享内存是否已被标记为'待销毁'。
(3)标识符和键
• 我们怎么评估共享内存是存在还是不存在?
• 我们怎么确定两个不同的进程拿到的是同一个共享内存?
① 标识符 shmid 每个内核中的 IPC 结构,都用一个非负整数的标识符加以引用。概念与文件描述符类似,但与文件描述符不同,IPC 标识符不是小的整数。
• shmid 是内核分配的临时标识符,在系统生命周期内唯一。
• 同一共享内存段在不同进程中的 shmid 可能不同(但指向同一物理内存)。
• 通过 shmget(key, ...) 返回。
② 键 key 标识符是 IPC 对象的内部名。为使多个合作进程能够在同一 IPC 对象上汇聚,需要提供一个外部命名方案。为此,每个 IPC 对象都与一个键 key 相关联。
ftok() :基于文件路径和项目 ID 计算产生一个 key。
#include <sys/types.h>
key_t ftok (const char *path, int id) ;
• ftok 创建的键通常是用下列方式构成的:按给定的路径名取得其 stat 结构中的部分 st_dev 和 st_ino 字段,然后再将它们与项目 ID 组合起来。
• 冲突:因为 i 节点编号和键通常都存放在长整型中,所以创建键时实际位数可能远超 key_t 的容量,可能会丢失信息。
补充知识:key_t key = (st_dev & 0xFF) << 24 | (st_ino & 0xFFFF) << 8 | (proj_id & 0xFF);
③ 总结 问题 判断依据 共享内存是否存在? shmget(key, 0, 0) 是否成功,或 shmctl(shmid, IPC_STAT, &buf) 是否有效。是否同一共享内存? • 两个进程是否使用相同的 key,并且能互相读写数据(物理内存相同)。\n• 即使 shmid 数值不同,只要 key 相同,就是同一共享内存。
(4)共享内存接口函数
① shmget() 创建或获取一个共享内存段。若成功返回共享内存的标识码 ID(shmid),若出错返回 -1.
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget (key_t key, size_t size, int shmflg) ;
参数 类型 说明 keykey_t共享内存的键值,通常由 ftok() 生成,或使用 IPC_PRIVATE 创建私有段。 sizesize_t共享内存段的大小(字节)。若为获取已有段,可设为 0。 shmflgint标志位组合,控制创建/访问权限。
标志 含义 IPC_CREAT若共享内存不存在则创建,若存在打开目标共享文件并返回。 IPC_EXCL与 IPC_CREAT 联用,若共享内存不存在则创建,若已存在则出错返回。 0666 等权限码设置共享内存的读写权限。
• 使用 IPC_CREAT | IPC_EXCL 保证只要 shmget 成功返回,就一定是一个全新的共享内存。
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
int main () {
key_t key = ftok("/tmp/shmdemo" , 'A' );
if (key == -1 ) { perror("ftok failed" ); exit (1 ); }
int shmid = shmget(key, 4096 , IPC_CREAT | 0666 );
if (shmid == -1 ) { perror("shmget failed" ); exit (1 ); }
printf ("Shared memory created, shmid=%d\n" , shmid);
return 0 ;
}
验证:共享内存的生命周期持续到显式释放(shmctl(IPC_RMID))或系统重启。
#pragma once
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <iostream>
#include <string>
#include <cstdio>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0)
const int defaultid = -1 ;
const int gsize = 4096 ;
const std::string pathname = "." ;
const int projid = 0x66 ;
class Shm {
public :
Shm () : _shmid(defaultid), _size(gsize) {}
void Create () {
key_t k = ftok (pathname.c_str (), projid);
if (k < 0 ) ERR_EXIT ("ftok" );
printf ("key: 0x%x\n" , k);
_shmid = shmget (k, _size, IPC_CREAT | IPC_EXCL);
if (_shmid < 0 ) ERR_EXIT ("shmget" );
printf ("shmid: %d\n" , _shmid);
}
~Shm () {}
private :
int _shmid;
int _size;
};
$ ./server
key: 0x66030437
shmid: 1
$ ./server
key: 0x66030437
shmget: File exists
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x66030437 1 zyt 0 4096 0
进程结束了,如果没有删除共享内存,共享内存就会一直存在。
=> 共享内存的资源、生命周期随内核!共享内存的生命周期持续到显式释放或系统重启。
删除共享内存:① ipcrm -m shmid ② 代码删除
注意:虽然前面我们解释 key 是外部名,shmid 是内部名。但在用户层删除、控制共享内存,不能用 key 值,而要用 shmid 来管理共享内存!(key 给内核用来进行唯一性区分的!)
② shmctl() 控制共享内存段的系统调用,可以查询状态、修改属性或删除共享内存段。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl (int shmid, int cmd, struct shmid_ds *buf) ;
命令 作用 IPC_STAT获取共享内存状态,保存到 buf。 IPC_SET修改共享内存属性(如权限),需通过 buf 传入新值。 IPC_RMID标记删除 共享内存段(实际释放需等待 shm_nattch=0)。IPC_INFO获取系统级共享内存限制信息(Linux 特有)。
void Destory () {
if (_shmid == defaultid) return ;
int n = shmctl (_shmid, IPC_RMID, nullptr );
if (n > 0 ) { printf ("shmctl delete shm : %d success\n" , _shmid); }
else ERR_EXIT ("shmctl" );
}
③ shmat() 将共享内存段挂接(attach)到当前进程的地址空间,使其能够直接访问共享内存。如果 shmat 成功执行,那么内核将使用与该共享内存段相关的 shmid_ds 结构中的 shm_nattch 计数器值加 1。
#include <sys/shm.h>
void *shmat (int shmid, const void *shmaddr, int shmflg) ;
参数 类型 说明 shmidint共享内存标识符(由 shmget() 返回)。 shmaddrconst void*指定附加固定地址(通常设为 NULL,由内核自动选择合适地址)。 shmflgint控制附加行为的标志位(如 SHM_RDONLY 表示只读访问)。
标志 作用 0默认读写权限。 SHM_RDONLY以只读方式附加(需共享内存权限允许)。 SHM_REMAP强制替换 shmaddr 处的现有映射(Linux 特有,谨慎使用)。
返回值:成功:返回共享内存段在进程地址空间中的起始地址(类型为 void*)。失败:返回 (void*) -1,并设置 errno。
④ shmdt() shmdt() 将从调用进程的地址空间中分离位于 shmaddr 的共享内存段;该内存段必须之前已经通过 shmat() 附加到进程。分离操作不会删除共享内存段 - 该段会一直存在直到使用 shmctl() 配合 IPC_RMID 命令显式删除。
#include <sys/shm.h>
int shmdt (const void *shmaddr) ;
• shmaddr: 要分离的共享内存段的地址,必须是之前 shmat() 调用返回的地址
• 成功时返回 0,失败时返回 -1 并设置 errno 来指示错误
(5)共享内存使用全流程
① comm.hpp #pragma once
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0)
const int defaultid = -1 ;
const int gsize = 4096 ;
const std::string pathname = "." ;
const int projid = 0x66 ;
const int gmode = 0666 ;
class Shm {
public :
Shm () : _shmid(defaultid), _size(gsize), _start_mem(nullptr ) { }
void Create () {
key_t k = ftok (pathname.c_str (), projid);
if (k < 0 ) ERR_EXIT ("ftok" );
printf ("key: 0x%x\n" , k);
_shmid = shmget (k, _size, IPC_CREAT | IPC_EXCL | gmode);
if (_shmid < 0 ) ERR_EXIT ("shmget" );
printf ("shmid: %d\n" , _shmid);
}
void Destory () {
if (_shmid == defaultid) return ;
int n = shmctl (_shmid, IPC_RMID, nullptr );
if (n > 0 ) { printf ("shmctl delete shm : %d success\n" , _shmid); }
else ERR_EXIT ("shmctl" );
}
void Attach () {
_start_mem = shmat (_shmid, nullptr , 0 );
if ((long long )_start_mem < 0 ) ERR_EXIT ("shmat" );
printf ("attach success\n" );
}
void *VirtualAddr () { printf ("VirtualAddr : %p\n" , _start_mem); return _start_mem; }
~Shm () { }
private :
int _shmid;
int _size;
void *_start_mem;
};
② server.cc #include "comm.hpp"
int main () {
Shm shm;
shm.Create ();
sleep (3 );
shm.Attach ();
shm.VirtualAddr ();
sleep (3 );
shm.Destory ();
return 0 ;
}
(6)基于共享内存实现进程间通信
① 无同步机制的进程间通信
#pragma once
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0)
const int defaultid = -1 ;
const int gsize = 4096 ;
const std::string pathname = "." ;
const int projid = 0x66 ;
const int gmode = 0666 ;
#define CREATER "creater"
#define USER "user"
class Shm {
private :
void CreateHelper (int flag) {
printf ("key: 0x%x\n" , _key);
_shmid = shmget (_key, _size, flag);
if (_shmid < 0 ) ERR_EXIT ("shmget" );
printf ("shmid: %d\n" , _shmid);
}
void Create () { CreateHelper (IPC_CREAT | IPC_EXCL | gmode); }
void Attach () {
_start_mem = shmat (_shmid, nullptr , 0 );
if ((long long )_start_mem < 0 ) ERR_EXIT ("shmat" );
printf ("attach success\n" );
}
void Destory () {
int n = shmctl (_shmid, IPC_RMID, nullptr );
if (n > 0 ) { printf ("shmctl delete shm : %d success\n" , _shmid); }
else ERR_EXIT ("shmctl" );
}
public :
Shm (const std::string &pathname, int projid, const std::string &usertype) : _shmid(defaultid), _size(gsize), _start_mem(nullptr ), _usertype(usertype) {
_key = ftok (pathname.c_str (), projid);
if (_key < 0 ) ERR_EXIT ("ftok" );
if (_usertype == CREATER) Create ();
else if (_usertype == USER) Get ();
Attach ();
}
void Get () { CreateHelper (IPC_CREAT); }
int Size () { return _size; }
void *VirtualAddr () { printf ("VirtualAddr : %p\n" , _start_mem); return _start_mem; }
~Shm () { if (_usertype == CREATER) Destory (); }
private :
int _shmid;
key_t _key;
int _size;
void *_start_mem;
std::string _usertype;
};
#include "shm.hpp"
int main () {
Shm shm (pathname, projid, CREATER) ;
char * mem = (char *)shm.VirtualAddr ();
while (true ) {
printf ("%s\n" , mem);
sleep (1 );
}
return 0 ;
}
#include "shm.hpp"
int main () {
Shm shm (pathname, projid, USER) ;
char *mem = (char *)shm.VirtualAddr ();
int index = 0 ;
for (char c = 'A' ; c <= 'Z' ; index += 2 ) {
mem[index] = c;
sleep (1 );
mem[index + 1 ] = c;
sleep (1 );
}
return 0 ;
}
我们发现读写共享内存,没有使用系统调用。共享区属于用户空间,可以让用户直接使用。这也解释了前面说共享内存是最快的一种 IPC。
映射之后读写直接被对方看到。
不需要进行系统调用获取或写入内容,以指针地址的方式。
注:client 端没有执行时,server 端不会等待 client 执行,而是照样打印命令行,我们就说通信双方没有所谓的'同步机制',所以共享内存没有保护机制(对共享内存中数据的保护)。这也是 IPC 快的原因之一!
为了解决没有'保护机制的问题',我们可以与前面学的管道相结合使用,看看能不能解决?
② 共享内存 + 管道
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_FILE "fifo"
#define PATH "."
#define FILENAME "fifo"
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0)
struct Namedfifo {
public :
Namedfifo (const std::string path, const std::string name) : _path(path), _name(name) {
_fifoname = _path + "/" + _name;
umask (0 );
int n = mkfifo (_fifoname.c_str (), 0666 );
if (n < 0 ) ERR_EXIT ("mkfifo" );
else std::cout << "mkfifo success" << std::endl;
}
~Namedfifo () {
int m = unlink (_fifoname.c_str ());
if (m == 0 ) std::cout << "remove fifo success" << std::endl;
else ERR_EXIT ("unlink" );
}
private :
std::string _path, _name, _fifoname;
};
class FileOper {
public :
FileOper (const std::string &path, const std::string &name) : _path(path), _name(name), _fd(-1 ) {
_fifoname = _path + "/" + _name;
}
void OpenForRead () { _fd = open (_fifoname.c_str (), O_RDONLY); if (_fd < 0 ) ERR_EXIT ("open" ); }
void OpenForWrite () { _fd = open (_fifoname.c_str (), O_WRONLY); if (_fd < 0 ) ERR_EXIT ("open" ); }
void Wakeup () { char c = 'c' ; write (_fd, &c, 1 ); }
bool Wait () { char c; int number = read (_fd, &c, 1 ); if (c > 0 ) return true ; return false ; }
void Close () { if (_fd > 0 ) close (_fd); }
~FileOper () { int n = unlink (_fifoname.c_str ()); }
private :
std::string _path, _name, _fifoname; int _fd;
};
#pragma once
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0)
const int defaultid = -1 ;
const int gsize = 4096 ;
const std::string pathname = "." ;
const int projid = 0x66 ;
const int gmode = 0666 ;
#define CREATER "creater"
#define USER "user"
class Shm {
private :
void CreateHelper (int flag) {
printf ("key: 0x%x\n" , _key);
_shmid = shmget (_key, _size, flag);
if (_shmid < 0 ) ERR_EXIT ("shmget" );
printf ("shmid: %d\n" , _shmid);
}
void Create () { CreateHelper (IPC_CREAT | IPC_EXCL | gmode); }
void Attach () {
_start_mem = shmat (_shmid, nullptr , 0 );
if ((long long )_start_mem < 0 ) ERR_EXIT ("shmat" );
printf ("attach success\n" );
}
void Detach () { int n = shmdt (_start_mem); if (n == 0 ) printf ("shmdt success\n" ); }
void Destory () {
int n = shmctl (_shmid, IPC_RMID, nullptr );
if (n > 0 ) { printf ("shmctl delete shm : %d success\n" , _shmid); }
else ERR_EXIT ("shmctl" );
}
public :
Shm (const std::string &pathname, int projid, const std::string &usertype) : _shmid(defaultid), _size(gsize), _start_mem(nullptr ), _usertype(usertype) {
_key = ftok (pathname.c_str (), projid);
if (_key < 0 ) ERR_EXIT ("ftok" );
if (_usertype == CREATER) Create ();
else if (_usertype == USER) Get ();
Attach ();
}
void Get () { CreateHelper (IPC_CREAT); }
int Size () { return _size; }
void *VirtualAddr () { printf ("VirtualAddr : %p\n" , _start_mem); return _start_mem; }
~Shm () { Detach (); if (_usertype == CREATER) Destory (); }
private :
int _shmid;
key_t _key;
int _size;
void *_start_mem;
std::string _usertype;
};
#include "shm.hpp"
#include "Fifo.hpp"
int main () {
Shm shm (pathname, projid, CREATER) ;
Namedfifo fifo (PATH, FILENAME) ;
FileOper readerfile (PATH, FILENAME) ;
readerfile.OpenForRead ();
char *mem = (char *)shm.VirtualAddr ();
while (true ) {
if (readerfile.Wait ()) {
printf ("%s\n" , mem);
} else break ;
}
readerfile.Close ();
return 0 ;
}
#include "shm.hpp"
#include "Fifo.hpp"
int main () {
FileOper writerfile (PATH, FILENAME) ;
writerfile.OpenForWrite ();
Shm shm (pathname, projid, USER) ;
char *mem = (char *)shm.VirtualAddr ();
int index = 0 ;
for (char c = 'A' ; c <= 'Z' ; index += 2 ) {
sleep (1 );
mem[index] = c;
mem[index + 1 ] = c;
c++;
sleep (1 );
mem[index + 2 ] = 0 ;
writerfile.Wakeup ();
}
writerfile.Close ();
return 0 ;
}
2. system V 消息队列
(1)基本概念 消息队列是一种进程间通信(IPC)机制,它允许不同进程通过发送和接收消息来进行异步通信。消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法。每个数据块都被认为是有类型的(相当于给消息贴标签),接受者进程接收的数据块可以有不同的类型值。接收进程可以通过类型值实现三种接收模式:
接收模式 参数设置 行为 应用场景 先进先出 (FIFO) msgtyp=0读取队列中最早的消息 普通队列 指定类型 msgtyp>0读取队列中第一个该类型消息 优先处理告警消息 (type=1) 类型阈值 msgtyp<0读取类型≤ msgtyp 的最小类型消息
struct msqid_ds 是 System V 消息队列的核心数据结构,用于描述消息队列的属性和状态。
struct msqid_ds {
struct ipc_perm msg_perm ;
time_t msg_stime;
time_t msg_rtime;
time_t msg_ctime;
unsigned long __msg_cbytes;
msgqnum_t msg_qnum;
msglen_t msg_qbytes;
pid_t msg_lspid;
pid_t msg_lrpid;
};
(2)接口函数
① msgget() #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget (key_t key, int msgflg) ;
key:消息队列的键值(用 ftok() 生成,或使用 IPC_PRIVATE 创建私有队列)。
msgflg:权限标志(如 IPC_CREAT | 0666 表示创建并设置权限)。
返回值:成功:返回消息队列标识符(msqid)。失败:返回 -1,并设置 errno。
② msgctl() int msgctl (int msqid, int cmd, struct msqid_ds *buf) ;
msqid:消息队列标识符。
cmd:控制命令:IPC_STAT、IPC_SET、IPC_RMID。
buf:指向 struct msqid_ds 的指针。
返回值:成功:返回 0。失败:返回 -1,并设置 errno。
• 消息队列的生命周期随内核,所以要删除 IPC 资源要用 msgctl,或者【ipcrm -p msgid】 删除。查看消息队列用【ipcs- p】。
③ msgsnd() int msgsnd (int msqid, const void *msgp, size_t msgsz, int msgflg) ;
msqid:消息队列标识符。
msgp:指向消息结构体的指针。
msgsz:消息正文的大小。
msgflg:标志位。
struct msgbuf {
long mtype;
char mtext[100 ];
};
④ msgrcv() ssize_t msgrcv (int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg) ;
msqid:消息队列标识符。
msgp:指向接收消息的缓冲区。
msgsz:缓冲区大小。
msgtyp:指定接收哪种类型的消息。
msgflg:标志位。
返回值:成功:返回接收到的消息正文的字节数。失败:返回 -1,并设置 errno。
3. System V 信号量
(1)并发编程,概念铺垫
多个执行流(进程),能看到的同一份公共资源:共享资源
被保护起来的资源叫做临界资源
保护的方式常见:互斥与同步
• 任何时刻,只允许一个执行流访问资源,叫做互斥
• 多个执行流,访问临界资源的时候,具有一定的顺序性,叫做同步
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源
在进程中涉及到互斥资源的程序段叫临界区。
(2)原子性
原子性 是指 一个操作(或一系列操作)要么完全执行,要么完全不执行,不会被其他线程/进程打断。原子操作是不可分割的最小执行单元,在执行过程中不会被中断。
在并发编程中,多个线程/进程可能同时访问共享资源,如果操作不是原子的,可能会导致 竞态条件 ,造成数据不一致。
(3)什么是信号量? 信号量与已经介绍过的 IPC 机构不同。信号量本质是一个计数器 ,用于表明临界资源中资源数量的多少,为多个进程提供队共享数据对象的访问。
测试控制该资源的信号量。
若此信号量的值为正,则进程可以使用该资源。在这种情况下,进程会将信号量值减 1,表示它使用了一个资源单位。(资源的预定机制)
否则,若此信号量的值为 0,则进程进入等待状态,直至信号量大于 0。
信号量本身也是共享资源,所以对信号量的操作也必须是原子的!为此,信号量通常是在内核中实现的。
当进程不再使用由一个信号量控制的共享资源时,该信号量值加 1(V 操作)。如果有进程正在休眠等待此信号量,内核会唤醒一个等待进程(按队列顺序或优先级),被唤醒的进程会自动给信号量值减 1(P 操作)。
操作 名称 行为 P (Proberen)等待/获取 尝试减少信号量值。若值 ≥ 1,则减 1 并继续;否则进程阻塞(休眠)。 V (Verhogen)释放/唤醒 增加信号量值。若有进程在等待该信号量,则唤醒其中一个,信号量值保持 0(被唤醒的进程会自动完成其 P 操作)。
常用的信号量形式被称为二元信号量 ,其值只能是 0 或 1。它控制单个资源,其初始值为 1。
0:资源被占用,其他进程/线程必须等待。
1:资源可用,可以立即获取。
先访问信号量 P,每个进程都得看到同一个信号量,满足进程间通信的前提。
通信的本质是信息传递!不是传递数据才是通信,通知、同步和互斥也是通信!
• 查看信号量【ipcs -s semid】,删除信号量【ipcrm -s】.
(4)信号量接口函数
① semget() #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget (key_t key, int nsems, int semflg) ;
• int nsems 指定信号量集中信号量的数量。
• int semflg 控制函数行为的标志位
返回值:成功:返回信号量集标识符(非负整数),失败:返回 -1,并设置 errno。
② semctl() #include <sys/ipc.h>
#include <sys/sem.h>
int semctl (int semid, int semnum, int cmd, ... ) ;
semid:信号量集的标识符。
semnum:指定信号量集中的具体信号量编号。
cmd:指定对信号量执行的操作命令。
...:可变参数,具体取决于 cmd 的值。
union semun {
int val;
struct semid_ds *buf ;
unsigned short *array ;
};
成功时,返回值取决于 cmd 的类型。
失败时,返回 -1,并设置 errno。
③ semop() #include <sys/ipc.h>
#include <sys/sem.h>
int semop (int semid, struct sembuf *sops, size_t nsops) ;
struct sembuf *sops:指向一个 struct sembuf 类型的数组。
size_t nsops:数组 sops 中的元素数量。
struct sembuf {
unsigned short sem_num;
short sem_op;
short sem_flg;
};
struct sembuf 是一个结构体,用于定义对信号量的操作。
sem_op :指定对信号量的操作类型:正数(V 操作),负数(P 操作),0(等待变为 0)。
sem_flg :指定操作标志:IPC_NOWAIT,SEM_UNDO。
返回值:成功时,返回 0。失败时,返回 -1,并设置 errno。
(5)内核信号量集属性 内核为每一个信号量集合维护者一个 semid_ds 结构,用于描述信号量集属性的结构体。
struct semid_ds {
struct ipc_perm sem_perm ;
time_t sem_otime;
time_t sem_ctime;
unsigned short sem_nsems;
};
struct ipc_perm {
key_t key;
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
};
四、内核是如何组织管理 IPC 资源的? 内核通过 统一权限模型(kern_ipc_perm)+ 分类资源管理(信号量/消息队列/共享内存)+ 命名空间隔离 实现 IPC 资源的高效组织。核心逻辑集中在 ipc/util.c 中提供通用服务,具体资源由各自模块实现,系统调用作为用户入口。
(1)核心数据结构物理布局
struct ipc_namespace {
struct ipc_ids ids [3];
struct shmem_info shm_info ;
};
struct ipc_ids {
struct kern_ipc_perm *entries ;
int in_use;
unsigned short seq;
struct rw_semaphore rwsem ;
};
struct kern_ipc_perm {
key_t key;
uid_t uid;
gid_t gid;
mode_t mode;
int id;
};
+---------------------+
| ipc_namespace |
|---------------------|
| *ids[3] |
| [0] → sem_ids |
| [1] → msg_ids |
| [2] → shm_ids |
| shm_info |
+---------------------+
+---------------------+
| ipc_ids (sem_ids) |
|---------------------|
| *entries |
| [0] → sem_array1 |
| [1] → NULL |
| [2] → sem_array2 |
| ... |
| in_use=2 |
+---------------------+
(2)内核源代码关系图示 相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
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