进程间的通信方式
重点1:进程间的通信方式
答:包含有六种、管道、消息队列、共享内存、信号量、信号、socket。
方式1:管道
管道可以分为匿名管道和有名管道。在管道中传输数据是==单向==的。
- 匿名管道(PiPe):只能用于有血缘关系的进程之间。例如,采用
fork
函数创建的父子进程。
案例说明:例如在shell中使用|代表左边的输出作为|右边的进程的输入。
1 | ps auxf | grep mysql |
在代码中创建的方法:
1 | int pipe(int fd[2]);// fd[0]代表读 fd[1]代表写 |
匿名管道作为特殊的文件,只存在于内存,不存在文件系统中。

两个描述符都存在于同一个进程中,因此使用fork
创建子进程时,创建的子进程会复制父进程的文件描述符。这样就使得两个进程各自有两个fd[0]和fd[1]了。

- 有名管道(FIFO):可以用于不存在血缘关系的进程之间。
创建有名管道的方法:mkfifo
,例如
1 | mkfifo myPipe |
代码中实现的函数:
1 | int mkfifo(const char* pathname,mode_t mode) |
==注意:==
管道其实本质是内核中的一个缓冲区,因此数据本质是通过内核在管道中流动。
管道的缺点:管道之间的通信效率较低,不适合在进程之间频繁地交换数据。
==生命周期:随进程的创建而建立,随进程的结束而销毁。==
方式2:消息队列
管道的通信方式缺点在于通信效率较低,不适合在进程之间频繁地交换数据。==消息队列==的通信模式在于,A进程要给B进程发送消息,A进程把数据放在对应的消息队列,B进程需要的时候去消息队列读取数据就可以了。同理B进程要给A进程发送消息也是一样的。
==消息队列是保存在内核中的消息链表。==在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的数据块,不像管道是无格式的字节流数据块。
生命周期:==随内核==,如果没有释放消息队列或没有关闭操作系统,消息队列会一直存在。
优点:
- 相比于管道通信方式,进程之间可以频繁的交换数据。
缺点:
通信不及时。因为消息体是存储在对应的消息队列,从而有一定的延迟。
附件的大小也有限制。不适合较大数据的传输,例如消息的最大长度(MSGMAX)和队列的最大长度(MAXMNB);
消息队列通信过程中,存在用户态和内核态之间的数据拷贝开销。
方式3:共享内存
实现思想:现代操作系统的内存管理,采用的是虚拟内存方式,每个内存都有独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存。==而共享内存的机制,就是将虚拟的地址空间映射到相同的物理内存中。==实现机制如下所示:

优点:
- 避免了消息队列中存在的内核态和用户态的拷贝开销问题。
缺点:
- 进程之间对于同一块物理内存操作时,存在的同步和异步问题。
方式4:信号量
解决问题:解决多进程竞争共享资源,导致的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。信号量就实现了这一保护机制。信号量本质是一个整形的计数器,==主要用于实现进程间的互斥和同步==,而不是用于缓存进程之间通信的数据。
实现方法:信号量表示资源的数量,控制信号量的方式有两种原子操作:
- 一个是P操作,该操作会将信号量减1,相减完如果信号量<0,**表示==**资源已经被占用**==,进程需要==**阻塞等待**==。相减后如果==**信号量>=0==,则表明还有资源可用,进程可以正常执行。
- 一个是V操作,该操作会把信号量加1,相加后如果信号量<=0**,则表明当前**有阻塞的进程**,于是会将该进程唤醒运行;相加后如果**信号量>0,则表明当前没有阻塞的进程。
案例1:实现两个进程==互斥==
将信号量设置为1,进程1先进行P操作访问共享内存,此时信号量为0表示资源可用,假设此时进程2进行P操作减1,为-1,处于阻塞状态,直到进程1访问完毕执行V操作,使得进程2才可以访问共享内存。
1 | s=1; // 初始信号量为0 |
案例2:实现两个进程==同步==
1 | s=0; // 初始信号量为0 |
方式5:信号(==唯一的异步通信机制==)
以上介绍的是常规状态下的工作模式。对于异常情况下的工作模式,就需要用信号的方式来通知进程。Linux中支持的信号有如下:
1 | kill -l |
注:查看所有信号的方式:kill -l
产生信号的方式:
- 硬件来源:
ctrl+c
产生SIGINT
信号,表示==终止==该进程;ctrl+z
产生SIGTSTP
信号,表示停止该进程,但还未结束。 - 软件来源:
kill -SINGNAL PID
处理信号的方式:
信号是进程间通信机制中==唯一的异步通信机制==,因为可以在任何时候发送信号给某一进程,一旦有信号产生,用户处理信号的方式有以下几种:
1、执行默认操作。Linux中针对每个信号都规定了默认的操作,例如,SIGTERM
信号,表示对进程终止的意思。
2、捕捉信号。为信号定义一个信号处理函数。信号发生时,执行默认的信号处理函数。
3、忽略信号。不希望处理某些信号时,可以忽略该信号,不做任何处理。
方式6:Socket
解决问题: ** 跨网络与不同主机之间的进程之间通信,就需要socket
通信了。(socket也可以用于同一主机**之间的不同进程之间通信)
使用方式:创建socket
的系统调用:int socket(int domain, int type, int protocal);
domain
:用于指示协议族,AF_INET用于IPV4,AF_INET6用于IPV6,AF_LOCAL/AF_UNIX用于本机。type
:用于指示通信特性,例如SOCK_STREAM表示字节流,对应于TCP;SOCK_DGRAM表示数据报,对应于UDP,SOCK_RAW表示的是原始套接字;protocal
:原本用于指示通信协议,但基本废弃,写为0即可。
类型:根据创建的socket类型不同,通信的方式也不同:
实现**
TCP
字节流**通信:socket
类型是AF_INET
和SOCK_STREAM
;1
2
3
4
5
6
7
8
9
10
11
12// 在服务器段需要
socket
bind
listen
accept
read
// 客户端
socket
bind
connect
write // 当客户端完成数据传输使会调用close,此时服务端read时会读到EOF,待处理完数据之后会调用close表示连接关闭。实现**
UDP
数据包**通信:socket
类型是AF_INET
和SOCK_DGRAM
;建立UDP通信过程不需要三次握手,也就不需要
listen
和connect
,但是UDP交互过程需要IP和端口号,因此需要bind
。对于UDP无需维护连接,因此没有发送方和接收方,只需要一个socket
多台机器之间就可以相互通信。每次通信的时候使用sendto
和recvfrom
都需要传入目标主机的IP地址和端口。实现本地进程间通信:【本地字节流】
AF_LOCAL
和SOCK_STREAM
;【本地数据报】AF_LOCAL
和SOCK_DGRAM
。另外AF_UNIX
和AF_LOCAL
是等价的,所以AF_UNIX
也属于本地socket
;和TCP、UDP最大的区别在于本地字节流
socket
和本地数据报socket
在bind
的时候不要绑定IP地址和端口,==而是绑定一个本地文件。==
。