文章目录
1 进程间通信的目的
①数据传输:一个进程需要将它的数据发送给另一个进程
②资源共享:多个进程之间共享同样的资源
③通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
④进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
2进程间通信方式的分类
2.1 linux原生可以提供
- 匿名管道
- 命名管道
2.2 SystemV
主要是多进程,单机通信
- 共享内存
- 消息队列
- 信号量
2.3 POSIX
主要是多线程,网络通信
3 进程间通信的本质
先让不同的进程看到同一份资源
4 管道
4.1 管道的特征
①有入口,有出口
②单向传输内容
③传输的都是"资源"(数据)
4.2 匿名管道
4.2.1 使用的接口
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
4.2.2 如何实现
①分别以读,写方式打开同一个文件
②fork()创建子进程
③双方进程各自关闭自己不需要的文件描述符
4.2.3 demo代码 父子进程进行通信
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstdio>
#include <cstring>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int pipefd[2] = {0};//创建文件描述符数组
int n = pipe(pipefd);//创建匿名管道
assert(n != -1);
pid_t id = fork();
assert(id != -1);
if (id == 0) // 子进程进行读
{
char buffer[1024 * 8];
close(pipefd[1]); // 关闭写端
while (true)
{
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s == 0) // 读到文件末尾
{
cout << "father quit I quit" << endl;
break;
}
else
{
cout << "child read a message[" << getpid() << "] father say:" << buffer << endl;
}
}
exit(0);
}
else // 父进程进行写
{
close(pipefd[0]); // 关闭读端
string message = "我是父进程,我正在给你发消息";
int count = 0;
char send[1024 * 8];
// sleep(10);
while (true)
{
int n = snprintf(send, sizeof(send) - 1, "%s[%d] %d", message.c_str(), getpid(), count++);
write(pipefd[1], send, sizeof(send) - 1);
sleep(1);
if (count == 10)
{
cout << "writer quit(father)" << endl;
break;
}
}
close(pipefd[1]);
int ret = waitpid(id, nullptr, 0);
assert(ret > 0);
cout << ret << endl;
return 0;
}
}
4.2.4 匿名管道的特点
①具有血缘关系的进程来进行进程间通信,常用于父子通信
②实现进程间协同,提供了访问控制
写快,读慢,写满便不能再写了
写慢,读快,管道没有数据的时候,读必须等待
③管道提供的是面向流式的通信服务:面向字节流
每次写一条,读可以读多条
④管道是基于文件的,文件的生命周期是随进程的,管道的生命周期也是随进程的。
⑤管道是单向通信的,就是半双工通信的一种特殊情况
验证:
可以看到,由于在父进程进行写之前进行了休眠,匿名管道没有数据,所以子进程在读的时候会先阻塞,直到父进程停止休眠,往管道里面写数据。
父进程一直往管道里面写数据,由于子进程刚开始在休眠,没有读数据,直到休眠结束后,是将管道中已写的数据一次全部读出。即可以一次读多条信息
4.3 命名管道
4.3.1 原理
管道文件:一种特殊的文件,可以被打开,但是不会将内存数据进行刷新到磁盘
文件一定在系统路径中,路径具有唯一性。所以双方进程,可以通过管道文件的路径,看到同一份资源。
4.3.2 如何实现
①创建命名管道
int mkfifo(const char *filename,mode_t mode)
filename:你要创建的文件名字
mode :文件权限
返回值: 0:success ; -1: error
②之后就是进行相关的文件操作。双方进程各自以读方式和写方式打开文件,一方进行写数据,另一方进行读数据。
4.3.3 代码实现
mutiServer.cxx
#include "comm.hpp"
#include <wait.h>
void RDMessage(int fd)
{
char message[SIZE];
memset(message, '\0', sizeof(message));
while (true)
{
ssize_t s = read(fd, message, sizeof(message));
if (s > 0)
{
std::cout << "[" << getpid() << "]client say:" << message << endl;
}
else if (s == 0) // 读到最后一个
{
cout << "Read the last one client quit server quit too" << endl;
break;
}
else
{
perror("read");
exit(3);
}
}
}
int main()
{
// 创建命名管道
if (mkfifo(ipcPath.c_str(), MODE) == -1)
{
perror("mkfifo");
exit(1);
}
Log("创建命名管道成功", Debug) << "step1" << endl;
// 进行正常的文件操作
// 打开文件
int fd = open(ipcPath.c_str(), O_RDONLY); // 以只读方式打开
if (fd == -1)
{
perror("open");
exit(2);
}
Log("打开文件成功", Debug) << "step2" << endl;
// 进行通信操作,读信息
for (int i = 0; i < 3; i++)
{
pid_t id = fork();
if (id == 0)
{
RDMessage(fd);
exit(3);
}
}
for (int i = 0; i < 3; i++)
{
int ret = waitpid(-1, nullptr, 0);
if (ret == -1)
{
perror("wait");
}
}
// 关闭文件
if (close(fd) == -1)
{
perror("close");
exit(4);
}
Log("关闭文件成功", Debug) << "step3" << endl;
// 删除管道
unlink(ipcPath.c_str());
Log("删除管道文件成功", Debug) << "step4" << endl;
return 0;
}
client.cxx
#include"comm.hpp"
int main()
{
//打开文件
int fd=open(ipcPath.c_str(),O_WRONLY);
if(fd<0)
{
perror("open");
}
//进行通信操作,往命名管道里面写
string message;
while(true)
{
cout<<"Please Enter Your Message"<<endl;
getline(cin,message);
write(fd,message.c_str(),message.size());
}
//关闭文件
close(fd);
return 0;
}
comm.hpp
#ifndef _COMM_H_
#define _COMM_H_
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
using namespace std;
#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc";
#endif
Log.hpp
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
可以看到,命名管道也是具有访问控制的。当前打开操作是为读而打开管道时,会先阻塞直到有相应进程为写而打开管道。进行写操作打开管道的时候也是如此。在读管道数据的时候,只有client进程先往管道里面写数据,server进程才会读数据。管道里面没有数据的时候会阻塞。
5 system V共享内存
5.1 实现原理
5.2 共享内存函数
共享内存提供者是操作系统
共享内存=共享内存块+对应的共享内存的内核数据结构
5.2.1 ftok函数
功能:通过一定的算法,使两个进程产生相同的key值
这是进行通信的关键:两个进程,只要key值相同,就代表看到的是同一个共享内存
原型
key_t ftok(const char *pathname, int proj_id);
参数
pathname:指定的文件名,这个文件必须是存在的而且可以访问的
proj_id:在0~255之间随意设置
返回值:当函数执行成功,则会返回key_t键值,否则返回-1
函数便是利用传进来的两个参数pathname和proj_id,使用一些算法,生成key值
5.2.2 shmget函数
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:ftok函数的返回值
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码(shmid);失败返回-1
5.2.3 shmat函数
功能:将共享内存段挂接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址(一般设置为NULL,自动选择一个地址)
shmflg:挂接以后允许的操作。比如只读、只写、可读可写等,一般设置为0,表示可读可写
返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1
5.2.4 shmdt函数
功能:将共享内存段与当前进程去关联
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
5.2.5 shmctl函数
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
5.3 代码实现
shmServer
#include "comm.hpp"
#include <iostream>
#include <cstring>
int main()
{
// 创建公共的key值
key_t k = ftok(PATH_NAME, PROJ_ID);
assert(k != -1);
Log("create key success ", Debug) << "server key:" << k << endl;
sleep(1);
// 创建一个全新的共享内存
//使用IPC_CREAT和IPC_EXCL:如果底层不存在,创建之,并返回;如果存在,出错返回。可以保证每一次创建的都是新的共享内存
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget");
exit(1);
}
Log("create shm success ", Debug) << "shmid:" << shmid << endl;
sleep(1);
// 将指定的共享内存,挂接到自己的地址空间
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
Log(" attach shm success ", Debug) << "shmid:" << shmid << endl;
sleep(1);
// 通信1
// 将shm想象成为一个大的数组
while (true)
{
cout << shmaddr << endl;//直接将共享内存中的内容输出
if (strcmp(shmaddr, "quit") == 0)//quit为结束标志
{
break;
}
sleep(1);
}
// 将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmaddr);
assert(n != -1);
(void)n;
Log(" detach shm success ", Debug) << "shmid:" << shmid << endl;
// 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
int s = shmctl(shmid, IPC_RMID, nullptr);
assert(s != -1);
(void)n;
Log(" delete shm success ", Debug) << "shmid:" << shmid << endl;
return 0;
}
shmClient
#include "comm.hpp"
#include <iostream>
int main()
{
// 生成公共的key值
key_t k = ftok(PATH_NAME, PROJ_ID);
if (k < 0)
{
perror("ftok");
exit(1);
}
Log("create key success ", Debug) << "client key:" << k << endl;
// 获得共享内存
int shmid = shmget(k, SHM_SIZE, 0);
if (shmid == -1)
{
perror("shmid");
exit(2);
}
Log("get shm success ", Debug) << "shmid:" << shmid << endl;
// 将共享内存挂接到地址空间上
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
Log("attach shm success ", Debug) << "shmid:" << shmid << endl;
// 通信过程1
char ch='a';
for(ch='a';ch<='d';ch++)//向共享内存中写入数据
{
snprintf(shmaddr,SIZE_MAX,"[%d] hello server I am Client :%c\n",getpid(),ch);
sleep(3);
}
snprintf(shmaddr,sizeof(shmaddr),"quit");
// 将指定的共享内存,从自己的地址空间去关联
shmdt(shmaddr);
Log("detach shm success ", Debug) << "shmid:" << shmid << endl;
return 0;
}
comm.hpp
#include <iostream>
#include<cstring>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cassert>
#include "Log.hpp"
using namespace std;
#define PATH_NAME "/home/ztb"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍
#define FIFE_NAME "./fifo.ipc"
通过运行结果可以发现
①只要通信双方使用shm,一方直接向共享内存中写入数据,另一方就可以立马看到。
这是因为共享内存创建好后属于用户空间,不用经过系统调用,直接可以访问。
所以共享内存是所有IPC中速度最快的,不需要过多拷贝(不需要将数据交给操作系统)
②共享内存缺乏访问控制
eg:在共享内存中没有数据的时候,也会进行读取
在写的一方写数据时,有可能数据还没有写完,此时读方已经将数据读走。
因为会带来并发问题
如何解决? 可以通过命名管道解决
要实现访问控制,即共享内存中写了数据之后才能读数据。
所以在读数据之前,如果共享内存是空的,便要阻塞在这里。
命名管道在读取数据的时候,如果没有数据,便会阻塞在这里
因此,在往共享内存写入数据后,往命名管道内写数据,证明可以开始读了
在读共享内存数据之前,先读命名管道内的数据,如果没有数据,证明此时共享内存也是空的,便会阻塞在这里,直到命名管道内有数据,可以开始读,此时共享内存也已经写了数据。实现了访问控制
comm.hpp
#pragma once
#include <iostream>
#include<cstring>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cassert>
#include "Log.hpp"
using namespace std; //不推荐
#define PATH_NAME "/home/ztb"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍
#define FIFE_NAME "./fifo.ipc"
class Init{
public:
Init()
{
int n=mkfifo(FIFE_NAME,0666);
assert(n==0);
Log("create fifo success\n",Notice);
}
~Init()
{
unlink(FIFE_NAME);
Log("delete fifo success\n",Notice);
}
};
//定义Init类,在构造函数里面进行创建命名管道文件
// 在析构函数里面删除匿名管道文件
#define READ O_RDONLY
#define WRITE O_WRONLY
int OPEN_FIFO(string pathname,int flags)//打开文件
{
int fd=open(pathname.c_str(),flags);
assert(fd>=0);
return fd;
}
//要实现同步控制,在写数据之后才能进行读数据
//在读共享内存中的数据之前,先调用wait()函数,只有写了数据后,才读数据
void wait(int fd)
{
cout<<"等待中....."<<endl;
uint32_t temp=0;
ssize_t s=read(fd,&temp,sizeof(uint32_t));
//命名管道中的数据是什么并不关心,主要是命名管道中没有数据便会阻塞在这里
assert(s==sizeof(uint32_t));
}
//在往共享内存中写数据之后,调用signal函数,往命名管道中写数据,这样另一方才能读数据,解除阻塞
void signal(int fd)
{
uint32_t temp=1;
ssize_t s= write(fd,&temp,sizeof(uint32_t));
assert(s==sizeof(uint32_t));
cout<<"唤醒Server进程"<<endl;
}
void CLOSE_FIFO(int fd)//关闭文件
{
close(fd);
}
shmServer
// 通信2 利用命名管道实现共享内存的访问控制
int fd=OPEN_FIFO(FIFE_NAME,READ);
cout<<"server fd:"<<fd<<endl;
while(true)
{
wait(fd);
cout<<shmaddr<<endl;
if(strcmp(shmaddr,"quit")==0)
break;
}
}
CLOSE_FIFO(fd);
shmClient
int fd = OPEN_FIFO(FIFE_NAME, WRITE);
cout << "client fd:" << fd << endl;
while (true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE); // 0号文件描述符代表的是标准输入,即将数据直接从标准输入读进共享内存中,实现写数据操作
if (s > 0)
{
shmaddr[s - 1] = 0;
signal(fd);
if (strcmp(shmaddr, "quit") == 0)
{
break;
}
}
}
CLOSE_FIFO(fd);