Linux编程之pipe与fifo

本文详细介绍了Linux编程中的管道(pipe)和命名管道(fifo)的使用,包括创建、读写操作及阻塞模式。同时,讲解了popen函数如何在进程间建立管道,实现命令执行的输出重定向。通过实例代码演示了各种用法。

pipe的使用

管道(pipe)是POSIX标准中一种可用于进程间通信的单向数据通道。一个管道有一个写端和一个读端,数据从写端写入,从读端读取,因此读端和写端分别有一个文件描述符。

管道的读写模式可以是阻塞模式,也可以是非阻塞模式(O_NONBLOCK)。对于阻塞模式,如果管道中没有数据,那么读操作将阻塞,直到写入足够数据;如果管道已满,那么写操作将阻塞,直到读出数据留出足够空间。

管道有一个最大容量,可用pathconf(_PC_PIPE_BUF)获取。当写入的数据小于最大容量时,写入操作是原子的,内核会把数据全部写入管道;而当写入的数据大于最大容量时,则写操作不再是原子的,多个进程写的数据可能交织在一起。

用于创建管道的函数如下。

       #include <unistd.h>

       int pipe(int pipefd[2]);

该函数所带的参数实际上是输出参数。如果该函数成功,则返回两个文件描述符,pipefd[0]代表管道读端的文件描述符,pipefd[1]则代表管道写端的文件描述符。如果要修改管道的阻塞模式,可以用fcntl函数来实现。Linux还提供一个特有的函数pipe2,允许在创建管道时直接指定阻塞模式。该函数如下:

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <fcntl.h>              /* Obtain O_* constant definitions */
       #include <unistd.h>

       int pipe2(int pipefd[2], int flags);

pipe函数创建的管道是匿名管道,因此只能在共享文件描述符的父子进程之间使用。Linux的man手册给出了一个例子:

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

       int
       main(int argc, char *argv[])
       {
           int pipefd[2];
           pid_t cpid;
           char buf;

           if (argc != 2) {
               fprintf(stderr, "Usage: %s <string>\n", argv[0]);
               exit(EXIT_FAILURE);
           }

           if (pipe(pipefd) == -1) {
               perror("pipe");
               exit(EXIT_FAILURE);
           }

           cpid = fork();
           if (cpid == -1) {
               perror("fork");
               exit(EXIT_FAILURE);
           }

           if (cpid == 0) {    /* Child reads from pipe */
               close(pipefd[1]);          /* Close unused write end */

               while (read(pipefd[0], &buf, 1) > 0)
                   write(STDOUT_FILENO, &buf, 1);

               write(STDOUT_FILENO, "\n", 1);
               close(pipefd[0]);
               _exit(EXIT_SUCCESS);

           } else {            /* Parent writes argv[1] to pipe */
               close(pipefd[0]);          /* Close unused read end */
               write(pipefd[1], argv[1], strlen(argv[1]));
               close(pipefd[1]);          /* Reader will see EOF */
               wait(NULL);                /* Wait for child */
               exit(EXIT_SUCCESS);
           }
       }

编译并运行该程序可得到如期结果。

gcc -o test_pipe test_pipe.c
./test_pipe "Test pipe message"
Test pipe message

fifo的使用

FIFO则是一种命名的管道,但其编程使用方式与管道有所不同。首先应调用mkfifo函数来创建一个FIFO,该函数会在文件系统中创建一个FIFO特殊文件。然后进程可以分别调用open来打开这个FIFO特殊文件进行读和写。mkfifo函数原型如下。

       #include <sys/types.h>
       #include <sys/stat.h>

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

该函数有两个参数,第一个参数是一个文件路径,代表要创建FIFO特殊文件;第二个参数是访问模式。如果创建成功,返回值为0。

创建好FIFO特殊文件后,任何进程都可以打开FIFO进行读写操作。但是跟普通文件不同的是,阻塞模式下open操作成对进行才能继续进行读写操作。比如一个进程如果要打开FIFO进行读,那么这个open操作会阻塞,直到有某个进程打开它进行写操作;反之亦然。如果要使用非阻塞模式,那么在open是应在flags参数中指定O_NONBLOCK。

把前面pipe的例子稍微改一下,就可以得到fifo的示例。

       #include <sys/types.h>
       #include <sys/wait.h>
       #include <sys/stat.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>
       #include <string.h>
       #include <fcntl.h>

       int
       main(int argc, char *argv[])
       {
	       int fd;
           pid_t cpid;
           char buf;

           if (argc != 2) {
               fprintf(stderr, "Usage: %s <string>\n", argv[0]);
               exit(EXIT_FAILURE);
           }

	       if (mkfifo("/tmp/test_fifo", 0666) == -1) {
               perror("mkfifo");
               exit(EXIT_FAILURE);
           }

           cpid = fork();
           if (cpid == -1) {
               perror("fork");
               exit(EXIT_FAILURE);
           }

           if (cpid == 0) {    /* Child reads from fifo */
	           fd = open("/tmp/test_fifo", O_RDONLY);

               while (read(fd, &buf, 1) > 0)
                   write(STDOUT_FILENO, &buf, 1);

               write(STDOUT_FILENO, "\n", 1);
               close(fd);
               _exit(EXIT_SUCCESS);

           } else {            /* Parent writes argv[1] to fifo */
	       fd = open("/tmp/test_fifo", O_WRONLY);
               write(fd, argv[1], strlen(argv[1]));
               close(fd);          /* Reader will see EOF */
               wait(NULL);                /* Wait for child */
	           unlink("/tmp/test_fifo");
               exit(EXIT_SUCCESS);
           }
       }

编译并运行,可得到如期结果。

gcc -o test_fifo test_fifo.c
./test_fifo "Test fifo message"
Test fifo message

popen的使用

POSIX还提供了另外一个API以便更方便地使用管道,即popen函数。该函数原型如下。

       #include <stdio.h>

       FILE *popen(const char *command, const char *type);

       int pclose(FILE *stream);

这个函数的作用是在调用popen的进程和执行command的进程之间建立一个管道。本质上,它组合pipe函数和fork函数的功能。第一个参数command指定要执行的命令,第二个参数则指定管道的类型(方向),可以为可读"r"或者可写"w",但不能为同时读写,因为管道是单向的。

对于可读"r"管道,管道的读端在调用进程,而管道的写端则在command进程的标准输出。而command进程的标准输入则继承自父进程也就是调用进程的标准输入,除非command进程自己做了重定向。默认情况下,管道是阻塞模式。也就是说command进程端写满时会等待调用进程读走才能继续;而调用进程读空时也会等待command进程写入才能继续。因此,这种管道的作用就是把command进程的标准输出重定向了。

对于可写"w"管道,管道的写端在调用进程,而管道的读端则在command进程的标准输入。而command进程的标准输出则继承自父进程也就是调用进程的标准输出,除非command进程自己做了重定向。同样,此管道默认也是阻塞模式,类似与"r"管道。因此,这种管道的作用就是把command进程的标准输入重定向了。

pclose函数用于关闭管道,它会等待关联的子进程退出后才返回。

下面这个例子演示了可读"r"管道的用法。从编译运行结果可以看到,子进程的printf输出都到了管道里,而父进程可从管道读取。

/*
 * test_popen.c: demostrates popen
 */
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

static void child_proc(void)
{
	printf("Hello from child\n");
	exit(EXIT_SUCCESS);
}

int main(int argc, char *argv[])
{
	FILE *pipefp = NULL;
	char *self_exe;
	char child_cmd[1024];
	char buf;

	if (argc == 1) {
		self_exe = realpath(argv[0], NULL);
		snprintf(child_cmd, sizeof(child_cmd), "%s child", self_exe);
		free(self_exe);
		pipefp = popen(child_cmd, "r");
		if (!pipefp) {
			perror("popen");
			return -1;
		}
		while (fread(&buf, 1, 1, pipefp)) {
			printf("From pipe: %c\n", buf);
		}
		pclose(pipefp);
	} else {
		child_proc();
	}
	return 0;
}
gcc -o test_popen test_popen.c
./test_popen
From pipe: H
From pipe: e
From pipe: l
From pipe: l
From pipe: o
From pipe:
From pipe: f
From pipe: r
From pipe: o
From pipe: m
From pipe:
From pipe: c
From pipe: h
From pipe: i
From pipe: l
From pipe: d
From pipe:

 

<think>我们正在讨论计算机中的管道(pipe)和命名管道(FIFO)。根据用户要求,我们需要详细介绍这两者,并参考之前的对话历史(尽管之前的对话主要关于进程和应用的关系,但pipeFIFO是进程间通信的重要机制,进程相关)。注意,用户要求参考站内引用,但之前的引用[1]和[2]是关于进程和容器的,所以我们在适当的地方可以关联,但主要关注pipeFIFO本身。 我们将按照以下结构回答: 1. 管道Pipe)的定义和特点 2. 命名管道FIFO)的定义和特点 3. PipeFIFO的区别 4. 使用示例(命令行和编程示例) 5. 引用关联(如果可能,关联到之前对话中提到的进程通信) 注意:根据指令,数学公式用$...$或$$...$$,但本主题可能涉及较少数学,但如果有,我们会正确使用。另外,生成相关问题。 开始回答: </think> ### 管道Pipe命名管道FIFO)详解 在操作系统中,**管道Pipe)** 和 **命名管道FIFO)** 是进程间通信(IPC)的核心机制,用于在进程间传递数据。以下是两者的系统级解析: --- #### 1. **管道Pipe)** 管道是一种**单向、匿名**的通信通道,通常用于**父子进程或有亲缘关系的进程**间通信。 - **工作方式**: - 通过系统调用 `pipe(int fd[2])` 创建,返回两个文件描述符:`fd[0]`(读端)和 `fd[1]`(端)。 - 数据流动方向固定:端输入 → 读端输出($ \text{Process}_A \xrightarrow{\text{write}} \text{fd[1]} \longrightarrow \text{fd[0]} \xrightarrow{\text{read}} \text{Process}_B $)。 - **关键特性**: - **匿名性**:无文件系统路径,仅通过继承文件描述符共享。 - **容量限制**:Linux 默认缓冲区为 64KB(`PIPE_BUF`),满时阻塞,读空时阻塞。 - **生命周期**:随进程结束自动销毁。 - **典型应用**: ```bash # Shell 示例:统计当前目录文件数 ls | wc -l ``` 此处 `|` 创建匿名管道,将 `ls` 的输出传递给 `wc`。 --- #### 2. **命名管道FIFO)** 命名管道是一种**具名、双向支持**的管道,允许**任意无亲缘关系进程**通信。 - **工作方式**: - 通过 `mkfifo` 命令或系统调用创建(如 `mkfifo("/path/to/fifo", 0666)`)。 - 在文件系统中以特殊文件形式存在(如 `/path/to/fifo`)。 - **关键特性**: - **具名访问**:进程通过路径名打开 FIFO 文件(`open()` 系统调用)。 - **双向通信**:支持多进程同时读(需协调避免冲突)。 - **持久性**:文件系统驻留,进程退出后仍存在(需手动删除)。 - **原子性**:单次入 ≤ `PIPE_BUF`(通常 4KB)的数据保证原子操作[^1]。 - **典型应用**: ```bash # 终端1:创建并入数据 mkfifo my_fifo echo "Hello" > my_fifo # 终端2:读取数据(无需亲缘关系) cat < my_fifo # 输出 "Hello" ``` --- #### 3. **核心区别对比** | **特性** | **管道Pipe)** | **命名管道FIFO)** | |----------------|--------------------------|--------------------------| | **命名** | 匿名 | 具名(文件系统路径) | | **进程关系** | 需亲缘关系(如父子进程) | 任意进程 | | **生命周期** | 随进程结束销毁 | 显式删除(`unlink()`) | | **通信方向** | 单向 | 双向(需协调读) | | **创建方式** | `pipe()` 系统调用 | `mkfifo()` 系统调用 | --- #### 4. **底层原理** - **数据流模型**: - 两者均基于**内核缓冲区**实现,数据传递不经过磁盘(内存直传)。 - 操作:数据复制到内核缓冲区(`write()`)。 - 读操作:从内核缓冲区复制到用户空间(`read()`)。 - **阻塞行为**: - 读空管道阻塞,直到有数据入。 - 管道阻塞,直到有空间释放(容量由 `ulimit -p` 定义)。 - **同步机制**: - FIFO 的原子入由内核保证(`PIPE_BUF` 限制内)[^1]。 --- #### 5. **编程示例(C 语言)** ```c // 管道示例:父子进程通信 #include <unistd.h> int main() { int fd[2]; pipe(fd); // 创建管道 if (fork() == 0) { // 子进程 close(fd[0]); // 关闭读端 write(fd[1], "Hello", 6); } else { // 父进程 close(fd[1]); // 关闭端 char buf[10]; read(fd[0], buf, 10); printf("Received: %s\n", buf); } } ``` ```c // FIFO 示例:独立进程通信 #include <fcntl.h> int main() { mkfifo("/tmp/myfifo", 0666); // 创建 FIFO 文件 int fd = open("/tmp/myfifo", O_WRONLY); write(fd, "Hello", 6); close(fd); } // 另一进程可打开同一 FIFO 读取数据 ``` --- #### 6. **进程模型的关系** - 管道FIFO 是进程间通信(IPC)的关键技术,**解耦应用逻辑进程边界**(参考[进程应用关系](^2))。 - 在容器化场景中,FIFO 常用于跨容器通信(需挂载共享卷),而管道适用于同一容器内进程协作[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值