进程通信的介绍
目的
数据传输:一个进程将它的数据发送给另一个进程;
资源共享:多个进程间共享资源;
通知事件:一个进程向另一个或一组进程发送消息,同时事件如,进程终止时要通知父进程;
进程控制:有的进程需要完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并及时知道它的状态;
进程间通信的必要性
在单进程下不通信是可以的,但是多进程的并发能力就无法使用,更加无法实现多进程协同,有的是为了传输数据,有的为了同步执行流,有的为了消息通知;
进程间通信的技术背景
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.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: 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 DEBUG cout << "pipefd[0]:" << pipefd[0] << endl; cout << "pipefd[1]:" << pipefd[1] << endl; #endif return 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 DEBUG cout << "pipefd[0]:" << pipefd[0] << endl; cout << "pipefd[1]:" << pipefd[1] << endl; #endif //2、创建子进程 pid_t id = fork(); assert(id != -1); if (id == 0) { //子进程 //3、构建单向通信的信道,父进程写入,子进程读取; //关闭子进程不需要的fd close(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 4 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
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 128 string 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);//读取,预留空间添加\0 if (s > 0) { cout << "client say" << buffer << endl; } else if (s == 0) { //end of file cerr << "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);//读取,预留空间添加\0 if (s > 0) { cout << "[" << getpid() << "]" << "client say" << buffer << endl; } else if (s == 0) { //end of file cerr << "[" << 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 4 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
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 类型的buffer char 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 类型的buffer char 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 类型的buffer while (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_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 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 类型的buffer int 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--操作只有一行汇编,那么该操作是原子的;
信号量是对临界资源的预订机制