进程间通信

进程间通信

进程间通信即IPC(InerProcess Communication)
Unix ipc 已经是而且继续是各种进程通信方式的统称,其中极少能在所有unix的实现中进行移植
不管哪一种Unix实现都可依靠唯一一种IPC是半双工的管道。管道、FIFOs、流管道、命令流管道、消息队列、信号量、共享存储通常限于同一台主机的各个进程间的IPC。套接口和流,则支持不同主机上各个进程间IPC.

管道

管道是unix IPC的最古老的形式,并且所有Unix系统都提供此种通信机制,管道有两种限制:1、他们是半双工的。数据只能在一个方向上流动。2、他们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可以应用该管道。
流管道没有第一种限制,FIFO和命名管道则没有第二种限制。尽管有这两种限制,半双工管道仍是最常用的IPC
管道是由pipe函数创建的。
int pipe(int filedes[2])
经由参数filedes返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。
有两种方法来描绘一个管道。
fstat函数对管道的每一端都返回一个FIFO类型的文件描述符,可以用S_ISFIFO宏来测试管道。
单个进程中的管道几乎没有任何用处。通常,调用pipe的进程接着调用fork,这样就创建了从父进程到子进程或反之的IPC通道。
fork之后做什么取决于我们想要有的数据流的方向。对于从父进程到子进程的管道,父进程关闭管道的读端,子进程则关闭写端。
当管道的一端被关闭后,下列规则起作用

  • 当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,以指示达到了文件结束处。
  • 如果写一个读端已经被关闭的管道时,则产生SIGPIPE信号。如果忽略该信号或者捕捉该信号并且从其处理程序返回,则write出错返回,errno设置为EPIPE

在写管道式,常数PIPE_BUF规定了内核中短刀缓存器的大小。如果对管道进行write调用,而且要求写的字节数小于等于PIPE_BUF,则此操作不会与其他进程对统一管道的write操作穿插进行。但是若有多个进程同时写一个管道,而且某个或某些进程要求写的字节数超过PIPE_BUF字节数,则数据可能会与其他写操作的数据相穿插.

popen和pclose函数

因为常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其发送输入,所以标准io库为实现这些操作提供了两个函数popen和pclose。这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭管道不使用的端,exec一个shell以执行命令,等待命令终止。
FILE *popen(const char *cmdstring, const char *type)
int pclose(FILE *fp)
函数open先执行fork,然后调用exec执行cmdstring,并且返回一个标准io文件指针。如果type是r,则文件指针连接到cmdstring的标准输.出如果type是w则文件指针连接到cmdstring的标准输入。
pclose函数关闭标准io流,等待命令执行结束,然后返回shell的终止状态。如果shell不能被执行,则pclose返回的终止状态与shell执行exit一样。

协同进程

unix过滤程序从标准输入读取数据,对其进行适当处理后写到标准输出。几个过滤进程通常在shell管道命令中线性地连接。当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则在过滤程序就成为协同进程。
komshell听歌了协同进程。bourne shell 和 c shell并没有提供将进程连接起来按协同进程方式工作的方法。协同进程通常在shell的后台运行,其标准输入和标准输出通过管道连接到另一个程序。
虽然要求初始化一个协同进程,并将其输入和输出连接到另一个进程的shell语法是十分奇特的,但是协同进程的工作方式在C程序中也是非常有用的。
popen提供连接到另一个进程的标准输入或标准输出的一个单行管道,而对于协同进程,则它有连接到另一个进程的两个单行管道,一个接到其标准输入,一个则来自标准输出。我们先要将数据写到其标准输入,经其处理后再从标准输出读取数据。协同程序中如果使用标准io则由于缓存问题的存在可能导致问题。如果我们无法修改协同程序的相关代码,则解决办法是是被调用的协同程序认为它的标准输入和输出被连接到一个终端。这使得协同进程中的标准io历程对这两个io流进行缓存。

FIFO

FIFO被称为命名管道,管道只能由相关进程使用,他们共同的祖先进程创建了管道。但是,通过FIFO,不相关的进程也能交换数据。
FIFO是一种文件结构。stat结构成员st_mode的编码指明文件是否是FIFO类型。可以用S_ISFIFO宏对此进行测试。
创建FIFO类似于创建文件。FIFO的路径名存在于文件系统中。
int mkfifo(const char *pathname, mode_t mode)
mode 与open函数中mode相同。一旦用mkfifo创建了一个FIFO就可以使用open打开它。一般的文件IO函数(close、read、write、unlink)等都可用于FIFO
当打开一个FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响

  • 在一般情况总,只读打开要阻塞到某个其他进城为写打开此FIFO,类似,为写而打开一个FIFO要阻塞到某个其他进程为读而打开它。
  • 如果指定了O_NONBLOCK,则只读打开立即返回。但是如果没有进程已经为读而打开一个FIFO,那么只写打开将出错返回,其errno实ENXIO

类似于管道,若刺耳一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE。若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束的标志。
一个给定的FIFO有多个写进程是常见的。这就意味着如果不希望多个进程所写的数据相互穿插,则需要考虑原子写操作。正如对于管道一样,常数PIPE_BUF说明了可被原子写到FIFO的最大数据量。
FIFO有两种用途:

  • FIFO由shell命令使用以便将数据从一条管道线传送到另一条,为此无需创建中间临时文件。
  • FIFO用于客户机-服务器应用程序中,以在客户机和服务器之间传递数据

管道只能用于进程间的线性连接,然而因为FIFO具有名字,所以它可用于非线性连接。

系统v ipc

标识符和关键字

每个内核中的IPC结构(消息队列、信号量或共享存储段)都用一个非负征地户的标识符加以引用。例如,为了对一个消息队列发送或取消息,只需知道其队列标识符。与文件描述符不同,IPC标识符不是小的整数。当一个IPC结构被创建,以后又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大正值,然后又回转到0.
无论何时创建IPC结构都应制定一个关键字,关键字的数据类型由系统规定为key_t,通常在头文件<sys.types.h>中被规定为长整型。关键字由内核变换成标识符。
有多宗方法使客户机和服务器同在一IPC结构上会合

  • 服务器可以指定关键字IPC_PRIVATE 创建一个新的IPC结构,将返回的标识符存放在某处以便客户机取用。关键字IPC_PRIVATE保证服务器创建一个新的IPC结构。这种技术的缺点是服务器要将整形标识符写到文件中,然后客户机在此后又要读文件取得此标识符。IPC_PRIVATE关键字也可用于父子关系进程。父进程指定IPC_PRIVATE创建一个新的IPC结构,所返回的标识符在fork后可由子进程使用。子进程可将此标识符作为exec函数的一个参数传给一个新的程序。
  • 在一个公用头文件中定义一个客户机和服务器都认可的关键字。然后服务器指定此关键字创建一个新的IPC结构。这种方法的问题是该关键字可能已与一个IPC结构相结合,在此情况下,get函数出错返回。服务器必须处理这一错误,删除已存在的IPC结构,然后试着再创建它。
  • 客户机和服务器认同一个路径名和课题ID,然后调用函数ftok将这两个值转换为一个关键字。然后在方法2中使用此关键字。ftok提供的唯一服务就是由一个路径和课题ID产生一个关键字。因为一般来说,客户机和服务器至少共享一个头文件,所以一个比较简单的方法是避免使用ftok,而只是在该头文件中存放一个大家都知道的关键字。这样海避免了使用另一个函数。

三个get函数(msgget,setmget,shmget)都有两个类似的参数key和一个整型的flag。如若满足下列条件,则创建一个新的IPC结构(通常由服务器创建):

  • key 是IPC_PRIVATE,或
  • key当前未与特定类型的IPC结构结合,flag中指定了IPC_CREAT位。为访问现存的队列,key必须等于创建该队列时所指定的关键字,并且不应指定IPC_CREATE.

注意,为了访问一个现存的队列,决不能指定IPC_PRIVATE关键字。因为这是一个特殊的键值,它总是拥有创建一个新的队列。为了访问一个用IPC_PRIVATE关键字创建的现存队列,一定要知道与队列相结合的标识符,然后在其他的IPC调用中使用该标识符。
如果希望创建一个新的IPC结构,保证不是引用具有统一标识符的一个线性IPC结构,那么必须在flag中同时制定IPC_CREAT和IPC_EXCL位。这样做了以后,如果IPC结构已经存在就会造成出错,返回EEXIST.

许可权结构

系统V ipc为每一个ipc结构设置了一个ipc_perm结构。该结构规定了许可权和所有者。
struct ipc_perm {
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
ulong seq;
key_t key;
}
在创建ipc结构时,除seq意外的所有字段都赋初值。以后可以调用msfctl,semctl或shmctl修改uid gid和 mode字段。为了改变这些值,调用进程必须是IPC结构的创建者或超级用户。更改这些字段类似于对文件调用chown和chmod.
对于任何IPC结构都不存在执行许可权限。另外消息队列和共享存储使用术语读和写,而信号量则使用术语读和更改。

结构限制

三种形式的系统V ipc都有我们可能会遇到的内在的限制。这些限制的大多数可以通过从新配置而加以更改。

有点和缺点

系统V ipc的主要问题是ipc结构在系统范围内起作用。没有访问计数。例如,如果创建了仪的消息队列,在该队列中放入了几则消息,然后终止,但是该消息队列及其内容并不被删除。他们余留在系统中直至:有某个进程调用msgrcv或msgctl读消息或者删除消息队列,或者某个进程执行ipcrm命令删除消息队列或者由正在再启动的系统删除消息队列。将此与管道pipe相比,那么当最后一个访问管道的进程终止时,管道就被完全删除了。对于FIFO而言虽然当最后一个引用FIFO的进程终止时其名字仍然留在系统中,直至显示的删除它,但是留在FIFO中的数据却在此时全部删除。
系统v ipc的另一个问题是:这些ipc结构并不按名字为文件系统所知。我们不能用文件相关的函数来存取他们或修改他们的特性。为了支持他们不得不增加了十个全新的系统调用。我们不能用ls命令看到他们,不能用rm命令删除他们,不鞥用chmod命令更改他们的存取权。于是也不得不增加了全新的命令ipcs和ipcrm
因为这些ipc不适用文件描述符,所以不能对他们使用多路转接io函数:select和poll这就使得一次使用多个ipc结构以及用文件或设备io来使用ipc结构很难做到。例如没有某种形式的忙-等待循环就不能使一个服务器等待一个消息放在两个消息队列中的任意一个中。
因为这些形式的ipc都是限制用在单主机上,所以他们是可靠的。当消息通过网路传送石,丢失消息的可能性就要加以考虑。

消息队列

消息队列是消息的链接表,存放在内核中并由消息队列标识符标识。我们将称消息队列为队列,其标识符为队列id。msgget用于创建一个新队列或打开一个现存的队列。msgsnd用于将新消息添加到队列尾端。每个消息包含一个正长整型类型字段,一个非负长度以及实际数据字节。所有这些都在将消息添加到队列时传送给msgsnd.msgrcv用于从队列中取消息。我们并不一定要以先进先出次序取消息,也可以按消息的类型字段取消息。
每个队列都有一个msqid_ds结构与其相关。此结构规定了队列的当前状态
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first;
struct msg *msg_last;
ulong msg_cbytes;
ulong msg_qnum;
ulong msg_qbytes;
pid_t msg_lspid;
pid_t msg_lrpid;
time_t msg_stime;
time_t msg_rtime;
time_t msg_ctime;
}
两个指针msg_first 和msg_last分别指向相应消息在内核中存放的位置,所以他们对用户进程而言是无价值的。
影响消息队列的系统限制

  • MSGMAX 可发送的最长消息的字节长度
  • MSGMNB 特定队列的最大字节长度
  • MSGMNI 系统中最大消息队列数
  • MSGTOL 系统中最大消息数

调用的第一个函数通常是msgget,其功能是打开一个现存的对垒或者创建一个新队列。
int msgget(key_t key, int flag)
当创建一个新的队列时,初始化msgqid-ds结构的下列成员

  • ipc-perm 该结构中mode按flag中的相应许可权限位设置。
  • msg_qnum,msg_lspid,msg_lrpid,msg_stime和msg_rtime都设置为0
  • msg_ctime 设置为当前时间
  • msg_qbytes设置为系统限制值

若执行成功则返回非负队列id,此后此值就可被用于其他三个消息队列函数。
msgctl函数对队列执行多种操作。它以及另外两个与信号量和共享存储有关的函数是系统v ipc的类似于ioctl的函数
int msgctl(int msqid, int cmd, struct msgqid_dsbuf)
cmd参数指定对于由msqid规定的队列要执行的命令:

  • IPC_STAT 取此队列的msqid_ds结构,并将其存放在buf指向的结构中。
  • IPC_SET 按由buf指向的结构中的值设置与此队列此昂管的结构中的下列四个字段:msg_perm.uid,msg_perm.gid,msg_perm.mode,msg_qbytes。此命令你只能由下列两种进程执行:一种是其有效用户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)也可用于信号量和共享存储
调用msgsnd将数据放到消息队列上
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
正如前面提及到的,每个消息都由三个部分组成,他们是:正长整型类型字段、非负长度以及实际数据字节。消息总是放在队列尾端。
ptr指向一个长整型数,它包含了正整形消息类型,在气候立即跟随了消息数据。若发送的最长消息是512字节,则可以定义下列结构
struct mymesg {
log mtype;
char mtext[512];
}
于是。ptr就是一个指向mymesg结构的指针。接收者可以使用消息类型以先进先出的次序取消息。
flag的值可以指定为IPC_NOWAIT.这类似于文件io的非阻塞io标志。若消息队列已满(或者是队列中的消息总数等于系统限制值,或队列中的字节总数等于系统限制值),则指定IPC_NOWAIT使得msgsnd立即出错返回EAGAIN.如果没有指定IPC_NOWAIT,则进程阻塞直到有空间可以容纳要发送的消息或从系统中删除了此队列或捕捉到一个信号,并从信号处理程序返回。在第二种情况下返回EIDRM最后一种情况则返回EINTR
注意,对消息队列删除的处理不是很完善。因为对每个消息队列并没有设置一个引用计数器,所以删除一个队列使得仍在使用这一队列的进程在下次队列操作时出错返回。信号量机构也以同样方式处理其删除。删除一个文件则要等到使用该文件的最后一个进程关闭了它,才能删除文件的内容。
msgrcv从队列中取用消息
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag)
如同msgsnd一样ptr参数指向一个长整数,跟随其后是存放实际消息数据的缓存。nbytes说明数据缓存的长度。若返回的消息大于nbytes,而且在flag中设置了MSG_NOERROR,则该消息被截短,如果没有设置这一标志,而消息又太长,则出错返回E2BIG
参数type使我们可以指定想要哪一种消息:

  • type==0返回队列中的第一个消息
  • type>0返回队列消息类型为type的第一个消息
  • type < 0返回队列中消息类型值小于或等于type绝对值,而且在这种消息中其类型值又最小的消息。

非0type用于以先进先出次序读消息。例如若应用程序对消息赋值优先权,那么tyoe就可以是优先权值。如果一个消息队列由多个客户机和一个服务器使用,那么type字段可以用来包含客户机进程id.
可以指定flag值为IPC_NOWAIT,使操作不阻塞。这使得如果没有所指定类型的消息则msgrcv出错返回ENOMSG.如果没有指定IPC_NOWAIT,则进程阻塞直至有了指定类型的消息。或系统中删除了此队列(出错返回EDRM)或捕捉到一个信号并从信号处理程序返回(出错返回EINTR)
消息队列原来实施的目的是提供比一般IPC更高速的进程通信方法,但现在与其他形式的IPC相比,在速度方面已经没有什么差别了。考虑到消息队列具有的问题,我们得出的结论是在新的应用程序中不应当再使用它们。

信号量

信号量与已经介绍过的IPC机构(管道,FIFO以及消息队列)不同。它是一个计数器,用于多进程对共享数据对象的存取。为了获得共享资源,进程需要指向下列操作。

  • 测试控制该资源的信号量
  • 如果此信号量为正,则进程可以使用该资源。进程将信号量减1,表示它使用了一个资源单位。
  • 若此信号量的值为0,则进程进入睡眠装填,直至信号量值大于0.若进程被唤醒后,它返回至1

当进程不在使用由一个信息量控制的共享资源时,该信号量值增至1.如果有进程正在睡眠等待此信号量则唤醒他们。
为了正确的实现信息量,信号量的测试及减1操作应当是原子操作。为此,信号量通常在内核中实现。
常用的信号量形式被称为双态信号量。它控制单个资源,其初始值为1.但是一般而言,信号量的初始值可以是任一正值,该值说明有多少个共享资源单位可供共享应用。
不幸的是,系统V的信号量与此相比要复杂得多。三种特性造成了这种非必要的复杂性

  • 信号量并非是一个非负值,而必须将信号量定义为含有一个或者多个信号量值得集合。当创建一个信号量时,要制定该集合中的各个值
  • 创建信息量与对其赋初值分开。这是一个致命的弱点,因为不能原子地创建一个信号量集合,并且对该集合中的所有值赋初值。
  • 即使没有进程正在使用各种形式的系统V IPC,它们仍然是存在的,所以不得不为这种程序担心,它在终止时并没有释放已经分配给它的信号量。

内核为每个信号量设置了一个semid_ds结构
struct semid_ds {
struct ipc_perm sem_perm;
struct sem *sem_base;
ushort sem_sems;
time_t sem_otime;
time_t sem_ctime;
}
对用户而言,sem_base指针是没有价值的,它指向内核中sem机构数组,该数组中包含了sem_nsems个元素,每个元素各对应于集合中的一个信号量值。
struct sem {
ushort semval;
pid_t sempid;
ushort semncnt;
ushort semzcnt;
}
要调用的第一个函数是semget以获得一个信号量id
int semget(key_t key, int nsems, int flag)
如果需要创建一个新的集合时,对semid_ds结构的下列成员赋初值

  • 对ipc_perm结构赋初值。该结构中的mode被设置为flag中的相应许可权。
  • sem_otime设置为0
  • sem_ctime设置为当前时间
  • sem_nsems设置为nsems

nsems是该集合中的信号量数。如果是创建新集合,则必须制定nsems.如果引用一个现存的集合,则将nsems指定为0
semctl函数包含了多种信号量操作
int semctl(int semid, int semnum, int cmd, union semumarg)
最后一个参数是个union
union semun {
int val;
struct semid_ds *buf;
ushort *array;
}
cmd 参数指定下列十种命令中的一种,使其在semid指定的信号量集合上执行此命令。其中有五条命令是针对一个特定的信号量的值,他们用semnum指定该集合中的一个成员。semnum值在0和nsems-1之间

  • IPC_STAT 对刺激和取semid_ds结构,并存放在由arg.buf指向的结构中
  • IPC_SET 按由arg.buf指向的机构中的值设置与此集合相关结构中的下列三个字段值:sem_perm.uid,sem_perm.gid,sem_perm.mode.此命令只能由下列两种进程执行:一种是其有效用户ID等于sem_perm.cuid或sem_perm.uid的进程;另一种是具有超级用户特权的进程
  • IPC_RMID 从系统中删除该信号量。这种删除是立即的。仍在使用此信号量的其他进程在他们下次意图对此信号量进行操作时,将出错返回EIDRM.此命令只能由下列两种进程执行:一种是具有有效用户id等于sem_perm.cuid或者sem_perm.uid的进程;另一种是具有超级用户特权的进程。
  • GETVAL 返回成员semnum的semval值
  • SETVAL 是指成员semnum的semval值。该值由arg.val指定
  • GETPID 返回成员semnum的sempid值
  • GETNCNT 返回成员semnum的semncnt值
  • GETALL 取该集合中的所有信号量的值,并将它们存放在array指向的数组中。
  • SETALL 按array中指向的额数组中的值设置该集合中的所有信号量的值

函数semop自动执行信号量集合上的操作数组
int semop(int semid, struct sembug semoparray[], size_t nops)
semoparray是一个指针,它指向一个信号量操作数组
struct sembuf {
ushort sem_num;
short sem_op;
short sem_flg;
}
nops规定该数组中操作的数量
对集合中每个成员的操作由相应的sem_op规定。此值可以是负值、0、正值

  • 最容易处理的情况是sem_op为正。这对应于返回进程占用的资源。sem_op值加到信号量的值上。如果指定了undo标志,则也从该进程的此信号量调整值中减去sem_op
  • 若sem_op为负,则表示要获取由该信号量控制的资源。
    如若该信号量的值大于或等于sem_op的绝对值,则从信号量值中减去sem_op的绝对值。这保证信号量的结果值大于等于0.如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量的调整值上。
    如果信号量值小于sem_op的绝对值,则
    1、若指定了IPC_NOWAIT 则出错返回EAGAIN;
    2、若未指定IPC_NOWAIT ,则该信号量的semncnt值加1,然后调用进程被挂起直至a、此信号量编程大于或等于sem_op的绝对值(即某个进程已经释放了某些资源)。此信号量的semncnt值减1,并且从信号量值中减去sem_op的绝对值。如果指定了undo标准,则sem_op的绝对值也加到该进程此信号量调整值上。b、从系统中删除了此信号量。在此情况下,函数出错返回ERMID。c、进程捕捉到一个信号,并从信号处理程序返回,在此情况下,此信号量的semncnt值减1,并且函数出错返回EINTR
  • 若sem_op为0,这表示希望等待到该信号量值变成0.如果此信号量值非0则:1、若指定了IPC_NOWAIT,则出错返回EAGAIN 2、若未指定IPC_NOWAIT,则该信号量的semncnt值加1(因为将进入睡眠状态),然后调用进程被挂起,直至a、此信号量值变成0.此信号量semzcnt减1(因为已经结束等待)b、从系统中删除了此信号量。在此情况下函数出错返回ERMID.c、进程捕捉到一个信号,并从信号处理程序返回。在此种情况下,此信号量的semzcnt值减1,并且函数出错返回EINTR

semop具有原子性,因为它或者执行数组中的所有操作,或者一个也不做
exit时的信号量调整
正如前面提到的,如果在进程终止时,它暂用了经由信号量分配的资源,那么就会成为一个问题。无论何时只要微信号量操作指定了SEM_UNDO标志,然后分配资源sem_op值小于0那么内核就会记住对于该特定信号量,分配给我们多少资源。当该进程终止时,不论资源或者不自愿,内核豆浆检验该进程是否还有尚未处理的信号量调整值,如果有,则按调整值对应相应量今次那个调整。
如果用带SETVAL或SETALL命令的semctl设置一信号量的值,则在所有进程中,该信号量的调整值都设置为0
虽然记录所稍慢于信号量,但如果只需锁一个资源,并且不需要使用系统v信号量所有的花哨功能,则宁可使用记录锁,理由是,使用简易。进程终止时,会处理任一遗留下的锁。

共享存储。

推荐阅读更多精彩内容