一、进程间通信
进程间通信的本质
它的本质就是让不同的进程看到一份公共的资源(内存的一段内存区域),该资源只能由第三方提供,即操作系统直接或者间接提供。
进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
二、管道
1.什么是管道
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
2.管道的两种类型
- 匿名管道pipe
- 命名管道FIFO
三、匿名管道
#include <unistd.h>
功能:创建无名管道
int pipe(int fd[2]);
参数:
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
1.用fork来共享管道原理
父进程创建一个管道,然后fork出子进程,接下来关闭管道两头各自不用的读端或者写端。如图所示:
关掉不用的描述符后,一个保留写端,一个保留读端,这样就实现了父进程与子进程之间的通信。
2.站在文件描述符角度-深度理解管道
Linux下一切皆文件,所以管道也是文件。父进程调用pipe创建管道时,其实就是创建了一个文件。此时文件描述符表除了默认的三个标准输入、标准输出、标准错误之外,又多了两个文件描述符。它们是管道的读端fd[0]和写端fd[1]。(读端对应0,像一个张开的嘴巴,写端对应1,像一支笔。这样就不会搞混了)
父进程fork出子进程,子进程与父进程共享代码,数据各自私有一份。所以父进程打开的文件子进程同样看得到。这样一来,二者的文件描述符表中也都有了读端fd[0]和写端fd[1],也保证了父子进程看到了一份公共的资源---->管道。
接下来我们可以根据自己的需要关闭相应的描述符就可以了。假如此时我们让子进程来读,父进程去写,那么我们关闭父进程的fd[0],关闭子进程的fd[1],这样就可以实现父进程和子进程之间的通信了。
3.匿名管道的五个特点.
- 只允许单向通信(父-->子或者子-->父),若想双向通信,可以利用两个管道。
- 只能用于具有血缘关系的进程间通信,常用于父子进程。
- 管道的生命周期随通信双方的进程。打开的文件在进程结束时自动关闭,管道也是文件,生命周期随进程。
- 自带同步与互斥机制
- 面向字节流,提供流式服务
这里还需要引入五个概念:
1>临界资源:多个进程看到的一份公共资源
2>临界区:访问临界资源的那部分区域
3>互斥:在任何一个时刻,只允许有一个进程进入临界资源进行资源访问,在其访问期间,其他进程不得进入访问。
4>同步:在保证安全的条件前提,进程按照特定的顺序访问临界资源。
5>原子性:一件事情,要么做了,要么没做,不会有第三态。
4.匿名管道的四种情况
前提条件:父子进程进行通信,父进程读,子进程写。
① 父进程的读端不读,但是也不关,那么子进程的写端就会一直写,子进程一直写一直写,最后就会把管道写满。此时如果再继续写,就会把以前的数据覆盖掉,而这些数据可能还没有读取。所以基于安全考虑,write调用阻塞,子进程会进行阻塞式等待。其实阻塞式等待就是操作系统不会调度子进程,把子进程的R状态变为非R状态,然后把子进程的PCB放置特定的等待队列中进行等待,等到读端开始读了,操作系统再把子进程唤醒,即把子进程的状态从非R变为R,再将子进程的PCB从等待队列拿到运行队列中。所以当读端不读并且不关的时候,写端写满的时候,写端进程就会阻塞式等待。
② 子进程的写端不写,但是也不关,此时父进程的读端如果检测到管道里还有有效数据,就会继续读。但是因为写端没有关,所以当有效数据被读完的时候,写端随时有可能来写,那么读端就会等写端进行有效数据的写入,也就意味着读端会阻塞式等待。所以当写端不写并且不关的时候,读端读完的时候,读端进程就会阻塞式等待。
③ 子进程的写端写完了不再写入,然后把自己的写端关闭了。那么如果管道里有数据,读端就会将数据读出来。因为写端已经关闭了,那么已经不会再有数据写入了,所以读端读完之后继续等就没有任何意义了,而操作系统绝对不会做浪费资源的事情,所以此时操作系统就会把读端返回。
④ 父进程的读端关闭了,那么写端写也就没有任何意义了,同上,操作系统绝对不会做浪费资源的事情,所以操作系统就会给写端发送13号SIGPIPE信号然后立即kill掉写端。
5.匿名管道的代码实现
输出的结果为:
四、命名管道
命名管道解决了匿名管道的一个最大的问题就是可以实现两个毫不相干的进程间的通信,命名管道是一种特殊类型的文件,我们可以通过命令行来创建一个命名管道,也可以使用mkfifo系统调用函数来创建命名管道,创建出来的是一个管道文件。让一个进程往管道文件里写入数据,另一个进程从该管道文件里读取数据,即可完成两个毫无关系的进程之间的通信。
1.创建命名管道
- 从命令行创建:
$ mkfifo filename
- 从程序里创建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo (const char *filename,mode_t mode);
创建命名管道:
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc , char *argc[])
{
mkfifo("p2",0644);
return 0;
}
2.server&client代码实例
server.c(读端)
1 #include <stdio.h>
2 #include <fcntl.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 int main()
6 {
7 if(mkfifo("./fifo",0644)<0){//调用系统调用接口创建命名管道,若创建失败,输出信息!
8 printf("mkfifo error!\n");
9 return 1;
10 }
11 int fd = open("./fifo",O_RDONLY);//用只读权限打开这个管道文件
12 if(fd<0){ //打开失败
13 perror("read");
14 return 2;
15 }
16
17 char buf[1024];
18 while(1){
19 ssize_t s = read(fd,buf,sizeof(buf)-1);//读文件
20 if(s>0){
21 buf[s]=0;//读取成功
22 printf("client# %s\n",buf);//输出
23 }else if(s==0){
24 printf("client quit,server quit too!\n");//读取结束
25 break;
26 }else{//读取出错
27 }
28 }
29 close(fd);//使用结束,关闭文件
30 return 0;
31 }
~
client.c(写端)
1 #include <stdio.h>
2 #include <string.h>
3 #include <fcntl.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 int main()
7 {
8 int fd = open("./fifo",O_WRONLY);//打开这个管道文件
9 if(fd<0){
10 perror("read");
11 return 2;
12 }
13
14 char buf[1024];
15 while(1){
16 printf("Please Enter:");//从标准输入中写内容
17 scanf("%s",buf);
18 write(fd,buf,strlen(buf));//写文件
19 }
20 close(fd);//使用完毕,关闭文件
21 return 0;
22 }
输出结果:我们可以看到,读端发出的消息,写端全部收到了,写端写什么,读端就读什么!!!
并且当我的读端关闭不在读数据时,写端同时退出也有信息:
五、匿名管道和命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- 匿名管道适合具有血缘关系的进程间通信,常用于父子进程
- 命名管道可以用于两个毫无关系的进程之间