当前位置: 首页 > news >正文

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

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

 

相关文章:

【Linux】八、进程通信

进程通信的介绍 目的 数据传输&#xff1a;一个进程将它的数据发送给另一个进程&#xff1b; 资源共享&#xff1a;多个进程间共享资源&#xff1b; 通知事件&#xff1a;一个进程向另一个或一组进程发送消息&#xff0c;同时事件如&#xff0c;进程终止时要通知父进程&#xf…...

不同类型的软件企业该如何有效的管理好你的软件测试团队?

最近在网上发现一篇记录了2012年《[视频]作为测试经理如何有效管理好你的软件测试团队》的文字内容&#xff0c;感谢记录的人&#xff0c;我也保存一下。顺便将演讲中的PPT重点截图也放上来&#xff0c;一并保存了&#xff01;。由于是现场速记&#xff0c;过度的口语化&#x…...

vue echart 立体柱状图 带阴影

根据一个博主代码改编而来 <template><div class"indexBox"><div id"chart"></div></div> </template><script setup> import * as echarts from "echarts"; import { onMounted } from "vue&…...

vscode远程linux安装codelldb

在windows上使用vscode通过ssh远程连接linux进行c调试时&#xff0c;在线安装codelldb-x86_64-linux.vsix扩展插件失败&#xff0c;原因是linux服务器上的网络问题&#xff0c;所以需要进行手动安装。 首先在windows上下载&#xff1a; codelldb-x86_64-linux.vsix&#xff1b;…...

【中间件篇-Redis缓存数据库08】Redis设计、实现、redisobject对象设计、多线程、缓存淘汰算法

Redis的设计、实现 数据结构和内部编码 type命令实际返回的就是当前键的数据结构类型&#xff0c;它们分别是&#xff1a;string(字符串)hash(哈希)、list(列表)、set(集合)、zset (有序集合)&#xff0c;但这些只是Redis对外的数据结构。 实际上每种数据结构都有自己底层的…...

华为云优惠券介绍、领取入口及使用教程

华为云是华为的云服务品牌&#xff0c;致力于为用户提供一站式云计算基础设施服务。为了吸引用户&#xff0c;华为云经常推出各种优惠活动&#xff0c;其中就包括优惠券的发放&#xff0c;下面将为大家详细介绍华为云优惠券的作用、领取入口以及使用教程。 一、华为云优惠券介绍…...

OPTEE安全通告之CVE-2023-41325(Double free in shdr_verify_signature)

安全之安全(security)博客目录导读 目录 一、受影响版本 二、漏洞描述 三、问题触发 四、官方Patch修复...

第12章 关于 Micro SaaS 的结论

从时间和地点的自由到一种新鲜的独立感,开发 Micro SaaS 应用程序有很多好处。 获得 6 位数的订阅收入。辞掉我朝九晚五的令人丧命的工作。消除毫无意义的会议、办公室政治、混乱和救火。想工作就工作。随时随地使用我想要的任何技术工作。花更多时间陪伴家人。与我开发的应用…...

postman调用接口报{“detail“:“Method \“DELETE\“ not allowed.“}错误, 解决记录

项目是python代码开发, urls.py 路由中访问路径代码如下: urlpatterns [path(reportmanagement/<int:pk>/, views.ReportManagementDetail.as_view(), namereport-management-detail),] 对应view视图中代码如下: class ReportManagementDetail(GenericAPIView):"…...

基于单片机的线路差动保护系统设计

摘 要 随着我国微型电子技术和嵌入式系统的发展&#xff0c;目前行业内相对比较传统的线路差动保护系统无法满足客户的需求。为了改进传统线路差动保护系统在控制上得短板问题&#xff0c;在本次毕业设计中&#xff0c;将使用相对先进、快捷、智能的控制机制。该系统的控制大脑…...

vscode 快速打印console.log

第一步 输入这些 {// Print Selected Variabl 为自定义快捷键中需要使用的name&#xff0c;可以自行修改"Print Selected Variable": {"body": ["\nconsole.log("," %c $CLIPBOARD: ,"," background-color: #3756d4; padding:…...

drawio连接线的样式设置

drawio是一款强大的图表绘制软件&#xff0c;支持在线云端版本以及windows, macOS, linux安装版。 如果想在线直接使用&#xff0c;则直接输入网址draw.io或者使用drawon(桌案), drawon.cn内部完整的集成了drawio的所有功能&#xff0c;并实现了云端存储&#xff0c;以及在线共…...

【力扣题:循环队列】

文章目录 一.题目描述二. 思路解析三. 代码实现 一.题目描述 设计你的循环队列实现。 循环队列是一种线性数据结构&#xff0c;其操作表现基于 FIFO&#xff08;先进先出&#xff09;原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。 循环队列的一个好…...

配置开启Docker2375远程连接与解决Docker未授权访问漏洞

一、配置开启Docker远程连接 首先需要安装docker,参考我这篇文章&#xff1a;基于CentOS7安装配置docker与docker-compose 配置开启Docker远程连接的步骤&#xff1a; //1-编辑/usr/lib/systemd/system/docker.service 文件 vim /usr/lib/systemd/system/docker.service //2…...

土木非科班转码测开,斩获10家大厂offer

大家好&#xff0c;我是洋子 24届秋招基本已经落下了帷幕&#xff0c;各大互联网大厂基本也开奖完毕&#xff0c;还没有拿到满意offer的同学也不要灰心&#xff0c;积极备战明年的春招。另外&#xff0c;25届想要找暑期实习的同学也可以开始准备起来了&#xff0c;基本大厂在春…...

HarmonyOS 高级特性

引言 本章将探讨 HarmonyOS 的高级特性&#xff0c;包括分布式能力、安全机制和性能优化。这些特性可以帮助你构建更强大、更安全、更高效的应用。 目录 HarmonyOS 的分布式能力HarmonyOS 的安全机制HarmonyOS 的性能优化总结 1. HarmonyOS 的分布式能力 HarmonyOS 的分布…...

Spring整合redis的key时出现\xac\xed\x00\x05t\前缀问题

AutowiredRedisTemplate redisTemplate;User usernew User(5,"tomhs","tttt");ValueOperations opsForValue redisTemplate.opsForValue();//存放key,opsForValue.set("user"user.getId(),user);//读取数据;System.out.println(opsForValue.get…...

centos 6.10 安装 tcmalloc

安装 libunwind-1.6.2 下载地址 解压文件 cd libunwind-1.6.2 ./configure make && make install另一种方式 从 github 上下载的项目, 在执行autoreconf -i 时一直报错&#xff0c;libtool 未定义&#xff0c; 要先在当前目录执行 libtoolize&#xff0c;再执行 au…...

下载huggingface预训练模型到本地并调用

写在前面 在大模型横行的时代&#xff0c;无法在服务器上连接外网的研究僧真的是太苦逼了&#xff0c;每次想尝试类似于CLIP&#xff0c;BLIP之类的大模型都会得到“requests.exceptions.ConnectionError: (MaxRetryError("HTTPSConnectionPool(host‘huggingface.co’, …...

基于Vue+SpringBoot的天然气工程业务管理系统 开源项目

目录 一、摘要1.1 项目介绍1.2 项目详细录屏 二、功能模块三、使用角色3.1 施工人员3.2 管理员 四、数据库设计4.1 用户表4.2 分公司表4.3 角色表4.4 数据字典表4.5 工程项目表4.6 使用材料表4.7 使用材料领用表4.8 整体E-R图 五、系统展示六、核心代码6.1 查询工程项目6.2 工程…...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度​

一、引言&#xff1a;多云环境的技术复杂性本质​​ 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时&#xff0c;​​基础设施的技术债呈现指数级积累​​。网络连接、身份认证、成本管理这三大核心挑战相互嵌套&#xff1a;跨云网络构建数据…...

对WWDC 2025 Keynote 内容的预测

借助我们以往对苹果公司发展路径的深入研究经验&#xff0c;以及大语言模型的分析能力&#xff0c;我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际&#xff0c;我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测&#xff0c;聊作存档。等到明…...

AI编程--插件对比分析:CodeRider、GitHub Copilot及其他

AI编程插件对比分析&#xff1a;CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展&#xff0c;AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者&#xff0c;分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

USB Over IP专用硬件的5个特点

USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中&#xff0c;从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备&#xff08;如专用硬件设备&#xff09;&#xff0c;从而消除了直接物理连接的需要。USB over IP的…...

HashMap中的put方法执行流程(流程图)

1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中&#xff0c;其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下&#xff1a; 初始判断与哈希计算&#xff1a; 首先&#xff0c;putVal 方法会检查当前的 table&#xff08;也就…...

Mysql中select查询语句的执行过程

目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析&#xff08;Parser&#xff09; 2.4、执行sql 1. 预处理&#xff08;Preprocessor&#xff09; 2. 查询优化器&#xff08;Optimizer&#xff09; 3. 执行器…...

iview框架主题色的应用

1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题&#xff0c;无需引入&#xff0c;直接可…...

Python 实现 Web 静态服务器(HTTP 协议)

目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1&#xff09;下载安装包2&#xff09;配置环境变量3&#xff09;安装镜像4&#xff09;node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1&#xff09;使用 http-server2&#xff09;详解 …...

Unity UGUI Button事件流程

场景结构 测试代码 public class TestBtn : MonoBehaviour {void Start(){var btn GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}当添加事件时 // 实例化一个ButtonClickedEvent的事件 [Formerl…...

华为OD最新机试真题-数组组成的最小数字-OD统一考试(B卷)

题目描述 给定一个整型数组,请从该数组中选择3个元素 组成最小数字并输出 (如果数组长度小于3,则选择数组中所有元素来组成最小数字)。 输入描述 行用半角逗号分割的字符串记录的整型数组,0<数组长度<= 100,0<整数的取值范围<= 10000。 输出描述 由3个元素组成…...