unsigned alarm(unsigned sec);
说明:参数sec指定定时的时间间隔,以秒为单位。用户进程可以先通过signal调用指定SIGALRM信号对应的捕获函数,然后调用alarm来设定闹钟,在定时这段时间内做自己的工作。定时时间一到,进程就接收到一个SIGALRM信号,并执行该信号对应的捕获函数。系统调用alarm在多进程编程中非常有用。
7.2 管道通讯
用信号来处理异常事件或错误是非常合适的,但它用来处理进程之间的大量信息传送,就非常不适宜。为此,UNIX又提供了一种称为管道的机构,主要处理进程间的大量信息传送。所谓管道是指进程间连接起来的一条通讯通道。它也UNIX文件概念的一种推广,管道通讯的介质是文件,称为管道文件。用户可以用文件操作的有关系统调用来操作管道文件,从而简化管道应用程序的设计。管道的形象描述如下图:
write 写端 读端 read
管道是UNIX最强大而最有特色的性能之一,特别是在命令行这一级,它允许任意的命令被顺序连接起来。例如:
%who | wc -l
该命令通过管道把命令who的输出送给字计数程序wc,选项-l告诉wc只计算行数。通过wc最终输出的系统已注册的用户个数。
1. 管道程序设计
在程序中可以用系统调用pipe建立一个管道。如果建立成功,就返回两个文件描述符,一个用于写入管道,一个用于从管道中读出。Pipe调用的格式如下:
int filedes[2], retval;
retval = pipe(filedes);
其中,fildes是一个含有两个整数的数组,用来存放标识管道的两个文件描述符。如果调用成功,filedes[0]将被打开用于从管道读,fildes[1]将被打开用于向管道写。
管道一旦建立,就能直接用read和write操作它。当管道与系统调用fork联用时,才能体现出管道的真正价值。这时,可以利用父进程已打开的文件,对于其子进程仍保持打开这一事实。下面的程序先建立一个管道,然后调用fork创建子进程,父进程通过管道向子进程发送信息。
/* pipe.c */
#include
#define MSGSIZE 16
char *msg1 = "hello, world#1";
char *msg2 = "hello, world#2";
char *msg3="hello, world#3";
main(argc,argv)
int argc;
char **argv;
{
char inbuf[MSGSIZE];
int p[2], pid,j;
/* 打开管道 */
if (pipe(p) < 0){
perror("pipe call");
exit(1);
}
if ((pid = fork()) <0 ){
perror("fork call");
exit(2);
}
/* 在父进程中向管道写入 */
if (pid >0 ){
write(p[1], msg1, MSGSIZE);
write(p[1], msg2, MSGSIZE);
write(p[1], msg3, MSGSIZE);
wait((int *)0);
}
/* 在子进程中从管道读入 */
if (pid == 0){
for (j=0; j<3; j++){
read(p[0], inbuf, MSGSIZE);
printf("Child Read:%s\n", inbuf);
}
}
exit(0);
}
程序的输入结果如下:
Child Read:hello, world#1
Child Read:hello, world#2
Child Read:hello, world#3
管道是在先进先出的基础上处理数据的。所以,首先放入管道的数据,在其另一端首先被读出。这个顺序不能被改变,因为系统调用lseek不能用于管道。
2. 命名管道-FIFO
我们已经看到,管道是一种功能很强的进程通讯机构。但是,它也存在一些严重的缺点。
首先,管道只能用于连接具有共同祖先的进程,如父子进程之间的连接。当要开发一个永远保持存在的,提供为全系统范围服务的程序时,这一缺点就更加突出,例如网络控制服务程序和打印机的假脱机程序等。我们要求调用进程应该能够用管道与任何服务进程进行通讯,然后再脱开。遗憾的是,普通管道不能实现上述功能。
其次,管道不能是常设的,在需要时可以建立它们,但是当访问它们的进程终止时,管道也随之被撤销。所以,它们不可能永久存在。
事实上,UNIX系统中的FIFO机制(又称命名管道),弥补了上述管道的不足之处。FIFO与管道一样,也是作为进程之间先进先出的通讯通道,但是FIFO是一种永久性的机构,并且具有一个UNIX文件名。FIFO也具有文件主、长度和访问权限。它能象其他UNIX文件那样被打开、关闭和删除。但在读和写时,其性能与管道相同。
在讨论FIFO程序设计之前,我们先来看一下FIFO在命令级的使用。UNIX命令mknod可以用来创建一个FIFO文件channel:
%/etc/mknod channel p
%ls -l channel
prw-r--r-- 1 yds user 0 2月 17日 14时19分 channel
命令ls的输出结果中的首字母p指出channel是一个FIFO类型的文件。从中我们还可以看到其访问权限为文件主可读写,组内及其他用户只读。其用户主是yds,所属组为user,长度为0,此外还有文件建立的时间。
FIFO程序设计大部分与管道相同,最主要的区别是在建立方面。FIFO是用mknod调用建立的,而不是用pipe建立的。另外,必须把八进制数010000(定义在文件/usr/include/sys/stat.h的常量S_IFIFO中)加入文件模式中,以指明这是一个FIFO。下面是一个建立FIFO的例子:
if (mknod("fifo", 010600,0) < 0)
perror("mknod(fifo) call");
这个例子建立一个名为fifo的FIFO,其权限为0600,所以此FIFO可以被其文件主读写。一旦建立一个FIFO,必须用系统调用open打开它,例如:
#include
.
.
fd = open("fifo", O_WRONLY);
实现打开一个FIFO文件用于写,下面的例子用于以非阻塞方式打开FIFO文件用于读:
if ((fd = open("fifo", O_RDONLY | O_NDELAY)) < 0)
perror("open on file");
下面介绍两个程序,说明FIFO的基本应用。值得注意的是,这两个程序构成了FIFO编程的基本框架,稍微修改即可用于其他场合的FIFO应用。
首先是sendfifo.c的程序清单,用于向FIFO文件写入字串:
/* sendfifo.c */
#include
#include
#include
#define MSGSIZ 63
extern int errno;
main(argc,argv)
int argc;
char **argv;
{
int fd;
char buf[MSGSIZ+1];
int i,nwrite;
if (argc < 2){
fprintf(stderr,"Usage : sendfifo msg ...!\n");
exit(1);
}
if ((fd = open("fifo", O_WRONLY | O_NDELAY)) < 0)
printf("fifo open failed!");
for ( i =1 ; i< argc; i++){
if (strlen(argv[i]) > MSGSIZ) {
fprintf(stderr, " message too long %s!\n", argv[i]);
continue;
}
strcpy(buf, argv[i]);
if ((nwrite = write(fd,buf,MSGSIZ+1)) <= 0){
if (nwrite == 0) /* full FIFO */
errno = EAGAIN;
printf("message write failed!");
}
}
}
下面是recvfifo.c的程序清单,实现从FIFO的读入:
#include
#include
#define MSGSIZ 63
main(argc,argv)
int argc;
char **argv;
{
int fd;
char buf[MSGSIZ+1];
mknod("fifo",010600,0);
if ((fd = open("fifo", O_RDWR)) < 0)
printf("fifo open failed!");
for (;;){
if ( read(fd,buf,MSGSIZ+1) < 0)
printf("message read failed!");
printf("FIFO message received: %s\n" , buf);
}
}
运行结果如下:
%recvfifo &
[1] 1706
%sendfifo hello world
FIFO message received: hello
FIFO message received: world
%
首先,运行recvfifo程序创建FIFO文件"fifo",并打开文件"fifo"用于读;然后,运行sendfifo程序发送字符串"hello world",写入文件"fifo"中。
7.3 IPC通讯机制
1. IPC概述
IPC是UNIX 系统V提供的一套新的进程间通讯进制,它大大增强了进程间的通讯功能。IPC机构包括三种:消息、信号量和共享内存。三种IPC机构的程序设计接口比较相似,这说明它们的内核实现是相似的。IPC最重要的通用特性就是键,键是UNIX系统中标识IPC目标的一个数,其方式类似于一个文件名标识一个文件。也就是说,键可以使多个进程容易共享IPC资源。键所标识的目标可以是一个消息队列、一组信号量或一个共享内存段。键的实际数据类型由实现有关的类型key_t决定,它在头文件/usr/include/sys/types.h中被定义。
当建立一个IPC目标时,系统也建立了一个IPC机构的状态结构,其中包含该目标有关的管理信息。对于消息队列、信号量和共享内存均有一种状态结构类型,每种类型必须含有仅与特定IPC机构有关的信息。但是,这三种状态结构类型都有有关权限结构,这种权限结构的类型用ipc_perm来标识,它包含以下内容:
u_short cuid /* IPC目标创建者的用户ID */
u_short cgid /* 创建者的用户组ID */
u_short uid /* 有效用户ID */
u_short gid /* 有效用户组ID */
u_short umode /*权限许可 */
该结构决定一个用户是否能对IPC目标进行读/写。权限的组成方法与文件的权限完全一样。所以,如果umode之值为0644,则表示属主能读写相应的目标,而其他用户只能读。注意,有效用户标识符和有效组标识符(记录在uid和gid内)与umode一起确定访问的许可性。
最后,IPC的每种形式都提供了各种操作功能,以便IPC进制可被使用。信息队列操作允许消息发送和接收。信号量操作允许信号量增加、减少以及检测到某个值。共享内存操作功能允许进程加上和减去共享内存的部分到它们的地址空间。
2. 消息队列
从本质上看,一个消息是一串字符或字节(不一定以NULL字符结尾)。进程之间通过消息队列传送消息。通过msgget建立或访问消息队列。一个消息队列一旦被建立,只要符合访问权限,进程就可以通过msgsnd把消息放入队列,另一个进程就能用msgrcv读出该信息。
·msgget系统调用
msgget调用格式如下:
#include
#include
#include
int msgget(key_t key, int msgflg);
说明:参数key是标识消息队列的键。如果该调用成功,就建立一个消息队列,或者使一个已经存在的消息队列能够被访问。调用返回一个该消息队列的标识符。参数msgflg确定msgget完成的动作。可以取两个常数:
(1) IPC_CREAT:创建消息队列,且在消息队列已经存在的情况下,不会被重写。如果没有设置该标志,那么当队列已存在时,msgget就返回该消息队列的标识符。
(2) IPC_EXCL:如果该标志与IPC_CREAT都被设置,本次msgget调用则只希望建立一个消息队列。所以,当给出的键值已对应一个存在的消息队列时,调用失败,并返回-1。
建立一个消息队列时,msgflg的低9位用来写出消息队列的权限,这与文件模式一样。如:
msg_id = msgget((key_t)0100, 0644 |IPC_CREAT|IPC_EXCL);
这个调用为键值(key_t)0100建立一个消息队列。如果调用成功,队列的权限为0644,其解释与文件权限一样。
·msgget和msgrcv系统调用
msgsnd和msgrcv调用格式如下:
#include
#include
#include
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
说明:参数msqid指明消息发送或接收的队列,它的值是通过msgget调用得到的。消息的结构类型如下:
struct {
long mtype; /* 消息类型 */
char mtext[]; /* 消息正文 */
}
程序员可以根据这个结构中的mtype域来对消息进行分类。该域的每种可能的值代表一种不同的类别。mtext域用来存放消息正文,正文大小可用用户设定。
系统调用msgsnd的参数msgsz指定发送消息的实际长度,其范围可以从0到系统规定的消息最大长度。系统调用msgrcv中的参数msgsz指定给出了结构内能存放消息的最大长度。如果调用成功,msgrcv返回接收到的消息的实际长度。
两个系统调用中的参数msgflg中有个IPC_NOWAIT。如果没有设定它,那么调用进程就会进入睡眠状态。否则调用就会立即返回。
·msgctl系统调用
msgctl调用格式如下:
#include
#include
#include
int msgctl(int msqid, int cmd, .../* struct msqid_ds *buf */);
说明:msgctl用来获取和修改一个已经存在的消息队列的属性。参数msgqid是消息队列的ID,命令常量cmd的取值有三种:
(1) IPC_STAT:放置关于结构中消息队列当前消息的一个备份。
(2) IPC_SET:为消息队列设置控制变量值。
(3) IPC_RMID:从系统中删除消息队列,但是只有超级用户或队列属主才能实现。
3. 信号量(略)
4. 共享内存
共享内存操作允许两个或两个以上进程共享一个物理存贮器段,它是所有IPC中效力最高的一种。一个共享内存段被唯一的标识符所描述。
·shmget系统调用
shmget调用格式如下:
#include
#include
#include
int shmget(key_t key, size_t size, int shmflg);
说明:参数key是标识共享内存的键。参数size是创建或访问共享内存的大小。如果调用成功,就创建一块共享内存,或者使一块已经存在的共享内存能够被访问。调用返回一个该共享内存的标识符。参数shmflg同调用msgget,semget中的参数msgflg, semflg一样。
·shmat和shmdt系统调用
shmat和shmdt调用格式如下:
#include
#include
#include
void *shmat(int shmid, void *shmaddr, int shmflg);
int shmdt (void *shmaddr);
说明:shmat调用把参数shmid标识的内存段连到调用进程的一个有效地址上。调用成功,shmat返回该地址memptr。参数shmaddr给出程序员在调用所选地址的控制。参数shmflg由标志SHM_RDONLY和SHM_RND构成。前者请求被连之段为只读,后者用于shmat处理shmaddr非0的情况。
Shmdt的功能与shmat刚好相反,它实现把一个共享内存段从进程的逻辑地址空间中分离出来。这意味着进程将不再使用它。
·shmctl系统调用
shmclt调用格式如下:
#include
#include
#include
int shmctl (int shmid, int cmd, .../* struct shmid_ds *buf */);
说明:这个调用实现对共享内存的操作控制,其使用与msgctl完全一样,其参数cmd可以取IPC_STAT、IPC_SET和IPC_RMID。