1进程间通信介绍
因为进程独立性的存在,导致进程的通信成本比较高
通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。
a,进程之间通信的本质:必须让不同的进程看到同一份资源b,资源:就是特定形式的内存空间
c,这个资源是由操作系统提供的
d,我们访问这个空间,进行通信其实就是访问操作系统
当然创建这个空间要用系统调用接口(从底层设计)
一般操作系统,会有一个独立的通信模块---隶属于文件系统---> IPC通信模块
定制的标准(进程之间通信是有标准的)---- system V && posix
基于文件级别的通信----管道
1.1进程间通信分类
管道
匿名管道pipe
命名管道
System V IPC
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
2管道
什么是管道?
管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"
子进程创建的时候,父进程和子进程的files_struct(进程打开的文件表)是一样的,所以子进程表中指向也是父进程打开的文件
即父子进程天然能够通信
管道其实就是文件(内存级文件,不会储存到磁盘中)
可见,父进程打开管道文件的时候既以读方式打开也以写方式打开
同时可以控制读写的方向,如例子3
站在内核角度-管道本质
读文件和写文件打开方式不同,那么这个file也不同(文件缓冲区是只有一个)
只想让父子进行单向通信,我们想让子进程进行写入,父进程进行读取
管道本来就是单项通信的,设计者就是图简单,设计为单项通信
注意:如果没有任何关系,就不能通信,进程之间需要有血缘关系,常用于父子
这个管道文件没有名字,没有inode(为空) 也叫匿名文件
管道的接口这个是输出型参数,我们搞一个数组给他传过去,然后这个函数会把对应数据放入到数组里面(这个参数是文件描述符)
向文件中写入的时候,不要加/n,文件中只要数据就可以里
从文件中读出来的时候也要加上/0,文件中无/0读出来是以字符串来用的
管道的特征:1,具有血缘关系的进程进行进程间通信
2,管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
3,管道是面向字节流的
4,管道是基于文件的,而文件的生命周期是随进程的
5,父子进程是会进程协同的,同步与互斥的---保护管道文件数据的安全
管道是有固定大小的,不同的内核里面大小也会不同
管道的四种情况
1,读写端正常,管道如果为空,读端就要阻塞
2,读写端正常,如果管道被写满,写端就要阻塞
3,读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞
4,写端正常些,读端关闭,操作系统就要杀掉正在写入的进程,通过信号杀掉,写端(子进程)会退出,退出码为13
如何在自己的shell中支持管道
2.1使用管道实现一个简易版本的进程池
.hpp文件指的是把头文件和库文件放到一块(函数的声明和定义在一块),这个也是一种头文件,只是这个头文件可以把函数的定义也写到这个文件中
Task.hpp
cpp
#pragma once
#include <iostream>
#include <vector>
typedef void (*task_t)();
void task1()
{
std::cout << "lol 刷新日志" << std::endl;
}
void task2()
{
std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}
void task3()
{
std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}
void task4()
{
std::cout << "lol 用户释放技能,更新用的血量和蓝量" << std::endl;
}
void LoadTask(std::vector<task_t> *tasks)
{
tasks->push_back(task1);
tasks->push_back(task2);
tasks->push_back(task3);
tasks->push_back(task4);
}
cpp
#include <vector>
#include <string>
#include <unistd.h>
#include <cassert>
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"
#define processnum 10
std::vector<task_t> tasks;
class channel
{
public:
channel(int cmdfd, int slaverid, std::string &processname)
: _cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
{
}
public:
int _cmdfd; // 发送任务的文件描述符
pid_t _slaverid; // 发送任务的pid
std::string _processname; // 进程的名字,方便我们调试打印
};
void slaver()
{
while (true)
{
int cmdcode = 0;
int n = read(0, &cmdcode, sizeof(cmdcode));
if (n == sizeof(cmdcode))
{
std::cout << "slaver say@ get a command: " << getpid() << " : cmdcode: " << cmdcode << std::endl;
if (cmdcode >= 0 && cmdcode < tasks.size())
tasks[cmdcode]();
}
else if (n == 0)
break;
}
}
void Menu()
{
std::cout << "################################################" << std::endl;
std::cout << "# 1. 刷新日志 2. 刷新出来野怪 #" << std::endl;
std::cout << "# 3. 检测软件是否更新 4. 更新用的血量和蓝量 #" << std::endl;
std::cout << "# 0. 退出 #" << std::endl;
std::cout << "#################################################" << std::endl;
}
// 输入:const &
// 输出:*
// 输入输出:&
void InitProcessPool(std::vector<channel> *channels)
{
std::vector<int> oldfds;
for (int i = 0; i < processnum; i++)
{
int pipefd[2];
int n = pipe(pipefd);
assert(!n);
pid_t id = fork();
if (id == 0)
{
std::cout << "child: " << getpid() << " close history fd: ";
for (auto fd : oldfds)
{
std::cout << fd << " ";
close(fd);
}
std::cout << "\n";
// child
close(pipefd[1]);
dup2(pipefd[0], 0);
close(pipefd[0]);
slaver(); // 子进程执行任务入口
std::cout << "process :" << getpid() << " quit" << std::endl;
exit(0);
}
// father
close(pipefd[0]);
std::string name = "process-" + std::to_string(i);
channels->push_back(channel(pipefd[1], id, name));
oldfds.push_back(pipefd[1]);
sleep(1); // 创建慢一点,观察每个子进程关闭多余的(从父进程继承而来的写端)的过程
}
}
void Debug(const std::vector<channel> &channels)
{
for (const auto &c : channels)
{
std::cout << c._cmdfd << " " << c._slaverid << " " << c._processname << std::endl;
}
}
void CtrlSlaver(const std::vector<channel> &channels)
{
int which = 0;
while (true)
{
int select = 0;
Menu();
std::cout << "Please Enter@ ";
std::cin >> select;
if (select <= 0 || select >= 5)
break;
// 1选择任务
int cmdcode = select - 1;
// 2选择进程
std::cout << "father say: " << " cmdcode: " << cmdcode << " already send to " << channels[which]._slaverid << " process name: "
<< channels[which]._processname << std::endl;
// 3发送任务
write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));
which++;
which %= channels.size();
}
}
void QuitProcess(const std::vector<channel> &channels)
{
for (const auto &c : channels)
{
close(c._cmdfd);
waitpid(c._slaverid, nullptr, 0);
}
// version1 ,倒着循环
// int last = channels.size() - 1;
// for (int i = last; i >= 0; i--)
// {
// close(channels[i]._cmdfd);
// waitpid(channels[i]._slaverid, nullptr, 0);
// }
// 原本的代码
// for (const auto &c : channels)
// {
// close(c._cmdfd);
// }
// sleep(5);
// for (const auto &c : channels)
// {
// waitpid(c._slaverid, nullptr, 0);
// }
// sleep(5);
// 注意,下面的代码不能正常退出
// 原因:先关闭第一个,去等待退出,管道的写端并没有关完,那么这个子进程并不会退出,还是会在阻塞等待
// for (const auto &c : channels)
// {
// close(c._cmdfd);
// waitpid(c._slaverid, nullptr, 0);
// }
}
int main()
{
LoadTask(&tasks);
// 组织起来
std::vector<channel> channels;
// 1.初始化 && 子进程执行任务的入口
InitProcessPool(&channels);
Debug(channels);
// 2.开始控制子进程
CtrlSlaver(channels);
// 3.清理收尾
QuitProcess(channels);
return 0;
}
演示:

2.2命名管道
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:mkfifo filename
理解:
1.如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统会打开几个文件?
还是一个,管道文件时内存级文件,不需要刷盘
2.你怎么知道你们两个打开的时同一个文件?为什么?
同路径下同一个文件名 = 路径 + 文件名 --->通过这种方法来确定(匿名管道时通过继承来确定是同一个文件)
程序中实现
unlink是一个在 Linux 系统中用于删除文件的命令。它的主要功能是通过系统中的unlink函数来删除指定的文件,其效果与rm命令相似
服务端打开管道的时候是要等待写入放打开之后,自己才会打开文件,向后执行(如果只有读端,没有写端,这样是没有意义的,同样只有写端也是一样),在这里open是阻塞的
而且命名管道也可以写成想上一节的进程池的样子的
2.3日志系统的完成
日志时间,日志的等级,日志的内容,文件名称和行号
Inof:普通信息
Warning:警告
Error:错误
Fatal:致命
Debug:调试
c语言的可变参数列表1完成基于命名管道的服务端和客户端
2完成自己的日志系统(用自己的日志系统代替perror等等错误的输出,改为自己的日志)
log.hpp
cpp
#pragma once
#include <iostream>
using namespace std;
#include <stdarg.h>
#include <stdio.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
_printMethod = Screen;
_path = "./log/";
}
void Enable(int method)
{
_printMethod = method;
}
string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
break;
case Debug:
return "Debug";
break;
case Warning:
return "Warning";
break;
case Error:
return "Error";
break;
case Fatal:
return "Fatal";
break;
default:
return "None";
}
}
// void logmessage(int level, const char *format, ...)
// {
// time_t t = time(nullptr);
// struct tm *ctime = localtime(&t);
// char leftbuffer[SIZE] = {0};
// snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// char rightbuffer[SIZE] = {0};
// va_list s;
// va_start(s, format);
// vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
// va_end(s);
// // 格式设置
// char logtxt[SIZE * 2] = {0};
// snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// // 打印
// // printf("%s", logtxt);
// printLog(level, logtxt);
// }
void printLog(int level, const string &logtxt)
{
switch (_printMethod)
{
case Screen:
cout << logtxt;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const string &logname, const string &logtxt)
{
string filename = _path + logname;
int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const string &logtxt)
{
string filename = LogFile;
filename += ".";
filename += levelToString(level); // log.txt.warning////
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE] = {0};
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
char rightbuffer[SIZE] = {0};
va_list s;
va_start(s, format);
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式设置
char logtxt[SIZE * 2] = {0};
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// 打印
// printf("%s", logtxt);
printLog(level, logtxt);
}
private:
int _printMethod;
string _path;
};
3共享内存
进程通信的原理:不同的进程看到同一份资源
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到 内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
上面的操作(申请内存,挂接,去关联释,放共享内存)都是进程直接做的吗?
不是,是由操作系统来做的--->就存在系统调用
shmget函数
功能:用来创建共享内存 原型 int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
1这个key是一个数字,他在内核中具有唯一性,能够让不同的进程进行唯一性标识
2第一个进程通过key创建共享内存,然后第二个进程拿着同样的key就可以和第一个进程看到同一个共享内存
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
IPC_CREAT(单独使用):如果你申请的共享内存不存在,就创建,存在,就获取并返回
IPC_CREAT | IPC_EXCL:如果你申请的共享内存不存在,就创建,存在,就出错返回。确保,如果我们申请成功了一个共享内存,这个共享内存一定是一个新的!
IPC_EXCL:不单独使用!
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
key是操作系统标定唯一性的,而返回的
shmid是只在你的进程内,表示资源的唯一性
如何获取这个key呢?这个函数其实就是一套算吗,算出来的key冲突概率比较小,你自己伪造的key会和系统的冲突
ipcs -m :查看所有的共享内存共享内存的生命周期是随内核的!
用户不主动关闭,共享内存会一直存在
除非内核重启(用户释放)
ipcrm -m + shmid : 删除共享内存(在应用层都用shmid,key是给操作系统用的)
共享内存的大小一般是建议4096的整数倍(你要4097,但是给你4096*2)
1调用接口
shmat函数
功能:将共享内存段连接到进程地址空间
原型 void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
说明:
shmaddr为NULL,核心自动选择一个地址 shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。
公式:shmaddr - (shmaddr % SHMLBA) shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数功能:将共享内存段与当前进程脱离
原型 int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数功能:用于控制共享内存
原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码 c
md:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
![]()
2 共享内存的特性:
1 共享内存没有同步和互斥之类的保护机制
2 共享内存是内存,是所有进程之间通信最快的(拷贝比较少)
3 共享内存中的数据,是由用户自己维护的
3共享内存的属性

扩展: 可以将这个共享内存和2中的管道结合起来,通过利用管道的同步性来使共享内存的通信变为同步
代码演示:
comm.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
using namespace std;
#include "log.hpp"
Log log;
const int size = 4096;
const string pathname = "/home/zcy";
const int proj_id = 0x6666;
int GetKey()
{
int key = ftok(pathname.c_str(), proj_id);
if (key == -1)
{
log(Fatal, "ftok error :%s", strerror(errno));
exit(1);
}
log(Info, "ftok success, key is : %d", key);
return key;
}
int GetShareMemHelper(int flag)
{
key_t k = GetKey();
int shmid = shmget(k, size, flag);
if (shmid < 0)
{
log(Fatal, "shmget error :%s", strerror(errno));
exit(2);
}
log(Info, "create share memory success, shmid: %d", shmid);
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
FIFO_CREATE_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
class PipeInit
{
public:
PipeInit()
{
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~PipeInit()
{
int m = unlink(FIFO_FILE);
if (m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
private:
};
在操作系统中,所有的IPC资源,都是整合进操作系统的IPC模块当中
"信号量"(Semaphore)是操作系统和并发编程中一个非常重要的同步机制 ,用来控制多个线程或进程对共享资源的访问,防止出现"竞争条件"(Race Condition),保证程序的正确性和稳定性。
1 互斥访问:在任何时刻都是只允许一个执行流访问共享资源(互斥的概念)2 任何时刻只允许一个执行流访问的资源:叫做临界资源
3 举例:100行代码,5-10行代码才在访问临界资源,我们访问临界资源的单吗---临界区
信号量的通俗解释: 本质是一个计数器,描述临界资源数量的多少
1 申请计数器成功,就表示我具有访问资源的权限了
2 申请了计数器资源,当我访问我要的资源了吗?
没有,申请了计数器资源是对资源的预定机制
3 计数器可以有效保证进去资源的执行流的数量
4 所以每一个执行流,想要访问共享资源的一部分的时候,不是直接访问,而是先申请计数器资源,得到信号量之后,才能访问呢临界资源。比如看电影的时候先买票
程序员把这个计数器叫做信号量!!!
;以下的操作其实都封装了一些方法,是通过调用相应的函数去执行信号量是一个整型变量 (通常非负),支持两个原子操作:
- P 操作 (也叫
wait、down、acquire):
- 如果信号量 > 0,就减 1,允许进入;
- 如果信号量 = 0,就阻塞等待,直到别人释放。
- V 操作 (也叫
signal、up、release):
- 信号量加 1,表示释放资源,并唤醒等待者
原子操作的概念:要么不做,要么就做完----两态的。没有正在做这个概念(这样才能保证这个计数器在cpu执行的时候不能被打断(看不到它正在执行这个操作,在技术上用一条汇编语句,这样就是原子的,一条语句是不可能被切换的))
信号量值1,0两态的,二元信号量,就是互斥功能
信号量凭什么是进程间通信的一种?
1 通信不仅仅是通信数据,互相协同也是
2 也协同,本质也是通信,信号量首先要被所有的通信进程看到









