无论是多进程编程还是多线程编程都是为了能够高效的处理事务,若进程或线程之间无法进行数据交换,多进程编程或者多线程编程将显得毫无意义,每个进程都完成着自己独立的任务,与单进程、单线程有什么区别?所以进程之间需要数据交流,我们称之为进程间通信。
进程都是独立运行的,所以进程要通信,必须有能够共享的东西——内核对象。
内核对象:由操作系统内核维护的,在操作系统内核中的结构体变量。一般内核对象都有标识符和键。标识符:标识符是内核对象的内部名,类似于文件描述符。键:相当于为用户维护的外部名称,(用户标识),通过相同的用户标识访问同一内核对象,类似于文件名。
进程之间相互通信的技术IPC(InterProcess Communication)。过去,UNIX系统IPC是各种进程通信方式的总称。进程间的通信方式我们大致分为5种,管道、消息队列、信号量、共享内存和套接字。我们来重点讨论前4种。
一、管道
管道就是借助文件系统在多个进程之间建立一条数据通信的信道,各进程通过自己打开的文件描述符操作同一块内存空间,从而完成数据的传递工作。管道文件的数据都是通过内存空间缓存,不在磁盘存储。管道是半双工通信(同一通信时刻数据只能单方面传输)。
管道分为有名管道和无名管道:
有名管道:可以在任意进程之间进行通讯,通讯是双向的,任意一段都可读可写,但同一时间只能一端读一端写。有名管道是建立在实际的磁盘介质或文件系统(而不是只存在于内存中)上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现有名管道,引入了一种新的文件类型——FIFO文件(遵循先进先出的原则)。实现一个有名管道实际上就是实现一个FIFO文件。有名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。
无名管道:只能在具有亲属关系的进程(父子进程)间通讯,不能再网络间通讯,并且是单向的,只能一端读另一端写。
管道是UNIX系统IPC的最古老形式,并且所有UNIX系统都提供此种通信机制。但管道有下面两种局限性:
(1)历史上,他们是半双工的(即数据只能在一个方向移动)。现在某些系统提供全双工管道,但是为了最佳的可移植性,一般坚决不应预先假定系统使用此特性。
(2)他们只能在具有公共祖先的进程之间使用,统称一个管道有一个进程创建,然后该进程调用fork(),此后父子进程之间就可应用该管道。
FIFO没有第二种局限性,UNIX域套接字和命名流管道则没有这两种局限性。
尽管有这两种局限性,半双工管道仍然是最常见的IPC形式。每当你在管道线中键入一个由shell执行的命令序列时,shell为每一条命令单独创建一进程,然后将前一条命令进程的标准输出管道与后一条命令的标准输入相连接。
无名管道是由调用pipe()函数而创建的:
#include<unistd.h>
int pipe(int filedes[2]); //若成功则返回0,若出错则返回-1
经由参数filedes返回两个文件描述符;filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。
有名管道需要一个FIFO文件:
//创建有名管道
int mkfifo(const char* pathname,mode_t mode);
//打开有名管道
int open(const char*pathname,int flag);
有两种方法来描绘一个半双工管道,如下图所见,左半图显示了管道的两端在一个进程中相互连接,右半图则说明数据通过内核在管道中流动。
单个进程中的管道几乎没有任何用处,通常,调用pipe()的进程接着调用fork(),这样就创建了从父进程到子进程(或反向)的IPC通道。如下图所示:
调用fork之后做什么取决于我们想要有的数据流的方向。对于父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程则关闭写端(fd[1])。如下图所示:
为了构造从子进程到父进程的管道,父进程关闭fd[1],子进程关闭fd[0]。
当管道的一端被关闭后,下列两条规则起作用:
(1)当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,以指示达到了文件结束处。(从技术方面考虑,管道的写端还有进程时,就不会产生文件的结束,可以复制一个管道的描述符,使得多个进程对它具有写打开文件描述符,但是通常一个管道只有一个读进程一个写进程)
(2)如果写一个读端以关闭的管道时,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno设置为EPIPE。
在写管道时,常量PIPE_BUF规定了内核中管道缓冲区的大小,如果对管道调用write,而且要求写的字节小于等于PIPE_BUF,则此操作不会与其他进程对同一管道的write操作穿插进行。但是,但是若有多个进程同时写一管道,而且有进程要求写的字节数超过PIPE_BUF字节数时,则写操作的数据可能相互穿插。用pathconf()或fpathconf()函数可以确定PIPE_BUF的值。
二、消息队列
消息队列是消息的链接表,存放在内核中并由消息队列标识符标识。
msgget用于创建一个新队列或打开一个已有队列。msgsnd将消息添加到队列尾端。每个消息包含一个正长整型类型字段,一个非负长度以及实际数据字节(对应于长度),所有这些都在将消息添加到队列时,传递给msgsnd。msgrcv用于从消息队列中取数据。我们并不一定要以先进先出次序取消息,也可以按消息类型字段取消息。
每个队列都有一个msqid_ds结构与其相关联:
struct msqid_ds{
struct ipc_perm msg_perm; //权限信息
msgqnum_t msg_qnum; //队列中的消息数
msglen_t msg_qbytes;
struct msg *msg_first; //第一个消息
struct msg *msg_last; //最后一个消息
pid_t msg_lspid; //最后一个写入消息的进程ID
pid_t msg_lrpid; //最后一个读取消息的进程ID
time_t msg_stime; //最后一次写入消息的时间
time_t msg_rtime; //最后一次读取消息的时间
time_t msg_ctime; //最后一次变动的时间
...
};
此结构体规定了队列的当前状态。
在Linux系统中,消息最大数基于队列最大数值和队列中允许数据量的最大值。如果最短消息长度是1字节,则系统范围内的消息数限制是最大消息队列数*队列的最大长度(字节)。Linux默认配置的最大消息数(系统范围内)是262 144。(即使一个消息可能包含0字节数据,Linux也将其处理为如同包含1字节那样,其目的是限制队列中的消息数)。
打开一个现有队列或创建一个新队列msgget()
#include<sys/msg.h>
int msgget(key_t key,int flag); //若成功返回消息队列的ID,出错返回-1
当创建一个新队列时,初始化msqid_ds结构体的下列成员:
·ipc_perm结构体初始化,该结构体中mode成员按flag中的相应权限位设置。
·msg_qnum、msg_lspid、msg_lrpid、msg_stime和msg_rtime都设置为0.。
·msg_ctime设置为当前时间。
·msg_qbyte设置Wie系统限制值。
此后消息队列的队列ID就可以用于其他三个消息队列函数。
#include<sys/msg.h>
int msgctl(int msqid,int cmd,struct msqid_ds *buf);//成功返回0,出错返回-1
cmd参数说明对由msqid指定的队列要执行的命令:
IPC_STAT 取此队列的msqid_ds结构,并将它放在buf指向的结构体中。
IPC_SET 按由buf指向结构体中的值,设置与此队列相关结构体中的字段。此命令只能由两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid;另一种是具有超级用户特权的进程。只有超级用户才能增加msg_qbytes的值。
IPC_RMID 从系统中删除该消息队列以及仍在该队列的所有数据。这种删除立即生效。仍在使用这一消息队列的其他进程在他们下一次试图对此队列进行操作时,将出错返回EIDRM。此命令只能由两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid;另一种是具有超级用户特权的进程。
这三条命令(IPC_STAT、IPC_SET和IPC_RMID)也可以用于信号量和共享内存。
将数据放到消息队列中。
#include<sys/msg.h>
int msgsnd(int msqid,const void *ptr,size_t nbytes,int flag);//成功返回0出错返回-1
每个消息都是由三部分组成,他们是:正长整型类型字段,非负长度(nbytes)以及实际数据字节(对应长度),消息总是放在队列的尾端。
ptr指向一个长整型数,他包含了正的整型消息类型,在其后紧跟着消息数据。(若nbyte是0,则无消息数据)若发送的最长消息是512字节,则可定义下列结构;
struct mymesg{
long mtype; //message type
char mtext[512]; //message data
};
于是,ptr就是一个指向mymesg结构体的指针。接收者可以使用消息类型以非先进先出的次序取消息。
参数flag的值可以指定为IPC_NOWAIT。这类似与文件I/O的非阻塞I/O标志,若消息队列已满,则指定IPC_NOWAIT使得msgsnd立即出错返回EAGAIN。如果没有指定IPC_NOWAIT,则进程阻塞知道下述情况出现为止;有空间可以容纳要发送的消息,从系统中删除此队列;或捕捉到一个信号,并从信号处理程序返回。在第二种情况下,返回EIDRM(“标识符被删除”)。最后一种情况则返回EINTR。
当msgsnd成功返回,与消息队列相关的msqid_ds结构体得到更新,以标明发出该调用的进程ID(msg_lspid)、进行该调用时间(msg_stime),并指示队列中增加了一条消息(msg-qnum)。
从队列中读取消息。
msgrcv()从队列中取消息
#include<sys/msg.h>
ssize_t msgrcv(int msqid,void *ptr,size_t nbytes,long type,int flag);//成功返回消息数据部分的长度,出错返回-1
如同msgsnd中一样,ptr参数指向一个长整型数(返回的消息类型存放在其中),跟随其后的是存放消息的实际数据的缓冲区。nbytes说明数据缓冲区的长度。若返回的消息大于nbytes,而且在flag中设置了MSG_NOERROR,则该消息被截短。(这种情况系不通知我们消息被截了,消息的截去部分被丢弃)如果没有设置这一标志,消息又太长,则出错返回E2BIG(消息仍留在队列中)。
可以指定flag值为IPCNOWAIT,使操作不阻塞。这使得如果没有所指定类型的消息。则msgrcv返回-1,errno设置为ENOMSG。如果没有指定IPC_NOWAIT,则进程阻塞直至如下情况出现才会终止:有了指定类型的消息,从系统中删除了此队列(出错返回-1且errno设置为EIDRM),或捕捉到一个信号并从信号处理程序返回(msgrcv返回-1,erron设置为EINTR)。
msgrcv成功执行时,内核更新与该消息队列相关联的msqid_ds结构,以指示调用者的进程ID(msg_lrpid)和调用时间(msg_rtime),并将队列中的消息数(msg_qnum)减1。
三、信号量
下接进程间通信(二)