💻文章目录
- 📄前言
- 进程间通信基础
- 概念
- 管道
- 概念
- 管道的工作原理
- 模拟实现shell中的管道
- 共享内存
- 概念
- 接口的介绍
- 共享内存的使用
- 📓总结
📄前言
你是否了解进程间是如何通信的呢?你是否知道管道的工作原理呢?管道是Linux中最基本的也是最常用的进程间通信手段,----(Todo)
进程间通信基础
概念
进程间通信(Inter-Process Communication)简称IPC,是不同进程之间传递数据的手段、接口。它是多进程间协同工作的核心机制。
Linux 主要的IPC接口有:
- 管道(pipe)
- 信号(Signal)
- 共享内存(Shared Memory)
- 信号量(Semaphore)
- 消息队列(Message Queue)
- 套接字(Socket)
本文将着重介绍管道与共享内存两种方式。
管道
概念
管道大概是最多Linux用户使用过的一种的IPC接口了吧,只要有学过控制台代码,你就一定会认识它。它是一种先进先出的结构,数据的流向是单向的,最简单的使用管道的方法就是在 shell 中使用 “ | ”.
echo "管道的使用方法" | grep "test.c"
# 其功能就像它的名字,将进程数据通过一个管子传到另一个进程
# [进程A] ---> |管道| ---> [进程B]
管道的类型有两种
-
匿名管道: 主要用于父子进程间的通信,在文件系统中没有一个实际的名称,进程结束便消失。
-
命名管道: 可用于不同进程之间的通信,与匿名管道不通命名管道在文件系统中有一个实际名称,可长久存在,并且可以通过两个管道来实现双向通信。
管道的工作原理
管道在实质上可以看成缓冲区,一端用于写,一端用于读。在进程将数据写入管道时,数据被储存在系统内核的缓冲区中,而不是直接存到管道文件中,管道文件只是作为一个标识符用于不同进程打开管道。正所谓实践出真知,其他的原理,让我们通过 Coding 来知晓吧。
- 匿名管道的使用:
// 匿名管道与命名管道的使用
// 头文件 unistd.h
// int pipe(int pipefd[2]);
// pipefd[0]被设为管道的读端,[1]为写端
const char *msg = "i like linux!";
void test_1()
{
char buf[1024] = {0};
int fd[2];
pipe(fd);
pid_t id = fork(); //生成子进程
if(id < 0) exit(1);
else if(id == 0) // 子进程入口
{
close(fd[0]);
write(fd[1], msg, strlen(msg));
close(fd[1]);
exit(0);
}
// 父进程
close(fd[1]); //关闭读端
read(fd[0], buf, sizeof(buf));
printf("%s\n", buf);
close(fd[0]);
wait(NULL); //等待子进程结束
}
// 父进程 子进程
// +----------------+ +----------------+
// | | 系统缓冲区 | |
// | 写入 fd[1] |------------>| 读取 fd[0] |
// | | 管道 | |
// +----------------+ +----------------+
// 写入端 读入端
// 你可能会有些疑问,为什么要用这么多个close。
// 关闭不需要用的文件描述符是个良好的编程习惯
// 因为文件描述符是有限的资源,而且不关闭会在某些情况下,导致进程阻塞。
- 命名管道的使用:
// 头文件 sys/types.h sys/stat.h
// int mkfifo(const char* pathname, mode_t mode) 创建管道文件
// pathname:管道的名称 mode:设置文件的权限。
// server.cpp
int main()
{
int ret = mkfifo("fifo", 0644);
const char* msg = "server:i love linux"
int fd = open("fifo", O_WRONLY); // O_WRONLY:只写模式
for (int i = 0; i < 10; ++i)
{
write(fd, msg, strlen(msg)); //读端没有打开,进程则会阻塞
sleep(1);
}
return 0;
}
// client.cpp
int main()
{
int fd = open("fifo", O_RDONLY); // O_RDONLY:只读模式
char buf[1024];
while (1)
{ // 如果管道写端没有打开,进程则会阻塞
ssize_t n = read(fd, buf, sizeof(buf));
buf[n] = '\n';
if (n == 0) // 读端断开连接,返回0
{
printf("process exit\n");
return 0;
}
else if (n > 0)
{
write(1, buf, n + 1); // 向标准输出打印
}
}
return 0;
}
管道的读写规则:
- 当管道读写端任意一端未打开,则进程会阻塞等待。
- 管道写满了,尝试写入的进程会阻塞
- 管道无数据可读,如果写端关闭则返回0,写端没有关闭则阻塞进程。
模拟实现shell中的管道
int main()
{
int pipefd[2];
if(pipe(pipefd) == -1 && errno != EEXIST)
{
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(pid == 0)
{ //子进程
dup2(pipefd[1], STDOUT_FILENO);
//dup = duplicate(复制),将pipefd[1]复制到标准输出(1号)
// 像标准输出打印的数据都讲传入管道写端
close(pipefd[0]);
close(pipefd[1]); // 已经将其复制到标准输出,可以关闭
char* argv[] = {"ls", "-l", nullptr};
execvp(argv[0], argv); //执行命令
exit(0);
}
pid_t pid2 = fork();
if(pid2 == 0)
{ //子进程
dup2(pipefd[0], STDIN_FILENO); //将fd[0]复制到标准输入
close(pipefd[0]);
close(pipefd[1]);
char* argv[] = {"grep", (char*)"test", nullptr};
execvp(argv[0], argv);
exit(0);
}
close(pipefd[0]);
close(pipefd[1]);
wait(nullptr);
wait(nullptr);
return 0;
}
共享内存
概念
共享内存是所有IPC机制中最快的一种机制,它能使得多个进程访问同一块内存区域,而不用在进程间复制拷贝。 例如管道,它实际就是系统中的一块缓存区,两个进程间交流就必须将缓冲区的数据拷贝的自己内存中。共享内存则是在物理内存中开辟一段空间,然后通过页表将其映射到程序的共享区,进程直接对内存进行读写。
+---------+ +-------------------+ +---------+
| | ----> | | <---- | |
| Process | | Shared Memory | | Process |
| A | <---- | Segment | ----> | B |
| | | | | |
+---------+ +-------------------+ +---------+
^ ^
| |
Read Write
接口的介绍
在使用共享内存前,必须先简单介绍一下System V 与 POSIX,它们是UNIX系统的两种不同系统标准,而在Linux上他们两者的接口都有兼容,接下来我们要使用的共享内存属于System V的。
要使用共享内存就得先知道如何检查系统中的共享内存,以及如果程序遇到异常时,如何删除共享内存。
#显示共享内存
ipcs -m
# 使用 ipcs 命令来检查system V的通信信息,默认情况下显示所有的资源。
# 删除共享内存
ipcrm [shm|msg|sem] ID ...
# 选项
# -m 根据共享内存的shmid来删除
# -M 根据共享内存的shmeky来删除
共享内存的接口:
// 接口介绍
// 涉及头文件: <sys/ipc.h> <sys/shm.h>
key_t ftok(const char* pathname, int proj_id);
//使用ftok来生成唯一的key值,参数为路径名与项目ID
int shmget(key_t key, size_t size, int shmflg);
// 返回值为shmid
// 根据key来生成共享,size为共享内存的字节数只能为1024的倍数。
// shmflg:共享内存的权限,一般使用像 0644,IPC_CREAT、IPC_EXCL等。
// 挂载共享内存
void* shmat(int shmid, const void* shmaddr, int shmflg);
// shmid 为shmget的返回值
// shmaddr:将共享内存连接到当前进程地址空间的特定地址
// shmflg:一般为0或者SHM_RDONLY(只读)
//取消挂载共享内存
int shmdt(const void* shmaddr);
// shmaddr :共享内存的地址
//控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
// shmid:共享内存的id号
// cmd:控制命令,一般为IPC_STAT(获取共享内存的状态)、IPC_SET(设置共享内存的参数)、IPC_RMID(删除共享内存)
// buf:指向shmid_ds 结构的指针,用于设置共享内存或存储共享内存的状态。
共享内存的使用
共享内存主要用于两个不同进程数据的交流,这里将使用两个不同进程,一个用于写数据,一个用于读数据。因为共享内存的读写是无法预测的(对方不知道你何时写了),所以需要用到命名管道来辅助。
server.cpp:
// server.cpp
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstring>
#include <string>
#include <unistd.h>
const char* pathname = "/home/catianri/code";
const int project_id = 0x11223344;
int main() {
key_t key = ftok(pathname, project_id); // 创建唯一的key
int cnt = 0, code = 0; // code用于通知对方进程,已经开始写入。
int shmid = shmget(key, 1024, 0666|IPC_CREAT); // 创建共享内存
mkfifo(".fifo", 0666); //创建命名管道
int fd = open(".fifo", O_WRONLY);
int* arr = static_cast<int*>(shmat(shmid, nullptr, 0)); // 将共享内存附加到进程的地址空间
while(cnt < 10)
{
arr[cnt++] = cnt; // 向共享内存写入数据
write(fd, &code, sizeof(int)); // 通知另一个进程
sleep(1);
}
shmdt(arr); // 断开共享内存连接
shmctl(shmid, IPC_RMID, NULL); // 销毁共享内存
close(fd);
return 0;
}
client.cpp:
#include <iostream>
#include <cstdio>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
const char* pathname = "/home/catianri/code";
const int project_id = 0x11223344;
int main() {
key_t key = ftok(pathname, project_id); // 使用相同的文件和项目ID来创建key
int shmid = shmget(key, 1024, 0666|IPC_CREAT); // 连接到共享内存
int *arr = static_cast<int*>(shmat(shmid, nullptr, 0)); // 将共享内存附加到进程的地址空间
mkfifo(".fifo", 0666);
int fd = open(".fifo", O_RDONLY);
int* code, cnt = 0;
while(1)
{
ssize_t n = read(fd, &code, sizeof(int));
if(n == 0) //server已经断开写端
{
close(fd);
std::cout << "process exit" << std::endl;
}
else
{ //拿取共享内存中的数据。
std::cout << arr[cnt++] << " ";
fflush(stdout); //刷新缓冲区。
}
}
shmdt(arr); // 断开共享内存连接
return 0;
}
共享内存的特点:
- 高效性: 共享内存是最快的IPC方式,它允许进程直接访问共享数据,避免了拷贝操作。
- 灵活性: 共享内存提供了高度的灵活性,开发者可以根据需要自定义共享数据的结构和管理方式。
- 手动同步: 由于可以多进程同时访问,所以需要外部同步机制。
- 复杂性: 与其他IPC机制(如管道和消息队列)相比,共享内存的使用和管理更为复杂。
📓总结
特性 | 管道 | System V共享内存 |
---|---|---|
性能和效率 | 数据需要在进程间复制,存在额外的CPU开销和延迟。 | 允许多个进程直接访问同一内存区,减少了数据复制,提高了效率。 |
使用场景 | 适用于顺序数据流通信,常用于父子进程或紧密相关进程间。 | 适用于性能要求高的场景,如大数据处理、实时系统,因为它几乎无延迟地实现数据共享。 |
同步机制 | 自带同步机制,写入端和读取端会在必要时阻塞,直到对方准备好。 | 需要额外的同步机制(如信号量、互斥锁)来防止数据竞争和保证一致性。 |
容错性和可靠性 | 相对简单可靠,但需要正确处理EOF和管道破裂等情况。 | 需要仔细管理资源和同步,避免竞态条件、死锁或数据损坏。 |
易用性 | API简单,容易实现和维护,但功能有限。 | 提供了一套功能丰富的API,但相较于管道,使用和维护更加复杂。 |
📜博客主页:主页
📫我的专栏:C++
📱我的github:github