一、引言

在当今计算机科学领域,多任务处理是操作系统的一个基本特性。在一个系统中,多个进程同时运行,彼此独立地执行各自的任务。然而,在实际应用场景中,这些进程之间往往需要协同工作,共享信息等,以便更有效地完成整体目标。这正是进程间通信(IPC)的关键作用所在。

背景

操作系统通过调度进程来实现任务的并行执行,但单个进程的能力是有限的。多进程协同工作可以提高系统的整体性能,使其更适应复杂的任务需求。然而,进程的独立性也使得它们不能直接交互信息,因此需要一种机制来实现它们之间的通信和协同,IPC应运而生。

IPC的核心概念

IPC是一种使得独立运行的进程之间能够相互传递信息和协同工作的技术。这些进程可以存在于同一台计算机上,也可以分布在网络中的不同计算机上。IPC不仅限于本地进程,还可以涉及远程通信。其目的是通过各种手段实现进程之间的数据传输、同步和互操作。

ps:本文首要介绍本地通信,后续文章再涉及网络通信!

二、进程间通信需求

在计算机系统中,不同进程之间需要进行通信的需求源于多种情境,其中一些关键的需求包括:

协同完成任务

多个进程可能需要合作完成同一个任务,通过通信可以使他们协同工作,共享中间结果,提高整体效率。

eg图像处理系统中,一个进程负责图像采集,另一个进程负责图像处理。它们通过通信协同工作,实现对图像的实时处理。

数据共享

不同进程可能需要访问相同的数据,通过通信可以安全地传递数据,确保共享信息的一致性和可靠性。

eg数据库系统中,一个进程负责更新数据,另一个进程负责查询数据。它们通过通信共享数据库的状态,以确保数据的一致性。

事件通知

某个进程的执行可能依赖于其他进程触发的事件,通过通信可以实现进程之间的及时事件通知。

eg 图形用户界面(GUI)应用中,一个进程监听鼠标点击事件,而另一个进程负责处理用户界面更新。通过通信,点击事件进程可以即时通知到处理界面的进程。

进程间同步

多个进程需要同步它们的执行顺序,以及对共享资源的访问。

eg 在并发编程中,两个进程同时访问共享的计数器。通过通信机制,可以实现对计数器的安全访问,避免数据竞争。

资源共享

多个进程需要共享特定的硬件资源。

eg 打印系统中,多个进程需要共享打印机。通过通信,可以实现对打印机的有序访问避免冲突和资源浪费

三、进程间的通信方式

在计算机系统中,进程间通信有多种方式,每种方式都有其独特的优势和适用场景。以下是常见的进程间通信方式:

管道(Pipes)

管道是一种半双工通信方式,允许数据在具有亲缘关系的进程之间单向传递。在Unix/Linux系统中,管道分为匿名管道和命名管道

  • 匿名管道(Anonymous Pipes):
    • 特点: 用于具有亲缘关系的进程通信,通常是父子进程之间。
    • 创建方式: 通过pipe()系统调用创建,不需要在文件系统中为其指定名称。
    • 通信方向: 是单向的,只能用于单向通信。
  • 命名管道(Named Pipes):
    • 特点: 用于非亲缘关系的进程通信,允许不同进程之间进行通信。
    • 创建方式: 通过mkfifo命令或mkfifo()系统调用创建,需要在文件系统中为其指定一个名称。
    • 通信方向: 可以是单向或双向,允许更灵活的通信。

消息队列(Message Queues)

消息队列允许不同进程通过消息进行通信,消息具有结构化的格式。这种通信方式适用于不同进程之间的异步通信,其中一个进程发送消息,另一个进程接收消息。在Linux中,可以使用System V消息队列,通过msgget()msgsnd()msgrcv()等函数进行操作。

共享内存(Shared Memory)

共享内存是一种高效的通信方式,它允许多个进程共享同一块内存区域,从而避免了数据的复制。这对于需要频繁交换大量数据的进程非常有用。在Linux中,可以使用System V共享内存,通过shmget()shmat()shmdt()等函数进行操作。

信号量(Semaphores)

信号量用于进程同步和对共享资源的访问进行控制。它可以防止多个进程同时访问共享资源,从而避免了数据竞争和不一致性。在Linux中,可以使用System V信号量,通过semget()semop()等函数进行操作。

套接字(Sockets)

套接字通信可以在本地或网络中实现进程间通信,适用于不同计算机之间的通信。套接字通信通常用于分布式系统或需要远程通信的场景。在网络编程中,可以使用socket()bind()listen()accept()等函数创建和操作套接字。

四、常见的进程间通信工具和API

为了满足不同的通信需求,操作系统提供了多种进程间通信(IPC)的工具和API。以下是一些常见的IPC工具和API:

POSIX消息队列 API:

  • 描述: POSIX消息队列提供了一种消息传递的机制,可以在不同进程之间进行通信。
  • API:
    • mq_open(): 打开或创建一个消息队列。
    • mq_send(): 发送消息到队列。
    • mq_receive(): 从队列接收消息。
    • mq_close(): 关闭消息队列。

System V消息队列 API

  • 描述: System V消息队列也是一种消息传递的机制,适用于进程间通信。
  • API:
    • msgget(): 获取消息队列标识符。
    • msgsnd(): 发送消息到队列。
    • msgrcv(): 从队列接收消息。
    • msgctl(): 控制消息队列。

POSIX共享内存 API

  • 描述: POSIX共享内存允许多个进程访问共享的内存区域。
  • API:
    • shm_open(): 打开或创建一个共享内存对象。
    • ftruncate(): 调整共享内存的大小。
    • mmap(): 映射共享内存到进程地址空间。
    • munmap(): 解除映射。

System V共享内存 API

  • 描述: System V共享内存也提供了共享内存的机制,允许多个进程访问共享的内存区域。
  • API:
    • shmget(): 获取共享内存标识符。
    • shmat(): 连接到共享内存。
    • shmdt(): 断开与共享内存的连接。
    • shmctl(): 控制共享内存。

套接字(Sockets)API

  • 描述: 套接字通信可以在本地或网络中实现进程间通信,适用于不同计算机之间的通信。
  • API:
    • socket(): 创建套接字。
    • bind(): 绑定套接字到地址和端口。
    • listen(): 监听连接请求。
    • accept(): 接受连接请求。
    • connect(): 发起连接请求。
    • send()recv(): 发送和接收数据。

信号量 API

  • 描述: 信号量用于进程同步和对共享资源的访问进行控制。
  • API:
    • semget(): 获取信号量集标识符。
    • semop(): 执行信号量操作。
    • semctl(): 控制信号量。

这些工具和API提供了不同层次和方式的进程间通信,可以根据具体的需求选择合适的工具和API。在使用这些工具和API时,需要注意处理同步和互斥的问题,以确保通信的安全性和有效性。

五、管道(Pipes)

管道(Pipes)是一种简单而有效的通信方式,特别适用于有亲缘关系的进程之间。管道提供了一个单向通道,允许数据在父子进程之间进行单向传递。在Unix/Linux系统中,管道分为匿名管道和命名管道。

匿名管道(Anonymous Pipes)

  • 特点: 用于具有亲缘关系的进程通信,通常是父子进程之间。
  • 创建方式: 通过pipe()系统调用创建,不需要在文件系统中为其指定名称。
  • 通信方向: 是单向的,只能用于单向通信。

pipe() 函数:

  • 原型:

    1
    2
    #include <unistd.h>
    int pipe(int pipefd[2]);
  • 描述: 创建一个管道,其中 pipefd 数组包含两个文件描述符,pipefd[0] 用于读取,pipefd[1] 用于写入。

write() 函数:

  • 原型:

    1
    2
    #include <unistd.h>
    ssize_t write(int fd, const void *buf, size_t count);
  • 描述: 将数据从缓冲区 buf 写入到文件描述符 fd 对应的管道。

read() 函数:

  • 原型:

    1
    2
    #include <unistd.h>
    ssize_t read(int fd, void *buf, size_t count);
  • 描述: 从文件描述符 fd 对应的管道读取数据到缓冲区 buf

close() 函数:

  • 原型:

    1
    2
    #include <unistd.h>
    int close(int fd);
  • 描述: 关闭文件描述符 fd,在管道通信完成后应该关闭不需要的文件描述符。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/* 功能概述:匿名管道(内存级文件)用于父子进程间通信 */

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <ctime>

/*
读快写慢?
读慢写快?
写关闭,读到0?
读关闭,写?
*/

// 获取当前时间并以字符串形式返回
std::string getCurrentTime()
{
time_t now = time(0);
struct tm tstruct;
char buf[80];
tstruct = *localtime(&now);
strftime(buf, sizeof(buf), "%Y-%m-d %X", &tstruct);
return buf;
}

// 打印带有指定颜色的消息
void printColoredMessage(const std::string &message, const std::string &color)
{
std::cout << "\033[" << color << "m" << message << "\033[0m" << std::endl;
}

int main()
{
int fds[2];

// 创建一个管道
int pipe_res = pipe(fds);
assert(pipe_res == 0);

pid_t id = fork();
assert(id >= 0);

if (id == 0)
{
// 在子进程中

// 关闭管道的读端
close(fds[0]);

// 子进程通信代码
const char *str = "I'm the child process";
int cnt = 0;
while (true)
{
char buffer[1024];
// 从子进程向父进程发送带有时间戳和自定义事件内容的事件通知
snprintf(buffer, sizeof(buffer), "Child[%d]: Event - Type: Alert, Description: Security breach detected", cnt++);
std::string message = getCurrentTime() + " | Type: Event | Sender: Child | " + buffer;
write(fds[1], message.c_str(), message.length());
// 每秒写入一次
sleep(4);
}

// 关闭管道的写端并退出子进程
close(fds[1]);
exit(0);
}

// 在父进程中

// 关闭管道的写端
close(fds[1]);

// 父进程通信代码
int cnt = 0;
while (true)
{
char buffer[1024];
// 从子进程接收事件通知
ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);

if (s > 0)
{
buffer[s] = '\0';
// 打印带有时间戳、彩色输出和发送方信息的事件通知
std::string receivedMessage = std::string(buffer);
std::string timestamp = getCurrentTime();
std::string senderInfo = receivedMessage.substr(receivedMessage.find("Sender:") + 8);
std::string eventType = receivedMessage.substr(receivedMessage.find("Type:") + 6, 9);
std::string eventDescription = receivedMessage.substr(receivedMessage.find("Description:") + 13);
std::string coloredMessage = "Parent[" + std::to_string(cnt++) + "]: " + buffer + " (" + timestamp + ")";
printColoredMessage(coloredMessage, "32"); // 32表示绿色
std::cout << " Event Type: " << eventType << std::endl;
std::cout << " Event Description: " << eventDescription << std::endl;
std::cout << " Sender: " << senderInfo << std::endl;
}
}

// 等待子进程完成
pid_t child_status = waitpid(id, nullptr, 0);
assert(child_status == id);

// 关闭管道的读端并退出父进程
close(fds[0]);
return 0;
}

匿名管道中的特殊情况

在匿名管道的使用过程中,存在一些特殊情况需要注意,这些情况可能对进程间通信产生影响。以下是针对匿名管道的几种特殊情况的总结:

读取速度快于写入

如果读取进程的速度快于写入进程,可能导致管道中的数据被读取完毕后,读取进程可能会阻塞等待更多的数据写入。

写入速度快于读取

反之,如果写入进程的速度快于读取进程,管道的缓冲区可能会填满,导致写入进程阻塞等待缓冲区有空间。

写入端关闭,读取返回0

当写入进程关闭管道的写入端时,读取进程在读取完所有数据后会收到读取到文件结束(EOF)的信号,read 函数返回 0,表示写入端的所有数据已经读取完毕。

读取端关闭,写入会引发SIGPIPE

如果读取进程关闭了管道的读取端,写入进程继续写入数据可能导致写入进程收到 SIGPIPE 信号。如果未处理该信号,写入进程可能会终止。

命名管道(Named Pipes)

  • 特点: 用于非亲缘关系的进程通信,允许不同进程之间进行通信。
  • 创建方式: 通过mkfifo命令或mkfifo()系统调用创建,需要在文件系统中为其指定一个名称。
  • 通信方向: 可以是单向或双向,允许更灵活的通信。

mkfifo() 函数:

  • 原型:

    1
    2
    3
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
  • 描述: 创建一个命名管道,pathname 是管道的路径名,mode 是权限设置。

open() 函数:

  • 原型:

    1
    2
    3
    4
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    int open(const char *pathname, int flags);
  • 描述: 打开一个命名管道,返回文件描述符。

write() 函数:

  • 原型:

    1
    2
    #include <unistd.h>
    ssize_t write(int fd, const void *buf, size_t count);
  • 描述: 将数据从缓冲区 buf 写入到文件描述符 fd 对应的管道。

read() 函数:

  • 原型:

    1
    2
    #include <unistd.h>
    ssize_t read(int fd, void *buf, size_t count);
  • 描述: 从文件描述符 fd 对应的管道读取数据到缓冲区 buf

close() 函数:

  • 原型:

    1
    2
    #include <unistd.h>
    int close(int fd);
  • 描述: 关闭文件描述符 fd,在管道通信完成后应该关闭不需要的文件描述符。

命名管道demo代码略长,读者可跳转至此篇文章进行查看命名管道代码demo详解