5.进程间通信
Nevermore 2022-09-03 OS
# 前置概念
- 进程是独立的,在用户层面,两个进程无法操作同一份内存资源,各自的地址空间会通过页表映射到物理内存的不同区块。因此需要操作系统介入才能实现进程间通信。
- 对于父子进程,子进程会拷贝一份父进程的PCB以保证进程的独立,而在子进程中保留了父进程的一些结构体指针,使得子进程和父进程可以指向同一份资源。文件不属于进程,子进程不需要重新拷贝文件的属性结构体,因此父子进程可以通过访问同一份文件实现通信。
# 普通管道
- 普通管道(匿名管道)允许两个进程以标准的生产消费者方式进行通信:生产者写入管道的一端(写入端),消费者从另一端(读取端)读取。因此,普通管道是单向的,只允许单向通信。如果需要双向通信,则必须使用两条管道。普通管道无法被除了创建它的进程以外的其他进程访问,即仅限于有继承关系的进程(父子、兄弟、爷孙)通信。通常,父进程创建一个管道与通过fork()创建的子进程通信,子进程从其父进程继承了打开的文件。而管道是一种特殊类型的文件(文件的内核缓冲区),因此子进程将从其父进程继承管道(具有继承访问同一个文件的能力)。下图描绘了父子进程的使用普通管道通信的过程:

父进程和子进程在通信开始前要先关闭管道的未使用端,以确保从管道中读取的进程可以在写入程序关闭管道时检测到文件的结尾(
read()
返回0)。一旦进程完成通信并终止,普通管道就不再存在——只要该缓冲区中的数据不向磁盘刷新,数据就存在于进程的生命周期。管道的生命周期<=
进程的生命周期。Linux中可以使用
pipe
创建一个匿名管道:
#include <unistd.h> int pipe(int pipefd[2]);
1
2返回值:创建成功返回0;创建失败返回-1.
pipefd[2] : 代表两个文件描述符,pipefd[0] 代表读端;pipefd[1]代表写端。数据从写端写入管道缓存,直到读端读取才会清除。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int pipefd[2] = {0}; // pipefd[0]为读 ;pipefd[1]为写
pipe(pipefd);
pid_t id = fork();
if(id < 0)
{
printf("fork failed!\n");
exit(-1);
}
else if(id == 0)
{
close(pipefd[0]); // 子进程关闭读端,让子进程写入
long long count = 0;
while(1)
{
// sleep(10); //让子进程后开始,pipe里无字符,这时读端阻塞等待,显示器无输出
write(pipefd[1], "a", 1);
printf("%d\n",count++); //count代表pipe能写入的字符数,
//在CPU调度时间内,count可以自增到这个空间的最容量(不大 64k)
if(count > 65536)
sleep(1);
}
}
close(pipefd[1]); // 父进程关闭写端,让父进程读取
while(1)
{
sleep(10); // 让子进程先运行,可以观察到子进程写入满后会阻塞停止,等待父进程读取。
//父进程读取不会等到把pipe读完,实际读了了一定量的字符后,子进程又可以开始写入
char buf[2000] = {0}; //父进程每次读取2000个字符
size_t s = read(pipefd[0], buf, sizeof(buf) - 1);
if(s > 0)
{
buf[3] = 0; //buf有20000个字符,为打印方便,将buf[3]置'\0'.
printf("Father Received: %s\n", buf);
}
else
break;
sleep(1);
}
return 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
49
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
运行结果
[test@VM-12-4-centos Pipe]$ make
gcc -o main main.c
[test@VM-12-4-centos Pipe]$ ./main
.....
65534
65535 #子进程不断写入,直到写入64Kb时不再写入
Father Received: aaa
Father Received: aaa
Father Received: aaa
65536 # 读完了6000个字符(实际可以小于这个数值,4096 bytes.),可以看到子进程可以继续写入了。
65537
.....#父进程将管道内容读出
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
- 通过修改以上代码父子进程的读写时间,可以得出以下结论:
- 管道是面向字节流的单向通信信道,管道里只要缓冲区没满,就可以一直写入。
- 写端不写入或者写的速度小于读取的速度,读端会阻塞等待
- 实际写入时,若写入条件不满足(如写入缓冲区不够大),则写入端会阻塞——自带同步机制,保证写入读取的原子性。实际上,在POSIX.1-2001中规定, 在执行write()写入的时候,当写入量的大小小于 PIPE_BUF时,写入必须是原子的,此时写入管道的数据是连续的。而写入超过了 PIPE_BUF写入可能是非原子的。POSIX要求PIPE_BUF是512 bytes,Linux实际上为4096 bytes。
- 若写端关闭文件描述符,读端在读取文件后会读到文件结尾,以此来判断是否需要继续读取
- 若读端关闭文件描述符,写端进程在后续会被系统杀掉——通过信号的方式(13号)
# 命名管道
- 命名管道通信可以是双向的,不需要父子关系。一旦建立了命名管道,几个进程就可以使用它进行通信。在通信过程完成后,命名管道仍然存在。虽然FIFO允许双向通信,但只允许半双工传输。如果数据必须双向传输,通常使用两个FIFO。
- 命名管道虽然需要在磁盘创建管道文件(通信的文件通过相应的inode进行读写),但是数据只存在内存中,不会刷新到磁盘,无论写入多少字符其大小都为0 bytes。
[test@VM-12-4-centos Mkfifo]$ mkfifo NamedPipe
[test@VM-12-4-centos Mkfifo]$ ls -l
total 0
prw-rw-r-- 1 test test 0 Jun 5 16:16 NamedPipe
# 一端
[test@VM-12-4-centos Mkfifo]$ while :; do ls > NamedPipe;echo "#############" > NamedPipe; sleep 1; done
# 另一端
[test@VM-12-4-centos Mkfifo]$ while :; do cat NamedPipe; sleep 1; done
NamedPipe
#############
NamedPipe
#############
...
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
命名管道创建的系统调用:
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);
1
2
3mode: 设置FIFO的权限,结果会是<mode & ~umask>。
返回值:创建成功返回0;创建失败返回-1.
写份两个进程通信的例子:
- server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#define FIFOPath "./FIFO"
int main()
{
if(-1 == mkfifo(FIFOPath, 0664))
{
perror("create pipe failed\n");
exit(-1);
}
int fd = open(FIFOPath, O_RDONLY);
if(fd != -1)
{
char buffer[1024] = {0};
while(1)
{
ssize_t ret = read(fd, buffer, sizeof(buffer) - 1); //减1是为了结尾设置'\0'时,防止越界访问
if(ret > 0)
{
buffer[ret] = '\0'; //手动带上结尾
printf("Server Received Data: %s", buffer);
}
else if(ret == 0)
{
printf("Client Already Quit!\n");
break;
}
else
perror("Server Read Failed\n");
}
}
return 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
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
- client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define FIFOPath "./FIFO"
int main()
{
int fd = open(FIFOPath, O_WRONLY);
if(fd != -1)
{
char buffer[1024] = {0};
while(1)
{
// printf("[Your Message]# ");
// fflush(stdout); //不使用fflush且不含'\n',则不能即时打印提示字符串,他在c标准库的缓冲区
write(1, "[Your Message]# ", strlen("[Your Message]# "));
ssize_t ret = read(0, buffer, sizeof(buffer) - 1);
if(ret > 0)
{
buffer[ret] = '\0';
printf("%s",buffer);
//消息回显,注意这里的'\n'也不可少,作用是打印行刷新。但是没写'\n',因为server输入结束时会输入回车。
write(fd, buffer, ret);
}
}
}
return 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
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
- Makefile
.PHONY:all
all:client server
client:client.c
gcc -o $@ $^
server:server.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f client server FIFO
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
运行结果
[test@VM-12-4-centos Mkfifo]$ ls
client client.c FIFO Makefile server server.c
[test@VM-12-4-centos Mkfifo]$ ./client
[Your Message]# hello daddy~
hello daddy~
[test@VM-12-4-centos Mkfifo]$ ./server
Server Received Data: hello daddy~
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 其他
POSIX共享内存:POSIX系统有几种IPC机制,包括共享内存和消息传递。在这里,我们将探讨用于共享内存的POSIX API。POSIX共享内存使用内存映射文件进行组织,这些文件将共享内存区域与文件相关联。进程必须首先使用shm open()系统调用创建共享内存对象,