进程间通信--管道

进程间通信学习

进程间通信,是很多面试中都会问到的问题,以前只听过管道可以实现,但是对原理一无所知,
看到IBM文档中,有个不错的介绍(美中不足的是代码缩进的不好看),所以,转载下来,以备不时之需:)

管道的特点

1.管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
2.只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
3.单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
4.数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

管道的创建:

一般使用如下代码创建管道:

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

管道的读写规则

管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;
另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。
如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。
很多系统调用,如read, write, close都可以用于管道操作。

hello world

pipe1.c如下,验证如何使用管道进行父子进程通信:

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 <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[]) {
int pipe_fd[2]; // 0为读管道,1为写管道
pid_t pid;
char read_buf[100];
char write_buf[4];
int read_num;
if (pipe(pipe_fd) < 0) {
fprintf(stderr, "pipe_fd error\n");
exit(1);
}
/* 子进程 */
if ((pid = fork()) == 0) {
printf("\n");
// 关闭写管道
close(pipe_fd[1]);
sleep(3); // 确保父进程关闭写端
// 从读管道读数据
read_num = read(pipe_fd[0], read_buf, 100);
printf("read num is %d the data read from the pipe %s\n", read_num, read_buf);
// 关闭读管道
close(pipe_fd[0]);
exit(0);
} else if(pid > 0) {
// 关闭读管道
close(pipe_fd[0]);
strcpy(write_buf, "111");
if (write(pipe_fd[1], write_buf, 4) != -1) {
printf("write to child over!\n");
// 关闭写管道
close(pipe_fd[1]);
printf("parent close write pipe: pipe_fd[1] over!\n");
sleep(10);
}
exit(0);
}
return 0;
}

运行结果如下:

1
2
3
4
5
6
write to child over!
parent close write pipe: pipe_fd[1] over!
read num is 4 the data read from the pipe 111

说明使用管道,确实可以让父子进程通信数据。

向管道写数据规则

向管道中写入数据时,Linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。

注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。

write.c如下:

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
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(int argc, char const *argv[]) {
int pipe_fd[2];
pid_t pid;
char read_buf[4];
char *write_buf;
int write_num;
if (pipe(pipe_fd) < 0) {
fprintf(stderr, "pipe_fd error\n");
exit(1);
}
if ((pid=fork()) == 0) {
printf("I'm Child, My PID is %ld, my Parent is %ld\n", (long)getpid(), (long)getppid());
close(pipe_fd[0]);
close(pipe_fd[1]);
sleep(10);
} else if(pid > 0) {
printf("I'm Parent, My PID is %ld, my Parent is %ld\n", (long)getpid(), (long)getppid());
sleep(1);
close(pipe_fd[0]);
write_buf = "111";
// 此处会触发 Program received signal SIGPIPE, Broken pipe. 程序会终止
write_num = write(pipe_fd[1], write_buf, 4);
if (write_num == -1) {
printf("write to pipe error\n");
} else {
printf("the bytes write to pipe is %d \n", write_num);
}
close(pipe_fd[1]);
}
return 0;
}

这个程序并不能运行完,也就是无法输出write to pipe error或者the bytes write to pipe is xxx
因为,在写之前,就关闭了读管道,所以,写就变得没有意义了:

On a last note, pipes must have a reader and a writer. If a process tries to write to a pipe that has no reader, it will be sent the SIGPIPE signal from the kernel.

如果进程尝试写入一个没有读的管道,会从内核收到SIGPIPE信号,默认情况下,会终止进程,所以,这个程序无法输出
write to pipe error或者the bytes write to pipe is xxx

Broken pipe,原因就是该管道以及它的所有fork()
产物的读端都已经被关闭。如果在父进程中保留读端,即在写完pipe后,再关闭父进程的读端,也会正常写入pipe。
比如下面的例子,就能写入了

close_after_write.c:

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
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(int argc, char const *argv[]) {
int pipe_fd[2];
pid_t pid;
char read_buf[4];
char *write_buf;
int write_num;
if (pipe(pipe_fd) < 0) {
fprintf(stderr, "pipe_fd error\n");
exit(1);
}
if ((pid=fork()) == 0) {
printf("I'm Child, My PID is %ld, my Parent is %ld\n", (long)getpid(), (long)getppid());
close(pipe_fd[0]);
close(pipe_fd[1]);
sleep(10);
} else if(pid > 0) {
printf("I'm Parent, My PID is %ld, my Parent is %ld\n", (long)getpid(), (long)getppid());
sleep(1);
write_buf = "111";
// 有一端保留读写管道,依然可以写入
write_num = write(pipe_fd[1], write_buf, 4);
if (write_num == -1) {
printf("write to pipe error\n");
} else {
printf("the bytes write to pipe is %d \n", write_num);
}
close(pipe_fd[0]);
close(pipe_fd[1]);
}
return 0;
}

Linux不保证写管道的原子性

测试代码如下:

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
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[]) {
int pipe_fd[2];
pid_t pid;
char r_buf[4096];
char w_buf[4096 * 2];
int writenum;
int rnum;
if (pipe(pipe_fd) < 0) {
printf("pipe error \n");
return -1;
}
if ((pid = fork()) == 0) {
close(pipe_fd[1]);
// 子进程一直读
while (1) {
sleep(1);
rnum = read(pipe_fd[0], r_buf, 1000);
printf("child: readnum is %d\n", rnum);
}
close(pipe_fd[0]);
exit(0);
} else if (pid > 0) {
close(pipe_fd[0]);
// 父进程去写
if((writenum = write(pipe_fd[1], w_buf, 1024)) == -1) {
printf("write to pipe error\n");
}else {
printf("the bytes write to pipe is %d \n", writenum);
}
writenum = write(pipe_fd[1], w_buf, 4096);
close(pipe_fd[1]);
}
return 0;
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
the bytes write to pipe 1000
the bytes write to pipe 1000 //注意,此行输出说明了写入的非原子性
the bytes write to pipe 1000
the bytes write to pipe 1000
the bytes write to pipe 1000
the bytes write to pipe 120 //注意,此行输出说明了写入的非原子性
the bytes write to pipe 0
the bytes write to pipe 0
...

In order for an operation to be considered ``atomic’’, it must not be interrupted for any reason at all. The entire operation occurs at once.

如果一个操作是原子性的,他必须不能被任何原因中断。整个操作离开发生。

Up to 512 bytes can be written or retrieved from a pipe atomically. Anything that crosses this threshold will be split, and not atomic. Under Linux, however, the atomic operational limit is defined in ``linux/limits.h’’ as:

超过512字节会被管道原子地写入或者取回。任何数据通过这个入口的都会被分割,不是原子性地。在Linux下,原子操作的
限制在linux/limits.h中定义:

1
#define PIPE_BUF 4096

As you can see, Linux accommodates the minimum number of bytes required by POSIX,
quite considerably I might add. The atomicity of a pipe operation becomes important when more than one process is involved (FIFOS).
For example, if the number of bytes written to a pipe exceeds the atomic limit for a single operation,
and multiple processes are writing to the pipe, the data will be interleaved'' orchunked’’.
In other words, one process may insert data into the pipeline between the writes of another.

总之,一个进程可以在另外一个进程在写管道时候同时写管道,而不必等管道的缓存区为空时候才能写。

写入管道的数据量大于4096字节时,缓冲区的空闲空间将被写入数据(补齐),直到写完所有数据为止,如果没有进程读数据,则一直阻塞
如果超过4096个字节时,那么操作将变为原子的,比如以下代码:

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
54
55
56
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[]) {
int pipe_fd[2];
pid_t pid;
char r_buf[4096];
char w_buf[4096 * 2];
int writenum;
int rnum;
if (pipe(pipe_fd) < 0) {
printf("pipe error \n");
return -1;
}
if ((pid = fork()) == 0) {
close(pipe_fd[1]);
// 子进程一直读
while (1) {
sleep(1);
rnum = read(pipe_fd[0], r_buf, 1000);
printf("child: readnum is %d\n", rnum);
}
close(pipe_fd[0]);
exit(0);
} else if (pid > 0) {
close(pipe_fd[0]);
// 父进程去写
if((writenum = write(pipe_fd[1], w_buf, 5000)) == -1) {
printf("write to pipe error\n");
}else {
printf("the bytes write to pipe is %d \n", writenum);
}
int i;
for (i = 0; i < 30; i++) {
sleep(1);
printf("%d\n", i);
}
writenum = write(pipe_fd[1], w_buf, 5000);
close(pipe_fd[1]);
}
return 0;
}

这里,数据量大于4096字节(5000),缓冲区的空闲空间将被写入数据,读进程读完了5000个以后,就不再读了,而是
等待写进程继续写,如果没有写,则会一直阻塞住。

管道应用实例

1.shell中的管道,比如输入输出重定向,这个命令:

1
pgrep -f a.out | xargs kill

将 pgrep 的输出作为 kill的输入,在这种应用方式下,管道的创建对于用户来说是透明的。

2.用于具有亲缘关系的进程间通信

比如,尝试编写一个然并卵的例子,父进程发送一个数字,子进程把这个数字乘以2:

double.c:

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
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[]) {
int pipe_fd[2];
pid_t pid;
char r_buf[3];
char w_buf[3] = "41";
int writenum;
int rnum;
if (pipe(pipe_fd) < 0) {
printf("pipe error \n");
return -1;
}
if ((pid = fork()) == 0) {
close(pipe_fd[1]);
if ((rnum = read(pipe_fd[0], r_buf, 3)) < 0 ) {
printf("error read \n");
}
printf("get num is %d, add one is %d\n", atoi(r_buf), atoi(r_buf) + 1);
close(pipe_fd[0]);
exit(0);
} else if (pid > 0) {
close(pipe_fd[0]);
// 父进程去写
if((writenum = write(pipe_fd[1], w_buf, 3)) == -1) {
printf("write to pipe error\n");
}else {
printf("the bytes write to pipe is %d \n", writenum);
}
}
return 0;
}

管道的局限性

1.只支持单向数据流;
2.只能用于具有亲缘关系的进程之间;
3.没有名字;
4.管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
5.管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;

有名管道

管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。
FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。
这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。
值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。
它们不支持诸如lseek()等文件定位操作。

简单的说,就是建立一个公共缓存区,只有能读到这片区域的进程,都可以利用这片区域进行通信了。

有名管道的创建

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

该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。
第二个参数与打开普通文件的open()函数中的mode 参数相同。
如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。

创建实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO_SERVER "/tmp/mybuff"
int main(int argc, char const *argv[]) {
int res = 0;
res = mkfifo(FIFO_SERVER, O_CREAT | O_EXCL );
printf("mkfifo res is %d\n", res);
return 0;
}

查看建立的文件如下:

1
2
file /tmp/mybuff
/tmp/mybuff: setuid sticky fifo (named pipe)

有名管道的打开规则

名管道比管道多了一个打开操作:open 比无名管道要复杂啊。。。

如果当前打开操作是为读而打开FIFO时,
若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;
否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);
或者,成功返回(当前打开操作没有设置阻塞标志)。

如果当前打开操作是为写而打开FIFO时,
若已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;
否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);
或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。

有名管道的读写规则

从FIFO中读取数据:

约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。

1.如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。

2.对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。

3.读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)

4.如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。

注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。

向FIFO中写入数据:

约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。

对于设置了阻塞标志的写操作:

1.当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。

  1. 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。

对于没有设置阻塞标志的写操作:

1.当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。

2.当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;

实际使用时,需要针对不同的场景,使用不同的方式。具体就不写实例了,因为太多种组合。。。

小结

FIFO可以说是管道的推广,克服了管道无名字的限制,使得无亲缘关系的进程同样可以采用先进先出的通信机制进行通信。
管道和FIFO的数据是字节流,应用程序之间必须事先确定特定的传输”协议”,采用传播具有特定意义的消息。
要灵活应用管道及FIFO,理解它们的读写规则是关键。

参考:
1.https://www.ibm.com/developerworks/cn/linux/l-ipc/part1/
2.http://www.tldp.org/LDP/lpg/node20.html
3.http://www.tldp.org/LDP/lpg/node13.html