一、进程间通信的本质
进程是相互独立的,要实现通信,必须让不同进程能看到同一份"资源"。"资源"是特定形式的内存空间,一般是由操作系统提供,进程访问这个空间进行通信,本质就是访问操作系统,通过系统调用接口,来访问和操作这份共享资源,以此完成数据交互。
二、进程间通信的常见需求
1、基本数据交互
2、发送命令
3、任务协同
4、事件通知
三、常见的IPC方式
System V IPC:
System V 消息队列
System V 共享内存
System V 信号量
四、匿名管道
1、管道通信的原理
只有具有血缘关系的进程之间可以进行通信,常用于父子之间,而且只能进行单向通信。
如果要进行双向通信,需要创建多个管道。
这个管道没有名字,所以叫做匿名管道。
我们让子进程进行写入,父进程进行读取。

目前,只是建立了通信信道,并没有通信,因为进程具有独立性,通信是有成本的。
2、接口
#include<unistd.h>
int pipe(int pipefd[2]);//输出型参数
//pipefd[0]:读下标 // 3
//pipefd[1]:写下标 // 4
//上面的值为文件描述符
该接口用于创建匿名管道。
管道是有固定大小的,为64KB。不同的内核里,大小可能有差别。
3、编码实现


运行结果:

管道的特征:
1)具有血缘关系的进程进行进程间通信
2)管道只能单向通信
3)父子进程是会进程协同,同步与互斥的------保护管道文件的数据安全
4)管道是面向字节流的
5)管道是基于文件的,而文件的生命周期是跟进程同步的
管道中的四种情况:
1)读写端正常,管道如果为空,读端就要阻塞
2)读写端正常,管道如果被写满,写端就要阻塞
3)读端正常读,写端关闭,读端就会读到0,表明读到了文件结尾,不会被阻塞
4)写端正常写入,读端关闭,操作系统就要杀掉正在写入的进程,通过13号信号杀掉


**共识:**操作系统是不会做低效,浪费等工作的,如果做了,就是操作系统的bug。
4、管道的应用场景
使用管道实现一个简易版本的进程池。

2)Task.hpp

3)makefile

运行示例:

五、命名管道
1、理解
命名管道可以让毫不相关的进程进行进程间通信。
两个不同的进程打开同一个文件时,在内核中,操作系统会打开一个文件。
我们通过路径+文件名(唯一性)的方式,确定打开的是同一个文件。
管道的本质就是文件,管道是内存级文件,不需要刷新到磁盘。
进程间通信的前提:先让不同进程看到同一份资源。
2、编码
1)comm.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <string>
#include <fcntl.h>
#include <cstring>
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
FIFO_CREATE_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
class Init
{
public:
Init()
{
//创建管道
int n = mkfifo(FIFO_FILE, MODE);
if(n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
int m = unlink(FIFO_FILE);
if(m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};
2)log.hpp
日志:日志时间,日志等级,日志内容,文件的名称和行号。
日志等级:
Info:常规消息
Warning:报警信息
Error:严重了,可能需要立即处理
Fatal:致命的
Debug:调试
实现一个简单的日志函数:
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.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;
}
std::string levelToString(int level)
{
switch(level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch(printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.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 std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level);
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
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);
va_list s;
va_start(s, format);//初始化s
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
//格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
#include <iostream>
#include "comm.hpp"
using namespace std;
int main()
{
int fd = open(FIFO_FILE, O_WRONLY);
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
cout << "client open file done" << endl;
string line;
while(true)
{
cout << "Please Enter@ ";
getline(cin, line);
write(fd, line.c_str(), line.size());
}
close(fd);
return 0;
}
#include "comm.hpp"
#include "log.hpp"
using namespace std;
//管理管道文件
int main()
{
Init init;
Log log;
log.Enable(Onefile);
//打开管道
int fd = open(FIFO_FILE, O_RDONLY);
if(fd < 0)
{
log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
exit(FIFO_OPEN_ERR);
}
log(Info, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
log(Warning, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
log(Fatal, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
log(Debug, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
//开始通信
while(true)
{
char buffer[1024] = {0};
int x = read(fd, buffer, sizeof(buffer));
if(x > 0)
{
buffer[x] = 0;
cout << "client say# " << buffer << endl;
}
else if(x == 0)
{
log(Debug, "client quit, me too!, error string: %s, error code: %d", strerror(errno), errno);
break;
}
else
break;
}
close(fd);
return 0;
}
5)makefile
.PHONY:all
all:server client
server:server.cc
mkdir -p ./log
g++ -o $@ $^ -g -std=c++11
client:client.cc
mkdir -p ./log
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -f server client
五、共享内存
1、原理
进程间通信的本质是:先让不同的进程,看到同一份资源。

操作系统要管理所有的共享内存 ----- 先描述,再组织 ----- 内核结构体描述共享内存
释放共享内存:去关联,释放共享内存。
上述操作是直接由操作系统来做的。
需求方 ------- 系统调用 ----- 执行方
2、接口和实现代码

size:创建共享内存的大小,单位是字节。
shmflg:
IPC_CREAT(单独使用):申请的共享内存不存在,就创建;存在,就获取并返回。
IPC_CREAT|IPC_EXCL:申请的共享内存不存在,就创建;存在,就出错返回。
确保成功申请一个新的共享内存。
IPC_EXCL:不单独使用。
key:
- key是一个数字,这个数字是几不重要,关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。
2)第一个进程可以通过key创建共享内存,第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了。
3)对于一个已经创建好的共享内存,key在共享内存的描述对象中。
4)第一次创建的时候,已经有一个key了。怎么有?

它是一套算法,将pathname和proj_id进行数值计算。
pathname和proj_id是由用户自由指定的。
5)key类似路径,具有唯一性。
返回值:shmid(共享内存标识符)
注意:共享内存的生命周期是随内核的。用户不主动关闭,共享内存会一直存在。除非内核重启(用户释放)。
key VS shmid:
key是操作系统内标定唯一性。
shmid只在我们的进程内,用来表示资源的唯一性。
ipcs -m:查看共享内存
ipcrm -m <shmid>:删除共享内存
代码(结合了管道以实现同步互斥机制):
1)comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "log.hpp"
using namespace std;
Log log;
// 共享内存的大小一般建议是4096的整数倍
// 4097,实际上操作系统给的是4096*2的大小,但我们只能访问4097,否则会越界访问
const int size = 4096;
const string pathname="/home/syh";
const int proj_id = 0x6666;
key_t GetKey()
{
key_t k = ftok(pathname.c_str(), proj_id);
if(k < 0)
{
log(Fatal, "ftok error: %s", strerror(errno));
exit(1);
}
log(Info, "ftok success, key is : 0x%x", k);
return k;
}
int GetShareMemHelper(int flag)
{
key_t k = GetKey();
int shmid = shmget(k, size, flag);
if(shmid < 0)
{
log(Fatal, "create share memory 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 Init
{
public:
Init()
{
// 创建管道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
int m = unlink(FIFO_FILE);
if (m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};
#endif
#include "comm.hpp"
extern Log log;
int main()
{
Init init;
int shmid = CreateShm();
char *shmaddr = (char*)shmat(shmid, nullptr, 0);//挂接
// ipc code 在这里!!
// 一旦有人把数据写入到共享内存,其实我们立马就能看到了!!
// 不需要经过系统调用,直接就能看到数据了!
int fd = open(FIFO_FILE, O_RDONLY);
if (fd < 0)
{
log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
exit(FIFO_OPEN_ERR);
}
struct shmid_ds shmds;
while(true)
{
char c;
ssize_t s = read(fd, &c, 1);
if(s == 0) break;
else if(s < 0) break;
cout << "client say@ " << shmaddr << endl; //直接访问共享内存
sleep(1);
shmctl(shmid, IPC_STAT, &shmds);
cout << "shm size: " << shmds.shm_segsz << endl;
cout << "shm nattch: " << shmds.shm_nattch << endl;
printf("shm key: 0x%x\n", shmds.shm_perm.__key);
cout << "shm mode: " << shmds.shm_perm.mode << endl;
}
shmdt(shmaddr);//去关联
shmctl(shmid, IPC_RMID, nullptr);//删除共享内存
close(fd);
return 0;
}
#include "comm.hpp"
int main()
{
int shmid = GetShm();
char *shmaddr = (char*)shmat(shmid, nullptr, 0);//挂接
int fd = open(FIFO_FILE, O_WRONLY);
if (fd < 0)
{
log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
exit(FIFO_OPEN_ERR);
}
// 一旦有了共享内存,挂接到自己的地址空间中,直接把它当成你的内存空间来用即可!
// ipc code
while(true)
{
cout << "Please Enter@ ";
fgets(shmaddr, 4096, stdin);
write(fd, "c", 1); // 通知对方
}
shmdt(shmaddr);//去关联
close(fd);
return 0;
}
3、特性
1)共享内存没有同步互斥之类的保护机制。
2)共享内存是所有的进程间通信中速度最快的,因为它拷贝次数少。
3)共享内存内部的数据,由用户自己维护。
共享内存的属性:


六、信号量(浅谈)
1、理解一些概念
1)当A正在写入,写入了一部分就被B拿走了,导致双方发的和收的数据不完整---数据不一致问题
2)任何时刻,只允许一个执行流访问共享资源---互斥
3)共享的,任何时刻只允许一个执行流访问(就是执行访问代码)的资源---临界资源---一般是内存空间
4)一段代码中一般只有几行代码会访问临界资源,我们访问临界资源的代码---临界区
2、理解信号量(信号灯)
信号量的本质是一个计数器,用来描述临界资源中资源数量的多少。程序员把这个"计数器"叫做信号量。
引入计数器:
1)申请计数器成功,就表示具有访问资源的权限了
2)申请了计数器资源,但是当前还没有访问资源,申请了计数器资源是对资源的预定机制(如同我们看电影前,提前预定电影票)
3)计数器可以有效保证进入共享资源的执行流的数量
4)每一个执行流,想访问共享资源中的一部分时,不是直接访问,而是先申请计数器资源(看电影先买票)
我们把值只能为1, 0两态的计数器叫做二元信号量---本质就是一个锁(互斥功能)。
其实就是将临界资源不要分成很多块了,而是当作一个整体。整体申请,整体释放。
申请信号量,本质是对计数器--,P操作
释放资源,释放信号量,本质是对计数器++,V操作
申请和释放-----PV操作-----原子的-----要么不做,要做就做完,两态的,没有"正在做"的概念。
3、信号量凭什么是进程间通信的一种?
1)通信不仅仅是通信数据,互相协同也是通信
2)要协同,本质也是通信,信号量首先要被所有的通信进程看到
七、IPC在内核中的数据结构设计
在操作系统中,所有的IPC资源,都是整合进操作系统的IPC模块中的。
