第44 章 管道和FIFO

        本章介绍管道和FIFO。管道是UNIX系统上最古老的IPC方法,他在20世纪70年代早期UNIX的第三个版本上就出现了。管道为一个常见需求提供了一个优雅的解决方案:给定两个运行不同的程序(命令)的进程,在shell中如何让一个进程的输出作为另一个进程的输入呢?管道可以用来在相关两个进程之间传递数据。FIFO是管道概念的一个变体,他们之间的一个重要区别在于FIFO可以用于任意进程间的通信。

 44.1 概述

        每个shell用户都对在命令中使用管道比较熟悉,如下面这个统计一个目录中文件数目的命令所示。

$ ls | wc -l

        为执行上面的命令,shell创建了两个进程分别执行ls和wc.(这时通过使用fork()和exec()来完成的,第24章和27章分别对这两个函数进行了介绍。)图44-1展示了这两个进程是如何使用管道的。

         除了说明管道的用法之外,图44-1的另外一个目的是阐明管道这个名称的由来。可以将管道卡成是一组铅管,它允许数据从一个进程流向另一个进程。

        在图44-1中有一点值得注意的是两个进程都连接到了管道上,这样写入进程(ls)就将其标准输出(文件描述符为 1)连接到了管道的写入段,读取进程(wc)就将其标准输入(文件描述符为0)连接到管道的读取段。实际上,这两个进程并不知道管道的存在,他们只是从标准文件描述符中读取和写入数据。shell必须要完成相关的工作,在44.4节会介绍。

        下面介绍管道的几个重要特征。

一个管道一个字节流

        当讲到管道是一个字节流时意味着在使用管道时是不存在消息或消息边界的概念的。从管道中读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是什么。此外,通过管道传递的数据是顺序的--从管道中读取出来的字节的顺序与他们被写入管道的顺序是完全一样的。在观众中无法使用lseek()来随即地访问数据。

        如果需要在管道中实现离散消息地概念,那么就必须要在引用程序中完成在这些工作。虽然这是可行地(参见44.8节),但如果碰到这种需求地话最好使用其他IPC机制,如消息队列和数据包socket,

从管道中读取数据

        试图从一个当前为空地管道中读取数据将会被阻塞直到至少有一个字节被写入到管道中为止。如果管道地写入段被关闭了,那么从管道中读取数据地进程在读完管道中剩余地所有数据之后将会看到文件结束(即read() 返回0).

管道是单向的

        在管道中数据地传递方向是单向地。管道地一端用于写入,另一端用于读取。

        在其他一些UNIX 实现上----特别是那些从System V Release 4烟花而来地系统---管道是双向地(所谓地流管道)。双向管道并没有在任何UNIX标准中进行规定,i因此即使在提供了双向管道地是向上也避免依赖这种语义。作为替代方案,可以使用UNIX domain流socket对(57.5 节介绍地socketpair()系统调用来创建),它提供了一种标准地双向通信机制,并且其语义与流管道是等价地。

可以确保写入不超过PIPE_BUF字节的操作是原子的

        如果多个继承写入同一管道,那么如果他们在同一时刻写入地数量不超过PIPE_BUF字节,那么就可以确保写入地数据不会发生相互混合地情况。

        SUSv3要求PIPE_BUF至少为POSIX_PIPE_BUF(512).一个实现应该定义PIPE_BUF(在<liit.h>中)并/或允许调用fpathconf(fd,_PC_PIPE_BUF)来返回原子写入操作地实际上限。不同UNIX 实现上地PIPE_BUF不同,如在FreeBSD6.0其值为512字节,在Tru64 5.1上其值为4096字节,在Solaris 8上其值为5120字节,在Linux上 PIPE_BUF为4096.

        当写入管道地数据块地大小超过了PIPE_BUF字节,那么内核可能会将数据分割成几个较小地片段来传输,在读者从管道中消耗数据时再附加上后续地数据。(write()调用会阻塞直到所有数据被写入管道为止。)但之后一个进程向管道写入数据时(通常地情况),PIPE_BUF地取值就没有关系地。但如果有多个写入进程,那么大数据块地写入可能会被分解成任意大小地段(可能小于PIPE_BUF字节),并且可能会出现与其他进程写入地数据交叉地现象。

        只有在数据被传输到管道地时候PIPE_BUF限制才会起作用。当写入地数据达到PIPE_BUF字节时,write()会在必消地时候阻塞直到管道中地可用空间足以原子地完成操作。如果写入地数据大于PIPE_BUF字节,那么write()会尽可能地多传输数据一充满整个管道,然后阻塞直到一些读取进程从管道中移除了数据,如果系类阻塞地write(0被一个信号处理器中断了,那么这个调用会被解除阻塞并返回成功传输到管道中地字节数,这个字节数会少于请求写入地字节数(所谓地部分写入)。

管道地容量是有限的

        管道起始是一个在内核中维护的缓冲器,这个缓冲器的存储能力是有限的。一旦管道被填满之后,后续向该管道的写入操作就会被阻塞直到读者从管道中一除了一些数据为止。

        一般来讲,一个应用程序无需直到管道的实际存储能力。如果需要放置写者进程阻塞,那么从管道中读取数据的进程应该被设计成以尽可能块的速度从管道中读取数据。

44.2 创建和使用管道

        pipe()系统调用创建一个新管道。

#include <unistd.h>
int pipe(int filedes[2]);
                        Returns on success, or -1 on error

        成功的pipe()调用会在数组filded中返回两个打开的文件描述符:一个表示管道的读取端(filedes[0]),另一个表示管道的写入端(filedes[1]).

        与所有文件描述符一样,可以使用read()和write()系统调用来在管道上执行I/O。一旦相关到的写入端写入数据之后立即就能从管道的读取段读取数据。管道上的read()调用会读取的数据量为所请求的字节数与管道中当前存在的字节数两者之间较小的那个(但当管道为空时阻塞)。

        也可以在管道上使用stdio函数(printf()、scanf()等),只需要首先使用fdopen()获取一个与filedes中某个描述符对应的文件流即可(参见13.7节)。但在这样做的时候需要清楚在44.6节中介绍的stdio缓冲问题。

        ioctl(fd,FIONREAD,&cnt)调用返回文件描述符fd所引用的管道或FIFO中未读取的字节数。

        图44-2给出了使用pipe()创建完管道之后的情况,其中调用进程通过文件描述符引用了管道的两端。

        在单个进程中管道的用途不多(在63.5.2节中将会介绍一种用途)。一般来讲都是使用管道让两个进程进行通信。为了让两个进程通过管道进行连接,在调用玩pipe()之后可以调用fork()。在fork()期间,子进程汇集成父进程的文件描述符的副本(参见24.2.1节),这样就会出现图44-3中左边那样的情形。

         虽然父进程和子进程都可以从管道中读取和写入数据,但这种做法并不常见。因此在调用fork()之后,其中一个进程应该立即关闭管道的写入端的描述符,另一个则关闭读取端的描述符。如,如果父进程需要向子进程传输数据,那么他就会关闭管道的读取段的描述符filedes[0],而子进程就会关闭管道的写入端的描述符filedes[1],这样就出现了图44-3中右边那样的情形。程序清单44-1给出了创建这个管道的代码。

程序清单44-1:使用管道将数据从父进程传输到子进程所需的步骤

int filedes[2];

if(pipe(filedes) == -1)  //create the pipe
{
    perror("pipe");
    return -1;
}
switch(fork()){   /*Create a child process*/
case -1:
    perror("fork");
    return-1;
case 0: /*Child*/
    if(close(filedes[1]) == -1)  /*Close unused write end*/
    {
        perror("close:");
        return -1;
    }
    /*Child now reads from pipe*/
    break;
default: /*Parent*/
    if(close(filedes[0]) == -1)  /*Close unused write end*/
    {
        perror("close:");
        return -1;
    }
    /*Parent now reads from pipe*/
    break;
}

         让父进程和子进程都能够从同一个管道中读取和写入数据这种做法并不常见的一个原因是如果两个几次呢很难过同时试图从管道中读取数据,那么就无法确定哪个进程会首先读取成功--两个进程竞争数据了。要防止这种竞争情况的出现就需要使用某种同步机制。但吐过需要双向通信则可以使用一个更加简单的办法:创建两个管道,在两个进程之间发送数据的两个方向上各使用一个。(如果使用这种计数,那么就需要考虑死锁的问题了,因为如果两个进程都试图从空管道种读取数据或尝试向已满的管道种写入数据就会发生死锁。)

        虽然可以有多个进程向单个管道写入数据,但通常只存在一个写者。(44.3节种会给出一个使用多个写者向一个管道写入数据的例子。)相反,在有些情况下让FIFO拥有多个写者是比较有用的,44.8节会给出这样一个例子。

管道允许相关进程间的通信

        目前为止本章已经介绍了如何使用管道来让父进程和子进程之间进行通信,其实管道可以用于任意两个(或更多)相关进程之间的通信,只要在创建子进程的系列fork()调用之前通过一个共同的祖先进程创建管道即可。(这就是本章开头所讲的“相关进程”的含义。)如管道可用于一个进程和其孙子进程之间的通信。第一个进程创建管道,然后创建子进程,接着子进程在创建第一个进程的孙子进程。管道通常用于两个兄弟进程之间的通信--他们的父进程创建可管道,然后创建两个子进程。这就是在构建管道线时shell所作的工作。

管道只能用于相关进程之间的通信这个说法存在一种例外情况。通过UNIX domain socket(61.13.3节种将会简要介绍)传递一个文件描述符使得将管道的文件描述符传递给一个非相关进程称为可能。

关闭未使用管道文件描述符 

        关闭未使用管道文件描述符不仅仅是为了确保进程不会耗尽其文件描述符的限制---这对于正确使用管道是非常重要的。

        从管道中读取数据的进程会关闭其持有的管道的写入描述符,这样当其他进程完成输出并关闭其写入描述符之后,读者就能够看到文件结束(在读完管道种的数据之后)。

        如果读取进程没有关闭管道的写入端,那么在其他进程关闭了写入庙输入之后,读者也不会看到文件结束,即使他读完了管道中的所有数据。相反,read()将会阻塞以等待数据,这时因为内核直到至少还存在一个管道的写入描述符打开着,即读取进程自己打开了这个描述符。从理论上来讲,这个进程仍然可以相关到写入数据,即使她已经被读取操作阻塞了。如read()可能会被一个向管道写入数据的信号处理器终端。(这是现实世界中的一种场景。)

        写入进程关闭其持有的读取描述符是出于不同的原因。当一个进程试图向一个管道中写入数据但没有任何进程拥有该管道的打开着的读取描述符时,内核会向写入进程发送一个SIGPIPE信号。在默认情况下,这个信号会杀死一个进程。但进程可以捕获或忽略该信号,这样就会导致管道上的write()操作因EPIPE错误(已损坏的管道)而失败。收到SIGPIPE信号或得到EPIPE错误对于标示出管道的状态是有用的,这就是为何需要关闭管道的未使用读取描述符的原因。

注意:对被SIGPIPE处理器中断的write()的处理是特殊的。通常,当write()(或其他“慢”系统调用)被一个信号处理器中断时,这个调用会根据是否使用sigaction()SA_RESTART标记安装了处理器而自动重启或因EINTR错误而失败(见21.5)。对SIGPIPE的处理不同是因为自动重启write()或简单标示出write()被一个处理器中断了是毫无意义的(意味着需要手动重启write())。不管何种处理方式,后续的write()都不会成功,因为管道仍然处于被损坏的状态。

        如果写入进程没有关闭管道的读取段,那么即使在其他进程已关闭了管道的读取段之后写入进程任然能够向管道写入数据,最后写入进程会将数据充满整个管道,后续的写入请求将会被永远阻塞。

        关闭未使用文件描述符的最后一个原因是只有当所有进程中所有引用一个管道的文件描述符被关闭之后才会销毁该管道以及释放该管道占用的资源以供其他进程复用,此时,管道中所有维度取的数据都会丢失。

示例程序

        程序清单44-2中的程序演示了如何将挂到用于父进程和子进程之间的通信。这个例子演示了前面提及的管道的字节流特性---父进程在一个操作中写入数据,子进程一小块一小块 的从管道中读取数据。

        主程序调用pipe()创建管道,然后调用fork()创建一个子进程。在fork()调用之后,父进程关闭了其持有的管道的读取端的文件描述符并将通过程序的命令行参数传递来的字符串写到管道的写入端。父进程接着关闭管道的读取端并调用wait()等待子进程终止。再关闭了所持有的管道的写入端的文件描述符之后,子进程进入了一个循环,在这个循环中从管道中读取数据块并将他们写入到标准输出中。当紫禁城碰到管道的文件结束时就退出循环,并写u人一个结尾换行字符以及关闭所持有的管道的读取端的描述符,最后终止。

下面时运行程序清单44-2中的程序时可能看到的输出。

程序清单44-2:在父进程和子进程之间使用管道通信--simple_pipe.c

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>


#define BUF_SIZE 10
int main(int argc, char *argv[])
{
    int pfd[2];  /*Pipe file descriptors*/
    char buf[BUF_SIZE];
    ssize_t numRead;

    if(argc !=2 || strcmp(argv[1],"--help") == 0)
    {
        printf("%s string \n",argv[0]);
        return -1;
    }

    if(pipe(pfd) == -1) /*Create pipe*/
    {
        perror("pipe():");
        return -1;
    }

    switch(fork()){
    case -1:
        perror("fork:");
        return -1;
    case 0:                          /*Child - reads from pipe*/
        if (close(pfd[1]) == -1)    /*Write end is unused*/
        {
            perror("close--child");
            return -1;
        }
        for(;;){  //Read data from pipe,echo on stdout
            numRead = read(pfd[0],buf,BUF_SIZE);
            if(numRead == -1)
            {
                perror("close--child");
                return -1;
            }
            if(numRead == 0)
               break;           /*End of file*/
            if(write(STDOUT_FILENO,buf,numRead)!=numRead)
            {
                printf("child-partial/failed write\n");
                return -1;
            }
        }
        write(STDOUT_FILENO,"\n",1);
        if(close(pfd[0]) == -1)
        {
            perror("close:");
            return -1;
        }
        _exit(EXIT_SUCCESS);

    default:   /*Parent -write to pipe*/
        if(close(pfd[0]) == -1)   /*Read end is unused*/
        {
            perror("close-parent");
            return -1;
        }
        if(write(pfd[1],argv[1],strlen(argv[1]))!=strlen(argv[1]))
        {
            printf("parent-partial/failed write\n");
            return -1;
        }
       if(close(pfd[1]) == -1)   /*Child will see EOF*/
       {
           perror("close:");
           return -1;  
       }
       wait(NULL);   //WAIT FOE CHILD TO FINISH
       exit(EXIT_SUCCESS);
    }
}

44.3 将管道作为一种进程同步的方法

        在24.5节中介绍了如何使用信号来同步父进程和子进程的动作以防止出现竞争条件。也可以使用管道来取得类似的结果,如程序清单44-3中各处的骨架程序所示,这个程序创建了多个子进程(每一个命令行参数对应一个子进程),每个子进程都完成某个动作,在本例中则是睡眠一段时间。父进程等待直到所有子进程完成了自己的动作为止。

        为了执行同步,父进程在创建子进程之前构建了一个管道,每个子进程汇集成管道的写入端的文件描述符并在完成动作之后关闭这些描述符。当所有进程都关闭了管道的写入端的文件描述符之后,父进程在管道上的read()就会结束并返回文件结束(0)。这时父进程就能够做其他工作了。(注意在父进程中关闭管道的未使用写入端对于这项技术的正常运转是至关重要的,否则父进程在试图从管道中读取数据时会被永远阻塞。)

        下面是使用程序清单创建三个分别睡眠 4 、2和 6秒的子进程时所看到的输出。

程序清单44-3:使用管道同步多个进程--pipe_sync.c

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>

#define BUF_SIZE 1000


char *
currTime(const char *format)
{
    static char buf[BUF_SIZE];  /* Nonreentrant */
    time_t t;
    size_t s;
    struct tm *tm;

    t = time(NULL);
    tm = localtime(&t);
    if (tm == NULL)
        return NULL;

    s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);

    return (s == 0) ? NULL : buf;
}


int main(int argc,char *argv[])
{
    int pfd[2];   /*Process synchronization pipe*/
    int j,dummy;

    if(argc <2 || strcmp(argv[1],"--help") == 0)
    {
        printf("%s sleep-time.... \n",argv[0]);
        return -1;
    }

    setbuf(stdout,NULL);   /*Make stdout unbuffered,since we terminate child with _exit()*/
    printf("%s Parent started\n",currTime("T"));

    if(pipe(pfd) == -1) /*Create pipe*/
    {
        perror("pipe():");
        return -1;
    }

    for(j=1;j<argc;j++){
        switch(fork()){
        case -1:
            perror("fork:");
            return -1;
        case 0:               /*Child */
            if (close(pfd[0]) == -1)    /*Read end is unused*/
            {
                perror("close--child");
                return -1;
            }
            /*Child does some work,and lets parent know it's done*/
            sleep(atoi(argv[j]));
            printf("%s Child %d (PID=%ld) closing pipe\n",
                currTime("%T"),j,(long)getpid());
            if(close(pfd[1]) == -1)
            {
                perror("close");
                return -1;
            }
            /*Child now carries on to do other things...*/
            _exit(EXIT_SUCCESS);
            default:  /*Parent loops to create next child*/
                break;
        }
    }
    /*Parent comes here; close write end of pipe so we can see EOF*/
    if(close(pfd[1]) == -1)
    {
        perror("close");
        return -1;
    }
    /*Parent may do other work,then synchronizes with children*/

    if(read(pfd[0],&dummy,1) !=0)
    {
        printf("arent didn't get EOF\n");
        return -1;
    }
    printf(" %s Parent ready to go \n",currTime("%T"));

    /*Parent can now carry on to do other things...*/
    exit(EXIT_SUCCESS);
}

        与前面使用信号来同步相比,使用管道同步具备一个优势:它可以同来协调一个进程的动作使之与多个其他(相关)进程匹配。而多个(标准)信号无法排队的事实使得信号不适用于这种情形。(相反,信号的优势是它可以被一个进程广播到进程组中的所有成员处。)

        其他同步结构也是可行的(如使用多个管道)。此外,还可以对这项计数进行扩展,即不关闭管道,每个子进程向管道写入一条包含齐金城ID和一些状态信息的消息。或者每个子进程可以向管道写入一个字节。父进程可以计数和分析这些消息。这种方法考虑到了子进程意外终止而不是显式地关闭管道地情形。

44.4 使用管道连接过滤器

        当管道被创建之后,为管道地两端分配地文件描述符是可用描述符中数值最小地两个。由于在通常情况下,进程已经使用了描述符0、1和2,因此会为管道分配一些数值更大地描述符。那么如何形成图44-1中给出地情形呢,使用管道连接两个过滤器(即从stdin读取和写入到stdout地程序)使得一个程序地标准输出被定向到管道中,而另一个程序地标准输入则从管道中读取?特别是在不修改过滤器本身地情况下完成这项工作呢?

        这个问题地答案是使用5.5节中介绍地技术,即复制文件描述符。一般来讲会使用下面的系统调用来获取与其的结果

int pfd[2];
pipe(pfd); // allocate (say) file descriptor 3 and 4 for pipe

/*other steps her e.g fork()*/

close(STDOUT_FILENO);   /*Free file descriptor 1*/
dup(pfd[1]);         /*Duplication uses lowest free file
                        descriptor i.e,fd 1*/

        上面这些调用的最终结果是进程的标准输出背板顶到了管道的写入端。而对应的一组调用可以用来将进程的标准输入绑定到管道的读取端上。

        注意,上面这些调用假设已经为进程打开了文件描述符0、1和2。(shell 通常能够确保为它执行的每个程序都打开了这三个描述符。)如果在执行上面的调用之前0描述符已经被关闭了,那么就会被错误地将进程地标准输入绑定到管道地写入端上。为避免这种情况地发生,可以使用dup2()调用来取代对close()和dup()地调用,因为通过这个函数可以显式地指定被绑定到管道一端地描述符。

 dup2(pfd[1],STDOUT_FILENO);  /*Close descriptor 1,and reopen bound to write end of pipe*/

       在复制完pfd[1]之后就拥有两个引用管道地写入端地文件描述符了:描述符1和pfd[1]。由于未使用地管道文件描述符应该被关闭,因此在dup2()调用之后需要关闭多余地描述符。

close(pfd[1]);

        前面给出地代码依赖于标准输出在之前已经被打开这个事实。假设在pipe()调用之前,标准输入和标准输出都被关闭了。那么在这种情况下,pipe()就会给管道分配两个描述符,即pfd[0]的值可能为0,pfd[1]的值可能为1.其结果是前面的dup2()和close()调用将下面的代码等价。

dup2(1,1);  /*Does nothing*/
close(1);  /*Closes sole descriptor for write end of pipe*/

        因此按照防御性编程实践的要求最好将这些调用放在一个if语句中,如下所示。

if(pfd[1] != STDOUT_FILENO){
    dup2(pfd[1],STDOUT_FILENO);
    close(pfd[1]);
}

程序清单44-4给出了使用本节介绍的技术实现了图44-1中给出的结构。在构建完一个轨道之后,这个程序创建了两个子进程。第一个子进程将其标准输出绑定到管道的写入端,然后执行ls.第二个子进程将其标准输入绑定到管道的写入端,然后执行wc.

程序清单44-4:使用管道连接ls 和wc----------pipe_ls_wc.c



#include <sys/wait.h>
#include <sys/types.h>  /* Type definitions used by many programs */
#include <stdio.h>      /* Standard I/O functions */
#include <stdlib.h>     /* Prototypes of commonly used library functions,
                           plus EXIT_SUCCESS and EXIT_FAILURE constants */
#include <unistd.h>     /* Prototypes for many system calls */
#include <errno.h>      /* Declares errno and defines error constants */
#include <string.h>  

int
main(int argc, char *argv[])
{
    int pfd[2];                                     /* Pipe file descriptors */

    if (pipe(pfd) == -1)                            /* Create pipe */
    {
        perror("pipe():");
        return -1;
    }

    switch (fork()) {
    case -1:
    {
        perror("fork():");
        return -1;
    }

    case 0:             /* First child: exec 'ls' to write to pipe */
        if (close(pfd[0]) == -1)                    /* Read end is unused */
        {
            perror(" close()--1:");
            return -1;
    	}

        /* Duplicate stdout on write end of pipe; close duplicated descriptor */

        if (pfd[1] != STDOUT_FILENO) {              /* Defensive check */
            if (dup2(pfd[1], STDOUT_FILENO) == -1)
            {
            	perror(" dup2()--1:");
            	return -1;
    	    }
            if (close(pfd[1]) == -1)
            {
                perror(" close()--2:");
                return -1;
    	    }
        }

        execlp("ls", "ls", (char *) NULL);          /* Writes to pipe */
        perror(" close()--2:");
        return -1;

    default:            /* Parent falls through to create next child */
        break;
    }

    switch (fork()) {
    case -1:
        {
            perror(" fork()--2:");
            return -1;
    	}

    case 0:             /* Second child: exec 'wc' to read from pipe */
        if (close(pfd[1]) == -1)                    /* Write end is unused */
        {
            perror(" close()--3:");
            return -1;
    	}

        /* Duplicate stdin on read end of pipe; close duplicated descriptor */

        if (pfd[0] != STDIN_FILENO) {               /* Defensive check */
            if (dup2(pfd[0], STDIN_FILENO) == -1){
                perror(" dup2():");
                return -1;
            }
                 
            if (close(pfd[0]) == -1)
            {
                perror(" close()--4:");
                return -1;
    	    }   
        }

        execlp("wc", "wc", "-l", (char *) NULL);
       {
            perror(" execlp:");
            return -1;
    	}   

    default: /* Parent falls through */
        break;
    }

    /* Parent closes unused file descriptors for pipe, and waits for children */

    if (close(pfd[0]) == -1)
     {
        perror(" close()--5:");
        return -1;
    }   

    if (close(pfd[1]) == -1)
     {
        perror(" close()--6:");
        return -1;
    }   

    if (wait(NULL) == -1)
     {
        perror(" wait()--1:");
        return -1;
    }   
    if (wait(NULL) == -1)
     {
        perror(" wait()--2:");
        return -1;
     }   

    exit(EXIT_SUCCESS);
}

当执行程序清单44-4中的程序时会看到下面的输出

44.5 通过管道与shell命令进行通信:popen()

        管道的一个常见用途是执行shell命令并读取其输出或向其发送一些输入。popen()和pclose()函数简化了这个任务。

#include <stdio.h>
 FILE *popen(const char *command,const char *mode);
                    Returns file stream,or NULL on error
 int pclose(FILE *stream);
                    Returns termination status of child prrocess,or -1 on error

        popen()函数创建了一个管道,然后创建了一个子进程来执行shell,而shell又创建了一个子进程来执行command字符串。mode参数是一个字符串,他确定调用进程是从管道中读取数据(mode是r)还是将数据写入管道中(mode是w).(由于管道是单向的,因此无法在执行的command中进行双向通信。)mode的取值确定了所执行的命令的标准输出是连接到管道的写入端还是将其标准输入连接到管道的读取端,如图44-4所示。

         popen()在成功时会返回可提供stdio库函数使用的文件流指针。当发生错误时(如mode不为r或w,创建管道失败,或通过fork()创建子进程失败),popen()会返回NULL并设置errno以标示出发生错误的原因。

        在popen()调用之后,调用进程使用管道来读取command的输出或使用管道向其发送输入。与使用pipe()创建的管道一样,当从管道中读取数据时,调用进程在command关闭管道的写入端之后会看到文件结束;当向管道写入数据时,如果command已经关闭了管道的读取端,那么调用进程会受到SIGPIPE信号并得到EPIPE错误。

        一旦I/O结束之后可以使用pclose()函数关闭管道并等待子进程的shell终止。(不应该使用fclose()函数,因为他不会等待子进程。)pclose()在成功时会返回子进程中shell的终止状态(26.1.3节)(即shell所执行的最后一条命令的终止状态,除非shell是被信号杀死的)。与system()(27.6节)一样,如果无法执行shell,那么pclose()会返回一个值就像是子进程中的shell通过调用_exit(127)来终止一样。如果发生了其他错误,那么pclose返回-1.其中可能发生的一个错误是无法取得终止状态。

        当执行等待以获取子进程中shell的状态时,SUSv3要求pclose()与system()一样,即在内部的waitpid()调用被一个信号处理器中断之后重启该调用。

        一般来讲,在27.6节中描述的有关system()的规范同样适用于popen()。使用popen()更加方便一些,他会构建管道、执行描述符赋值、关闭未使用的描述符并帮助开发人员处理fork()和exec的所有细节。此外shell处理针对的是命令。这种便捷性所牺牲的是效率,因为至少需要创建两个额外的进程:一种用于shell,一个或多个用于shell执行的命令。与system()一样在特权进程中永远都不应该使用popen()。

        虽然system()和popen()以及pclose()之间存在很多相似之处,但也存在显著的差异。这些差异原于一个事实,即使用system()时shell命令的执行是被封装在单个函数调用中的,而使用popen()时,调用进程是与shell命令并行运行的,然后会调用pclose()。具体的差异包括以下两个方面。

  • 由于调用进程与被执行的命令是并行运行的饿,因此SUSv3要求popen()不忽略SIGINT和SIGUIT信号。如果这些信号是从键盘产生的,那么它们会被发送到调用进程和被执行的命令中。之所以这样是因为两个进程位于同一个进程组中,而由中断产生的信号是会向34.5节中描述的那样被发送到(前台)进程组中的所有成员的。
  • 由于调用进程在执行popen()和执行pclose()之间可能会创建其他子进程,因此SUSv3要求popen()不能阻塞SIGCHILD信号。这意味着如果调用进程在pclose()调用之前执行了一的等待操作,那么他就能够取得popen()创建的子进程的状态。这样当后面调用popen()时,他就会返回-1,同时将errno设置为ECHILD,表示pclose()无法取得子进程的状态。

示例程序

        程序清单44-5演示了popen()和pclose()的用法。这个程序重复读取一个文件名通配符模式,然后使用popen()获取将这个模式传入ls命令之后的结果。(在较早的UNIX实现上会使用类似的技术执行文件名生成任务,这种技术被称为globbing,他在引入glob()库函数之前就已经存在了。)

程序清单44-5:使用popen()统配文件名模式------popen_glob.c


#include <ctype.h>
#include <limits.h>
#include <stdio.h>      /* Standard I/O functions */
#include <stdlib.h>     /* Prototypes of commonly used library functions,
                           plus EXIT_SUCCESS and EXIT_FAILURE constants */
#include <unistd.h>     /* Prototypes for many system calls */
#include <errno.h>      /* Declares errno and defines error constants */
#include <string.h> 

#define POPEN_FMT "/bin/ls -d %s 2> /dev/null"
#define PAT_SIZE 50
#define PCMD_BUF_SIZE (sizeof(POPEN_FMT) + PAT_SIZE)
typedef enum { FALSE, TRUE } Boolean;

void                    /* Examine a wait() status using the W* macros */
printWaitStatus(const char *msg, int status)
{
    if (msg != NULL)
        printf("%s", msg);

    if (WIFEXITED(status)) {
        printf("child exited, status=%d\n", WEXITSTATUS(status));

    } else if (WIFSIGNALED(status)) {
        printf("child killed by signal %d (%s)",
                WTERMSIG(status), strsignal(WTERMSIG(status)));
#ifdef WCOREDUMP        /* Not in SUSv3, may be absent on some systems */
        if (WCOREDUMP(status))
            printf(" (core dumped)");
#endif
        printf("\n");

    } else if (WIFSTOPPED(status)) {
        printf("child stopped by signal %d (%s)\n",
                WSTOPSIG(status), strsignal(WSTOPSIG(status)));

#ifdef WIFCONTINUED     /* SUSv3 has this, but older Linux versions and
                           some other UNIX implementations don't */
    } else if (WIFCONTINUED(status)) {
        printf("child continued\n");
#endif

    } else {            /* Should never happen */
        printf("what happened to this child? (status=%x)\n",
                (unsigned int) status);
    }
}



int
main(int argc, char *argv[])
{
    char pat[PAT_SIZE];                 /* Pattern for globbing */
    char popenCmd[PCMD_BUF_SIZE];
    FILE *fp;                           /* File stream returned by popen() */
    Boolean badPattern;                 /* Invalid characters in 'pat'? */
    int len, status, fileCnt, j;
    char pathname[PATH_MAX];

    for (;;) {                  /* Read pattern, display results of globbing */
        printf("pattern: ");
        fflush(stdout);
        if (fgets(pat, PAT_SIZE, stdin) == NULL)
            break;                      /* EOF */
        len = strlen(pat);
        if (len <= 1)                   /* Empty line */
            continue;

        if (pat[len - 1] == '\n')       /* Strip trailing newline */
            pat[len - 1] = '\0';

        /* Ensure that the pattern contains only valid characters,
           i.e., letters, digits, underscore, dot, and the shell
           globbing characters. (Our definition of valid is more
           restrictive than the shell, which permits other characters
           to be included in a filename if they are quoted.) */

        for (j = 0, badPattern = FALSE; j < len && !badPattern; j++)
            if (!isalnum((unsigned char) pat[j]) &&
                    strchr("_*?[^-].", pat[j]) == NULL)
                badPattern = TRUE;

        if (badPattern) {
            printf("Bad pattern character: %c\n", pat[j - 1]);
            continue;
        }

        /* Build and execute command to glob 'pat' */

        snprintf(popenCmd, PCMD_BUF_SIZE, POPEN_FMT, pat);

        fp = popen(popenCmd, "r");
        if (fp == NULL) {
            printf("popen() failed\n");
            continue;
        }

        /* Read resulting list of pathnames until EOF */

        fileCnt = 0;
        while (fgets(pathname, PATH_MAX, fp) != NULL) {
            printf("%s", pathname);
            fileCnt++;
        }

        /* Close pipe, fetch and display termination status */

        status = pclose(fp);
        printf("    %d matching file%s\n", fileCnt, (fileCnt != 1) ? "s" : "");
        printf("    pclose() status = %#x\n", (unsigned int) status);
        if (status != -1)
            printWaitStatus("\t", status);
    }

    exit(EXIT_SUCCESS);
}

下面的shell的shell会话演示了程序清单44-5中给出的程序的用法。在本例中首先提供了一个匹配的两个文件名的模式,然后有给出了一个与任何文件名都不匹配的模式。

        这里需要对程序清单44-5中通配命令的构建解释一下。真正执行匹配模式的是shell.ls命令仅仅用来列出匹配的文件名,每一行列出一个。读者可以尝试使用echo命令,但当模式与所有文件名都不匹配时这种做法会出现非预期的结果,然后shell就会爆出模式不变,而echo会简单地打印处模式。相反如果传递给ls地文件名不存在,那么他就会在stderr(通过将stderr重定向到/dev/null来丢弃写入这恶鬼描述符中地数据)上打印出一条错误消息,而不会在stdout上打出任何消息,并且最后退出的状态为1 。

还需要注意程序清单44-5中程序所做地输入检测,之所以这样做是为了放置非法输入引起地popen()执行一个预期之外地shell命令。假设忽略了这些检测,并且用户输入了下面地shur-。

$ pattern:;rm *

程序会将下面地命令传递给popen(),其结果是损失惨重。

/bin/ls -d; rm * 2 >/dev/null

        在使用popen()(或system())执行根据用户输入构建地shell命令地程序中永远都需要做输入检测。(应用程序可以选在另一张方法,即将那些无需检测地字符放在引号中,这样shell就不会对那些字符进行特殊处理了。)

44.6 管道和stdio缓冲

        由于open()调用返回地文件流指针没有引用一个终端,因此stdio库会对这种文件流应用快缓冲(13.2节)。这意味着当将mode的值设置为w来调用popen()时,在默认情况下只有当stdio缓冲器被充满或使用pclose()关闭了之后输出再回被发送到管道另一端的子进程。在很多情况下,这种处理方式是不存在问题的。但如果需要确保子进程能够立即从管道中接收数据,那么就需要定期调用fflush()或使用setbuf(fp,NULL)调用来禁用stdio缓冲。当使用pipe()系统调用创建管道,然后使用fdopen()获取一个与管道的写入端对应的stdio流时也可以使用这项技术。

        如果调用popen()的进程正在从管道中读取数据(即mode是r),那么事情就不是那么简单了。在这样情况下,如果子进程正使用stdio库,那么---除非它显式地调用了fflush()或setbuf---其输出只有在子进程填满staio缓冲器或调用了fclose()之后才会对调用进程可用。(如果正在使用pipe()创建地管道中读取数据并且向另一端写入数据地进程正在使用stdio库,那么同样地规则也是适用地。)如果这是一个问题,那么能采取地措施就比较有限地,除非能够修改在子进程中运行地程序地源代码使之包含对setbuf()或fflush()调用。

        如果无法修改源代码,那么可以使用伪终端来替换管道。一个伪终端是一个IPC通道,对进程来将他就像是一个终端。其结果是stdio库会逐行输出缓冲器地数据。

44.7 FIFO

        从语义上来讲,FIFO与管道类似,他们之间最大地差别在于FIFO在文件系统中拥有一个名称,并且其打开方式与打开一个普通文件是一样地。这样就能够将FIFO用于非相关进程之间地通信。

        一旦打开了FIFO,就能在它上面使用与操作管道和其他文件地系统调用一样地I/O系统调用了(read()、write()和close())。与管道一样,FIFO也有一页写入端和读取端,并且从管道中读取数据地顺序与写入的顺序是一样的。FIFO的名称也由此而来:先入先出。FIFO有时候也别成为命名管道。

        与管道一样,当所有引用FIFO的描述符被关闭之后,所有未被读取的数据会被丢弃。

        使用mkfifo命令可以在shell中创建一个FIFO。

$ mkfifo [-m mode] pathname

        pathname是创建的FIFO名称,-m选项用来指定权限mode,其工作方式与chmod命令一样。

        当在FIFO上调用fstat()和stat()函数时他们会在stat结构的st_mode字段返回一个类型为S_IFIFO的文件(15.1节)。当使用ls-l列出文件时,FIFO文件在第一列的类型为p,ls-F会在FIFO路径名后面附加上一个管道符(|)。

        mkfifo()函数创建一个名为pathname的全新的FIFO.

#include <sys/stat.h>
int mkfifo(const char *pathname,mode_t mode);
                    Returns 0 on success, or -1 on error

        mode()参数指定了新FIFO的权限。这些权限是通过将表15-4中的常量取OR来指定的。与往常一样,这些权限会按照进程的umask值(15.4.6节)来获取掩码。

        一旦FIFO被创建,任何进程都能打开它,只要它能通过常规的文件权限检测。(15.4.3节)

        打开一个FIFO具备一些不寻常的语义使用FIFO 的唯一明智的做法是在两端分别设置一个读取进程和一个写入进程。这样在默认情况下,打开一个FIFO以便读取数据(open()O_RDONLY标记)将会阻塞知道另一个进程打开FIFO以写入数据(open() O_WRONLY标记)为止。相应的,打开一个FIFO以写入数据将会阻塞直到另一个继承打开FIFO以读取数据为止。换句话说,打开一个FIFO会同步读取进程和写入进程。如果一个FIFO的另一端已经打开(可能是因为一堆进程已经打开了FIFO的两端),那么open()调用会立即成功。

        在大多数UNIX实现(包括Linux)上,当打开一个FIFO时可以通过指定O_RDWR标记来绕过打开FIFO时的阻塞行为。这样open()就会立即返回,但无法使用返回的文件描述符在FIFO上读取和写入数据。这种做法破坏了FIFO的I/O模型,SUSv3明确之处以O_RDWR标记打开一个FIFO的结果是未知,因此出于可移植性原因,开发人员不应该使用这项技术。对于那些需要在打开FIFO时发生阻塞的需求,open()的O_NONBLOCK标记提供了一种标准化的方法来完成这个任务(44.9节)。

        在打开一个FIFO时避免使用O_RDWR标记还有另外一个原因。当采用那种方式调用open()之后,调用进程在从返回的文件描述符中读取数据时永远不会看到文件结束,因为永远都至少有一个文件描述符被打开着以等待数据被写入FIFO,即几次呢很难过从中读取数据的哪个描述符。

使用FIFO和tee(1)创建双重管道线

        shell管道线的其中一个特征是他们是线性的,管道线中的每个进程都读取前一个进程产生的数据并将数据发送到最后一个进程中。使用FIFO就能够在管道线中创建子进程,这样除了将一个进程的输出发送给管道线中的后面一个进程之外,还可以复制进程的输出并将数据发送到另一个进程中。要完成这个任务需要使用tee命令,他将其下哦那个标准输入中读取到的数据复制两份并输出:一份写入到标准输出,零一分写入到通过命令行参数指定的文件中。

        将传给tee命名的file参数设置为一个FIFO可以让两个进程同事读取tee产生的两份数据。下面的shell会话演示了这种用法,他创建了一个名为myfifo的FIFO,然后在后台启动一个wc命令,该命令会打开FIFO以读取数据(这个操作会阻塞直到有进程打开FIFO写入数据为止),接着执行一条管道线将ls的输出发送给tee,tee会将输出传递给管道线中的下一个minglingsort,同时还会将输出发送给名为myfifo的FIFO。(sort 的 -k5n选项会导致ls的输出按照第五个以空格分隔的字段的数值升序排序。)

从图表上面来看,上面的命令创建了图44-5中给出的情形。

tee 程序之所以这样命名是因为其外形。可以将tee看成是功能与管道类似的一个实体,但它存在一个额外的分支发送一份输出的副本。从图表上来看,其形状像是一个大写字母T(参见图44-5)除了上面描述的用途之外,tee对于管道线调试和保存复杂管道线中某些中间节点的输出结果也是非常有用的。

 44.8 使用管道实现一个客户端、服务器应用程序

        本节将介绍一个简单的FFO进行IPC的客户端、服务器应用程序。服务器提供的服务是向每个发送请求的客户端赋一个唯一的顺序数字。在对这个应用程序进行讨论的过程中会介绍与服务器设计相关的一些概念和技术。

应用程序概述

        在这个示例应用程序中,所有客户端使用一个服务器FIFO来向服务器发送请求。头文件(程序清单44-6)定义了众所周知的名称(/tmp/seqnum_sv),服务器的FIFO将使用这个名称。这个名称是固定的,因此所有客户端知道如何联系到服务器。(在这个示例应用程序中系那个会在/tmp 目录创建FIFO,这样在大多数系统上都能够在不修改程序的情况下方便地运行这个程序。但正如在38.7节中指出地那样,在一个像/tmp 这样地公共科协地目录中创建文件可能会导致各种安全隐患,因此现实世界中地应用程序不应该使用这种目录。)

在客户端-服务器应用程序中将会不断地碰到一个概念,即服务器用来使用服务对客户端可见地中所周知地地址或名称。对于客户端如何知道在何处联系服务器这个问题来讲,使用众所周知地地址是一种解决方案。另一种可能地解决方案是提供某种名称服务器,服务器可以将他们地服务地名称注册到名称服务器上。然后每个客户端联系名称服务器以获取服务地为止。这个解决方案允许灵活地配置服务器地为止,而付出地代价则是需要额外地编程。当然客户端和服务器需要知道到何处联系名称服务器,它位于一个众所周知地地址。

        无法使用单个FIFO向所有哭护短发送响应,因为多个客户端在从FIFO读取数据时会相互竞争,这样就可能会出现各个客户端读取到了其他客户端地响应消息,而不是自己地响应消息。因此每个客户端需要创建一个唯一地FIFO,服务器使用这个FIFO来向该客户端递送响应,并且服务器需要知道如何找出各个客户端地FIFO。解决这个问题地一种方式是让客户端生成自己地FIFO路径名,然后将路径名作为请求消息地一部分传递给服务器。或者客户端和服务器可以约定一个构建客户端FIFO路径名地规则,然后客户端可以将构建自己夫人路径名所需要的相关信息作为请求地一部分发送给服务器。本例中将会使用后面一种解决方案。每个客户端地FIFO是从一个由包含客户端的而进程ID的路径名构成的模板(CLIENT_FIEO_TEMPLATE)中构建而来的。在生成过程中包含进程ID可以很容易产生一个对各个客户端唯一的名称。

        图44-6展示了这个应用程序如何使用FIFO来完成客户端与服务器进程之间的通信。

        头文件(程序清单44-6)定义了客户端发送给服务器的请求消息的格式和服务器发送给客户端的响应消息的格式。

         记住管道和FIFO中的数据是字节流,消息之间是没有边界的。这意味着可当多条消息被传递到一个进程中时,如本例中的服务器,发送者和接收者必须要约定某种规则来分割消息。这可以使用很多办法。

  • 每条消息使用诸如换行符之类的分隔字符结束。(使用这项技术的一个例子是程序清单59-1中的readLine()函数。)这样就必须要保证分隔符不会出现在消息中或者当他出现在消息中是2必须要采用某种规则进行转义。例如,如果使用换行符作为分隔符,那么字符\加上换行可以用来表示消息中一个真实的换行符,而\\则可以用来表示真实的\。这种方法的不足指出是读取消息的进程在从FIFO中扫描数据时必须要逐个字节地分析知道找到分隔符为止。
  • 在每条消息中包含一个大小固定地头,投中包含一个表示消息长度地字段,该字段指定了消息中剩余部分地长度。这样读取进程就需要首先从FIFO中读取头,然后使用投中地长度字段来确定需读取的消息中剩余部分的字节数。这种方法能够高效地读取任意大小地消息,但一旦不合规则(如错误地length字段)地消息被写入管道之后问题就出来了。
  • 使用固定长度地消息并让服务器总是读取这个大小固定地消息。这种方法地优势子在于简单性。但他对消息地大小设置了一个上限,意味着会浪费一些通道容量(因为需要对较短地消息进行填充以满足固定长度)。此外,如果其中一个客户端意外的或故意的发送了一条长度不对地消息,那么后续所有地消息都会出现步调不一致地情况,并且在这种情况下服务器是难以恢复的。

        图44-7展示了这三种技术。注意不管使用这三种技术的哪种,每条消息的总长度必须要小于PIPE_BUF字节以防止内核对消息进行拆分,从而造成与其他写者发送的消息错乱的情况发生。

         在正文描述的三种技术中,所有客户端发送的消息都会被放在一个通道(FIFO)中。另一种方法是为每条消息使用一个连接。发送者打开通信通道,发送消息,然后关闭通道。读取进程再碰到文件结束时就知道达到消息结尾了,如果多个写者都打开了一个FIFO,那么这种法就不可行了,因为读取在其中一个写者关闭FIFO之后不会看到文件结束。但当使用流socket时这种方法就变的可行了,因为服务器进程会为每个进入的客户端连接创建一个唯一的通信通道。

        在本章的示例应用程序中,将使用上面介绍的第三种技术,即每个客户端向服务器发送的消息的长度是固定的。程序清单44-6中的requext结构定义了消息,每个发送给服务器的请求都包含了客户端的进程ID,这样服务器就能够构建客户端用来接收响应的FIFO名称了。请求中还包括了一个seqLen气短,它指定了应该为这个客户端分配的序号的数量。服务器向客户端发送的响应消息有一个字段seqNum构成,他是为这个客户端分配的一组序号的起始值。

程序清单44-6:  fifo_seqnum.h

        

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>  /* Type definitions used by many programs */
#include <stdio.h>      /* Standard I/O functions */
#include <stdlib.h>     /* Prototypes of commonly used library functions,
                           plus EXIT_SUCCESS and EXIT_FAILURE constants */
#include <unistd.h>     /* Prototypes for many system calls */
#include <errno.h>      /* Declares errno and defines error constants */
#include <string.h>     /* Commonly used string-handling functions */


typedef enum { FALSE, TRUE } Boolean;

#define SERVER_FIFO "/tmp/seqnum_sv"
                                /* Well-known name for server's FIFO */
#define CLIENT_FIFO_TEMPLATE "/tmp/seqnum_cl.%ld"
                                /* Template for building client FIFO name */
#define CLIENT_FIFO_NAME_LEN (sizeof(CLIENT_FIFO_TEMPLATE) + 20)
                                /* Space required for client FIFO pathname
                                  (+20 as a generous allowance for the PID) */

struct request {                /* Request (client --> server) */
    pid_t pid;                  /* PID of client */
    int seqLen;                 /* Length of desired sequence */
};

struct response {               /* Response (server --> client) */
    int seqNum;                 /* Start of sequence */
};

服务器程序

        程序清单44-7是服务器的代码。这个服务器按序万郴个下面的工作。

  • 创建服务器的众所周知的FIFO并打开FIFO以便读取。服务器必须要在客户端运行之前运行,这样服务器的FIFO在客户端试图打开它之前就已经存在了。服务器的open()调用将会阻塞直到第一个客户端打开了服务器的FIFO的另一端以写入数据为止。
  • 再次打开服务器的FIFO,这次是为了写入数据。这个调用永远不会被阻塞,因为之前已经因需读取而打开FIFO了。第二个打开操作是为了确保服务器所在客户端关闭了FIFO的写入端之后不会看到问价结束。
  • 忽略SIGPIPE信号,这样如果服务器试图向一个没有读者的客户端FIFO写入数据时不会受到SIGPIPE信号(默认会杀死进程),而是会从write()系统调用中收到一个EPIPE错误。
  • 进入一个循环从每个进入的客户端请求中读取数据并响应,要发送响应,服务器需要构建客户端FIFO的名称,然后打开这个FIFO.
  • 如果服务器在打开客户端FIFO时发生了错误,那么就丢弃那个客户端的请求。

这是一种迭代式服务器,这种服务器会在读取和处理完当前客户端之后蔡虎去处理下一个客户端。当每个客户端请求的处理和响应都能够快速完成时采用这种迭代式服务器设计是合理的,因为不会对其他客户端请求的处理产生延迟。另一种设计方法是并发式服务器,在这种设计中主服务器进程使用单独的子进程(或线程)来处理各个客户端的请求。

程序清单44-7:使用FIFO的迭代式服务器----------fifo_seqnum_server.c


#include <signal.h>
#include "fifo_seqnum.h"

int
main(int argc, char *argv[])
{
    int serverFd, dummyFd, clientFd;
    char clientFifo[CLIENT_FIFO_NAME_LEN];
    struct request req;
    struct response resp;
    int seqNum = 0;                     /* This is our "service" */

    /* Create well-known FIFO, and open it for reading */

    umask(0);                           /* So we get the permissions we want */
    if (mkfifo(SERVER_FIFO, S_IRUSR | S_IWUSR | S_IWGRP) == -1
            && errno != EEXIST)
    {
        perror("mkfifo :");
        return -1;
    }
        
    serverFd = open(SERVER_FIFO, O_RDONLY);
    if (serverFd == -1)
    {
        perror("open :");
        return -1;
    }

    /* Open an extra write descriptor, so that we never see EOF */

    dummyFd = open(SERVER_FIFO, O_WRONLY);
    if (dummyFd == -1)
    {
        perror("open2 :");
        return -1;
    }

    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
         perror("signal :");
        return -1;
    }   

    for (;;) {                          /* Read requests and send responses */
        if (read(serverFd, &req, sizeof(struct request))
                != sizeof(struct request)) {
            fprintf(stderr, "Error reading request; discarding\n");
            continue;                   /* Either partial read or error */
        }

        /* Open client FIFO (previously created by client) */

        snprintf(clientFifo, CLIENT_FIFO_NAME_LEN, CLIENT_FIFO_TEMPLATE,
                (long) req.pid);
        clientFd = open(clientFifo, O_WRONLY);
        if (clientFd == -1) {           /* Open failed, give up on client */
            printf("open %s", clientFifo);
            continue;
        }

        /* Send response and close FIFO */

        resp.seqNum = seqNum;
        if (write(clientFd, &resp, sizeof(struct response))
                != sizeof(struct response))
            fprintf(stderr, "Error writing to FIFO %s\n", clientFifo);
        if (close(clientFd) == -1)
            printf("close");

        seqNum += req.seqLen;           /* Update our sequence number */
    }
}

  客户端程序

            程序清单44-8是客户端的代码。客户端按序完成了下面的工作。

  • 创建一个FIFO以从服务器接收响应。这项工作是在发送请求之前完成的,这样才能确保FIFO在服务器试图打开它冰箱其发送响应消息之前就已经存在了。
  • 构建一条发送给服务器的消息,消息中包含了客户端的进程ID和一个只当了客户端希望赋给它的序号长度的数字(从可选的命令行参数中获取)如果没有提供命令行参数,那么默认的序号长度是1.
  • 打开服务器FIFO,并将消息发送给服务器
  • 大开客户端FIFO,然后读取和打印服务器的响应。

        另一个需要注意的地方是通过atexit()建立的退出处理器,他确保了当进程退出之后客户端的FIFO会被删除。或者可以在客户端FIFO的open调用之后立即调用unlink()。在那个时刻这种做法是能够正常工作的,因为他们都执行了阻塞FIFO的open()调用,服务器和客户端各自持有了FIFO的打开着的文件描述符,而从文件系统中删除FIFO名称不会对这些描述符以及它们所引用的打开着的文件描述符产生影响。

        下面是这个客户端和服务器程序时看到的输出

 程序清单44-8 序号服务器的客户端 ---fifo_seqnum_client.c



#include "fifo_seqnum.h"

static char clientFifo[CLIENT_FIFO_NAME_LEN];

static void             /* Invoked on exit to delete client FIFO */
removeFifo(void)
{
    unlink(clientFifo);
}

int
main(int argc, char *argv[])
{
    int serverFd, clientFd;
    struct request req;
    struct response resp;

    if (argc > 1 && strcmp(argv[1], "--help") == 0)
    {
        printf("%s [seq-len]\n", argv[0]);
        return -1;
    }

    /* Create our FIFO (before sending request, to avoid a race) */

    umask(0);                   /* So we get the permissions we want */
    snprintf(clientFifo, CLIENT_FIFO_NAME_LEN, CLIENT_FIFO_TEMPLATE,
            (long) getpid());
    if (mkfifo(clientFifo, S_IRUSR | S_IWUSR | S_IWGRP) == -1
                && errno != EEXIST)
        {
            perror("mkfifo:");
            return -1;
        }

    if (atexit(removeFifo) != 0)
    {
        perror("atexit:");
        return -1;
    }

    /* Construct request message, open server FIFO, and send message */

    req.pid = getpid();
    req.seqLen = (argc > 1) ? atoi(argv[1]) : 1;

    serverFd = open(SERVER_FIFO, O_WRONLY);
    if (serverFd == -1)
    {
        perror("open:");
        return -1;
    }

    if (write(serverFd, &req, sizeof(struct request)) !=
            sizeof(struct request))
    {
        printf("Can't write to server");
        return -1;
    }
        

    /* Open our FIFO, read and display response */

    clientFd = open(clientFifo, O_RDONLY);
    if (clientFd == -1)
    {
        perror("open2:");
        return -1;
    }

    if (read(clientFd, &resp, sizeof(struct response))
            != sizeof(struct response))
    {
        printf("Can't read response from server");
        return -1;
    }

    printf("%d\n", resp.seqNum);
    exit(EXIT_SUCCESS);
}

44.9 非阻塞I/O

        前面曾经提到过当一个进程打开一个FIFO的一端时,如果FIFO的另一端还没有被打开,那么该进程就会被阻塞。但有时候阻塞并不是期望的行为,而这可以在通过调用open()时指定O_NONBLOCK标记来实现。

fd = open("fifopath",O_RDONLY | O_NONBLOCK);
if(fd == -1)
{
    perror("open:");
    return -1;
}

        如果FIFO的另一端已经被打开,那么O_NONBLOCK对open调用不会产生任何影响--他会像往常一样立即成功地打开FIFO。只有当FIFO的另一端还没有被打开的时候O_NONBLOCK标记才会其作用,二聚体产生的影响依赖于打开FIFO是用于读取还是用于写入的。

  • 如果打开FIFO 是为了读取,并且FIFO的写入端当前已经被打开,那么open()调用会立即成功(就像FIFO的另一端已经被打开一样)。
  • 如果打开FIFO是为了写入,并且还没有打开FIFO的另一端来读取数据,那么open(0调用会失败,并将errno设置为ENXIO。

        为了读取而打开FIFO和为写入而打开FIFO时O_NONBLOCK标记所起的作用不同是有原因的。当FIFO的另一个端没有写着时打开一个FIFO以便读取数据是没有问题的,因为任何试图从FIFO读取数据的操作都不会返回任何数据。但当试图向没有读者的FIFO中写入数据时将会导致SIGPIPE信号的产生以及write()返回EPIPE错误。

        表44-1:在FIFO上调用open的语义

         在打开一个FIFO时使用O_NONBLOCK标记存在的两个目的。

  • 它允许单个进程打开一个FIFO的两端。这个几次呢很难过首先会在打开FIFO时指定O_NONBLOCK标记以便读取数据,接着打开FIFO以便写入数据。
  • 它放置打开两个FIFO的进程之间产生死锁。

        当两个或多个进程中每个进程痘印等待对方完成某个动作而阻塞时会产生死锁。图44-8给出了两个进程发生死锁的情形。各个进程痘印等待打开一个FIFO以便读取数据而阻塞。如果各个进策划那个都可以执行其第二个步骤(打开一个FIFO以便写入数据)的话就不会发生阻塞。这个特定的死锁问题是通过颠倒进程Y中的步骤1和步骤2并保持进程X中了两个步骤的顺序不变来解决,反之亦然。但在一些引用程序中进行这样的调整可能并不容易。相反,可以通过在为读取而打开的FIFO时让其中一个进程或两个进程都指定O_NONBLOCK标记来解决这个问题。

 非阻塞read()和write()

        O_NONBLOCK标记不仅会影响open()的语义,而且还会印象---因为在打开的文件描述符中这个标记仍然被设置着--后续的read()和write()调用的语义。

        有些是有需要修改一个已经打开的FIFO(或另一种类型的文件)的O_NONBLOCK标记的状态,具体存在这个需求的场景包括以下几种。

  • 使用O_NONBLOCK打开了一个FIFO但需要让后续的read()和write()调用在阻塞模式下运作。
  • 需要启用从pipe()返回的一个文件描述符的非阻塞模式。更一般地,可能需要更改从除open()调用之外地其他调用中---如每个由shell运行地新程序中自动被打开地三个标准描述符地其中一个或socket()返回地文件描述符--取得的任意文件描述符地非阻塞状态。
  • 出于一些应用程序地特殊要求,需要切换一个文件描述符地O_NONBLOCK色湖之地开启和关闭状态。

        当碰到上面地需求时可以使用fcntl()启用或禁用打开者的文件地O_NONBLOCK状态标记。通过i下面地代码可以启用这个标记。

flags =fcntl(fd,F_GETFL);  //Fetch open files atstus flags
FLAGS = |O_NONBLOCK;        // Enable O_NONBLOCK bit
fcntl(fd,F_SETFL,flags);  //Update open files status flags

通过下面地代码可以禁用这个标记。

flags =fcntl(fd,F_GETFL);   
FLAGS &= ~O_NONBLOCK;        // Disable O_NONBLOCK bit
fcntl(fd,F_SETFL,flags);   

44.10 管道和FIFO中地read()和write()的语义

        表44-2对管道和FIFO上的read()操作进行了总结,包括O_NONBLOCK标记的作用

         只有当没有数据并且写入端没有被打开时阻塞和非阻塞读取之间才存在差别。在这种情况下,普通的read()会被阻塞,而非阻塞read()会失败并返回EAGAIN错误。

        当O_NONBLOCK标记与PIPE_BUF限制共同起作用时O_NONBLOCK标记对象管道或FIFO写入数据的影响会变得复杂。表44-3对write()的行为进行了总结。

         当数据无法立即被传输时O_NONBLOCK标记会导致在一个管道或FIFO上的write()失败(错误是EAGAIN)。这意味着 当写入了PIPE_BUF字节之外,如果在管道或FIFO中没有足够的空间了,那么write()会失败,因为内核无法立即完成这个操作并且无法执行部分写入,否则就会破坏了不超过PIPE_BUF字节的写入操作的原子性的要求。

        当一次写入的数量超过PIPE_BUF字节时,该写入操作无需是原子的。因此write()会尽可能地多传输字节(部分写)以充满管道或FIFO。在这种情况下,从write()返回地值是实际传输地zijieshu-,并且调用者随后必须要进行重试以写入剩余地字节。但如果管道或FIFO已经满了,从而导致哪怕连一个字节都无法传输了,那么write()会失败并返回EAGAIN错误。

44.11 总结

        管道是UNIX系统上出现地第一种IPC 方法,shell以及其他应用程序经常会使用管道,管道是一个单项、容量有限地字节流,它可以用于相关进程之间地通信。尽管写入管道地数据块地大小是随意地,但只有那些写入地数据量不超过PIPE_BUF字节地写入操作才被确保是原子地。除了是一种IPC方法之外,管道还可以用于进程同步。

        在使用管道时必须要小心地关闭未使用地描述符以确保进程能够收到SIGPIPE信号或EPIPE错误。(通常,最简单地做法是让向管道写入数据地应用程序忽略SIGPIPE并通过EPIPE错误检测管道是否坏了。)

        popen()和pclose()函数允许一个程序像一个标准shell命令传输数据或从中读取数据,而无需处理创建管道、执行shell以及关闭未使用地文件描述符地细节。

        FIFO除了mkfifo()创建和在文件系统中存在一个名称以及可以被拥有合适地权限地任意进程打开之外,其运作方式与管道完全一样。在默认情况下,为读取数据而打开一个FIFO会被阻塞直到另一个进程为写入数据而打开了FIFO,反之亦然。

        本章讨论了几个相关地主题。首先介绍了如何赋值闻不见描述符使得一个锅炉恶气地标准输入或输出可以被绑定到一个管道上。在介绍使用FIFO构建一个客户端-服务器地例子中介绍了几个与服务器的例子中介绍了几个客户端与服务器设计相关的主题,包括为服务器使用一个众所周知的地址以及迭代式服务器和并发服务器之间的对比。在开发示例FIFO应用程序时提到尽管通过管道传输的数据是一个字节流,但有时候将数据打包成消息对于通信来讲也是有用的,并且介绍了几种将数据打包成消息的方法。

        最后介绍了在打开一个FIFO并执行I/O时O_NONBLOCK标记(非阻塞I/O)的影响。O_NONBLOCK标记对于在打开FIFO时不希望阻塞来讲是有用的,同时对读取操作在没有数据可用时不阻塞或再写入操作在管道或FIFO没有足够的空间时不阻塞也是有用的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值