目录
进程间通信
进程间通信的目的:
进程间通信的本质:
管道:
管道的定义:
匿名管道
单向通信的管道通路:
进程和文件之间的解耦:
单向管道的读写端回收问题:
管道通信主要实现动态数据传递:
父子进程通过管道传输数据的案例:
匿名管道的4种情况:
匿名管道的5种特性:
进程间通信
进程间通信的目的:
-
数据传输:不同进程之间需要传递信息或共享数据。
-
资源共享:多个进程可以共享系统资源。
-
事件通知:进程之间可以通过通信机制向对方发送信号或消息。
-
进程控制:一个进程可以通过通信机制对另一个进程的状态进行控制,如启动、挂起、终止等操作。进程控制有助于父进程管理子进程的执行和生命周期。
进程间通信的本质:
-
进程间同行的本质,就是让不同的进程看到同一份资源,而这份资源不是由进程提供,而是由操作系统提供。
管道:
管道的定义:
-
管道是一种进程间通信机制,用于在两个或多个进程之间传递数据,允许一个进程的输出作为另一个进程的输入来使用。管道的本质是内存中的一块缓冲区。
-
它通过将数据流从一个进程的标准输出(stdout)直接传送到另一个进程的标准输入(stdin),从而实现数据的流式传递。
匿名管道
-
主要用于在父子进程或兄弟进程之间传递数据。它是最简单、最基础的管道形式,特点是单向传输,即数据只能从一端(写端)写入,从另一端(读端)读取。
-
如果要实现双向通信,需要分别创建两个管道。
单向通信的管道通路:
-
创建单向管道:管道的本质是内存中的一块缓冲区,在使用 pipe() 系统调用创建单向管道时,内核会自动为管道分配两个文件描述符:一个用于读端,一个用于写端。这两个文件描述符分别对应这块缓冲区的读指针和写指针。
-
注意,管道的读写端不是直接通过文件打开两次实现,而是通过 pipe() 生成一个匿名的内存缓冲区(管道),分别分配给读端和写端。
-
父进程fork() 后,子进程会继承父进程的文件描述符表。这意味着子进程会复制父进程的文件描述符表项,指向同一个内核文件表项和缓冲区。因此,父进程和子进程都拥有两个文件描述符,分别指向管道的读端和写端。
-
为了实现单向通信,父进程关闭管道的读端(只需要写入数据)。子进程关闭管道的写端(只需要读取数据)。这样父进程只能往管道里写数据,而子进程只能从管道里读数据,形成单向通信。
-
实现通信:此时,父进程通过管道的写端将数据写入内核的管道缓冲区,子进程通过管道的读端读取数据。管道内部实现了数据同步,父进程在写满缓冲区之前可以持续写数据,而子进程读取数据时会从缓冲区中取出。
进程和文件之间的解耦:
-
文件结构体中有一个引用计数器(reference counter),用于跟踪有多少个文件描述符指向该文件。当一个新的文件描述符指向这个文件结构体时,计数器会加一;当一个文件描述符被关闭时,计数器会减一。
-
当一个进程关闭文件描述符(例如调用 close(fd)),文件描述符表中该文件描述符所指向的文件结构体的引用计数会减少一。但此时文件并不会立即关闭,因为内核会检查这个文件结构体的引用计数。
-
当文件结构体的引用计数降为零时,意味着没有任何文件描述符再指向该文件。这时,内核会释放与该文件相关的资源,关闭文件,清理内核中的文件表项和相关的数据结构。
-
这种设计减少了文件与进程之间的耦合。进程不在直接管理文件的关闭,而是交给操作系统来统一管理。
单向管道的读写端回收问题:
-
在形成单向管道后,开始传递数据,数据传递完毕,是否需要关闭父子进程的读写端呢?
-
如果子进程在完成任务后立即退出,那么不需要显式关闭管道的读端。因为当子进程终止时,操作系统会自动回收它所持有的所有资源,包括打开的文件描述符。
-
对于父进程,在完成数据写入后,如果不再使用管道,应显式调用 close() 关闭写端。虽然父进程退出后系统会回收资源,但在父进程继续运行的情况下,未关闭的文件描述符可能会造成资源浪费或影响其他操作。
管道通信主要实现动态数据传递:
-
当父进程通过 fork() 创建子进程时,子进程会继承父进程的某些资源,包括文件描述符、环境变量和内存中的数据(在写时拷贝机制下),从而实现父子间的静态数据传递。这种传递的特点是:父进程的数据在创建子进程时就已经存在,子进程在初始时拥有这些数据的副本。然而这些数据是静态的,一旦子进程启动后,父子进程各自拥有自己的独立副本,互相的修改不会影响对方。
-
如果需要在父子进程之间实现动态数据传递,即:在运行过程中父子进程能够实时通信和交换数据,静态传递方式是不够的。这种情况下,就需要使用管道(pipe)或其他进程间通信(IPC)机制来实现动态数据传递。
父子进程通过管道传输数据的案例:
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <cerrno>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#define MAX 1024
using namespace std;
int main()
{
int pipearr[2]={0}; //创建管道
int n = pipe(pipearr); //pipearr是一个输出型参数
assert(n == 0); //断言是否创建成功
pid_t id = fork();//创建子进程
if (id < 0)
{
perror("pid");
return 1;
}
if(id==0)
{
//child
close(pipearr[0]); //子进程关闭读端
int cnt=10;
while(cnt) //子进程向管道写入
{
char mess[MAX];
snprintf(mess,sizeof(mess),"pid:%d,cnt:%d",getpid(),cnt);
cnt--;
write(pipearr[1],mess,strlen(mess));
sleep(1);
}
exit(0);
}
//parent
close(pipearr[1]); //父进程关闭写端
char buff[MAX];
while(true) //父进程持续读取管道数据
{
ssize_t n=read(pipearr[0],buff,sizeof(buff)-1);
if(n>0)
{
buff[n]='\0';
cout<<buff<<endl;
}
}
pid_t ret=waitpid(id,nullptr,0); //父进程等待子进程
if(ret==id)
{
cout<<"wait success"<<endl;
}
return 0;
};
匿名管道的4种情况:
-
正常情况下,管道没有数据了,读端会一直等待,直到写端写入数据到管道。
-
正常情况下,管道被写满,写端会停止写入,直到读端读取数据后管道出现可用空间。
-
写端关闭,读端read读取,会持续接收到返回值0,表示读到文件的结尾。
-
读端关闭,写端进程会直接被操作系统发送13号信号杀死,可以通过父进程接受子进程的退出码验证。
匿名管道的5种特性:
-
匿名管道,允许具有血缘关系的进程通信,常用于父子进程。
-
匿名管道,会提供读写端的同步机制,
-
匿名管道,面向字节流读取,他会按照你指定的大小尽可能从缓冲区读取数据,如果读写速率不匹配,每次写入很少的数据,在很多次写入后才读取一次,那么一次读取可能读取到多次写入的数据。
-
管道是有上限的,无法一直写入。
-
管道的生命周期跟随进程,进程结束,管道释放。
-
管道是单向通信的,是半双工通信的一种特殊情况。