本章目录
1. 进程间通信介绍
- 目前进程间最大的通信方式:网络!!!
- 由于进程间独立性的存在,两个进程间想要交换数据是非常困难的,所以,我们需要进程通信来解决进程间的数据交换。
- 进程间通信目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知他们发生了某种事件(如进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
- 进程间通信发展
- 管道(分类:匿名管道、命名管道)
- System V进程间通信(System V消息队列、System V 共享内存、System V 信号量)
- POSIX进程间通信(消息队列、共享内存、信号量、互斥量、条件变量、读写锁)
2. 管道介绍
什么是管道?
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
3. 匿名管道
3.1 从命令行感受管道
3.2 从内核角度理解管道
管道就是内核当中的一块缓冲区(一块内存),进程A和进程B可以通过这个缓冲区进行交换数据,匿名管道不具备标识符
3.3 代码创建匿名管道
#include<unistd.h>
功能:创建一无名管道
原型: int pipe(int pipefd[2]);
参数:pipefd:文件描述符数组,其中有两个元素,分别是pipefd[0]和pipefd[1],它们当中保存的是一个文件描述符
pipefd[0]表示读端,不能写可读,pipefd[1]表示写端,不能读可写
(参数为输出型参数)返回值:创建成功返回0,创建失败返回-1.
探究点:pipefd[0]、pipefd[1]当中的值是pipe函数进行赋值的,直白的说,当我们调用pipe函数的时候,只需要给pipe函数传递一个拥有两个元素的整型数据的数组,pipe函数在创建完毕管道之后,会给pipefd[0]、pipefd[1]进行赋值。
下面是代码创建管道的一个演示:
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 int fd[2];
7 int ret=pipe(fd);
8 if(ret<0)
9 {
10 perror("pipe");
11 return 0;
12 }
13 pid_t pid=fork();
14 if(pid<0)
15 {
16 perror("fork");
17 return 0;
18 }
19 else if(pid==0)
20 {
21 //child
22 char buf[1024]={0};
23 read(fd[0],buf,sizeof(buf)-1);
24 printf("child read:%s\n",buf);
25
26 }
27 else
28 {
29 //father
30 write(fd[1],"I am father",11);
31 }
32 }
3.4 从PCB的角度去分析管道
- 匿名管道只适用于具有亲缘关系的进程,例如父进程和子进程。
- 在创建管道时,要先进行创建管道,再创建子进程,这样才能实现进程间通信。
- 那么两个子进程间想要使用匿名管道实现进程间通信可不可以呢?
在理论上是可以的,但是还是要遵循先创建管道,在创建子进程的原则 - 【注意】管道的数据只能从写端流向读端,这是一种半双工的通信方式。这里对比一下全双工的通信方式:数据可以从写端流向读端也可以从写、读端流向写端。
- 通过fd[0]从管道中去读数据的时候,是将数据读走了,管道中将不存在这份数据,并不是拷贝一份。
- 从管道中读取数据的时候,可以指定读取任意大小的数据。如果管道中没有数据,默认情况下,进行读(read),则会堵塞。
- 多次写入的数据之间是没有明显的分界线的,上一条数据的末尾连接下一条数据的开头位置,不能够区分数据是否是分次写入的。
- 匿名管道的生命周期跟随进程。
- 那么我们再来讨论一下默认情况下(并没有对管道创建出来的文件描述符做任何操作)的读写属性
- 如果读端不读,写段一直写,会产生什么情况?
写端将管道写满以后,就会阻塞;管道的大小:65536字节 - 如果写端不写,读端一直读,会产生什么情况?
读端将管道的数据读完之后,再次进行读的时候,就会阻塞
3.5 匿名管道的非阻塞读写特性
- 要了解匿名管道的非阻塞属性,我们认识一个函数——fcntl
fcntl函数:设置/获取文件描述符的属性
int fcntl(int fd,int cmd,.../*arg*/);
fd:文件描述符
cmd: F_GETFL:获取文件描述符的属性(可变参数列表就可以不用传递任何值) eg:fcntl(fd[0],F_GETFL)
F_SETFL:设置文件描述符的属性
可变参数列表为:O_RDONLY(只读)/O_WRONLY(只写)/O_RDWR(可读可写)
O_NONBLOCK(非阻塞属性) 都采用按位或的方式
eg:fcntl(fd[0],F_SETFL,flag0|O_NONBLOCK);
返回值:如果是获取(F_GETFL),返回文件描述符的属性。
- 函数使用演示:
1 #include<stdio.h>
2 //fcntl函数
3 #include<fcntl.h>
4 #include<unistd.h>
5
6 int main()
7 {
8 int fd[2];
9 int ret=pipe(fd);//创建一个管道
10 if(ret<0)
11 {
12 perror("pipe");
13 return 0;
14 }
15
16 int flag0=fcntl(fd[0],F_GETFL);//获取管道属性
17 printf("read fd[0]:flag:%d\n",flag0);
18 int flag1=fcntl(fd[1],F_GETFL);
19 printf("write fd[1]:flag:%d\n",flag1);
20
21 // fcntl(fd[0],F_SETFL,flag0|O_NONBLOCK);//设置管道为非阻塞形式
22 // printf("read fd[0]:flag:%d\n",flag0);
23 // fcntl(fd[1],F_SETFL,flag1|O_NONBLOCK);
24 // printf("write fd[1]:flag:%d\n",flag1);
25
26 return 0;
27 }
这里可以看到fd[0]和fd[1]的属性,分别为0和1,这里再进行讨论一下源码,可以看到O_RDONLY和O_WRONLY的表示为00000000和00000001,这样我们就可以联想到为什么要用或的方式,将特定位置置为1,就可以表示出特定的属性。
- 下面再进行讨论以下两种情况下会发生的情况:
- 在管道读端进行读(非阻塞情况下),写端不写
(1)写端关闭:调用read 函数返回-1
(2)写端不关闭:读端调用read,read函数会返回-1,表示管道当中没有内容 - 在管道写端进行写(非阻塞情况下),读端不读
(1)读端关闭:当前在通过fd[1],往管道中去写的时候,会导致管道破裂,调用写的进程的时候,会被直接终止(终止信号)
(2)读端不关闭:write返回-1,errno为EAGAIN,表示管道写满了。
- 在管道读端进行读(非阻塞情况下),写端不写
- 代码验证以上两种情况:
- 写端关闭,读端进行读
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<fcntl.h>
4 #include<errno.h>
5
6 void SetNonBlock(int fd)
7 {
8 int flag=fcntl(fd,F_GETFL);
9 fcntl(fd,F_SETFL,flag|O_NONBLOCK);
10 }
11
12 int main()
13 {
14 int fd[2];
15 int ret=pipe(fd);
16 if(ret<0)
17 {
18 perror("pipe");
19 return 0;
20 }
21
22 ret=fork();
23 if(ret<0)
24 {
25 perror("fork");
26 return 0;
27 }
28 else if(ret==0)
29 {
30 //child
31 close(fd[0]);
32 close(fd[1]);
33 while(1)
34 {
35 sleep(1);
36 }
37 }
38 else
39 {
40 //father
41 close(fd[1]);
42 SetNonBlock(fd[0]);
43
44 char buf[1024]={0};
45 int read_size=read(fd[0],buf,sizeof(buf)-1);
46 printf("read_size:%d,buf:%s\n",read_size,buf);
47 }
48 return 0;
49
50 }
- 写端不关闭,读端进行读
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<fcntl.h>
4 #include<errno.h>
5
6 void SetNonBlock(int fd)
7 {
8 int flag=fcntl(fd,F_GETFL);
9 fcntl(fd,F_SETFL,flag|O_NONBLOCK);
10 }
11
12 int main()
13 {
14 int fd[2];
15 int ret=pipe(fd);
16 if(ret<0)
17 {
18 perror("pipe");
19 return 0;
20 }
21
22 ret=fork();
23 if(ret<0)
24 {
25 perror("fork");
26 return 0;
27 }
28 else if(ret==0)
29 {
30 //child
31 close(fd[0]);
32 SetNonBlock(fd[1]);
33 //写端不关闭
34 while(1)
35 {
36 sleep(1);
37 }
38 }
39 else
40 {
41 //father
42 close(fd[1]);
43 SetNonBlock(fd[0]);
44
45 char buf[1024]={0};
46 int read_size=read(fd[0],buf,sizeof(buf)-1);
47 if(read_size<0)
48 {
49 if(errno==EAGAIN)
50 {
51 printf("管道为空\n");
52 printf("read_size:%d,buf %s\n",read_size,buf);
53 }
54 }
55 }
56 return 0;
57
58 }
- 读端关闭,写端进行写
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<fcntl.h>
4 #include<errno.h>
5
6 void SetNonBlock(int fd)
7 {
8 int flag=fcntl(fd,F_GETFL);
9 fcntl(fd,F_SETFL,flag|O_NONBLOCK);
10 }
11
12 int main()
13 {
14 int fd[2];
15 int ret=pipe(fd);
16 if(ret<0)
17 {
18 perror("pipe");
19 return 0;
20 }
21
22 ret=fork();
23 if(ret<0)
24 {
25 perror("fork");
26 return 0;
27 }
28 else if(ret==0)
29 {
30 //child
31 close(fd[0]);
32 SetNonBlock(fd[1]);
33 int count=0;
34 while(1)
35 {
36 int write_size=write(fd[1],"a",1);
37 if(write_size<0)
38 {
39 printf("write_size:%d\n",write_size);
40 if(errno==EAGAIN)
41 {
42 //将管道写满了
43 break;
44 }
45 }
46 printf("count:%d\n",count++);
47 }
48
49 while(1)
50 {
51 sleep(1);
52 }
53 }
54 else
55 {
56 //father
57 close(fd[1]);
58 close(fd[0]);
59 while(1)
60 {
61 sleep(1);
62 }
63 }
64 return 0;
65
66 }
- 读端不关闭,写端进行写
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<fcntl.h>
4 #include<errno.h>
5
6 void SetNonBlock(int fd)
7 {
8 int flag=fcntl(fd,F_GETFL);
9 fcntl(fd,F_SETFL,flag|O_NONBLOCK);
10 }
11
12 int main()
13 {
14 int fd[2];
15 int ret=pipe(fd);
16 if(ret<0)
17 {
18 perror("pipe");
19 return 0;
20 }
21
22 ret=fork();
23 if(ret<0)
24 {
25 perror("fork");
26 return 0;
27 }
28 else if(ret==0)
29 {
30 //child
31 close(fd[0]);
32 SetNonBlock(fd[1]);
33 int count=0;
34 while(1)
35 {
36 int write_size=write(fd[1],"a",1);
37 if(write_size<0)
38 {
39 printf("write_size:%d\n",write_size);
40 if(errno==EAGAIN)
41 {
42 //将管道写满了
43 break;
44 }
45 }
46 printf("count:%d\n",count++);
47 }
48
49 while(1)
50 {
51 sleep(1);
52 }
53 }
54 else
55 {
56 //father
57 close(fd[1]);
58 //close(fd[0]);
59 while(1)
60 {
61 sleep(1);
62 }
63 }
64 return 0;
65
66 }
代码验证结果如图所示:
4. 命名管道
4.1 命名管道介绍
命名管道也是在内核当中开辟了一块缓冲区,这块缓冲区具有标识符,可以被任何进程通过标识符去找到。
4.2 命名管道创建
- 创建命令:mkfifo
- 函数创建命名管道
(1)创建管道
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/shm.h>
4
5 #define key 0x23232323
6
7 int main()
8 {
9 int shmid=shmget(key,1024,IPC_CREAT|0664);
10 if(shmid<0)
11 {
12 perror("shmget");
13 return 0;
14 }
15 return 0;
16 }
(2)创建A_write.c,向管道中写入"I am process A"
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<fcntl.h>
4
5 int main()
6 {
7 int fd=open("_shm.c",O_RDWR);
8 if(fd<0)
9 {
10 perror("open");
11 return 0;
12 }
13
14 while(1)
15 {
16 write(fd,"I am process A",14);
17 sleep(1);
18 }
19 close(fd);
20 return 0;
21
22 }
(2)创建B_read.c,从管道中读出数据
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<fcntl.h>
4
5 int main()
6 {
7 int fd=open ("_shm.c",O_RDWR);
8 if(fd<0)
9 {
10 perror("open");
11 return 0;
12 }
13 while(1)
14 {
15 char buf[1024]={0};
16 read(fd,buf,sizeof(buf)-1);
17
18 printf("buf: %s\n",buf);
19 sleep(1);
20 }
21 close(fd);
22 return 0;
23 }
4.3 命名管道特性
- 命名管道的生命周期也是跟随进程
- 命名管道有标识符,所以,命名管道支持不同进程之间的进程通信
- 其他特性和匿名管道一样。