进程间通信介绍
首先进程是具有独立性的,要让两个不同的进程,进行通信,前提是:先让两个进程,看到同一份资源,这份资源及不能属于进程A也不能属于进程B,所以只能有操作系统直接或间接提供,然后让一方写入,一方读取完成通信过程,至于通信的目的与后续工作,要结合后续具体场景。
管道
首先Linu下一切皆文件,管道也一样。管道文件也有缓冲区,和磁盘文件一样。但是管道是一个操作系统提供的内存文件,它并不需要再将自己的有效内容刷新到磁盘当中,这种文件也称之为匿名文件。最后我们关闭它操作系统会直接把结构释放掉。
匿名管道
站在文件描述符角度-深度理解管道
- 父进程创建管道
- 父进程fork出子进程
- 父进程关闭fd[0],子进程关闭fd[1]
父进程以读和写的方式打开文件,那么fork之后它的读端和写端都能够被子进程继承下去。然后形成通信的信道,那么我们要形成单向通信的信道,就必须得关闭特定的读写端。
#include <iostream>
#include <string>
#include <errno.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
//让不同的进程看到同一份资源
//任何一种进程间通信,一定要先保证不同的进程看到同一份资源
int pipefd[2] = {0};
//1.创建管道
int n = pipe(pipefd);
if(n == -1)
{
std::cout << "pipe调用失败," << errno << ":" << strerror(errno)<< std::endl;
return 1;
}
std::cout << "pipefd[0]" << ":" << pipefd[0]<< std::endl;//读端
std::cout << "pipefd[1]" << ":" << pipefd[1]<< std::endl;//写端
//2.创建子进程
pid_t id = fork();
if(id == -1)
{
std::cout << "fork失败," << errno << ":" << strerror(errno)<< std::endl;
return 1;
}
if(id == 0)
{
//子进程
//3.关闭不需要的fd,让父进程读取,让子进程写入
close(pipefd[0]);
//4.开始通信---结合具体场景
std::string namestr = "hello,我是子进程";
int cnt = 1;
char buffer[1024] = {0};
while(true)
{
snprintf(buffer,sizeof buffer,"%s,计数器:%d,我的PID: %d\n",namestr.c_str(), ++cnt, getpid());
write(pipefd[1],buffer,strlen(buffer));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//父进程
//3.关闭不需要的fd,让父进程读取,让子进程写入
close(pipefd[1]);
//4.开始通信---结合具体场景
char buffer[1024] = {0};
while(true)
{
int n = read(pipefd[0],buffer,sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = '\0';
std::cout<< "我是父进程,读取的内容是:"<< buffer << std::endl;
}
else if(n == 0)//表示read读取到了文件结尾了
{
std::cout<<"我是父进程,读取到文件结尾"<<std::endl;
break;
}
else // read返回-1,表示出错了
{
std::cout<<"我是父进程,读取出错了"<<std::endl;
break;
}
}
close(pipefd[0]);
int status = 0;
waitpid(id, &status, 0);
std::cout << "sig: " << (status & 0x7F) << std::endl;
return 0;
}
管道的特点:
- 单向通信,如果想双向通信就在加一个管道。管道是半双工的。
- 管道的本质是文件,因为文件描述的生命周期随进程,所以管道的生命周期是随进程的。
- 管道通信,通常用来进行具有"血缘”关系的进程,进行进程间通信。常用与父子通信—原理还是
fork
之后的继承。pipe
打开的管道,并不清楚管道的名称,所以称为匿名管道。 - 在管道通信中,写入的次数,和读取的次数,不是严格匹配的读写次数的多少没有强相关----表现字节流。
- 具有一定的协同能力,让reader和writer能够按照一-定的步骤进行通信-----自带同步机制。
管道的几种特殊场景
- 如果
read
读取完了管道内所有数据,如果对方不写入,就只能等待。 - 如果
writer
端将管道写满了,管道也是文件,文件就有固定大小,所以就不能写了。 - 如果关闭了写端,读取完毕管道数据,在读就会
read
返回0,表示读到了文件结尾。 - 写端一直写,读端关闭,OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程。OS会通过信号来终止进程。13号信号。
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性
注意:
在管道中写入的数据,只要读取了,那么读取过的数据就会被 “删除”。这里的删除,实际上是这些数据被读取过了,它就无效了,意味着下次在写入的时候可以拷贝覆盖。
命名管道
- 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件,命名管道是内存级文件,不会刷盘。
当两个不同的进程,打开同一个文件时,操作系统不会维护两个一样的结构体(struct file
)。当新进程想打开其他文件,新进程做的第一件事情,是在所有已经打开的文件列表里去找这个文件是否已经被打开了,如果没有被打开,就创建一个结构体(struct file
),如果打开了他就直接把对应的结构体(struct file
)对象的地址填入到当前进程的文件描述符表里,而对应的struct file
里面它包含一个叫做int ret
的引用计数器就会加加。
命令行上创建命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo fifo
函数创建命名管道
命名管道也可以从程序里创建,相关函数有:
comm.hpp
#pragma once
#include <iostream>
#include <string>
#define NUM 1024
const std::string fifoname = "./fifo";//要创建的文件名
uint32_t mode = 0666; //创建的文件的权限
server.cc
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
int main()
{
// 1. 创建管道文件,我们今天只需要一次创建
umask(0);//将当前进程的umask 设置为0,这个设置并不影响系统的默认配置,只会影响当前进程
int n = mkfifo(fifoname.c_str(),mode);
if(n == -1)
{
std::cerr << "mkfifo 创建命名管道失败" << errno << " : " << strerror(errno)<< std::endl;
exit(-1);
}
std::cout << "创建 fifo 文件成功" << std::endl;
// 2. 让服务端直接开启管道文件
int fd = open(fifoname.c_str(), O_RDONLY);
if(fd == -1 )
{
std::cerr << "open 打开文件失败" << errno << " : " << strerror(errno) << std::endl;
return 2;
}
std::cout << "打开fifo成功, 开始通信" << std::endl;
// 3. 正常通信
char buffer[NUM]= {0};
while(true)
{
buffer[0] = 0;
ssize_t r = read(fd, buffer, sizeof(buffer) - 1);
if(r > 0)
{
buffer[r] = 0;
std::cout << "client# " << buffer << std::endl;
}
else if( r == 0)
{
std::cout<< "读取到文件结束" << std::endl;
break;
}
else
{
std::cerr <<"文件读取错误:" <<errno << " : " << strerror(errno) << std::endl;
break;
}
}
// 关闭不要的fd
close(fd);
unlink(fifoname.c_str());//删除创建的管道文件
return 0;
}
client.cc
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
int main()
{
//1. 不需创建管道文件,我只需要打开对应的文件即可!
int fd = open(fifoname.c_str(), O_WRONLY);
if(fd == -1 )
{
std::cerr << "open 打开文件失败" << errno << " : " << strerror(errno) << std::endl;
return 2;
}
// 可以进行常规通信了
char buffer[NUM];
while(true)
{
std::cout << "请输入你的消息# ";
char *msg = fgets(buffer, sizeof(buffer), stdin);
buffer[strlen(buffer) - 1] = 0;
// abcde\n\0
// 012345
if(strcasecmp(buffer, "quit") == 0) break;
ssize_t n = write(fd, buffer, strlen(buffer));
}
close(fd);
return 0;
}
运行结果
匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义
system V共享内存
共享内存的原理
先创建出一块物理内存,然后再通过一定的接口将物理内存的地址映射到我们两个要通信进程,各自的地址空间当中,那么最终我们返回地理空间时。进程A和进程B,它们使用时直接就用的是虚拟地址,经过页表转化不就可以访问到我们的物理内存当中特定的同一块内存了吗?所以进程间通信的前提,我们一直在强调,叫做让不同的进程得先看到同一份资源,那么这份同样的资源,是一个内存块,那么我们把它称之为共享内存。
注意:匿名管道和命名管道是通过文件来实现通信的,共享内存是通过内存块让不同的进程实现通信。共享内存允许多个进程进行通信。
共享内存函数
shmget
创建方式 | 说明 |
---|---|
IPC_CREAT | 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回 |
IPC_EXCL | 不能单独使用,一般都要配合IPC_CREAT |
IPC_CREAT l IPC_EXCL | 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 ---- 如果创建成功,对应的共享内存(shmget的返回值),一定是最新的! |
ftok
在Linux中,ipcs
查看进程间通信方式的信息,包括共享内存,消息队列,信号。可以携带一下选项
- q: 只查看系统消息队列信息
- m: 只查看系统共享内存信息
- s: 只查看系统信号量信息
ipcs -m 命令的查看共享内存的列头 | 解释 |
---|---|
key | 操作系统识别共享内存的唯一标识 |
shmid | 用户层识别共享内存的id标识 |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
shmat
shmdt
shmctl
cmd的三种权限 | 说明 |
---|---|
IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid ds数据结构中给出的值 |
IPC_RMID | 删除共享内存段 |
指令ipcrm -m shmid
释放共享内存为shmid的内存。
comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <assert.h>
#include <unistd.h>
#define PATHNAME "."
#define PROJID 0x6667
//共享内存的大小是以PAGE页(4KB)为单位的,开辟空间时以4KB为单位开辟。
const int gsize = 4096; //暂时
key_t getFtok()
{
key_t k = ftok(PATHNAME, PROJID);//保证生成的值是一样的。
if(-1 == k)
{
std::cerr << "调用ftok函数失败" << "error: " << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
return k;
}
std::string toHex(int x)
{
char buffer[64] = {0};
snprintf(buffer, sizeof(buffer), "0x%x", x);
return buffer;
}
static int createShmHelper(key_t k, int size, int flag)
{
int shmid = shmget(k, size, flag);
if(-1 == shmid)
{
std::cerr << "调用shmid函数失败" << "error: " << errno << " : " << strerror(errno) << std::endl;
exit(2);
}
return shmid;
}
//创建共享内存
int createShm(key_t k, int size)
{
umask(0);
//IPC_CREAT | IPC_EXCL | 0666 创建新的共享内存并将共享内存设置权限(这里的权限与文件权限一样)
return createShmHelper(k, size, IPC_CREAT | IPC_EXCL | 0666);
}
//获取共享内存
int getShm(key_t k, int size)
{
return createShmHelper(k, size, IPC_CREAT);
}
//链接共享内存
char* attachShm(int shmid)
{
char *start = (char*)shmat(shmid,nullptr,0);
//shmat 与 malloc 的
return start;
}
//脱离共享内存的链接
void detachShm(void *start)
{
int n = shmdt(start);
assert(n != -1);
(void)n;
}
//删除共享内存
//1. 函数删除
//2. 指令删除
void delShm(int shmid)
{
//shmid_ds * temp; 如果你想看这个结构体的话,就定义一个结构体传入就行,不想看设置为nullptr就行
int n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
}
#define SERVER 1
#define CLIENT 0
struct Init
{
public:
Init(int type)
:_type(type)
{
key_t k = getFtok();
if(type == SERVER)
{
shmid = createShm(k,gsize);
}
else
{
shmid = getShm(k, gsize);
}
start = attachShm(shmid);
}
char *getStart()
{
return (char*)start;
}
~Init()
{
detachShm(start);
if(_type == SERVER)
delShm(shmid);
}
private:
int shmid;
void* start;
int _type; //server or client
};
#endif
shmserver.cc
#include "comm.hpp"
int main()
{
// //1. 创建key
// key_t k = getFtok();
// std::cout << "server key: " << toHex(k) << std::endl;
// //2. 创建共享内存
// int shmid = createShm(k, gsize);
// std::cout << "server shmid: " << shmid << std::endl;
// char *start = attachShm(shmid);
// sleep(15);
// detachShm((void*)start);
// delShm(shmid);
Init init(SERVER);
// start 就已经执行了共享内存的起始空间
char *start = init.getStart();
int n = 0;
// 我们在通信的时候,没有使用任何接口?一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程 直接看到了!
// 因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信,速度最快的
// 共享内存没有任何的保护机制(同步互斥) -- 为什么?管道通过系统接口通信,共享内存直接通信
while(n <= 30)
{
std::cout <<"client -> server# "<< start << std::endl;
sleep(1);
n++;
}
return 0;
}
shmclient.cc
#include "comm.hpp"
int main()
{
// key_t k = getFtok();
// std::cout << "client key: " << toHex(k) << std::endl;
// int shmid = getShm(k, gsize);
// std::cout << "client shmid: " << shmid << std::endl;
// char *start = attachShm(shmid);
// sleep(10);
// detachShm((void*)start);
Init init(CLIENT);
// start 就已经执行了共享内存的起始空间
char *start = init.getStart();
char c = 'A';
while(c <= 'Z')
{
start[c - 'A'] = c;
c++;
start[c - 'A'] = '\0';
sleep(1);
}
return 0;
}
共享内存与管道文件通信速度的比较
在考虑外设的情况下,首先是管道文件要经历4次拷贝,进程从外设(键盘,网络)中读取到数据,写入到语言(C, C++,…)缓冲区中(一次拷贝),按语言缓冲区的刷新策略,刷新到管道文件中(二次拷贝),然后另一进程再将管道文件中的数据拷贝到语言(C, C++,…)缓冲区中(三次拷贝),在拷贝到外设中进行读取(四次拷贝)。其次是共享内存要经历2次拷贝,进程从外设(键盘,网络)中读取到数据写入到虚拟地址空间,虚拟地会通过页表进行映射到物理地址空间完成拷贝(一次拷贝),然后另一进程直接进行读取就行,在这之中虚拟地会通过页表进行映射到物理地址空间,找到数据,然后将数据拷贝到外设中(二次拷贝)。
共享内存和管道不一样,如果写端没有写,读端可以一直读。共享内存在并发访问的时候没有进行任何保护。共享内存了,没有同步和互斥