一、进程间通信介绍
进程是具有独立性的,我们想要通信 — 通信的成本一定不低。
- 你需要先让不同的进程看到同一份资源
- 通信
1.1 什么是通信
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.2 为什么要通信
因为有时候我们需要多进程协同的, 完成某种业务。 比如 cat file | grep ‘hello’。
1.3 怎么通信
- System --聚焦在本地通信
- System V 消息队列
- System V 共享内存
- System V 信号量
- POSIX --让通信过程可以跨主机
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
1.4 如何理解通信的本质问题
- OS需要直接或者间接给通信双方的进程提供 “内存空间”
- 要通信的进程,必须看到一份公共的资源
不同的通信种类, 本质就是,上面所说的资源, 是OS的哪一个模块提供的。
二、管道 - 基于文件系统
2.1 什么是管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
2.2 匿名管道
由上图可知,父进程和子进程可以通过 struct file 这一份公共资源通信。
匿名管道的使用
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
实例代码:
#include<iostream>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstring>
using namespace std;
int main()
{
//第一步:创建管道文件,打开读写端
int fds[2];
int n = pipe(fds);
assert(n == 0);
//第二步:fork
pid_t id = fork();
assert(id >= 0);
if(id ==0)
{
//子进程进行写入
close(fds[0]);
//子进程的通信代码
const char *s = "我是子进程,我正在给你发消息。";
int cnt = 0;
while(1)
{
cnt++;
char buffer[1024];
snprintf(buffer, sizeof buffer, "child->parent:%s[%d][%d] ", s, cnt, getpid());
//写入
write(fds[1], buffer, strlen(buffer));
sleep(1);//一秒读一次
}
//子进程
close(fds[1]);
exit(0);
}
//父进程进行读取
close(fds[1]);
//父进程的通信代码
while(1)
{
char buffer[1024];
ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
if(s > 0)
{
buffer[s] = 0;
}
cout << "Get Message#" << buffer << "my pid:" << getppid() << endl;
}
n = waitpid(id, nullptr, 0);
assert(n == id);
close(fds[0]);
//0:嘴巴,读取
//1:钢笔,输入
return 0;
}
运行结果:
2.3 用fork来共享管道原理
2.4 站在文件描述符角度-深度理解管道
匿名管道:目前能用来进行父子进程之间的通信。
2.5 管道的读写特征
- 当没有数据可读时,read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
- 当管道满的时候,write调用阻塞,直到有进程读走数据。
- 如果文件写端关闭,read会返回0。
- 如果文件读端关闭,OS会终止写端,会给写进程发送信号,终止写端。
2.6 管道的特征
- 管道的生命周期随进程
- 管道可以用来进行具有血缘关系的进程之间进行通信,常用于父子通信
- 管道是面向字节流的(网络)
- 半双工 – 单向通行(特殊概念)
- 互斥与同步机制 – 对共享资源保护的方案
2.7 管道的应用
实现一个父进程与多个子进程进行通信,并且父进程随机与各个子进程发送任务,让子进程执行任务,保持各个子进程保持均匀的执行任务。
实例代码:
#include<iostream>
#include<unistd.h>
#include<cstring>
#include<vector>
#include<string>
#include<stdlib.h>
#include<assert.h>
#include<functional>
#include<sys/types.h>
#include<sys/wait.h>
#include<time.h>
using namespace std;
#define PROCESS_NUM 5
typedef function<void()> func;
class subEp // Endpoint
{
public:
subEp(pid_t subId, int writeFd)
: _subId(subId), _writeFd(writeFd)
{
char nameBuffer[1024];
snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", _num++, _subId, _writeFd);
_name = nameBuffer;
}
public:
static int _num;
std::string _name;
pid_t _subId;
int _writeFd;
};
int subEp::_num = 0;
int recvTask(int readFd)
{
int code = 0;
ssize_t s = read(readFd, &code, sizeof code);
if(s == 4)
{
return code;
}
else if(s <= 0)
{
return -1;
}
else
{
return 0;
}
}
void createProcess(vector<subEp>& subs, vector<func>& funcMap)
{
for(int i = 0; i < PROCESS_NUM; i++)
{
int fds[2];
int n = pipe(fds);
assert(n == 0);
(void)n;
pid_t id = fork();
if(id == 0)
{
//子进程,进行任务处理
close(fds[1]);
while(true)
{
// 1. 获取命令码,如果没有发送,我们子进程应该阻塞
int commandCode = recvTask(fds[0]);
// 2. 完成任务
if (commandCode >= 0 && commandCode < funcMap.size())
funcMap[commandCode]();
else if(commandCode == -1)
break;
}
close(fds[0]);
exit(0);
}
close(fds[0]);
subEp sub(id, fds[1]);
subs.push_back(sub);
}
}
void sendTask(const subEp& process, int taskNum)
{
cout << "send task num:" << taskNum << " send to->" << process._name << endl;
int n = write(process._writeFd, &taskNum, sizeof(taskNum));
assert(n == sizeof(int));
}
//负载均衡
void loadBlanceContrl(vector<subEp>& subs, vector<func>& funcMap)
{
int processNum = subs.size();
int taskNum = funcMap.size();
while(true)
{
//1.选择一个子进程
int subIndex = rand() % processNum;
//2.选择一个任务
int taskIndex = rand()% taskNum;
//3.任务发送给选择的进程
sendTask(subs[subIndex], taskIndex);
sleep(1);
}
//由于子进程会继承父进程所有的东西,包括写端,这样的话,第一个子进程的读端所对应的写端会被第二个子进程所继承,这样第一个子进程会被阻塞,需要关闭第二个子进程的写端才能让第一个子进程正常关闭。所以我们可以倒着关闭文件,先关闭最后一个,这样就可以依次关闭所有文件的端口。
for(int i = processNum; i >= 0; i--)
{
close(subs[i]._writeFd);//waitpid()
}
}
void waitProcess(vector<subEp> process)
{
int size = process.size();
int n = 0;
for(int i = 0; i < size; i++)
{
waitpid(process[i]._subId, nullptr, 0);
cout << "wait process success ... : " << process[i]._subId << endl;
}
}
int main()
{
srand(time(0));
//1.建立子进程,建立和子进程通信的信道,进行读取
//1.1 加载方法表
//[子进程, fds]
vector<subEp> subs;
vector<func> funcMap;
funcMap.push_back([]{cout << "getpid: " << getpid() << " 下载任务" << endl;sleep(1);});
funcMap.push_back([]{cout << "getpid: " << getpid() << " IO任务" << endl;sleep(1);});
funcMap.push_back([]{cout << "getpid: " << getpid() << " 刷新任务" << endl;sleep(1);});
createProcess(subs, funcMap);
//2.父进程控制子进程进行写入
loadBlanceContrl(subs, funcMap);
//3.回收子进程信息
waitProcess(subs);
return 0;
}
运行结果演示:
进阶:通过命名管道实现以上功能。
三、命名管道
- 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件。
3.1 命名管道本质
由上图可知,命名管道本质就是,让不同的进程打开指定名称(路径 + 文件名)的同一个文件,在文件上进行读取操作。所以看待管道,就如同看待文件一样, 管道的使用和文件一致,迎合了“linux一切皆文件的思想”。
3.2 创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
3.3 匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
3.4 命名管道的打开规则
- 如果当前打开操作是为读而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_NONBLOCK enable:立刻返回成功
- 如果当前打开操作是为写而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
3.5 用命名管道实现server&client通信
makefile
.PHONY:all
all:server client
server:server.cpp
g++ -o $@ $^ -std=c++11 -g
client:client.cpp
g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
rm -f server client
comm.hpp
#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<errno.h>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
using namespace std;
#define NAME_PIPE "./mypipe"
bool createFifo(const string& path)
{
umask(0);
int n = mkfifo(path.c_str(), 0600);
if(n == 0)
{
return true;
}
else
{
cout << "errno: " << errno << "err string: " << strerror(errno) << endl;
return false;
}
}
void removeFifo(const string& path)
{
int n = unlink(path.c_str());
assert(n == 0);
(void)n;
}
server.cpp
#include "comm.hpp"
int main()
{
bool r = createFifo(NAME_PIPE);
assert(r);
(void)r;
int rfd = open(NAME_PIPE, O_RDONLY);
if(rfd < 0)
{
cout << "errno: " << errno << "err string: " << strerror(errno) << endl;
exit(1);
}
//read
char buffer[1024];
while(1)
{
ssize_t s = read(rfd, buffer, sizeof(buffer) - 1);
if(s > 0)
{
buffer[s] = 0;
cout << "cilent->server# " << buffer << endl;
}
else if(s == 0)
{
cout << "client quit, me too!" << endl;
break;
}
else
{
cout << "errno: " << errno << "err string: " << strerror(errno) << endl;
break;
}
}
close(rfd);
removeFifo(NAME_PIPE);
return 0;
}
client.cpp
#include "comm.hpp"
int main()
{
int wfd = open(NAME_PIPE, O_WRONLY);
if(wfd < 0)
{
cout << "errno: " << errno << "err string: " << strerror(errno) << endl;
exit(1);
}
//write
char buffer[1024];
while(1)
{
cout << "Please Say# ";
fgets(buffer, sizeof(buffer) - 1, stdin);
if(strlen(buffer) > 0)
buffer[strlen(buffer) - 1] = 0;
ssize_t n = write(wfd, buffer, strlen(buffer));
assert(n == strlen(buffer));
(void)n;
}
close(wfd);
return 0;
}
运行结果演示:
四、system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
4.1 共享内存示意图
- 通过某种调用,在内存中创建一份内存空间。
- 通过某种调用, 让进程(参与通信的多个进程)挂接到这份新开辟的内存空间上!
然后就可以让不同进程看到同一份资源。
不用共享内存的时候,我们可以通过:
- 去关联(去挂接)
- 释放共享内存
4.2 准备工作
- OS内可不可能存在多个进程,同时使用不同的共享内存来进行进程间通信呢?
答:共享内存在系统中可能存在多份,并且操作系统要管理这些不同的共享内存,如何管理呢?先描述。在组织。 - 你怎么保证。两个或多个进程,看到是同一个共享内存呢?
答:共享内存一定要有一定的标识唯一性的ID(这个ID在描述共享内存的数据结构中),方便让不同的进程能识别同一个共享内存资源。这个唯一的标识符,用来进行进程间通信的,本质:让不同的进程看到同一份资源。首先的让不同的进程看到同一个ID。这个ID需要用户自己设定。
4.3 共享内存函数
1. shmget函数
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
2. shmctl函数
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
- IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值。
- IPC_SET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值。
- IPC_RMID:删除共享内存段
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
3. shmat函数 和 shmdt函数
shmat函数
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmdt函数
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
4.4 共享内存资源的释放
systemV的IPC资源,生命周期是随内核的,只能通过。程序员显示的释放(命令,system call)或者是OS重。
查找共享内存命令:ipcs -m
释放共享内存命令: ipcrm -m [shmid]
程序员显示的释放:shmctl(int shmid,IPC_RMID, struct shmid_ds *buf);
shmid:由shmget返回的共享内存标识码(指你开辟的共享内存的唯一标识ID)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构(一般是你开辟等共享内存的地址)
返回值:成功返回0;失败返回-1
4.5 共享内存通信的本质
4.6 共享内存数据结构
由上图可知,在内核中,所有的ipc资源都是通过数组组织起来的。所有的system V标准的ipc资源,XXXid_s结构体的第一个成员都是ipc_perm.
通过上图,我们知道,每个ipc资源的头部元素都一样都是一个ipc_perm 的结构体。struct ipc_perm*[]结构体指针数组只需要保存每个ipc资源的头部四个字节,这就是我们的c++切片。
4.7 共享内存通信举例
makefile
.PHONY:all
all:server client
server:server.cpp
g++ -o $@ $^ -std=c++11 -g
client:client.cpp
g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
rm -f server client
comm.hpp
#pragma once
#include<iostream>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
#include<sys/stat.h>
#include<unistd.h>
using namespace std;
#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4097
server.cpp
#include "comm.hpp"
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0600);//创建全新的共享内存,如果冲突,我们出错返回
if(shmid < 0)
{
perror("shmget");
return 2;
}
cout << "key:" << key << "shmid:" << shmid << endl;
char *mem = (char*) shmat(shmid, NULL, 0);
cout << "attaches shm success" << endl;
//在共享内存中读取数据
int cnt = 26;
while(cnt--)
{
cout << mem << endl;
//sleep(1);
}
shmdt(mem);
cout << "process detaches success" << endl;
//sleep(5);
shmctl(shmid, IPC_RMID, NULL);
cout << "key:" << key << " shmid:" << shmid << "->shm delete sucess..." << endl;
//sleep(5);
return 0;
}
client.cpp
#include "comm.hpp"
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
cout << "key:" << key << endl;
//client这里只需要获取即可
int shmid = shmget(key, SIZE, IPC_CREAT);
if(shmid < 0)
{
perror("shmget");
return 1;
}
char* mem = (char*)shmat(shmid, NULL, 0);
//sleep(5);
cout << "client process attaches success..." << endl;
//在共享内存中写入数据
char c = 'A';
while(c <= 'Z')
{
mem[c - 'A'] = c;
c++;
mem[c - 'A'] = 0;
//sleep(1);
}
shmdt(mem);
//sleep(5);
cout << "client process detaches success..." << endl;
cout << key << endl;
return 0;
}
五、感性认识
信号 vs 信号量
根本毫无关系。