过程间通信
引言
在操作系统中,一个过程能够了解为是对于计算机资源汇合的一次运行流动,其就是一个正在执行的程序的实例。从概念上来说,一个过程领有它本人的虚构 CPU 和虚拟地址空间,任何一个过程对于彼此而言都是互相独立的,这也引入了一个问题 —— 如何让过程之间相互通信?
因为过程之间是相互独立的,没有任何伎俩间接通信,因而咱们须要借助操作系统来辅助它们。举个艰深的例子,如果 A 与 B 之间是独立的,不能彼此分割,如果它们想要通信的话能够借助第三方 C,比方 A 将信息交给 C,C 再将信息转交给 B —— 这就是过程间通信的次要思维 —— 共享资源。
这里要解决的一个重要的问题就是如何防止竞争,即防止多个过程同时拜访临界区的资源。
共享内存
共享内存是过程间通信中最简略的形式之一。共享内存容许两个或更多过程拜访同一块内存。当一个过程扭转了这块地址中的内容的时候,其它过程都会察觉到这个更改。
你可能会想到,我间接创立一个文件,而后过程不就都能够拜访了?
是的,但这个办法有几个缺点:
- 拜访文件须要陷入零碎调用,由用户态切入内核态,而后执行内核指令。这样做效率是非常低的,并且是不受用户把握的。
- 间接拜访磁盘是十分慢的,比拜访内存要慢上几百倍。从某种意义上说,这是共享磁盘不算共享内存。
Linux 下采纳共享内存的形式来使过程实现对共享资源的拜访,它将磁盘文件复制到内存,并创立虚拟地址到该内存的映射,就如同该资源原本就在过程空间之中,尔后咱们就能够像操作本地变量一样去操作它们了,理论的写入磁盘将由零碎抉择最佳形式实现,例如操作系统可能会批量解决加排序,从而大大提高 IO 速度。
如同上图一样,过程将共享内存映射到本人的虚拟地址空间中,过程访问共享过程就如同在拜访本人的虚拟内存一样,速度是十分快的。
共享内存的模型应该是比拟好了解的:在物理内存中创立一个共享资源文件,过程将该共享内存绑定到本人的虚拟内存之中。
这里要解决的一个问题是如何将同一块共享内存绑定到本人的虚拟内存中,要晓得在不同过程中应用 malloc
函数是会程序调配闲暇内存,而不会调配同一块内存,那么要如何去解决这个问题呢?
Linux 操作系统曾经想方法帮咱们解决了这个问题,在 #include <sys/ipc.h>
和#include <sys/shm.h>
头文件下,有如下几个 shm 系列函数:
-
shmget 函数:由 ftok()函数获取须要共享文件资源标识符(IPC 键),将该资源标识符作为参数获取共享内存区域的惟一标识 ID。
ftok()函数用以标识零碎 IPC 资源,例如这里的共享资源、下文的音讯队列、管道 …… 都属于 IPC 资源。
IPC(Inter-Process Communication,过程间通信),IPC 是指两个过程的数据之间产生交互。
- shmat 函数:通过由 shmget 函数获取的标识符,建设由共享内存到过程独立空间的映射。
- shmdt 函数:开释映射。
计算机界有一句名言:如果平时不常常应用某个函数工作的话,记住这些函数简单的参数以及应用办法是没有太大意义的,学习是须要重视函数用处以及原理。
因为咱们次要走 Java/Go 开发岗位,咱们与这些 Linux 零碎下的 C 函数打交道的次数可能为 0,因而在学习中我不会具体的形容函数的具体应用办法,当咱们用上时 Bing 搜寻即可。
通过上述几个函数,每个独立的过程只有有对立的共享内存标识符便能够建设起虚拟地址到物理地址的映射,每个虚拟地址将被翻译成指向共享区域的物理地址,这样就实现了对共享内存的拜访。
还有一种相像的实现是采纳 mmap 函数,mmap 通常是间接对磁盘的映射——因而不算是共享内存,存储量十分大,但拜访慢;shmat 与此相反,通常将资源保留在内存中创立映射,拜访快,但存储量较小。
不过要留神一点,操作系统并不保障任何并发问题,例如两个过程同时更改同一块内存区域,正如你和你的敌人在线编辑同一个文档中的同一个题目,这会导致一些不好的后果,所以咱们须要借助信号量或其余形式来实现同步。
信号量
信号量是迪杰斯特拉最先提出的一种为解决 同步不同执行线程问题
的一种办法,过程与线程形象来看大同小异,所以 信号量同样能够用于同步过程间通信。
信号量的工作原理
信号量 s 是具备非负整数值的全局变量,由两种非凡的 原子操作 来实现,这两种原子操作称为 P 和 V:
- P(s):如果 s 的值大于零,就给它减 1,而后立刻返回,过程继续执行。;如果它的值为零,就挂起该过程的执行,期待 s 从新变为非零值。
- V(s):V 操作将 s 的值加 1,如果有任何过程在等在 s 值变为非 0,那么 V 操作会重启这些期待过程中的其中一个(随机地),而后由该过程执行 P 操作将 s 从新置为 0,而其余期待过程将会持续期待。
了解信号量
信号量并不用来传送资源,而是用来爱护共享资源,了解这一点是很重要的,信号量 s 的示意的含意为 同时容许最大拜访资源的过程数量,它是一个全局变量。来思考一个下面简略的例子:两个过程同时批改而造成谬误,咱们不思考读者而仅仅思考写者过程,在这个例子中共享资源最多容许一个过程批改资源,因而咱们初始化 s 为 1。
开始时,A 率先写入资源,此时 A 调用 P(s),将 s 减一,此时 s = 0,A 进入共享区工作。
此时,过程 B 也想进入共享区批改资源,它调用 P(s)发现此时 s 为 0,于是挂起过程,退出期待队列。
A 工作结束,调用 V(s),它发现 s 为 0 并检测到期待队列不为空,于是它随机唤醒一个期待过程,并将 s 加 1,这里唤醒了 B。
B 被唤醒,继续执行 P 操作,此时 s 不为 0,B 胜利执行将 s 置为 0 并进入工作区。
此时 C 想要进入工作区 ……
能够发现,在无论何时只有一个过程可能访问共享资源,这就是信号量做的事件,他管制进入共享区的最大过程数量,这取决于初始化 s 的值。尔后,在进入共享区之前调用 P 操作,出共享区后调用 V 操作,这就是信号量的思维。
在 Linux 下并没有间接的 P &V 函数,而是须要咱们依据这几个根本的 sem 函数族进行封装:
- semget:初始化或获取一个信号量,这个函数须要承受 ftok()的返回值以及初始 s 的值,它将全局计数变量 s 绑定在由 ftok 标识的共享资源上,并返回一个惟一标识的信号量组 ID。
- semop:这个函数承受下面函数返回的信号量组 ID 以及一些其余参数,依据参数的不同有一些不同的操作,他将对与该信号量组 ID 绑定的全局计数变量 s 进行一些操作,P&V 操作便是基于此实现。
- semctl:这个函数承受下面函数返回的信号量组 ID 以及一些其余参数,次要进行管制信号量相干信息,如删除该信号量等。
管道
正如其名,管道就如同生存中的一根管道,一端输送,而另一端接管,单方不须要晓得对方,只须要晓得管道就好了。
管道是一种最 根本的过程间通信机制。 管道由 pipe 函数来创立:调用 pipe 函数,会在内核中开拓出一块缓冲区用来进行过程间通信,这块缓冲区称为管道,它有一个读端和一个写端。管道被分为匿名管道和有名管道。
匿名管道
匿名管道通过 pipe 函数创立,这个函数接管一个长度为 2 的 Int 数组,并返回 1 或 0 示意胜利或者失败:
int pipe(int fd[2])
这个函数关上两个文件描述符,一个读端文件,一个写端,别离存入 fd[0]和 fd[1]中,而后能够作为参数调用 write
和read
函数进行写入或读取,留神 fd[0]只能读取文件,而 fd[1]只能用于写入文件。
你可能有个疑难,这要怎么实现通信?其余过程又不晓得这个管道,因为过程是独立的,其余过程看不到某一个过程进行了什么操作。
是的,‘其余’过程的确是不晓得,然而它的子过程却能够!这里波及到 fork 派生过程的相干常识,一个过程派生一个子过程,那么子过程将会复制父过程的内存空间信息,留神这里是复制而不是共享,这意味着父子过程依然是独立的,然而在这一时刻,它们所有的信息又是相等的。因而子过程也晓得该全局管道,并且也领有两个文件描述符与管道挂钩,所以 匿名管道只能在具备亲缘关系的过程间通信。
还要留神,匿名管道外部采纳环形队列实现,只能由写端到读端,因为设计技术问题,管道被设计为半双工的,一方要写入则必须敞开读描述符,一方要读出则必须敞开写入描述符。因而咱们说 管道的音讯只能单向传递。
留神管道是梗塞的,如何梗塞将依赖于读写过程是否敞开文件描述符。如果读管道,如果读到空时,假如此时写端口还没有被齐全敞开,那么操作系统会假如还有数据要读,此时读过程将会被梗塞,直到有新数据或写端口被敞开;如果管道为空,且写端口也被敞开,此时操作系统会认为曾经没有货色可读,会间接退出,并敞开管道。
对于写一个曾经满了的管道同理而言。
管道外部由内核治理,在半双工的条件下,保证数据不会呈现并发问题。
命名管道
理解了匿名管道之后,有名管道便很好了解了。在匿名管道的介绍中,咱们说其余过程不晓得管道和文件描述符的存在,所以匿名管道只实用于具备亲缘关系的过程,而命名管道则很好的解决了这个问题 —— 当初管道有一个惟一的名称了,任何过程都能够拜访这个管道。
留神,操作系统将管道看作一个形象的文件,但管道并不是一般的文件,管道存在于内核空间中而不搁置在磁盘(有名管道文件系统上有一个标识符,没有数据块),访问速度更快,但存储量较小,管道是长期的,是随过程的,当过程销毁,所有端口主动敞开,此时管道也是不存在的,操作系统将所有 IO 形象的看作文件,例如网络也是一种文件,这意味着咱们能够采纳任何文件办法操作管道,了解这种形象是很重要的,命名管道就利用了这种形象。
Linux 下,采纳 mkfifo 函数创立,能够传入要指定的‘文件名’,而后其余过程就能够调用 open 办法关上这个非凡的文件,并进行 write 和 read 操作(那必定是字节流对吧)。
留神,命名管道实用于任何过程,除了这一点不同外,其余大多数都与匿名管道雷同。
音讯队列
什么是音讯队列?
音讯队列亦称报文队列,也叫做信箱,是 Linux 的一种通信机制,这种通信机制传递的数据会被拆分为一个一个独立的数据块,也叫做音讯体,音讯体中能够定义类型与数据,克服了无格局承载字节流的缺点(当初收到 void* 后能够晓得其本来的格局惹):
struct msgbuf {
long mtype; /* 音讯的类型 */
char mtext[1]; /* 音讯注释 */
};
同管道相似,它有一个有余就是每个音讯的最大长度是有下限的,整个音讯队列也是长度限度的。
内核为每个 IPC 对象保护了一个数据结构 struct ipc_perm,该数据结构中有指向链表头与链表尾部的指针,保障每一次插入取出都是 O(1)的工夫复杂度。
1. msgget
性能:创立或拜访一个音讯队列
原型:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflag);
参数:
key:某个音讯队列的名字,用 ftok()产生,音讯队列为 PIC 资源,该 key 标识了此音讯队列,如果传入 key 存在,则返回对应音讯队列 ID,否则,创立并返回音讯队列 ID。
msgflag:有两个选项 IPC_CREAT 和 IPC_EXCL,独自应用 IPC_CREAT,如果音讯队列不存在则创立之,如果存在则关上返回;独自应用 IPC_EXCL 是没有意义的;两个同时应用,如果音讯队列不存在则创立之,如果存在则出错返回。返回值:胜利返回一个非负整数,即音讯队列的标识码,失败返回 -1
2. msgctl
性能:音讯队列的管制函数原型:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
msqid:由 msgget 函数返回的音讯队列标识码
cmd:有三个可选的值,在此咱们应用 IPC_RMID
- IPC_STAT 把 msqid_ds 构造中的数据设置为音讯队列的以后关联值
- IPC_SET 在过程有足够权限的前提下,把音讯队列的以后关联值设置为 msqid_ds 数据结构中给出的值
- IPC_RMID 删除音讯队列
返回值:
胜利返回 0,失败返回 -13. msgsnd
性能:把一条音讯增加到音讯队列中原型:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
msgid:由 msgget 函数返回的音讯队列标识码
msgp:指针指向筹备发送的音讯
msgze:msgp 指向的音讯的长度(不包含音讯类型的 long int 长整型)
msgflg:默认为 0返回值:胜利返回 0,失败返回 -1
4. msgrcv
性能:是从一个音讯队列承受音讯原型:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);参数:与 msgsnd 雷同
返回值:胜利返回理论放到接收缓冲区里去的字符个数,失败返回 -1
特点
- 与管道不同,音讯队列的生命周期随内核,不会随过程销毁而销毁,须要咱们显示的调用接口删除或应用命令删除。
- 音讯队列能够双向通信。
- 克服了管道只能承载无格局字节流的毛病。
信号
对于信号
一个过程能够发送信号给另一个过程,一个信号就是一条音讯,能够用于告诉一个过程组发送了某种类型的事件,该过程组中的过程能够采取处理程序处理事件。
Linux 下 unistd.h
头文件下定义了如图中的常量,当你在 shell 命令行键入 ctrl + c
时,内核就会前台过程组的每一个过程发送 SIGINT
信号,停止过程。
咱们能够看到上述只有 30 个信号,因而操作系统会为每一个过程保护一个 int 类型变量 sig,利用其中 30 位代表是否有对应信号事件,每一个过程还有一个 int 类型变量 block,与 sig 对应,其 30 位示意是否梗塞对应信号(不调用处理程序)。如果存在多个雷同的信号同时到来,多余信号会被存储在一个期待队列中期待。
咱们要了解过程组是什么,每个过程属于一个过程组,能够有多个过程属于同一个组。每个过程领有一个过程 ID,称为pid
,而每个过程组领有一个过程组 ID,称为pgid
,默认状况下,一个过程与其子过程属于同一过程组。
软件方面 (诸如检测键盘输入是硬件方面) 能够利用 kill 函数发送信号,kill 函数承受两个参数,过程 ID 和信号类型,它将该信号类型发送到对应过程,如果该 pid 为 0,那么会发送到属于本身过程组的所有过程。
接管方能够采纳 signal 函数给对应事件增加处理程序,一旦事件产生,如果未被梗塞,则调用该处理程序。
Linux 下有一套欠缺的函数用以解决信号机制。
特点
- 信号是在软件档次上对中断机制的一种模仿,是一种异步通信形式。
- 信号能够间接进行用户空间过程和内核过程之间的交互,内核过程也能够利用它来告诉用户空间过程产生了哪些零碎事件。
- 如果该过程以后并未处于执行态,则该信号就由内核保存起来,直到该过程复原执行再传递给它;如果一个信号被过程设置为阻塞,则该信号的传递被提早,直到其阻塞被勾销时才被传递给过程。
- 信号有明确生命周期,首先产生信号,而后内核存储信号直到能够发送它,最初内核一旦有闲暇,会适当解决信号。
- 处理程序是能够被另一个处理程序中断的,因而这可能造成并发问题,所以 在处理程序中的代码应该是线程平安的,通常通过设置 block 位图以梗塞所有信号。
套接字
Socket 套接字是用与网络中不同主机的通信形式,多用于客户端与服务器之间,在 Linux 下也有一系列 C 语言函数,诸如 socket、connect、bind、listen 与 accept,咱们无需花太多工夫钻研这些函数,因为咱们可能一辈子都不会与他们打交道,对于原理的学习,后续我会对 Java 中的套接字 socket 源码进行分析。
结语
对于工作而言,咱们可能一辈子都用不上这些操作,但作为对于操作系统的学习,意识到过程间是如何通信还是很有必要的。
面试的时候对于这些办法咱们不须要把握到很深的水平,但咱们必须要讲的来有什么通信形式,这些形式都有什么特点,实用于什么条件,大抵是如何操作的,能说出这些,根本足以让面试官对你十分满意了。