进程间通信
创建一个子进程,我们可以使用
fork
函数来启动一个新的子进程;一个ELF
可执行文件启动之后就是一个进程了,这个文件里的代码和数据会被加载到内存中,以前的程序是直接运行在物理内存上的,这样的话会有几个问题:
- 物理内存实际上是有限的,我们的操作系统运行的进程很多,物理内存很快就会被占满;如果此时系统就
100M
内存,此时有个A
进程需要 50M,B 进程也需要 50M,那么 C 进程怎么运行呢?以前就会把 A 进程的数据暂时写入硬盘缓存,这样的话,就会在硬盘和内存来回读写,内存使用效率不高。- 进程 A 可能会恶意修改进程 B 的内存数据,甚至恶意的攻击
后来搞了一个进程虚拟地址virtual address,CPU通过虚拟内存访问会经过内存映射管理MMU(memory management unit)映射转换到物理内存;
这样每个进程都有自己独立的虚拟内存地址,就可以达到进程间互相隔离
也就是说进程间要相互通信,就必须要依赖操作系统内核实现了;也就是说系统内核提供一个缓冲区,进程A,进程B操作这个缓冲区【读写数据】来实现通信
Linux 进程间通信有一下几种方式:
IPC: inter-process communication 内部进程通信
- 管道(用于单个机器上进程间具有血缘关系的进程间通信,比如父子进程,兄弟进程)【匿名管道
pipe
,命名管道 FIFO】实现了一个标准 unix IPC,posix
封装的posix_mkfifo
- 中断信号,只能用于进程异步事件通知,并不能发送很多消息
system V IPC
标准提供的进程间通信【消息队列、信号量、共享内存】,php
进程扩展只是封装了systemp V IPC
socket stream
通信,可以实现跨机器进程间通信
# 管道通信
命名管道:FIFO:First in First out 实际上内核实现了类似队列
使用posix_mkfifo
创建命名管道文件
<?php
$file = "fifo_x";
if (!posix_access($file, POSIX_F_OK)) {
if (posix_mkfifo($file, 0666)) {
fprintf(STDOUT, "create ok\n");
}
}
2
3
4
5
6
7
8
9
10
[root@jb51 process]# php demo22.php
create ok
[root@jb51 process]# file fifo_x
fifo_x: fifo (named pipe)
2
3
4
5
# 父子进程间通信
# 阻塞方式
<?php
$file = "fifo_x";
if (!posix_access($file, POSIX_F_OK)) {
if (posix_mkfifo($file, 0666)) {
fprintf(STDOUT, "create ok\n");
}
}
// 父进程以写的方式打开管道文件
$fd = fopen($file, "w");
$pid = pcntl_fork();
if ($pid == 0) {
// 子进程负责接收数据
// 子进程以读的方式打开管道文件
$fd = fopen($file, "r");
// 没有数据读取到就会阻塞
$data = fread($fd, 5);
if ($data) {
fprintf(STDOUT, "read process pid=%d, recv: %s\n", posix_getpid(), $data);
}
// 接收完数据,就退出子进程
exit(0);
}
// 父进程写执行的概率比较大
$len = fwrite($fd, "hello", 5);
fprintf(STDOUT, "write process pid=%d write len=%d\n", posix_getpid(), $len);
fclose($fd);
// 回收退出的子进程
$pid = pcntl_wait($status);
if ($pid > 0) {
fprintf(STDOUT, "exit pid=%d\n", $pid);
}
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
注意
[root@jb51 process]# strace -f -s 65500 -p 32209
strace: Process 32209 attached
open("/data/work/php/process/fifo_x", O_WRONLY|O_CREAT|O_TRUNC, 0666
2
3
这里执行会有问题,会在父进程打开文件地方进行阻塞,而子进程都没有进行创建,所以这里是不行的。我们需要将打开文件放到下面去
<?php
$file = "fifo_x";
if (!posix_access($file, POSIX_F_OK)) {
if (posix_mkfifo($file, 0666)) {
fprintf(STDOUT, "create ok\n");
}
}
$pid = pcntl_fork();
if ($pid == 0) {
// 子进程负责接收数据
// 子进程以读的方式打开管道文件
$fd = fopen($file, "r");
// 没有数据读取到就会阻塞
$data = fread($fd, 5);
if ($data) {
fprintf(STDOUT, "read process pid=%d, recv: %s\n", posix_getpid(), $data);
}
// 接收完数据,就退出子进程
exit(0);
}
// 父进程以写的方式打开管道文件
$fd = fopen($file, "w");
// 父进程写执行的概率比较大
$len = fwrite($fd, "hello", 5);
fprintf(STDOUT, "write process pid=%d write len=%d\n", posix_getpid(), $len);
fclose($fd);
// 回收退出的子进程
$pid = pcntl_wait($status);
if ($pid > 0) {
fprintf(STDOUT, "exit pid=%d\n", $pid);
}
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
[root@jb51 process]# php demo22.php
write process pid=534 write len=5
read process pid=535, recv: hello
exit pid=535
2
3
4
我们使用跟踪命令来看看具体走了哪些步骤
[root@jb51 process]# strace -f -s 65500 -o demo22_1.txt php demo22.php
write process pid=880 write len=5
read process pid=882, recv: hello
exit pid=882
2
3
4
880 open("/data/work/php/process/fifo_x", O_WRONLY|O_CREAT|O_TRUNC, 0666 <unfinished ...>
882 set_robust_list(0x7f94a2094c60, 24) = 0
882 getcwd("/data/work/php/process", 4096) = 23
882 open("/data/work/php/process/fifo_x", O_RDONLY) = 3
2
3
4
一个以写的方式打开,一个以读的方式打开。
stat(3, {st_mode=S_IFIFO|0644, st_size=0, ...}) = 0
880 write(3, "hello", 5) = 5
880 write(1, "write process pid=880 write len=5\n", 34) = 34
880 close(3) = 0
880 wait4(-1, <unfinished ...>
882 fstat(3, {st_mode=S_IFIFO|0644, st_size=0, ...}) = 0
882 read(3, "hello", 8192) = 5
882 write(1, "read process pid=882, recv: hello\n", 34) = 34
882 close(3) = 0
882 close(0) = 0
2
3
4
5
6
7
8
9
10
# 非阻塞方式
<?php
$file = "fifo_x";
if (!posix_access($file, POSIX_F_OK)) {
if (posix_mkfifo($file, 0666)) {
fprintf(STDOUT, "create ok\n");
}
}
$pid = pcntl_fork();
if ($pid == 0) {
// 子进程负责接收数据
// 子进程以读的方式打开管道文件
$fd = fopen($file, "r");
// 将文件设置为非阻塞模式
stream_set_blocking($fd, 0);
// read 系统函数,不管有没有数据,甚至写端还没写数据 都不管,read函数立马返回
$data = fread($fd, 5);
if ($data) {
fprintf(STDOUT, "read process pid=%d, recv: %s\n", posix_getpid(), $data);
}
// 如果以非阻塞方式,不管有没有读取到数据,程序会非常快的结束
exit(0);
}
// 父进程以写的方式打开管道文件
$fd = fopen($file, "w");
stream_set_blocking($fd, 0);
// 如果有缓存空间就能写进去,也有可能写不进去
$len = fwrite($fd, "hello", 5);
fprintf(STDOUT, "write process pid=%d write len=%d\n", posix_getpid(), $len);
fclose($fd);
// 回收退出的子进程
$pid = pcntl_wait($status);
if ($pid > 0) {
fprintf(STDOUT, "exit pid=%d\n", $pid);
}
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
[root@jb51 process]# php demo23.php
write process pid=3621 write len=0
exit pid=3622
2
3
这里子进程很快结束,只要调用read
函数不会阻塞到有数据,不会挂起,会立马返回
现在我们改一下,读的,我们只有读到数据才退出
$pid = pcntl_fork();
if ($pid == 0) {
// 子进程负责接收数据
// 子进程以读的方式打开管道文件
$fd = fopen($file, "r");
// 将文件设置为非阻塞模式
stream_set_blocking($fd, 0);
$i = 0;
while (1) {
// read 系统函数,不管有没有数据,甚至写端还没写数据 都不管,read函数立马返回
$data = fread($fd, 5);
echo $i++;
echo "\r\n";
if ($data) {
fprintf(STDOUT, "read process pid=%d, recv: %s\n", posix_getpid(), $data);
// 只有读到数据才退出
break;
}
}
// 如果以非阻塞方式,不管有没有读取到数据,程序会非常快的结束
exit(0);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@jb51 process]# php demo23.php
0
1
2
...
1256
1257
1258
1259
1260
read process pid=7201, recv: hello
write process pid=7200 write len=5
exit pid=7201
2
3
4
5
6
7
8
9
10
11
12
13
14
可以看到读取了 1260 次才读到了数据。
# 测试一下当读端关闭的时候,如过写端还在继续写,这个时候就无法写进去了,并且还会产生中断信号SIGPIPE
<?php
$file = "fifo_x";
if (!posix_access($file, POSIX_F_OK)) {
if (posix_mkfifo($file, 0666)) {
fprintf(STDOUT, "create ok\n");
}
}
// 编写信号处理函数
pcntl_signal(SIGPIPE, function ($signo) {
fprintf(STDOUT, "signo=%d\n", $signo);
});
$pid = pcntl_fork();
if ($pid == 0) {
// 子进程负责接收数据
// 子进程以读的方式打开管道文件
$fd = fopen($file, "r");
// 将文件设置为非阻塞模式
stream_set_blocking($fd, 0);
$i = 0;
while (1) {
// read 系统函数,不管有没有数据,甚至写端还没写数据 都不管,read函数立马返回
$data = fread($fd, 5);
if ($data) {
$i++;
fprintf(STDOUT, "read process pid=%d, recv: %s\n", posix_getpid(), $data);
// 读到5次之后整个子进程退出,并且关掉管道文件
if ($i == 5) {
fclose($fd);
break;
}
}
}
exit(0);
}
// 父进程以写的方式打开管道文件
$fd = fopen($file, "w");
stream_set_blocking($fd, 0);
while (1) {
// 信号调度
pcntl_signal_dispatch();
$len = fwrite($fd, "hello", 5);
fprintf(STDOUT, "write process pid=%d write len=%d\n", posix_getpid(), $len);
}
fclose($fd);
// 回收退出的子进程
$pid = pcntl_wait($status);
if ($pid > 0) {
fprintf(STDOUT, "exit pid=%d\n", $pid);
}
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
57
58
59
60
执行内容过多就不做展示了。
# 总结
如果读端已经关闭,写端还在继续写数据,是无法继续写了,并且还会产生中断信号
SIGPIPE
,这个时候让进程终止即可。
# 非血缘关系进程间通信
发送消息端
<?php
// 该进程作为消息发送进程
$file = "fifo_x";
if (!posix_access($file, POSIX_F_OK)) {
if (posix_mkfifo($file, 0666)) {
fprintf(STDOUT, "create ok\n");
}
}
$fd = fopen($file, "w");
while (1) {
// 从终端接收数据
$data = fgets(STDIN, 128);
if ($data) {
$len = fwrite($fd, $data, strlen($data));
fprintf(STDOUT, "pid=%d, write bytes=%d\n", posix_getpid(), $len);
}
}
fclose($fd);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
接收消息端
<?php
// 接收消息进程
$file = "fifo_x";
if (!posix_access($file, POSIX_F_OK)) {
if (posix_mkfifo($file, 0666)) {
fprintf(STDOUT, "create ok\n");
}
}
$fd = fopen($file, "r");
// 读以非阻塞的方式读
stream_set_blocking($fd, 0);
while (1) {
$data = fread($fd, 128);
if ($data) {
fprintf(STDOUT, "pid=%d, recv: %s\n", posix_getpid(), $data);
}
}
fclose($fd);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
注意:接收消息的必须以非阻塞的方式接收,否则接收不到消息。
这里读端使用cat fifo_x
也可以
[root@jb51 process]# php demo25.php
create ok
hello world
pid=20929, write bytes=12
[root@jb51 process]# php demo26.php
pid=20974, recv: hello world
2
3
4
5
6
7
8
1 个写进程,2 个读进程;如果有一个read
进程收到数据之后,其他read
进程就没有机会读取数据了
[root@jb51 process]# php demo25.php
create ok
hello world
pid=20929, write bytes=12
lalala
pid=20929, write bytes=7
dqwdqw
pid=20929, write bytes=7
# 第一次是这个接收
[root@jb51 process]# php demo26.php
pid=20974, recv: hello world
pid=20974, recv: lalala
# 第二次是这个接收
[root@jb51 process]# cat fifo_x
dqwdqw
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
如果读进程关掉,写进程也就没啥作用了;一个发送端对应多个接收端,接收端读取消息这里是 2 个,可能会接收的次数会比较平均。
1 个读进程,2 个写进程
这个时候,2 个写进程写,读进程都能收到消息。
# 总结:
命名管道通信:可以用于父子之间(血缘关系)进程间通信,也可以用于非血缘关系进程间通信