进程间通信简介(三)——命名管道

来源:互联网 发布:佳为软件称软件 编辑:程序博客网 时间:2024/06/05 10:21

4 命名管道

        命名管道(named pipe)也称为FIFO,它是一种文件类型,在文件系统中可以看到它,创建一个FIFO文件类似于创建一个普通文件。在程序中可以通过查看文件stat结构中st_mode成员的值来判断该文件是否为FIFO。

4.1 命名管道的概念

        管道应用的一个重大限制就是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在命名管道(或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO互相通信(能够访问该路径的进程及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。

        命名管道区别于管道主要体现在以下两点:

  • 命名管道可以用于任何两个进程间的通信,而并不限制这两个进程同源,因此命名管道的使用比管道的使用更灵活方便的多。
  • 命名管道作为一种特殊的文件存放于文件系统中,而不是像管道一样存放于内存(使用完毕后消失)。当进程对命名管道的使用结束后,命名管道依然存在于文件系统中,除非对其进行删除操作,否则该命名管道不会消失。

        FIFO的出现也极好地解决了系统在应用过程中产生的大量的中间临时文件的问题。FIFO可以被shell调用使数据从一个进程到另一个进程,系统不必为该中间通道而清理不必要的垃圾,或者去释放该通道的资源,它可以被留作后来的进程使用。另外,需要注意的是,FIFO严格遵循先进先出(first in first out)的规则,对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持注入lseek等文件定位操作。

        在shell中可以使用mkfifo命令创建一个命名管道,mkfifo命令的格式如下:

mkfifo [option] name...

        其中option选项中可以选择要创建FIFO的模式,使用形式为-m mode,这里mode指出将要创建FIFO的八进制模式,注意,这里新创建的FIFO会像普通文件一样受到创建进程的umask修正。name表示所要创建的FIFO管道的名称。

        过去,命令行上用来创建命名管道的程序是mknod,如下所示:

mknod filename p

4.2 命名管道的创建

        创建一个FIFO文件类似于创建一个普通文件,并且FIFO文件和其他文件一样,也是可以通过路径名来访问的。FIFO管道通过函数mkfifo创建,函数原型如下:

#include <sys/types.h>

#include <sys/stat.h>

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

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

        该函数的第一个参数是一个普通的路径名,也就是创建后FIFO文件的名字。第二个参数与打开普通文件的open函数中的mode参数相同。如果mkfifo的第一个参数是一个已经存在的路径名,则返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等。

        还有一种函数调用也可以创建命名管道文件,但是不是所有系统都能使用,所以一般就用mkfifo进行命名管道的创建。

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

int mknod (const char *pathname, mode_t mode, dev_t dev);

        mknod可以创建许多特殊类型的文件,想要通过这个函数创建一个命名管道,唯一具有可移植性的方法是使用一个dev_t类型的值0,并将访问模式与S_IFIFO按位或,所以使用mknod创建一个命名管道的函数形式可以固定为:

int mknod (const char *pathname, mode_t mode | S_IFIFO, (dev_t) 0);

        接下来演示使用mkfifo函数创建一个FIFO。

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <errno.h>#include <stdlib.h>int main(int argc, char **argv){mode_t mode = 0666;if (argc != 2){printf("USEMSG: FIFO_create { FIFO name }\n");exit(1);}if ((mkfifo(argv[1], mode)) < 0){perror("failed to mkfifo!\n");exit(1);}else{printf("you successfully create a FIFO name is: %s\n", argv[1]);}exit(EXIT_SUCCESS);}

        编译并运行:

$ ./FIFO_create
USEMSG: FIFO_create { FIFO name }
$

        缺少创建的FIFO的名称,程序自动提示。再次运行:

$ ./FIFO_create test
you successfully create a FIFO name is: test
$

        成功创建一个名字为test的FIFO。再次运行:

$ ./FIFO_create test
failed to mkfifo!
: File exists
$

        还想创建一个叫test的FIFO的时侯,它会检测到文件已存在并且报错。

        值得一提的是,命名管道FIFO比管道多了一个打开操作open,这是因为命名管道是Linux下的一种文件类型,而管道不是。然而,FIFO的打开与其他文件的打开是不同的,FIFO的打开规则是:

  • 如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。
  • 如果当前打开操作是为写而打开FIFO时,若已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,饭或ENXIO错误(当前打开操作没有设置阻塞标志)。

4.3 命名管道的读写

        使用命名管道的操作和使用普通文件十分相似,可以使用系统调用open打开一个命名管道,使用read和write函数对命名管道进行读写,使用close关闭一个命名管道,若要删除一个命名管道,则使用系统调用unlink。

        从FIFO中读取数据的规则是:

  • 如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。
  • 如果有劲诚写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞表示的符操作来说,将一直阻塞。对于没有设置阻塞表示的读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
  • 对于设置了阻塞表示的读操作来说,造成阻塞的原因有两种:当前FIFO内有数据,但有其他进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论新写入数据量的大小,也不论读操作请求多少数据量。
  • 读打开的阻塞表示只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其他将要执行的读操作将不再阻塞,即使在执行符操作时,FIFO中没有数据也一样(此时,读操作返回0)。
  • 如果没有进程写打开FIFO,则设置了阻塞表示的读操作会阻塞。

        注意,如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。

        向FIFO中写入数据的规则是:

  • 如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。
  • 对于设置了阻塞标志的写操作,当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
  • 当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
  • 对于没有设置阻塞标志的写操作,当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
  • 当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写。

        注意,原子操作(atomic opeartion)指的是由多步组成的操作。简单来讲,操作的原子性是指某一事务中的所有操作要么全部执行,要么全部不执行,不可能只执行所有步骤的一个子集。

        接下来演示通过FIFO管道传输数据。

        fifo_write程序源码:

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <limits.h>#include <stdlib.h>#include <time.h>#define BUFS PIPE_BUFint main(){int fd;int n, i;char buf[BUFS];time_t tp;printf("I am %d\n", getpid());if (access("fifo1", F_OK) != 0){if (mkfifo("fifo1", 0666)){printf("fifo1 not existed, and create fifo1 failed!\n");exit(1);}}if ((fd = open("fifo1", O_WRONLY)) < 0){printf("Open failed!\n");exit(1);}for (i = 0; i < 10; i++){time(&tp);n = sprintf(buf, "write_fifo %d sends %s", getpid(), ctime(&tp));printf("Send msg:%s", buf);if ((write(fd, buf, n + 1)) < 0){printf("Write failed!\n");close(fd);exit(1);}sleep(1);}close(fd);exit(0);}
        fifo_read程序源码:

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>#include <fcntl.h>#include <limits.h>#include <stdlib.h>#define BUFS PIPE_BUFint main(){int fd;int len;char buf[BUFS];if ((fd = open("fifo1", O_RDONLY)) < 0){printf("Open failed\n");exit(1);}while ((len = read(fd, buf, BUFS)) > 0)printf("Read_fifo read: %s", buf);close(fd);exit(0);}

        开启两个shell,一个运行fifo_write,一个运行fifo_read,fifo_write运行结果如下:

$ ./fifo_write
I am 9267
Send msg:write_fifo 9267 sends Mon Oct 26 09:45:34 2015
Send msg:write_fifo 9267 sends Mon Oct 26 09:45:35 2015
Send msg:write_fifo 9267 sends Mon Oct 26 09:45:36 2015
Send msg:write_fifo 9267 sends Mon Oct 26 09:45:37 2015
Send msg:write_fifo 9267 sends Mon Oct 26 09:45:38 2015
Send msg:write_fifo 9267 sends Mon Oct 26 09:45:39 2015
Send msg:write_fifo 9267 sends Mon Oct 26 09:45:40 2015
Send msg:write_fifo 9267 sends Mon Oct 26 09:45:41 2015
Send msg:write_fifo 9267 sends Mon Oct 26 09:45:42 2015
Send msg:write_fifo 9267 sends Mon Oct 26 09:45:43 2015
$

        fifo_read运行结果如下:

$ ./fifo_read
Read_fifo read: write_fifo 9267 sends Mon Oct 26 09:45:34 2015
Read_fifo read: write_fifo 9267 sends Mon Oct 26 09:45:35 2015
Read_fifo read: write_fifo 9267 sends Mon Oct 26 09:45:36 2015
Read_fifo read: write_fifo 9267 sends Mon Oct 26 09:45:37 2015
Read_fifo read: write_fifo 9267 sends Mon Oct 26 09:45:38 2015
Read_fifo read: write_fifo 9267 sends Mon Oct 26 09:45:39 2015
Read_fifo read: write_fifo 9267 sends Mon Oct 26 09:45:40 2015
Read_fifo read: write_fifo 9267 sends Mon Oct 26 09:45:41 2015
Read_fifo read: write_fifo 9267 sends Mon Oct 26 09:45:42 2015
Read_fifo read: write_fifo 9267 sends Mon Oct 26 09:45:43 2015
$

        程序fifo_write使用open函数以写方式打开一个名为fifo1的FIFO管道(当然,之前还要检测是否存在此文件,不存在则创建一个),并分10次向fifo1中写入字符串,其中的数据有当前进程ID及写入时的系统时间,并把这个数据传输出到标准输出,然后程序自动睡眠1秒。程序fifo_read使用open函数以读方式打开一个名为fifo1的FIFO管道,并循环读出管道的数据,这里使用while循环的作用就是确保数据可以全部读出,因为在读FIFO管道数据时,默认的是一次性读取PIPE_BUF个字节,当管道中数据多于PIPE_BUF个字节时,一次性读出PIPE_BUF - 1个字节,然后read函数返回,再打印数据到标准输出。

        以上例子可以扩展成客户端与服务器通信的实例,fifo_write的作用类似于客户端,可以打开多个客户端向一个服务器发送请求信息,fifo_read类似于服务器,它适时监控着FIFO的读出端,当有数据时,读出并运行处理,但是有一个关键的问题是,每一个客户端必须预先知道服务器提供的FIFO接口。

4.4 访问FIFO文件

        命名管道的一个非常有用的特点是:由于它们出现在系统文件中,所以它们可以像平常的文件名一样在命令中使用。在把创建的FIFO文件用在程序设计中之前,我们先通过普通的文件命令来观察FIFO文件的行为。
        实验:访问FIFO文件
        1. 首先,我们来尝试读这个(空的)FIFO文件:
        $ cat < /tmp/my_fifo
        2. 现在,尝试向FIFO写数据。必须用另一个终端来执行下面的命令,因为第一个命令现在被挂起以等待数据出现在FIFO中。

        $ echo "Hello World" > /tmp/my_fifo

        你将看到cat命令产生输出。如果不向FIFO发送任何数据,cat命令将一直挂起,直到你中断它,常用的中断方式是使用组合键“Ctrl+c”。

        3. 我们可以将第一个命令放在后台执行,这样即可一次执行两个命令:

        $ cat < /tmp/my_fifo &

        [1] 20321

        $ echo "Hello World" > /tmp/my_fifo

        Hello World

        [1]+    Done                                  cat < /tmp/my_fifo

        $

        解析:因为FIFO中没有数据,所以cat和echo程序都阻塞了,cat等待数据的到来,而echo等待其它进程读取数据。在上面的第三步中,cat进程一开始就在后台被阻塞了,当echo向它提供了一些数据后,cat命令读取这些数据并把它们打印到标准输出上,然后cat程序退出,不再等待更多的数据。它没有阻塞是因为当第二个命令将数据放入FIFO后,管道将被关闭,所以cat程序中的read调用返回0字节,表示已经到达文件尾。

        现在我们已看过用命令行程序访问FIFO的情况,接下来我们将仔细分析FIFO的编程接口,它可以让我们在访问FIFO文件时更多地控制其读写行为。与通过pipe调用创建管道不同,FIFO是以命名文件的形式存在,而不是打开的文件描述符,所以在对它进行读写操作之前必须先打开它。FIFO也用open和close函数打开和关闭,这与我们前面看到的对文件的操作一样,但它多了一些其它的功能,传递给open调用的是FIFO的路径名,而不是一个正常的文件。

4.4.1 使用open打开FIFO文件

        打开FIFO的一个主要限制是,程序不能以O_RDWR模式打开FIFO文件进行读写操作,这样做的后果并未明确定义。但这个限制是有道理的,因为我们通常使用FIFO只是为了单向传递数据,所以没有必要使用O_RDWR模式。如果一个管道以读/写方式打开,进程就会从这个管道读回它自己的输出。如果确实需要在程序之间双向传递数据,最好使用一对FIFO或管道,一个方向使用一个,或者(但并不常用)采用先关闭再重新打开FIFO的方法来明确地改变数据流的方向。后续再讨论用FIFO进行双向数据交换的问题。
        打开FIFO文件和打开普通文件的另一点区别是,对flags(open函数的第二个参数)的O_NONBLOCK选项的用法。使用这个选项不仅改变open调用的处理方式,还会改变对这次open调用返回的文件描述符进行的读写请求的处理方式。
        O_RDONLY、O_WRONLY和O_NONBLOCK标志共有4中合法的组合方式,接下来逐个介绍。
open(const char *path, O_RDONLY);
        在这种情况下,open调用将阻塞,除非有一个进程以写方式打开同一个FIFO,否则它不会返回,这与前面第一个cat命令的例子类似。
open(const char *path, O_RDONLY | O_NONBLOCK);
        在这种情况下,即使没有其它进程以写方式打开FIFO,这个open调用也将成功并立刻返回。
open(const char *path, O_WRONLY);
        在这种情况下,open调用将阻塞,直到有一个进程以读方式打开同一个FIFO为止。
open(const char *path, O_WRONLY | O_NONBLOCK);
        这个函数调用总是立刻返回,但如果没有进程以读方式打开FIFO文件,open调用将返回一个错误-1并且FIFO也不会被打开。如果确实有一个进程以读方式打开FIFO文件,那么我们就可以通过它返回的文件描述符对这个FIFO文件进行写操作。
        注意O_NONBLOCK分别搭配O_RDONLY和O_WRONLY在效果上的不同,如果没有进程以读方式打开管道,非阻塞写方式的open调用将失败,但非主色读方式的open调用总是成功。close调用的行为并不受O_NONBLOCK标志的影响。
        接下来演示如何通过使用带O_NONBLOCK标志的open调用的行为来同步两个进程。

#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <sys/types.h>#include <sys/stat.h>#define FIFO_NAME "/tmp/my_fifo"int main(int argc, char **argv){        int res;        int open_mode = 0;        int i;        if (argc < 2)        {                fprintf(stderr, "Usage: %s <some combination of O_RDONLY O_WRONLY O_NONBLOCK>\n", *argv);                exit(EXIT_FAILURE);        }        for (i = 1; i < argc; i++)        {                if (strncmp(*++argv, "O_RDONLY", 8) == 0)                        open_mode |= O_RDONLY;                if (strncmp(*argv, "O_WRONLY", 8) == 0)                        open_mode |= O_WRONLY;                if (strncmp(*argv, "O_NONBLOCK", 10) == 0)                        open_mode |= O_NONBLOCK;        }        if (access(FIFO_NAME, F_OK) == -1)        {                res = mkfifo(FIFO_NAME, 0777);                if (res != 0)                {                        fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);                        exit(EXIT_FAILURE);                }        }        printf("Process %d opening FIFO\n", getpid());        res = open(FIFO_NAME, open_mode);        printf("Process %d result %d\n", getpid(), res);        sleep(5);        if (res != -1) close(res);        printf("Process %d finished\n", getpid());        exit(EXIT_SUCCESS);}
        编译并运行发现,当运行时传入参数为O_RDONLY或者O_WRONLY的时候,程序在打开FIFO_NAME的时候就阻塞了。当运行参数传入O_NONBLOCK的时候,程序执行没有遇到阻塞的情况。打印结果略。

4.4.2 不带O_NONBLOCK标志的O_RDONLY和O_WRONLY

        现在有了测试程序,可以逐个尝试标志的不同组合方式。我们将第一个读取者程序放在后台运行:
$ ./fifo_behaviour O_RDONLY &
[1] 3682
$ Process 3682 opening FIFO
$ ./fifo_behaviour O_WRONLY
Process 3684 opening FIFO
Process 3684 result 3
Process 3682 result 3
Process 3684 finished
$ Process 3682 finished
        这可能是命名管道最常见的方法了。它允许先启动读进程,并在open调用中等待,当第二个程序打开FIFO文件时,两个程序继续运行。注意,读进程和写进程在open调用处取得同步。当一个Linux进程被阻塞时,它并不消耗CPU资源,所以这种进程的同步方式对CPU来说是非常有效率的。

4.4.3 带O_NONBLOCK标志的O_RDONLY和不带该标志的O_WRONLY

        这次读进程执行open调用并立刻继续执行,即使没有写进程的存在。随后写进程开始执行,它也在执行open调用后立刻继续执行,但这次是因为FIFO已被读进程打开。
$ Process 3713 opening FIFO
Process 3713 result 3
./fifo_behaviour O_WRONLY
Process 3714 opening FIFO
Process 3714 result 3
Process 3713 finished
Process 3714 finished
[1]+  Done                    ./fifo_behaviour O_RDONLY O_NONBLOCK
$
        这两个例子可能是open模式的最常见的组合形式。你还可以用这个示例程序随意尝试其它组合方式。

4.4.4 对FIFO进行读写操作

        使用O_NONBLOCK模式会影响到对FIFO的read和write调用。对一个空的、阻塞的FIFO(即没有用O_NONBLOCK标志打开)的read调用将等待,直到有数据可以读时才继续执行。与此相反,对一个空的、非阻塞的FIFO的read调用将立刻返回0字节。对一个完全阻塞FIFO的write调用将等待,直到数据可以被写入时才继续执行。如果FIFO不能接收所有写入的数据(这里所指的情况是当FIFO被设置为非主色模式时),它将按下面的规则执行(前面已经说过,再啰嗦一遍)。
  • 如果请求写入的数据长度小于等于PIPE_BUF字节,调用失败,数据不能写入。
  • 如果请求写入的数据长度大于PIPE_BUF字节,将写入部分数据,返回实际写入的字节数,返回值也可能是0。
        FIFO的长度是需要考虑的一个很重要的因素。系统对任一时刻在一个FIFO中可以存在的数据长度是有限制的。它由#define PIPE_BUF语句定义,通常可以在头文件limits.h中找到它。在Linux和许多其他类Unix系统中,它的值通常是4096字节,但在某些系统中它可能会小到512字节。系统规定:在一个以O_WRONLY方式(即阻塞方式)打开的FIFO中,如果写入的数据长度小于等于PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。
        虽然,对只有一个FIFO写进程和一个FIFO读进程的简单情况来说,这个限制并不是非常重要,但只使用一个FIFO并允许多个不同的程序向一个FIFO读进程发送请求的情况是很常见的。如果几个不同的程序尝试同时向FIFO写数据,能够保证来自不同程序的数据块不相互交错就非常关键了。也就是说,每个写操作都必须是“原子化”的。怎么样做到这一点呢?如果你能保证所有的写请求是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据绝不会交错在一起。通常将每次通过FIFO传递的数据长度限制为PIPE_BUF字节是个好方法,除非你只使用一个写进程和一个读进程。
        接下来演示不相关的进程是如何使用命名管道通信的。需要两个独立程序。
        生产者程序,它在需要时创建管道,然后尽可能地向管道中写入数据。
#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <limits.h>#include <sys/types.h>#include <sys/stat.h>#define FIFO_NAME "/tmp/my_fifo"#define BUFFER_SIZE PIPE_BUF#define TEN_MSG (1024 * 1024 * 10)int main(){        int fd;        int res;        int open_mode = O_WRONLY;        int bytes_sent = 0;        char buffer[BUFFER_SIZE + 1];        if (access(FIFO_NAME, F_OK) == -1)        {                res = mkfifo(FIFO_NAME, 0777);                if (res != 0)                {                        fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);                        exit(EXIT_FAILURE);                }        }        printf("Process %d opening FIFO O_WRONLY\n", getpid());        fd = open(FIFO_NAME, open_mode);        printf("Process %d result %d\n", getpid(), fd);        if (fd != -1)        {                while (bytes_sent < TEN_MSG)                {                        res = write(fd, buffer, BUFFER_SIZE);                        if (res == -1)                        {                                fprintf(stderr, "Write error on pipe\n");                                exit(EXIT_FAILURE);                        }                        bytes_sent += res;                }                close(fd);        }        else                exit(EXIT_FAILURE);        printf("Process %d finished\n", getpid());        exit(EXIT_SUCCESS);}
        消费者程序,从FIFO读取数据并丢弃它们。
#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <limits.h>#include <sys/types.h>#include <sys/stat.h>#define FIFO_NAME "/tmp/my_fifo"#define BUFFER_SIZE PIPE_BUFint main(){        int fd;        int res;        int open_mode = O_RDONLY;        char buffer[BUFFER_SIZE + 1];        int bytes_read = 0;        memset(buffer, '\0', sizeof(buffer));        printf("Process %d opening FIFO O_RDONLY\n", getpid());        fd = open(FIFO_NAME, open_mode);        printf("Process %d result %d\n", getpid(), fd);        if (fd != -1)        {                do                {                        res = read(fd, buffer, BUFFER_SIZE);                        bytes_read += res;                } while (res > 0);                close(fd);        }        else                exit(EXIT_FAILURE);        printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);        exit(EXIT_SUCCESS);}
        两个程序编译并运行,同时用time命令对读进程进行计时。
$ ./fifo_producer &
[1] 3913
Process 3913 opening FIFO O_WRONLY
$ time ./fifo_customer
Process 3914 opening FIFO O_RDONLY
Process 3913 result 3
Process 3914 result 3
Process 3914 finished, 10485760 bytes read
Process 3913 finished


real    0m0.061s
user    0m0.003s
sys     0m0.049s
[1]+  Done                    ./fifo_producer
$
        两个程序使用的都是阻塞模式的FIFO。Linux会安排好这两个进程之间的调度,使它们在可以运行的时候运行,在不能运行的时候阻塞。因此,写进程将在管道满时阻塞,读进程将在管道空时阻塞。time命令的输出显示,读进程只运行了不到0.1秒的时间,却读取了10MB的数据,这说明管道(至少在现代Linux系统中的实现)在程序之间传递数据是很有效率的。

4.4.5 高级主题:使用FIFO的客户/服务器应用程序

        接下来通过命名管道来编写一个非常简单的客户/服务器应用程序。我们想只用一个服务器进程来接受请求,对它们进行处理,最后把结果数据返回给发送请求的一方——客户。
        我们想允许多个客户进程都可以向服务器发送数据,为了使问题简单化,我们假设被处理的数据可以被拆分为一个个数据块,每个的长度都小于PIPE_BUF字节。当然,我们可以用很多方法来实现这个系统,但在这里只考虑一种方法,即可以体现如何使用命名管道的方法。
        因为服务器每次只能处理一个数据块,所以只使用一个FIFO应该是合乎逻辑的,服务器通过它读取数据,每个客户向它写数据。只要将FIFO以阻塞模式打开,服务器和客户就会根据需要自动被阻塞。将处理后的数据返回给客户时,需要为每个客户安排第二个管道来接收返回的数据。通过在传递给服务器的原先数据中加上客户的进程标识符(PID),双方就可以使用它来为返回数据的管道生成一个唯一的名字。
        首先,创建一个头文件client.h,它定义了客户和服务器程序都会用到的数据,为了方便使用,它还包含了必要的系统头文件。
        系统头文件client.h:
#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <string.h>#include <fcntl.h>#include <limits.h>#include <sys/types.h>#include <sys/stat.h>#define SERVER_FIFO_NAME "/tmp/serv_fifo"#define CLIENT_FIFO_NAME "/tmp/cli_%d_fifo"#define BUFFER_SIZE 20struct data_to_pass_st{        pid_t client_pid;        char data[BUFFER_SIZE - 1];};
        服务端代码:
#include "client.h"#include <ctype.h>int main(){        int server_fifo_fd, client_fifo_fd;        struct data_to_pass_st my_data;        int read_res;        char client_fifo[256];        char *tmp_char_ptr;        mkfifo(SERVER_FIFO_NAME, 0777);        server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);        if (server_fifo_fd == -1)        {                fprintf(stderr, "Server fifo failure\n");                exit(EXIT_FAILURE);}        sleep(5);        do        {                read_res = read(server_fifo_fd, &my_data, sizeof(my_data));                if (read_res > 0)                {                        tmp_char_ptr = my_data.data;                        while (*tmp_char_ptr)                        {                                *tmp_char_ptr = toupper(*tmp_char_ptr);                                tmp_char_ptr++;                        }                        sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);                        client_fifo_fd = open(client_fifo, O_WRONLY);                        if (client_fifo_fd != -1)                        {                                write(client_fifo_fd, &my_data, sizeof(my_data));                                close(client_fifo_fd);                        }                }        } while (read_res > 0);        close(server_fifo_fd);        unlink(SERVER_FIFO_NAME);        exit(EXIT_SUCCESS);}
        客户端代码:
#include "client.h"#include <ctype.h>int main(){        int server_fifo_fd, client_fifo_fd;        struct data_to_pass_st my_data;        int times_to_send;        char client_fifo[256];        server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY);        if (server_fifo_fd == -1)        {                fprintf(stderr, "Sorry, no server\n");                exit(EXIT_FAILURE);        }        my_data.client_pid = getpid();        sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);        if (mkfifo(client_fifo, 0777) == -1)        {                fprintf(stderr, "Sorry, can't make %s\n", client_fifo);                exit(EXIT_FAILURE);        }        for (times_to_send = 0; times_to_send < 5; times_to_send++)        {                sprintf(my_data.data, "Hello from %d", my_data.client_pid);                printf("%d sent %s, ", my_data.client_pid, my_data.data);                write(server_fifo_fd, &my_data, sizeof(my_data));                client_fifo_fd = open(client_fifo, O_RDONLY);                if (client_fifo_fd != -1)                {                        if (read(client_fifo_fd, &my_data, sizeof(my_data)) > 0)                                printf("received: %s\n", my_data.data);                        close(client_fifo_fd);                }        }        close(server_fifo_fd);        unlink(client_fifo);        exit(EXIT_SUCCESS);}
        为了测试这个程序,需要运行一个服务器和多个客户程序,为了让多个客户程序尽可能在同一时间启动,可以使用shell命令:
$ ./server &
$ for i in 1 2 3 4 5
do
./client &
done
$
        打印输出结果:
4328 sent Hello from 4328, received: HELLO FROM 4328
4331 sent Hello from 4331, received: HELLO FROM 4331
4332 sent Hello from 4332, received: HELLO FROM 4332
4328 sent Hello from 4328, received: HELLO FROM 4328
4332 sent Hello from 4332, received: HELLO FROM 4332
4328 sent Hello from 4328, received: HELLO FROM 4328
4332 sent Hello from 4332, received: HELLO FROM 4332
4328 sent Hello from 4328, received: HELLO FROM 4328
4332 sent Hello from 4332, received: HELLO FROM 4332
4328 sent Hello from 4328, received: HELLO FROM 4328
4332 sent Hello from 4332, received: HELLO FROM 4332
4331 sent Hello from 4331, received: HELLO FROM 4331
4331 sent Hello from 4331, received: HELLO FROM 4331
4331 sent Hello from 4331, received: HELLO FROM 4331
4331 sent Hello from 4331, received: HELLO FROM 4331
4329 sent Hello from 4329, received: HELLO FROM 4329
4330 sent Hello from 4330, received: HELLO FROM 4330
4330 sent Hello from 4330, received: HELLO FROM 4330
4329 sent Hello from 4329, received: HELLO FROM 4329
4330 sent Hello from 4330, received: HELLO FROM 4330
4329 sent Hello from 4329, received: HELLO FROM 4329
4329 sent Hello from 4329, received: HELLO FROM 4329
4330 sent Hello from 4330, received: HELLO FROM 4330
4330 sent Hello from 4330, received: HELLO FROM 4330
4329 sent Hello from 4329, received: HELLO FROM 4329
        由结果可以看出,不同的客户请求交错在一起,但每个客户都获得了正确的服务器返回它的处理数据。要注意的是客户请求的交错顺序是随机的,服务器接收到客户请求的顺序随机器的不同而不同,即使是在同一台机器上,每次运行的情况也可能发生变化。
        现在,我们将解释客户和服务器在交互时各种操作的执行顺序。服务器以只读模式创建它的FIFO并阻塞,直到第一个客户以写方式打开同一个FIFO来创建连接为止。此时,服务器进程解除阻塞并执行sleep语句,这使得来自客户的数据排队等候。在实际的应用程序中,应该把sleep语句删除。我们在这里使用它只是为了演示当有多个客户的请求同时到达时,程序的正确操作方法。与此同时,在客户打开了服务器FIFO后,它创建自己唯一的一个命名管道来读取服务器返回的数据。完成这些工作后,客户发送数据给服务器(如果管道满或服务器仍在休眠中就阻塞),然后阻塞在对自己的FIFO的read调用上,等待服务器的响应。接收到来自客户的数据后,服务器处理它,然后以写方式打开客户管道并将处理后的数据返回,这将解除客户的阻塞状态。客户被解除阻塞后,它即可从自己的管道中读取服务器返回的数据。整个处理过程不断重复,直到最后一个客户关闭服务器管道为止,这将使服务器的read调用失败(返回0),因为已经没有进程以写方式打开服务器管道了。如果这是一个真正的服务器进程,它还需要继续等待客户的请求,我们就需要对它进行修改,有两种方法:
  • 对它自己的服务器管道打开一个文件描述符,这样read调用将总是阻塞而不是返回0。
  • 当read调用返回0时,关闭并重新打开服务器管道,使服务器进程阻塞在open调用处以等待客户的到来,就像它最初启动时那样。


整理自 《Linux程序设计第4版》、《Linux C编程从初学到精通》。
0 0
原创粉丝点击