进程间通信
进程间通信(InterProcess Communication)
进程间通信的概念
- 每个进程各自有不同的用户地址空间, 任何一个进程的全局变量在另一个进程中都看不到, 所以进程之间要交换数据必须通过内核, 在内核中开辟一块缓冲区, 进程1把数据从用户空间拷贝到内核缓冲区中, 进程2再从内核缓冲区中把数据读走, 内核提供的这种机制称为进程间通信(IPC, InterProcess Communication)
进程间通信的7种方式
-
管道/匿名管道(pipe)
- 管道是半双工的, 数据只能向一个方向流动; 需要双方通信时, 需要建立起两个管道
- 只能用于父子进程或者兄弟进程之间的通信(具有亲缘关系的进程)
- 管道单独构成了一种独立的文件系统: 管道对于管道两端的进程而言, 就是一个文件, 但它不是普通的文件, 它不属于某种文件系统, 而是独立存在于内存中的
- 数据的读出和写入: 一个进程向管道中写的内容被管道另一端的进程读出; 写入的内容每次都添加在管道缓冲区的末尾, 并且每次都是从缓冲区的头部读出数据
- 管道的实质: 是一个内核缓冲区, 进程以先进先出的方式从缓冲区存取数据; 管道也可以看作是一个循环队列, 读写位置都是自动增长, 不能随意改变, 一个数据只能被读一次, 读出来后在缓冲区就不存在了
- 管道的缓冲区是有限的(在创建管道时, 为缓冲区分配一个页面大小4K左右)
- 管道传送的是无格式字节流, 这就要求管道的读写方必须事先约定好数据的格式, 比如多少字节算作一个消息
-
有名管道
- 有名管道的文件形式存在于文件系统中, 只要能够访问该路径, 进程就能够彼此进行通信, 有名管道的名字存在于文件系统中, 内容存放在内存中.
- 管道是特殊类型的文件, 在满足先入先出的原则条件下可以进行读写, 但不能进行定位读写
- 匿名管道是特殊类型的文件, 只能在有亲缘关系的进程间通信; 有名管道以磁盘文件的方式存在, 可以实现本机任意两个进程通信
- 无名管道阻塞问题, 写太大或者读时为空将阻塞, 另一端断开将自动退出
- 有名管道在打开时需要对方的存在, 否则将阻塞; 以读方式打开某管道, 必须有一个进程以写方式打开管道, 否则阻塞; 当前进程读,当前进程写, 不会阻塞.
-
信号(Signal)
- 信号是Linux系统用于进程间互相通信或者操作的一种机制, 信号可以在任何时候发给某一进程, 而无需知道该进程的状态
- 如果该进程当前并未处于执行状态, 该信号就由内核保存起来, 直到进程恢复执行并传递给它为止
- 如果一个信号被进程设置为阻塞, 则该信号的传递被延迟, 直到其阻塞被取消时才被传递给进程
- 信号是软件层次上对中断机制的一种模拟, 是一种异步通信方式, 信号可以在用户进程和内核之间直接交互, 内核可以利用信号来通知用户空间的发生了那些系统事件, 信号事件主要有两个来源:
- 硬件来源: 用户按键键入
ctrl+c
退出, 硬件异常如无效的内存访问 - 软件终止: 终止进程信号, 其他进程调用kill函数, 软件异常产生信号.
- 硬件来源: 用户按键键入
- 目的进程接收到信号后, 将根据当前进程对此信号设置预处理方式, 暂时终止当前代码的执行, 保护上下文(主要包括: 临时寄存器数据, 当前程序位置以及当前CPU的状态), 转而执行中断服务程序, 执行完成后在恢复中断的位置
- 对于抢占式内核, 在中断返回时将引发新的调度
-
消息队列(Message) system V消息队列和POSIX 消息队列
- 消息队列是存放在内核中的消息链表, 每个消息队列由消息队列标识符表示
- 与管道不同的是消息队列存在于内核中(无名管道存在于内存的文件中, 有名管道存在于实际的磁盘或者文件系统中), 只有在内核重启(操作系统重启)或者显示地删除消息队列, 该消息队列才会被真正的删除.
- 另外与管道不同的是, 消息队列在某一个进程向另一个队列写入消息之前, 并不需要另外某个进程在该队列上等待消息的到达
- 消息队列特点总结:
- 消息队列是消息的链表, 具有特定的格式, 存放在内核中, 并有消息队列标识符标识.
- 消息队列允许一个或多个进程向它写入与读取数据
- 管道和消息队列的通信数据都是先进先出的原则
- 消息队列可以实现消息的随机查询, 消息不一定要以先进先出的次序读取, 也可以按消息的类型进行读取, 比FIFO更具有优势
- 消息队列克服了信号承载信息量少, 管道只能承载无格式字节流以及缓冲区大小有限的问题
- 目前主要有两种类型的消息队列: POSIX消息队列以及System V消息队列; System V消息队列目前被大量使用, 系统V消息队列是随内核持续的, 只有在内核重启或人工删除时, 该消息队列才会被删除.
-
共享内存(share memory)
- 使得多个进程可以直接读写一块内存空间, 是最快的可用IPC形式, 是针对其他通信机制运行效率较低而设计的.
- 为了在多个进程间交换信息, 内核专门留出了一块内存区, 可以由需要访问的进程将其映射到自己的私有地址空间
- 进程就可以直接读写这一块内存而不需要进行数据的拷贝, 从而大大提高了效率
- 由于多个进程共享一段内存, 因此需要依靠某种同步机制来达到进程的同步及互斥
-
信号量(semaphore) -- PV原语
- 信号量是一个计数器, 用于多进程对共享数据的访问, 信号量的意图在于进程间同步
- 为了获得共享资源, 进程需要执行一些操作:
- 创建一个信号量: 调用者指定初始值(二值信号量通常是1)
- 等待一个信号量: 该操作会测试这个信号量的值, 如果小于0, 就阻塞, 也称为P操作.
- 挂出一个信号量: 该操作将信号量的值加1, 也称为V操作
- 信号量通常是在内核中实现:
- Posix有名信号量
- Posix基于内存的信号量(存放在共享内存区中)
- System V信号量(在内核中维护)
- 信号量与互斥量的区别:
- 互斥量用于线程的互斥, 信号量用于线程的同步; 互斥量和信号量本质区别是, 互斥和同步之间的区别.
- 互斥: 是指某一资源同时只允许一个访问者对齐进行访问, 具有唯一性和排它性; 但互斥无法限制访问者对资源访问的顺序, 即访问是无序的;
- 同步: 是指在互斥的基础上(大多数情况下), 通过其他机制实现访问者对哦资源的有序访问
- 大多数情况下, 同步已经实现了互斥, 特别是所有写入资源的情况必须是互斥的, 少数情况下是可以允许多个访问者同时访问资源.
-
套接字
- 套接字的特性由3个属性确定: 域, 端口号, 协议类型
- Linux下的同步机制
- 原子操作
- 信号量
- 读写信号量
- 自旋锁(spinlock)
死锁
- 死锁: 死锁是指两个或两个以上的进程(或线程)在执行过程中, 因争夺资源而造成的一种互相等待的现象, 若无外力作用, 他们将无法推进下去
- 因为系统资源不足
- 进程运行顺序不合适
- 资源分配不当等
- 会因为争夺有限资源陷入死锁: (死锁必要条件)
- 互斥条件: 一个资源每次只能被一个进程使用
- 请求与保持条件: 一个进程因请求资源而阻塞时, 对已获得的资源保持不放
- 不剥夺条件: 进程已获得的资源, 在未使用完之前, 不能强行剥夺
- 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系
- 进程A占有对象1的锁,进程B占有对象2的锁,进程A需要对象2的锁才能继续执行,所以进程A会等待进程B释放对象2的锁,而进程B需要对象1的锁才能继续执行,同样会等待进程A释放对象1的锁,由于这两个进程都不释放已占有的锁,所以导致他们进入无限等待中