一、进程间通信的介绍
1.进程间通信的概念
进程通信(Interprocess communication),简称:IPC;
本来进程之间是相互独立的。但是由于不同的进程之间可能要共享某些信息,所以就必须要有通讯来实现进程间的互斥和同步。比如说共享同一块内存、管道、消息队列、信号量等等就是实现这一过程的手段,相当于移动公司在打电话的作用。
2.进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
3.进程间通信的前提
进程间通信的前提本质:由操作系统参与,提供一份所有通信进行都能看到的公共资源;两个或多个进程相互通信,必须先看到一份公共的资源,这里的所谓的资源是属于操作系统的,就是一段内存(可能以文件的方式提供、可能以队列的方式提供,也有可能提供的就是原始内存块),这也就是通信方式有很多种的原因;
4.进程间通信的分类
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存(重点介绍)
- System V 信号量
POSIX IPC(本次不做介绍)
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
二、管道
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
通过管道我们查看test.c文件写了多少行代码。其中cat和wc是两个命令,运行起来也就是进程,cat test.c 进程将查看内容通过管道交给了下一个进程wc -l 来计算代码行数;
三、匿名管道
1.基本原理
匿名管道用于进程间通信,且仅限于父子进程之间的通信。
我们知道进程的PCB中包含了一个指针数组 struct file_struct,它是用来描述并组织文件的。父进程和子进程均有这个指针数组,因为子进程是父进程的模板,其代码和数据是一样的;
打开一个文件时,其实是将文件加载到内核中,内核将会以结构体(struct file)的形式将文件的相关属性、文件操作的指针集合(即对应的底层IO设备的调用方法)等;
当父进程进行数据写入时(例如:写入“hello Linux”),数据是先被写入到用户级缓冲区,经由系统调用函数,又写入到了内核缓冲区,在进程结束或其他的操作下才被写到了对应的设备中;
如果数据在写入设备之前,“hello Linux”是在内核缓冲区的,因为子进程和父进程是同时指向这个文件的,所以子进程是能够看到这个数据的,并且可以对其操作;
简单来说,父进程向文件写入数据时,不直接写入对应的设备中,而是将数据暂存在内核缓冲区中,交给子进程来处理;
所以这种基于文件的方式就叫做管道;
2.管道的创建步骤
在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
匿名管道属于单向通信,意味着父子进程只有一个端是打开的,实现父子通信的时候就需要根据自己的想要实现的情况,关闭对应的文件描述符;
1.pipe函数
#include <unistd.h> int pipe(int pipefd[2]);
函数的参数是两个文件的描述符,是输出型参数:
- pipefd[0]:读管道 --- 对应的文件描述符是3
- pipefd[1]:写管道 --- 对应的文件描述符是4
返回值:成功返回0,失败返回-1;
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> int main() { int pipefd[2] = {0}; if(pipe(pipefd) != 0){ perror("pipe error!"); return 1; } //pipefd[0]:读取段 pipefd[1]:写入端 printf("pipefd[0]:%d\n",pipefd[0]);//3 printf("pipefd[1]:%d\n",pipefd[1]);//4 return 0; }
2.代码实战
接下来我们来实现子进程写入数据,父进程读取数据;那么我们就需要针对父子进程关闭对应的文件描述符fd,子进程关闭读端,父进程关闭写端;
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> //让子进程sleep int main() { int pipefd[2] = {0}; if(pipe(pipefd) != 0){ //创建匿名管道 perror("pipe error!"); return 1; } //pipefd[0]:读取端 pipefd[1]:写入端 printf("pipefd[0]:%d\n",pipefd[0]);//3 printf("pipefd[1]:%d\n",pipefd[1]);//4 if(fork() == 0){ //子进程---写入 close(pipefd[0]); //关闭子进程的读取端 const char* msg = "hello-linux!"; while(1){ write(pipefd[1], msg, strlen(msg)); //子进程不断的写数据 sleep(1); } exit(0); } //父进程---读取 close(pipefd[1]); //关闭父进程的写入端 char buffer[64] = {0}; while(1){ //如果read返回值是0,就意味着子进程关闭文件描述符了 ssize_t s = read(pipefd[0], buffer, sizeof(buffer)); //父进程不断的读数据 if(s == 0){ break; } else if(s > 0){ buffer[s] = 0; printf("child say to father:%s\n",buffer); } else{ break; } } return 0; }
3.管道的五个特点和四种情况
五个特点:
- 管道是一个只能单向通信的通信信道,仅限于父子间通信
- 管道提供流式服务
- 管道操作自带同步与互斥机制
- 进程退出,管道释放,所以管道的生命周期随进程
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
四种情况:
- 读端不读或者读的慢,写端要等待读端;
- 读端关闭,写端收到SIGPIPE信号直接终止;
- 写端不写或者写的慢,读端要等待写端;
- 写端关闭,读端读完pipe内部的数据然后再读,会读到0为止,表明读到文件结尾;
接下来我们通过下面的程序进行验证 :管道是单向通信和面向字节流
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> int main() { int pipefd[2] = {0}; if(pipe(pipefd) != 0){ //创建匿名管道 perror("pipe error!"); return 1; } //pipefd[0]:读取端 pipefd[1]:写入端 printf("pipefd[0]:%d\n",pipefd[0]);//3 printf("pipefd[1]:%d\n",pipefd[1]);//4 if(fork() == 0){ //子进程---写入 close(pipefd[0]); //关闭子进程的读取端 const char* msg = "hello-linux!"; while(1){ write(pipefd[1], msg, strlen(msg)); //子进程写数据 sleep(1); } exit(0); } //父进程---读取 close(pipefd[1]); //关闭父进程的写入端 char buffer[64] = {0}; while(1){ sleep(1); ssize_t s = read(pipefd[0], buffer, sizeof(buffer)); //父进程读数据 if(s == 0){ break; } else if(s > 0){ buffer[s] = 0; printf("child say to father:%s\n",buffer); } else{ break; } } return 0; }
上述代码中,在父子进程中都有sleep函数:(我们切换使用)
1.当子进程sleep时,父进程没有sleep,运行结果如下:
我们可以发现,子进程在写入数据后经由管道交给父进程处理,这就验证了管道是单向通信的信道;
2.当父进程sleep时,子进程没有sleep,运行结果如下:
我们发现打印出来的数据并不想像刚才那样一条一条的打印,这是因为子进程在写入数据时,只要pipe内部有缓冲区,就不断的写入;当父进程在读取的时候,只要管道内有数据就会一直读;这就是所谓的字节流;即管道是面向字节流的(提供流式服务)
通过下面的程序来验证:同步机制
int main() { int pipefd[2] = {0}; if(pipe(pipefd) != 0){ perror("pipe error!"); return 1; } //pipefd[0]:读取端 pipefd[1]:写入端 printf("pipefd[0]:%d\n",pipefd[0]);//3 printf("pipefd[1]:%d\n",pipefd[1]);//4 if(fork() == 0){ //子进程---写入 close(pipefd[0]); int count = 0; while(1){ write(pipefd[1], "a", 1); count++; printf("count: %d\n",count); } exit(0); } //父进程---读取 close(pipefd[1]); while(1){ sleep(1); } return 0; }
上面的代码中,子进程在不断的写入数据,而父进程一直不读取数据,运行结果如下:
我们运行起来后,就会一直刷屏,直到count为65536的时候停下来。这里为什么子进程不继续写了呢?这首先说明管道是有大小的,在我的云服务器下Linux的管道容量是65536(64Kb),其次子进程不继续写了,表明写端写满后要等待读端读取,才可以继续写入;
我们对上面的代码进行修改,让父进程一次读取一个字符,检验一下子进程会不会继续写入。
//这里简写了,其他内容和上面的代码一样 //父进程---读取 close(pipefd[1]); while(1){ sleep(10); char c = 0; read(pipefd[0], &c, 1); printf("father taken:%c\n", c); }
我们发现父进程每过10秒读取一个字符,但是子进程并没有写入,我们试着将读取字符大小调整到4096个字节时,会发现读端读走数据后,写端就进行写入了;这表明管道自带同步机制(当然管道肯定也是有互斥机制的,这里不做讲解)。
通过下面的程序验证:写端不写或者写的慢,读端会等待写端;(读端不写同理)
int main() { int pipefd[2] = {0}; if(pipe(pipefd) != 0){ perror("pipe error!"); return 1; } //pipefd[0]:读取端 pipefd[1]:写入端 printf("pipefd[0]:%d\n",pipefd[0]);//3 printf("pipefd[1]:%d\n",pipefd[1]);//4 //子进程写的慢 if(fork() == 0){ //子进程---写入 close(pipefd[0]); const char* msg = "hello-linux!"; while(1){ write(pipefd[1], msg, strlen(msg)); sleep(10); } exit(0); } //父进程---读取 close(pipefd[1]); while(1){ sleep(10); char c[64] = {0}; ssize_t s = read(pipefd[0], &c, sizeof(c)-1); c[s] = 0; printf("father taken:%s\n", c); } return 0; }
运行结果如下:
从运行结果可以看出,读端是在等待写端的,这也就是所谓的同步机制,当我们对写端不在进行写入时,读端也会一直在的等待写端的数据写入
通过下面的程序验证:写端关闭,读端读完pipe内部的数据然后再读,会读到0为止,表明读到文件结尾
int main() { int pipefd[2] = {0}; if(pipe(pipefd) != 0){ perror("pipe error!"); return 1; } //pipefd[0]:读取端 pipefd[1]:写入端 printf("pipefd[0]:%d\n",pipefd[0]);//3 printf("pipefd[1]:%d\n",pipefd[1]);//4 //子进程写的慢 if(fork() == 0){ //子进程---写入 close(pipefd[0]); const char* msg = "hello-linux!"; while(1){ write(pipefd[1], msg, strlen(msg)); sleep(10); break; } close(pipefd[1]); exit(0); } //父进程---读取 close(pipefd[1]); while(1){ sleep(10); char c[64] = {0}; ssize_t s = read(pipefd[0], &c, sizeof(c)-1); if(s > 0){ c[s] = 0; printf("father taken:%s\n", c); } else if(s ==0){ printf("write quit...\n"); break; } else{ break; } } return 0; }
在上面的程序中,我们让写端写入一条数据后,10秒直接退出,然后关闭读端,运行结果如下:
当写端写入数据后关闭了写端,读端会从管道内读取到文件的末尾,接收到写端关闭后,就自行退出了。
通过下面的程序验证: 读端关闭,写端收到SIGPIPE信号直接终止
int main() { int pipefd[2] = {0}; if(pipe(pipefd) != 0){ perror("pipe error!"); return 1; } //pipefd[0]:读取端 pipefd[1]:写入端 printf("pipefd[0]:%d\n",pipefd[0]);//3 printf("pipefd[1]:%d\n",pipefd[1]);//4 //子进程写的慢 if(fork() == 0){ //子进程---写入 close(pipefd[0]); const char* msg = "hello-linux!"; while(1){ write(pipefd[1], msg, strlen(msg)); } exit(0); } //父进程---读取 close(pipefd[1]); while(1){ sleep(10); char c[64] = {0}; ssize_t s = read(pipefd[0], &c, sizeof(c)-1); if(s > 0){ c[s] = 0; printf("father taken:%s\n", c); } else if(s ==0){ printf("write quit...\n"); break; } else{ break; } break; } close(pipefd[0]); return 0; }
首先我们对程序进行分析,子进程处于一直写的状态,父进程读取一次数据后就break了,然后将读端关闭了(文件描述符0);
当我们的读端关闭,写端还在写入,在操作系统的层面上,严重不合理;这本质上就是在浪费操作系统的资源,所以操作系统在遇到这样的情况下,会将子进程杀掉(发送13号信号---SIGPIPE);
close(pipefd[0]); //在源程序的基础上加上,用来获取子进程退出信号 int status = 0; waitpid(-1, &status, 0); printf("exit code: %d\n",(status >> 8)& 0xFF); printf("exit signal: %d\n",status& 0x7F);