进程通信的介绍
目的
数据传输:一个进程将它的数据发送给另一个进程;
资源共享:多个进程间共享资源;
通知事件:一个进程向另一个或一组进程发送消息,同时事件如,进程终止时要通知父进程;
进程控制:有的进程需要完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并及时知道它的状态;
进程间通信的必要性
在单进程下不通信是可以的,但是多进程的并发能力就无法使用,更加无法实现多进程协同,有的是为了传输数据,有的为了同步执行流,有的为了消息通知;
进程间通信的技术背景
1、进程具有独立性,使用虚拟地址空间+页表的方式,保证进程运行的独立性(进程内核数据结构+进程的代码和数据);
2、通信成本会比较高,进程间通信的前提是首先让不同的进程看到同一块空间;
3、那么进程看到的同一块空间,不能属于任何一个进程,应该更强调共享;
理解进程间通信
进程的运行具有独立性,所以进程想要通信难度是比较大的,通信的本质是进程之间数据的交换,而交换就要拷贝,拷贝就要有一份提供给两个进程拷贝的空间,也就是在进程通信之时需要让不同的进程看到同一份资源(内存空间);
为什么要进行进程间通信,交换数据,控制,通知等目标,需要进程协同;
进程通信的发展
在早期参与操作系统开发的人员,再使用操作系统工作时,发现只是用单进程完成一些任务比较困难;比如要将文件的内容打印出来,然后要逆置,还要统计行数,做这些事情的时候就耦合在一起了,所以就可以做成三个功能,让三个进程分别执行不同的功能;
管道
System V进程间通信(了解)是一种标准,接口复杂,不好整合到文件场景,主要做单机通信;
POSIX进程间通信(使用较多),网络通信;
进程通信分类
管道
匿名管道,pipe;
命名管道;
System V IPC
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
管道
生活中的管道一般只能单方向通信;传送的都是资源;
而计算机中的管道也是传输资源的,这种资源是数据;
把一个进程连接到另一个进程的数据流成为一个管道;
who | wc -l
将who进程的数据通过管道传送给wc -l进程;
管道的提供者是操作系统;
父进程和子进程通过集成文件描述符表方案,让父进程先把文件打开,然后让子进程也指向同一份文件,这个文件就是管道;
首先父进程(下图中上边的进程),让这个进程分别以读写打开文件,给父进程返回的时候会有两个操作符,分别是以读打开的和以写打开的操作符;然后创建子进程,子进程也会有一份文件描述符表;在子进程最初创建的时候,会拷贝父进程的文件描述付表,所以子进程也会指向,同一个文件;这样就让不同的进程看到了同一份资源;然后双进程关闭自己不需要的文件描述符,如果让父进程进行写入,子进程进行读取,则关闭父进程的读端,关闭子进程的写端;通信数据不会写到磁盘中;
这种方式能够让具有血缘关系的进程进行进程间通信,常用于父子进程;
Makefile
mypipe:mypipe.ccg++ -o $@ $^ -std=c++11.PHONY:cleanclean:rm -f mypipe
mypipe
#include <iostream> #include <assert.h> #include <unistd.h>using namespace std;int main() {//1、创建管道int pipefd[2] = { 0 };int n = pipe(pipefd);//完成管道的创建assert(n != -1);//判断pipe调用是否失败,如果等于-1则失败;(void)n;//assert在debug下式有效的,但是在release断言就没了,如果没有这一行,那么在release下就会出现//n被定义了但是没有被使用;cout << "pipefd[0]:" << pipefd[0] << endl;cout << "pipefd[1]:" << pipefd[1] << endl;return 0; }
此时的运行结果为:
将代码改为
#include <iostream> #include <assert.h> #include <unistd.h>using namespace std;int main() {//1、创建管道int pipefd[2] = { 0 };int n = pipe(pipefd);//完成管道的创建assert(n != -1);//判断pipe调用是否失败,如果等于-1则失败;(void)n;//assert在debug下式有效的,但是在release断言就没了,如果没有这一行,那么在release下就会出现//n被定义了但是没有被使用; #ifdef DEBUGcout << "pipefd[0]:" << pipefd[0] << endl;cout << "pipefd[1]:" << pipefd[1] << endl; #endifreturn 0; }
条件编译并且将makefile中改为 g++ -o $@ $^ -std=c++11 -DDEBUG ,此时编译后打印结果与上面相同,如果不想进入调试的话,就将Makefile改为g++ -o $@ $^ -std=c++11 #-DDEBUG,将 -DDEBUG 注释即可;
#include <iostream> #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() {//1、创建管道int pipefd[2] = { 0 };//pipefd[0(嘴巴)]:读端; pipefd[1(钢笔)]:写端;int n = pipe(pipefd);//完成管道的创建assert(n != -1);//判断pipe调用是否失败,如果等于-1则失败;(void)n;//assert在debug下式有效的,但是在release断言就没了,如果没有这一行,那么在release下就会出现//n被定义了但是没有被使用; #ifdef DEBUGcout << "pipefd[0]:" << pipefd[0] << endl;cout << "pipefd[1]:" << pipefd[1] << endl; #endif//2、创建子进程pid_t id = fork();assert(id != -1);if (id == 0){//子进程//3、构建单向通信的信道,父进程写入,子进程读取;//关闭子进程不需要的fdclose(pipefd[1]);//要想让子进程读取需要有缓冲区char buffer[1024];while (true)//一直读取,从pipefd[0]读出,读到buffer中,读buffer-1个;{ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);if (s > 0)//此时读取成功{buffer[s] = 0;//相当于给字符串添加'/0';cout << "child get message[" << getpid() << "]Father#" << buffer << endl;}}exit(0);}//父进程close(pipefd[0]);string message = "i am father ,loading!!";int count = 0;//计数器,看给子进程发了多少条;char send_buffer[1024];while (true){//构建变化的字符串snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d", message.c_str(), getpid(),count++);//写入write(pipefd[1], send_buffer, strlen(send_buffer));sleep(1);}pid_t ret = waitpid(id, nullptr, 0);assert(ret < 0);return 0; }
运行结果为
那为什么不定义一个全局的buffer来进行通信呢?
因为存在写时拷贝的存在,所以无法通过此方法通信;
管道特点总结
1、管道是用来进行具有血缘关系的进程进行进程间通信,常用与父子进程通信;
2、管道具有通过让进程间协同,提供了访问控制;上面的代码父进程是1s写一条,但是子进程在读取的时候没有任何时间限制,当时在程序运行时,是按照父进程写入的时间进行读取的;管道是一个文件,显示器也是一个文件,父子进程同时往显示器写入的时候,默认没有任何等待的打印,缺乏访问控制;但是对管道文件进行读取的时候,具有访问控制;
如果父进程一直,写入,子进程读取较慢,就会出现,父进程写满之后就不会在写入了,读端读取后,才会继续写入,而且读端会一次读取一批数据;
3、管道提供的是面向字节流式的通信服务;
4、管道是基于文件的;如果通信双方将文件描述符关闭,文件的生命周期是随进程的,管道的生命周期是随进程的;
5、管道是单向的,就是半双工的通信的一种特殊情况,正常的一个读一个写叫做半双工,两个同时又读又写,就是全双工的;
四种情况:
a、写得快,读的慢,写满后就不能继续写了,需要等读端读出一批后再写入;
b、写的慢,读的快,管道没有数据的时候,读端必须等待;
c、将写端关闭,读0,标识读到了文件结尾;
d、读关,写继续写,OS将会终止写的进程;
前两种是由管道的访问控制决定的;
扩展(管道的作用)
父进程创建若干子进程,父进程和每一个子进程建立对应的管道,每一个子进程内部预先防止很多处理任务的方法,让每一个子进程通过读端读取,父进程将四个写端进行管理,父进程有任务时,就可以指派给某一个子进程来完成该任务,于是就实现了进程池的概念;
代码如下
Task.hpp
#pragma once#include <iostream> #include <string> #include <vector> #include <unordered_map> #include <unistd.h> #include <functional>typedef std::function<void()> func; //等效 using func = std::function<void()>;std::vector<func> callbacks; std::unordered_map<int, std::string> desc;void readMySQL()//四个任务 {std::cout << "sub process[" << getpid() << "]执行访问数据库的任务" << std::endl; }void execuleUrl() {std::cout << "sub process[" << getpid() << "]执行Url解析" << std::endl; }void cal() {std::cout << "sub process[" << getpid() << "]执行加密任务" << std::endl; }void save() {std::cout << "sub process[" << getpid() << "]执行数据持久化任务" << std::endl; }void load()//接口 {desc.insert({ callbacks.size(),"readMySQL:读取数据库" });callbacks.push_back(readMySQL);desc.insert({ callbacks.size(),"execuleUrl:进行Url解析" });callbacks.push_back(execuleUrl);desc.insert({ callbacks.size(),"cal:进行加密计算" });callbacks.push_back(cal);desc.insert({ callbacks.size(),"save:进行数据的文件保存" });callbacks.push_back(save); }void showHandler() {for (const auto &iter : desc){std::cout << iter.first << "\t" << iter.second << std::endl;} }int handlerSize() {return callbacks.size(); }
processPool.cc
#include <iostream> #include <vector> #include <cassert> #include <cstdlib> #include <ctime> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include "Task.hpp"#define PROCESS_NUM 5 //进程池的进程个数using namespace std;int waitCommand(int waitFd, bool &quit) {uint32_t command = 0;ssize_t s = read(waitFd, &command, sizeof(command));if (s == 0)//读到0,就证明文件描述符关了{quit = true;return -1;}assert(s == sizeof(uint32_t));return command; }void sendAndWakeUp(pid_t who, int fd, uint32_t command) {write(fd, &command, sizeof(command));cout << "main process call process" << who << " execute" << desc[command] << "through" << fd << endl; }int main() {load();vector<pair<pid_t, int>>slots;//先创建多个进程for (int i = 0; i < PROCESS_NUM; i++){//创建管道int pipefd[2] = { 0 };int n = pipe(pipefd);assert(n == 0);(void)n;pid_t id = fork();assert(id != -1);//子进程进行读取if (id == 0){//关闭读端close(pipefd[1]);//子进程while (true){//等命令bool quit = false;int command = waitCommand(pipefd[0],quit);//如果对方不发,就阻塞;if (quit) break;//执行对应的命令if (command >= 0 && command < handlerSize()){callbacks[command]();}else{cout << "非法command命令" << command << endl;}}exit(1);}//father,进行写入,关闭读端close(pipefd[0]);slots.push_back(pair<pid_t, int>(id,pipefd[1]));}//父进程派发任务//单机版的负载均衡,随机的,rr(轮询的);不能只让一个子进程去工作srand((unsigned long)time(nullptr) ^ getpid() ^ 123543556);//让数据源更随机while (true){int select;int command;cout << "#############################" << endl;cout << "## 1. show functions ####" << endl;cout << "## 2. send command ####" << endl;cout << "#############################" << endl;cout << "Please Select";cin >> select;if (select == 1) showHandler();else if (select == 2){cout << "Enter Your Command> ";cin >> command;//选择进程int choice = rand() % slots.size();//布置任务,把任务给指定的进程sendAndWakeUp(slots[choice].first,slots[choice].second,command);}else{}}//关闭fd,结束所有进程for (const auto slot : slots){close(slot.second);}//回收所有的子进程信息for (const auto &slot : slots){waitpid(slot.first, nullptr, 0);}return 0; }
命名管道
管道应用的一个限制就是只能在具有共同祖先的的进程通信;
如果想要在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道;
命名管道是一种特殊类型的文件;
进程间通信首要是先让不同的进程看到同一份资源;
A进程打开了文件,在内核中会出现一个描述该被打开文件的结构体,如果此时B进程也要打开此文件,只要B进程将文件的地址填入,直接指向struct_file即可,但是一般文件,如果数据写进去会刷盘,将数据就写到了磁盘中,要是进行刷盘的话就要执行IO,效率会很低;操作系统设计了新的方案,会在磁盘上创建管道文件,可以被多个进程打开,但是不会将内存数据进行刷盘操作,这种管道文件没有内容,因为是在磁盘上被创建的,所以就会有路径,也就有了名字,该文件一定会在系统路径中,路径具有唯一性,双方进程就可以通过管道,看到同一份资源;
命名管道与匿名管道的本质是一样的,一种是通过继承的方式,一种是通过唯一的路径的方式;
运行指令 mkfifo name_pipe ,可以看到出现了文件name_pipe,并且第一个字母是p,表示管道文件;
代码
函数mkfifo;
Log.hpp
#ifndef _LOG_H_ #define _LOG_H_#include <iostream> #include <ctime>#define Notice 1 #define Warning 2 #define Error 3 #define Debug 4const 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#ifdef _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 128string ipcPath = "./fifo.ipc";#endif
serve.cc
#include "comm.hpp"int main() {//1\创建管道文件if (mkfifo(ipcPath.c_str(), MODE) < 0){perror("mkfifo");exit(1);}Log("创建管道文件成功", Debug) << "step 1" << endl;//2、正常的文件操作int fd = open(ipcPath.c_str(), O_RDONLY);if (fd < 0){perror("open");exit(2);}Log("打开管道文件成功", Debug) << "step 2" << endl;//编写正常的通信代码char buffer[SIZE];//定义好缓冲区;while (true){memset(buffer, '\0', sizeof(buffer));//清空缓冲区ssize_t s = read(fd, buffer, sizeof(buffer) - 1);//读取,预留空间添加\0if (s > 0){cout << "client say" << buffer << endl;}else if (s == 0){//end of filecerr << "read end of file, client quit,server quit too!" << endl;break;}else{//读取失败perror("read");break;}}close(fd);Log("关闭管道文件成功", Debug) << "step 3" << endl;unlink(ipcPath.c_str());Log("删除管道文件成功", Debug) << "step 4" << endl;return 0; }
client.cc
#include "comm.hpp"int main() {//1.获取管道文件int fd = open(ipcPath.c_str, O_WRONLY);if (fd < 0){perror("open");exit(1);}//2、通信string buffer;while (true){cout << "please Enter Message Line :>";std::getline(std::cin, buffer);write(fd, buffer.c_str(), buffer.size());}//3.关闭close(fd);unlink(ipcPath.c_str());//通信完毕删除文件return 0;}
运行结果,左边为client,右边为server
修改代码 server.cc
#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);//读取,预留空间添加\0if (s > 0){cout << "[" << getpid() << "]" << "client say" << buffer << endl;}else if (s == 0){//end of filecerr << "[" << getpid() << "]" << "read end of file, client quit,server quit too!" << endl;break;}else{//读取失败perror("read");break;}} }int main() {//1\创建管道文件if (mkfifo(ipcPath.c_str(), MODE) < 0){perror("mkfifo");exit(1);}Log("创建管道文件成功", Debug) << "step 1" << 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){getMessage(fd);}}for (int i = 0; i < nums; i++){waitpid(-1, nullptr, 0);}close(fd);Log("关闭管道文件成功", Debug) << "step 3" << endl;unlink(ipcPath.c_str());Log("删除管道文件成功", Debug) << "step 4" << endl;return 0; }
就可以实现一个管道多进程进行读取;
System V共享内存
原理
现在内存中申请空间,然后建立映射,将申请的空间与调用映射接口的进程建立映射;
释放就是将映射去掉,然后将内存释放;
一般共享内存、内存映射和共享库位于共享区中;
共享内存的建立
共享内存不属于任何一个进程,属于操作系统,是操作系统单独设立的内核模块;
如果有很多进程在通信,那么内存终究会存在很多共享内存,所以操作系统需要对共享内存需要管理,于是就需要先描述在组织,所以重新理解共享内存:共享内存块+对应的共享内存的内核数据结构;
创建共享内存的代码
shmflg: IPC_CREAT 单独使用时,如果创建共享内存,如果底层已经存在,获取它并且返回,如果不存在就创建并返回;
IPC_EXCL,单独使用时,是没有意义的;
IPC_CREAT 和 IPC_EXCL 一起使用,如果底层不存在,创建并返回,如果底层存在,出错返回;返回成功一定是一个全新的共享内存;
返回值是共享内存的用户层标识符,类似曾经的fd;
key:操作系统要先描述在组织,创建好共享内存和对应的数据结构;当一个进程通过操作系统创建好了共享内存,要通信的对方进程,需要保证对方能看到,并且看到的就是我创建的共享内存,也就是如何让要通信的进程能够找到自己创建的内存空间;通过key,数据是几,不重要,只要能够在系统唯一即可;server和client只要他们使用同一个key,就可以看到同一个共享内存了,只要key值相同就可以看到同一个共享内存;
进程client和server,server进程和client通过算法形成一个唯一值,然后server调用接口在底层创建了一个共享内存,创建好后server将标识符写到共享内存里(管理共享内存生成的数据结构中),然后client,也形成的是1234,就可以匹配起来,看到同一个共享内存了;下层是操作系统;
如何形成唯一的key值;
调用函数,ftok;
#include <comm.hpp>int main() {key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值,失败后返回-1;assert(k != -1);Log("create key done", Debug) << "server key:" << k << std::endl;//创建共享内存 建议要创建一个全新的共享内存 通信的发起者;int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL);//成功返回共享内存的标识符,失败返回-1;if (shmid == -1){perror("shmget");exit(1);}return 0; }
此时运行完程序后,创建的内存还会存在;
查看ipc的命令, ipcs -m 查看共享内存;
perms:权限,权限为0表示没有任何的进程有权利读取;
attach是映射的意思;
nattch:有多少个进程与此共享内存关联;
删除共享内存,ipcrm -m 22,此处使用的是shmid;
ipc资源生命周期随内核;所以需要
1、手动删除,就是用使用上边的命令删除;
2、代码删除,将server的代码改为;
#include <comm.hpp>int main() {key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值,失败后返回-1;assert(k != -1);Log("create key done", Debug) << "server key:" << k << std::endl;//创建共享内存 建议要创建一个全新的共享内存 通信的发起者;int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL);//成功返回共享内存的标识符,失败返回-1;if (shmid == -1){perror("shmget");exit(1);}Log("create shm done", Debug) << "shmid :" << shmid << std::endl;//删除共享内存int n = shmctl(shmid, IPC_RMID, nullptr);assert(n != -1);(void)n;Log("Delete shm done", Debug) << "shmid :" << shmid << std::endl;return 0; }
挂接进程与共享内存;
shmat
成功后返回挂接成功的address,失败返回-1;
将指定的共享内存从自己的地址空间中去关联;
shmdt
共享区域是在内核空间还是用户空间?是属于对应的用户空间;
在用户空间就是不用经过系统调用,直接访问;
双方进程如果要通信,直接进行内存级的读和写即可;
Log.hpp
#ifndef _LOG_H_ #define _LOG_H_#include <iostream> #include <ctime>#define Notice 1 #define Warning 2 #define Error 3 #define Debug 4const 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 <cassert> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include "Log.hpp"#define PATH_NAME "/home/zyl" #define PROJ_ID 0x66 #define SHM_SIZE 4096 //共享内存的大小最好是页(PAGE:4096)的整数倍;
shmServer.cc
#include <comm.hpp>string TransToHex(key_t k)//转换为16进制 {char buffer[32];snprintf(buffer, sizeof buffer, "0x%x", k);return buffer; }int main() {key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值,失败后返回-1;assert(k != -1);Log("create key done", Debug) << "server key:" << TransToHex(k) << std::endl;//创建共享内存 建议要创建一个全新的共享内存 通信的发起者;int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);//成功返回共享内存的标识符,失败返回-1;//0666表示运行权限;此时运行后共享内存的perms就会变为666;if (shmid == -1){perror("shmget");exit(1);}Log("create shm done", Debug) << "shmid :" << shmid << std::endl;//将指定的共享内存挂接到自己的共享内存空间;char shmaddr = (char*)shmat(shmid, nullptr, 0);Log("attach shm done", Debug) << "shmid :" << shmid << std::endl;//此处是通信的逻辑//将共享内存当做是一个大字符串for (;;){printf("%s\n", shmaddr);sleep(1);}//将指定的共享内存,从自己的地址空间去关联int n = shmdt(shmaddr);assert(n != -1);(void)n;Log("detach shm done", Debug) << "shmid :" << shmid << std::endl;//删除共享内存,就算有内存和当前的内存挂接依旧会强制删除共享内存;int n = shmctl(shmid, IPC_RMID, nullptr);assert(n != -1);(void)n;Log("Delete shm done", Debug) << "shmid :" << shmid << std::endl;return 0; }
shmClient.cc
#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 << std::endl;exit(1);}Log("create key done", Debug) << "client key:" << k << std::endl;//获取共享内存int shmid = shmget(k, SHM_SIZE, 0);if (shmid < 0){Log("create shm failed", Error) << "client key:" << k << std::endl;exit(2);}char *shmaddr = (char*)shmat(shmid, nullptr, 0);if (shmaddr == nullptr){Log("attach shm failed", Error) << "client key:" << k << std::endl;exit(3);}Log("attach shm success", Error) << "client key:" << k << std::endl;//使用//将共享内存看做一个对应的char 类型的bufferchar a = 'a';for (; a <= 'z'; a++){//向shmaddr进行写入;snprintf(shmaddr, SHM_SIZE - 1, "hello server,我是其他进程,pid:%d,%c\n",getpid(), a);sleep(2);}//去关联int n = shmdt(shmaddr);Log("detach shm success", Error) << "client key:" << k << std::endl;//不需要删除;return 0; }
此时代码存在一些问题
在退出时,不会删除共享内存
server中加入
//此处是通信的逻辑//将共享内存当做是一个大字符串for (;;){printf("%s\n", shmaddr);if (strcmp(shmaddr, "quit") == 0) break;sleep(1);}
client中加入
//使用//将共享内存看做一个对应的char 类型的bufferchar a = 'a';for (; a <= 'z'; a++){//向shmaddr进行写入;snprintf(shmaddr, SHM_SIZE - 1, "hello server,我是其他进程,pid:%d,%c\n",getpid(), a);sleep(2);}strcpy(shmaddr, "quit");
结论1、只要是通信双方使用共享内存,一方直接向共享内存中写入数据,另一方会立马看到;所以共享内存是所有进程间通信速度最快的;因为不需要过多的拷贝,不需要将数据交给操作系统;例如管道通信,首先操作系统创建一个管道文件,首先发送方获取数据,通过调用接口将数据写到管道文件中,调用接口的本质就是,将数据先拷贝到操作系统中,然后将数据从管道文件中拷贝到另一个文件的缓冲区中;
键盘到进程是一次拷贝,从进程到管道文件也是一次拷贝,从管道进程到另一个文件也是一次拷贝,进程将内容打印出来也是一次拷贝;共四次拷贝;
共享内存在同种情况下相当于只有两次拷贝;
可以修改代码为从键盘获取字符
client修改为
//使用//将共享内存看做一个对应的char 类型的bufferwhile (true){ssize_t s = read(0, shmaddr, SHM_SIZE - 1);if (s > 0){shmaddr[s - 1] = 0;//此处需要-1,是因为在输入的时候会按回车键就会输入一个\n,//s-1就相当于,将 \n 修改为0;if (strcmp(shmaddr, "quit") == 0) break;}}
结论2、共享内存没有访问控制;会带来并发问题;如果想要一定程度的访问控制怎么办?
用管道实现共享内存同步的过程;
Log.hpp不变;
comm.hpp
#pragma once#include <iostream> #include <cstdio> #include <cassert> #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 "Log.hpp"#define PATH_NAME "/home/zyl" #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 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); }
server变化
#include <comm.hpp>//对应的程序在加载的时候会自动构建全局变量,就要调用该类的构造函数----创建管道文件 //程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件; Init init;string TransToHex(key_t k)//转换为16进制 {char buffer[32];snprintf(buffer, sizeof buffer, "0x%x", k);return buffer; }int main() {key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值,失败后返回-1;assert(k != -1);Log("create key done", Debug) << "server key:" << TransToHex(k) << std::endl;//创建共享内存 建议要创建一个全新的共享内存 通信的发起者;int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);//成功返回共享内存的标识符,失败返回-1;//0666表示运行权限;此时运行后共享内存的perms就会变为666;if (shmid == -1){perror("shmget");exit(1);}Log("create shm done", Debug) << "shmid :" << shmid << std::endl;//将指定的共享内存挂接到自己的共享内存空间;char shmaddr = (char*)shmat(shmid, nullptr, 0);Log("attach shm done", Debug) << "shmid :" << shmid << std::endl;//此处是通信的逻辑//将共享内存当做是一个大字符串int fd = OpenFIFO(FIFO_NAME, READ);for (;;){Wait(fd);//临界区printf("%s\n", shmaddr);if (strcmp(shmaddr, "quit") == 0) break;sleep(1);}//将指定的共享内存,从自己的地址空间去关联int n = shmdt(shmaddr);assert(n != -1);(void)n;Log("detach shm done", Debug) << "shmid :" << shmid << std::endl;//删除共享内存,就算有内存和当前的内存挂接依旧会强制删除共享内存;int n = shmctl(shmid, IPC_RMID, nullptr);assert(n != -1);(void)n;Log("Delete shm done", Debug) << "shmid :" << shmid << std::endl;CloseFifo(fd);return 0; }
client.cc改变
#include <comm.hpp>int main() {Log("child pid is", Debug) << getpid() << std::endl;key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值if (k < 0){Log("create key failed", Error) << "client key:" << k << std::endl;exit(1);}Log("create key done", Debug) << "client key:" << k << std::endl;//获取共享内存int shmid = shmget(k, SHM_SIZE, 0);if (shmid < 0){Log("create shm failed", Error) << "client key:" << k << std::endl;exit(2);}char *shmaddr = (char*)shmat(shmid, nullptr, 0);if (shmaddr == nullptr){Log("attach shm failed", Error) << "client key:" << k << std::endl;exit(3);}Log("attach shm success", Error) << "client key:" << k << std::endl;//使用//将共享内存看做一个对应的char 类型的bufferint fd = OpenFIFO(FIFO_NAME, WRITE);while (true){ssize_t s = read(0, shmaddr, SHM_SIZE - 1);if (s > 0){shmaddr[s - 1] = 0;//此处需要-1,是因为在输入的时候会按回车键就会输入一个\n,//s-1就相当于,将 \n 修改为0;Signal(fd);if (strcmp(shmaddr, "quit") == 0) break;}}CloseFifo(fd);//去关联int n = shmdt(shmaddr);Log("detach shm success", Error) << "client key:" << k << std::endl;//不需要删除;return 0; }
信号量
基于共享内存的理解
为了让进程间通信,首先要做的是让不同的进程先看到同一份资源,所以管道,共享内存的本质都是优先解决一个问题,让不同的进程看到同一份资源;
让不同的进程看到同一份资源带来了一些时序的问题,造成了数据不一致;
把多个进程(执行流)看到的一份资源成为临界资源;
将自己的进程访问临界资源的代码称为临界区;在非临界区多个执行流是互相不影响的;
为了更好的进行临界区的保护,可以让多执行流在任何时刻都只能有一个进程进入临界区,这就叫做互斥;
原子性,要么不做,要么做完,没有中间状态;
每一个进程想要进入临界资源,访问临界资源中的额一部分,不能让进程直接去使用临界资源,需要先申请信号量;信号量本质是一个计数器;
先申请信号量:
首先申请信号量的本质是让信号量计数器 -- ;
只要申请信号量成功,临界资源内部一定预留了你想要的资源,申请信号量本质其实是对临界资源的一种预定机制;
工作流程:申请信号量,访问临界资源(进程执行自己的临界区代码),释放信号量 ++ ;
可以用一个整数标识信号量吗?
多进程不能对全局变量同时做修改;父子进程会有写时拷贝,不相关的进程也不能看到同一个全局变量;
假设让多个进程能看到同一个全局变量(整数n在共享内存中),大家都进行申请信号量n -- ,也不可以,假设对同一个变量进行n--操作,也会出现问题;
常规情况下,client将内存中的5加载到cpu中,cpu运算--后将值再返回到内存中;
但是执行流在执行的时候,在任何时候都有可能被切换;
如果一个n--操作只有一行汇编,那么该操作是原子的;
信号量是对临界资源的预订机制