系列文章:
- 文件操作
- 数据管理
- 进程和信号
- POSIX 线程
- 进程间通信:管道
- 信号量共享内存和消息队列
- 套接字
文章目录
- 1. 什么是管道
- 2. 进程管道
- 2.1 popen 函数
- 2.2 pclose 函数
- 3. 将输出送往 popen
- 3.1 传递更多的数据
- 3.2 如何实现 popen
- 4. pipe 调用
- 5. 父进程和子进程
- 5.1 管道关闭后的读操作
- 5.2 把管道用作标准输入和标准输出
- 6. 命名管道:FIFO
- 6.1 访问 FIFO 文件
- 6.2 使用 FIFO 的客户/服务器应用程序
之前我们可以使用信号在两个进程之间发送消息,但传送的信息只限于一个信号值。在本章,我们将介绍管道,通过它在进程之间可以交换更有用的数据。
- 管道的定义
- 进程管道
- 管道调用
- 父进程和子进程
- 命名管道:FIFO
- 客户/服务器架构
1. 什么是管道
当从一个进程连接数据流到另一个进程时,我们使用术语管道(pipe),通常是把一个进程的输出通过管道连接到另一个进程的输入。对于 shell 命令来说,命令的连接是通过管道字符来完成的:cmd1 | cmd2
,cmd1 的标准输入来自终端键盘、cmd1 的标准输出传递给 cmd2,作为它的标准输入、cmd2 的标准输出连接到终端屏幕。
shell 所做的工作实际上是对标准输入和标准输出流进行了重新连接,使数据流从键盘输入通过两个命令最终输出到屏幕上。
2. 进程管道
可能最简单的在两个程序之间传递数据的方法就是使用 popen 和 pclose 函数了,原型如下:
FILE* popen(const char* command, const char* open_mode);
int pclose(FILE* stream_to_close);
2.1 popen 函数
popen 函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。command 字符串是要运行的程序名和相应的参数。open_mode 必须是 r 或者 w。
如果 open_mode 是 r,被调用程序的输出就可以被调用程序使用,调用程序利用 popen 函数返回的 FILE* 文件流指针,就可以通过常用的 stdio 库函数来读取被调用程序的输出。如果 open_mode 是 w,调用程序就可以用 fwrite 调用向被调用程序发送数据,而被调用程序可以在自己的标准输入流上读取这些数据,被调用的程序通常不会意识到自己正在从另一个进程读取数据,它只是在标准输入流上读取数据,然后做出相应的操作。
popen 函数在失败时返回一个空指针,如果想通过管道实现双向通信,最普通的解决方法是使用两个管道,每个管道负责一个方向的数据流。
2.2 pclose 函数
用popen启动的进程结束时,我们可以用pclose函数关闭与之关联的文件流。pclose调用只在popen启动的进程结束后才返回。如果调用 pclose 时它仍在运行,pclose 调用将等待该进程的结束。
pclose 调用的返回值通常是它所关闭的文件流所在进程的退出码。如果调用进程在调用 pclose 之前执行了一个 wait 语句,被调用进程的退出状态就会丢失,因为被调用进程已结束。此时,pclose 将返回 -1 并设置 errno 为 ECHILD。
读取外部程序的输出
int main()
{
FILE* read_fp;
char buffer[BUFSIZ + 1];
int chars_read;
memset(buffer, '\0', sizeof(buffer));
read_fp = popen("uname -a", "r");
if(read_fp != NULL)
{
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
if(chars_read > 0)
{
printf("Output was: -\n%s\n", buffer);
}
pclose(read_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
这个程序用 popen 调用启动带有 -a 选项的 uname 命令。然后用返回的文件流读取最多 BUFSIZ 个字符的数据,并将它们打印出来显示在屏幕上。
3. 将输出送往 popen
将输出送往外部程序
int main()
{
FILE* write_fp;
char buffer[BUFSIZ + 1];
sprintf(buffer, "Once upon a time, there was ...\n");
write_fp = popen("od -c", "w");
if(write_fp != NULL)
{
fwrite(buffer, sizeof(char), strlen(buffer), write_fp);
pclose(write_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
程序使用带有参数 w
的 popen 启动 od -c
命令,这样就可以向该命令发送数据了,使用下面的命令同样可以达到效果:echo "Once upon a time, there was ... | od -c"
。
3.1 传递更多的数据
有时,我们可能希望能以块方式发送数据,或者我们不知道输出数据的长度,为了避免定义一个非常大的缓冲区,我们用多个 fread 或 fwrite 调用来将数据分为几个部分处理。
从被调用的进程 ps -ax
中读取数据,该进程输出的数据有多少事先无法直到,所以我们必须对管道进行多次读取。
int main()
{
FILE* read_fp;
char buffer[BUFSIZ + 1];
int chars_read;
memset(buffer, '\0', sizeof(buffer));
read_fp = popen("ps ax", "r");
if(read_fp != NULL)
{
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
while(chars_read > 0)
{
buffer[chars_read - 1] = '\0';
printf("Reading %d:-\n%s\n", BUFSIZ, buffer);
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
}
pclose(read_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
它连续从文件流中读取数据,直到没有数据可读为止,虽然 ps 命令的执行要花费一些时间,但 Linux 会安排好进程间的调度,让两个程序可以在可以运行时继续运行。如果读进程没有数据可读,它将会被挂起直到读进程读取了一些数据。
3.2 如何实现 popen
请求 popen 调用运行一个程序时,首先启动 shell,将 command 字符串作为一个参数传递给它。
好处是,可以借助 shell 完成参数扩展,即使用 popen 启动非常复杂的 shell 命令,使用 execl 创建进程就十分复杂,调用进程必须自己完成 shell 扩展。
坏处是,针对每个 popen 调用,不仅需要启动一个被请求的程序,还要启动一个 shell,即每个 popen 调用将多启动两个进程,popen 的调用成本较高,而且对目标命令的调用比正常方式慢一些。
对所有.c 源文件的总行数进行统计
int main()
{
FILE *read_fp;
char buffer[BUFSIZ + 1];
int chars_read;
memset(buffer, '\0', sizeof(buffer));
read_fp = popen("cat *.c | wc -l", "r"); // 在 popen 调用中启动了 shell、cat、wc 程序,并进行了一次输出重定向
if(read_fp != NULL)
{
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
while(chars_read > 0)
{
buffer[chars_read - 1] = '\0';
printf("Reading:-\n %s\n", buffer);
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
}
pclose(read_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
jiaming@jiaming-pc:~/Documents/test$ ./a.out
Reading:-
262
4. pipe 调用
通过这个函数在两个程序之间传递数据,不需要启动一个 shell 来解释请求的命令,同时提供了对读写数据的更多的控制。
int pipe(int file_descriptor[2]);
pipe 函数的参数是一个由两个整数类型的文件描述符组成的数组的指针,该函数在数组中填上两个新的文件描述符后返回 0。
两个返回的文件描述符以一种特殊的方式连接起来,写到 file_descriptor[1]
的所有数据都可以从 file_descriptor[0]
读回来,数据基于 FIFO 进行处理。
这里使用的是文件描述符而非文件流,必须使用底层的 read 和 write 来访问数据,而非文件流库函数 fread 和 fwrite。
使用 pipe函数来创建一个管道
int main()
{
int data_processed;
int file_pipe[2];
const char some_data[] = "123";
char buffer[BUFSIZ + 1];
memset(buffer, '\0', sizeof(buffer));
if(pipe(file_pipe) == 0)
{
data_processed = write(file_pipe[1], some_data, strlen(some_data));
printf("Wrote %d bytes\n", data_processed);
data_processed = read(file_pipe[0], buffer, BUFSIZ);
printf("Read %d bytes: %s\n", data_processed, buffer);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
jiaming@jiaming-pc:~/Documents/test$ ./a.out
Wrote 3 bytes
Read 3 bytes: 123
这个程序用数组 file_pipes[] 中的两个文件描述符创建一个管道,然后它用文件描述符 file_pipes[1] 向管道中写数据,再从 file_pipes[0] 读回数据。管道有一些内置的缓冲区,在 write 和 read 调用之间保存数据。
管道的真正优势体现在,当你想在两个进程之间传递数据的时候,当用 fork 创建新进程时,原先打开的文件描述符仍将保持打开状态,如果在原先的进程中创建一个管道,然后再调用 fork 创建新进程,即可通过管道在两个进程之间传递数据。
跨越 fork 调用管道
int main()
{
int data_processed;
int file_pipe[2];
const char some_data[] = "123";
char buffer[BUFSIZ + 1];
pid_t fork_result;
memset(buffer, '\0', sizeof(buffer));
if(pipe(file_pipe) == 0)
{
fork_result = fork();
if(fork_result == -1)
{
fprintf(stderr, "Fork failure");
exit(EXIT_FAILURE);
}
if(fork_result == 0)
{
// 子
data_processed = read(file_pipe[0], buffer, BUFSIZ);
printf("Read %d bytes: %s\n", data_processed, buffer);
exit(EXIT_SUCCESS);
}
else
{
// 父
data_processed = write(file_pipe[1], some_data, strlen(some_data));
printf("Wrote %d bytes\n", data_processed);
}
}
exit(EXIT_FAILURE);
}
这个程序首先用 pipe 创建了一个管道,接着用 fork 调用创建了一个新进程,如果 fork 调用成功,父进程就写数据到管道中,而子进程从管道中读取数据。父子进程都在只调用了一次write或read之后退出,如果父进程在子进程之前退出,就会在两部分输出中间看到 shell 提示符。
5. 父进程和子进程
在子进程中运行一个和父进程完全不同的另一个程序,使用 exec 调用来完成这一工作,通过 exec 调用的进程需要知道应该访问哪个文件描述符,如果没有 exec,子进程本身有 file_pipes 的数据副本,但是 exec 后,原先的进程被子进程替换了,为了解决这个问题,可以将文件描述符作为一个参数传递给用 exec 启动的程序。
int main()
{
int data_processed;
int file_pipe[2];
const char some_data[] = "123";
char buffer[BUFSIZ + 1];
pid_t fork_result;
memset(buffer, '\0', sizeof(buffer));
if(pipe(file_pipe) == 0)
{
fork_result = fork();
if(fork_result == -1)
{
fprintf(stderr, "Fork failure");
exit(EXIT_FAILURE);
}
if(fork_result == 0)
{
// 子
sprintf(buffer, "%d", file_pipe[0]); // 将读取管道数据的文件描述符保存到一个缓存区中,该缓存中的内容将构成 pipe4 程序的一个参数。
printf("child process...\n");
(void)execl("pipe4", "pipe4", buffer, (char*)0);
exit(EXIT_FAILURE);
}
else
{
// 父
printf("parent process...\n");
data_processed = write(file_pipe[1], some_data, strlen(some_data));
printf("%d - Wrote %d bytes\n", getpid(), data_processed);
}
}
exit(EXIT_SUCCESS);
}
pipe4.c
int main(int argc, char *argv[])
{
printf("child process operation...\n");
int data_processed;
char buffer[BUFSIZ + 1];
int file_descriptor;
memset(buffer, '\0', sizeof(buffer));
sscanf(argv[1], "%d", &file_descriptor);
data_processed = read(file_descriptor, buffer, BUFSIZ);
printf("%d - read %d bytes: %s\n", getpid(), data_processed, buffer);
exit(EXIT_SUCCESS);
}
jiaming@jiaming-pc:~/Documents/test$ ./a.out
parent process...
52280 - Wrote 3 bytes
child process...
child process operation...
52281 - read 3 bytes: 123
5.1 管道关闭后的读操作
只有把父子进程中的针对管道的写文件描述符都关闭,管道才会被认为是关闭的,对管道的 read 调用也才会失败。
5.2 把管道用作标准输入和标准输出
将管道文件的其中一个文件描述符设置为一个已知值,一般是标准输入 0 或者标准输出 1,这样做的最大好处是可以调用标准程序(不需要以文件描述符为参数的程序)。
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);
dup 调用的目的是打开一个新的文件描述符,这与 open 调用有点类似,不同的是,dup 调用创建的新文件描述符与作为它的参数的那个已有文件描述指向同一个文件(管道),对于 dup 函数来说,新的文件描述符总是取最小的可用值(如果我们先关闭了文件描述符 0(标准输入),然后调用 dup,那么新的文件描述将是 0(取可用的最小值),新的文件描述符和作为dup参数的文件描述均指向同一个管道,此时管道有五个端口,将管道的一头变为了标准输入,无须指定文件描述符)。
int main()
{
int data_processed;
int file_pipe[2];
const char some_data[] = "123";
char buffer[BUFSIZ + 1];
pid_t fork_result;
memset(buffer, '\0', sizeof(buffer));
if(pipe(file_pipe) == 0)
{
fork_result = fork();
if(fork_result == -1)
{
fprintf(stderr, "Fork failure");
exit(EXIT_FAILURE);
}
if(fork_result == 0)
{
// 子
close(0);
dup(file_pipe[0]);
close(file_pipe[0]);
close(file_pipe[1]);
execlp("od", "od", "-c", (char*)0);
}
else
{
// 父
close(file_pipe[0]);
data_processed = write(file_pipe[1], some_data, strlen(some_data));
close(file_pipe[1]);
printf("%d - Wrote %d bytes\n", getpid(), data_processed);
}
}
exit(EXIT_SUCCESS);
}
jiaming@jiaming-pc:~/Documents/test$ ./a.out
53315 - Wrote 3 bytes
jiaming@jiaming-pc:~/Documents/test$ 0000000 1 2 3
0000003
子进程先用 close(0)
关闭标准输入,然后调用dup(file_pipe[0])
把与管道的读取端关联的文件描述符复制为文件描述符 0,即标准输入,子进程关闭从管道读写数据的文件描述符,接下来,子进程就可以用 exec 来启动任何从标准输入读取数据的程序了。
6. 命名管道:FIFO
至此,我们能够在相关程序之间传递数据,即这些程序是由一个共同的祖先进程启动的,如果想在不相关的进程之间交换数据该怎么做?
我们可以使用 FIFO 文件来完成这项工作,它通常也被称为命名管道,命名管道是一种特殊类型的文件§,它在文件系统中以文件名的形式存在,但它的行为却和没有名字的管道类似。
我们可以在命令行上创建管道,也可以在程序中创建。
mkfifo filename
或者,
mknod filename p
int mkfifo(const char *filename, mode_t mode);
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0);
创建命名管道
int main()
{
int res = mkfifo("/tmp/my_fifo", 0777);
if(res == 0) printf("FIFO created\n");
exit(EXIT_SUCCESS);
}
查看创建的管道:
jiaming@jiaming-pc:~/Documents/test$ ls -lF /tmp/my_fifo
prwxrwxr-x 1 jiaming jiaming 0 5月 24 20:06 /tmp/my_fifo|
输出结果中的第一个字符为 p,表示这是一个管道,最后的 | 符号是由 ls 命令的 -F 选项添加的,它也表示这是一个管道。
这个程序用 mkfifo
创建了一个特殊的文件,虽然要求的文件模式是 0777,但是会被用户掩码(umask)设置改变。
6.1 访问 FIFO 文件
命名管道的一个非常有用的特点是:由于它们出现在文件系统中,所以可以像平常的文件名一样在命令中使用,把创建的FIFO文件用在程序设计之前,先通过普通的文件命令来观察 FIFO 的行为。
第一个控制台中命令被挂起,等待数据。
因为 fifo 中没有数据,所以 cat 和 echo 程序都阻塞了,cat 等待数据的到来,而 echo 等待其它进程读取数据,当 echo 向它提供了一些数据后,cat 命令读取这些数据并把它们打印到标准输出上,然后 cat 程序退出,不再等待更多的数据。当第二个命令将数据放入 FIFO 后,管道被关闭,所以 cat 程序中的 read 调用返回 0 字节,表示已经到达文件尾。
与通过 pipe 调用创建管道不同,FIFO 是以命名文件的形式存在,而不是打开的文件描述符,所以在对它进行读写操作之前必须先打开它,FIFO 也用 open 和 close 函数打开和关闭,这与前面看到的对文件的操作一样,但它多了一些其它的功能,对 FIFO 来说,传递给 open 调用的是 FIFO 的路径名,而不是一个正常的文件。
1. 使用 open 打开 FIFO 文件
打开 FIFO 的一个主要限制是,程序不能以 O_RDWR 模式打开 FIFO 文件进行读写操作,这样做的后果并未明确定义,这个限制是有意义的,因为我们通常使用 FIFO 只是为了单向传递数据,所以没有必要使用 O_RDWR 模式,如果一个管道以读/写方式打开,进程就会从这个管道读回它自己的输出。
如果确实需要在程序之间双向传递数据,最好使用一对FIFO,一个方向使用一个。
打开 FIFO 文件和打开普通文件的另一点区别是,对 open_flag 的 O_NONBLOCK 选项的用法,使用这个选项不仅改变 open 调用的处理方式,还会改变对这次 open 调用返回的文件描述符进行的读写请求的处理方式。
O_RDONLY、O_WRONLY、O_NONBLOCK的四种组合方式:
a. open(const char *path, O_RDONLY)
:open 调用将被阻塞,除非有一个进程以写方式打开同一个 FIFO,否则它不会返回。
b. open(const char *path, O_RDONLY | O_NONBLOCK)
:即使没有其它进程以写方式打开FIFO,这个open调用也将成功并立刻返回。
c. open(const char *path, O_WRONLY)
:open 调用将阻塞,直到有一个进程以读方式打开同一个 FIFO 为止。
d. open(const char *path, O_WRONLY | O_NONBLOCK)
:这个函数调用总是立刻返回,但如果没有进程以读方式打开 FIFO 文件,open 调用将返回一个错误 -1,并且 FIFO 也不会被打开,如果确实有一个进程以读方式打开 FIFO 文件,那么就可以进行写操作。
打开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) (void)close(res);
printf("Process %d finished\n", getpid());
exit(EXIT_SUCCESS);
}
2. 不带有 O_NONBLOCK 标志的 O_RDONLY 和 O_WRONLY
先启动读进程,并在 open 调用中等待,当第二个程序打开 FIFO 文件时,两个程序继续运行,读进程和写进程在 open 调用处取得同步。
当一个进程被阻塞时,它并不消耗 CPU 资源,所以这种进程的同步方式对于 CPU 来说是非常有效率的。
jiaming@jiaming-pc:~/Documents/test$ ./a.out O_RDONLY &
[1] 113943
jiaming@jiaming-pc:~/Documents/test$ Process 113943 opening FIFO
jiaming@jiaming-pc:~/Documents/test$ ./a.out O_WRONLY
Process 114088 opening FIFO
Process 114088 result 3
Process 113943 result 3
Process 114088 finished
Process 113943 finished
[1]+ Done ./a.out O_RDONLY
3. 带有 O_NONBLOCK 标志的 O_RDONLY 和不带有 O_NONBLOCK 标志的 O_WRONLY
读进程执行open调用并立刻继续执行,即使没有写进程的存在,随后写进程开始执行,它也在执行 open 调用之后立刻继续执行,但这次是因为读进程打开了 fifo。
jiaming@jiaming-pc:~/Documents/test$ ./a.out O_RDONLY O_NONBLOCK &
[1] 114443
jiaming@jiaming-pc:~/Documents/test$ Process 114443 opening FIFO
Process 114443 result 3
jiaming@jiaming-pc:~/Documents/test$ ./a.out O_WRONLY
Process 114469 opening FIFO
Process 114469 result 3
Process 114443 finished
Process 114469 finished
[1]+ Done ./a.out O_RDONLY O_NONBLOCK
4. 对 FIFO 进行读写操作
使用 O_NONBLOCK 模式会影响到对 FIFO 的read和write调用。
对一个空的、阻塞的 FIFO(即没有用 O_NONBLOCK标志打开)的read调用a将等待,直到有数据可以读时才继续执行,与此相反,对于一个空的、非阻塞的 FIFO 的read调用将立刻返回 0 字节。
对一个完全阻塞 FIFO 的 write 调用将等待,直到数据可以被写入时才继续z还行,如果 FIFO 不能接收所有写入的数据,它将按下面的规则执行。
- 如果请求写入的数据的长度小于等于 PIPE_BUF 字节,调用失败,数据不能写入。
- 如果请求写入的数据的长度大于 PIPE_BUF 字节,将写入部分数据,返回实际写入的字节数,返回值也可能是 0。
FIFO 的长度由 #define PIPE_BUF
语句定义,通常可以在头文件 limits.h 中找到它,在 Linux 和许多其它 UNIX 系统中,它的值通常是 4096 字节,但在某些系统中,可能为 512,系统规定,在一个以 O_WRONLY 方式打开的 FIFO 中,如果写入的数据长度小于等于 PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。
如果保证多个不同的程序向一个 FIFO 读进程发送请求,原子化操作?
如果能够保证所有的写请求都是发往一个阻塞的 FIFO 的,并且数据长度小于等于 PIPE_BUF 字节,系统就可以确保数据绝不会交错在一起。
使用FIFO实现进程间通信
生产者:
int main(int argc, char *argv[])
{
int pipe_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());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d result %d\n", getpid(), pipe_fd);
if(pipe_fd != -1)
{
while(bytes_sent < TEN_MEG)
{
res = write(pipe_fd, buffer, BUFFER_SIZE);
if(res == -1)
{
fprintf(stderr, "Write error on pipe\n");
exit(EXIT_FAILURE);
}
bytes_sent += res;
}
(void)close(pipe_fd);
}
else
{
exit(EXIT_FAILURE);
}
printf("Process %d finished\n", getpid());
exit(EXIT_SUCCESS);
}
消费者:
int main(int argc, char *argv[])
{
int pipe_fd;
int res;
int open_mode = O_RDONLY;
int bytes_read = 0;
char buffer[BUFFER_SIZE + 1];
memset(buffer, '\0', sizeof(buffer));
printf("Process %d opening FIFO O_RDONLY\n", getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d result %d\n", getpid(), pipe_fd);
if(pipe_fd != -1)
{
do{
res = read(pipe_fd, buffer, BUFFER_SIZE);
bytes_read += res;
}while (res > 0);
(void)close(pipe_fd);
}
else
{
exit(EXIT_FAILURE);
}
printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
exit(EXIT_SUCCESS);
}
jiaming@jiaming-pc:~/Documents/test$ gcc fifo3.c -o fifo3
jiaming@jiaming-pc:~/Documents/test$ gcc fifo4.c -o fifo4
jiaming@jiaming-pc:~/Documents/test$ ./fifo3 &
[1] 115377
jiaming@jiaming-pc:~/Documents/test$ Process 115377 opening FIFO O_WRONLY
jiaming@jiaming-pc:~/Documents/test$ time ./fifo4
Process 115411 opening FIFO O_RDONLY
Process 115411 result 3
Process 115377 result 3
Process 115377 finished
Process 115411 finished, 10485760 bytes read
[1]+ Done ./fifo3
real 0m0.075s
user 0m0.020s
sys 0m0.052s
linux 会安排好这两个进程之间的调度,使它们在可以运行的时候运行,不能运行的时候阻塞,因此,写进程将在管道满时阻塞,读进程将在管道空时阻塞。
time 命令显示,读进程只花了不到0.1s的时间,便读取了 10 MB 的数据。
6.2 使用 FIFO 的客户/服务器应用程序
用一个服务器进程来接受请求,对它进行处理,最后把结果数据返回给发送请求的一方:客户。
假设只使用一个 FIFO,服务器通过它读取数据,每个客户都向它写数据,只要将 FIFO 以阻塞模式打开,服务器和客户就会根据需要自动被阻塞。
为每个客户安排第二个管道来接收返回的数据。通过在传递给服务器的原先数据中加上客户的进程标识符,双方就可以使用它来为返回数据的管道生成一个唯一的名字。
一个客户/服务器应用程序的例子
client.h
struct data_to_pass_st
{
pid_t client_pid;
char some_data[BUFFER_SIZE - 1];
};
server.c
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(10); // lets clients queue for demo purposes
do{
read_res = read(server_fifo_fd, &my_data, sizeof(my_data));
if(read_res > 0)
{
tmp_char_ptr = my_data.some_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);
}
client.c
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.some_data, "Hello from %d", my_data.client_pid);
printf("%d sent %s, ", my_data.client_pid, my_data.some_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.some_data);
}
close(client_fifo_fd);
}
}
close(server_fifo_fd);
unlink(client_fifo);
exit(EXIT_SUCCESS);
}
jiaming@jiaming-pc:~/Documents/test$ gcc server.c -o server
jiaming@jiaming-pc:~/Documents/test$ gcc client.c -o client
jiaming@jiaming-pc:~/Documents/test$ ./server &
[1] 116509
jiaming@jiaming-pc:~/Documents/test$ for i in 1 2 3 4 5
> do
> ./client &
> done
[2] 116546
[3] 116547
[4] 116548
[5] 116549
[6] 116550
jiaming@jiaming-pc:~/Documents/test$ 116548 sent Hello from 116548, received: HELLO FROM 116548
116546 sent Hello from 116546, received: HELLO FROM 116546
116547 sent Hello from 116547, received: HELLO FROM 116547
116548 sent Hello from 116548, received: HELLO FROM 116548
116546 sent Hello from 116546, received: HELLO FROM 116546
116547 sent Hello from 116547, received: HELLO FROM 116547
116549 sent Hello from 116549, received: HELLO FROM 116549
116550 sent Hello from 116550, received: HELLO FROM 116550
116547 sent Hello from 116547, received: HELLO FROM 116547
116547 sent Hello from 116547, received: HELLO FROM 116547
116547 sent Hello from 116547, received: HELLO FROM 116547
116549 sent Hello from 116549, received: HELLO FROM 116549
116549 sent Hello from 116549, received: HELLO FROM 116549
116549 sent Hello from 116549, received: HELLO FROM 116549
116549 sent Hello from 116549, received: HELLO FROM 116549
116548 sent Hello from 116548, received: HELLO FROM 116548
116550 sent Hello from 116550, received: HELLO FROM 116550
116546 sent Hello from 116546, received: HELLO FROM 116546
116550 sent Hello from 116550, received: HELLO FROM 116550
116548 sent Hello from 116548, received: HELLO FROM 116548
116546 sent Hello from 116546, received: HELLO FROM 116546
116548 sent Hello from 116548, received: HELLO FROM 116548
116550 sent Hello from 116550, received: HELLO FROM 116550
116546 sent Hello from 116546, received: HELLO FROM 116546
116550 sent Hello from 116550, received: HELLO FROM 116550
[1] Done ./server
[2] Done ./client
[3] Done ./client
[4] Done ./client
[5]- Done ./client
[6]+ Done ./client