目录
[1.1 进程间通信的目的](#1.1 进程间通信的目的)
[1.2 进程间通信的发展](#1.2 进程间通信的发展)
[2.1 管道通信的四种情况](#2.1 管道通信的四种情况)
[2.2 匿名管道](#2.2 匿名管道)
[2.3 基于匿名管道的进程池](#2.3 基于匿名管道的进程池)
[2.3.1 Process.hpp](#2.3.1 Process.hpp)
[2.3.2 Task.hpp](#2.3.2 Task.hpp)
[2.4 命名管道](#2.4 命名管道)
[2.5 管道的特性](#2.5 管道的特性)
[3、System V IPC](#3、System V IPC)
[3.1 System V 共享内存](#3.1 System V 共享内存)
[3.1.1 shmget()](#3.1.1 shmget())
[3.1.2 shmat()&&shmdt()](#3.1.2 shmat()&&shmdt())
[3.1.3 shmctl()](#3.1.3 shmctl())
[3.2 内核中System V IPC资源的组织管理](#3.2 内核中System V IPC资源的组织管理)
前言:
- 作者的环境切换为Ubuntu 20.04,创建用户后,root用户vim /etc/sudoers,
- 用户需要sudo usermod -s /bin/bash 用户名。
- 通过Linux的基础开发工具,把远程仓库克隆到本地。
- 语言使用C/C++,因为系统调用是C写的。
- VSCode下载Remote - SSH插件,远程连接服务器 ,
ssh 用户名@IP地址,再选择第一个配置文件。
- 使用VSCode的代码编辑器 代替vim,并通过VSCode的bash终端 (ctrl+`)和Xshell的bash终端进行操作。
1、进程间通信的介绍
1.1 进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或组组进程发送消息,通知它(它们)发生了某种事件(如子进程退出时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
注意:
进程间通信 的前提 :让不同的进程先看到同一份资源("内存")
1.2 进程间通信的发展
进程间通信(IPC,Inter-Process Communication)
管道:
- 匿名管道。
- 命名管道。
System V IPC :
- System V 消息队列。
- System V 共享内存。
- System V 信号量。
POSIX IPC:
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
本篇文章,重点介绍 管道 和 system V 共享内存。
2、管道
基于文件。
2.1 管道通信的四种情况
- 写慢,读快。读端阻塞。
- 写快,读慢。满了,写端阻塞。
- 写关闭,读继续。读到文件结尾,返回0。
- 读关闭,写继续。无意义,OS信号杀死。
2.2 匿名管道
- int pipe(int pipefd[2]);建立一个匿名管道文件,以读和写的方式打开,将匿名管道文件的读写 描述符分别写到pipedf[0 ],pipefd[1]中,成功返回0,失败返回-1。
- 匿名管道文件是内存级的(没有路径,没有文件名,不需要保存到磁盘)。
- 匿名管道文件只能用于具有血缘关系的进程间通信(通常用于父子间通信)。
- 匿名管道文件的生命周期 ,是随进程 的。所有进程关闭了该管道的文件描述符 (引用计数降为 0),释放资源。
2.3 基于匿名管道的进程池
以父子间通信 为例,父进程写 ,子进程读 并完成指定任务。

注意:
代码中想以,父进程写端关闭 ,子进程读继续,子进程读到文件结尾,返回0,子进程退出,回收子进程。
但是,继承下来的子进程,也会有哥哥进程的匿名管道文件的写端描述符,只要存在写端,对应子进程读段就会一直阻塞 ,所以子进程关闭自己写端时,也要关闭哥哥的写端 。才能实现父进程写端关闭,子进程退出。
2.3.1 Process.hpp
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <vector>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include "Task.hpp"
#define CHILD_PROCESS_NUM 3
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
class Channel // 子进程的pid,父进程对应该子进程的匿名管道的写端
{
public:
Channel(pid_t chpid, int wfd)
: _chpid(chpid), _wfd(wfd)
{
}
pid_t getpid()
{
return _chpid;
}
int getwfd()
{
return _wfd;
}
void Close()
{
close(_wfd);
}
void Wait()
{
waitpid(_chpid,nullptr,0);
}
private:
pid_t _chpid;
int _wfd;
};
class ChannelManager
{
public:
void InsertChannel(pid_t pid, int wfd)
{
channels.emplace_back(pid, wfd);
}
int wfd(int index)
{
return channels[index % channels.size()].getwfd();
}
int pid(int index)
{
return channels[index % channels.size()].getpid();
}
void Printf()
{
for (auto e : channels)
printf("child pid : %d , wfd : %d\n", e.getpid(), e.getwfd());
}
void Close()
{
for (auto e : channels)
e.Close();
}
void Wait()
{
for (auto e : channels)
e.Wait();
}
private:
std::vector<Channel> channels;
};
class ProcessPool
{
public:
ProcessPool(int num)
: _child_process_num(num)
{
_tm.RegisterTask(func1);
_tm.RegisterTask(func2);
_tm.RegisterTask(func3);
}
void Work(int rfd)
{
while (true)
{
int code = 0;
int n = read(rfd, &code, sizeof(int));
if (n == 0)
break;
else if (n == 4)
{
_tm.ExecuteTask(code);
}
else
{
continue;
}
}
}
void Create()
{
for (int i = 0; i < _child_process_num; ++i)
{
int fds[2] = {0};
int n = pipe(fds);
if (n != 0)
ERR_EXIT("pipe");
int pid = fork();
if (pid == -1)
ERR_EXIT("fork");
else if (pid == 0)
{
// child
close(fds[1]); // 关闭自己的写端
_cm.Close(); // 关闭哥哥进程的写端
Work(fds[0]);
close(fds[0]);
exit(0);
}
else
{
// parent
close(fds[0]);
_cm.InsertChannel(pid, fds[1]);
}
}
std::cout << "Create Success" << std::endl;
_cm.Printf();
}
void Run()
{
int i = 0;
while (true)
{
sleep(1);
int code = _tm.Code();
std::cout << "发送child " << _cm.pid(i) << " 一个任务码" << code << std::endl;
write(_cm.wfd(i), &code, sizeof(int));
i++;
}
}
~ProcessPool()
{
// 关闭父进程的写端即可,子进程都读到结尾,退出。
_cm.Close();
// 回收子进程。
_cm.Wait();
}
private:
int _child_process_num;
ChannelManager _cm;
TaskManager _tm;
};
2.3.2 Task.hpp
cpp
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <vector>
#include <functional>
void func1()
{
std::cout<<"***** 打开数据库 *****"<<std::endl;
}
void func2()
{
std::cout<<"***** 打开日志 *****"<<std::endl;
}
void func3()
{
std::cout<<"***** 运行Hello World *****"<<std::endl;
}
class TaskManager
{
public:
TaskManager()
{
srand(time(nullptr));
}
void RegisterTask(std::function<void()> f)
{
_tasks.push_back(f);
}
int Code()
{
return rand() % _tasks.size();
}
void ExecuteTask(int code)
{
if(code >= 0 && code < _tasks.size())
_tasks[code]();
}
private:
std::vector<std::function<void()>> _tasks;
};
2.4 命名管道
- int mkfifo(const char *filename,mode_t mode);创建一个命名管道文件filename(可指定路径),mode指定文件的权限,成功返回0,失败返回-1。
- unlink(const char *filename);会立即移除命名管道在文件系统中的路径(即filename不再可见),但不会影响已经打开该管道的进程。成功返回0,失败返回-1。
- 命名管道文是内存级的(有路径,有文件名,但是数据不需要刷新到磁盘)。
- 命名管道文件用于不同的进程间通信(通常用于父子间通信)。
- unlink后,所有进程关闭了该管道的文件描述符 (延迟到引用计数降为 0),释放资源。
2.5 管道的特性
- 管道文件用于单向通信,属于半双工(一发一收)。通常收发在一开始就确定了,不能改。
- 管道文件,自带同步机制 (一发一收,有顺序)。
- 管道文件,面向字节流(没有按特定的序列读写)。
- 管道文件,大小 通常为64KB。
- 管道文件的写入的数据 <= PIPE_BUF (通常为4KB ) 时,具有原子性(数据要么全部写入,要么完全不写入,不会被其他进程的写入穿插)。
3、System V IPC
3.1 System V 共享内存
共享内存区 是最快的IPC 形式。因为不需要系统调用。但是没有"保护机制",数据读取随意。
3.1.1 shmget()
int shmget(key_t key, size_t size, int shmflg);
- key:用户层共享 的共享内存标识符 ,在用户层,让不同进程打开同一个共享内存。因为内核的共享内存的shmid是运行后才有的,此时不能通信,给不了id,不能指向同一个共享内存。共享内存是内存级,没有名字,用key标识。一般使用ftok,创建一个唯一的key。
- size:共享内存的大小。共享内存的大小为4KB的整数倍,但是申请了多少,就给多少,如:申请4097B,共享内存在内核中为8KB,但是只给你4097B。
- shmflg:IPC_CREAT ,不存在,就创建,存在,就打开。用于打开 。;IPC_CREAT|IPC_EXCL|权限 ,不存在,就创建,存在,就报错。用于创建。
- 成功返回shmid,失败返回-1。
注意:
对共享内存的其他操作都使用内核共享内存的shmid,不使用key。
3.1.2 shmat()&&shmdt()
- void *shmat(int shmid, const void *shmaddr, int shmflg);将共享内存挂接到进程的虚拟地址空间 ,成功返回起始虚拟地址,失败返回(void*)-1。shmaddr 是设置其实虚拟地址的位置,一般用不上,传NULL 就行。shmflg 当前进程的映射行为(仅自身有效),通常传0,可读可写。
- int shmdt(const void *shmaddr);去共享内存的关联。共享内存的引用计数为0,释放资源。
3.1.3 shmctl()
- int shmctl(int shmid, int cmd, struct shmid_ds *buf);
命令 | 用途 | 使用频率 |
---|---|---|
IPC_STAT |
获取共享内存的状态信息 (如 shm_segsz 、shm_nattch )。 |
高(调试/监控) |
IPC_SET |
修改共享内存的某些属性(如权限 shm_perm )。 |
中(需谨慎) |
IPC_RMID |
标记删除共享内存 (实际释放需等待引用计数归零)。 | 高(资源清理) |
- ipcs -m shmid,查看共享内存。
- ipcrm -m shmid,删除指定的共享内存。
3.2 内核中System V IPC资源的组织管理
- System V 是一个标准,Linux支持了这种标准,并专门设计了一个IPC模块。
- 共享内存 ,消息队列,信号量 (本质是一个计数器,描述的是临界(共享)资源中,资源的数量),是System V IPC的三种核心机制,接口使用类似 ,都有struct ipc_perm* -> 独立的数据结构 struct shmid_ds*, struct msqid_ds*, struct semid_ds*(通过强制类型转换,实现多态) ,key区分唯一(不能使用相同的key)。