cpp-doc-管道

管道

概念

管道限制

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;
  • 通常一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道

读写规则

  • 当没有数据可读时
    • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据为止
    • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN
  • 当管道满的时候
    • O_NONBLOCK disbale:write调用阻塞
    • O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE
  • 当要写入的数据量不大于PIPE_BUF时,linux将保持写入的原子性
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性

匿名管道与命名管道

  • 匿名管道由pipe函数创建并打开
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一旦这些工资完成之后,它们具有相同的语义

匿名管道 pipe

创建

使用pipe系统调用

获取两个“文件描述符”,分别对应管道的读端和写端。

fd[0]: 是管道的读端

fd[1]: 是管道的写端

如果对fd[0]进行写操作,对fd[1]进行读操作,可能导致不可预期的错误

使用

单进程使用管道进行通信

创建管道后,获得该管道的两个文件描述符,不需要普通文件操作中的open操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];

ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}

strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1));
printf("send information:%s\n", buff1);

bzero(buff2, sizeof(buff2));
read(fd[0], buff2, sizeof(buff2));
printf("received information:%s\n", buff2);

return 0;
}

多进程使用管道进行通信

创建管道之后,再创建子进程,此时一共有4个文件描述符。
4个端口,父子进程分别有一个读端口和一个写端口。
向任意一个写端口写数据,即可从任意一个读端口获取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;

ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}

pd = fork();
if (pd == -1) {
// 进程创建失败
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
// 子进程
bzero(buff2, sizeof(buff2));
read(fd[0], buff2, sizeof(buff2));
printf("child process(%d) received information:%s\n", getpid(), buff2);
} else {
// 父进程
strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1));
printf("parent process(%d) send information:%s\n", getpid(), buff1);
}

if (pd > 0) {
wait(0);
}

return 0;
}

execl启动新程序进行通信

子进程使用exec启动新程序运行后,新进程能够使用原来进程的管道(因为exec能共享原来的文件描述符)
但问题是新进程并不知道原来的文件描述符是多少!最简单的方法就是把进程中的管道文件描述符,用exec的参数传递给新进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;

ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}

pd = fork();
if (pd == -1) {
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
// 子进程
//bzero(buff2, sizeof(buff2));
sprintf(buff2, "%d", fd[0]);
execl("main32", "main32", buff2, 0); // 创建出错就会执行后面代码
printf("child process execl error!\n");
exit(1);
} else {
// 父进程
strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1));
printf("parent process(%d) send information:%s\n", getpid(), buff1);
}

if (pd > 0) {
wait(0);
}

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[])
{
int fd;
char buff[1024] = {0,};

sscanf(argv[1], "%d", &fd);
read(fd, buff, sizeof(buff));

printf("execl Process(%d) received information:%s\n", getpid(), buff);
return 0;
}

关闭管道的读写端

对管道进行read时,如果管道中已经没有数据了,此时读操作将被“阻塞”。
如果此时管道的写端已经被close了,则写操作将可能被一直阻塞!而此时的阻塞已经没有任何意义了。(因为管道的写端已经被关闭,即不会再写入数据了)

如果不准备再向管道写入数据,则把该管道的所有写端都关闭,此时再对该管道read时,就会返回0,而不再阻塞该读操作。(管道的特性)
如果有多个写端口,而只关闭了一个写端,那么无数据时读操作仍将被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;

ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}

pd = fork();
if (pd == -1) {
// 进程创建失败
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
// 子进程
close(fd[1]);
bzero(buff2, sizeof(buff2));
read(fd[0], buff2, sizeof(buff2));
printf("child process(%d) received information:%s\n", getpid(), buff2);
} else {
// 父进程
strcpy(buff1, "Hello!");
close (fd[0]);
write(fd[1], buff1, strlen(buff1));
printf("parent process(%d) send information:%s\n", getpid(), buff1);

close (fd[1]);
}

if (pd > 0) {
wait(0);
}

return 0;
}

把管道作为标准输入和标准输出

把管道作为标准输入和标准输出的优点:

  1. 子进程使用execl启动新程序时,就不需要再把管道的文件描述符传递给新程序了。

  2. 可以直接使用使用标准输入(或标准输出)的程序。

    比如 od –c (统计字符个数,结果为八进制)

实现原理:

  1. 使用dup复制文件描述符

  2. 用exec启动新程序后,原进程中已打开的文件描述符仍保持打开,即可以共享原进程中的文件描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;

ret = pipe(fd);
if (ret !=0) {
// 创建管道失败
printf("create pipe failed!\n");
exit(1);
}

pd = fork();
if (pd == -1) {
// 创建进程失败
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
// 子进程
//bzero(buff2, sizeof(buff2));
//sprintf(buff2, "%d", fd[0]);
close(fd[1]);

close(0);
dup(fd[0]);
close(fd[0]);

execlp("./main52", "./main52", "-c", 0);
printf("child process execl error!\n");
exit(1);
} else {
// 父进程
close(fd[0]);

strcpy(buff1, "Hello!\n");
write(fd[1], buff1, strlen(buff1));

strcpy(buff1, "world!\n");
write(fd[1], buff1, strlen(buff1));

close(fd[1]);
}

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int ret = 0;
char buff[80] = {0,};

ret = scanf("%s", buff);
printf("main52 recv %s\n", buff);

ret = scanf("%s", buff);
printf("main52 recv %s\n", buff);
return 0;
}

popen

popen用来在两个程序之间传递数据

在程序A中使用popen调用程序B时,有两种用法:

  1. 程序A读取程序B的输出(使用fread读取)

  2. 程序A发送数据给程序B,以作为程序B的标准输入。(使用fwrite写入)

  • popen接收别程序的输出做输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>

#define BUFF_SIZE 1024

int main(void)
{
FILE * file;
char buff[BUFF_SIZE+1];
int cnt;

// system("ls -l > result.txt");
file = popen("ls -l", "r");
if (!file) {
printf("fopen failed!\n");
exit(1);
}

cnt = fread(buff, sizeof(char), BUFF_SIZE, file);
if (cnt > 0) {
buff[cnt] = '\0';
printf("%s", buff);
}

pclose(file);

return 0;
}
  • popen向别的程序输出数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFF_SIZE 1024

int main(void)
{
FILE * file;
char buff[BUFF_SIZE+1];
int cnt;

file = popen("./main63", "w");
if (!file) {
printf("fopen failed!\n");
exit(1);
}

strcpy(buff, "main62 say hello world!");
cnt = fwrite(buff, sizeof(char), strlen(buff), file);

pclose(file);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
char buff[1024] = {0};

read(0, buff, sizeof(buff));
printf("main63 recv : %s\n", buff);

return 0;
}

命名管道 FIFO

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件。
  • 命令创建方式
    • mkfifo filename

命名管道的打开规则

  • 如果当前打开操作是为了读而打开FIFO时
    • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
    • O_NONBLOCK enable:立刻返回成功
  • 如果当前打开操作是为写而打开FIFO时
    • O_NONBLOCK disbale:阻塞直到有相应进程为读而打开该FIFO
    • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO