进程间通信
进程间通信是多进程协作的基础,利用多进程协作,我们可以将功能模块化,增强模块间的隔离,以及提高容错能力。
进程间通信的一个重要功能就是在进程间传递数据,“消息”就是IPC中常用的数据传递方式。
- 消息一般包含一个头部和数据体,头部中存有元信息(例如 Magic Number,校验码,消息长度等),数据体里可以是字符串,或者系统资源信息(例如文件描述符等)
消息的传递一般需要一个中间人,一般可分为两种形式:
- 直接用共享内存进行消息传递,操作系统在通信过程中不干预数据传输。即操作系统只是在需要通信的进程间的各自的虚拟内存中映射了同一段物理内存。他的好处在于性能强,因为少去了内核的干预导致的从用户态到核心态的一些复制。
- 操作系统辅助消息传递。由操作系统支持了更复杂的消息协议(共享内存需要用户自己封装),并且保证了安全性与通用性。
经典的内存通信方式由管道、消息队列、信号量,共享内存、信号机制,以及套接字。接下来主要介绍由操作系统辅助的通信。
管道
管道是两个进程间的一条通道,一端负责投递,一端负责接收。所以如果要实现双向通信,需要两个管道。
管道的通信消息是字节流,所以需要应用自己去解析消息数据,
具体实现上,Unix系统会把管道当作一个文件,内核为用户态提供管道的文件描述符。不过实际上,管道不会使用存储设备,而是使用内存作为一个数据的缓冲区
命名管道和匿名管道
命名管道和匿名管道的区别在于创建方式。匿名管道通过pipe系统调用创建。在创建时进程只会拿到两个文件描述符,所以通常情况下,只是结合fork使用,也就是父进程与子进程间的通信
命名管道通过mkfifo命令创建。在创建时会指定一个全局文件名。这样就适合任意进程间的通信
消息队列
消息队列支持多个发送者和接收者,发送和接收的接口是内核提供的。当创建消息队列时,内核将从系统内存中分配一个队列数据结构,作为消息队列的内核对象。消息队列以链表的形式管理,每个节点中,包含了数据以及类型标志,类型的具体意义需要用户态程序自己管理。
消息队列一般被抽象为4个操作:msgget(获取已有消息队列的连接,或者创建),msgsnd(发送消息),msgrcv(接收消息),msgctl(管理消息队列)。可以设置发送和接收消息是否为阻塞的。
消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
信号量
信号量在实际中主要作用是进程间的同步。管道,消息队列等可以传递数据但是不提供强制同步机制。
信号量一般来说只有一个共享的整型计数器,由内核维护,对信号量的操作需要经过内核的系统调用。
对信号量的主要操作是P,V原语。这两个操作是原子的
- 某进程需要使用资源时,通过P原语申请
- 进程使用完资源后,通过V原语释放
通过信号量,可以实现进程间的互斥以及同步
-
互斥:是指某一资源同时只允许一个访问者对其进行访问(这样的资源就叫临界区),具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
-
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。也就是让本来异步并发的进程相互配合,有序推进。
如果要实现互斥,可设信号量mutex,初始值为1,在进入临界区之前执行P(mutex),在临界区之后执行V(mutex),PV操作必须成对出现。这类似于加锁,而锁只有一把,实现了互斥性。
如果要实现同步,可设信号量Semaphore s= 0 。假设要求A步骤在前,B步骤在后,则可在A操作后执行V(s), 在B操作前执行P(s)。P,V操作由不同的进程分别执行,这样可保证A,B操作的顺序性。至于原因,可以通过上述P,V操作具体实现进行推导易得。
信号
信号提供了单向的事件通知能力。信号量也有通知能力,但是需要进程主动去查询计数器状态或者陷入阻塞。使用信号,一个进程可以随时发送一个事件到特定的进程、线程或者进程组,并且接收信号的进程不需要阻塞,内核会帮助切换到其对应的信号处理函数,并在处理完之后恢复之前的上下文。
Linux内核会为每个进程和线程准备一个信号事件等待队列。一个进程内的线程共享进程的等待队列,并且有自己私有的队列。
Linux还提供了系统调用sigprocmask,允许用户程序设置对特定信号的阻塞状态。当一个信号被阻塞,Linux将不会触发这个信号对应的处理函数,直到该信号被解除阻塞、(但是有很多信号是不能被阻塞的,例如用户使用crtl+c 杀死一个进程)。
信号被处理的时机通常是内存执行完异常、中断、系统调用等返回用户态的时刻。这时,内核会检查一个状态来判断是否有信号需要处理。
- 如果用户注册了信号处理函数,则调用
- 如果用户没有注册,则调用内核处理函数(一般来说是直接杀死进程或者忽略信号)
特别注意,信号处理函数需要是可重入的,用户自己实现时需要注意。
- 信号处理函数是可以嵌套调用的
- 如果在调用处理函数时,拿到了全局锁,又接到了相同的信号,不可重入的函数会造成死锁
套接字Socket
套接字是又可以用于本地,又可以用于网络的通信机制。在本地通信时,一般利用本地回环IP(127.0.0.1),不同的进程绑定不同的端口。
套接字可以利用不同的协议,例如:
- 可靠传输的TCP,由TCP协议负责数据的完整性和顺序性,从而保证可靠性
- 不可靠传输UDP,不能保证数据的完整性但是有较快的速度
- 甚至原始套接字,原始套接字允许对较低层次的协议直接访问,比如IP、 ICMP比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。