操作系统核心原理-3.进程原理(下):进程通信
进程作为人类的发明,自然也免不了脱离人类的习性,也有通信的需求。如果进程之间不进行任何通信,那么进程所能完成的任务就要大打折扣。人类的通信方式无外乎对白(通过声音沟通)、打手势、写信、发电报、拥抱等方法。同理,进程也可以通过同样的方式来进行通信。本篇我们就来看看进程的这些交互方式。
一、进程对白:管道、套接字
人们最常用的通信手段就是对白,一方发出声音,另一方接收声音。而声音的传递需要通过一些介质,例如:空气(face to face)、线缆(有线电话)等。类似的,进程对白就是一个进程发出某种数据信息,另外一方接收数据信息,而这些数据信息通过一片共享的存储空间进行传递。
1.1 管道
一个进程向存储空间的一端写入信息,另一个进程存储空间的另外一端读取信息,这个就是管道。就像两个人对白的媒介是空气也可以是线缆一样,管道所占的空间既可以是内存也可以是磁盘。
要创建一个管道,一个进程只需要调用管道创建的系统调用即可,该系统调用所做的事情就是在某种存储介质上划出一片空间,赋给其中一个进程写的权利,另一个进程读的权利即可。
例如在Linux下,我们通过Shell命令输入两个命令,中间通过符号“|”来创建两个命令之间的管道:
$ sort < file1 | grep zou
上面一个命令表示:对file1的内容首先进行排序,排序完成后的结果将作为grep的输入,在结果里面找出所有包括字符串zou的文本行。也就是说,在两个任务“排序“(sort)和”查找”(grep)之间创建了一个管道,数据从sort流向了grep。
1.2 套接字
套接字(Socket)的功能非常强大,可以支持不同层面、不同应用、跨网络的通信。使用套接字进行通信需要双方均创建一个套接字,其中一方作为服务器方,另外一方作为客户方。服务器方必须首先创建一个服务区套接字,然后在该套接字上进行监听,等待远方的连接请求。客户方也要创建一个套接字,然后向服务器方发送连接请求。服务器套接字在受到连接请求之后,将在服务器方机器上新建一个客户套接字,与远方的客户方套接字形成点到点的通信通道。之后,客户方和服务器方便可以直接通过类似于send和recv的命令在这个创建的套接字管道上进行交流了。
例如,在C#中我们可以轻松地创建一个服务器方的Socket:
// 创建Socket->绑定IP与端口->设置监听队列的长度->开启监听连接 socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socketWatch.Bind(new IPEndPoint(IPAddress.Parse(txtIPAddress.Text), int.Parse(txtPort.Text))); socketWatch.Listen(10);
1.3 不足之处
(1)必须首先在通信的进程间建立连接(管道或套接字),这需要消耗系统资源;
(2)通信是自愿的,而管道和套接字需要强制双方进行通信;
(3)由于建立连接需要消耗时间,一旦建立就应该尽可能多的通信,如果通信信息量很小,则就是“杀鸡用牛刀”了;
二、进程电报与旗语:信号与信号量
2.1 电报:信号
信号类似于我们生活中的电报,如果你想给某人发一封电报,就拟好电文,然后将报文和收报人的信息都交给电报公司。电报公司则将电报发送到收报人所在地的邮局,并通知收报人来取电报。其中,发报文时无需收报人实现知道,也无需进行任何协调。如果对方选择不对信号做出响应,则将被OS终止运行。
在计算机中,信号就是一个内核对象或者是一个内核数据结构。发送方将该数据结构的内容填好,并指明该信号的目标进程后,发出特定的软件中断(这就是一个发电报的操作)。OS接收到特定的中断请求后,知道是有进程要发送信号,于是到特定的内核数据结构里查找信号接收方,并进行通知。接到通知的进程则对信号进行相应处理。
2.2 旗语:信号量
信号量来源于铁路的运行:在一条单轨铁路上,任何时候只允许有一列火车行驶在该铁路上,而管理这条铁路的系统就是信号量。任何一列火车必须等到表明该铁路可以行驶的信号后才能进入轨道。当列车进入后,需要将信号改为禁止状态进入来防止别的列车同时进入。而当列车驶出单轨后,则需要将信号变回允许进入状态,这很像以前的旗语。当然,通过联想到我们实际开发中经常用的锁,这就更容易理解了。
在计算机中,信号量实际上就是一个简单整数。一个进程在信号变为0或1的情况下推进,并将信号变为1或0来防止别的进程同时推进。当该进程完成任务后,则将信号再改为0或1,从而允许其他进程执行。从而我们也可以看出,信号量已经不只是一种通信机制,更是一种同步机制。
三、进程拥抱:共享内存
前面通过对话、发电报、旗语已经满足了多种通信需要,但是当两个进程要共享大量数据时就没法十分满足需求。就如同两个坠入爱河的骚年,它们互相喜欢并想要在一起同居(共享大量数据),这时打电话、发电报、握手对话就显得不够了。这时候,它们需要的就是拥抱,只有紧紧拥抱才能尽可能地共享,feeling so good!
3.1 共享内存
进程的拥抱就是共享内存,两个进程共同拥有同一片内存。对于这片内存中的任何内容,二者均可以访问。要使用共享内存进行通信,进程A首先需要创建一片内存空间作为通信用,而其他进程B则将片内存映射到自己的(虚拟)地址空间。这样,进程A读写自己地址空间中对应共享内存的区域时,就是在和进程B进行通信。
3.2 不足之处
(1)使用共享内存机制通信的两个进程必须在同一台物理机上;
(2)安全性脆弱,假如一个进程有病毒,会很容易传给另外一个进程;
四、信件发送:消息队列
消息队列是一列具有头和尾的消息排列,新来的消息放在队列尾部,而读取消息则从队列头部开始,如下图所示:
这样看来,它和管道十分类似,一头读,一头写?的确,看起来很像管道,但又不是管道:
(1)消息队列无固定的读写进程,任何进程都可以读写;而管道需要指定谁读和谁写;
(2)消息队列可以同时支持多个进程,多个进程可以读写消息队列;即所谓的多对多,而管道是点对点;
(3)消息队列只在内存中实现,而管道还可以在磁盘上实现;
参考资料
邹恒明,《操作系统之哲学原理》,机械工业出版社