进程间通信IPC
进程间通信在面试过程中出现过多次,并且是融合了内存,内核,文件系统等多方面的知识,是个很好的学习案例
1. 概念
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)
2. IPC的分类起源
2.1. 传统的Unix进程间通信方式
- 管道(pipe)
- 有名管道(fifo)
- 信号(signal)
2.2. System V IPC对象
- 共享内存(share memory)
- 消息队列(message queue)
- 信号量(semaphore)
2.3. BSD
- 套接字(socket)
2.4. Posix IPC对象
- 共享内存(share memory)
- 消息队列(message queue)
- 信号量(semaphore)
与之前System V相比,只是因为出现了一个新的Unix标准,基本理念是相同的。
3. 匿名管道(pipe)
特性:
- 半双工通信
- 只能作用于父子进程或者兄弟进程之间
- 保存于内存中,对管道两端的进程而言,是在操作文件,但是实际上不属于某种文件系统,自成一体。
- 数据流动为先进先出,写入数据在缓冲区末尾,读取数据在缓冲区头部
- 传输的数据是无格式字节流,需要实现约定数据格式
实质:
管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,该缓冲区可以看做是一个循环队列,读和写的位置都是自动增长的,不能随意改变,一个数据只能被读一次,读出来以后该数据在缓冲区就不复存在了。
阻塞问题:
无名管道无需显示打开,创建时直接返回文件描述符,在读写时需要确定对方的存在,否则内核会发送SIGPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是使应用程序终止)。
如果当前进程向无名管道的一端写数据,必须确定另一端有某一进程。如果写入无名管道的数据超过其最大值,写操作将阻塞,如果管道中没有数据,读操作将阻塞,如果管道发现另一端断开,将默认自动退出。
4. 有名管道(FIFO)
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO)。
特性:
- 半双工通信
- 提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,但是仅仅存储名字,内存依旧存放于内存之中
- 即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信
- 该文件类型不支持文件内容的定位操作,严格按照先进先出的读取和写入方式,只能从头读或者从末尾写
阻塞问题:
有名管道在打开时需要确实对方的存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写,不会阻塞。
5. 信号
- 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
- 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,直到该进程恢复执行并传递给它为止。
- 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。
信号来源
信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:
- 硬件来源:用户按键输入
Ctrl+C
退出、硬件异常如无效的存储访问等。 - 软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。
信号生命周期和处理流程
- 信号被某个进程产生(也可以是内核由于某些事件直接产生),并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统;
- 操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。
- 目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。
6. 消息队列
特点:
- 消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.
- 消息队列允许一个或多个进程向它写入与读取消息.
- 管道和消息队列的通信数据都是先进先出的原则。
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。
- 消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
- 目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。
7. 共享内存
- 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
- 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
- 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。
最简单的理解就是因为进程使用的都是虚拟内存,所以只需要将一块虚拟地址映射到相同的物理地址,那么两个进程就可以直接读写内存而不用进行数据的拷贝
mmap共享内存
mmap将一个磁盘文件或一个POSIX共享内存区对象映射到调用进程的地址空间(虚地址空间)
POSIX共享内存区
POSIX提供了两种在无缘关系进程间共享内存区的方法:
内存映射文件:由open函数打开,由mmap函数把得到的描述符映射到当前进程地址空间中的一个文件。
共享内存区对象:指定 一个POSIX.1 IPC名字(或许为文件系统的一个路径名),由shm_open,以创建一个新的共享内存区对象或打开一个已存在的共享内存区对象,所返回的描述符由mmap函数将这个共享内存区映射到当前进程的地址空间
这两种技术都用到mmap,其差别在于作为mmap参数之一的描述符获得的手段:前者通过open、后者头通过shm_open。
System V共享内存区
- 由shmget函数创建一个新的共享内存区或访问一个已存在的共享内存区。该函数创建或打开一个共享内存区但是并没有给调用进程提供访问该内存区的手段。
- 调用shmat把它连接到调用进程的地址空间。
- 当一个进程完成某个共享内存区的使用时,可调用shmdt断开与这个内存区的链接。
- 但是上一步中并没有从系统删除其标示符以及其数据结构,直到某个进程调用shmctl(带命令IPC_RMID)特地删除它。
8. 信号量
信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。
其本质是一个计数器,比如信号量初始值为0,当进程1来访问内存a时,信号量将被置为1,这是当另一个进程2要来访问相同的内存a时,由于发现信号量为1,就知道已经有进程在访问内存a,因此会被互斥。所以某种意义上来说,信号量也是IPC的一种。
两种基本类型:
- 二值信号量:最简单的信号量形式,信号量的值只能取0或1,类似互斥锁。虽然二值信号量能够实现互斥锁的功能,但两者的关注内容不同。信号量强调共享资源,只要共享资源可用,其他进程同样可以修改信号量的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
- 计数信号量:信号量的值可以取任意非负值(当然受内核本身的约束),用来统计资源,其值就代表可用资源的个数。
为了获得共享资源,进程需要执行下列操作:
(1)创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。
(2)等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞。也称为P操作。
(3)挂出一个信号量:该操作将信号量的值加1,也称为V操作。
为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。
Linux环境中,有三种实现类型:
- Posix有名信号量(存放在文件系统,即一个内容为0或者1的文件)
- Posix基于内存的信号量(存放在共享内存区中)
- System V信号量,是一个或多个信号量的集合,它对应的是一个信号量结构体,这个结构体是为 System V IPC 服务的,信号量只不过是它的一部分。(在内核中维护)。
这三种信号量都可用于进程间或线程间的同步。
9. socket
这是一种通信机制,不同于前几种方式只能进行本机进程通信,套接字可用于不同机器之间的进程通信。套接字是我们熟知的TCP/IP的网络通信的基本操作单元,也可以把它理解成不同主机之间的进程进行双向通信的端点。
详细的我有写过原文,直接跳转阅读吧
参考
http://blog.chinaunix.net/uid-26833883-id-3227144.html