您的位置:首页 > 健康 > 美食 > 操作系统:进程间通信方式详解(上:无名管道、有名管道、高级管道)

操作系统:进程间通信方式详解(上:无名管道、有名管道、高级管道)

2024/12/28 6:57:12 来源:https://blog.csdn.net/upgrador/article/details/142300017  浏览:    关键词:操作系统:进程间通信方式详解(上:无名管道、有名管道、高级管道)

操作系统:进程间通信方式详解(上:无名管道、有名管道、高级管道)

在现代操作系统中,进程间通信(Inter-Process Communication,IPC)是实现多个进程之间数据交换和同步的关键技术。进程间通信提供了一系列机制,帮助进程在分布式或本地环境中高效、安全地进行数据传递。而现有的关于进程间通信介绍往往局限于概念的介绍,本文则结合实际应用,通过极为详细的可以运行的 C++ 和 Java 示例代码,帮助读者理解这些机制的实现原理和应用场景。至于后续的关于消息队列、信号量、共享内存和套接字的介绍,请看下篇


文章目录

  • 操作系统:进程间通信方式详解(上:无名管道、有名管道、高级管道)
    • 一、进程间通信概述
    • 二、无名管道(Pipe)
      • 2.1 无名管道的定义与特点
      • 2.2 无名管道的C++示例代码
      • 2.3 无名管道的Java示例代码
    • 三、有名管道(Named Pipe)
      • 3.1 有名管道的定义与特点
      • 3.2 有名管道的C++示例代码
      • 3.3 有名管道的Java示例代码
    • 四、高级管道(popen)
      • 4.1 高级管道的定义与特点
      • 4.2 C++ 示例代码
      • 4.3 Java 示例代码
    • 五、总结(前面可以跳过,这里表格必看)
      • 5.1 区别对比表
      • 5.2 详细区别分析


一、进程间通信概述

进程间通信是多个进程之间传递数据或同步操作的机制。在操作系统中,进程是独立的执行单元,拥有独立的内存空间,因此需要特殊的机制来实现数据的共享与传递。常见的进程间通信方式有无名管道、命名管道、消息队列、信号量、共享内存和套接字,它们各有优缺点,适用于不同的场景。

二、无名管道(Pipe)

2.1 无名管道的定义与特点

无名管道(Pipe)是最简单的进程间通信方式之一,用于具有亲缘关系的进程之间(如父子进程)的单向数据传输。无名管道由内核创建,使用文件描述符进行读写操作。

2.2 无名管道的C++示例代码

无名管道(Pipe)是一种在父子进程间进行单向数据传输的机制,管道通过内核提供的文件描述符进行读写操作,数据只能从写端进入从读端取出。在C++中,管道通常用于父子进程之间的简单通信。管道的生命周期是进程的生命周期,当所有关联的进程终止时,管道也随之销毁。

以下代码展示了无名管道在父子进程之间传递数据的实现方式:

#include <iostream>  // 导入输入输出流库
#include <unistd.h>  // 导入 POSIX 操作系统 API 包括 pipe() 和 fork()
#include <cstring>   // 导入字符串操作库int main() {int fd[2];  // 定义文件描述符数组 fd[0] 是读端,fd[1] 是写端pid_t pid;  // 定义进程 IDchar buffer[50];  // 定义缓冲区// 创建无名管道if (pipe(fd) == -1) {  // 使用 pipe() 函数创建管道,失败返回 -1perror("pipe failed");  // 输出错误信息return 1;  // 结束程序}// 创建子进程pid = fork();  // fork() 创建子进程,返回 0 表示子进程,返回正数表示父进程if (pid < 0) {  // 如果返回值小于 0,表示创建子进程失败perror("fork failed");  // 输出错误信息return 1;  // 结束程序}if (pid == 0) {  // 子进程代码块close(fd[0]);  // 关闭子进程中的管道读端,因为子进程只需要写入数据const char* message = "Hello from child!";  // 定义要发送的消息write(fd[1], message, strlen(message) + 1);  // 使用 write() 函数将消息写入管道close(fd[1]);  // 关闭子进程中的写端} else {  // 父进程代码块close(fd[1]);  // 关闭父进程中的管道写端,因为父进程只需要读取数据read(fd[0], buffer, sizeof(buffer));  // 使用 read() 函数从管道中读取数据std::cout << "Received from child: " << buffer << std::endl;  // 输出从子进程接收到的消息close(fd[0]);  // 关闭父进程中的读端}return 0;  // 程序结束
}

解释
在这个示例中:

  • 父进程使用 pipe() 创建无名管道,并通过 fork() 创建子进程。子进程从父进程继承了管道的文件描述符。
  • 子进程关闭管道的读端,使用 write() 将消息写入管道的写端,然后关闭写端。
  • 父进程关闭管道的写端,使用 read() 从管道的读端读取子进程写入的消息并打印输出。管道在进程终止时自动销毁。

2.3 无名管道的Java示例代码

在Java中,PipedInputStreamPipedOutputStream 类用于实现无名管道,允许在两个线程之间进行单向数据传输。它们的工作原理类似于C++中的无名管道。PipedOutputStream 用于将数据写入管道,而 PipedInputStream 用于从管道读取数据。

以下是无名管道的Java版本,利用 PipedInputStreamPipedOutputStream 实现数据在两个线程之间的传递:

import java.io.PipedInputStream;  // 导入 PipedInputStream 类,用于接收管道输入数据
import java.io.PipedOutputStream;  // 导入 PipedOutputStream 类,用于发送管道输出数据public class PipeExample {public static void main(String[] args) throws Exception {  // throws Exception 表示可能抛出异常PipedInputStream input = new PipedInputStream();  // 创建管道输入流对象,用于读取数据PipedOutputStream output = new PipedOutputStream(input);  // 创建管道输出流对象,并连接到输入流// 创建子线程模拟子进程,用于写入数据Thread writerThread = new Thread(() -> {try {String message = "Hello from writer!";  // 定义要发送的消息output.write(message.getBytes());  // 将消息转换为字节数组并写入输出流output.close();  // 关闭输出流} catch (Exception e) {e.printStackTrace();  // 捕获并输出异常}});writerThread.start();  // 启动子线程// 主线程读取管道中的数据int data;  // 用于保存读取的数据StringBuilder message = new StringBuilder();  // 用于保存完整的消息while ((data = input.read()) != -1) {  // 使用 read() 方法从输入流中读取数据,直到流的结尾message.append((char) data);  // 将读取到的数据转为字符并添加到 message 中}System.out.println("Received: " + message);  // 输出接收到的消息input.close();  // 关闭输入流}
}

解释

  • PipedInputStreamPipedOutputStream 是Java中实现无名管道的类,用于在线程之间传递数据。
  • output.write() 将数据写入管道,input.read() 从管道读取数据。
  • throws Exception 用于声明方法可能抛出的异常,简化错误处理。
  • 此代码中,子线程 模拟子进程,主线程 读取数据,实现了两个线程之间的通信。管道在两端关闭后自动销毁。

通过这些示例,可以看出无名管道在不同语言中的实现方式和使用场景。无名管道的核心是通过内存缓冲区实现进程或线程之间的数据传输,适用于简单的数据交换任务。

三、有名管道(Named Pipe)

3.1 有名管道的定义与特点

有名管道(Named Pipe),又称为 FIFO(First In, First Out),允许无亲缘关系的进程之间进行通信。有名管道在文件系统中具有名称,可以实现双向通信。使用有名管道的典型场景是多个进程需要在不同时间进行数据交换。

3.2 有名管道的C++示例代码

以下是有名管道在两个独立进程之间通信的示例:

首先,创建有名管道:

mkfifo /tmp/myfifo  # 使用 mkfifo 命令在 /tmp 目录下创建一个名为 myfifo 的有名管道

进程1(写入端):

#include <iostream>  // 标准输入输出库
#include <fcntl.h>   // 文件控制定义,用于 open() 函数
#include <unistd.h>  // POSIX 操作系统 API,包括 read()、write()、close() 等函数
#include <cstring>   // 字符串操作函数库int main() {int fd = open("/tmp/myfifo", O_WRONLY);  // 使用 open() 以只写模式打开有名管道if (fd == -1) {  // 检查是否打开成功perror("Failed to open named pipe");  // 输出错误信息return 1;}const char* message = "Hello from process 1!";  // 定义要发送的消息write(fd, message, strlen(message) + 1);  // 将消息写入管道,+1 用于包括字符串终止符close(fd);  // 关闭文件描述符,释放资源return 0;
}

进程2(读取端):

#include <iostream>  // 标准输入输出库
#include <fcntl.h>   // 文件控制定义,用于 open() 函数
#include <unistd.h>  // POSIX 操作系统 API,包括 read()、write()、close() 等函数int main() {int fd = open("/tmp/myfifo", O_RDONLY);  // 使用 open() 以只读模式打开有名管道if (fd == -1) {  // 检查是否打开成功perror("Failed to open named pipe");  // 输出错误信息return 1;}char buffer[50];  // 定义一个缓冲区用于接收消息read(fd, buffer, sizeof(buffer));  // 从管道读取数据到缓冲区std::cout << "Received: " << buffer << std::endl;  // 输出接收到的消息close(fd);  // 关闭文件描述符,释放资源return 0;
}

解释

  1. mkfifo /tmp/myfifo:使用 mkfifo 命令创建一个有名管道,它在文件系统中生成一个特殊文件,进程可以通过该文件进行通信。
  2. open():用于打开管道文件,O_WRONLY 表示只写模式,O_RDONLY 表示只读模式。
  3. write()read():进行数据传输,write 将消息写入管道,read 从管道读取消息。
  4. close():关闭管道文件描述符,释放资源。

3.3 有名管道的Java示例代码

在 Java 中没有直接实现有名管道的类,但可以通过文件 I/O 操作模拟有名管道的行为。这种方式虽然不是真正的管道通信,但在效果上实现了进程间的数据传递。

写入端(模拟有名管道的写入操作):

import java.io.FileWriter;  // 导入 FileWriter 类,用于写入字符文件
import java.io.IOException;  // 导入 IOException 类,用于捕获输入输出异常public class NamedPipeWriter {public static void main(String[] args) {try (FileWriter writer = new FileWriter("/tmp/myfifo")) {  // 使用 FileWriter 打开 /tmp/myfifo 文件writer.write("Hello from Java process!");  // 将消息写入文件} catch (IOException e) {  // 捕获文件操作中可能发生的异常e.printStackTrace();  // 输出异常的堆栈信息}}
}

读取端(模拟有名管道的读取操作):

import java.io.BufferedReader;  // 导入 BufferedReader 类,用于读取文本内容
import java.io.FileReader;  // 导入 FileReader 类,用于读取字符文件
import java.io.IOException;  // 导入 IOException 类,用于捕获输入输出异常public class NamedPipeReader {public static void main(String[] args) {try (BufferedReader reader = new BufferedReader(new FileReader("/tmp/myfifo"))) {  // 使用 BufferedReader 读取 /tmp/myfifo 文件String message = reader.readLine();  // 读取一行文本System.out.println("Received: " + message);  // 输出接收到的消息} catch (IOException e) {  // 捕获文件操作中可能发生的异常e.printStackTrace();  // 输出异常的堆栈信息}}
}

解释

  1. FileWriterFileReader:用于文件的读写操作。虽然它们本质上是文件 I/O 类,但在有名管道模拟中,这些文件实际上是有名管道。
  2. BufferedReader:用于读取文本行,提供了更高效的读取机制。
  3. IOException:处理可能发生的 I/O 异常,确保程序健壮性。
  4. 通信过程:Java程序通过文件 I/O 与有名管道进行交互,写入端将数据写入到管道文件,读取端则从该文件中获取数据,达到跨进程通信的目的。

这些示例展示了有名管道在 C++ 和 Java 中的实现及应用。C++ 直接调用系统级函数实现了进程间通信,而 Java 通过文件操作模拟了管道的行为。这两种实现方式都展示了有名管道如何在不同环境中进行数据传递。

四、高级管道(popen)

4.1 高级管道的定义与特点

高级管道(popen) 是一种利用标准库函数 popen() 来创建管道的方式,常用于父进程与子进程之间的通信。与无名管道不同,popen() 函数不仅能够创建管道,还能启动子进程并连接子进程的标准输入或输出。

4.2 C++ 示例代码

高级管道是通过 popen() 函数或其他系统调用来执行系统命令或可执行程序,创建一个子进程,并在父进程和子进程之间建立通信通道。高级管道允许父进程与子进程进行数据交换,这种方式常用于将程序的输出作为另一程序的输入,或将输入传递给子进程进行处理。
以下是通过 popen() 函数实现高级管道通信的 C++ 示例代码:

#include <iostream>  // 导入标准输入输出流库
#include <cstdio>    // 导入 C 标准 I/O 库
#include <cstdlib>   // 导入 C 标准库,用于系统相关的函数int main() {FILE *fp;  // 定义文件指针用于指向管道char buffer[128];  // 定义缓冲区用于存储管道读取的数据// 使用 popen 打开一个管道,执行系统命令 `ls -l`,模式为读取 ("r")fp = popen("ls -l", "r");if (fp == nullptr) {  // 检查管道是否成功打开perror("popen failed");  // 输出错误信息return 1;  // 返回错误码}// 使用 fgets 从管道读取输出行并输出到标准输出while (fgets(buffer, sizeof(buffer), fp) != nullptr) {std::cout << buffer;  // 打印读取到的行}// 使用 pclose 关闭管道,释放资源pclose(fp);return 0;
}

解释

  1. popen():用于打开一个管道并执行给定的命令,返回一个 FILE* 指针,允许通过该指针读写子进程的标准输入输出。
    • 语法FILE *popen(const char *command, const char *mode);
    • command:执行的命令。
    • mode:操作模式,"r"表示读取子进程的输出,"w"表示写入子进程的输入。
  2. fgets():从管道中读取输出,将其存入缓冲区。
  3. pclose():关闭管道并等待子进程结束,释放资源。

4.3 Java 示例代码

在 Java 中,可以使用 Runtime.getRuntime().exec() 方法来执行系统命令,并通过 InputStream 读取子进程的输出,实现类似高级管道的功能。

import java.io.BufferedReader;  // 导入 BufferedReader 类,用于读取输入流
import java.io.InputStreamReader;  // 导入 InputStreamReader 类,用于转换输入流public class AdvancedPipe {public static void main(String[] args) {try {// 使用 Runtime.getRuntime().exec() 执行系统命令并创建进程Process process = Runtime.getRuntime().exec("ls -l");// 使用 InputStreamReader 包装进程的输入流,并进一步包装为 BufferedReaderBufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));String line;  // 定义字符串变量用于存储读取的每一行// 逐行读取命令的输出并打印到控制台while ((line = reader.readLine()) != null) {System.out.println(line);}// 关闭 BufferedReader,释放资源reader.close();} catch (Exception e) {  // 捕获和处理可能发生的异常e.printStackTrace();  // 打印异常堆栈信息}}
}

解释

  1. Runtime.getRuntime().exec():用于执行系统命令,返回一个 Process 对象,代表正在执行的进程。
    • 语法Process exec(String command);
    • command:要执行的系统命令。
  2. InputStreamReaderBufferedReader:将进程的输入流包装为可读的字符流,方便逐行读取子进程输出。
  3. readLine():逐行读取子进程的输出,直到所有输出读取完成。

总结:通过 popen()(C++)和 Runtime.exec()(Java)实现的高级管道通信机制,可以将父进程与子进程连接起来,使得父进程能够通过输入输出流与子进程交互。这种方式常用于自动化任务和脚本化操作,广泛应用于各种系统级编程中。

解释:这些代码展示了高级管道的使用,popen() 在 C 中通过命令行执行程序,Java 中则通过 exec() 方法启动进程,并将标准输出通过流返回给父进程。


五、总结(前面可以跳过,这里表格必看)

无名管道、有名管道、高级管道的区别(总结表格)(前面可以跳过,这里表格必看)

无名管道、有名管道和高级管道是最常见的管道通信方式,它们各自具有不同的特点和应用场景。以下是它们之间的主要区别:

  1. 无名管道(Pipe)
    无名管道是进程间通信的基本方式,通常用于父子进程之间的数据传递。管道是单向的,数据从一个端口写入,从另一个端口读取。无名管道在进程创建时由操作系统分配,没有名称,仅在创建的进程之间可用。

  2. 有名管道(Named Pipe, FIFO)
    有名管道支持无亲缘关系的进程间通信,管道在文件系统中有一个路径名。它允许双向通信,并可以在不同的进程间共享。由于有名管道存在文件系统中,因此即使进程关闭后,管道文件依然存在,直到手动删除。

  3. 高级管道(popen)
    高级管道通过 popen() 函数创建,结合了管道和进程控制的功能。它允许父进程与子进程之间进行数据交互,同时还能启动和控制子进程的执行。高级管道适用于需要通过执行系统命令与子进程进行简单通信的场景。

5.1 区别对比表

特性无名管道(Pipe)有名管道(Named Pipe, FIFO)高级管道(popen)
数据传输方向单向双向单向
是否有名称有名称,存在于文件系统中
是否支持无亲缘关系进程是(但多用于父子进程)
存在范围仅在创建的进程之间文件系统中的命名文件进程间通信
是否需要显式创建由系统自动创建需要通过 mkfifo 等命令创建popen() 自动创建
适用场景父子进程的简单数据传递不相关进程的通信,常用于客户端与服务端父子进程间数据传递及命令控制
编程复杂性
性能
数据持久性不持久管道文件持久,但数据不持久不持久

5.2 详细区别分析

  1. 数据传输方向:无名管道和高级管道都仅支持单向数据传输,这意味着数据只能从一端写入从另一端读取。有名管道支持双向通信,可以实现更加灵活的进程间数据交换。

  2. 命名和存在范围:无名管道由操作系统管理,没有名称,仅在创建的进程之间存在;有名管道存在于文件系统中,有明确的名称,可以被不同进程访问;高级管道没有名称,但可以在父子进程之间快速创建和销毁。

  3. 亲缘关系要求:无名管道严格要求在有亲缘关系(如父子进程)间使用,而有名管道和高级管道则支持无亲缘关系的进程间通信,扩展了应用场景。

  4. 适用场景和编程复杂性:无名管道编程简单,适合基本的父子进程通信;有名管道适用于需要在不相关进程间实现持久通信的场景;高级管道结合了进程控制和数据传输,适合需要通过命令行交互的场合。

总结:在具体开发中,应根据通信需求选择合适的管道类型。无名管道适合简单的父子进程通信,有名管道用于需要名称且能在文件系统中访问的进程通信,而高级管道则适合与系统命令或外部进程的简单交互。理解它们之间的区别,可以更好地应用到不同的操作系统开发场景中。

✨ 我是专业牛,一个渴望成为大牛🏆的985硕士🎓,热衷于分享知识📚,帮助他人解决问题💡,为大家提供科研、竞赛等方面的建议和指导🎯。无论是科研项目🛠️、竞赛🏅,还是图像🖼️、通信📡、计算机💻领域的论文辅导📑,我都以诚信为本🛡️,质量为先!🤝

如果你觉得这篇文章对你有所帮助,别忘了点赞👍、收藏📌和关注🔔!你的支持是我继续分享知识的动力🚀!✨ 如果你有任何问题或需要帮助,随时留言📬或私信📲,我都会乐意解答!😊

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com