一、进程间通信是什么
进程间通信(IPC),顾名思义,进程之间需要进行信息交换。 如:数据传输、资源共享、通知事件、进程控制。
进程间通信的方式有:管道、System V IPC、POSIX IPC。
由于进程具有独立性,进程间通信的前提就是,不同的进程能看到同一份资源。
二、管道
1. 什么是管道
管道是类 Unix 系统中最古老的进程间通信的方式。我们把从一个进程连接到另一个进程的数据流称为一个'管道'。

管道是单向通信的,称为单工通信。
管道分为匿名管道和命名管道。
2. 匿名管道
匿名管道(pipe)是亲缘进程间单向通信机制,本质是内核管理的一份文件,两个进程通过一对文件描述符实现一端读一端写,随进程退出自动销毁。匿名管道只能用在有血缘关系的进程之间!
系统调用 pipe,用于创建一个匿名管道。

参数是一个文件描述符数组,管道创建后,fd[0] 表示读端,fd[1] 表示写端。 成功创建返回 0,失败则返回错误码。

匿名管道是一个纯内存级的文件,不需要打开磁盘文件,没有路径,所以称为匿名管道。匿名管道没有名字、没有文件实体,只靠文件描述符来传递。这就是为什么它只能用在有血缘的进程之间,因为这些进程能拷贝文件描述符表,才能拿到同一根管道的读写端。
匿名管道通信有以下几种情况:
- 子进程写得慢,父进程就要阻塞等待。等到管道有数据,父进程才能读。
- 子进程写得快,父进程不读,管道一旦写满,子进程必须阻塞了。
- 读端一直读,写端关闭,读端读完管道中的数据时,read 返回 0,表明读到文件末尾。
- 写端一直写,读端关闭,操作系统会杀掉写端进程,进程异常终止,终止信号为 13!
管道还有以下特点:
- 管道只能单向通信。如果想要两个进程间互相通信,需要创建两个管道。
- 匿名管道只能用在有血缘关系的进程之间,因为必须继承文件描述符表。
- 管道是面向字节流的。多次写入的字节流,在读取时可能被一次读取完,也可能被拆分成多次。
- 管道的生命周期随进程。管道是内核中的临时对象,没有持久化到磁盘。当所有持有管道文件描述符的进程都关闭后,管道会被内核自动销毁,数据也随之丢失。
- 管道通信,对于多进程,自带同步与互斥机制。读空管道时,读进程会阻塞等待数据;写满管道时,写进程会阻塞等待空间。
使用演示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int pipefd[2] = {0};
if (pipe(pipefd) != 0) {
exit(1);
}
// 根据文件描述符分配规则,这里 pipefd 内容应该是 3 4,3 为读端,4 为写端
printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0], pipefd[1]);
// 下面测试子进程向父进程通信
pid_t id = fork();
if (id == 0) {
// 子进程中,要向管道写,所以要关闭读端,也就是关闭文件描述符 pipefd[0]
close(pipefd[0]);
char* msg = "hello pipe";
int cnt = 5;
char outbuffer[256];
while (cnt) {
snprintf(outbuffer, sizeof(outbuffer), "子->父# %s %d", msg, cnt--);
// 向管道中写
write(pipefd[1], outbuffer, strlen(outbuffer));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
// 父进程中,要从管道读,所以要关闭写端,也就是关闭文件描述符 pipefd[1]
close(pipefd[1]);
char inbuffer[1024];
while (1) {
inbuffer[0] = 0;
// 从管道中读
ssize_t n = read(pipefd[0], inbuffer, sizeof(inbuffer) - 1);
// -1 为了给\0 预留一个位置,避免缓冲区溢出。
if (n > 0) {
inbuffer[n] = 0;
// 管道也是文件,结尾不会自动加\0,需要手动设置
printf("%s\n", inbuffer);
} else if (n == 0) {
printf("管道读取结束\n");
close(pipefd[0]);
break;
} else {
perror("read");
break;
}
}
pid_t rid = waitpid(id, NULL, 0);
return 0;
}

3. 命名管道
匿名管道只能用在有血缘关系的进程之间。 如果我们想用在不相关的进程之间通信,可以使用命名管道(FIFO)完成! 无关的进程之间想要通信,必须看到同一份资源,所以命名管道必须有路径(名字),双方才都能看到他。
命名管道本质是一种特殊类型的文件——管道文件。
命名管道可以从命令行上创建,使用命令 mkfifo 文件名
也可以在程序中创建,使用函数 mkfifo,第一个参数是文件名,第二个参数是文件权限。成功创建返回 0,失败返回 -1:

命名管道使用完需要我们手动删除,可以使用函数 unlink 删除文件的方式:

匿名管道由 pipe 函数创建并打开;命名管道由 mkfifo 函数创建,用 open 打开。它们的唯一区别在于创建与打开的方式不同,这些工作完成时候,它们具有相同的语义。
三、实例:匿名管道实现进程池
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
// 进程池,是指提前创建好多个子进程,在需要使用时直接分配任务。省去了创建子进程的开销
// 父进程需要管理'通道',组织管理子进程
enum {
OK = 0,
PIPE_ERR,
FORK_ERR,
READ_ERR,
WRITE_ERR,
WAIT_ERR
};
// 全局定义好子进程数量,任务数量
const int gprocessnum = 7;
void task1() {
std::cout << "这是下载数据任务" << std::endl;
}
void task2() {
std::cout << "这是打印日志任务" << std::endl;
}
void task3() {
std::cout << "这是刷新磁盘任务" << std::endl;
}
void task4() {
std::cout << << std::endl;
}
;
gtasknum = ;
tasks[gtasknum] = {task1, task2, task3, task4};
{
:
{
:
_wfd;
_id;
std::string channel_name;
:
( wfd, id) : _wfd(wfd), _id(id) {
channel_name = + std::(id);
}
{
(_wfd);
}
{
(, _wfd, channel_name.());
}
{
_wfd;
}
{
channel_name.();
}
{
rid = (_id, , );
(rid < ) {
(WAIT_ERR);
}
std::cout << << _id << std::endl;
}
};
:
() {
(());
}
{
();
}
{
(& channel : channels) {
channel.();
}
}
{
cnt = ;
(cnt--) {
itask = ();
ichannel = ();
(, channels[ichannel].(), itask + );
(itask, ichannel);
();
}
}
{
(& channel : channels) {
channel.();
channel.();
}
}
:
{
( i = ; i < gprocessnum; i++) {
pipefd[] = {};
n = (pipefd);
(n < ) {
(PIPE_ERR);
}
id = fork();
(id < ) {
(FORK_ERR);
} (id == ) {
(!channels.()) {
(& channel : channels) channel.();
}
(pipefd[]);
(pipefd[]);
(OK);
} {
(pipefd[]);
channels.(pipefd[], id);
(, id);
}
}
}
{
() {
task_code;
n = (fd, &task_code, (task_code));
(n == (task_code)) {
(task_code >= && task_code < gtasknum) {
tasks[task_code]();
}
} (n == ) {
(, ());
;
} {
(READ_ERR);
}
}
}
{
() % gtasknum;
}
{
i = ;
selected = i;
i++;
i %= gprocessnum;
selected;
}
{
( <= itask && itask < gtasknum && ichannel >= && ichannel < gprocessnum);
n = (channels[ichannel].(), &itask, (itask));
(n < ) {
(WRITE_ERR);
}
}
:
std::vector<Channel> channels;
};
{
ProcessPool pp;
pp.();
pp.();
pp.();
pp.();
;
}
效果演示:

四、实例:命名管道实现服务端客户端通信模型
// Fifo.hpp
#pragma once
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <string>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define FORREAD 1
#define FORWRITE 2
const std::string myfifo = "./fifo";
class Fifo {
public:
Fifo(const std::string& filename = myfifo) : _filename(filename), _mode(0666), _fd(-1) {}
// 创建管道
void Build() {
// 如果管道文件已存在,就 return
if (IsExist()) return;
int n = mkfifo(_filename.c_str(), _mode);
if (n < 0) {
std::cerr << "mkfifo error: " << strerror(errno) << std::endl;
();
}
std::cout << << std::endl;
}
{
(mode == FORREAD) {
_fd = (_filename.(), O_RDONLY);
} (mode == FORWRITE) {
_fd = (_filename.(), O_WRONLY);
}
(_fd < ) {
std::cerr << << (errno) << std::endl;
();
}
std::cout << << std::endl;
}
{
(!()) {
;
}
n = (_filename.());
(n < ) {
std::cerr << << (errno) << std::endl;
();
}
std::cout << << std::endl;
}
{
n = (_fd, msgin.(), msgin.());
}
{
buffer[];
n = (_fd, buffer, (buffer) - );
(n > ) {
buffer[n] = ;
msgout = buffer;
n;
} (n == ) {
;
} {
;
}
}
:
{
st;
n = (_filename.(), &st);
(n == ) {
;
} {
errno = ;
;
}
}
:
std::string _filename;
_mode;
_fd;
};
// Server.cc
#include "Fifo.hpp"
int main() {
// 服务端 创建并打开管道
Fifo pipefile;
pipefile.Build();
pipefile.Open(FORREAD);
std::string msg;
while (1) {
int n = pipefile.Receive(msg);
if (n > 0) {
std::cout << "客户端说:" << msg << std::endl;
} else {
break;
}
}
pipefile.Delete();
return 0;
}
// Client.cc
#include "Fifo.hpp"
int main() {
// 客户端 写入信息
Fifo fileclient;
fileclient.Open(FORWRITE);
while (1) {
std::cout << "请输入:" << std::endl;
std::string msg;
std::getline(std::cin, msg);
fileclient.Send(msg);
}
return 0;
}
效果演示:
命名管道实现服务端客户端通信演示


