进程间通信📮
1. 进程间通信相关概念💩
1.1 什么是进程间通信
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能 相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,interProcess Communication)
1.2 进程间通信的方式
文件、管道、信号、共享内存、消息队列、套接字、命名管道等
常用的进程通信方式:
- 管道(使用最简单)
- 信号(开销最小)
- 共享映射区(无血缘关系)
- 本地套接字(最稳定)
2. 管道pipe🔌
2.1 管道的概念
管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道
- 管道的本质是一块内核缓冲区
- 由两个文件描述符引用,一个表示读端,一个表示写端
- 规定数据从管道的写端流入管道,从读端流出
- 当两个进程都终结的时候,管道也自动消失
- 管道的读端和写端默认都是阻塞的
2.2 管道的原理
- 管道的实质是内核缓冲区,内部使用环形队列实现
- 默认缓冲区大小为4K,可以使用
ulimit -a
命令获取大小 - 实际操作过程中缓冲区会根据数据压力做适当调整
2.3管道的局限性
- 数据一旦被读走,便不在管道中存在,不可反复读取
- 数据只能在一个方向流动,若要实现双向流动,必须使用两个管道
- 只能在有血缘关系的进程间使用管道
2.4 创建管道pipe函数
#include <unistd.h>
- 函数作用:创建一个管道
- 函数原型:
int pipe(int fildes[2]);
- 函数参数:
fildes[0]
存放管道的读端,fildes[1]
存放管道的写端 - 返回值:
- 成功返回 0
- 失败返回 -1,并设置
errno
值
2.5 父子进程使用管道通信
- 父进程创建管道
- 父进程 fork 子进程
- 父进程关闭
fildes[0/1]
,子进程关闭fildes[1/0]
1 //pipe函数测试
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<unistd.h>
7 #include<sys/wait.h>
8
9 int main()
10 {
11 //创建管道
12 int fd[2];
13 int ret = pipe(fd);
14 if (ret<0)
15 {
16 perror("pipe error");
17 return -1;
18 }
19 //创建子进程
20 pid_t pid = fork();
21 if (pid<0)
22 {
23 perror("fork error");
24 return -1;
25 }
26 else if (pid>0) //父进程
27 {
28 //关闭读端
29 close(fd[0]);
30 sleep(5);
31 write(fd[1], "hello world", strlen("hello world"));
32 wait(NULL);
33 }
34 else if (pid==0) //子进程
35 {
36 //关闭写端
37 close(fd[1]);
38 char buf[64];
39 memset(buf, 0x00, sizeof(buf));
40 int n = read(fd[0], buf, sizeof(buf));
41 printf("read over,n==[%d],buf==[%s]\n",n,buf);
42 }
43 return 0;
44 }
>>>>执行结果
read over,n==[11],buf==[hello world]
2.6 管道练习
【💊】父子进程间通信实现 ps aus | grep bash
- 创建管道pipe
- 创建子进程fork
- 在父进程中关闭读端
fd[0]
- 在子进程中关闭写端
fd[1]
- 在父进程中将标准输出重新定向到管道的写端
- 在子进程中将标准输入重新定向到管道的读端
- 在父进程调用execl函数执行
ps aux
命令 - 在子进程调用execl函数执行
grep bash
命令 - 在父进程中回收子进程wait函数
1 //父子进程管道模拟ps aux | grep bash
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7 #include<unistd.h>
8
9 int main()
10 {
11 //创建管道
12 int fd[2];
13 int ret = pipe(fd);
14 if (ret<0)
15 {
16 perror("pipe error");
17 return -1;
18 }
19 //创建子进程
20 pid_t pid = fork();
21 if (pid<0)
22 {
23 perror("fork error");
24 return -1;
25 }
26 else if (pid>0) //父进程
27 {
28 close(fd[0]);
29 dup2(fd[1], STDOUT_FILENO);
30 execl("/usr/bin/ps", "ps", "aux", NULL);
31 perror("execl error");
32 wait(NULL);
33 }
34 else if (pid==0) //子进程
35 {
36 close(fd[1]);
37 dup2(fd[0], STDIN_FILENO);
38 execl("/usr/bin/grep", "grep", "--color==auto", "bash", NULL);
39 perror("execl error");
40 }
41 return 0;
42 }
【💊】兄弟进程间通信实现 ps aus | grep bash
1 //兄弟进程管道模拟ps aux | grep bash
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7 #include<unistd.h>
8
9 int main()
10 {
11 //创建管道
12 int fd[2];
13 int ret = pipe(fd);
14 if (ret<0)
15 {
16 perror("pipe error");
17 return -1;
18 }
19 //创建子进程
20 int i = 0;
21 for(i=0;i<2;i++)
22 {
23 pid_t pid = fork();
24 if (pid<0)
25 {
26 perror("fork error");
27 return -1;
28 }
29 else if (pid==0) //子进程
30 {
31 break;
32 }
33 }
34 if (i==0)
35 {
36 close(fd[0]);
37 dup2(fd[1], STDOUT_FILENO);
38 execl("/usr/bin/ps", "ps", "aux", NULL);
39 perror("execl error");
40 }
41 if (i==1)
42 {
43 close(fd[1]);
44 dup2(fd[0], STDIN_FILENO);
45 execl("/usr/bin/grep", "grep", "--color=auto", "bash", NULL);
46 perror("execl error");
47 }
48 if (i==2)
49 {
50 close(fd[0]);
51 close(fd[1]);
52 while(1)
53 {
54 pid_t wpid = waitpid(-1, NULL, WNOHANG);
55 if (wpid==-1)
56 {
57 break;
58 }
59 }
60 }
61 return 0;
62 }
2.7 管道的读写行为
- 读操作
- 有数据
read
正常读,返回读出的字节数 - 无数据
- 写端全部关闭
read
解除阻塞,返回 0 ,相当于读文件读到了尾部 - 没有全部关闭
read
阻塞
- 写端全部关闭
- 有数据
- 写操作
- 读端全部关闭
管道破裂,进程终止,内核给当前进程发SIGPIPE
信号 - 读端没有全部关闭
- 缓冲区写满了
write
阻塞 - 缓冲区没有满
继续write
- 缓冲区写满了
- 读端全部关闭
2.8 设置管道为非阻塞
默认管道两端阻塞,设置读端非阻塞方法:
int flags = fcntl(fd[0],F_GETFL,0);
flag |= O_NONBLOCK;
fcntl(fd[0],F_SETFL,flags);
若是读端设置为非阻塞:
- 写端没有关闭,管道中没有数据可读,则
read
返回 -1 - 写端没有关闭,管道中有数据可读,则
read
返回实际读到的字节数 - 写端已经关闭,管道中有数据可读,则
read
返回实际读到的字节数 - 写端已经关闭,管道中没有数据可读,则
read
返回 0
2.9 查看管道缓冲区大小
- 命令
ulimit -a
- 函数
long fpathconf(int fd,int name);
printf("pipe size==[%d]\n", fpathconf(fd[0], _PC_PIPE_BUF));
printf("pipe size==[%d]\n", fpathconf(fd[1], _PC_PIPE_BUF));
3. FIFO💉
3.1 FIFO介绍
FIFO 常被称为命名管道,以区分管道(pipe)。pipe只能用于有血缘关系的进程间通信。而FIFO可在不相关的进程间交换数据。
FIFO是LInux基础文件类型中的一种(文件类型为 p)。但FIFO文件在磁盘上没有数据块,文件大小为 0,仅仅用来标识内核中一条通道。进程可以打开着个文件进行read/write,实际是在读写内核缓冲区,这样就实现了进程间通信。
3.2 创建管道
- 方式1️⃣ 使用命令
mkfifo 管道名
- 方式2️⃣ 使用函数
int mkfifo(const char *pathname,mode_t mode);
【📢】 FIFO
文件IO函数,均适用(除 lseek()
等文件定位操作)
严格遵循先进先出,对FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾
[xfk@centos FIFODIR]$ mkfifo fifo
[xfk@centos FIFODIR]$ ls -l
总用量 0
prw-rw-r--. 1 xfk xfk 0 1月 19 15:39 fifo
3.3 FIFO进程通信
- 进程A:
- 创建FIFO文件,代码中使用函数,终端使用shell命令
- open FIFO文件,获得一个文件描述符fd
- 写FIFO文件
write(fd,"xxx",...)
- 关闭FIFO文件
close(fd);
- 进程B:
- open FIFO文件,获得一个文件描述符fd
- 读FIFO文件
read(fd,buf,sizeof(buf))
- 关闭FIFO文件
close(fd);
1 //fifo进程通信测试 进程A
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<unistd.h>
7 #include<sys/stat.h>
8 #include<fcntl.h>
9
10 int main()
11 {
12 //创建fifo文件
13 int ret = access("./myfifo", F_OK); //判断文件是否存在
14 if (ret!=0)
15 {
16 int ret = mkfifo("./myfifo", 0777);
17 if (ret<0)
18 {
19 perror("mkfifo error");
20 return -1;
21 }
22 }
23 //打开文件
24 int fd = open("./myfifo", O_RDWR);
25 if (fd<0)
26 {
27 perror("open error");
28 return -1;
29 }
30 //写fifo文件
31 int n = 0;
32 char buf[64];
33 while(1)
34 {
35 memset(buf, 0x00, sizeof(buf));
36 sprintf(buf, "%d:%s", n, "hello world");
37 write(fd, buf, strlen(buf));
38 sleep(1);
39 n++;
40 }
41 //关闭文件
42 close(fd);
43 //getchar();
44 return 0;
45 }
1 //fifo进程通信测试 进程B
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<unistd.h>
7 #include<sys/stat.h>
8 #include<fcntl.h>
9
10 int main()
11 {
12 //打开文件
13 int fd = open("./myfifo", O_RDWR);
14 if (fd<0)
15 {
16 perror("open error");
17 return -1;
18 }
19 //读fifo文件
20 int n;
21 char buf[64];
22 while(1)
23 {
24 memset(buf, 0x00, sizeof(buf));
25 n = read(fd, buf, sizeof(buf));
26 printf("n==[%d],buf==[%s]\n", n, buf);
27 }
28 //关闭文件
29 close(fd);
30 //getchar();
31 return 0;
32 }
>>>>执行结果 进程B
n==[13],buf==[0:hello world]
n==[13],buf==[1:hello world]
n==[13],buf==[2:hello world]
n==[13],buf==[3:hello world]
n==[13],buf==[4:hello world]
n==[13],buf==[5:hello world]
n==[13],buf==[6:hello world]
n==[13],buf==[7:hello world]
n==[13],buf==[8:hello world]
n==[13],buf==[9:hello world]
n==[14],buf==[10:hello world]
n==[14],buf==[11:hello world]
...
【📢】先运行read进程,阻塞;再运行write进程
4. 内存映射区🍁
4.1 存储映射区介绍
存储映射I/O(Memory-mapped I/O)使一个磁盘文件与储存空间中的一个缓冲区相映射。从缓冲区中读取数据,就相当于读文件中的相应字节;将数据写入缓存区,则会将数据写入文件。这样,就可以在不使用read和write函数的情况下,使用地址(指针)完成I/O操作
【🎫】使用存储映射,首先应该通知内核,将一个指定文件映射到存储区域中。
映射工作可由 mmap
函数实现
4.2 mmap函数
#include<sys/mman.h>
- 函数作用:建立存储映射区
- 函数原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- 函数参数:
addr
指定映射的起始地址,通常设为NULL,由系统指定length
映射到内存的文件长度
lseek
或stat
函数prot
映射区的保护方式- 读:
PROT_READ
- 写:
PROT_WRITE
- 读写:
PROT_READ | PROT_WRITE
- 读:
flags
映射区的特性MAP_SHARED
写入映射区的数据会写回文件,且允许其他映射该文件的进程共享MAP_PRIVATE
对映射区的写入操作会产生一个映射区的复制(copy-on-write),对此区域所做的修改不会写回原文件
fd
由open反悔的文件描述符,代表要映射的文件offset
以文件开始处的偏移量,必须是4k的整数倍,通常为0,表示从文件头开始映射
- 返回值:
- 成功:返回创建的映射区首地址
- 失败:
MAP_FAILED
宏
【🎫】匿名映射
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
MAP_SHARED|MAP_ANONYMOUS
配合使用,fd
指定为 -1
只能用于有血缘关系的进程间通信
【💊】父子进程共享映射区通信
1 //mmap函数测试父子进程
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<unistd.h>
7 #include<sys/stat.h>
8 #include<fcntl.h>
9 #include<sys/wait.h>
10 #include<sys/mman.h>
11
12 int main()
13 {
14 //打开文件
15 int fd = open("./test.log", O_RDWR);
16 if (fd<0)
17 {
18 perror("open error");
19 return -1;
20 }
21 int len = lseek(fd, 0, SEEK_END);
22 //使用mmap函数建立共享映射区
23 void *addr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
24 if (addr==MAP_FAILED)
25 {
26 perror("mmap error");
27 return -1;
28 }
29 //创建子进程
30 pid_t pid = fork();
31 if (pid<0)
32 {
33 perror("fork error");
34 return -1;
35 }
36 else if (pid>0) //父进程
37 {
38 memcpy(addr, "hello world", strlen("hello world"));
39 wait(NULL);
40 }
41 else if (pid==0) //子进程
42 {
43 sleep(1);
44 char *p = (char *)addr;
45 printf("[%s]", p);
46 }
47 return 0;
48 }
【💊】AB进程(不相关)共享映射区通信
1 //mmap函数测试 进程A:write
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<unistd.h>
7 #include<sys/stat.h>
8 #include<fcntl.h>
9 #include<sys/wait.h>
10 #include<sys/mman.h>
11
12 int main()
13 {
14 //打开文件
15 int fd = open("./test.log", O_RDWR);
16 if (fd<0)
17 {
18 perror("open error");
19 return -1;
20 }
21 int len = lseek(fd, 0, SEEK_END);
22 //使用mmap函数建立共享映射区
23 void *addr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
24 if (addr==MAP_FAILED)
25 {
26 perror("mmap error");
27 return -1;
28 }
29 memcpy(addr, "0123456789", 10);
30 return 0;
31 }
1 //mmap函数测试 进程B:read
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<unistd.h>
7 #include<sys/stat.h>
8 #include<fcntl.h>
9 #include<sys/wait.h>
10 #include<sys/mman.h>
11
12 int main()
13 {
14 //打开文件
15 int fd = open("./test.log", O_RDWR);
16 if (fd<0)
17 {
18 perror("open error");
19 return -1;
20 }
21 int len = lseek(fd, 0, SEEK_END);
22 //使用mmap函数建立共享映射区
23 void *addr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
24 if (addr==MAP_FAILED)
25 {
26 perror("mmap error");
27 return -1;
28 }
29 char buf[64];
30 memset(buf, 0x00, sizeof(buf));
31 memcpy(buf, addr, 10);
32 printf("buf==[%s]\n",buf);
33 return 0;
34 }
【📢】注意事项:
- 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区
- 映射区的释放与文件关闭无关,只要 映射建立成功,文件可以立即关闭
- 当映射文件大小为0, 不能创建映射区
- 文件的偏移量必须为 0 或 4K 的整数倍
- mmap创建映射区出错概率非常高,一定要检查返回值,确保成功再进行后续操作
4.3 munmap函数
#include<sys/mman.h>
- 函数作用:释放由munmap函数建立的存储映射区
- 函数原型:
int munmap(void *addr, size_t length);
- 函数参数:
addr
调用mmap函数成功返回的映射区首地址length
映射区大小(mmap函数的第二个参数)
【📢】munmap传入的地址一定是mmap的返回地址
✍️ 小方块xfk
📅 写于 2023年1月