Linux管道编程小记

匿名管道pipe是单向的,或者说是半双工的。

提供了一个和它有共同先祖的进程的通信方式。

注意:如果需要全双工通信,应该转而考虑套接字API

另一种类型的管道被称为命名管道。一个命名管道像正常管道一样工作,但是它存在于文件系统中,因而任意进程都可以找到它。这使得不同先祖的进程之间也可以进行通信。

注意:管道只不过是一对文件描述符,因此所有能够操作文件描述符的函数都可以用于管道。这些函数包括但不限于 select,read,write,fcntl,freopen。

pipe 函数

1
2
#include <unistd.h>
int pipe(int fds[2]);

pipe 函数成功时返回0,失败时返回-1,并设置相应的errno。

成功返回时,fds数组(作为pipe函数的参数传递)会被赋予两个活动的文件描述符。

数组中第一个元素是应用程序可读取的文件描述符,第二个元素是应用程序可写入的文件描述符。

例子:

创建一个管道,然后把进程分支为一个父进程和一个子进程。(16行)

在子进程中,试图从管道的输入文件描述符进行读取(第18行),这会将该进程挂起,直到有什么东西可以读取为止。

父进程就是使用写文件描述符(pipe数组的第二个元素)向管道中写入一个测试字符串,然后用wait函数等待子进程退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<wait.h>
#define MAX_LINE 80
int main(){
int thePipe[2], ret;
char buf[MAX_LINE+1];
const char *testbuf = {"a test string."};
if(pipe(thePipe) == 0){
if(fork()==0){
ret = read(thePipe[0],buf,MAX_LINE);
buf[ret]=0;
printf("Child read %s\n",buf);
}
else{
ret = write(thePipe[1],testbuf,strlen(testbuf));
ret = wait(NULL);
}
}
return 0;
}
1
2
fork()为系统调用,一个现有的进程可以调用fork()创建一个子进程。
fork函数被调用一次返回两次,在子进程中返回0,父进程中返回子进程ID,出错返回-1
1
2
pid_t wait (int *status);
等待子进程结束,返回子进程PID,子进程结束状态由status返回。

当进程结束后,为管道分配的资源会被自动释放。

尽管如此,在编程中最好还是调用close关闭管道使用的描述符。

1
2
close(myPipe[0]);
close(myPipe[1]);

dup和dup2

1
2
int dup(int oldfd);
int dup2(int oldfd, int newfd);

这两个函数是用来复制文件描述符的。

举个栗子,用 dup(2) 实现输出的重定向。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <unistd.h>
int main (void)
{
/* 如何在不改变下面的内容的情况下,使输出的内容到文件中? */
puts("dup test.");
return 0;
}

puts(3) 函数是将参数字符串写入到标准输出 stdout(文件描述符是 1) 中,而标准输出默认目标是我们的 shell。

如果想要让 puts(3) 的参数输出到一个文件中,实现思路是:首先打开一个文件获得一个新的文件描述符,然后关闭标准输出文件描述符(1),然后使用 dup(2) 函数族复制产生一个新的文件描述符,此时的 1 号文件描述符就不是标准输出的文件描述符了,而是我们自己创建的文件的描述符了。

还记得我们之前提到过吗,文件描述符优先使用可用范围内最小的。进程中当前打开的文件描述符有标准输入(0)、标准输出(1)、标准错误(2)和我们自己打开的文件(3),当我们关闭了 1 号文件描述符后,当前可用的最小文件描述符是 1,所以新复制的文件描述符就是 1。而标准库函数 puts(3) 在调用系统调用 write(2) 函数向 1 号文件描述符打印时,正好是打印到了我们所指定的文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main (void)
{
int fd = -1;
fd = open("tmp", O_WRONLY | O_CREAT | O_TRUNC, 0664);
close(1); // 关闭标准输出
dup2(fd, 1);
close(fd);
/* 要求在不改变下面的内容的情况下,使输出的内容到文件中 */
puts("dup test.");
return 0;
}

由于题目的要求是 puts(3) 上面注释以下的内容都不能修改,原则上 1 号文件描述符在这里使用完毕也需要 close(2),所以这里造成了一个内存泄漏,但并不影响对 dum(2) 函数族的解释和测试。

例二

模拟 ls -l | wc -l的指令,即将ls的结果通过管道作为输入给wcwc指令:我们可以计算文件的Byte数、字数、或是列数,若不指定文件名称、或是所给予的文件名为”-“,则wc指令会从标准输入设备读取数据。

参数

  • -c或–bytes或–chars 只显示Bytes数。
  • -l或–lines 只显示行数。
  • -w或–words 只显示字数。
  • –help 在线帮助。
  • –version 显示版本信息。

使用dup实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
int main()
{
int pfds[2]
if(pipe(pfds)==0){
if(fork()==0){
close(1);
dup2(pfds[1],1);//pipe的两个参数,第0位为input,第1位为output
//而标识符为1的文件操作符为stdout,0为标准输入stdin
close(pfds[0]);
execlp("ls","ls","-1",NULL);//execlp下面介绍
}else{
close(0);
dup2(pfds[0],0);
close(pfds[1]);
execlp("wc","wc","-l",NULL);
}
}
}

execlp

从PATH 环境变量中查找文件并执行

定义

int execlp(const char * file,const char * arg,……);

头文件

#include<unistd.h>

说明

execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名, 找到后便执行该文件, 然后将第二个以后的参数当做该文件的argv[0]、argv[1]……, 最后一个参数必须用空指针(NULL)作结束。

函数mkfifo

mkfifo用于在文件系统中创建一个文件,提供FIFO功能(也称命名管道)。

之前的匿名管道只能用于进程与其子进程之间,

命名管道在文件系统中可见,因此可以用于任何进程之间。

函数mkfifo原型如下:

1
2
3
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *pathname,mode_t mode);

第一个参数(pathname)在文件系统中指明要创建的文件。

第二个参数表示对该FIFO的读写权限设置。

函数成功时返回0,错误返回-1,并设置相应errno

示例:

1
2
3
4
5
6
7
int ret;
ret = mkfifo("/tmp/cmd_pipe",S_IFIFO |0666);
if(ret == 0){
...
}else{
...
}

本文标题:Linux管道编程小记

文章作者:Yang Shuai

发布时间:2018年05月13日 - 21:05

最后更新:2018年05月24日 - 11:05

原始链接:https://ysbbswork.github.io/2018/05/13/Linux管道编程小记/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!