操作系统之进程间通信

每个进程都有不同的用户地址空间,每个进程的⽤户地址空间都是独⽴的,⼀般⽽⾔不能互相访问,但内核空间是每个进程都共享的,所以进程之间通信必须通过内核。在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信( IPC,InterProcess Communication) 。

一、信号

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。在软件层次上是对中断机制的一种模拟,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制。如果程序在执行处于阻塞状态的系统调用时接收到信号,并且为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且erno被设置为EINTR。可以使用sigaction函数为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。
对于默认行为是暂停进程的信号(比如SIGSTOP、SIGTTIN), 如果没有为它们设置信号处理函数,则它们也可以中断某些系统调用。POSIX没有规定这种行为,这是Linux独有的。

信号产⽣,有下⾯⼏种⽤户进程对信号的处理⽅式。 1.执⾏默认操作。Linux 对每种信号都规定了默认操作,例如SIGTERM 信号,就是终⽌进程的意思。 2.捕捉信号。可以为信号定义⼀个信号处理函数。当信号发⽣时,执⾏相应的信号处理函数。 3.忽略信号。当不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应⽤进程⽆法捕捉和忽略的, SIGKILL 和 SEGSTOP ,它们⽤于在任何时候中断或结束某⼀进程。

二、管道

一个管道实际上就是存在于内存中的文件,对这个文件的操作要通过两个已经打开进程进行,它们分别代表管道的两端。一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

1.无名管道

(1)只能用于具有亲缘关系的进程之间的通信,通常一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可以通过管道通信。

(2)半双工的通信模式,具有固定的读端和写端,传输方向同时只能是一个方向。

(3)管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数,但是在文件系统里并不存在pipe对应的文件而且不支持如lseek() 操作。

无名管道的使用

无名管道可由pipe()函数创建,pipe函数调用成功返回0,调用失败返回-1。

#include <unistd.h>
int pipe(int pipefd[2]);

调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信。它有一个读端一个写端,通过pipefd参数传出给用户程序两个文件描述符,pipefd[0]指向管道的读端,pipefd[1]指向管道的写端(就像0是标准输入1是标准输出)。所以管道在用户程序看起来就像一个打开的文件,通过read(pipefd[0]);或者write(pipefd[1]);向这个文件读写数据其实是在读写内核缓冲区。

step1: 父进程创建一个pipe,其中fd[0]固定用于读管道,而fd[1]固定用于写管道。
Step2:父进程fork,子进程继承了父进程的管道
Step3:之后取决于我们想要的数据流方向来关闭相应的端。

无名管道读写注意事项
当读一个写端已被关闭的管道时,在所有数据都被读取后, read返回0,以指示达到了文件结束处。可以复制一个管道的描述符,使得有多个进程具有写打开文件描述符。通常一个管道只有一个读进程,一个写进程。如果写一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write出错返回,errno设置为EPIPE。在写管道时常数PIPE_BUF规定了内核中管道缓存器的大小。

2.有名管道

有名管道为了克服无名管道的不足之处:无名管道只能用于具有亲缘关系的进程之间,限制了使用范围。有名管道可以使互不相关的两个进程互相通信。

Linux中设立了一个类型为管道的文件系统,以FIFO的文件形式存在于文件系统中,这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够通过FIFO相互通信。因此,通过FIFO不相关的进程也能交换数据。但在磁盘上只是一个节点,而文件的数据则只缓存在内存中,与普通管道一样。

有名管道的读写规则

有名管道的读写原则和无名管道的读写原则基本一致,通信数据都遵循先进先出原则。

三、消息队列

消息队列是保存在内核中的消息链表,在发送数据时分成⼀个个独⽴的数据单元,也就是消息体(数据块)。消息体是⽤户⾃定义的数据类型,消息的发送⽅和接收⽅约定好消息体的数据类型。每个消息体都是固定⼤⼩的存储块,不像管道是⽆格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
消息队列不适合⽐较⼤数据的传输,每个消息的最大长度有上限,每个消息队列总的字节数,系统消息队列的总数也有上限。可以用cat/proc/sys/kernel/msgmax查看具体的数据。 
消息队列通信过程中,存在⽤户态与内核态之间的数据拷⻉开销,因为进程写⼊数据到内核中的消息队列 时,会发⽣从⽤户态拷⻉数据到内核态的过程,同理另⼀进程读取内核中的消息数据时,会发⽣从内核态 拷⻉数据到⽤户态的过程。

生命周期随内核,如果没有手动释放或者关闭操作系统,消息队列会⼀直存在。消息队列可以双向通信,克服了管道只能承载无格式字节流的缺点。

四、共享内存

共享内存很好的解决了消息队列的读取和写⼊时⽤户态与内核态之间的消息拷⻉过程这⼀问题。机制就是拿出⼀块虚拟地址空间来,映射到相同的物理内存中。这个进程写⼊的东⻄, 另外⼀个进程⻢上就能看到了,不需要拷⻉来拷⻉去,⼤⼤提⾼了进程间通信的速度。

最快的一种通信方式,因为没有中间介质,可以有多个进程共享一块内存。通常共享内存有一个进程来创建,和其他进程共享。缺点:多个进程同时访问共享内存时存在同步问题,即可能一个进程在读的时候其他的进程不能读写等等,通过信号量来调节。共享内存可以通过mmap()映射普通文件(特殊情况下还可以采用匿名映射)机制实现,也可以通过系统V共享内存机制实现。
系统加载一个进程的时候,分配给进程的内存并不是实际物理内存,而是虚拟内存空间。两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中。两个进程虽然有着独立的虚拟内存空间,但有一部分却是映射到相同的物理内存,这就完成了内存共享机制。进程间的数据不用传送,而是直接访问内存,加快了程序的效率。共享内存没有提供同步机制,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以通常需要用其他的机制来同步对共享内存的访问,例如信号量。
共享内存的通信原理
在Linux中,每个进程都有属于自己的进程控制块(PCB) 和地址空间,并且都有一个与之对应的页表。共享内存采用引用计数原理,当进程脱离共享存储区后计数器减一,连接时计数器加一,只有当计数器变为零时才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。

五、信号量

共享内存存在一个问题是如果多个进程同时修改同⼀个共享内存,就冲突了。例如两个进程都同时写⼀个地址,先写的那个进程会发现内容被别⼈覆盖了。 为了防⽌多进程竞争共享资源⽽造成的数据错乱,需要保护机制,使得共享的资源在任意时刻只能被⼀个进程访问。信号量就实现了这⼀保护机制。

信号量是一种用来表示资源使用情况的数据结构,主要作为进程间以及同一进程不同线程之间的同步手段。 其值由P,V操作改变。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,就把信号量的值设为 0,然后进程b访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这时进程 b 就访问不了内存1。所以信号量也是进程之间的一种通信方式。

PV操作

P操作wait,P原语操作的动作是:
(1)S减1;
(2)若S减1后仍大于或等于零,则进程继续执行;
(3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转进程调度。
wait(S){
    while (S<=0);
    S=S-1;
}

wait操作中,只要信号量S<=0,就会不断地测试。因此,该机制使进程处于“忙等”的状态。
V操作signal,V原语操作的动作是:
(1)S加1;
(2)若相加结果大于零,则进程继续执行;
(3)若相加结果小于或等于零,则转进程调度。
PV操作对于每一个进程来说,都只能进行一次,而且必须成对使用。在PV原语执行期间不允许有中断的发生。
signal(S){
    S=S+1;
}
每个程序中用户实现互斥的P、V操作必须成对出现,先做P操作,进临界区,后做V操作,出临界区。若有多个分支,要检查其成对性。P、V操作应分别紧靠临界区的头尾部,临界区的代码应尽可能短,不能有死循环。互斥信号量的初值一般为1。

解决互斥同步问题

1.生产者消费者问题

生产者一消费者问题指若干进程通过有限的共享缓冲区交换数据时的缓冲区资源使用问题。假设“生产者”进程不断向共享缓冲区写入数据(即生产数据),而“消费者”进程不断从共享缓冲区读出数据(即消费数据);共享缓冲区共有n个;任何时刻只能有一个进程可对共享缓冲区进行操作。所有生产者和消费者之间要协调,以完成对共享缓冲区的操作。

可把共享缓冲区中的n个缓冲块视为共享资源,生产者写入数据的缓冲块成为消费者可用资源,而消费者读出数据后的缓冲块成为生产者的可用资源。可设置三个信号量:full、empty和mutex。其中full表示有数据的缓冲块数目,初值是0;empty表示空的缓冲块数初值是n;mutex用于访问缓冲区时的互斥,初值是1。

2.读者写者问题

有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出。

读者和写者互斥,写者和写者也互斥,而读者和读者不存在互斥问题。
读者和写者两个进程,写者是比较简单的,它和任何进程互斥,用互斥信号量的P操作、V操作即可。读者的问题比较复杂,它与写者互斥的同时还要实现与其他读者的同步。在这里用到了一个计数器来判断当前是否有读者读文件。当有读者的时候写者无法写文件,此时读者会一直占用文件,当没有读者的时候写者才可以写文件。
首先设置信号量count为计数器,用来记录当前读者数量,初值为0; 设置mutex为互斥信号量,用于保护更新count变量时的互斥;设置互斥信号量rw用于保证读者和写者的互斥访问。

3.哲学家进餐问题

有五个哲学家,他们的生活方式是交替地进行思考和进餐。他们共用一张圆桌,分别坐在五张椅子上。在圆桌上有五个碗和五支筷子,平时一个哲学家进行思考,饥饿时便试图取用其左、右最靠近他的筷子,只有在他拿到两支筷子时才能进餐。进餐完毕放下筷子又继续思考。哲学家进餐问题可看作是并发进程并发执行时处理共享资源的一个有代表性的问题。

六、socket

进程间通信首选 Sockets,其最大的好处在于可以跨主机.。相反,前面列出的其他 IPC 都不能跨机器(比如共享内存效率最高,但不能高效地共享两台机器的内存)。在编程上,TCP sockets 和 pipe 都是一个文件描述符,用来收发字节流,都可以read/write/fcntl/select/poll 等。不同的是TCP 是双向的,pipe 是单向的,进程间双向通讯还得开两个文件描述符,不方便;而且进程要有父子关系才能用 pipe,这些都限制了 pipe 的使用。在收发字节流这一通讯模型下没有比sockets/TCP 更自然的 IPC了。pipe 也有一个经典应用场景,那就是写Reactor/Selector 时用来异步唤醒 select(或等价的 poll/epoll) 调用(Sun JVM 在 Linux 就是这么做的)。
TCP port 是由一个进程独占,且操作系统会自动回收(listening port 和已建立连接的TCP socket 都是文件描述符,在进程结束时操作系统会关闭所有文件描述符)。所以程序意外退出也不会给系统留下垃圾,程序重启之后能比较容易地恢复,而不需要重启操作系统(用跨进程的 mutex 就有这个风险)。还有既然 port 是独占的,那么可以防止程序重复启动(后面那个进程抢不到 port就没法工作了),造成意料之外的结果。
两个进程通过 TCP 通信,如果一个崩溃了,操作系统会关闭连接,这样另一个进程几乎立刻就能发现。与其他 IPC 相比,TCP 协议的一个好处是“可记录可重现”,tcpdump/Wireshark是解决两个进程间协议/状态争端的好帮手。
如果网络库带“连接重试”功能的话,可以不要求系统里的进程以特定的顺序启动,任何一个进程都能单独重启,这对开发牢靠的分布式系统意义重大。使用 TCP 这种字节流方式通信,会有 marshal/unmarshal 的开销,这要求选用合适的消息格式,准确地说是wire format(Google Protocol Buffers)。
TCP 是字节流协议,只能顺序读取,有写缓冲;共享内存是消息协议,a 进程填好一块内存让 b 进程来读,基本是“停等”方式。TCP本身是个数据流协议, 除了直接使用它来通信 ,还可以在此 之上构建RPC/REST/SOAP之类的上层通信协议。除了点对点的通信之外,应用级的广播协议也是非常有用的,可以方便地构建可观可控的分布式系统。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值