在 Linux 和其他类 Unix 操作系统中,管道(pipe)是一种用于在进程间进行通信的机制。管道允许在命令之间传递数据,通常用于将一个命令的输出作为另一个命令的输入。管道是 Unix哲学 “一切皆文件” 的一个体现,每个管道都可以被视为一个特殊的文件。
管道的类型
-
匿名管道 (
pipe
): 只能在具有亲缘关系的进程间(例如父子进程)进行通信。它没有名字,因此只能在创建它的进程及其子进程之间使用。 -
命名管道 (
FIFO
或named pipe
): 可以在任意两个进程之间进行通信,即使它们之间没有亲缘关系。命名管道在文件系统中有一个名字,因此可以在不同的进程之间传递数据。
ls命令(其实也是一个进程)会把当前目录中的文件都列出来,但是它不会直接输出,而是把本来要输出到屏幕上的数据通过管道输出到grep这个进程中,作为grep这个进程的输入,然后这个进程对输入的信息进行筛选,把存在string的信息的字符串(以行为单位)打印在屏幕上
1.匿名管道
popen
在 C 语言中,popen
函数提供了一个高级接口来创建一个管道,并与另一个进程进行通信。它允许你启动一个命令,并能够从标准输入或标准输出读取数据。
FILE *popen(const char *command, const char *type);
command
是一个指向要执行的 shell 命令的指针。type
是一个指向字符串的指针,该字符串必须是 “r” 或 “w”,分别表示你想要从命令读取输出(读取模式)还是向命令写入输入(写入模式)。- 返回值:如果成功,
popen
返回一个FILE
指针,可以通过这个指针使用标准 I/O 函数,(如fread
,fgets
,fprintf
,fwrite
等)与管道进行通信。如果失败,返回NULL
【testcode】
在这个例子中,popen
被用来执行 ls -l
命令,并将输出重定向到 FILE 指针 fp
。然后,使用 fgets
读取命令的输出,并打印到标准输出。最后,使用 pclose
函数关闭文件指针并等待命令结束。
【注意事项】
- 使用
popen
创建的管道必须通过pclose
函数关闭,而不是fclose
。pclose
会等待命令结束,并返回命令的退出状态。 - 当使用
popen
与另一个进程通信时,必须确保该进程正确地处理了所有的输入和输出,否则可能会导致进程挂起或资源泄露。 popen
函数在执行 shell 命令时可能会受到 shell 注入攻击,因此在使用时应谨慎处理命令字符串,特别是当命令包含用户输入时。- popen的优缺点:
当请求popen()调用运行一个程序时,它首先启动shell,即系统中的sh命令,然后将command字符串作为一个参数传递给它。
这样就带来了一个优点和一个缺点。优点是:在Linux中所有的参数扩展都是由shell来完成的。所以在启动程序(command中的命令程序)之前先启动shell来分析命令字符串,也就可以使各种shell扩展(如通配符)在程序启动之前就全部完成,这样我们就可以通过popen()启动非常复杂的shell命令。
而它的缺点就是:对于每个popen()调用,不仅要启动一个被请求的程序,还要启动一个shell,即每一个popen()调用将启动两个进程,从效率和资源的角度看,popen()函数的调用比正常方式要慢一些
pclose
int pclose(FILE *stream_to_close);
入参:FILE *stream_to_close:文件流指针
pipe
在计算机科学中,pipe
是一种用于进程间通信(IPC)的机制,它允许在两个相关进程之间传递数据。以下是关于 pipe
的一些详细信息:
【定义】
pipe
创建一个单向的数据通道,通常用于父进程和子进程之间的通信。它由一个读端和一个写端组成,数据从写端流入,从读端流出。
【使用方式】
在 POSIX 兼容的操作系统中,pipe
函数的原型如下:
#include <unistd.h>
int pipe(int pipefd[2]);
pipefd
是一个包含两个整数的数组,pipe
函数会在成功时填充这个数组。pipefd[0]
是管道的读端。pipefd[1]
是管道的写端。
【工作原理】
- 当调用
pipe
函数时,内核会创建一个新的管道,并返回两个文件描述符。 - 写入端(
pipefd[1]
)用于写入数据,读取端(pipefd[0]
)用于读取数据。 - 数据写入管道后,会一直存在于管道中,直到被读取端读取
【testcode】
举个例子,父进程通过管道向子进程发送字符串 “hello pipe\n”,子进程读取这个字符串并打印到标准输出
方式1:
方式2:
【注意事项】
- 管道是半双工的,数据只能在一个方向上流动。
- 管道的大小通常是有限的,如果写入的数据超过了这个限制,写操作可能会阻塞,直到有足够的空间来接收更多的数据。
- 管道只能在具有共同祖先的进程之间使用,例如父子进程。
- 如果说popen()是一个高级的函数,pipe()则是一个底层的调用。与popen()函数不同的是,它在两个进程之间传递数据不需要启动一个shell来解释请求命令,同时它还提供对读写数据的更多的控制
-
数组中的两个文件描述符以一种特殊的方式连接起来,数据基于先进先出的原则,写到
pipefd
[1]的所有数据都可以从pipefd
[0]读回来。由于数据基于先进先出的原则,所以读取的数据和写入的数据是一致的。特别提醒:
1、从函数的原型我们可以看到,它跟popen函数的一个重大区别是,popen()函数是基于文件流(FILE)工作的,而pipe是基于文件描述符工作的,所以在使用pipe后,数据必须要用底层的read()和write()调用来读取和发送。
2、不要用
pipefd
[0]写数据,也不要用pipefd
[1]读数据,其行为未定义的,但在有些系统上可能会返回-1表示调用失败。数据只能从pipefd
[0]中读取,数据也只能写入到pipefd
[1],不能倒过来。
close
在编程和系统调用中,close
是一个用于关闭文件描述符的函数。以下是关于 close
的一些详细信息:
定义
close
函数用于关闭一个打开的文件描述符。文件描述符是一个非负整数,由操作系统用于标识打开的文件或资源。
使用方式
在 POSIX 兼容的操作系统中,close
函数的原型如下:
#include <unistd.h>
int close(int fd);
fd
是要关闭的文件描述符。
功能
- 释放与文件描述符关联的资源。
- 如果文件描述符指向的是一个文件,
close
将确保所有缓冲的数据被写入磁盘。 - 如果文件描述符指向的是套接字、管道或终端,
close
将断开连接或释放相关资源。
返回值
- 成功时,返回
0
。 - 失败时,返回
-1
并设置errno
来指示错误。
注意事项
- 在程序结束前,应该关闭所有打开的文件描述符,以避免资源泄露。
- 如果一个进程终止,操作系统会自动关闭所有它打开的文件描述符。
- 如果文件描述符已经被关闭,再次调用
close
会导致错误。
dup
dup
是一个系统调用,用于复制一个现有的文件描述符到另一个指定的文件描述符。如果目标文件描述符已经打开,dup
会先将其关闭,然后进行复制操作。
在 Linux 中,dup
系统调用用于复制一个现有的文件描述符。当一个文件描述符被复制时,新的文件描述符与旧的文件描述符指向同一个打开的文件描述表项(file description),这意味着它们共享相同的文件偏移量、文件状态标志以及文件访问模式。
以下是 dup
系统调用的详细信息:
【系统调用原型】
在 C 语言中,dup
的原型如下:
int dup(int oldfd);
oldfd
:要复制的文件描述符。- 返回值:成功时返回新的文件描述符,它是
oldfd
的副本;出错时返回 -1 并设置 errno 来指示错误。 - 参数
oldfd
是要复制的文件描述符,而返回值是新创建的文件描述符,它是oldfd
的副本。如果调用成功,返回的文件描述符是当前最低可用数值的文件描述符。如果出错,返回 -1 并设置 errno 来指示错误。
【错误处理】
以下是一些可能发生的错误:
EBADF
:oldfd
不是一个有效的文件描述符。EMFILE
:进程已经达到了它可以打开的最大文件描述符数量。ENFILE
:系统范围内打开的文件数量超过了限制。
【testcode】
在这个例子中,我们首先打开一个文件,然后使用 dup
复制它的文件描述符。之后,我们通过两个文件描述符分别写入数据到文件中。最后,我们关闭这两个文件描述符。
请注意,dup
函数不会复制文件描述符的属性,比如非阻塞标志或者执行时关闭标志(close-on-exec)。如果需要复制这些属性,可以使用 dup2
或 dup3
系统调用,它们提供了更多的控制
【注意事项】
dup
不会复制文件描述符的close-on-exec
标志。如果需要复制这个标志,可以使用dup2
或fcntl
系统调用。- 当使用
dup
创建新的文件描述符时,系统会自动选择当前进程中最低的未使用的文件描述符编号
dup2
dup2
是一个在 POSIX 系统中定义的函数,用于复制一个现有的文件描述符到另一个指定的文件描述符。如果目标文件描述符已经打开,dup2
会先将其关闭,然后复制源文件描述符。
以下是 dup2
函数的原型:
int dup2(int oldfd, int newfd);
参数说明:
oldfd
: 要复制的现有文件描述符。newfd
: 指定的目标文件描述符。如果newfd
已经打开,它会被关闭,然后oldfd
的副本会被创建在newfd
上。
返回值:
- 成功时,返回新的文件描述符,即
newfd
。 - 失败时,返回 -1,并设置
errno
来指示错误。
下面是 dup2
函数的一些常见用法:
1.重定向标准输出到一个文件:
2.复制一个文件描述符到另一个指定的文件描述符:
3.继承文件描述符:在 fork
之后,子进程可以通过 dup2
继承父进程的文件描述符
在使用 dup2
时,通常需要注意以下几点:
使用 dup2
时,一定要确保处理了可能出现的错误情况,并且正确地关闭了不再需要的文件描述符,以避免资源泄漏。
匿名管道的一个缺点,就是通信的进程,它们的关系一定是父子进程的关系,这就使得它的使用受到了一点的限制,但是我们可以使用命名管道来解决这个问题
- 如果
oldfd
不是有效的文件描述符,dup2
会失败,并返回 -1。 - 如果
oldfd
是一个套接字,dup2
可能会复制套接字的状态,包括非阻塞标志和任何挂起的错误。 - 如果
newfd
大于oldfd
,并且系统中存在介于这两个值之间的文件描述符,那么dup2
可能会复制这些文件描述符,而不是oldfd
。这种行为在不同的系统上可能不同。为了避免这种情况,通常使用fcntl
函数的F_DUPFD
命令。 - 如果
oldfd
是一个特殊文件描述符(例如套接字或管道),dup2
将复制它的特殊属性。 - 如果
newfd
已经打开,dup2
将先关闭它,然后再复制oldfd
。 - 如果
newfd
是一个有效的文件描述符,并且大于等于RLIMIT_NOFILE
的当前值,dup2
可能会失败。 - 在多线程程序中,使用
dup2
可能需要同步,因为文件描述符表是进程级的,而不仅仅是线程级的。
2.命名管道
这里将会介绍进程的另一种通信方式——命名管道,来解决不相关进程间的通信问题。
命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。
由于Linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,也使它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使用
原理图如下:
mkfifo
在Linux系统中,mkfifo
是一个用于创建命名管道(也称为 FIFOs)的系统调用。命名管道是一种特殊类型的文件,它允许在进程间进行单向数据传输,而无需它们之间有亲缘关系。以下是 mkfifo
的详细信息:
创建一个FIFO文件,注意是创建一个真实存在于文件系统中的文件,filename指定了文件名,而mode则指定了文件的读写权限
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数
pathname
: 指定要创建的命名管道的路径。mode
: 指定新命名管道的权限位,它遵循传统的UNIX文件权限位。通常,它是由umask
值修改后的结果。
返回值
- 成功时,返回0。
- 失败时,返回-1,并设置
errno
来指示错误。
使用场景
命名管道通常用于以下情况:
- 进程间通信(IPC):当两个无关的进程需要通信时,可以使用命名管道来传递数据。
- 数据缓冲:命名管道可以用来作为数据缓冲区,允许生产者进程在消费者进程准备好之前写入数据
【testcode】
下面是一个简单的例子,演示如何使用 mkfifo
创建一个命名管道,并使用它进行进程间通信。
fifowrite.c实现:
fiforead.c实现:
运行如下:
如上可以看到,我们用的编译指令是gcc fifowrite.c -o w 生成w可执行文件(也就是写端操作),同理用gcc fiforead.c -o r 生成r可执行文件(也就是读端操作);然后先执行w文件,如图1所示,可以看到此进程阻塞,等待对方读段操作响应,接下来执行r文件,此刻读端操作运行后,写段操作接收到这一响应后,立刻写入数据,所以读端操作读取了相应的数据“Hello FIFO, Write data to mkfifo......!!!”并将其放入buf中,最后将其打印显示出来,当然,你若是想要放入其他文件也可以,看你需求如何,整个基本流程如上,两个进程之间通过命名管道进行通信完毕
open
在Linux系统中,open
函数是用于打开或创建文件的系统调用。它通常在程序中通过系统调用或库函数(如 fopen
)来使用。open
系统调用可以返回一个文件描述符,该描述符是一个非负整数,用于后续的读写操作。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname
:要打开或创建的文件的路径名。flags
:指定文件打开方式的标志,可以是以下几种的组合:O_RDONLY
:只读方式打开。O_WRONLY
:只写方式打开。O_RDWR
:读写方式打开。O_CREAT
:如果文件不存在,则创建该文件。O_APPEND
:追加方式写入。O_TRUNC
:如果文件存在,则清空该文件。O_EXCL
:与O_CREAT
一起使用,表示如果文件已存在,则返回错误。
mode
:当flags
包含O_CREAT
时,指定新创建文件的权限。
【testcode】
在这个示例中,我们首先尝试打开名为 example.txt
的文件,然后读取其内容,并将其打印到控制台
运行:
注意事项
open
系统调用返回的文件描述符是一个整数,它是一个小的非负整数,用于后续的读写操作。- 如果
open
调用失败,它会返回-1
,并设置errno
以指示错误原因。 - 当打开文件进行写入时,如果文件不存在,则
open
会创建该文件。如果文件已存在,则根据flags
中的标志决定是清空文件还是追加内容。 mode
参数仅在创建新文件时使用,它指定了文件的权限。这些权限可以使用umask
值进行修改。
read
在Linux系统中,read
函数是用于从文件描述符中读取数据的系统调用。它通常在程序中通过系统调用或库函数(如 fgets
)来使用。read
系统调用可以从文件、管道、套接字等读取数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
fd
:要读取的文件的文件描述符。buf
:用于存储读取数据的缓冲区。count
:要读取的最大字节数。
返回值
- 成功时,
read
函数返回实际读取的字节数。 - 如果到达文件末尾,返回0。
- 如果出错,返回-1,并设置
errno
以指示错误原因。
注意事项
read
系统调用返回的值表示实际读取的字节数。如果返回值小于count
,可能是因为到达了文件末尾或读取过程中发生了错误。- 如果
read
调用失败,它会返回-1
,并设置errno
以指示错误原因。 - 如果
count
为0,read
也会返回0,表示没有读取任何数据。 read
系统调用可以用于读取任何类型的文件,包括普通文件、管道、套接字等
【testcode】
查看open函数的testcode便可
close
在Linux系统中,close
函数是用于关闭文件描述符的系统调用。当一个文件描述符不再需要时,应该使用 close
函数来释放它,这样可以避免资源泄露。
#include <unistd.h>
int close(int fd);
fd
:要关闭的文件的文件描述符。
返回值
- 成功时,
close
函数返回0。 - 如果出错,返回-1,并设置
errno
以指示错误原因。
注意事项
close
函数会释放与文件描述符关联的所有资源,包括文件锁和I/O缓冲区。- 如果一个文件描述符被多次关闭,
close
函数的行为是未定义的,通常会导致程序崩溃。 - 如果
close
调用失败,它会返回-1
,并设置errno
以指示错误原因。 - 如果
close
成功,它返回0,表示文件描述符已被成功关闭
【testcode】
查看open函数的testcode便可