进程间通信(IPC)的方式有很多种,常用方式有管道pipe
、信号signal
、映射mmap
、本地套接字socket
等。这些方式各有优势,其中管道的方式最为简单。本篇文章主要记录管道在进程间通信的使用,在编程实例中还有dup
、exec
函数族的使用。
1 管道的介绍
在使用 Linux 命令的时,经常会借助管道来将一个命令的输出作为另一个命令的输入来执行,如
1 | ps aux | grep init |
通常所说的管道pipe
即匿名管道,区别于FIFO
命名管道。
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。 其实现原理是:内核使用环形队列机制,借助内核缓冲区(4k)实现。
其有以下特质
- 文件类型为伪文件,本质是内核缓冲区
- 由两个文件描述符引用,一个表示读端,一个表示写端
- 数据必须从管道写端流入,从读端流出
由以上特质可以看出其局限性
- 使用同一个管道的两个进程,自己写入的数据不能自己读
- 数据被读走后,便不在管道中存在,不可反复读取
- 半双工通信,数据只能在一个方向上流动
- 只能在有公共祖先的进程间使用
2 pipe()
2.1 函数原型
使用pipe()
函数可以创建一个管道,函数原型如下
1 |
|
函数成功执行时返回0,否则返回-1。使用的时候要注意两点
- 传出参数的
pipefd[2]
中,规定了必须使用pipefd[0]
来读取数据,pipe[1]
来写入数据。 - 在使用读功能的进程中,要关闭
pipe[1]
描述符,在使用写功能中,要关闭pipe[2]
描述符
2.2 管道读写行为
管道的读写行为特性如下
读管道时
- 管道有数据时,
read
返回实际读到的字节数 - 管道无数据时:1)无写端,
read
返回0,,类似于读到文件尾;2)有写端,read
阻塞等待
写管道时
- 无读端时,将会异常终止
- 有读端时:1)管道已满,阻塞等待;2)管道未满,返回写出的字节个数
2.3 使用小demo
1 |
|
将程序编译执行
1 | gcc pipe.c -o pipe |
3 dup和exec函数族
3.1 dup()和dup2()
在本系列的第二篇中,已经对文件描述符有了大概介绍。某种意义上说,文件描述符就是一个数组的下标,其对应了一个文件指针,文件指针指向了文件表,然后一层一层指向最终文件。
dup()
和dup2()
的作用就是复制一个现存的文件描述符。先看函数原型
1 |
|
当调用dup
函数时,内核在进程中创建一个新的文件描述符,此描述符是当前可用文件描述符的最小数值,这个文件描述符指向oldfd
所拥有的文件表项。
dup2
和dup
的区别就是可以用newfd
参数指定新描述符的数值,如果newfd
已经打开,则先将其关闭。如果newfd
等于oldfd
,则dup2
返回newfd
, 而不关闭它。dup2
函数返回的新文件描述符同样与参数oldfd
共享同一文件表项。
3.2 execl()和execlp()
exec
函数族可以使进程执行某一程序。当进程转去执行程序时,exec
函数后面的代码将不会执行。执行的程序会继承原进程的PID。
1 |
|
execl()
和execlp()
的区别在于,前者参数1需要输入执行程序的完整路径,而后者专用于执行系统命令,参数1只需输入命名名称即可;而之后的参数都是执行程序的参数,相当于C程序中的argv[]
,需要从argv[0]
开始输入。
3.3 实现文件重定向
通过dup()
函数,我们可以实现文件重定向功能。
下面的例子中,使用execl
执行ps ajx
命令,通过dup
将结果重定向到文件中。
1 |
|
4 实现管道命令
4.1 实现思路
先看一个管道命令
1 | ls | wc -l |
该命令可以将ls
的输出结果通过wc
命令来计数。
由于使用exec
函数来调用系统命令后,原程序会被命令程序替换,而这里我们需要执行两个命令ls
、wc
,显然如果不自己实现命令的话,是不能用一个进程来调用两个命令程序的。
因此需要使用fork()
来创建子进程,用不同的子进程来调用命令,最后还需要父进程将其回收。而不同子进程之间的通信就可以使用上文所介绍的管道pipe
来实现。
另外ls
的输出默认是STDOUT_FILENO
,需要使用dup2
,将其重定向到管道中,以便传输到wc
;同理,wc
的输入默认是STDIN_FILENO
,也需要使用dup2
重定向。
4.2 实现代码
代码实现如下
1 |
|
将其编译执行效果如下
1 | ./ls_wc_l2 |
可见该程序可以完成功能,且成功回收了两个子进程。
5 FIFO管道
5.1 FIFO介绍
由于基于fork机制,所以管道只能用于父进程和子进程之间,或者拥有相同祖先的两个子进程之间 (有亲缘关系的进程之间)。为了解决这一问题,Linux提供了FIFO方式连接进程。
FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。
当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。
FIFO管道又称为命名管道。其实它只是借用了文件系统(file system,命名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失 。
跟管道类似,FIFO也有读写规则
从FIFO中读取数据: 约定:如果一个进程为了从FIFO中读取数据而阻塞打开了FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作
从FIFO中写入数据: 约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。
5.2 mkfifo()
可以使用mkfifo
来创建一个FIFO
管道,函数原型如下
1 |
|
传入参数1代表管道名,参数2则是设定创建出来管道文件的权限,具体权限为mode & ~umask
。
由于命名管道和匿名管道在底层实现上其实完全一致,区别只是命名管道具有一个全局可见的文件名供其他进程使用open()
函数打开。
6 结束语
本篇文章主要介绍了管道这种进程间通信的方式,总结了pipe
和fifo
的读写规则。另外还介绍了能够实现文件重定向的dup
函数、在程序中执行其他程序的exec
函数,并相应地做了一些简单的练习。后续文章还需继续讲到其他进程间通信方式如mmap
、signal
以及socket
。