这里列出Unix系统提供的进程间通信的基本机制:
管道和FIFO(命名管道):最适合在进程之间实现生产者/消费者的交互。有些进程向管道中写入数据,而另外一些进程则从管道中读出数据。
信号量:
消息:允许进程在预定义的消息队列中读和写消息来交换消息(小块数据)。Linux内核提供两种不同的消息版本:System V IPC消息和POSIX消息一节。
共享内存区:允许进程通过共享内存块来交换信息。在必须共享大量数据的应用中,这可能是最高效的进程通信形式。
套接字:允许不同计算机上的进程通过网络交换数据。套接字还可以用作相同主机上的进程之间的通信工具;
一、管道
管道(pipe)是所有Unix都愿意提供的一种进程间通信机制。管道是进程之间的一个单向数据流:一个进程写入管道的所有数据都由内核定向到另一个进程,另一个进程由此就可以从管道中读出数据。
在Unix的命令shell中,可以使用“|”操作符来创建管道。例如,下面的语句通知shell创建两个进程,并使用一个管道把这两个进程连接在一起:
$ls | more
第一个进程(执行ls程序)的标准输出被重定向到管道中;第二个进程(执行more程序)从这个管道中读取输入。
注意,执行下面这两条命令也可以得到相同的结果:
$ls > temp
$more < temp
第一个命令把ls的输出重定向到一个普通文件中;接下来,第二个命令强制more从这个普通文件中读取输入。
1.1、使用管道
当shell 命令对ls | more语句进行解释时,实际上要执行以下操作:
1、调用pipe()系统调用;让我们假设pipe()返回文件描述符3(管道的读通道)和4(管道的写通道)。
2、两次调用fork()系统调用。
3、两次调用close()系统调用来释放文件描述符3和4。
第一个子进程必须执行ls程序,它执行以下操作:
1、调用dup2(4, 1)把文件描述符4拷贝到文件描述符1。从现在开始,文件描述符1就代表该管道的写通道。
2、两次调用close()系统调用来释放文件描述符3和4。
3、调用execve()系统调用来执行ls程序。缺省情况下,这个程序要把自己的输出写到文件描述符为1的那个文件(标准输出)中,也就是说,写入管道中。
第二个子进程必须执行more程序;因此,该进程执行以下操作:
1、调用dup2(3, 0)把文件描述符3拷贝到文件描述符0。从现在开始,文件描述符0就代表管道的读通道。
2、两次调用close()系统调用来释放文件描述符3和4。
3、调用execve()系统调用来执行more程序。缺省情况下,这个程序要从文件描述符为0那个文件(标准输入)中读取输入,也就是说,从管道中读取输入。
1.2、管道数据结构
我们现在又一次在系统调用的层次考虑问题。只要管道一被创建,进程就可以使用read()和write()的这两个VFS系统调用来访问管道。因此,对于每个管道来说,内核都要创建一个索引节点对象和两个文件对象,一个文件对象用于读,另外一个对象用于写。当进程希望从管道中读取数据或向管道中写入数据时,必须使用适当的文件描述符。
16个缓冲区可以被看作一个整体环形缓冲区:写进程不断向这个大缓冲区追加数据,而读进程则不断移出数据。所有管道缓冲区中当前写入而等待读出的字节数就是所谓的管道大小。为提高效率,仍然要读的数据可以分散在几个未填充满的管道缓冲区内:事实上,在上一个管道缓冲区没有足够空闲存放新数据时,每个写操作都可能会把数据拷贝到一个新的空管道缓冲区。因此,内核必须记录:
* 下一个待读字节所在的管道缓冲区、页框中的对应偏移量。该管道缓冲区的索引存放在pipe_inode_info数据结构的curbuf字段,而偏移量在相应pipe_buffer对象的offset字段。
* 第一个空管道缓冲区。它可以通过增加当前管道缓冲区的索引得到(模为16),并存放在pipe_inode_info数据结构的curbuf字段,而存放有效数据的管道缓冲区号存放在nrbufs字段。
1.2.1、pipefs特殊文件系统
1.3、创建和撤销管道
1.4、从管道中读取数据
希望从管道中读取数据的进程发出一个read()系统调用,为管道的读端指定一个文件描述符。内核最终调用与这个文件描述符相关的文件操作表中所找到的read方法。在管道的情况下,read方法在read_pipe_fops表中的表项指向pipe_read()函数。
这个系统调用可能以两种方式阻塞当前进程:
* 当系统调用开始时管道缓冲区为空。
* 管道缓冲区没有包含所有请求的字节,写进程在等待缓冲区的空间时曾被置为睡眠。
注意,读操作可以是非阻塞的。在这种情况下,只要所有可用的字节(即使是0个)一被拷贝到用户地址空间中,读操作就完成。
还要注意,只有在管道为空而且当前没有进程正在使用与管道的写通道相关的文件对象时,read()系统调用才会返回0。
1.4.1、向管道中写入数据
希望向管道中写入数据的进程发出一个write()系统调用,为管道的写端指定一个文件描述符。内核通过调用适当文件对象的write方法来满足这个请求;write_pipe_fops表中相应的项指向pipe_write()函数。
还有,如果管道没有读进程(也就是说,如果管道的索引节点对象的readers字段的值是0),那么任何对管道执行的写操作都会失败。在这种情况下,内核会向写进程发送一个SIGPIPE信号,并停止write()系统调用,使其返回一个-EPIPE错误码,这个错误码就是我们熟悉的“Broken pipe(损坏的管道)”消息。
二、FIFO
虽然管道是一种十分简单、灵活、有效的通信机制,但它们有一个主要的缺点,也就是无法打开已经存在的管道。这就使得任意的两个进程不可能共享同一个管道,除非管道由一个共同的祖先进程创建。
Unix系统引入了一种称为命令管道(named pipe)或者FIFO[FIFO代表“先进先出(first in, first out)”:最先进入文件的字节总是被最先读出]的特殊文件类型。FIFO在这几个方面都非常类似于管道:在文件系统中不拥有磁盘块,打开的FIFO总是与一个内核缓冲区相关联,这一缓冲区中临时存放两个或多个进程之间交换的数据。
在Linux 2.6中,FIFO和管道几乎是相同的,并使用相同的pipe_inode_info结构。事实上,FIFO的read和write操作就是由前面“从管道中读取数据”和“向管道中写入数据”这两节描述的pipe_read()和pipe_write()函数实现的。事实上,只有两点主要的差别:
* FIFO索引节点出现在系统目录树上而不是pipefs特殊文件系统中。
* FIFO是一种双向通信管道:也就是说,可能以读/写模式打开一个FIFO。
2.1、创建并打开FIFO
FIFO一旦被创建,就可以使用普通的open()、read()、write()和close()系统调用访问FIFO,但是VFS对FIFO的处理方法比较特殊,因为FIFO的索引节点及文件操作都是专用的,并且不依赖于FIFO所在的文件系统。
FIFO的三个专用文件操作表的主要区别是read和write方法的实现不同。如果访问类型允许读操作,那么read方法是使用pipe_read()函数实现的;否则,read方法就是使用bad_pipe_r()函数实现的,该函数只是返回一个错误码。类似地,如果访问类型允许写操作,那么write方法就是使用pipe_write()函数实现的;否则,write方法就是使用bad_pipe_w()函数实现的,该函数也只是返回一个错误代码。
三、System V IPC
IPC是进程间通信(Interproccess Communication)的缩写,通常指允许用户态进程执行下列操作的一组机制:
* 通过信号量与其他进程进行同步。
* 向其他进程发送消息或者从其他进程接收消息。
* 和其他进程共享一段内存区。
IPC数据结构是在进程请求IPC资源(信号量、消息队列或者共享内存区)时动态创建的。每个IPC资源都是持久的:除非被进程显示地释放,否则永远驻留在内存中(直到系统关闭)。IPC资源可以由任一进程使用,包括那些不共享祖先进程所创建的资源的进程。
当两个或者更多的进程要通过一个IPC资源进行通信时,这些进程都要引用该资源的IPC标识符。
3.1、使用IPC资源
根据新资源是信号量、消息队列还是共享内存区,分别调用semget()、msgget()或者shmget()函数创建IPC资源。
这三个函数的主要目的都是从IPC关键字(作为第一个参数传递)中导出相关的IPC标识符,进程以后就可以使用这个标识符对资源进行访问。如果还没有IPC资源和IPC关键字相关联,就创建一个新的资源。如果一切都顺利,那么函数就返回一个正的IPC标识符;否则,就返回一个错误码。
假设两个独立的进程想共享一个公共的IPC资源。这可以使用两种方法来达到:
* 这两个进程统一使用固定的、预定义的IPC关键字。这是最简单的情况,对于由很多进程实现的任一复杂的应用程序也工作得很好。然而,另外一个无关的程序也可能使用了相同的IPC关键字。在这种情况下,IPC函数可能被成功地调用,但返回错误资源的IPC标识符。
* 一个进程通过指定IPC_PRIVATE作为自己的IPC关键字来调用semget()、msgget()或shmget()函数。一个新的IPC资源因此而被分配,这个进程或者可以与应用程序中的另一个进程共享自己的IPC标识符,或者自己创建另一个进程。这种方法确保IPC资源不会偶然被其他应用程序使用。
semget()、msgget()和shmget()函数的最后一个参数可以包括三个标志。PC_CREAT说明如果IPC资源不存在,就必须创建它;IPC_EXCL说明如果资源已经存在而且设置了IPC_CREAT标志,那么函数就必定失败;IPC_NOWAIT说明访问IPC资源时进程从不阻塞(典型的情况如取得消息或获取信号量)。
一旦一个IPC资源被创建,进程就可以通过一些专用函数对这个资源进行操作。进程可以执行semop()函数获得或释放一个IPC信号量。当进程希望发送或接收一个IPC消息时,就分别使用msgsnd()和msgrcv()函数。最后,进程可以分别使用shmat()和shmdt()函数把一个共享内存区附加到自己的地址空间中或者取消这种附加关系。
3.2、ipc()系统调用
3.3、IPC信号量
IPC信号量和在第五章中介绍的内核信号量非常类似:二者都是计数器,用来为多个进程共享的数据结构提供受控访问。
如果受保护的资源是可用的,那么信号量的值就是正数;如果受保护的资源现不可用,那么信号量的值就是0。要访问资源的进程试图把信号量的值减1,但是,内核阻塞这个进程,直到在这个信号量上的操作产生一个正值。当进程释放受保护的资源时,就把信号量的值增加1;在这样处理的过程中,其他所有正在等待这个信号量的进程就都被唤醒。
实际上,IPC信号量比内核信号量的处理更复杂是由于两个主要的原因:
* 每个IPC信号量都是一个或者多个信号量值的集合,而不像内核信号量一样只有一个值。这意味着同一个IPC资源可以保护多个独立、共享的数据结构。在资源正在被分配的过程中,必须把每个IPC信号量中的信号量的个数指定为semget()函数的一个参数。从现在开始,我们就把信号量内部的计数器作为原始信号量(primitive semaphore)来引用。IPC信号量资源的个数和单个IPC资源内原始信号量的个数都有界限,其缺省值前者为128,后者为250;不过系统管理员可以通过/proc/sys/kernel/sem文件很容易地修改这两个界限。
* System V IPC信号量提供了一种失效的安全机制,这是用于进程不能取消以前对信号量执行的操作就死亡的情况的。当进程选择使用这种机制时,由此引起的操作就是所谓的可取消的(undoable)信号量操作。当进程死亡时,所有IPC信号量都可以恢复成原来的值,就好像从来都没有开始它的操作。这有助于防止出现这种情况:由于正在结束的进程不能手工取消它的信号量操作,其他使用相同信号量的进程无限地停留在阻塞状态。
sem_base字段指向sem数据结构的数组,每个元素对应一个IPC原始信号量。sem数据结构只包括两个字段:
semval:信号量的计数器的值。
sempid:最后一个访问信号量的进程的PID。进程可以使用semctl()封装函数查询该值。
3.3.1、可取消的信号量操作
正是由于两个链表(我们称之为每个进程的链表和每个信号量的链表),使得内核可以有效地处理这些任务。第一个链表记录给定进程以可取消操作处理的所有信号量。第二个链表记录对以可取消操作对给定信号量进行操作的所有进程。更确切地说:
* 每个进程链表包含所有的sem_undo数据结构,该结构对应于进程执行了可取消操作的IPC信号量。进程描述符的sysvsem.undo_list字段指向一个sem_undo_list类型的数据结构,而该结构又包含了指针指向该链表的第一个元素。每个sem_undo数据结构的proc_next字段指向该链表的下一个元素。
* 每个信号量链表包含的所有sem_undo数据结构对应于在该信号量上执行可取消操作的进程。sem_array数据结构的undo字段指向链表的第一个元素,而每个sem_undo数据结构的id_next字段指向链表的下一个元素。
3.3.2、挂起请求的队列
3.4、IPC消息
进程彼此之间可以通过IPC消息进行通信。进程产生的每条消息都被发送到一个IPC消息队列中,这个消息一直存放在队列中直到另一个进程将其读走为止。
消息是由固定大小的首部和可变长度的正文组成的,可以使用一个整数值(消息类型)标识消息,这就允许进程有选择地从消息队列中获取消息。只要进程从IPC消息队列中读出一条消息,内核就把这个消息删除;因此,只能有一个进程接收一条给定的消息。
当消息队列满时(或者达到了最大消息数,或者达到了队列最大字节数),则试图让新消息入队的进程可能被阻塞。msg_queue数据结构的q_senders字段是所有阻塞的发送进程的描述符形成的链表的头。
当消息队列为空时(或者当进程指定的一条消息类型不在队列中时),则接收进程也会被阻塞。msg_queue数据结构的q_receivers字段是msg_receiver数据结构链表的头。每个阻塞的接收进程对应其中一个元素。其中的每个结构本质上都包含一个指向进程描述符的指针、一个指向消息的msg_msg结构的指针和所请求的消息类型。
3.5、IPC共享内存
最有用的IPC机制是共享内存,这种机制允许两个或多个进程通过把公共数据结构放入一个共享内存区(IPC shared memory region)来访问它们。如果进程要访问这种存放在共享内存区的数据结构,就必须在自己的地址空间中增加一个新内存区,它将映射与这个共享内存区相关的页框。这样的页框可以很容易由内核通过请求调页进行处理。
3.5.1、换出IPC共享内存区的页
3.5.2、IPC共享内存区的请求调页
四、POSIX消息队列
POSIX消息队列比老的队列具有许多优点:
* 更简单的基于文件的应用接口。
* 完全支持消息优先级(优先级最终决定队列中消息的位置)。
* 完全支持消息到达的异步通知,这通过信号或是线程创建实现。
* 用于阻塞发送与接收操作的超时机制。
阅读(910) | 评论(0) | 转发(0) |