IPC(二)Syestem V

1.共享内存

进程间通信,管道的效率较低(拷贝次数过多),而且管道文件大小毕竟是有限的

其实从匿名管道->命名管道->共享内存,本质都是让通信的进程看到同一份资源,这份资源由一个进程创建,另一个进程获取,比如匿名管道是父进程创建,子进程获取;命名管道是其中一个进程创建,另一个进程获取;而共享内存也是,因为资源只有一份,不可能二者都创建,创建工作只需要一个进程来做,另外一个进程获取即可,双方使用

System V有三种通信方式,共享内存,消息队列和信号量

先看共享内存,其实名字很好理解了,我们知道进程有自己的虚拟地址空间,但是进程具有独立性,所以进程A的虚拟地址空间其它进程是看不到的,如果要让其它进程看到,首先技术上肯定是可以实现的,毕竟OS在这,但是原则上,是不可以的,违反了进程的独立性;而进程通信的本质是让不同进程看到同一份资源,那么这个时候只能操作系统来了,所以共享内存肯定也涉及到系统调用

原理是,既然我们都只能看到各自的虚拟地址空间,那就把同一块内存既映射到进程A的虚拟地址空间,也映射到进程B的虚拟地址空间,那这样就可以实现不同进程看到同一份资源,所以共享内存的本质就是由OS在物理内存中申请一块内存,接着挂载(也就是映射)到进程的虚拟地址空间的共享区中(和动态库加载有些类似,把动态库加载到内存,映射到多个使用动态库的进程虚拟地址空间,修改页表)

而通信的进程肯定不止一组,所以肯定存在多份共享内存,那么肯定需要管理,先描述,再组织,使用struct 结构体来描述共享内存的物理起始地址,大小,都映射到哪些进程,分别映射的起始虚拟地址分别是什么,包括共享内存肯定有唯一标识,因此共享内存的本质其实是内存块+结构体

综上,共享内存特点

1.共享内存映射到进程虚拟地址空间的共享区

2.共享内存原理本质是简化的动态库加载

3.共享内存管理的数据结构+共享内存=共享内存

4.使用共享内存步骤:1)申请共享内存 2)关联挂接到进程的虚拟地址空间 3)使用 4)去关联 5)释放共享内存

s是shared,m是memory,共享内存

size是4096B的整数倍(4KB,磁盘数据块大小)

shmflg

IPC_CREAT 可以单独传递,如果共享内存不存在,就创建,否则获取(需要一个共享内存,不管是不是刚创建的),有现成的就捡现成的,没有就自力更生

IPC_EXCL 不可以单独传递,IPC_CREAT | IPC_EXCL 如果共享内存不存在,就创建,否则报错(我只要新的刚创建的共享内存),不食嗟来之食

shmflg也可以传权限,因为新创建的文件肯定需要规定权限位

那怎么知道存在不存在呢?共享内存struct肯定有标识共享内存唯一性的属性

而key这个参数是就是内核中用来标识共享内存唯一性的属性,由用户传递,在创建/获取共享内存的时候,

那为啥不由OS提供呢?OS:臣妾做不到啊

我们来分析一波,首先假设key由OS生成,进程A创建了共享内存,生成了该共享内存的key,那么进程B和A要通信,肯定要访问同一个共享内存,因此B肯定要获取A的key,之后去内核中找key所标识的共享内存的内核数据结构,那也就是进程A要给进程B发数据,也就是二者要通信,而二者要通信就必须让A给B发key,而要发key,就要通信,变成了一个哲学难题🤔,先有🐔还是先有🥚

当然也可以通过搞一个命名管道来帮忙,但是一般通信方式不依赖其它方式,解耦合

于是乎,OS说搞不了啊,所以key就由进程自己传递,也就是用户,只要进程A和B约定好用同一个key,就可以访问同一块共享内存,其实进程就是程序呗,程序都是程序员👩‍💻写的,写程序的时候,规定就可以了

而这个key可以自行规定,也可以让系统生成,通过库函数ftok,设定pathname为/tmp,而proj_id为0x66

Server创建共享内存

ipcrm -m 跟的不是key,因为指令的本质是代码,是用户执行,而用户通过smid来操作共享内存

而且可以看到在Server结束之后共享内存还在,说明共享内存生命周期不随进程,而是随内核,类似OS,除非重启系统或者用户主动删除共享内存,否则共享内存,包括System V标准下的通信方式的通信资源一直存在

perms是权限,nattch是挂载进程数

能申请就得能删除,谁创建,谁删除,使用shmctl(删除是控制的方式之一),op设为IPC_RMID,RMID是remove identifier,标识符指的就是shmid,struct shmid_ds是一个结构体类型,就是用来管理共享内存的结构体,其成员ipc_perm中的key_t key就是内核中用于标识共享内存唯一性的,但是shmid是用户用来操作的(就像inode号和fd)

shmat挂接共享内存到当前进程的地址空间,进程是不能直接访问物理内存的,所以必须使用shmat将共享内存挂载到进程地址空间的共享区,返回值是虚拟地址空间的起始地址,因此访问共享内存不需要系统调用

可以通过第二个参数shmaddr指定挂载到具体的虚拟地址,但是一般不推荐,nullptr表示由OS挂载到合适的虚拟地址,返回值void*类似malloc返回堆上任意一块空间的起始地址

shmflg为0表示不设限,就是不降低shmget设置的权限,在shmget设置权限位为可读可写,表示挂载后后的内存段是可读可写的

perms是权限,bytes是大小,size是4096的整数倍,但是给用户显示的还是用户申请的数目

nattch是挂接数,表示有多少进程把该共享内存挂接到其进程地址空间,attach是挂接,detach是取消挂接

有挂载就有卸载

Client和Server都要取消挂载

有意思的是,只运行Client,因为shmflg是IPC_CREAT,没有设置权限,所以不存在就创建共享内存,但是在挂接的时候失败了

我们也可以通过shmctl获取共享内存的信息

也可以通过op参数来获取或设置共享内存的属性

目前为止,双方能看到同一份资源,包括对资源的收尾工作,现在,我们要进行通信了,首先我们肯定要看到要写的虚拟地址和虚拟地址块大小(这里测试用的是128,4096太大,刷屏了,很难观察)

Shm提供接口,获取虚拟地址内存块大小和起始地址,之后Client进行写入,Server进行读取

如果是管道,首先要把数据从外设(网卡,磁盘或者其他)读到栈上,接着通过系统调用write写到管道中,接着通过read系统调用读到栈上,这至少3次,如果还要从栈到文件(比如标准输出)等,就是4次,包括系统调用涉及到内核态和用户态之间的切换,而共享内存,读和写都是在用户态,比如下面直接从stdin到内存,内存到stdout,就是两次拷贝,而且都是在用户态;因此拷贝次数少,直接映射,不需要系统调用

所以说共享内存是进程间通信方式中最快的,写端拷贝的shm,其他端立马能看到,但是假如写的过快,导致覆盖,不具备同步,包括并行访问可能存在问题(比如二义性),以及要自己控制是否越界,也就是缺乏资源的保护机制,不具备同步或互斥,需要用户自行设置,完成保护;而如果要设置互斥,比如可以再叠加一个命名管道,当Client向管道中写入数据,Server的read才会被唤醒,那么就可以具有同步

最后,其实进程通信是为了上进程协同,共享->进程通信->独立性->看到同一份资源->多执行流并发访问的问题->同步和互斥,因为要进程通信,那就要为通信做准备,所以要看到同一份资源,在哲学上就是解决问题的同时可能会带来新的问题,问题是时时刻刻都存在的,不过是紧不紧急罢了

2.消息队列

创建和获取

消息队列也是进程间通信的一种方式,消息队列由节点构成,第一个节点是管理该队列的内核数据结构相关信息,后续节点是进程每次发送数据会创建一个节点,放到队列中,那如何区分是谁发的呢?每个节点有类型,比如进程A发的数据有类型是type A,进程B发的数据类型是type B,这样假如只有进程A和B通过这一队列通信,那在消息队列中的节点,进程A取下类型为typeB的节点就是进程B发的数据

而内核中可能有多组进程通过消息队列通信,那么也就是说会存在多个消息队列,肯定要做管理,先描述,再组织,主要是通过消息队列头,里面存储struct结构体描述消息队列的信息

那进程具有独立性,怎么让不同进程看到同一份资源?可以看到消息队列的创建msgget需要传入key,通过同一个key就可以看到同一份资源

而每个节点是一个数据块,因此可以通过共享队列,实现有类型的,数据块级别的通信,也就是消息队列

消息队列和共享内存都是IPC System V标准下的,共享内存获取是shmget,消息队列获取是msgget,xxxflg包括IPC_CREAT以及IPC_EXEL,控制是xxxctl,op为IPC_RMID是删除,IPC_STAT是查内核数据结构xxxid_ds,IPC_SET是设置内核数据结构;

即所谓标准规定接口,返回值,参数,数据结构,key共享方式,创建和删除,底层原理,具有共性

包括ipcs查看所有System V标准下得通信资源,-q查看消息队列

使用msgsnd来发送消息,msgrcv接收消息,msgsnd的参数从左往右,第一个是msgget返回的msgid,第二个指针指向struct msgbuf,第一个成员是消息类型(>0),第二个成员是发送的消息;接着是消息数据部分的大小,msgflg

对于msgrcv,第一个是谁来接收的msgid,第二个是接收struct msgbuf,也就是把内核中消息队列的struct msgbuf拷贝到用户定义的struct msgbuf,接着是消息数据部分的大小,以及要接收消息的类型,最后是msgflg

msgflg一般为0,表示阻塞模式,没有符合类型的消息就阻塞进程

A进程类型为1,B进程创建struct msgbuf data;用来接收即可

3.信号量

3.1 原理

进程间需要通信->进程具有独立性->让不同的进程看到同一份资源(file,内存块,消息队列)->OS提供

解决进程间通信问题,而不同进程访问同一份资源就会存在并发访问的问题,也就是数据不一致,

多个执行流,能同时看到并访问的公共资源称为共享资源

为了保护共享资源,任何时刻,只能有一个执行流访问公共资源,称为互斥

互斥访问的的共享资源,称临界资源

临界资源的访问,本质是进程,CPU执行访问临界资源的代码,称为临界区,代码分为临界区和非临界区

原子性(要么不做,要么做完),两态的操作,不容易被打扰,同步

下面可以看到A和B交错打印,结果很不可预料,都混杂在一起,信息被干扰,因为显示器是文件,是共享资源,父子进程在向同一份共享资源写入

又称信号灯,本质是计数器,但是又不止于计数器

举个例子,看电影要买票,一个放映厅(共享资源)假设有100个座位,那卖票上限是100,把票看作信号量,就是初始值为100的信号量,买票就是申请信号量(是对资源的预定机制),信号量的本质是一个计数器,描述临界资源中,资源的数量。

申请资源要先申请信号量,成功就可以访问资源,失败进程会被阻塞

申请座位资源,买到票,座位就是我的,而不是访问资源,也就是看电影时票才是我的(深有体会,买错地点了,还退不了,没去看,但是钱还是付了),因为买到票了,还没有去看,但是已经预定了这个座位,别人不能去预定以及后续看电影

资源数量>1就是多元信号量,=1就是二元信号量,一次只能放进来一个人,互斥(比如一个人包场)

下面的伪代码其实是模拟,但是有一些瑕疵

假如进程A执行if(sem>0),此时sme=1,但是之后进程A被调度,进程B上处理机,sem--导致sem=0,之后进程A回来,也导致sem--为-1,这时候就出问题了,在没有资源的情况下还申请到了资源,所以资源的申请(P)和释放(V)都必须是原子性的,由OS来保证

假如sem是一种全局变量,但是我们知道,进程要申请信号量,就要看到信号量,信号量本身也是共享资源,进程具有独立性,所以信号量不可能是全局变量,信号量作为共享资源由OS来保护,PV操作的原子性,而不同的进程看到同一个信号量,也是通过同一个key

上面提到PV操作是原子的,但实际上并不准确,因为原子性通过加锁来保证,或者汇编之后只有一条汇编代码,

假设sem初始值为10,进程A执行sem--,首先把内存中的sem值move到CPU中的寄存器,接着执行--操作,再move更新到内存,但是如果move到寄存器之后,进程A被调度(进程可以在任何时刻被调度),之后进程B把sem减为0,之后进程A又把9更新到内存,这就出问题了,所以汇编之后如果是一条语句就是原子的,要不然就得加锁保证原子性

要保护共享资源,就要先看到同一份资源(同一个信号量,计数器资源),因此信号量被归为IPC

3.2 接口

获取信号量资源semget

nsems是信号量数量,可以一次创建多个信号量(信号量集),sem semsnsems

通过ipcs -q可以查到信号量资源以集为单位

semctl-初始化和删除

op为IPC_RMID,删除

op为SETVAL,设置的是union semnu(用于缺省参数...)中的val,也就是初始值,这个联合体需要自己定义,OS不提供

semop-操作信号量(PV)

semop第二个参数是struct sembuf spops1(柔性数组),每一个struct sembuf包含右边三种成员,第一个是要操作的信号量在信号量集中的下标,第二个是操作P/V

4.内核组织ipc资源

shm,msg,sem相似,IPC本质是共享资源本身(内存块,队列,计数器)+内核数据结构

可以看到共享内存,消息队列和信号量第一个成员都是struct ipc_perm类型,结构体的地址和结构体中第一个成员的地址在数值上相同,因此内核中可以把这三者以struct ipc_perm数组的形式组织起来,每个数组元素是结构体的地址,通过强转为struct shmid_ds的地址,再用->来访问成员,也就是多态,把基类组织起来,访问子类

那么内核是如何组织ipc资源的?

首先内核struct ipc_ids里的struct ipc_id_ary* entries指向一个结构体,结构体第一个成员是size,第二个成员是柔性数组(变长数组进行动态扩容),本来第二个成员是不占内存的,大小计算=nsizeof(struct kern_ipc_perm )+sizeof(ipc_id_ary),每一个数组成员都是一个ipc资源,kern_ipc_perm就是上面查到的ipc_perm,因为本质上给用户看的数据结构!=内核存储的数据结构,比如下面我们知道前面几列都是inode存储的信息,但是inode不存文件名,文件名在目录文件中存储,所以我们看到的信息其实是组合过的

那么用户每申请一个ipc资源,就会malloc一个结构体,地址填在柔性数组中,柔性数组元素个数有限制,通过%运算来保证

然而我们看到kern_ipc_perm中有key标识共享内存唯一性,但是却不知道是哪一种ipc资源,共享内存?信号量?消息内存?

其实内核中共享内存(shm),消息队列(msg),信号量(sem)有各自的ipc_id结构体对象,也就是三种资源分开管理,这样无论是增删改查某种资源查各自的结构体即可,也就是为什么不同资源的系统调用不同

类似给用户看到的,肯定有管理共享内存,消息队列,信号量的内核数据结构,第一个成员都是struct kern_ipc_perm,通过把第一个成员的地址组织起来,之后在访问的时候进行强转为(struct msg_queue*)pi->x来访问数组p第i个数据的成员x,如果访问的是kern_ipc_perm中的属性,不需要强转,如果访问的是msg_queue中的属性需要强转之后通过->访问(访问信号量和共享内存类似)

其实shm/sem/msgget返回值是索引(不等价于数组下标,因为还有移位和与操作)

那么上面做到了用C语言实现多态(面试题)用一个类型的struct来组织不同类型的struct,也就是C++中的多态,是属性方面的多态,实现方法是不同的类第一个成员是同一个数据类型(结构体嵌套等),运用结构体起始地址和第一个成员地址相同,再通过指针强转+箭头运算符来访问struct中特有的其它成员

而方法层面的多态,通过函数指针来实现,把不同方法抽象成统一的函数接口,通过同一接口的函数调用不同内容的函数,实现方法上的多态,硬件方法都是这么实现的,task_struct里的struct files_struct* files的文件描述符表,每一个strcut file* 里的操作表,因为每一个硬件都抽象为文件

C++中的属性+方法都搞定了

q_receivers是接受者队列,q_senders是发送者队列,如果进程A要读取type B类型的数据,但是消息队列中没有,就阻塞进程A,从运行队列挂到q_receivers队列,进程既等待硬件资源如打印机,也等待软件资源如消息,等到进程B写入数据,再把进程A从q_receivers中唤醒,挂到运行队列;如果消息队列满了,此时还有进程要往里写,就阻塞进程,把进程从运行队列挂到q_senders队列,等到消息队列有空位了,再从q_senders中唤醒进程进行写入

每个进程都有虚拟地址空间,由task_struct里的strcut mm_struct维护,mm_struct里的vm_area_struct维护进程地址空间的段,其vm_start和vm_end界定一个段,其struct flie* vm_file和shmid_kernel里的shm_file都指向共享内存的struct file,struct file里的f_mmaping指向内核缓冲区也就是共享内存,vm_area_struct里的vm_private_data指向shmid_struct,多个task_struct都能看到struct shmid_kernel和struct file就能通信

上面是匿名共享内存映射,对于磁盘中的文件也是一样,打开文件,创建内核缓冲区,把内核缓冲区映射到进程的虚拟地址空间,两个进程就可以不需要系统调用,访问各自虚拟地址空间就可以访问文件内容

使用mmap创建vm_area_struct将虚拟地址空间和文件的struct file连接起来,就可以映射到虚拟地址空间

动态库的实现就是,比如libc.so加载到内核,打开文件创建struct file,内核缓冲区指向共享区,vm_area_struct维护,vm_start和vm_end维护起始和结束虚拟地址,更新页表,动态重定位,更新got表,就可以实现多个进程共享一个动态库,访问虚拟地址空间而不是系统调用

牛刀小试

  1. 进程间通讯的方式中哪种的访问速度最快()
    A.管道
    B.消息队列
    C.共享内存
    D.套接字

C

  1. 以下描述正确的有 ()
    A.共享内存实现通信的原理是因为所有进程操作映射同一块物理内存
    B.共享内存的操作是进程安全的
    C.共享内存被删除后,则其它进程直接无法实现通信
    D.所有进程与共享内存断开映射后,则共享内存自动被释放

A

  1. 以下描述正确的有()
    A.使用ipcrm -m命令删除指定共享内存后,则会直接释放共享内存
    B.使用ipcs -m命令删除指定共享内存后,则会直接释放共享内存
    C.使用ipcrm -a选项可以删除所有进程间通信资源
    D.使用ipcrm命令不指定选项可以删除所有进程间通信资源

A

  1. 以下关于ipcrm命令描述正确的有()
    A.ipcrm命令不指定选项可以删除所有进程间通信
    B.ipcrm -m命令可以删除共享内存
    C.ipcrm -s命令可以删除共享内存
    D.ipcrm -q命令可以删除管道

B

  1. 以下关于ipc命令描述正确的有()
    A.ipcs -m用于查看消息队列的信息
    B.ipcs -q可以查看消息队列的信息
    C.ipcrm -s可以查看共享内存的信息
    D.ipcrm -q可以查看共享内存的信息

B

vscode远程连接的bug,可能vs code突然退出或者电脑突然关机,但是没退干净,再连的时候就连不上

相关推荐
zzqssliu1 小时前
Next.js图片自适应压缩:跨境站点图片加载提速代码方案
linux·javascript·ubuntu
干掉乔治的猪1 小时前
【如何恢复 Ubuntu 引导分区:Windows11 + Ubuntu22.04 双系统 GRUB 修复踩坑记录】
linux·ubuntu·grub·修复·双系统
流浪0011 小时前
Linux系统篇(五):Linux 进程控制全解:fork、exec、wait 核心原理与实战
linux·运维·服务器
不会就选b1 小时前
Linux之make,makefile
linux·运维·服务器
code monkey.1 小时前
【Linux之旅】HTTP 协议解析:从请求格式到构建 Web 服务器
linux·服务器·网络·http
vortex52 小时前
Linux 传统设计哲学:通过调用名区分行为的艺术
linux·运维·网络
嵌入式-老费2 小时前
esp32开发与应用(esp32-s3的usb转串口功能)
linux·运维·服务器
壮Sir不壮2 小时前
GO语言——GMP调度模型
linux·开发语言·golang·go·操作系统·线程·协程
m0_693200652 小时前
VSCode使用ssh remote插件远程连接linux主机
linux·vscode·ssh