🌈欢迎来到Linux专栏~~进程通信
- (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
- 目前状态:大三非科班啃C++中
- 🌍博客主页:张小姐的猫~江湖背景
- 快上车🚘,握好方向盘跟我有一起打天下嘞!
- 送给自己的一句鸡汤🤔:
- 🔥真正的大师永远怀着一颗学徒的心
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
- 🎉🎉欢迎持续关注!
文章目录
一. 进程间通信介绍
进程之间会存在特定的协同工作的场景:
- 数据传输:一个进程要把自己的数据交给另一个进程,让其继续进行处理
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信的本质就是,让不同的进程看到同一份资源
进程是具有独立性的。虚拟地址空间+页表 保证了进程运行的独立性(进程内核数据结构+进程代码和数据)
进程通信的前提,首先需要让不同的进程看到同一份“内存”(特定的结构组织)
- 这块内存应该属于谁呢?为了维持进程独立性,它一定不属于进程A或B,它属于操作系统。
综上,进程间通信的前提就是:由OS参与,提供一份所有通信进程都能看到的公共资源。
进程间通信的发展
- 管道 - 匿名管道pipe- 命名管道pipe
- System V标准 进程间通信 - System V 消息队列- System V 共享内存- System V 信号量
- POSIX标准 进程间通信(多线程详谈) - 消息队列- 共享内存- 信号量- 互斥量- 条件变量- 读写锁
二. 管道
什么是管道?
- 有入口,有出口,都是单向传输资源的(数据)
所以计算机领域设计者,设计了一种单向通信的方式 —— 管道
🌍匿名管道
众所周知,父子进程是两个独立进程,父子通信也是进程间通信的一种,基于父子间进程通信就是匿名管道。我们首先要对匿名管道有一个宏观的认识
父进程创建子进程,子进程需要以父进程为模板创建自己的
files_struct
,而不是与父进程共用;但是struct file这个结构体就不会拷贝,因为打开文件也与创建进程无关(文件的数据不用拷贝)
- 因为左边是进程相关数据结构,右边是文件相关结构
😎匿名管道原理
- 父进程创建管道,对同一文件分别以读&写方式打开
- 父进程
fork
创建子进程 - 因为管道是一个只能单向通信的信道,父子进程需要关闭对应读写端,至于谁关闭谁,取决于通信方向。
于是,通过子进程继承父进程资源的特性,双方进程看到了同一份资源。
😎创建匿名管道pipe
pipe
谁调用就让以读写方式打开一个文件(内存级文件)
#include<unistd.h>intpipe(int pipefd[2]);
- 参数
pipefd
:输出型参数!通过这个参数拿到两个打开的fd - 返回值:成功返回0;失败返回-1
数组
pipefd
用于返回两个指向管道读端和写端的文件描述符:
数组元素含义pipefd[0]~嘴巴管道读端的文件描述符pipefd[1] ~ 钢笔管道写端的文件描述符
此处提取查一下要用到的函数
man2
是获得系统(linux内核)调用的用法;man 3
是获得标准库(标准C语言库、glibc)函数的文档
//linux中用man可以查哦#include<unistd.h>
pid_t fork(void);#include<unistd.h>intclose(int fd);#include<stdlib.h>voidexit(int status);
下面按照之前讲的原理进行逐一操作:①创建管道 ②父进程创建子进程 ③关闭对应的读写端,形成单向信道
#include<iostream>#include<unistd.h>#include<cstdio>#include<cstring>#include<string.h>#include<assert.h>usingnamespace std;intmain(){//1.创建管道int pipefd[2]={0};int n =pipe(pipefd);//失败返回-1assert(n !=-1);//只在debug下有效(void)n;//仅此证明n被使用过#ifdefDEBUG
cout<<"pipefd[0]"<< pipefd[0]<< endl;//3
cout<<"pipefd[1]"<< pipefd[1]<< endl;//4#endif//2.创建子进程
pid_t id =fork();assert(id !=-1);if(id ==0){//子进程//3. 构建单向通信的信道//3.1 子进程关闭写端[1]close(pipefd[1]);exit(0);}//父进程//父进程关闭读端[0]close(pipefd[0]);return0;}
在此基础上,我们就要进行通信了,实际上就是对某个文件进行写入,因为管道也是文件,下面提提前查看要用到的函数
#include<unistd.h>
ssize_t write(int fd,constvoid*buf, size_t count);#include<unistd.h>
ssize_t read(int fd,void*buf, size_t count);
返回值:- 返回写入的字节数
- 零表示未写入任何内容,这里意味着对端进程关闭文件描述符
#include<unistd.h>unsignedintsleep(unsignedint seconds);
😎demo代码
简单实现了管道通信的demo版本:
#include<iostream>#include<unistd.h>#include<cstdio>#include<cstring>#include<string.h>#include<assert.h>#include<sys/types.h>#include<sys/wait.h>usingnamespace std;intmain(){//1.创建管道int pipefd[2]={0};int n =pipe(pipefd);//失败返回-1assert(n !=-1);//只在debug下有效(void)n;//仅此证明n被使用过#ifdefDEBUG
cout<<"pipefd[0]"<< pipefd[0]<< endl;//3
cout<<"pipefd[1]"<< pipefd[1]<< endl;//4#endif//2.创建子进程
pid_t id =fork();assert(id !=-1);if(id ==0){//子进程 - 读//3. 构建单向通信的信道//3.1 子进程关闭写端[1]close(pipefd[1]);char buffer[1024];while(1){
size_t s =read(pipefd[0], buffer,sizeof(buffer)-1);if(s >0){
buffer[s]=0;//因为read是系统调用,没有/0,此处给加上
cout<<"child get a message["<<getpid()<<"] 爸爸对你说"<< buffer << endl;}}//close(pipefd[0]);exit(0);}//父进程 - 写//父进程关闭读端[0]close(pipefd[0]);
string message ="我是父进程,我正在给你发消息";int count =0;//计算发送次数char send_buffer[1024];while(true){//3.2构建一个变化的字符串snprintf(send_buffer,sizeof(send_buffer),"%s[%d] : %d",message.c_str(),getpid(), count);
count++;//3.3写入write(pipefd[1], send_buffer,strlen(send_buffer));//此处strlen不能+1//3.4 故意sleepsleep(1);}
pid_t ret =waitpid(id,nullptr,0);assert(ret !=-1);(void)ret;return0;}
此处有个问题:为什么不定义一个全局的buffer来进行通信呢?
- 因为有写时拷贝的存在,无法更改通信!
上面的方法就是把数据交给管道,让对方通过管道进行读取
😎匿名管道通信的4种情况
之前父子进程同时向显示器中写入的时候,二者会互斥 —— 缺乏访问控制
而对于管道进行读取的时候,父进程如果写的慢,子进程就会等待读取 —— 这就是说明管道具有访问控制
✨读阻塞:写快,读慢
父进程疯狂的进行写入,子进程隔10秒才读取,子进程会把这10秒内父进程写入的所有数据都一次性的打印出来!
代码如非就是在父进程添加了打印conut,子进程sleep(10),可以自行的在demo代码上添加
父进程写了1220次,子进程一次就给你读完了,读写之间没有关系,这就叫做流式的服务。
也就是管道是面向字节流的,也就是只有字节的概念,究竟读成什么样也无法保证,甚至可能读出乱码,所以父子进程通信也是需要制定协议的,但这个我们网络再细说。。
✨写阻塞:写慢,读快
管道没有数据的时候,读端必须等待:父进程每隔2秒才进行写入,子进程疯狂的读取
✨写端关闭
父进程写入10秒,后把写端fd关闭,读端会怎么样?
- 写入的一方,fd没有关闭,如果有数据就读,没有数据就等
- 写入的一方,fd关闭了,读取的一方,
read
会返回0
,表示读到了文件结尾,退出读端
#include<iostream>#include<unistd.h>#include<cstdio>#include<cstring>#include<string.h>#include<assert.h>#include<sys/types.h>#include<sys/wait.h>usingnamespace std;intmain(){//1.创建管道int pipefd[2]={0};int n =pipe(pipefd);//失败返回-1assert(n !=-1);//只在debug下有效(void)n;//仅此证明n被使用过#ifdefDEBUG
cout<<"pipefd[0]"<< pipefd[0]<< endl;//3
cout<<"pipefd[1]"<< pipefd[1]<< endl;//4#endif//2.创建子进程
pid_t id =fork();assert(id !=-1);if(id ==0){//子进程 - 读//3. 构建单向通信的信道//3.1 子进程关闭写端[1]close(pipefd[1]);char buffer[1024*8];while(1){//sleep(10);//20秒读一次//写入的一方,fd没有关闭,如果有数据就读,没有数据就等//写入的一方,fd关闭了,读取的一方,read会返回0,表示读到了文件结尾
size_t s =read(pipefd[0], buffer,sizeof(buffer)-1);if(s >0){
buffer[s]=0;//因为read是系统调用,没有/0,此处给加上
cout<<"child get a message["<<getpid()<<"] 爸爸对你说"<< buffer << endl;}elseif(s ==0){
cout <<"write quit(father), me quit!!!"<<endl;break;}}//close(pipefd[0]);exit(0);}//父进程 - 写//父进程关闭读端[0]close(pipefd[0]);
string message ="我是父进程,我正在给你发消息";int count =0;//计算发送次数char send_buffer[1024*8];while(true){//3.2构建一个变化的字符串snprintf(send_buffer,sizeof(send_buffer),"%s[%d] : %d",message.c_str(),getpid(), count);
count++;//3.3写入write(pipefd[1], send_buffer,strlen(send_buffer));//此处strlen不能+1//3.4 故意sleepsleep(1);
cout<< count <<endl;if(count ==5){
cout<<"父进程写端退出"<< endl;break;}}close(pipefd[1]);//关闭读端
pid_t ret =waitpid(id,nullptr,0);assert(ret !=-1);(void)ret;return0;}
运行结果如下:
✨读端关闭
读端关闭,写端继续写入,直到OS终止写进程
#include<stdio.h>#include<unistd.h>#include<string.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>intmain(){int fd[2]={0};if(pipe(fd)<0){//使用pipe创建匿名管道perror("pipe");return1;}
pid_t id =fork();//使用fork创建子进程if(id ==0){//childclose(fd[0]);//子进程关闭读端//子进程向管道写入数据constchar* msg ="hello father, I am child...";int count =10;while(count--){write(fd[1], msg,strlen(msg));sleep(1);}close(fd[1]);//子进程写入完毕,关闭文件exit(0);}//fatherclose(fd[1]);//父进程关闭写端close(fd[0]);//父进程直接关闭读端(导致子进程被操作系统杀掉)int status =0;waitpid(id,&status,0);printf("child get signal:%d\n", status &0x7F);//打印子进程收到的信号return0;}
运行结果显示,子进程退出时收到的是13号信号
通过kill -l命令可以查看13对应的具体信号
由此可知,当发生情况四时,操作系统向子进程发送的是
SIGPIPE
信号将子进程终止的。
🐋总结上述的4中场景:
- 写快,读慢,写满了不能再写了
- 写慢,读快,管道没有数据的时候,读端必须等待
- 写关,读取的一方,
read
会返回0
,表示读到了文件结尾,退出读端 - 读关,写继续写,OS终止写进程 ——
🧐由上总结出匿名管道的5个特点 ——
- 管道是一个单向通信的通信管道,是半双工通信的一种特殊情况
- 管道是用来进行具有血缘关系的进程进行进程间通信 —— 常用于父子通信
- 管道具有通过让进程间协同,提供了访问控制!
- 管道是 面向字节流 —— 协议(后面详谈)
- 管道是基于文件的,管道的声明周期是随进程的
😎管道的大小
管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,那么管道的最大容量是多少呢?
ps:原子性:要么做了,要么不做,没有中间状态
方法1 :man手册查询
然后我们可以使用
uname -r
命令,查看自己使用的Linux版本
我使用的是Linux 2.6.11之后的版本,因此管道的最大容量是65536字节
方法二:自行测试
也就是如果读端一直不读取,写端又不断的写入,当管道被写满后,写端进程就会被挂起。据此,我们可以写出以下代码来测试管道的最大容量。
#include<unistd.h>#include<stdio.h>#include<stdlib.h>#include<sys/wait.h>intmain(){int fd[2]={0};if(pipe(fd)<0){//使用pipe创建匿名管道perror("pipe");return1;}
pid_t id =fork();//使用fork创建子进程if(id ==0){//child close(fd[0]);//子进程关闭读端char c ='a';int count =0;//子进程一直进行写入,一次写入一个字节while(1){write(fd[1],&c,1);
count++;printf("%d\n", count);//打印当前写入的字节数}close(fd[1]);exit(0);}//fatherclose(fd[1]);//父进程关闭写端//父进程不进行读取waitpid(id,NULL,0);close(fd[0]);return0;}
写端进程最多写65536字节的数据就被操作系统挂起了,也就是说,我当前Linux版本中管道的最大容量是65536字节
🌍命名管道
为了解决匿名管道只能在父子之间通信,我们引入命名管道,可以在任意不相关进程进行通信
多个进程打开同一个文件,**OS只会创建
一个struct_file
**
命名管道就是一种特殊类型的文件(可以被打开,但不会将数据刷新进磁盘),两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
命名管道就是通过唯一路径/文件名的方式定位唯一磁盘文件的
ps:命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像(所以有名字),但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
🎨创建命名管道
💛 make FIFOs 在命令行上创建命名管道
mkfifo(named pipes)
FIFO
:First In First Out 队列呀
来个小实验:
命令行上执行的命令
echo
和
cat
都是进程,所以这就是通过管道文件进行的进程间通信 ——
💛 那么如何用代码实现命名管道进程间通信的呢?
//查手册:man 3 mkfifo#include<sys/types.h>#include<sys/stat.h>intmkfifo(constchar*pathname, mode_t mode);
pathname
:管道文件路径mode
:管道文件权限- 返回值:创建成功返回0;创建失败返回-1,并设置错误码
我touch了server.c和client.c,最终希望在
server
和
client
两个进程之间相互通信,先写一个Makefile ——
.PHONY:all
all:client server
client:client.cxx
g++-o $@ $^-std=c++11
server:server.cxx
g++-o $@ $^-std=c++11.PHONY:clean
clean:
rm -f client server
- Makefile自顶向下扫描,只会把第一个目标文件作为最终的目标文件。所以要一次性生成两个可执行程序,需要定义伪目标
.PHONY: all
,并添加依赖关系
🎨基于命名管道通信
comm.h
我们创建一个共用的头文件,这只是为了两个程序能有看到同一个资源的能力了
#ifndef_COMM_H_ //能避免头文件的重定义#define_COMM_H_//hpp和.h的区别:.h里面只有声明,没有实现,而.hpp里声明实现都有,后者可以减少.cpp的数量#include<iostream>#include<string>#include<unistd.h>#include<cstdio>#include<cstring>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>usingnamespace std;#defineMODE0666#defineSIZE128
string ipcPath ="./fifo.ipc";#endif
server.c
- 创建命名管道
- 读信息,并实现相应业务逻辑
#include"comm.hpp"intmain(){//1.创建管道文件if(mkfifo(ipcPath.c_str(), MODE)<0){perror("mkfifo");exit(1);}//2. 正常的文件操作int fd =open(ipcPath.c_str(), O_RDONLY);if(fd <0){perror("open");exit(2);}//3.编写正常的通信代码char buffer[SIZE];while(1){memset(buffer,'\0',sizeof(buffer));
ssize_t s =read(fd, buffer,sizeof(buffer)-1);if(s >0){
cout <<"client say >"<< buffer << endl;}elseif(s ==0){//说明写端关闭了
cerr <<"read end of file, client quit, server quit too"<<endl;}else{//读取失败perror("read");break;}}//4. 关闭文件close(fd);unlink(ipcPath.c_str());//通信完毕,删除文件return0;}
client.c
此时不需要再创建命名管道,只需要获取已打开的命名管道文件
- 从键盘拿到了待发送数据
- 发送数据,也就是向管道中写入
#include"comm.hpp"intmain(){//不需要创建fifo,只需获取即可int fd =open(ipcPath.c_str(), O_WRONLY);if(fd <0){perror("open");exit(1);}//2.ipc通信
string buffer;while(1){
cout <<"Place Enter Message:";
std::getline(std::cin, buffer);write(fd, buffer.c_str(),sizeof(buffer));}//3.关闭close(fd);return0;}
效果展示:
一定要先运行服务端
server
创建命名管道,再运行客户端,实现了不相关进程通信 ——
如果我想让多个子进程来执行打印任务
当然我们就要调整一下
server.c
的业务逻辑:
#include"comm.hpp"#include<sys/wait.h>staticvoidgetMessage(int fd){//3.编写正常的通信代码char buffer[SIZE];while(1){memset(buffer,'\0',sizeof(buffer));
ssize_t s =read(fd, buffer,sizeof(buffer)-1);if(s >0){
cout <<"["<<getpid()<<"] "<<"client say >"<< buffer << endl;}elseif(s ==0){//说明写端关闭了
cerr <<"["<<getpid()<<"] "<<"read end of file, client quit, server quit too"<<endl;}else{//读取失败perror("read");break;}}}intmain(){//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);exit(2);}}for(int i =0; i < nums; i++){waitpid(-1,nullptr,0);}//4. 关闭文件close(fd);//log("关闭管道文件成功", Debug) << "step 3" <<endl;unlink(ipcPath.c_str());//通信完毕,删除文件//log("删除管道文件成功", Debug) << "step 4" <<endl;return0;}
🌍 pipe vs fifo
为什么pipe叫做匿名管道和和fifo叫做命名管道?
- 匿名管道文件属于内存级的文件,不需要名字,因为它是通过父子继承的方式看到同一份资源
- 命名管道一定要有名字,从而使不相关进程通过唯一路径定位同一个文件
三. System V标准下的进程间通信方式
下面我们要学习System V标准,是在同一主机内的进程间通信方案,是站在OS层面,专门为进程间通信设计的方案。
进程通信的本质是先让不同进程看到同一份资源,System V提供了这三个主流方案 ——
- 共享内存 - 传递数据
- 消息队列(有点落伍) - 传递数据
- 信号量 (多线程讲POSIX标准) - 实现进程同步&控制详谈
🌈共享内存
基于共享内存进行进程间通信原理 ——
- 首先在物理内存当中申请一块内存空间,将这块内存空间分别与各个进程各自的页表之间建立映射
- 进程虚拟地址空间当中开辟空间(共享内存)并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系
- 所以两个进程便看到了同一份物理内存,这块物理内存就叫做共享内存
💦共享内存的建立
共享内存提供者是操作系统OS,那么操作系统要不要管理共享内存呢? -> 先描述再组织
共享内存 = 共享内存块 + 对应的共享内存的内核数据结构(来描述其属性)
💛 创建共享内存
#include<sys/ipc.h>#include<sys/shm.h>intshmget(key_t key, size_t size,int shmflg);
参数:
key
:为了使不同进程看到同一段共享内存,即让不同进程拿到同一个ID,需要由用户自己设定,但如何设定的与众不同好难啊,就要借助下面这个函数。所以怎么样保证两个进程拿到同一个key
值呢?#include<sys/types.h>#include<sys/ipc.h>key_t ftok(constchar*pathname,int proj_id);
-pathname
:自定义路径名-proj_id
:自定义项目ID- 返回值:成功后,返回生成的key_t值。失败时返回1szie
:共享内存的大小,建议是4KB
的整数倍,因为共享内存在内核中申请的基本单位是页(内存页)。shmflg
:标记位,这一看就是宏,都是只有一个比特位是1且相互不重复的数据,这样|
在一起,就能传递多个标志位-IPC_CREAT
:如果单独使用IPC_CREAT或者flg为0,如果创建共享内存时,底层已经存在,获取之;如果不存在,就创建之-IPC_EXCL
:单独使用没有意义,通常要搭配起来IPC_CREAT | IPC_EXCL
,如果底层不存在,就创建,并返回;如果底层存在就出错返回。这样的意义在于 如果调用成功,得到的一定是一个全新的共享内存。
返回值:成功后,将返回有效的共享内存标识符。失败了,返回-1,并设置errno错误码。
💛 控制共享内存
手动查看与手动删除
ipcs -m 查看ipc资源,不带选项默认查看消息队列(-q)、共享内存(-m)、信号量(-s)
ipcrm -m + shmid //删除共享内存
system V IPC资源,生命周期随内核!所以我们要手动 / 自动删除,那怎么样自动删除呢?
💛 控制共享内存
#include<sys/ipc.h>#include<sys/shm.h>intshmctl(int shmid,int cmd,structshmid_ds*buf);
参数:
cmd
:设置IPC_RMID就行,IPC_RMID
:即便是有进程和当下的shm挂接,依旧删除共享内存(强大)buf
:这就是描述共享内存的数据结构啊! 返回值:失败返回-1,成功返回0
💛 挂接和去关联
attach 挂接 ——
#include<sys/types.h>#include<sys/shm.h>void*shmat(int shmid,constvoid*shmaddr,int shmflg);
shmaddr
:挂接到什么位置,我们也不知道,给NULL,让操作系统来设置shmflg
: 给0
最重要的是返回值:
- 这个地址一定是虚拟地址,类似malloc返回申请到的起始地址
- 失败返回-1,并设置错误码
detach 去关联 ——
intshmdt(constvoid*shmaddr);
shmaddr
:shmat返回的地址
注意:去关联,不是释放共性内存,而是取消当前进程和共享内存的关系,本质是去掉进程和物理内存构建映射关系的页表项去掉
返回值:成功返回0,失败返回-1
💛 shmid 和 key
只有创建的时候用key,大部分用户访问共享内存,都用的是shmid(用户层)
💦共享内存的进程间通信
comm.h
#pragmaone#include<iostream>#include<cstdio>#include<sys/types.h>#include<sys/ipc.h>#include<sys/shm.h>#include"log.hpp"usingnamespace std;//不推荐#definePATH_NAME"/home/ljj"#definePROJ_ID0x66
server.c
- 创建公共的
key
值 - 创建共享内存 - 建议创建一个全新的共享内存:因为是通信的发起者 带选项
IPC_CREAT | IPC_EXCL
若和系统中已经存在的ID冲突,则出错返回; 注意到其中权限perm
是0,那也可以设置一下int shmid =shmget(key, SIZE, IPC_CREAT | IPC_EXCL |0666);
- 将指定的共享内存,挂接到自己的地址空间上
- 将指定的共享内存,从自己的地址空间去关联
- 删除共享内存
#include"comm.hpp"
string TransToHex(key_t k){char buffer[32];snprintf(buffer,sizeof(buffer),"0x%x", k);return buffer;}intmain(){//1.创建公共的key值
key_t k =ftok(PATH_NAME, PROJ_ID);assert(k !=-1);Log("create key done", Debug)<<"server key : "<<TransToHex(k)<< endl;//2. 创建共享内存 - 建议创建一个全新的共享内存:因为是通信的发起者int shmid =shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL |0666);if(shmid ==-1){perror("shmget");exit(1);}Log("creat shm done", Debug)<<"shmid : "<< shmid << endl;sleep(10);//3.将指定的共享内存,挂接到自己的地址空间上char*shmaddr =(char*)shmat(shmid,nullptr,0);Log("attach shm done", Debug)<<"shmid : "<< shmid << endl;sleep(10);//这里就是通信的代码//4.将指定的共享内存,从自己的地址空间去关联int n =shmdt(shmaddr);assert(n !=-1);(void)n;Log("detach shm done", Debug)<<"shmid : "<< shmid << endl;sleep(10);//5.删除共享内存
n =shmctl(shmid, IPC_RMID,nullptr);assert(n !=-1);(void)n;Log("delete shm done", Debug)<<"shmid : "<< shmid << endl;return0;}
关于申请共享内存的大小size,我们说建议是4KB的整数倍,因为共享内存在内核中申请的基本单位是页(内存页),4KB。如果我申请4097Byte大小的空间,内核会向上取整给我4096* 2Byte,诶?那我监视到的↑怎么还是4097啊!虽然在底层申请到的是4096*2,但不会多给你,这样也可能引起错误~
client.c
- 只需获取共享内存;不用删除
#include"comm.hpp"intmain(){
key_t k =ftok(PATH_NAME, PROJ_ID);if(k <0){Log("create key failed", Error)<<"client key : "<< k << endl;exit(1);}Log("create key done", Debug)<<"client key : "<< k << endl;//获取共享内存int shmid =shmget(k, SHM_SIZE, IPC_CREAT);if(shmid <0){Log("create shm failed", Error)<<"client key : "<< k << endl;exit(2);}Log("attach shm success", Error)<<"client key : "<< k << endl;sleep(10);//挂接地址char* shmaddr =(char*)shmat(shmid,nullptr,0);if(shmaddr ==nullptr){Log("attach shm failed", Error)<<"client key : "<< k << endl;exit(3);}Log("attach shm success", Error)<<"client key : "<< k << endl;sleep(10);//使用//去关联int n =shmdt(shmaddr);assert(n !=-1);Log("datach shm success", Error)<<"client key : "<< k << endl;sleep(10);//你只管用,不需要删除共享内存return0;}
效果展示:
写一个命令行脚本来监视共享内存 ——
while:;do ipcs -m; echo "_________________________________________________________________"; sleep 1; done
注意观察
nattch
这个参数的变化:0->1->2->1->0
上面的框架都搭建好了之后,接下来就是通信部分:
1️⃣客户端不断向共享内存写入数据:
//client将共享内存看成一个char类型的bufferchar a ='a';for(; a <='z'; a++){//每一次都想共享内存shmaddr的起始地址snprintf(shmaddr, SHM_SIZE -1,\
"hello server, 我是其他进程, 我的pid: %d, inc: %c\n",\
getpid(), a);sleep(2);}
2️⃣服务端不断读取共享内存当中的数据并输出:
//将共享内存当成一个大字符串for(;;){printf("%s\n", shmaddr);sleep(1);}
结果如下:
ps:我们发现即使我们没有向
server
端发消息,server也是不断的在读取信息的
💦共享内存与管道进行对比
共享内存是所有进程间通信方式中最快的一种通信方式。
将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:
我们再来看看共享内存通信:
键盘写入shm,另一端可以直接获取到,哪里还需要什么拷贝?最多两次拷贝(键盘输入一次,输出到外设一次)
💦共享内存归属谁
共享内存的区域是在OS内核?还是在用户空间?
- 用户空间!
其中文本、初始化数据区、未初始化数据区、堆、栈、环境变量、命令行参数、再 往上就是
1G
的OS内核,其中剩余
3G
都是用户自己支配的
用户空间:不用经过系统调用,直接进行访问!
- 所以双方进程如果要进行通信,直接进行内存级的读和写(减少了许多拷贝)
那为什么之前将的pipe和fifo都要通过read、write进行通信,为什么呢?
因为管道双方看到的资源都属于内核级的文件,我们无权直接进行访问,必须调用系统接口
💦共享内存的特征
- 共享内存的生命周期随内核
- 共享内存是所有进程中速度最快的,只需要经过页表映射,不需来回拷贝(不经过OS)
- 共享内存没有提供访问控制,读写双方根本不知道对方的存在,会带来并发问题
🌈消息队列(了解)
严重过时:接口与文件不对应
创建消息队列,与创建共享内存极其相似:
#include<sys/types.h>#include<sys/ipc.h>#include<sys/msg.h>intmsgget(key_t key,int msgflg);
删除消息队列:
#include<sys/types.h>#include<sys/ipc.h>#include<sys/msg.h>intmsgctl(int msqid,int cmd,structmsqid_ds*buf);
我们可以通过key找到同一个共享内存。
我们发现共享内存、消息队列、信号量的 ——
- 接口都类似
- 数据结构的第一个结构类型
struct ipc_perm
是完全一致的!
我们由
shmid
申请到的都是01234… 大胆推测,在内核中,所有的
ipc
资源都是通过数组组织起来的。可是描述它们的结构体类型并不相同啊?但是~ System V标准的IPC资源,xxxid_ds结构体的第一个成员都是
ipc_perm
都是一样的。
🌈信号量
简单认识一下信号量
多个执行流,互相运行的时候互相干扰,我们不加保护的访问了同样的资源(临界资源),在非临界区多个执行流互相是不影响的
信号量本质是一个计数器,类似
int count
,用来衡量临界资源中的资源数目(好比电影院里面的座位,我们需要买票进入)
- 什么是临界资源?我们把多个进程(执行流)看到的公共资源 叫做临界资源
- 什么是临界区?我们把自己的进程,访问临界资源的代码 —— 临界区
- 什么是原子性?一件事儿,要么不做,要么就做完,没有中间态。
- 什么是互斥?在任意一个时刻,只能允许一个执行流进入临界区
n--
:可能因为时序问题,而导致n有中间状态,导致数据不一致
- 这种操作是不安全的,如果一个
n--
操作只有一行汇编,该操作是原子的!!
做总结: 信号量计数器
- 申请信号量 -> 计数器 — -> P操作 -> 必须是原子的!
- 释放信号量 -> 计数器 — -> P操作 -> 必须是原子的!!
📢写在最后
应该是我写过最长的一篇博客了
版权归原作者 张小姐的猫 所有, 如有侵权,请联系我们删除。