进程间通信
进程间通信的目的:
- 数据传输:一个进程需要将它的数据发生给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发生消息,通知他发生了某种事件(如进程终止时要通知父进程)
- 进程控制:有些进程行为完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时找到人的状态变化
相关名词解释:
- 数据不一致:读写双方共同访问资源,写端还未写完,但是读端已读,此时读到的数据可能就是错误的
- 临界资源区:来个毫不相干的进程看到的公共资源
- 临界区:访问临界资源的代码
- 互斥:任一时刻,在临界区访问临界资源的,而且仅有它一个
- 同步:在互斥的访问临界资源的基础上,具有顺序性
- 原子性:要么不做,要么做完,没有中间状态
管道
什么是管道?
- 管道是Linux中最古老的进程间通信的形式
- 进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中看不到,所以进程间交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从缓冲区把数据拿走
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
匿名管道:
函数原型:
#include<unistd.h>
int pipe(int fd[2]);
- 函数功能:创建一个无名管道
- 参数:fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
- 返回值:成功返回0,失败返回错误代码
调用pipe函数时在内核中开辟一块缓冲区(称为管道),用于用户之间通信,它有一个读端,一个写端,然后通过filedes函数传给用户程序。
俩个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端,所以管道在用户程序看起来像一个打开的文件。通过read(filedes[0])或者write(fildes[1])向这个文件读写数据,其实就是在读写内核缓冲区
管道建立过程:
- 父进程调用pipe开辟管道,得到俩个文件描述符,指向管道的俩端
- 父进程调用fork函数创建子进程,那么子进程也有俩个文件描述符指向同一个管道
- 父进程关闭管道读端,子进程关闭管道写端,父进程可以向里面写入数据,子进程可以从里面读取数据
管道的特征:
- 只能实现单项通信,父进程写,子进程读。若要实现双向通信,可以建立俩个管道
- 管道之间必须有血缘关系(命名管道可以实现任意俩个进程之间的通信,不用管是否有血缘关系)
- 管道的生命周期随进程,进程退出,管道释放
- 管道自带互斥与同步机制(不用担心若俩个进程同时向管道里写入数据而产生的数据的不一致等问题)
- 管道提供面向字节流的服务(不用将管道内的数据全部读完,想读多少字节完全由上层应用决定)
从文件描述符角度来理解管道:
1.父进程创建管道
2.子进程fork出子进程
3.父进程关闭fd[0],子进程关闭fd[1]
在内核角度了解管道本质:
看待管道应该向看待文件一样,在7向管道读写数据时,可以调用write,read函数,迎合了“Linux下一切皆文件的思想”
管道的读写规则:
- 管道内无数据可读时:读端一直阻塞式等待,直到有数据为止
- 管道满时:写端阻塞,直到数据被读走
- 写端写入一定数据后不再写入,且关闭;读端读完管道内的数据后(读完管道内数据时,会返回一个0值,表示读到结尾),通信才算完成,管道才关闭
- 若写端一直在写,但读端不读且关闭,写端会被OS发送13号信号中止
匿名管道的实现:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5 int main()
6 {
7 int fd[2];
8 int ret = pipe(fd);
9 if(ret<0)
10 {
11 perror("pipe");
12 return 1;
13 }
14
15 pid_t id = fork();
16 if(id==0)
17 {
18 close(fd[0]);
19 const char* msg = "hello,i am child\n";
20 while(1)
21 {
22 sleep(1);
23 write(fd[1],msg,strlen(msg));
24 }
25 }
26 else
27 {
28 close(fd[1]);
29 char buf[64];
30 while(1)
31 {
32 size_t s = read(fd[0],buf,sizeof(buf)-1);
33 if(s>0)
34 {
35 buf[s]='\0';
36 printf("f say :%s\n",buf);
37 }
38 }
39 }
40 return 0;
41 }
运行结果如下:
命名管道:
- 匿名管道只能在有血缘关系(父子,兄弟,爷孙…)的进程间实现通信
- 如果我们想在任意俩个进程实现通信,可以使用FIFO文件来实现。它经常被称为命名管道。
- 命名管道同样是一种特殊的文件类型
FIFO不同于管道之处在于它提供一个路径名与之关联,以fifo的文件形式存储于文件系统中,命名管道是一个设备文件,因此,即使进程和创建FIFO的基础之间不存在亲缘关系,只有可以访问路径,就能够通过FIFO相互通信,FIFO按照先进先出的原则工作.
创建一个命名管道:
- 命名管道可以在令行上创建
- 命名管道可以通过程序创建:
函数原型:
#include<sys/stat.h>
int mkfifo(const char *filename,mode_t mode);
参数:
- path为创建的命名管道的全路路径
- mode为创建的命名管道的模式,指明其存取权限
返回值:成功返回0,失败返回-1
匿名管道和命名管道的区别:
- 匿名管道由pipe函数创建并打开
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)和pipe(匿名管道)之间唯一的区别在他们创建与打开的方式不同,一旦这些工作完成之后,他们具有相同的予以
命名管道打开规则:
- 若当前打开方式为读而打开FIFO,阻塞到由进程为写打开FIFO
- 若当前打开方式为写打开FIFO,阻塞到有进程为读打开FIFO
命名管道实现
1.读取文件txt内容,写入管道myfifo
1 #include<stdio.h>
2 #include<sys/stat.h>
3 #include<sys/types.h>
4 #include<fcntl.h>
5
6 int main()
7 {
8 mkfifo("myfifo",0644);
9
10 int infd = open("txt",O_RDONLY);
11 if(infd==-1)
12 {
13 perror("open");
14 return;
15 }
16
17 int outfd = open("myfifo",O_WRONLY);
18 if(outfd==-1)
19 {
20 perror("open");
21 return;
22 }
23
24 char buf[64];
25 size_t s;
26 while((s = read(infd,buf,sizeof(buf)))>0)
27 {
28 write(outfd,buf,s);
29 }
30 close(infd);
31 close(outfd);
32 return 0;
33 }
2.读取管道,将数据写到txt1文件中
1 #include<stdio.h>
2 #include<sys/stat.h>
3 #include<sys/types.h>
4 #include<fcntl.h>
5
6 int main()
7 {
8 mkfifo("myfifo",644);
9
10 int outfd = open("txt1",O_WRONLY | O_CREAT | O_TRUNC,0664);
11
12 if(outfd == -1)
13 {
14 perror("open");
15 return;
16 }
17
18 int infd = open("myfifo",O_RDONLY);
19 if(infd == -1)
20 {
21 perror("open");
22 return;
23 }
24
25 char buf[64];
26 size_t s;
27 while((s = read(infd,buf,sizeof(buf)))>0)
28 {
29 write(outfd,buf,s);
30 }
31 close(infd);
32 close(outfd);
33 unlink("myfifo");
34 return 0;
35 }