首页 > 系统服务 > 详细

Unix15-进程间通信

时间:2020-03-08 00:27:58      阅读:73      评论:0      收藏:0      [点我收藏+]

Unix编程第15章-进程间通信

15.1 

进程之间通信IPC(InterProcess Communication)。

本章讨论经典的IPC:管道、FIFO、消息队列、信号量以及共享存储。

 

15.2 管道

管道是Unix系统IPC的最古老形式,所有Unix系统都提供此种通信机制。管道有以下两种局限性:

1)历史上,它们是半双工的(即数据只能在一个方向上流动)。现在,某些系统提供全双工管道,但是为了最佳的可移植性,我们决不应预先假定系统支持全双工管道。

2)管道只能在具有公共祖先的两个进程之间使用。通常,一个管道由一个进程创建,在进程调用fork后,这个管道就能在父进程和子进程之间使用了。

 

FIFO没有第二种局限性,Unix域套接字没有这两种局限性。

 

尽管有局限性,半双工管道仍是最常用的IPC形式。每当在管道中键入一个命令序列,让shell执行时,shell都会为每一条命令单独创建一个进程,然后用管道将前一条命令进程的标准输出与后一条命令的标准输入相接。( “|” )。

管道是通过调用pipe函数创建的:

#include<unistd.h>

int pipe( int fd[2] );

  返回值:若成功,返回0;若出错,返回-1。

经由参数,fd返回两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。将内容写入fd[1],再将内容从fd[1]读出。

 

管道的实现既可以是在用户进程中相互连接,也可以通过内核流动。

fstat函数对管道的每一端都返回一个FIFO类型的文件描述符。可以用S_ISFIFO宏来测试管道。

 

单个进程中管道几乎没什么用处。通常,进程会先调用pipe,再接着调用fork,从而创建从父进程到子进程的IPC管道,反之亦然。。

fork之后做什么取决于我们想要的数据流的方向。对于从父进程到子进程的管道,父进程关闭管道的读端fd[0],子进程关闭写端fd[1]。

对于一个从子进程到父进程的管道,父进程关闭fd[1],子进程关闭fd[0]。

 

当管道的一端被关闭后,下列两条规则起作用:

1)当读(read)一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。(从技术上讲,如果管道的写端还有进程,就不会产生文件的结束。可以复制一个管道的描述符,使得有多个进程对它具有写打开文件描述符。但是,通常一个管道只有一个读进程和一个写进程。而FIFO单个读可能对应于多个写进程。)

2)如果写(write)一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno设置为EPIPE。

 

在写管道(或FIFO)时,常量PIPE_BUF规定了内核的管道缓冲区的大小如果对管道调用write,而且要求写的字节数小于等于PIPE_BUF,则此操作不会与其它进程对同一管道(或FIFO)的write操作交叉进行。但是,若有多个进程同时写一个管道(或FIFO),而且我们要求写的字节数超过PIPE_BUF,那么我们所写的数据可能会与其它进程所写的数据相互交叉。用pathconf或fpathconf函数可以确定PIPE_BUF的值。

 

 

15.3 函数popen和pclose

常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据,为此,标准I/O库提供了两个函数popen和pclose。这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,然后等待命令终止。

#include<stdio.h>

FILE *popen( const char *cmdstring, const char *type);

  返回值:若成功,返回文件指针;若出错,返回NULL。

int pclose( FILE *fp );

  返回值:若成功,返回cmdstring的终止状态;若出错,返回-1。

函数popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O文件指针。如果type为"r",则文件指针连接到cmdstring的标准输出。

如果type是"w",则文件指针连接到cmdstring的标准输入

 

pclose函数关闭标准I/O流,等待命令终止,然后返回shell的终止状态。如果shell不能被执行,则pclose返回的终止状态与shell已执行exit(127)一样。

POXIS.1要求popen关闭那些以前调用popen打开的、现在仍然在子进程中打开着的I/O流。为此,在子进程中从头逐个检查childpid数组的各个元素,关闭仍旧打开着的描述符。

若pclose的调用者已经为信号SIGCHLD设置了一个信号处理程序,则pclose中的waitpid调用将返回一个错误EINTR。(pclose通过wait系列调用等待popen函数创建的子进程结束。)

 

 

15.4 协同进程

Unix系统过滤程序从标准输入读取数据,向标准输出写数据几个过滤程序通常在shell管道中线性连接。当一个过滤程序即产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了协同进程(coprocess)。

协同进程通常在shell的后台运行,其标准输入和标准输出通过管道连接到另一个程序。

popen只提供连接到另一个进程的标准输入或标准输出的一个单向管道,而协同进程则有连接到另一个进程的两个单向管道:一个接到其标准输入,另一个则来自其标准输出。

 

 

15.5 FIFO

FIFO有时被称为命名管道。未命名的管道只能在两个相关的进程之间使用,而且这两个相关的进程还要有一个共同的创建了它们的祖先进程。但是,通过FIFO,不相关的进程也能交换数据。

FIFO是一种文件类型。通过stat结构的st_mode成员的编码可以知道文件是否是FIFO类型。可以用S_ISFIFO宏对此进行测试。

创建FIFO类似于创建文件。确实,FIFO的路径名存在于文件系统中。

#include<sys/stat.h>

int mkfifo( const char *path, mode_t mode);

int mkfifoat( int fd, const char *path, mode_t mode);

  两个函数的返回值:若成功,返回0;若出错,返回-1。

mkfifo函数中mode参数的规格说明与open函数中mode相同。新FIFO的用户和组的所有权规则与普通文件相同。

mkfifoat函数和mkfifo函数相似,但是mkfifoat函数可以被用来在fd文件描述符表示的目录相关的位置创建一个FIFO。像其它*at函数一样,有三种情形:

1)如果path参数指定的是绝对路径名,则fd参数会被忽略掉,并且mkfifoat函数的行为和mkfifo类似。

2)如果path参数指定的是相对路径名,则fd参数是一个打开目录的有效文件描述符,路径名和目录有关。

3)如果path参数指定的是相对路径名,并且fd参数有一个特殊值AT_FDCWD,则路径名以当前目录开始,mkfifoat和mkfifo类似。

 

当我们用mkfifo或者mkfifoat创建FIFO时,要用open来打开它。

当open一个FIFO时,非阻塞标志(O_NONBLOCK)会产生下列影响。

1)在一般情况下(没有指定O_NONBLOCK),只读open要阻塞到某个其它进程为写而打开这个FIFO为止。类似地,只写open要阻塞到某个其它进程为读而打开它为止。

2)如果指定了O_NONBLOCK,则只读open立即返回。但是,如果没有进程为读而打开一个FIFO,那么只写open将返回-1,并将errno设置成ENXIO。

 

类似于管道,若write一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE。若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志。

一个给定的FIFO有多个写进程是常见的。这就意味着,如果不希望多个进程所写的数据交叉,则必须考虑原子操作。和管道一样,常量PIPE_BUF说明了可被原子地写到FIFO的最大数据量。

FIFO有以下两种用途:

1)shell命令使用FIFO将数据从一条管道传送到另一条时,无需创建中间临时文件。

2)客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程二者之间传递数据。

 

用FIFO复制输出流:

FIFO有名字,可用于非线性连接。

 

使用FIFO进行客户进程-服务器进程通信:

如果有一个服务器进程,它与很多客户进程有关,每个客户进程都可将其请求写到一个该服务器进程创建的众所周知的FIFO中。因为该FIFO有多个写进程,所以客户进程发送给服务器进程的请求的长度要小于PIPE_BUD字节。这样就能避免客户进程的多次写之间的交叉。

这种情况下的问题是:服务器进程如何将回答送回各个客户进程。不能使用单个FIFO,因为客户进程可能不知道何时去读它们的响应以及何时响应其它客户进程。

一种解决方法是,每个客户进程都在其请求中包含它的进程ID。然后服务器进程为每个客户进程创建一个FIFO,所使用的路径名是以客户进程创建一个FIFO,所使用的路径名是以客户进程的进程ID为基础的。

虽然这种安排可以工作,但服务器进程不能判断一个客户进程是否崩溃终止,这就使得客户进程专用FIFO会遗留在文件系统中。另外,服务器进程还必须得捕捉SIGPIPE信号,因为客户进程在发送一个请求后有可能没有读取响应就终止了,于是留下一个只有写进程(服务器进程)而无读进程的客户进程专用FIFO。

所以在服务器与客户端进程之间有两种FIFO:一种是众所周知的FIFO(客户写服务器读)、一种是每个客户进程ID对应的普通FIFO(服务器写,客户读)。这时如果服务器以只读方式打开众所周知的FIFO,则每当客户进程个数从1变为0时,服务器进程就将在FIFO中读到一个文件结束标志。为使服务器进程避免处理这种情况,一种常用技巧是使服务器进程以读-写方式打开该众所周知的FIFO。

 

 

15.6 XSI IPC

有3种称作XSI IPC的IPC:消息队列、信号量以及共享存储器。

 

15.6.1 标识符和键

每个内核中的IPC结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符加以引用。

与文件描述符不同,IPC标识符不是小的整数,当一个IPC结构被创建,然后又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大正值,然后又回转到0.

标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。为此,每个IPC对象都与一个键key相关联,将这个键作为该对象的外部名。

无论何时创建IPC结构(通过调用msgget、semget或shmget创建),都应指定一个键。这个键的数据类型是基本系统数据类型key_t,通常在头文件<sys/types.h>中被定义成长整型。这个键由内核变换成标识符。

 

有多种方法使客户进程和服务器进程在同一IPC结构上汇聚

1)服务器进程可以指定键IPC_PRIVATE创建一个新IPC结构,将返回的标识符存放在某处(如一个文件)以便客户进程取用。键IPC_PRIVATE保证服务器进程创建一个新IPC结构。这种技术的缺点是:文件系统操作需要服务器进程将整型标识符写到文件中,此后客户进程又要读这个文件取得此标识符。

  IPC_PRIVATE键也可用于父进程子关系。父进程指定IPC_PRIVATE创建一个新IPC结构,所返回的标识符可供fork后的子进程使用。接着,子进程又可将此标识符作为exec函数的一个参数传给一个新程序。

2)可以在一个公用头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定此键创建一个新的IPC结构。这种方法的问题是该键可能已与一个IPC结构相结合,在此情况下,get函数(msgget、semget或shmget)出错返回。服务器进程必须处理这一错误,删除已存在的IPC结构,然后试着再创建它。

3)客户进程和服务器进程认同一个路径和项目ID(项目ID是0~255之间的字符值),接着,调用函数ftok将这两个值变换为一个键。然后在方法2)中使用此键。ftok提供的唯一服务就是由一个路径名和项目ID产生一个键。

#include<sys/ipc.h>

key_t ftok( const char *path, int id);

  返回值:若成功,返回键;若出错,返回(key_t)-1。

path参数必须引用一个现有的文件。当产生键时,只使用id参数的低8位。

ftok创建的键通常是用下列方式构成的:按给定的路径名取得其stat结构中的部分st_dev和st_ino字段,然后再将它们与项目ID组合起来。

 

3个get函数(msgget、semget和shmget)都有两个类似的参数:一个key_t型键和一个整型flag。在创建新的IPC结构(通常由服务器进程创建)时,如果key_t键是IPC_PRIVATE或者和当前某种类型的IPC结构无关,则需要指明flag的IPC_CREAT标志位。为了引用一个现有队列(通常由客户进程创建),key_t键必须等于队列创建时指明的键值,并且IPC_CREAT必须不被指明。

如果希望创建一个新的IPC结构,而且要确保没有引用具有同一标识符的一个现有IPC结构,那么必须在flag中同时指定IPC_CREAT和IPC_EXCL位。这样做了以后,如果IPC结构已经存在就会造成出错,返回EEXIST(这与指定了O_CREAT和O_EXCL标志的open类似)。

 

15.6.2 权限结构

XSI IPC为每个IPC结构关联了一个ipc_perm结构。该结构规定了权限和所有者,它至少包括下列成员:

struct ipc_perm{

  uid_t  uid;

  gid_t  gid;

  uid_t  cuid;

  gid_t  cgid;

  mode_t  mode;

};   //所有者和创建者的有效用户ID和组ID,权限

在创建IPC结构时(三个get函数),对所有的字段都赋初值。以后,可以调用msgctl、semctl或shmctl修改uid、gid和mode字段。为了修改这些值,调用进程必须是IPC结构的创建者或超级用户。修改这些字段类似于对文件调用chown和chmod。

mode字段类似open创建文件时的权限取同类值,但是对于任何IPC都不存在执行权限。消息队列和共享存储使用术语“读”和“写”,信号量使用术语“读”和“更改”。

 

15.6.3 结构限制

XSI IPC三种结构都有内核限制,可通过重新配置内核来改变。

 

15.6.4 优点和缺点

XSI IPC的一个问题是:IPC结构是在系统范围内起作用的,没有引用计数。例如,如果进程创建了一个消息队列,并且在该队列中放入了几则消息,然后终止,那么该消息队列及其内容不会被删除。它们会一直留在系统中直至发生下列动作为止:由某个进程调用msgrecv或msgctl读消息或删除消息队列;或某个进程执行ipcrm命令删除消息队列;或正在自举的系统删除消息队列。将此与管道相比,当最后一个引用管道的进程终止时,管道就被完全地删除了。对于FIFO而言,在最后一个引用FIFO的进程终止时,虽然FIFO的名字仍保留在系统中,直至被显示地删除,但是留在FIFO中的数据已被删除了

XSI IPC的另一个问题是:这些IPC结构在文件系统中没有名字。为了支持这些IPC对象,内核中增加了十几个新的系统调用(msgget、semop、shmat等)。ipcs和ipcrm。

 因为这些形式的IPC不使用文件描述符,所以不能对它们使用多路转接I/O函数。这使得它很难一次使用一个以上这样的IPC结构,或者在文件或设备I/O中使用这样的IPC结构。

 

 

15.7 消息队列

消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。

msgget用于创建一个新队列或打开一个现有队列。msgget将新消息添加到队列尾端。每个消息包含一个正的长整型类型的字段,一个非负的长度以及实际数据字节数(对应于长度),所有这些都在将消息添加到队列时,传送给msgsnd。msgrcv用于从队列中取消息。我们并不一定要以先进先出次序取消息,也可以按消息的类型字段取消息。

每个队列都有一个msqid_ds结构与其相关联:

struct msqid_ds{

  struct ipc_perm  msg_perm;  //XSI IPC权限结构

  msgqnum_t  msg_qnum;  //消息队列号

  msglen_t  msg_qbytes;  //消息队列的最大字节数

  pid_t  msg_lspid;  //最后一个msgend()的进程ID

  pid_t  msg_lrpid;  //最后一个msgrcv()的进程ID

  time_t  msg_stime;  //最后一次msgsnd()的时间

  time_t  msg_rtime;  //最后一次msgrcv()的时间

  time_t  msg_ctime;  //最后一次改变的时间

};

此结构定义了队列的当前状态。

消息队列的系统限制:可发送的最长消息的字节数、一个特定队列的最大字节数(即队列中所有消息长度之和)、系统中最大消息队列数、系统中最大消息数。

 

调用的第一个函数通常是msgget,其功能是打开一个现有队列或创建一个新队列:

#include<sys/msg.h>

int msgget( key_t key, int flag);

  返回值:若成功,返回消息队列ID;若出错,返回-1。

在在创建新队列时,要初始化msqid_ds结构的下列成员:

-ipc_perm结构按15.6.2中所述进行初始化。该结构中的mode成员按flag中的相应权限位设置,这些权限用下表中的值指定。

权限
用户读 0400
用户写(更改) 0200
组读 0040
组写(更改) 0020
其它读 0004
其它写(更改) 0002

 

 

 

 

 

 

-msg_qnum、msg_lspid、msg_lrpid、msg_stime和msg_rtime都设置为0.

-msg_ctime设置为当前时间。

-msg_qbytes设置为系统限制值。

 

若执行成功,msgget返回非负队列ID。此后,该值就可被用于其它3个消息队列函数中。

 

msgctl函数对队列执行多种操作。它和另外两个与信号量及共享存储有关的函数(semctl和shmctl)都是XSI IPC类似于ioctl的函数(亦即垃圾桶函数)。

#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  将字段msg_perm.uid、msg_perm.gid、msg_perm.mode和msg_qbytes从buf指向的结构复制到与这个队列相关的msqid_ds结构中。此命令只能由下列的两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid,另一种是具有超级用户特权的进程。只有超级用户才能增加msg_qbytes的值。

-IPC_RMID  从系统中删除该消息队列以及仍在该队列中的所有数据。这种删除立即生效。仍在使用这一消息队列的其它进程在它们下一次试图对此队列进行操作时,将得到EIDRM错误。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid;另一种是具有超级用户特权的进程。

 

这3条命令(IPC_STAT、IPC_SET和IPC_RMID)也可用于信号量和共享存储。

 

调用msgsnd函数将数据放到消息队列中:

#include<sys/msg.h>

int msgsnd( int msqid, const void *ptr, size_t nbytes, int flags);

  返回值:若成功,返回0;若失败,返回-1。

每个消息都由3部分组成:一个正的长整型类型的字段、一个非负的长度(nbytes)以及实际数据字节数(对应于长度)。消息总是放在消息队列尾端。

ptr参数指向一个长整型数,它包含了正的整形消息类型其后紧接着的是消息数据(若nbytes是0,则无消息数据)。若发送的最长消息是512字节的,则可定义下列结构:

struct mymesg{

  long mtype;

  char mtext[512];

};

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(消息仍留在队列中)。

参数type可以指定想要哪一种消息:

type==0  返回队列中的第一个消息。

type>0  返回队列中消息类型为type的第一个消息。

type<0  返回队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。

 

可以将flag值指定为IPC_NOWAIT,使操作不阻塞,这样,如果没有所指定类型的消息可用,则msgrcv返回-1,errno设置为ENOMSG。

如果没有指定IPC_NOWAIT,则进程会一直阻塞到有了指定类型的消息可用,或者从系统中删除了此队列(返回-1,errno设置为EIDRM),或者捕捉到一个信号并从信号处理程序返回(这会导致msgrcv返回-1,errno设置为EINTR)。

 

msgrcv成功执行时,内核会更新与该消息队列相关联的msqid_ds结构,以指示调用者的进程ID(msg_lrpid)和调用时间(msg_rtime),并指示队列中的消息数减少了1个(msg_qnum)。

 

15.8 信号量

XSI IPC中的信号量相比POSIX信号量有些不足。后面介绍POSIX信号量使用方式。

 

15.9 共享存储

共享存储允许两个或多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种IPC。使用共享存储时要掌握的唯一窍门是,在多个进程之间同步访问一个给定的存储区。若服务器进程正在将数据放入共享存储区,则在它做完这一操作之前,客户进程不应当去取这些数据。通常,信号量用于同步共享存储访问。(不过也可以用记录锁或互斥量。)

 

在高级I/O那章中,已经介绍了共享存储的一种形式,就是在多个进程将同一文件映射到它们的地址空间的时候。XSI共享存储和内存映射的文件的不同之处在于,前者没有相关问价。XSI共享存储段是内存的匿名段。

内核为每个共享存储段维护着一个结构,该结构至少要为每个共享存储段包含以下成员:

struct shmid_ds{

  struct ipc_perm  shm_perm;

  size_t  shm_segsz;  //共享存储段的字节数

  pid_t  shm_lpid;  //上次shmop()的pid

  pid_t  shm_cpid;  //创建者的pid

  shmatt_t  shm_nattach;  //当前attaches的数目

  time_t  shm_atime;  //上次attach的时间

  time_t  shm_dtime;  //上次detach的时间;

  time_t  shm_ctime;  //上次change的时间

};

按照支持共享存储段的需要,每种实现会增加其它结构成员。

shmatt_t类型定义为无符号整型,它至少与unsigned short一样大。

共享存储的限制:共享存储段的最大字节长度、共享存储段的最小字节长度、系统中共享存储段的最大段数、每个进程共享存储段的最大段数。

 

调用的第一个函数通常是shmget,它获得一个共享存储标识符。

#include<sys/shm.h>

int shmget( key_t key, size_t size, int flag);

  返回值:若成功,返回共享存储ID;若出错,返回-1。

当创建一个新段时,初始化shmid_ds结构的下列成员:

-ipc_perm结构初始化,该结构中的mode按flag中的相应权限位设置。

-shm_lpid、shm_nattach、shm_atime和shm_dtime都设置为0.

-shm_ctime设置为当前时间。

-shm_segsz设置为请求的size。

 

参数size是该共享存储段的长度,以字节为单位。实现通常将其向上取为系统页长的整倍数。但是,若应用指定的size值并非系统页长的整倍数,那么最后一页的余下部分是不可使用的。如果正在创建一个新段(通常在服务器进程中),则必须指定其size。如果正在引用一个现存的段(一个客户进程),则将size指定为0.当创建一个新段时,段内的内容初始化为0。

 

shmctl函数对共享存储段执行多种操作。

#include<sys/shm.h>

int shmctl( int shmid, int cmd, struct shmid_ds *buf);

  返回值:若成功,返回0;若出错,返回-1。

cmd参数指定下列5中命令中的一种,使其在shmid指定的段上执行。

IPC_STAT  取此段的shmid_ds结构,并将它存储在由buf指向的结构中。

IPC_SET  按buf指向的结构中的值设置与此共享存储段相关的shmid_ds结构中的下列3个字段:shm_perm.uid、shm_perm.gid和shm_perm.mode。此命令只能由下列两种进程执行:一种是其有效用户ID等于shm_perm.uid或shm_eprm.cuid的进程;另一种进程是具有超级用户特权的进程。

-IPC_RMID  从系统中删除该共享存储段。因为每个共享存储段维护着一个连接计数(shmid_ds结构中的shm_nattach字段),所以除非使用该段的最后一个进程终止或该段分离,否则不会实际上删除该存储段。管此段是否仍在使用,该段标识符都会被立即删除,所以不能再用shmat与该段连接。此命令只能由下列两种进程执行:一种是其有效用户ID等于shm_perm.uid或shm_perm.cuid的进程;另一种是具有超级用户特权的进程。

Linux和Solaris提供了另外两种命令:

-SHM_LOCK  在内存中对共享存储段加锁。此命令只能由超级用户执行。

-SHM_UNLOCK  解锁共享存储段。此命令只能由超级用户执行。

 

一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中。

#include<sys/shm.h>

void *shmat( int shmid, const void *addr, int flag);

  返回值:若成功,返回指向共享存储段的指针;若出错,返回-1。

共享存储段连接到调用进程的哪个地址上与addr参数以及flag中是否指定SHM_RND位有关。

-如果addr为0,则此段连接到由内核选择的第一个可用地址上。这是推荐使用方式。

-如果addr非0,并且没有指定SHM_RND,则此段连接到addr所制定的地址上。

-如果addr非0,并且指定了SHM_RND,则此段连接到(addr-(addr mod SHMLBA))所表示的地址上。SHM_RND命令的意思是“取整”。SHMLBA的意思是“低边界地址倍数”,它总是2的乘方。该算式是将地址向下取最近个SHMLBA的倍数。

 

如果在flag中指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。

shmat的返回值是该段所连接的实际地址,如果出错则返回-1.如果shmat成功指向,那么内核将使与该共享存储段相关的shmid_ds结构中的shm_nattach计数器值加1。

当对共享存储段的操作已经结束时,则调用shmdt与该段分离。注意,这并不从系统中删除其标识符以及其相关的数据结构。该标识符仍然存在,直至某个进程(一般是服务器进程)带IPC_RMID命令的调用shmctl特地删除它为止。

#include<sys/shm.h>

int shmdt( const void *addr);

  返回值:若成功,返回0;若出错,返回-1。

addr参数是以前调用shmat时的返回值。如果成功,shmdt将使相关shmid_ds结构中的shm_nattach计数器值减1。

 

 

15.10 POSIX信号量

POSIX信号量接口意在解决XSI信号量接口的几个缺陷:

-相比XSI接口,POSIX信号量接口考虑到了更高性能的实现。

-POSIX信号量接口使用更简单:没有信号量集,在熟悉的文件系统操作后一些接口被模式化了。尽管没有要求一定要在文件系统中实现,但是一些系统的确是这么实现的。

-POSIX信号量在删除时表现更完美。

 

POSIX信号量有两种形式:命名的和未命名的。它们的差异在于创建和销毁的形式上,但其它工作一样。未命名的信号量只存在于内存中,并要求能使用信号量的进程必须可以访问内存。这意味着他们只能应用在同一进程中的线程,或者不同进程中已经映射相同内存内容到它们的地址空间中的线程。相反,命名信号量可以通过名字访问,因此可以被任何已知它们名字的进程中的线程使用。

我们可以使用·sem_open函数来创建一个新的命名信号量或者使用一个现有信号量。

#include<semaphore.h>

sem_t *sem_open( const char *name, int oflag, ... /* mode_t mode, unsigned int value */);

  返回值:若成功,返回指向信号量的指针;若出错,返回SEM_FAILED。

当使用一个现有的命名信号量时,我们仅仅指定两个参数:信号量的名字和oflag参数的0值。当这个oflag参数有O_CREAT标志集时,如果命名信号量不存在,则创建一个新的。如果它已经存在,则会被使用,但是不会有额外的初始化发生。

当我们指定O_CREAT标志时,需要提供两个额外的参数。mode参数指定谁可以访问信号量。mode的取值和打开文件的权限位相同:用户读、用户写、用户执行、组。。。。。赋值给信号量的权限可以被调用者的文件创建屏蔽字修改。注意,只有读和写访问要紧,但是当我们打开一个现有信号量时接口不允许指定模式。实现经常为读和写打开信号量。

在创建信号量时,value参数用来指定信号量的初始值。它的取值是0~SEM_VALUE_MAX。

如果我们想确保创建的是信号量,可以设置pflag参数为O_CREAT|O_EXCL。如果信号量已经存在,会导致sem_open失败。

为了增加可移植性,在选择信号量命名时必须遵循一定的规则:

-名字的第一个字符应该为“/”;

-名字不应包含其它斜杠以此避免实现定义的行为;

-信号量名字的最大长度是实现定义的。名字长度不应该超过_POSIX_NAME_MAX。

 

如果想在信号量上进行操作,sem_open函数会为我们返回一个信号量指针,用于传递到其它信号量函数上。当完成信号量操作时,可以调用sem_close函数来释放任何信号量相关的资源。

#include<semaphore.h>

int sem_close(sem_t *sem);

  返回值:若成功,返回0;若出错,返回-1。

如果进程没有首先调用sem_close而退出,那么内核将自动关闭任何打开的信号量。

 

可以使用sem_unlink函数来销毁一个命名信号量。

#include<semaphore.h>

int sem_unlink( const char *name);

  返回值:若成功,返回0;若出错,返回-1。

sem_unlink函数删除信号量的名字。如果没有打开的信号量引用,则该信号量会被销毁。否则,销毁将延迟到最后一个打开的引用关闭。

 

可以使用sem_wait或者sem_trywait函数来实现信号量的减1操作。

#include<semaphore.h>

int sem_trywait( sem_t *sem);

int sem_wait(sem_t *sem);

  两个函数的返回值:若成功,返回0;若出错,返回-1。

使用sem_wait函数时,如果信号量计数是0就会发生阻塞。直到成功使信号量减1或者被信号中断才返回。可以使用sem_trywait函数来避免阻塞。调用sem_trywait时,如果信号量是0,则不会阻塞,而是会返回-1并且将errno置为EAGAIN。

 

第三个选择是阻塞一段确定的时间。为此,可以使用sem_timedwait函数。

#include<semaphore.h>

#include<time.h>

int sem_timedwait( sem_t *restrict sem, const struct timespec *restrict tsptr);

  返回值:若成功,返回0;若出错,返回-1。

 

可以调用sem_post函数使信号量值增1.这和解锁一个二进制信号量或者释放一个计数信号量相关的资源的过程是类似的。

#include<semaphore.h>

int sem_post(sem_t *sem);

  返回值:若成功,返回0;若出错,返回-1。

调用sem_post时,如果在调用sem_wait(或者sem_timedwait)中发生进程阻塞,那么进程会被唤醒并且被sem_post增1的信号量计数会再次被sem_wait(或者sem_timedwait)减1.

 

 

当我们想在单个进程中使用POSIX信号量时,使用未命名信号量更容易。这仅仅改变创建和销毁信号量的方式。可以调用sem_init函数来创建一个未命名的信号量。

#include<semaphore.h>

int sem_init( sem_t *sem, int pshared, unsigned int value);  

  返回值:若成功,返回0;若出错,返回-1。

pshared参数表吗是否在多个进程中使用信号量。如果是,将其设置成一个非0值。value参数指定了信号量的初始值。

需要声明一个sem_t类型的变量并把它的地址传递给sem_init来实现初始化,而不是像sem_open函数那样返回一个指向信号量的指针。如果要在两个进程之间使用信号量,需要确保sem参数指向两个进程之间共享的内存范围。

对未命名信号量的使用已经完成时,可以调用sem_destroy函数丢弃它。

#include<semaphore.h>

int sem_destroy( sem_t*sem);

  返回值:若成功,返回0;否则返回-1.

调用sem_destroy后,不能再使用任何带有sem的信号量函数,除非通过调用sem_init重新初始化它

sem_getvalue函数可以用来检索信号量值。

#include<semaphore.h>

int sem_getalue( sem_t *restrict sem, int *restrict valp);

  返回值:若成功,返回0;若出错,返回-1.

成功后,valp指向的整数值将包含信号量值。但是,我们试图要使用我们刚读出来的信号量值得时候,其值可能已经改变了。除非使用额外的同步机制来避免这种竞争,否则sem_getvalue函数只能用来调试。

 

Unix15-进程间通信

原文:https://www.cnblogs.com/cjj-ggboy/p/12397380.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!