当今计算机系统中,进程间通信扮演着至关重要的角色。随着计算机系统的发展和复杂性的增加,多个进程之间的协作变得更加必要和常见。进程间通信使得不同进程能够共享资源、协调工作、传输数据,并实现更加复杂和强大的功能。本文将深入探讨进程间的通信,以及管道的作用。它为多个进程提供了一种有效的交互方式,使得系统能够更好地协同工作、共享资源,并实现更高级别的功能。通过恰当地选择和使用进程间通信的方式,我们可以构建出高效、可靠且高度协同的系统。下面话不多说坐稳扶好咱们要开车了
进程间通信(IPC)是操作系统中的一个重要概念,它允许不同的进程在执行过程中交换数据、共享资源、协调行为等。在多道程序设计环境下,多个进程可能需要相互通信以完成复杂的任务,而进程间通信提供了各种机制来实现这种交互。
进程间通信的主要目的包括:
以下是几种常见的进程间通信方式:
管道(Pipe):管道是一种半双工的通信方式,用于具有亲缘关系的进程间通信。它可以是匿名管道(使用pipe
系统调用)或命名管道(使用mkfifo
命令),并且数据只能在一个方向上流动。
信号(Signal):信号是一种异步的通信机制,用于通知进程发生了某种事件。进程可以向另一个进程发送信号,比如终止信号(SIGTERM
)、中断信号(SIGINT
)等。
消息队列(Message Queue):消息队列是一种消息传递机制,可以在不同进程之间按队列方式传递数据。它允许一个进程向另一个进程发送消息,而不需要直接的数据连接。
共享内存(Shared Memory):共享内存允许多个进程访问同一块物理内存,因此它是最快的 IPC 方式之一。但需要开发者自行解决竞争条件和同步的问题。
信号量(Semaphores):信号量是一种计数器,用于控制对共享资源的访问。它通常与共享内存一起使用,以避免多个进程同时访问共享内存时产生的竞争条件。
套接字(Socket):套接字是一种进程间通信的常见方式,可以用于不同主机之间的通信,也可以用于同一主机上不同进程之间的通信。
管道(Pipe)是一种在UNIX和类UNIX系统中用于进程间通信的机制。管道允许一个进程将其输出直接发送到另一个进程的输入,从而实现两个进程之间的数据传输。
管道的特点包括:
在Linux系统中,pipe()
函数用于创建匿名管道,它是一个系统调用函数,位于
头文件中。该函数创建一个管道,返回两个文件描述符,一个用于读取数据,另一个用于写入数据。
语法
#include
int pipe(int pipefd[2]);
参数
pipefd
: 一个整型数组,用于存储管道的文件描述符。pipefd[0]
用于从管道中读取数据,pipefd[1]
用于向管道中写入数据。返回值
pipe()
系统调用来创建匿名管道。pipe()
系统调用会创建一个管道,返回两个文件描述符,一个用于读取数据,另一个用于写入数据。pipe()
系统调用创建匿名管道。#include
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
// 处理创建失败的情况
}
// 现在pipefd[0]是用于读取的文件描述符,pipefd[1]是用于写入的文件描述符
}
close()
系统调用显式地关闭管道的读取端或写入端。#include
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
// 处理创建失败的情况
}
// 在适当的时机关闭管道
close(pipefd[0]); // 关闭读取端
close(pipefd[1]); // 关闭写入端
}
⭕父子进程之间的通信:父子进程可以通过匿名管道进行通信。通常的做法是在调用fork()之后,子进程继承了父进程的文件描述符,包括管道。子进程可以关闭不需要的文件描述符,然后使用write()函数向管道中写入数据,父进程则使用read()函数从管道中读取数据。
⭕兄弟进程之间的通信:兄弟进程之间也可以通过匿名管道进行通信。通常的做法是在调用pipe()和fork()之后,子进程再次调用fork()创建兄弟进程。然后兄弟进程可以通过管道进行通信,一个进程负责写入,另一个进程负责读取。
command1 | command2
。阻塞式读写:
数据顺序性:
局限性:
单向通信:匿名管道是一种单向通信机制,数据只能在一个方向上传输。其中一个进程负责写入数据,而另一个进程负责读取数据。这使得匿名管道适用于一些特定的通信场景,如父子进程或者兄弟进程之间的通信。
半双工通信:匿名管道是半双工的,意味着它可以在两个进程之间进行双向通信,但是不能同时进行读和写操作。虽然它可以实现双向通信,但是在任意给定的时间点,数据只能在一个方向上传输。
自动关闭:当所有指向管道的文件描述符全部关闭时,操作系统会自动关闭管道。这样做可以确保在程序结束时释放资源,并且不会造成资源泄漏。
#include
#include
#include
#include
#include
#include
#include
#include
#define PROCESS_NUM 5 // 定义常量 PROCESS_NUM 为 5
using namespace std;
// 从文件描述符 waitFd 中读取命令
int waitCommand(int waitFd, bool &quit) {
uint32_t command = 0;
ssize_t s = read(waitFd, &command, sizeof(command)); // 从文件描述符中读取命令
if (s == 0) { // 如果成功读取到命令
quit = true; // 标记为需要退出
return -1;
}
assert(s == sizeof(uint32_t)); // 断言读取的字节数与命令长度相等
return command; // 返回读取到的命令
}
// 向指定的进程发送命令,并在标准输出中打印相关信息
void sendAndWakeup(pid_t who, int fd, uint32_t command) {
write(fd, &command, sizeof(command)); // 向文件描述符中写入命令
cout << "main process: call process " << who << " execute " << desc[command] << " through " << fd << endl; // 打印相关信息
}
int main()
{
load(); // 加载一些内容
vector<pair<pid_t, int>> slots; // 用于保存子进程的PID和管道写端文件描述符
// 先创建多个进程
for (int i = 0; i < PROCESS_NUM; i++)
{
int pipefd[2] = {0};
assert(pipe(pipefd) == 0); // 创建管道并检查是否成功
pid_t id = fork();
assert(id != -1); // 检查fork()是否成功
if (id == 0) // 子进程逻辑
{
close(pipefd[1]); // 关闭写端
while (true)
{
bool quit = false;
int command = waitCommand(pipefd[0], quit); // 等待命令
if (quit)
break; // 如果收到退出命令,则退出循环
if (command >= 0 && command < handlerSize())
{
dummyHandler(); // 执行对应的命令处理函数
}
else
{
cout << "非法command: " << command << endl;
}
}
exit(1);
}
else // 父进程逻辑
{
close(pipefd[0]); // 关闭子进程的读端
slots.push_back(pair<pid_t, int>(id, pipefd[1])); // 保存子进程的PID和写端管道文件描述符
}
}
// 父进程派发任务
srand((unsigned long)time(nullptr) ^ getpid() ^ 23323123123L); // 设置随机数种子
while (true)
{
int command = rand() % handlerSize(); // 随机选择一个任务
int choice = rand() % slots.size(); // 随机选择一个子进程
sendAndWakeup(slots[choice].first, slots[choice].second, command); // 向选定的子进程发送任务
sleep(1); // 休眠一秒
}
// 关闭fd, 所有的子进程都会退出
for (const auto &slot : slots)
{
close(slot.second); // 关闭所有子进程的写端
}
// 回收所有的子进程信息
for (const auto &slot : slots)
{
waitpid(slot.first, nullptr, 0); // 回收子进程
}
}
这段代码是一个简单的进程调度和通信示例,它创建了多个子进程,并使用管道进行进程间通信,父进程通过随机选择一个子进程来派发任务。
创建多个子进程:
fork()
函数创建子进程,并使用 pipe()
函数创建管道用于进程间通信。slots
向量中。子进程逻辑:
父进程逻辑:
srand()
来初始化随机数种子,使得每次运行产生的随机数不同。最后,父进程关闭所有子进程的写端,然后回收所有子进程的信息。
mkfifo()
函数用于创建一个FIFO(First In First Out)或者称为命名管道,它允许进程之间进行通信。下面是关于 mkfifo()
函数的详细介绍:
函数原型
#include
#include
int mkfifo(const char *pathname, mode_t mode);
参数
pathname
:要创建的命名管道的路径名。mode
:创建命名管道时设置的权限模式,通常以 8 进制表示,比如 0666
。返回值
功能
mkfifo()
函数的作用是在文件系统中创建一个特殊类型的文件,该文件在外观上类似于普通文件,但实际上是一个FIFO,用于进程之间的通信。这种通信方式是单向的,即数据写入FIFO的一端,可以从另一端读取出来,按照先进先出的顺序。
包含头文件:首先需要包含相关的头文件,以便使用相关函数和数据结构。
#include
#include
#include
调用 mkfifo()
函数:使用 mkfifo()
函数创建命名管道。该函数原型如下:
int mkfifo(const char *pathname, mode_t mode);
pathname
:要创建的命名管道的路径名。mode
:创建命名管道时设置的权限模式,通常以 8 进制表示,比如 0666
。示例代码:
std::string fifoPath = "/tmp/my_named_pipe"; // 命名管道的路径名
mkfifo(fifoPath.c_str(), 0666); // 创建权限为0666的命名管道
处理返回值:检查 mkfifo()
的返回值,若返回 0 表示成功创建,若返回 -1 表示创建失败,并通过 errno
来获取具体的错误信息。
注意事项
mkfifo()
函数的返回值进行适当的错误处理,根据具体的错误原因进行相应的处理和日志记录。#include
#include
#include
#include
#include
int main() {
std::string fifoPath = "/tmp/my_named_pipe"; // 命名管道的路径名
if (mkfifo(fifoPath.c_str(), 0666) == -1) {
if (errno == EEXIST) {
std::cerr << "Named pipe already exists" << std::endl;
} else {
perror("Error creating named pipe");
}
} else {
std::cout << "Named pipe created successfully" << std::endl;
}
return 0;
}
使用命名管道进行读写操作:在打开命名管道后,可以通过 read()
或 write()
函数对其进行读写操作。
关闭命名管道:当进程使用完毕命名管道后,需要调用 close()
函数来关闭文件描述符,释放相关资源。
close(fd); // 关闭命名管道
注意事项
示例
下面是一个简单的示例,演示了关闭命名管道的过程:
#include
#include
#include
#include
int main() {
int fd = open("/tmp/my_named_pipe", O_RDONLY); // 以只读方式打开命名管道
// 进行读取操作...
if (close(fd) == -1) {
perror("Error closing named pipe");
} else {
std::cout << "Named pipe closed successfully" << std::endl;
}
return 0;
}
总之,关闭命名管道是确保在进程使用完毕后释放相关资源的重要步骤。通过调用 close()
函数可以关闭文件描述符,释放命名管道相关的资源。
单向通信:
命名管道提供了一种单向通信的方式,一个进程可以向管道中写入数据,而另一个进程则可以从管道中读取数据。这种通信方式适用于需要单向数据传输的场景。
持久性:
命名管道与匿名管道不同之处在于,它以文件的形式存在于文件系统中,具有持久性。即使管道的创建进程终止,命名管道仍然存在,其他进程可以继续使用该管道进行通信。
阻塞和非阻塞:
在进行命名管道通信时,可以选择阻塞或非阻塞模式。在阻塞模式下,如果读取进程尝试从空管道中读取数据,它将被阻塞直到有数据可读;而在非阻塞模式下,读取进程将立即返回一个错误,从而避免阻塞。
下面是一个简单的示例,演示了两个进程通过命名管道进行通信的方式:
进程 A 写入数据到命名管道
int fd = open("/tmp/my_named_pipe", O_WRONLY); // 以只写方式打开命名管道
write(fd, "Hello, named pipe!", 18); // 向管道中写入数据
close(fd); // 关闭命名管道
进程 B 从命名管道读取数据
int fd = open("/tmp/my_named_pipe", O_RDONLY); // 以只读方式打开命名管道
char buffer[50];
read(fd, buffer, 50); // 从管道中读取数据
close(fd); // 关闭命名管道
持久性:命名管道以文件的形式存在于文件系统中,并且具有持久性。即使创建了命名管道的进程终止,该管道仍然存在于文件系统中,其他进程可以继续使用它进行通信。
单向通信:命名管道提供单向通信的能力,允许一个进程向管道中写入数据,而另一个进程则可以从管道中读取数据。这种单向通信模式适用于需要单向数据传输的场景。
实时数据传输:命名管道允许实时的数据传输,写入管道的数据会立即被读取进程获取,从而实现了实时通信的能力。
阻塞和非阻塞模式:对于读取和写入操作,命名管道可以选择阻塞或非阻塞模式。在阻塞模式下,读取进程将被阻塞直到有数据可读,而在非阻塞模式下,读取进程将立即返回错误,避免阻塞。
简单易用:使用命名管道进行进程间通信相对简单,只需通过类似文件操作的方式打开、读取和关闭管道即可完成通信过程。
适用范围广泛:命名管道适用于各种场景,例如实现多个进程之间的数据共享、进程之间的控制和协调等。
匿名管道和命名管道分别适用于不同的通信需求。
⭕匿名管道适用于有亲缘关系的父子进程间的通信。
⭕命名管道更适合不相关进程间的通信,且具有持久性和更灵活的应用方式。
感谢您对博主文章的关注与支持!如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C++编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新,不要错过任何精彩内容!
再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索Linux、C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!