【Linux】八、进程通信

进程通信的介绍

目的

数据传输:一个进程将它的数据发送给另一个进程;

资源共享:多个进程间共享资源;

通知事件:一个进程向另一个或一组进程发送消息,同时事件如,进程终止时要通知父进程;

进程控制:有的进程需要完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并及时知道它的状态;

进程间通信的必要性

在单进程下不通信是可以的,但是多进程的并发能力就无法使用,更加无法实现多进程协同,有的是为了传输数据,有的为了同步执行流,有的为了消息通知; 

进程间通信的技术背景

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--操作只有一行汇编,那么该操作是原子的;

信号量是对临界资源的预订机制 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/190124.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Django框架FAQ

文章目录 问题1:Django数据库恢复问题2:null和blank的区别3.报错 django.db.utils.IntegrityError: (1062, “Duplicate entry ‘‘ for key ‘mobile‘“)4.报错 Refused to display ‘url‘ in a frame because it set ‘X-Frame-Options‘ to deny5.报错 RuntimeError: cryp…

开放领域问答机器人1

开放领域问答机器人是一种智能机器人&#xff0c;它不受限制&#xff0c;可以回答任何问题。这种机器人主要通过自然语言处理技术来理解用户的问题&#xff0c;并从大量的数据中获取相关信息&#xff0c;以提供准确的答案。它的应用领域广泛&#xff0c;包括客户服务、教育、医…

如何使用软路由R4S+iStoreOS实现远程访问内网电脑桌面

软路由R4SiStoreOS实现公网远程桌面局域网内电脑 文章目录 软路由R4SiStoreOS实现公网远程桌面局域网内电脑简介一、配置远程桌面公网地址二、家中使用永久固定地址 访问公司电脑**具体操作方法是&#xff1a;** 简介 上篇教程我们介绍了如何在iStoreOS中安装Cpolar&#xff0…

《QT从基础到进阶·二十》QThreadPool线程池的使用

什么情况下比较适合用线程池&#xff1f; 比如我有上百个任务要同时处理&#xff0c;难道开上百个线程&#xff1f;NO&#xff01;&#xff01;&#xff01; 有了线程池的加持&#xff0c;自动给任务分配线程处理&#xff0c; 多线程不再是真爱~ 线程池创建&#xff1a; 1、自…

面试被问答3-5年职业规划,该怎么回答

面试官问这些问题的目的是什么&#xff1f;他想得到什么满意的答案。只要清楚这些&#xff0c;就不难回答这个问题。 1、你有没有上进心&#xff1f;公司是否值得培养呢&#xff1f; 你需要对专业能力充满向往&#xff0c;希望自己在3~5年内&#xff0c;把专业能力做好&#…

Ansible命令使用

ansible ansible的命令 ansible命令模块Pingcommand 模块shell 模块copy 模块file 模块fetch 模块cron 模块yum 模块service 模块user 模块group 模块script 模块setup 模块get_url模块stat模块unarchive模块unarchive模块 ansible的命令 /usr/bin/ansible  Ansibe AD-Hoc 临…

ultrascale+mpsoc系列的ZYNQ中DDR4参数设置说明

ultrascalempsoc系列的ZYNQ中DDR4参数设置说明 标题1 概述标题2 讲述平台标题3 ZYNQ的DDR设置界面参数标题4 DDR参数界面说明如下 标题1 概述 本文用于讲诉ultrascalempsoc系列中的ZYNQ的DDR4的参数设置与实际硬件中的DDR选型之间的关系&#xff0c;为FPGA设计人员探明道路。 …

移植LVGL到单片机的一个demo简单介绍

简介 背景&#xff1a; 本文使用的是主控IC为stm32f103zet6, 显示IC为ST7735s&#xff0c;它是128*160的像素&#xff0c;色深为RGB565颜色。 官方虽然说LVGL移植平台只需 64kB 闪存和 8kB RAM 就足以满足简单的用户界面。但我移植到stm32f103c8t6&#xff0c;不管怎么修改配…

Postman:如何对WebService接口进行测试

前言 由于工作所需,需要使用Postman测试工具&#xff0c;对基于ws规范的WebService接口进行测试。在经过多种尝试后&#xff0c;终于找到了正确的测试方法&#xff0c;下面我便详细记录测试步骤&#xff0c;以便以后再次测试时可以拿来主义。 第一步&#xff1a;确保WebServi…

使用matlab实现图像信号的色彩空间转换

利用matlab对图像信号进行读取&#xff0c;并对RGB空间进行转换&#xff0c;如转换到HSI空间等。 下面的这个代码是在使用了rgb2hsi()方法失败后&#xff0c;进行修改的。 rgb2hsi(img)这个方法可以将RGB图像转换为HIS图像&#xff1b;但是爆出了 Untitled5(line 5)hsi rgb2h…

SpringBoot 自动配置原理

SpringBoot 自动配置原理 注: 本文使用的springboot版本为 2.7.11 1、SpringBootApplication 字面分析&#xff0c;这个注解是标注一个Spring Boot应用。 Target({ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Documented Inherited SpringBootConfiguration En…

哈希竞猜游戏开发源码部署方案

随着互联网技术的发展&#xff0c;越来越多的人开始关注网络安全问题&#xff0c;而哈希算法作为一种重要的加密技术&#xff0c;在网络安全领域得到了广泛应用。其中&#xff0c;哈希竞猜游戏作为一种新型的网络安全挑战赛&#xff0c;也受到了越来越多人的关注。本文将介绍哈…

应用层——HTTP协议

文章目录 HTTP协议1.HTTP简介2.认识URL3.urlencode和urldecode4.HTTP协议格式&#xff08;1&#xff09;HTTP请求协议格式&#xff08;2&#xff09;HTTP响应协议格式 5.HTTP的方法6.HTTP的状态码7.HTTP常见的Header8.Cookie和Session HTTP协议 1.HTTP简介 HTTP&#xff08;Hy…

centos7 安装网络文件共享NFS详细过程

网络文件系统&#xff0c;英文Network File System(NFS)&#xff0c;是由SUN公司研制的UNIX表示层协议(presentation layer protocol)&#xff0c;能使使用者访问网络上别处的文件就像在使用自己的计算机一样。 多个服务器之间需要共享文件&#xff0c;通过NFS服务共享是一个简…

说说你对React Router的理解?常用的Router组件有哪些?

一、是什么 react-router等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面 路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新 因此,可以通过前端路由可以实现单页(SPA)应用 react-router主要分成了几个不…

如何向MapInfo Pro添加自定义符号?

用户可以在MapInfo Pro中创建和使用自己的自定义图像作为符号。要访问这些自定义符号&#xff0c;请将它们放在CUSTSYMB目录中&#xff0c;然后从“符号样式”对话框&#xff08;Style>符号样式&#xff09;的“字体”列表中的“自定义符号”选项中选择它们。MapInfo Pro中的…

微信小程序_02

能够使用WXML模版语法渲染页面结构 数据绑定 1、数据绑定的基本原则 在data中定义数据在WXML中使用数据 2、在data中定义页面的数据 ​ 在页面对应的.js文件中&#xff0c;把数据定义到data对象中即可&#xff1a; Page({data:{//字符串类型的数据info:init data,//数组类…

信驰达科技加入车联网联盟(CCC),推进数字钥匙发展与应用

CCC)的会员。 图 1 深圳信驰达正式成为车联网联盟(CCC)会员 车联网联盟(CCC)是一个跨行业组织&#xff0c;致力于推动智能手机与汽车连接解决方案的技术发展。CCC涵盖了全球汽车和智能手机行业的大部分企业&#xff0c;拥有150多家成员公司。CCC成员公司包括智能手机和汽车制造…

navicat创建MySql定时任务

navicat创建MySql定时任务 前提 需要root用户权限 需要开启定时任务 1、开启定时任务 1.1 查看定时任务是否开启 mysql> show variables like event_scheduler;1.2 临时开启定时任务(下次重启后失效) set global event_scheduler on;1.3 设置永久开启定时任务 查看my…