进程间通信
- 一、管道
- 创建管道
- 父子进程的管道单向通信
- 父子间的双向通信管道
- Shell中的管道通信
- 匿名管道与命名管道
- 管道特点
- 二、消息队列
- 不足
- 三、共享内存
- 四、信号量
- 五、信号
- 六、Socket
- 创建Socket的系统调用
- 通信方式
- TCP协议通信的Socket编程模型
- UDP协议通信的Socket编程模型
- 本地进程间通信的Socket编程模型
进程是相互独立的,他们之间的通信只能通过内核。
一、管道
创建管道
匿名管道的创建,需要通过下这个系统调用:
这里表示创建⼀个匿名管道,并返回了两个描述符,
- ⼀个是管道的读取端描述符 fd[0]
- ⼀个是管道的写入端描述符 fd[1] 。
这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中。
其实,所谓的管道,就是内核里面的⼀串缓存。从管道的⼀段写入的数据,实际上是缓存在内核中的,另⼀端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。
父子进程的管道单向通信
使用fork创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个fd[0]fd[1],两个进程就可以通过各自的 fd 写入和读取同⼀个管道文件实现跨进程通信了。
父子间的双向通信管道
如果需要双向通信,则应该创建两个管道。
Shell中的管道通信
在 shell里面执行A | B 命令的时候,A 进程和 B 进程都是 shell 创建出来的子进程,A 和 B 之间不存在父子关系,它俩的父进程都是 shell。
匿名管道与命名管道
- 匿名管道:通信范围存在父子关系的进程。因为它没有管道文件,只能通过fork复制父进程的fd文件描述符。
- 命名管道:对于命名管道,它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了⼀个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。
管道特点
- 管道传输数据是单向的,如果想相互通信,我们需要创建两个管道。
- 管道这种通信方式效率低,不适合进程间频繁地交换数据。
- 简单,同时也我们很容易得知管道⾥的数据已经被另⼀个进程读取了。
二、消息队列
消息队列是保存在内核中的消息链表,在发送数据时,会分成⼀个⼀个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型, 所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。 消息队列⽣命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会⼀直存在,二而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。消息这种模型,两个进程之间的通信就像平时发邮件⼀样,你来⼀封,我回⼀封,可以频繁沟通了
不足
- 通信不及时
- 附件有大小限制
三、共享内存
共享内存的机制,就是拿出⼀块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西, 另外⼀个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。
四、信号量
为了防止进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只 能被⼀个进程访问。正好,信号量就实现了这⼀保护机制。
信号量其实是⼀个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
信号量表示资源的数量,控制信号量的方式有两种原子操作
- P 操作,这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行
- V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程
P 操作是用在进⼊共享资源之前,V 操作是⽤在离开共享资源之后,这两个操作是必须成对出现的。
- 信号初始化为 1 ,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有⼀个进程在访问,这就很好的保护了共享内存
- 信号初始化为 0 ,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行
五、信号
上面说的进程通信,是常规模式状态下的工作模式。对于异常情况下的工作模式,就需要用信号的方式来通知进程。
信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。 信号是进程间通信机制中唯⼀的异步通信机制,因为可以在任何时候发送信号给某⼀进程,⼀旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式
- 执行默认操作,Linux 对每种信号都规定了默认操作
- 捕捉信号,我们可以为信号定义⼀个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
- 忽略信号,当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP ,它们用于在任何时候中断或结束某⼀进程。
六、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:原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成,protocol目前⼀般写成 0 即可
通信方式
根据创建 socket 类型的不同,通信的方式也就不同
- 实现 TCP 字节流通信: socket 类型是AF_INET和SOCK_STREAM
- 实现 UDP 数据报通信:socket 类型是AF_INET 和 SOCK_DGRAM
- 实现本地进程间通信: 本地字节流 socket类型是 AF_LOCAL和 SOCK_STREAM,本地数据报 socket 类型是 AF_LOCAL 和SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等价的,所以 AF_UNIX 也属于本地 socket
TCP协议通信的Socket编程模型
- 服务端和客户端初始化 socket ,得到文件描述符
- 服务端调用bind ,将绑定在 IP 地址和端口
- 服务端调用listen ,进行监听
- 服务端调用accept ,等待客户端连接
- 客户端调用connect ,向服务器端的地址和端⼝发起连接请求
- 服务端accept返回用于传输的socket的文件描述符
- 客户端调用write写入数据
- 服务端调用read 读取数据
- 客户端断开连接时,会调用close ,那么服务端read读取数据的时候,就会读取到了EOF ,待处理完数据后,服务端调调用 close ,表示连接关闭。
UDP协议通信的Socket编程模型
UDP是没有连接的,所以不需要三次握⼿,也就不需要像 TCP调用listen和connect,但是UDP的交互仍然需要 IP 地址和端口号,因此也需要 bind。对于UDP 来说,不需要维护连接,那么也就没有所谓的发送方和接收方,甚⾄都不存在客户端和服务端的概念,只要有⼀个 socket多台机器就可以任意通信,因此每⼀个UDP 的 socket 都需要 bind。另外,每次通信时,调用sendto和recvfrom,都要传入目标主机的 IP地址和端口。
本地进程间通信的Socket编程模型
本地 socket 被用于在同⼀台主机上进程间通信的场景:
- 本地socket的编接口和 IPv4 、IPv6 套接字编程接口是⼀致的,可以支持字节流和数据报」两种协议
- 本地 socket 的实现效率大高于 IPv4 和 IPv6 的字节流、数据报 socket 实现
- 对于本地字节流 socket,其 socket 类型是 AF_LOCAL 和 SOCK_STREAM。 对于本地数据报 socket,其socket 类型是 AF_LOCAL 和 SOCK_DGRAM。 本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定⼀个本地文件,这也就是它们之间的最大区别。