高级通信机制可归结为三大类:共享存储器系统,消息传送系统和管道通信系统。
共享存储器系统
基于共享数据结构的通信方式
在这种通信方式中,要求诸进程公用某个数据结构,进程通过它们交换信息。如在生产者-消费者问题中,就是把缓冲池(有界缓冲区)这种数据结构用来作通信的。这种通信方式是低效的,只适用传送少量的数据。
基于共享存储区的通信方式
共享存储区是UNIX系统V中通信速度最高的一种通信机制,它包含在进程通信软件包IPC中。
系统调用shmget建立一个共享存储区,返回该表项的描述符shmid。
系统调用shmat将共享存储区附接到各自进程的虚地址空间上。
系统调用shmctl对共享存储区的状态信息(如长度,当前连接的进程数等)进行读取,也可设置和改变共享存储区的属性(如共享存储区的许可权等)。最后仍可利用系统调用shmctl断开进程与共享存储区的连接。
消息传递系统
消息传递系统分为直接通信方式和间接通信方式两种
直接通信方式
这是指发送进程利用OS提供的发送命令直接把消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上。接收进程利用OS提供的接收命令直接从消息缓冲队列中取得消息。此时要求发送进程和接收进程都以显示的方式提供对方的标识符,通常系统提供下述两条通信原语。
Send(Receiver ,message) ;
Receive(Sender ,message) ;或(Receive(message))
直接通信实例:消息缓冲队列通信机制
消息缓冲队列通信机制
消息缓冲队列通信机制的原理是:由系统管理一组缓冲区,其中每个缓冲区可以存放一个消息。当发送进程要发送消息时先要向系统申请一个缓冲区,然后把消息写进去,接着把该缓冲区连接到接收进程的消息缓冲队列中。接收进程可以在适当的时候从消息缓冲队列中摘下消息缓冲区,读取消息,并释放该缓冲区。
消息缓冲队列通信的数据结构有消息缓冲区和进程PCB中有关通信的扩充数据项。
消息缓冲区描述如下:
type message buffer=record
sender ;发送进程的标识符
size ;消息长度
text ;消息正文
next ;指向下一个消息缓冲区的指针
end
PCB描述如下:
type PCB = record
mutex ;消息缓冲队列互斥信息量 ;
Sm ;消息缓冲队列资源信息量 ;
mq ;消息缓冲队列首指针 ;
end
消息缓冲队列通信的发送和接收过程:
发送原语:
Proceduce send (receiver,a)
begin
getbuf(a.size,i) ;
i.sender = a.sender
i.size = a.size ;
i.text = a.text ;
i.next = 0 ;
getid (PCB set , receiver ,j) ;
P(j.mutex) ;
insert(j.mq,i) ;
V(j.mutex) ;
V(j.Sm) ;
end
接收原语:
proceduce receive(b)
begin
j := internal name ;
P (j.Sm) ;
P (j.mutex) ;
Remove(j.mq,i ) ;
V(j.mutex) ;
b.sender :=i.sender ;
b.size :=i.size ;
b.text :=i.text ;
Releasebuf(i) ;
end
间接通信方式
在间接通信情况,消息不直接从发送者发送到接收者,而是发送到暂存消息的共享数据结构组成的队列,这个实体称为信箱(mailbox).因此二个进程通信情况,一个进程发送一个消息到某个信箱,而另一个进程从信箱中摘取消息。
间接通信的使用好处是增加了使用消息的灵活性。发送者和接收者的关系可能是一对一、多对一,一对多或多对多。一对一的关系允许一个专用通信链路用于二个进程间的交互,它能使俩进程间交互不受其它进程错误干予的影响,多对一的关系对客户/服务器交互特别有用,一个进程对多个其它进程(用户)提供服务。在这种情况信箱经常称作为端口(port)。一对多关系允许一个发送进程和多个接收进程交互,这可用来将消息广播(broadcast)给一组进程。
进程与信箱的联系有二种:静态和动态。端口经常与特定的进程保持静态的联系,即端口创建后永久地分配给某个进程。同样一对一关系是典型地规定为静态的和永久的。当有许多发送者时,一个发送者与信箱的联系可以动态发生,例如联接(conect)和去联接原语可以应用于此用途。
对于端口它是接收进程拥有和创建的。当进程撤消时相应端口也撤消。对于一般信箱,操作系统可以提供创建和撤消信箱的服务。
对于端口它是接收进程拥有和创建的。当进程撤消时相应端口也撤消。对于一般信箱,操作系统可以提供创建和撤消信箱的服务。
UNIX系统消息机制
消息机制的数据结构:
UNIX系统将消息分为消息首部和消息数据两部分。
消息首部:在消息首部中记录有消息的类型、大小、指向消息数据区的指针、消息队列的链接指针等
消息队列头表:消息队列头表的每一表项是作为一个消息队列的消息头。在消息头中包含了指向消息队列中第一个消息的指针和指向最后一个消息指针,队列中消息的数目、队列中消息数据的总字节数等。
消息首部和消息头表的关系、消息首部和消息数据的关系见下图:
管道通信
管道定义:管道(pipe)通信机制也称为共享文件通信机制, UNIX系统在OS的发展上最重要的贡献之一便是该系统首创了管道(pipes) 。管道通信的实质是借助于一个特殊的共享文件连接发送进程和接收进程。发送进程向管道写入信息,接收进程从管道读信息,如图4.10所示。 管道通信是一种单向的传递信息方式,先进入管道的信息首先从管道中流出。发送进程只能写入信息,接收进程只能接收信息。从实现机理上说,就是发送进程将发送的信息写入文件,接收进程从文件中读出信息。因此管道需要作为文件引用。
利用管道通信的特点:
-
在通信前需要创建管道
管道创建成功后,发送信息的进程将信息写入管道,接收信息的进程从管道读出信息。管道不再使用时,为了回收资源,系统可以删除管道 -
利用管道通信的进程需要双方同步
当一个进程在对管道进行读写操作时,欲访问管道的其他进程必须等待;当一个进程发送数据到管道时,接收进程必须在管道等待接收;或者发送进程必须等待接收进程已经准备好到管道取信息时,才发送。如果发送进程与接收进程不能协调,则会造成通信信息丢失。
UNIX操作系统中的管道
在UNIX操作系统中,为进程通信提供了两种形式的管道:无名管道和有名管道
- 无名管道:无名管道是操作系统提供的资源,可以被所有的进程使用,无名管道在使用上的限制是:每次打开管道的进程,只能是与自己进程相关的进程(子进程)共享对管道的操作,完成相互之间的通信。
在UNIX操作系统中无名管道创建的系统调用函数定义为:
int pipe(files);
int files[2];
操作系统核心在执行pipe()调用后创建无名管道并返回文件描述符files[0]和files[1]。发送进程通过files[1]向管道中写入发送的信息,接收进程通过files[0]从管道中取走信息。
UNIX系统V的管道文件最大长度为10个盘块,核心为它设置了一个读指针和一个写指针。为了确保利用管道使进程间传送数据的正确性。系统在管道读写时设置互斥和同步机制,它首先保证写进程和读进程互斥地进行写和读,同时又要保证写和读的同步。即写进程将数据写入管道后,读进程才能来读取数据,如写进程未将数据写入管道,则读进程必须阻塞等待。如写进程将数据写满管道后读进程未来读,则发出写溢出,此时写进程必须阻塞等待。与一般文件不同是管道数据的先进先出(FIFO)处理方式和管道文件数据不可再现性。即被读取的数据在管道中不能再利用,无名管道在关机后自动消失。
pipe系统调用的工作原理:
核心创建一条管道完成下列工作:
- 分配一个隶属于root文件系统的磁盘和内存索引结点inode
- 在系统打开文件表中分别分配一读管道文件表项和一写管道文件表项
- 在创建管道的进程控制块的文件描述表(进程打开文件表u-ofile) 中分配二表项,表项中的偏移量filedes[o]和ditedes[1]分别指向系统打开文件表的读和写管道文件表项
有关数据结构:
在UNIX、MS-DOS系统中,用户可以在命令级使用管道,这时管道是一个程序的标准输出和另一个程序的标准输入之间的连接,管道命令用符号“|”表示。
C语言实现父子进程通信:
/*用C语言编写的父子进程利用无名管道通信程序。在该程序中父进程创建子进程,然后父进程向子进程发送数据,子进程接收数据。*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
/* 主程序 */
Main()
{
int filedes[2],i,pid;
char inbuf[256];
if (pipe(filedes)<0) /* 创建可擦写的无名管道 */
{
perror("could not create unnamed pipe");
exit(1);
}
if ((pid = fork())<0) /* 创建子进程*/
{
perror("could not create subprocess");
exit(2);
}
if (pid>0) /* 父进程*/
{
write(filedes[1],string,23); /* 向管道中写入数据 */
printf("the parent process sended\n");
wait(0);
}
if (pid==0) /* 子进程*/
{
memset(inbuf, 0, sizeof(inbuf));
read(filedes[0],inbuf,23); /* 从管道接收数据 */
printf("the subparent process received\n");
printf("%s\n",inbuf);
}
exit(0);
}
- 有名管道:有名管道克服了无名管道使用上的限制,所有的进程都可以共享对管道的操作,相互之间通过管道通信。有名管道用系统调用函数mknod建立管道,该系统调用的格式为:
mknod(pathname,type and permissions,dev);
其中:pathname是要建立的特殊文件名(有名管道名、目录名);type and permission是特殊文件类型和访问权限;dev为块特殊文件和字符特殊文件规定主设备号和次设备号,如果是有名管道,则dev参数为0。
与文件操作一样,有名管道在每次通信前打开,通信结束后关闭。再次使用时再打开,用完再关闭。有名管道文件有目录项并且可通过路径名存取。进程打开有名管道与打开正规文件方式一样。打开有名管道的系统调用格式为:
int open(path,flags);
该调用返回文件描述符(整数)。
其中path为文件名;flags为打开类型,按POSIX标准包括:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)、O_APPEND、O_CREAT、O_EXCL、O_NOCTTY、O_NONBLOCK、O_TRUNC。
对有名管道读写也需要采用进程同步的方式。有名管道读写系统调用与无名管道形式相同
进程之间利用有名管道通信:
/*在主进程中首先创建一个有名管道,然后创建子进程P1和P2,打开管道。子进程P1向该管道发送信息,子进程P2从管道接收信息。*/
#include <sys/stat.h>
#include <fcntl.h>
char filename[] = "./myfifo";
/* 主程序 */
main(argc,argv)
int argc;
char *argv [];
{
int filedes[2];
int pid1,pid2;
char buf[256];
/* 创建可擦写的有名管道myfifo */
if (mknod(filename, O_CREAT | S_IRUSR | S_IWUSR, 0) == −1)
{
perror("mknod error");
exit(1);
}
fork(P1,0); /* 创建子进程P1*/
fork(P2,0); /* 创建子进程P2*/
filedes[0]=open(filename, O_RDONLY);/*以只读方式打开管道myfifo */
filedes[1]=open(filename, O_WRONLY);/*以读写方式打开管道myfifo */
exit(0);
}
/* 子进程P1 */
Process P1
{
While(true)
{
…
write(filedes[1],string,21); /*写入filedes[1]的数据是从
filedes[0]读出的*/
printf("wrote message: |%s|\n", string);
sleep(3);
exit(0);
}
}
/* 子进程P2 */
Process P2
{
While(true)
{
…
memset(buf, 0 ,sizeof(buf)); /* 设置接收缓冲 */
read(filedes[0],buf,21);
printf("read message: |%s|\n", buf);
unlink(filename);
exit(0);
}
}
在UNIX操作系统或Linux操作系统中,利用管道通信非常方便,特别是无名管理,操作系统能够直接提供。