`
文章目录
- 进程间的通行方式
- 一、管道模型
- 二、消息队列模型
- 三、共享内存
- 四 信号量机制
- 五、socket
进程间的通行方式
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)。
不同的进程之间是不可以之间进行访问的,但所属的线程可以访问进程中的数据,如何进行进程之间的互相访问?其对应的方式有以下几种。
一、管道模型
管道模型是linux命令行中常用的模式。
管道模型分为两类,一类是匿名管道,另一类是命名管道。
linxu命令行cat xx.txt | grep -n ‘xxx’ 这个’|'可以看作一个单向的匿名管道,使得前后两个进程之间进行通信,前一个命名的输出作为后一个命令的输入,该管道使用结束则立即销毁。命名管道在linux中以文件的形式存在,只要访问该文件就可以实现任意两个进程间的通信,命名管道可以看作是是硬盘上存在的设备文件,所以打开需要使用open(linxu中一切皆文件原则)。
管道本质上就是内核中的一个缓存,当进程创建一个管道后,Linux会返回两个文件描述符,一个是写入端的描述符,一个是输出端的描述符,可以通过这两个描述符往管道写入或者读取数据。如果想要实现两个进程通过管道来通信,则需要让创建管道的进程fork子进程,这样子进程们就拥有了父进程的文件描述符,这样子进程之间也就有了对同一管道的操作(管道应用于父子进程之间的通信)。
局限性:
半双工通信,一条管道只能一个进程写,一个进程读。
一个进程写完后,另一个进程才能读,反之同理。
管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等
管道是最简单的通信方式,也是效率最差的一种,不适合进程间频繁的交换数据。
二、消息队列模型
消息队列的通信方式可以解决进程间频繁的交换数据。
消息队列模型于在程序中使用的队列是差不多的,不同之处在于队列是同一个进程往队列里放东西拿东西。消息队列模型多应用于多线程生产者消费者模式,一端线程生产数据放入消息队列中,一端进程判断消息队列中有数据便拿出数据去处理。
A进程往消息队列写入数据后就可以正常返回,B进程需要时再去读取就可以了,效率比较高。
而且,数据会被分为一个一个的数据单元,称为消息体,消息发送方和接收方约定好消息体的数据类型,不像管道是无格式的字节流类型,这样的好处是可以边发送边接收,而不需要等待完整的数据。
但是同样也有缺点,每个消息体有一个最大长度的限制,并且队列所包含消息体的总长度也是有上限的,这是其中一个不足之处。
另一个缺点是消息队列通信过程中存在用户态和内核态之间的数据拷贝问题。进程往消息队列写入数据时,会发送用户态拷贝数据到内核态的过程,同理读取数据时会发生从内核态到用户态拷贝数据的过程。
三、共享内存
共享内存解决了消息队列存在的内核态和用户态之间数据拷贝的问题。
传输一些大文件,如果采用管道或者消息队列传输大文件,涉及到重复拷贝,比较消耗性能。
因此我们开辟一块存储空间,让所有的进程都可以访问到这片存储空间,就像线程访问一样,这便是共享内存技术。
共享内存技术模拟多线程,在内存中开辟一块特殊的内存用于多个进程共享访问。也是进程间最高效的通信方式
现代操作系统对于内存管理采用的是虚拟内存技术,也就是说每个进程都有自己的虚拟内存空间,虚拟内存映射到真实的物理内存。共享内存的机制就是,不同的进程拿出一块虚拟内存空间,映射到相同的物理内存空间。这样一个进程写入的东西,另一个进程马上就能够看到,不需要进行拷贝。
共享内存技术的特别是速度快。因为它减少了很多的不必要的访问和传输。但缺点是如果很多进程对同一片存储空间进行读写,会出现一些数据乱行和脏数据问题。因此共享内存通常适合信号量机制一起使用的。
四 信号量机制
信号和信号量是两种不同的概念。
信号量可以看作一种数据操作锁,通过对临界资源的控制访问以管理进程之间的通信,PV原语操作。
临界资源:同一时间只能被同一进程访问,其他申请访问的进程只能等这一进程访问完之后才能进行访问。
信号量本质上是一个整型的计数器,用于实现进程间的互斥和同步
信号量代表着资源的数量,操作信号量的方式有两种:
P操作:这个操作会将信号量减一,相减后信号量如果小于0,则表示资源已经被占用了,进程需要阻塞等待;如果大于等于0,则说明还有资源可用,进程可以正常执行。
V操作:这个操作会将信号量加一,相加后信号量如果小于等于0,则表明当前有进程阻塞,于是会将该进程唤醒;如果大于0,则表示当前没有阻塞的进程。
信号量实现互斥:
信号量初始化为1
进程 A 在访问共享内存前,先执行了 P 操作,由于信号量的初始值为 1,故在进程 A 执行 P 操作后信号量变为 0,表示共享资源可用,于是进程 A 就可以访问共享内存。
若此时,进程 B 也想访问共享内存,执行了 P 操作,结果信号量变为了 -1,这就意味着临界资源已被占用,因此进程 B 被阻塞。
直到进程 A 访问完共享内存,才会执行 V 操作,使得信号量恢复为 0,接着就会唤醒阻塞中的线程 B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执行 V 操作,使信号量恢复到初始值 1。
信号量实现同步:
由于多线程下各线程的执行顺序是无法预料的,有些时候我们希望多个线程之间能够密切合作,这时候就需要考虑线程的同步问题。
信号量初始化为0
如果进程 B 比进程 A 先执行了,那么执行到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表示进程 A 还没生产数据,于是进程 B 就阻塞等待;
接着,当进程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
最后,进程 B 被唤醒后,意味着进程 A 已经生产了数据,于是进程 B 就可以正常读取数据了。
信号:
在Linux中,为了响应各种事件,提供了几十种信号,可以通过kill -l命令查看。
如果是运行在shell终端的进程,可以通过键盘组合键来给进程发送信号,例如使用Ctrl+C产生SIGINT信号,表示终止进程。
如果是运行在后台的进程,可以通过命令来给进程发送信号,例如使用kill -9 PID产生SIGKILL信号,表示立即结束进程。
五、socket
与前面几种进程建通信方式不同,它们都是在同一台主机上进行进程间通信,如果想要跨网络和不同主机上的进程进行通信,则需要用到socket(套接字)。套接字用于多个进程之间的网络传输。可以是单机多进程也可以是不同机器上的多进程通信。打开的socket在linux下也是以文件描述符fd存在。服务端创建套接字,绑定ip端口,监听端口号,等待客户端调用,客户端创建之后与服务端TCP三次握手建立连接完成,双方就可以发送和接收数据。
- 服务端和客户端初始化Socket,得到文件描述符
- 服务端调用bind,绑定IP和端口
- 服务端调用listen,进行监听
- 服务端调用accept,等待客户端连接
- 客户端调用connect,向服务端发起连接请求。(TCP三次握手)
- 服务端调用accept返回用于传输的Socket的文件描述符(和第一点得到的Socket不同)
- 客户端使用write写入数据,服务端调用read读取数据
- 客户端断开连接时会调用close,服务端也会调用close(TCP四次挥手)
这里要注意的是,调用accept,连接成功得到的Socket是用来传输数据的,而第一次初始化Socket是用来监听的,是两个不同作用的Socket。