进程通信
- 进程为什么需要通信呢?
- 进程通信的技术背景
- 进程通信本质
- 进程通信分类
- 管道
- 匿名管道pipe
- 匿名管道原理
- 管道特点
- 命名管道
- 创建命名管道
- 命名管道原理
- System V IPC
- 管道与 System V的区别
- 共享内存函数
- ftok()
- shmget()
- shmat()
- shmdt()
- shmctl()
- 删除共享内存
- System V 代码演示
- 管道和共享内存总结
进程为什么需要通信呢?
虽然进程都有相对独立性,但是还是需要进行互相通信的,比如说QQ发消息、一个进程想要给另一个进程发送数据、几个进程之间想共享一份数据等等,这些都需要进程进行通信。、
进程通信的技术背景
- 进程是有独立性的。虚拟地址空间+页表 保证进程运行的独立性(进程内核数据结构+进程的代码和数据)
- 由于独立性的原因,通信成本会比较高
进程通信本质
- 进程通信的前提,首先需要让不同的进程看到同一块“内存”(特定的结构组织的)
- 同一块“内存”,不属于任意进程,是在进程的共享代码段。
进程通信分类
管道
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
# | 命令就是连接两个进程之间的管道命令
# who命令输出两行数据,交给wc -l 进程,统计行数,输出。
who | wc -l
匿名管道pipe
功能:创建一个无名管道
int pipe(int fd [2])
参数:
fd:文件描述符数组,其中fd[0],为读端;fd[1]为写端。
返回值:成功返回0,失败返回错误代码。
使用fork(创建父子进程进行通信)来验证管道的原理:
fork之后:子进程会创建一份新的PCB,并且会复制一份父进程的文件描述符数组;像打开文件的信息,管道等都只共享的一份。
下面这段代码实现了,子进程写入管道,父进程从管道读出这一功能:
#include<iostream>
#include<unistd.h>
//#include <string.h>
#include<cstring>
#include<cerrno>
#include<cassert>
#include<cstdlib>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{//利用fork让两个不的进程看到同一份资源int pipefd[2] = {0};//1.创建匿名管道int n = pipe(pipefd);if(n<0) //创建管道失败{std::cout<<"pipe error, "<<errno<<" : "<<strerror(errno)<<std::endl;return 1;}std::cout<<"pipefd[0]: "<<pipefd[0]<<std::endl; //0读端std::cout<<"pipefd[1]: "<<pipefd[1]<<std::endl; //1写端//2. 创建子进程pid_t id = fork();assert(id!=-1);if(id == 0) //子进程{//3.关掉不需要得fd,实现让父进程读,子进程写得功能close(pipefd[0]);//4.开始通信 --结合某种场景int cnt = 0;while(true){char x = 'X';write(pipefd[1],&x,1);std::cout<<"Cnt: "<<cnt++<<std::endl;sleep(1);}close(pipefd[1]);exit(0);}//父进程//3. 关闭不需要的fd,让父进程读,子进程写close(pipefd[1]);//4.开始通信char buffer[1024];int cnt = 0;while(true){int n = read(pipefd[0], buffer, sizeof(buffer)-1);if(n>0){buffer[n]='\0';std::cout<<"我是父进程,child give me messages:"<<buffer<<std::endl;}else if(n==0){std::cout<<"我是父进程,读到了文件结尾"<<std::endl;break;}else{std::cout<<"我是父进程,读管道异常"<<std::endl;break;}sleep(2);if(cnt++>5) break;}close(pipefd[0]);int status = 0;waitpid(id, &status, 0);std::cout<<" sig: "<<(status &0x7f) << std::endl;sleep(20);return 0;
}
匿名管道原理
fork之后,子进程会复制一份父进程的文件描述符,指向父进程已经打开的文件资源等等。而用父进程创建匿名管道后,会在内存中,开辟一份空间,为父子进程之间提供通信,而父进程创建的这份资源会以fd的形式存在,父进程的打开文件表中。子进程复制后,子进程也能访问。
如下图:
管道特点
- 管道是用来进行具有血缘关系的进程进行进程见通信。—常用于父子通信
- 管道具有通过让进程间协同,提供了访问控制。
- 写快,读慢,写满了不能再写了。
- 写慢,读快,管道中没有数据的时候,读必须等待
- 写关,读0,标识读到了文件结尾。
- 读关,写继续写,OS终止写进程。
- 管道提供的是面向流式的通信服务,----面向字节流
- 管道是基于文件的,文件的生命周期是随进程的,管道的生命周期是随进程的
- 管道是单向通信,属于半双工通信的特殊情况,如果要进行全双工通信,需要创建两个管道。
命名管道
管道(匿名管道)应用的一个限制是只能在具有共同祖先的进程间通信。
但是,如果想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,这就是命名管道(一种特殊类型的文件)。
创建命名管道
- 可以从命令行中创建管道:p开头即为管道文件
mkfifo [filename]
- 可以使用函数创建命名管道:
int mkfifo(const char* filename, mode_t mode)
第一个参数是创建管道的路径
第二个参数是管道读写的权限,一般是0666
命名管道原理
首先命名管道是一个特殊的文件,因此它可以被打开到内存,但是不会将内存数据刷新到磁盘,该文件在系统中具有唯一路径,因此进程可以通过该路径找到管道文件,进行通信。
下面实现一段功能:父进程创建管道文件,然后从管道读消息,子进程从管道写消息。
//1.comm.hpp
#ifndef _COMM_H_
#define _COMM_H_#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<cstring>
#include "Log.hpp"
using namespace std;#define MODE 0666
#define SIZE 128string ipcPath = "./fifo.ipc";#endif//2. Log.hpp
#ifndef _LOG_H_
#define _LOG_H_#include<iostream>
#include<ctime>#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3const 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//3.client.cxx
#include "comm.hpp"int main()
{//1.获取管道文件int fd = open(ipcPath.c_str(), O_WRONLY);if(fd<0){perror("open");exit(1);}//2.ipc过程string buffer;while (true){cout<<"Please Enter Message Line :>";std::getline(std::cin, buffer);write(fd, buffer.c_str(), buffer.size());}//3.关闭文件close(fd);return 0;
}//4. server.cxx
#include "comm.hpp"
#include<sys/wait.h>static void getMessage(int fd)
{char buffer[SIZE];while(true){memset(buffer, '\0', sizeof(buffer));ssize_t s = read(fd, buffer, sizeof(buffer)-1);if(s > 0){cout<<"[ "<<getpid()<<" ]"<<"client sat>:"<<buffer<<endl;}else if(s == 0){//end of filecerr<<"[ "<<getpid()<<" ]"<<"read end of file, client quit, server quit too!"<<endl;break;}else{//read errorperror("read");break;}}
}int main()
{//1.创建管道文件if(mkfifo(ipcPath.c_str(), MODE)<0){perror("mkfifo");exit(1);}Log("管道文件创建成功", Debug)<<" step1"<<endl;//2.正常文件操作int fd = open(ipcPath.c_str(), O_RDONLY);if(fd<0){perror("open");exit(2);}Log("打开管道文件成功",Debug)<<" step 2"<<endl;int nums = 3;for(int i = 0; i < nums; i++){pid_t id = fork();if(id == 0){//3.编写正常的通信代码getMessage(fd);exit(1);}}for(int i = 0; i<nums;i++){waitpid(-1, nullptr, 0);}//4.关闭文件close(fd);Log("关闭管道文件成功",Debug)<<" step 3"<<endl;unlink(ipcPath.c_str()); //通信完毕,删除管道文件Log("删除管道文件成功", Debug)<<" step 4"<<endl;return 0;
}
System V IPC
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程地址空间,这些进程间的数据传递不再涉及到内核,换句话说,进程不再通过执行进入内核的系统调用来传递彼此的数据。
如下图是 System V的通信方式:
该方式直接在内存里建立一块空间,供进程通信(进程AB都会在自己的页表上,建立好虚拟地址到物理地址的映射),因此只需访问自己空间的地址,就可以实现进程间通信。
管道与 System V的区别
管道对应的公共资源是文件,而文件是OS内核对应的数据结构,需要操作系统维护,因此需要系统调用来实现。而System V只需要在物理内存上申请一块空间,而内存是用户空间里的内容,用户可以不经过系统调用直接进行访问,直接进行内存级的读写即可。
但是共享内存的提供者是OS,因为OS要管理共享内存,因为OS要先描述再组织->共享内存 = 共享内存快+对应的共享内存的内核数据结构。 申请需要OS管理,但是申请完了之后,用户可以直接访问。
共享内存函数
ftok()
第0步
功能:生成一个唯一的key,供shmget使用生成共享内存段。
int ftok(const char* pathname, int proj_id);
参数:
pathname:必须是存在的、可访问的文件路径
proj_id:至少是8bit的非0数字(自己给定)
返回值:若生成成功,返回唯一的key值。失败返回-1
shmget()
1 .第一步
功能:用来创建共享内存
int shmget(key_t key, size_t size, int shmflg);
参数:
key:这个共享内存段的名字(唯一id)
size:共享内存的大小
shmfg:由九个标志权限构成,他们的用法和创建文件open使用的mode模式标志一样
返回值:成功返回一个非负整数(该段共享内存段的标识码,类似于fd);失败返回-1
- 说明shmfg参数的具体解释:
- IPC_CREAT:创建共享内存,如果底层已经存在,获取已存在的id,并且返回;如果不存在,创建共享内存,并返回
- IPC_EXCL: 单独使用无意义,和上面一起使用:若底层不存在,创建它并返回;若底层存在,出错返回。–>若返回成功的一定是一个全新的shm
shmat()
2.第二步
功能:将共享内存段连接到进程地址空间(在页表上生成映射)
void shmat(int shmid, const void shmaddr, int shmflg)
参数:
shmid:共享内存标识
shmaddr:指定连接的地址(一般填nullptr)
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存的第一个地址;失败返回-1
- 说明shmflg:
- shmaddr:为NULL,核心自动选择一个地址
- shmaddr:不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址
- shmadd:不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
- shmflg=SHM_RDONLY,标识该连接操作用作只读共享内存
shmdt()
第3步
功能:将共享内存点与当前进程脱离
int shmdt(const void* shmaddr);
参数:
shmaddr:由shmat返回的指针
返回值:成功返回0, 失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段!
shmctl()
第四步
功能:用来控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
- 说明:cmd:
- IPC_STAT:把shmid_ds结构中的数据结构设置为共享内存的关联值
- IPC_SET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中的值
- IPC_RMID:删除共享内存段
删除共享内存
首先这种内存共享方式,当我们程序结束后,如果代码不主动删除,那么该内存不会被释放!
我们可以使用代码删除、也可以手动删除,手动删除使用如下命令即可。
注意:key只有在创建的时候才有,其他时候访问共享内存都是用shmid
System V 代码演示
该功能为:Server创建共享内存,并删除。Cilentt使用该共享内存传输数据。
// Log.hpp
#ifndef _LOG_H_
#define _LOG_H_#include<iostream>
#include<ctime>#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3const 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//comm.hpp
#pragma once#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cassert>
#include"Log.hpp"using namespace std; //不推荐#define PATH_NAME "/home/xty"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存大小,最好是4096的整数倍//shmClient.cc
#include "comm.hpp"int main()
{key_t k = ftok(PATH_NAME, PROJ_ID);if(k<0){Log("creat key failed", Error)<<" client key : "<< k <<endl;exit(-1);}Log("creat key done", Debug)<<" client key : "<< k <<endl;//获取共享内存int shmid = shmget(k, SHM_SIZE, 0);if(shmid < 0){Log("create shm failed", Error) <<" client key : "<< k <<endl;exit(2);}Log(" create shm success", Debug) <<" client key : " << k <<endl;sleep(10);char* shmaddr = (char *)shmat(shmid, nullptr, 0);if(shmaddr == nullptr){Log(" attach shm failed", Error) <<" client key : "<< k<<endl;exit(3);}Log(" attach shm success", Debug) <<" client key : "<< k<<endl;sleep(10);// 使用//去关联int n = shmdt(shmaddr);assert(n!=-1);Log("detach shm success", Debug) << "client key : "<< k <<endl;sleep(10);// client 要不要chmctl删除呢?不需要return 0;
}//shmServer.cc
#include "comm.hpp"string TransToHex(key_t k)
{char buffer[32];snprintf(buffer, sizeof(buffer), "0x%x", k);return buffer;
}int main()
{//1.创建公共的key值key_t k = ftok(PATH_NAME, PROJ_ID);assert(k!=-1);Log("create key done", Debug)<<" server key : "<< TransToHex(k) <<endl;//2.创建共享内存 --建议要创建一个全新的共享内存--通信的发起者int shmid = shmget(k, SHM_SIZE, IPC_CREAT|IPC_EXCL|0666);if(shmid == -1){perror("shmget");exit(1);}Log(" create shm done", Debug) << " shmid : "<< shmid<<endl;sleep(10);//3.将指定的共享内存,挂接到自己的地址空间char * shmaddr = (char *)shmat(shmid, nullptr, 0);Log("attach shm done", Debug) << " shmid : " << shmid <<endl;sleep(10);//通信的逻辑...//4.将指定的共享内存,从自己的地址空间中 去 关联int n = shmdt(shmaddr);assert(n!=-1);(void)n;Log("deatch shm done", Debug) << " shmid : " << shmid <<endl;sleep(10);//5. 删除共享内存, IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存n = shmctl(shmid, IPC_RMID, nullptr);assert( n!=-1);(void)n;Log("delete shm done", Debug) << " shmid : " <<shmid<<endl;return 0;
}//makefile
.PHONY:allall:shmClient shmServershmClient:shmClient.ccg++ -o $@ $^ -std=c++11
shmServer:shmServer.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f shmClient shmServer
管道和共享内存总结
管道通信的过程:由键盘->自己定义的缓冲区->进程A->write给内核缓冲区->内核缓冲区给管道文件->管道给内核缓冲区->read读到进程B处->自己定义的缓冲区->打印到屏幕。
共享内存通信:由键盘->自己定义的缓冲区->进程A->直接写入共享内存->进程B读贡献内存->到自己定义的缓冲区->屏幕。