1.进程间通信
进程间通信的背景:
进程之间是相互独立的,进程由内核数据结构和它所对应的代码和数据,通信成本比较高。
进程间通信目的:
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。进程间通信本质:
进程间通信的前提就是让不同的进程看到相同的一份“内存”,这块“内存”不属于任何一个进程,属于操作系统。
2.进程间通信的方式
1.管道 (匿名管道 命名管道)
2.System V通信 (多进程 单机通信)
3.POSIX 通信 (多线程 网络结构)(在这里不讲)
3.管道讲解
1.管道分类:匿名管道,命名管道。
2.什么是管道
管道是一种古老的传输资源的方式,是UNIX中过来的传输方式,是从一个进程传递到另一个进程的方法。管道是单向通信的,传输的都是资源,不能同时完成双向通信。
3.实现原理:
匿名管道:
如何做到不同进程看到相同的内存呢?
fork()函数让具有血缘关系的进程进行进程间通信,常用于父子进程。
创建管道文件,int pipe(int pipefd[2]);
具体实现看代码:
#include <iostream> #include <cstdio> #include <cassert> #include <cstring> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h>using namespace std;int main() {int pipefd[2]={0}; pipefd[0(嘴巴,读书)]: 读端 , pipefd[1(钢笔,写)]: 写端int n = pipe(pipefd);assert(n != -1); // debug assert, release assert(void)n;#ifdef DEBUGcout << "pipefd[0]: " << pipefd[0] << endl; // 3cout << "pipefd[1]: " << pipefd[1] << endl; // 4#endifpid_t id=fork();if(id==0){close(pipefd[1]);char buff[1024*8];while(true){ssize_t s = read(pipefd[0], buff, sizeof(buff) - 1);if(s>0){buff[s] = 0;cout << "child get a message[" << getpid() << "] Father# " << buff << endl;}else{cout << "writer quit(father), me quit!!!" << endl;break;}}close(pipefd[0]);//关闭文件,可以不用exit(0);//return 0;}else if(id>0){close(pipefd[0]);string message = "我是父进程,我正在给你发消息\n";int count = 0;char send_buffer[1024 * 8];while(true){//构建一个变化的字符串snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",message.c_str(), getpid(), count++);//写到一个字符串write(pipefd[1], send_buffer, strlen(send_buffer));if(count==5){cout<<count<<endl;break;}}close(pipefd[1]);//进程等待//int status;pid_t ret = waitpid(id,NULL,0);cout << "id : " << id << " ret: " << ret <<endl;assert(ret > 0); (void)ret;}return 0; }
结论:
- 管道是用来进行具有血缘关系的·进程实现进程间通信--常用于父子进程。
- 具有让进程间协同通信,提供了访问控制。
- 提供面向流的听信服务,面向字节流的服务。
- 管道是基于文件的,文件的生命周期是基于进程的,所以管道的生命周期是基于进程的。
- 管道是单向通信的,就是半双工通信的一种特殊形式。
如果写得快,读得慢,则写满管道之后不再写入。
如果写的慢,读得快,则管道没有数据是,则停下来等待。
如果写入端关闭,则读到文件末尾结束
关闭读,则OS会关闭写进程。
命名管道:
- 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件。
命名管道的创建:
直接在命令行上创建:mkfifo filename
也可以在程序中创建:int mkfifo(const char *filename,mode_t mode);
第一个参数为文件名,第二个为权值
创建命名管道:
int main(int argc, char *argv[]) {mkfifo("p2", 0644);return 0; }
匿名管道与命名管道之间的区别:
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open。
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完。
- 成之后,它们具有相同的语义。
读写规则:
如果当前打开操作是为读而打开FIFO时:
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时:
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO代码实现:
#ifndef _COMM_H_ #define _COMM_H_ //公共文件 #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/wait.h> #include <fcntl.h> #include "log.hpp" using namespace std;#define MODE 0666 #define SIZE 128string ipcPath = "./fifo.ipc";#endif //日志文件#include <iostream> #include <ctime> #include <string>#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//读文件 #include "comm.hpp" #include "log.hpp"static void getMessage(int fd) {char buffer[SIZE];while(true){memset(buffer,'\0',sizeof buffer);int s=read(fd,buffer,sizeof(buffer)-1);if(s>0){cout <<"[" << getpid() << "] "<< "client say> " << buffer << endl;//还有文件}else if(s=0){cerr <<"[" << getpid() << "] " << "read end of file, clien quit, server quit too!" << endl;//读到文件末尾break;}else if(s<0){perror("read s");break;}} } int main() {int id=mkfifo("textfifo.txt",0666);//创建命名管道文件if(id<0)//创建失败{perror("mkfifo id");return 0;}Log("创建管道文件成功", Debug) << " step 1" << endl;//打印日志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){getMessage(fd);exit(1);}}//父进程阻塞等待for(int i=0;i<nums;i++){waitpid(-1, nullptr, 0);}close(id);//关闭文件Log("关闭文件",Debeg) << "step 3" << endl;unlink(ipcPath.c_str()); //删除文件Log("删除文件",Debeg) << "step 4" << endl;return 0; }//写文件 #include "comm.hpp" #include "log.hpp"int main() {int id=open("ipcPath.c_str",O_WRONLY);//获取文件if(id<0){perror("open file");}string buffer;while(true){cout << "Please Enter Message Line :> ";std::getline(std::cin, buffer);//写文件write(id,buffer.c_str(),buffer.size());}close(id);//关闭//unlink(id);return 0; }
结论:双方进程,可以看到同一份文件,,该文件一定在系统路径中,路径具有唯一性,管道文件可以被打开,但是不会将内存中的数据刷新到磁盘中。且有名字。
3.System V 通信
共享内存:共享内存区是最快的IPC形式。共享内存是在物理内存上申请一块空间,再让两个进程各自在页表建立虚拟地址和这块空间的映射关系。这样两个进程看到的就是同一份资源,这一份资源就叫做共享内存。
原理:
共享内存的提供者是操作系统,操作系统通过先描述再组织的方式管理共享内存。
共享内存=共享内存块+对应的共享内存的内核数据结构。
两个进程创建共享内存需要以下步骤:
- 创建共享内存
- 将两个进程关联到共享内存
- 取消两个进程和共享内存的关联
- 删除共享内存
注意: 前两个步骤是为了让两个进程实现通信,后面两个步骤是释放共享内存空间,要不然就会内存泄漏了。(与我们之前用的malloc是类似的)。
创建共享内存所需要的函数:
1.ftok——获取一个共享内存的唯一标识符
函数:key_t ftok(const char *pathname, int proj_id);
功能:获取一个共享内存的唯一标识符 key
参数: pathname 文件名 ;proj_id 只有是一个非0的数都可以 .
返回值:成功返回key;失败返回 -1
2..shmget——创建共享内存
函数:int shmget(key_t key, size_t size, int shmflg);
key:传入ftok函数获取的共享内存唯一标识符
size:共享内存的大小(页(4kb)的整数倍)
shmflg:权限,由9个权限标准构成这里介绍两个选项
IPC_CREAT: 如果底层存在这个标识符的共享内存空间,就打开返回,不存在就创建
IPC_EXCL: 如果底层存在这个标识符的共享内存空间,就出错返回
两个选项合起来用就可以穿甲一个权限的共享内存空间
返回值:
成功返回共享内存标识码值(给用户看的),失败返回-1.3.shmat——将共享内存空间关联到进程地址空间
函数:void *shmat(int shmid, const void *shmaddr, int shmflg);
功能: 将共享内存空间关联到进程地址空间
参数:
shmid:共享内存标识符
shmaddr:指定连接地址。
shmfig:两个可能取值是SHM_RND和SHM_RDONLY
返回值: 成功返回一个指针(虚拟地址空间中共享内存的地址,是一个虚拟地址),失败返回-1
说明:
shmaddr为NULL,核心自动选择一个地址。
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整> 数倍。公式:shmaddr -(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存。4.shmdt——取消关联
函数:int shmdt(const void *shmaddr);
功能: 取消共享内存空间和进程地址空间的关联
参数:
shmaddr:共享内存的起始地址(shmat获取的指针)
返回值: 成功返回0,失败返回-15.shmctl——控制共享内存
函数:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能: 控制共享内存
参数:
shmid:共享内存标识符
cmd:命令,有三个
IPC_STAT: 把shmid_ds结构中设置为共享内存当前关联值
IPC_SET: 把共享内存的当前关联值设置为shmid_ds数据结构中的值
IPC_RMID:删除共享内存段
buf:指向一个报错这共享内存的模式状态和访问权限的数据结构
返回值: 成功返回0,失败返回-1。
共享内存的特性:
对于共享内存来说,它与管道有不同的特性,导致共享内存不同的使用方法
1.管道需要使用系统接口来调用,但是共享内存可以不用经过系统调用,直接可以访,双方进程如果要进行通信,直接进行内存级的读和写即可。共享内存实在堆栈之间的区域的,堆栈相对而生,中间区域为共享内存,不用经过操作系统。
共享内存是最快的,为什么呢?
因为如果是管道,需要从键盘写入,然后再拷贝到自己定义的缓冲区中,然后再次拷贝到内存中,再从内存中拷贝到用户级缓冲区中,最后再拷贝到屏幕中,需要经历最少4词的拷贝过程。
共享内存直接面向用户,所以从键盘中输入的内容直接进入到内存中,然后经过内存到达显示器中,最少只有2次拷贝,所以他的速度是最快的。
对于共享内存的理解:
为了进行进程间通信,需要让不同的进程看到相同的一份资源,所以之前的管道,本质都是优先解决一个问题,让不同的进程看到同一份资源!!!
让不同的进程看到相同的内存,带来了有些时序问题,造成数据不一致问题。
结果:
我们把多个进程(执行流)看到的同一份资源称为临界资源。
我们把自己的进程,访问临界资源的代码,称为临界区。
为了更好地进行临界区的保护,可以让多执行流在任何时刻都只有一个进程进入临界区。即互斥!!!
原子性:要么不做,要么做完,没有中间状态,即为原子性!!
所以,多个执行流,互相运行的时候互相干扰,主要是我们不加保护的访问了相同的资源(临界资源),在非临界区多个执行流是互不干扰的。
代码演示:
#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/SSS"//环境变量 #define PROJ_ID 0x66 #define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍 #define FIFO_NAME "./fifo" //创建一个管道,形成访问控制 class Init { public:Init(){umask(0);int n = mkfifo(FIFO_NAME, 0666);assert(n == 0);(void)n;Log("create fifo success",Notice) << "\n";}~Init(){unlink(FIFO_NAME);Log("remove fifo success",Notice) << "\n";} };#define READ O_RDONLY #define WRITE O_WRONLYint OpenFIFO(std::string pathname, int flags) {int fd = open(pathname.c_str(), flags);assert(fd >= 0);return fd; }void Wait(int fd) {Log("等待中....", Notice) << "\n";uint32_t temp = 0;ssize_t s = read(fd, &temp, sizeof(uint32_t));assert(s == sizeof(uint32_t));(void)s; }void Signal(int fd) {uint32_t temp = 1;ssize_t s = write(fd, &temp, sizeof(uint32_t));assert(s == sizeof(uint32_t));(void)s;Log("唤醒中....", Notice) << "\n"; }void CloseFifo(int fd) {close(fd); }#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#include "comm.hpp" #include "log.hpp"string TransToHex(key_t k) {char buffer[32];snprintf(buffer, sizeof buffer, "0x%x", k);return buffer; }int main() {key_t key=ftok(PATH_NAME,PROJ_ID);assert(key!=-1);Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;//创建共享内存int shmid=shmget(key,SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);assert(shmid!=-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);// 这里就是通信的逻辑了// 将共享内存当成一个大字符串// char buffer[SHM_SIZE];// 结论1: 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到对方写入的数据。// 共享内存是所有进程间通信(IPC),速度最快的!不需要过多的拷贝!!(不需要将数据给操作系统)// 结论2: 共享内存缺乏访问控制!会带来并发问题 【如果我想一定程度的访问控制呢? 能】int fd = OpenFIFO(FIFO_NAME, READ);for(;;){Wait(fd);// 临界区printf("%s\n", shmaddr);if(strcmp(shmaddr, "quit") == 0) break;// sleep(1);}// 4. 将指定的共享内存,从自己的地址空间中取消关联int n = shmdt(shmaddr);assert(n != -1);(void)n;Log("detach 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; }#include "comm.hpp"int main() {key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值if (k < 0){Log("create key failed", Error) << " client key : " << k << endl;exit(1);}Log("create 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", Error) << " 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", Error) << " client key : " << k << endl;sleep(10);int fd = OpenFIFO(FIFO_NAME, WRITE);//使用// client将共享内存看做一个char 类型的bufferwhile(true){ssize_t s = read(0, shmaddr, SHM_SIZE-1);if(s > 0){shmaddr[s-1] = 0;Signal(fd);if(strcmp(shmaddr,"quit") == 0) break;}}CloseFifo(fd)// 去关联int n = shmdt(shmaddr);//取消关联assert(n != -1);Log("detach shm success", Error) << " client key : " << k << endl;sleep(10);// client 要不要chmctl删除呢?不需要!!return 0; }