13 进程间的通信:管道

13.1 什么是管道

11章中我们用信号在两个进程之间发送消息,但用信号传送的信息仅限于一个信号值,使用管道,我们可以在进程间更有效的交换数据。

13.2 进程管道

#include <stdio.h>
FILE *popen(const char *command, const char *open_mode);
int pclose(FILE *stream_to_close);

popen

popen函数将另一个程序作为新的进程来启动,第一个参数就是要新启动的程序,第二个参数必须是"r"或"w"。

如果是r,表示调用进程从被调用进程中读,被调用进程的输出写在了open返回的FILE中,调用进程用fread可以读到数据。

如果是w,调用程序可以用fwrite向被调用程序写数据。被调用程序用标准输入流接受fwrite写的数据。

pclose

pclose函数关闭与之关联的文件流。如果调用pclose时,之前用popen打开的进程还在运行,则调用进程会等待直到被调用进程结束。

pclose的返回值通常是它关闭的文件流所在的进程的退出吗。如果pclose之前调用了wait,则会返回-1,并设置errno为ECHILD。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
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);
}
leo@ubuntu:~/c_test$ ./a.out
Output was:-
Linux ubuntu 5.0.0-37-generic #40~18.04.1-Ubuntu SMP Thu Nov 14 12:06:39 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

13.3 将输出送往popen

上一个例子是从管道中读取。我们再来看一个向管道中输出的例子

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
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);
}

输出

0000000 O n c e u p o n a t i m e
0000020 , t h e r e w a s . . . \n
0000037

多次从管道中读取数据

有时数据很多,为了避免开过大的缓冲区,我们可以多次调用fread或fwrite。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
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);
}

输出

leo@ubuntu:~/c_test$ ./a.out
Reading 8192: -
PID TTY STAT TIME COMMAND
1 ? Ss 0:53 /lib/systemd/systemd --system --deserialize 35
2 ? S 0:00 [kthreadd]
3 ? I< 0:00 [rcu_gp]
4 ? I< 0:00 [rcu_par_gp]
... 省略

popen是如何工作的

popen实际上是会启动一个shell,然后把command字符串作为参数传给shell执行。

  • 优势:可以很方便的执行复杂的shell命令

  • 劣势:要开启一个shell,效率低

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
FILE *read_fp;
char buffer[BUFSIZ + 1];
int chars_read;
memset(buffer, '\0', sizeof(buffer));
read_fp = popen("cat popen*.c | wc -l", "r"); // wc -l用来获取行数
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);
}

可以看到,popen只会输出最后管道的结果。

leo@ubuntu:~/c_test$ ./a.out
Reading: -
69

13.4 pipe调用

popen是标准库函数。pipe是POSIX函数。pipe的参数是两个文件描述符组成的数组。

pipe是系统调用函数,使用底层文件描述符。我们在读写的时候也需要使用底层的读写函数。下面的程序利用文件描述符file_pipes[1]向管道中写数据,再从file_pipes[0]中读回数据。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
int data_processed;
int file_pipes[2];
const char some_data[] = "123";
char buffer[BUFSIZ + 1];
memset(buffer, '\0', sizeof(buffer));
if (pipe(file_pipes) == 0) {
data_processed = write(file_pipes[1], some_data, strlen(some_data));
printf("Wrote %d bytes\n", data_processed);
data_processed = read(file_pipes[0], buffer, BUFSIZ);
printf("Read %d bytes: %s\n", data_processed, buffer);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}

输出

leo@ubuntu:~/c_test$ ./a.out
Wrote 3 bytes
Read 3 bytes: 123

我们改一下上面的程序,在父进程中write,在子进程中read

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
int data_processed;
int file_pipes[2];
const char some_data[] = "123";
char buffer[BUFSIZ + 1];
pid_t fork_result;
memset(buffer, '\0', sizeof(buffer));
if (pipe(file_pipes) == 0) {
fork_result = fork();
if (fork_result == -1) {
fprintf(stderr, "Fork failure");
exit(EXIT_FAILURE);
}
if (fork_result == 0) {
/* child process */
data_processed = read(file_pipes[0], buffer, BUFSIZ);
printf("Read %d bytes: %s\n", data_processed, buffer);
exit(EXIT_SUCCESS);
} else {
data_processed = write(file_pipes[1], some_data, strlen(some_data));
printf("Wrote %d bytes\n", data_processed);
}
}
exit(EXIT_SUCCESS);
}

注意这里的shell提示,父进程先结束了

leo@ubuntu:~/c_test$ ./a.out
Wrote 3 bytes
leo@ubuntu:~/c_test$ Read 3 bytes: 123

13.5 父进程和子进程

上面的父进程和子进程写在一起的,现在我们分开。注意execl的参数,第一个是path,后面分别是argv[0], argv[1]...。

/* pipe3 */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
int data_processed;
int file_pipes[2];
const char some_data[] = "123";
char buffer[BUFSIZ + 1];
pid_t fork_result;
memset(buffer, '\0', sizeof(buffer));
if (pipe(file_pipes) == 0) {
fork_result = fork();
if (fork_result == (pid_t)-1) {
fprintf(stderr, "Fork failure");
exit(EXIT_FAILURE);
}
if (fork_result == 0) {
/* 这里在子进程中用execl进行了进程替换,pipe4成了新的子进程 */
sprintf(buffer, "%d", file_pipes[0]);
(void)execl("pipe4", "pipe4", buffer, (char *)0);
exit(EXIT_FAILURE);
} else {
data_processed = write(file_pipes[1], some_data, strlen(some_data));
printf("%d - wrote %d bytes\n", getpid(), data_processed);
}
}
exit(EXIT_SUCCESS);
}
/* pipe4 */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
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);
}
leo@ubuntu:~/c_test$ ./pipe3
4412 - wrote 3 bytes
leo@ubuntu:~/c_test$ 4413 - read 3 bytes: 123

13.6 命名管道:FIFO

FIFO文件就是命名管道。下面两个命令可以创建命名管道。同时也是两个库函数的名字。

$ mknod filename p
$ mkfifo filename

第一个字符p表示是管道,最后有一个|也表示是管道。

leo@ubuntu:~/c_test$ mkfifo /tmp/my_fifo
leo@ubuntu:~/c_test$ ls -lF /tmp/my_fifo
prw-rw-r-- 1 leo leo 0 Mar 3 07:08 /tmp/my_fifo|

未完