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 DEBUG cout << "pipefd[0]: " << pipefd[0] << endl; // 3 cout << "pipefd[1]: " << pipefd[1] << endl; // 4 #endif pid_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 128 string 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_WRONLY int 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 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" 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 类型的buffer while(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; }