文章目录
- 一、进程通信原理(让不同进程看到同一份资源)
- 二、管道通信
-
- [2.1 管道原理及其特点](#2.1 管道原理及其特点)
- [2.1 匿名管道和命名管道](#2.1 匿名管道和命名管道)
- 三、共享内存通信
-
- [3.1 共享内存原理](#3.1 共享内存原理)
- [3.2 创建和关联共享内存](#3.2 创建和关联共享内存)
- [3.3 去关联、ipc 指令和删除共享内存](#3.3 去关联、ipc 指令和删除共享内存)
- 四、消息队列和信号量(了解)
-
- [4.1 消息队列](#4.1 消息队列)
- [4.2 信号量](#4.2 信号量)
- [4.3 system V](#4.3 system V)
一、进程通信原理(让不同进程看到同一份资源)
尽管每个进程在其独立的地址空间中运行,它们之间并没有直接共享的内存区域,但所有进程都共享同一个OS操作系统。这个共享的操作系统会提供相应的进程通信机制,如共享内存、消息传递、信号、管道、套接字等。这些机制允许进程间实现数据的转发和共享,即便它们在内存中没有直接共享的空间。
因此,进程通信(Inter-Process Communication, IPC )的原理就是:操作系统是所有进程共享的第三方实体,为进程提供了一套丰富的通信工具和协议,使得进程能够在保持独立性的同时,**又让不同的进程看到同一份资源!!!**以此实现数据的共享和任务的协调。这种设计不仅提高了系统的稳定性和效率,还确保了通信的安全性和有效性,能够管理了进程间的同步和互斥,防止数据在传输过程中发生冲突。
二、管道通信
2.1 管道原理及其特点
我们把从一个进程连接到另一个进程的一个数据流称为一个 管道 ,这个数据流就是一段内核缓冲区,默认大小为 4kb,会根据实际情况做适当的调整。也可以叫做伪文件,不会刷新到磁盘。管道的操作形式是基于文件的,它只允许单向通信,如果要双向通信的话,需要建立两个管道,"互相读写"!
管道通信时可能会遇到以下四种情况:
- 1、读写端正常,管道如果为空,读端就要阻塞
- 2、读写端正常,管道如果被写满,写端就要阻塞
- 3、读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞
- 4、写端正常写,读端关闭了。操作系统就要向目标文件发送 SIGPIPE(13) 信号,杀掉正在写入的进程
管道通信特点:
- 1、管道是基于文件的,且文件生命周期是跟随进程的
- 2、为了保护管道文件的数据安全,进程之间需要进行进程协同,同步与互斥
- 3、匿名管道,仅允许有血缘关系的进程(常用于父子)进行通信,且是单向通信的
- 4、是面向字节流的(面向字节流读取, 跟写的时候的格式无关, 读取的时候只跟字节数有关)
2.1 匿名管道和命名管道
匿名管道:int pipe(int pipefd[2]);
pipefd 数组是一个输入输出型参数,创建成功返回 0 ,创建失败返回 -1 ,并设置错误码!pipefd[0] 表示读端文件描述符,pipedf[1] 表示写端文件描述符。匿名管道,只允许有血缘关系的进程之间进行进程间通信,管道是单向通信的,在我们 fork 之后,应让父子进程中的一方写入,另外一方读取。
命名管道:int mkfifo(const char *pathname, mode_t mode);
创建成功返回 0 ,失败返回 -1 ,并设置错误码,参数 pathname 为文件名, mode 是其对应的权限 。命名管道是一种进程间通信机制,它和匿名管道的区别是,它可以让没有血缘关系的进程进行通信。同时命名管道有对应的 inode ,可以理解为磁盘上的伪文件。但它不是一个实际的数据存储文件,无论写入多少数据,其大小都是 0 。
命名管道通过路径+文件名
作为该文件的唯一标识,用法和普通文件一样,使用 open、write、read
接口,但仍得保持其单向通信的特性,一端读取,一段写入。且需要等待写端打开之后,读端才会打开文件,否则读端会阻塞等待。
三、共享内存通信
3.1 共享内存原理
共享内存(Shared Memory)是进程间通信的一种方式,它允许两个或多个进程访问同一块物理内存区域。每个进程都有自己的虚拟地址空间,共享区存在于地址空间上的栈区和堆区之间。在用户需要申请共享内存时,操作系统在物理内存中申请一块空间,每个进程在自己的共享区中与这块物理内存单独建立页表映射,这种多个进程共同映射同一块物理内存的操作就叫做共享内存(即让不同的进程,看到同一份资源)
这些进程可以通过共享区页表映射直接读写这块物理内存,如同访问本进程的私有内存一样,进行数据交互。因此,它是内存级别的通信,没有额外的复制开销,使得通信速度非常快。但由于有多个进程访问同一块空间,也得考虑同步和互斥控制,我们可以通过使用管道、条件变量、消息队列、信号量,加锁等方法来避免竞态条件和数据不一致的问题。
3.2 创建和关联共享内存
从不同角度上来理解实现共享内存的步骤:
操作系统角度:
①创建共享内存
②删除共享内存
进程角度:
③关联共享内存
④去关联共享内存
因此我们需要按①③④②顺序区管理一个共享内存
创建共享内存 int shmget(key_t key, size_t size, int shmflg);
创建成功返回 shmid ,失败返回 -1 ,错误码被设置。size 为要申请的共享内存大小,由于操作系统对内存管理的最小单位是页(4KB),所以 size 建议设置成为页的整数倍。
shmflg为创建共享内存的选项,常见的有两个选项:
IPC_CREAT:
创建共享内存,不存在,就创建,存在,就获取IPC_EXCL:
不单独使用,必须和IPC_CREAT
配合使用。如果不存在指定的共享内存,就创建。存在,则出错返回。这样可以保证,如果shmget函数调用成功,一定是一个全新的共享内存。- 并且 shmflg 可以按位或文件权限,设置共享内存权限,类似
shmget(key,SIZE,flags | 0666);
现在关键的问题来了,你怎么保证不同的进程看到的是同一个共享内存呢?
操作系统中可能有很多个共享内存在被使用,所以我们就需要用一个唯一值来标识每一个共享内存,即 key 值。在内核中,让不同的进程看到同一份共享内存,让他们拥有同一个 key 即可。
那么这个 key 由谁来提供呢?
如果由操作系统来提供,那么创建共享内存的进程可以知道 key 值,但是其它要使用这个共享内存的进程如何获取这个 key 呢,不可能让创建共享内存的进程通信给他们吧。因为此时你要解决的就是双方通信的问题,这就变成了鸡生蛋、蛋生鸡的问题。所以 key 值必须由用户之间指定、用户之间的约定的,这样才能确保看到的是同一个共享内存。
获取 key 值:key_t ftok(const char *pathname, int proj_id);
将文件路径和一个项目标识符,通过一套算法转化为唯一 key 值,这里的路径名和项目标识符就是用户之间约定好的,成功返回一个唯一的 key 值,失败返回 -1,错误码被设置。
创建共享内存成功返回共享内存标识符 shmid ,它是一个由系统分配的唯一的整数值,用于唯一标识一个共享内存段。key 在操作系统内标定唯一性,而shmid 只在你的进程内,用来表示资源的唯一性!!!接下来的对共享内存的操作(关联、去关联、删除)我们都采用 shmid 作为参数而非 key
关联共享内存:void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- shmid:共享内存标识符,由 shmget 函数返回
- shmaddr:指定附加共享内存的位置。如果设置为 NULL,则由系统选择地址
- shmflg:控制共享内存的访问方式和其他选项的标志,一般设置为0使用默认行为
成功返回附加到进程地址空间的共享区的地址。失败,返回 (void *) -1,错误码被设置。使用类似于 molloc,使用时需要强转为需要的指针类型。对此我们就像访问自己的私有内存一样,与其它进程进行数据交互通信
3.3 去关联、ipc 指令和删除共享内存
去关联共享内存:int shmdt(const void *shmaddr);
成功返回0,失败返回-1,错误码被设置。参数为共享区中 shmat 函数返回的共享区起始地址
ipc 系列相关指令:
一、查看IPC
ipcs 和 ipcs -a :
查看所有IPC对象ipcs -q:
查看消息对列对象ipcs -m:
查看共享内存对象ipcs -s:
查看信号量对象二 、删除 IPC 对象
ipcrm -Q key :
根据键值key,删除指定的消息对列ipcrm -q id:
根据ID,删除指定的消息对列ipcrm -M key:
根据键值key,删除指定的共享内存ipcrm -m id:
根据ID,删除指定的共享内存ipcrm -S key :
根据键值key,删除指定的信号量ipcrm -s id:
根据ID,删除指定的信号量
删除共享内存:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
- shmid:由 shmget 函数返回的共享内存标识码
- cmd:将要采取的动作,
IPC_STAT:
提取 shmid_ds 结构中的数据;IPC_SET:
设置 shmid_ds 结构中的数据;IPC_RMID:
删除共享内存- buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
毋庸置疑,共享内存可能存在多个必然是要先描述后组织起来的,shmid_ds 就是描述共享内存的结构体。它包括共享内存的大小、创建者和最后操作者的进程ID、当前有多少进程附加到这个共享内存段等属性。顺着往下找,其结构体内部的结构体 ipc_perm 中,就保存了由用户提供的 key 值。
四、消息队列和信号量(了解)
4.1 消息队列
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法,读端和写端公用一个队列。当发送方有数据发送时,将数据先打包成一个节点,然后尾插到内核中的消息队列中去。当接收方接收数据时,从队列头部开始去找所需要的节点,然后进行解包得到数据。同时每个数据块都会有个记录类型的数据,来判断该数据块该被哪个进程读取。
消息队列接口:使用几乎和共享内存一样,这里就不详细介绍了
获取:int msgget(key_t key, int msgflg);
控制:int msgctl(int msqid, int cmd, struct msqid_ds *buf);
发送:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
接受:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
4.2 信号量
信号量:
信号量本质上就是个计数器,它统计的是公共资源资源的剩余量。而公共资源可以被多个进程同时访问,如果访没有同步与互斥,就会导致数据不一致问题(一个进程还在写的时候另一个进程就开始读)。而这种通过同步与互斥被保护起来的资源称为临界资源,访问临界资源的那部分代码称为临界区,其他的代码就称为非临界区。
互斥:当有多个执行流想要访问同一份资源的时候,我们只允许一个执行流进行访问,当这个执行流访问完了,下一个执行流能访问
进程在访问公共资源前要先申请信号量,需要让多个进程看到同一个计数器,避免资源不足而访问失败。看到同一个计数器说明信号量也是个公共资源,也需要保护,因此对信号量的 pv 操作必须是原子的 。
P 操作:申请信号量,计数器减 1
V 操作:释放信号量,计数器加 1
原子性:要么不做,要么做完,只有这两种状态的情况
4.3 system V
进程间通信除了通过管道,都是基于文件的通信方式,还有一种方式是:System V 标准的进程间通信方式。System V 标准是一个在OS层面专门为进程通信设计的一个方案,它是被精心设计过的。system V IPC 提供的通信方式有三种: 共享内存、消息队列和信号量,你可以发现它们的系统调用接口都非常相似,同时它们都有描述自己字段的结构体 xxx_ds 。而在它们的 xxx_ds 结构体中,开头都有一个共同的字段 ipc_perm ,这个 ipc_perm 类似于基类,被其它子类继承下去
cpp
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
因为它们的第一个字段 ipc_perm 是一样的,所以可以维护一个 struct ipc_perm * 的指针数组,存共享内存、消息队列、信号量它们三者中,第一个元素的地址,也就是 &ipc_perm 。这样就可以把共享内存、消息队列和信号量三个部分直接管理起来了。而我们知道结构体的第一个成员的地址和结构体对象的地址在数值上是相同的,并且操作系统在内部可以识别这个对象是共享内存、消息队列和信号量中的哪一个,因此我们拿到这个数组中的 ipc_perm 地址便能访问这结构体的其它成员,将它们管理起来