文章目录
进程间通信介绍
进程间通信的概念
进程间通信(ipc), 指不同进程之间的信息传输或交换。
进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信的本质理解
我们知道进程间具有独立性,因为写时拷贝存在,父子进程之间向持续性的交换数据是不可能的。如果想让两个进程通信,必须先让不同的进程看到同一份资源作为数据交换的场所。
进程间通信分类
管道:
- 匿名管道
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
注意:针对以上分类,我们主要对管道和共享内存详细学习。
管道
什么是管道
- 管道是Unix中最古老的进程间通信的形式,将数据单项传输,是一种单向通信的方式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
匿名管道
匿名管道的原理
匿名管道通信原理:进程之间通过管道进行通信。
主要步骤如下:
假设,我们让父进程对目标文件写入数据,子进程对目标文件读取数据。
- 那么此时父进程通过files_struct中的四号文件描述符(关闭三号文件描述符)找到struct_file文件指针,再通过inode找到目标文件加载到物理内存中文件缓冲区的数据。
- 子进程通过files_struct中的三号文件描述符(关闭四号文件描述符)找到struct_file文件指针,再通过inode找到目标文件加载到物理内存中文件缓冲区的数据。
- 综上,此时父子进程就看到了同一份“资源”,父子进程进而可以对该文件进行写,读。操作,从而父子进程打实现进程间通信。
注意:
-
在创建父子进程的时候 进程相关数据结构需要重新拷贝,被打开文件相关内核结构不会被拷贝。因为fork函数只是为了创建子进程,不会对文件相关数据结构作拷贝。
-
此时父子进程看到的同一份文件资源,并对其进行写,读操作,并不会发生写时拷贝,因为被打开文件内核数据结构是由操作系统维护的,不受进程维护。
-
文件描述符意义:0:标准输入,1:标准输出,2:标准错误,3:读文件描述符,4:写文件描述符。
-
进程间通信是内存级通信,不需要将数据写入到磁盘文件中,因为反复的IO会降低效率,也没有必要。
pipe函数
pipe函数一般用于创建匿名管道,pipe函数原型如下:
int pipe( int fd[2] )
参数:
fd: 文件描述符数组,其中fd[0]代表读端,fd[1]代表写端。
返回值:
成功返回0,失败返回-1.
实践代码:
//父进程写入文件,子进程读取文件
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>
using namespace std;
int main()
{
//创建管道
int pipefd[2] = {
0 }; //pipefd[0]: 读端 //pipefd[1]: 写端.
int n = pipe( pipefd );
assert( n !=- 1 );
(void)n;
//创建子进程。
pid_t id = fork();
assert( id != -1 );
if( id == 0 ) //子进程,子进程读取。
{
close(pipefd[1]);
char buffer[1024];
while( true ) //先将数据读取到缓冲区中打印出来。
{
ssize_t s = read( pipefd[0],buffer,sizeof(buffer)-1);
if( s > 0 )
{
buffer[s] = '\0';
cout << " child get a message[" << getpid() <<"] father#" << buffer << endl;
}
}
exit(0);
}
close(pipefd[0]); //父进程,父进程写入。
string message = "我是父进程,我正在给你发信息";
int count = 0;
char send_buffer[1024];
while( true )
{
snprintf( send_buffer,sizeof(send_buffer), "%s: %d",message.c_str(),count++ );
write(pipefd[1],send_buffer,strlen(send_buffer));
sleep(1);
}
pid_t ret = waitpid(id,nullptr,0);
(void ) ret;
assert(ret > 0 );
return 0;
}
运行结果如下:
管道的特点
1 . 管道一般用来具有血缘关系的进程用来进程间通信---->常用于父子进程。
2 .管道具有通过让进程间协同,进而提供访问控制。
- 如果父进程写数据慢(每一次写入暂停10秒),子进程读取数据快,那么子进程便会等待父进程将数据写入后再读取(子进程也暂停10秒)。
- 但是如果父进程写得快,子进程读得慢(每读一次暂停10秒),这样父进程在缓冲区写满之后就不会继续写了,等子进程将数据全部读完,父进程再进行写入。
3.管道提供的是面向流式的通信服务(面向字节流,写一条都一条,如果写得快,读的慢,那么就会一次性读取数据)。
4.管道是基于文件的,文件的生命周期是基于进程的,从而管道的生命周期跟随于进程。如果在通信过程中,父子进程都退出,那么文描述符便会被关闭,管道也会自动退出。
例如,当父进程写入文件10次后便关闭文件描述符退出,而子进程此时read的返回值便会为零,那么子进程也会退出。
int main()
{
//创建管道
int pipefd[2] = {
0 }; //pipefd[0]: 读端 //pipefd[1]: 写端.
int n = pipe( pipefd );
assert( n !=- 1 );
(void)n;
//创建子进程。
pid_t id = fork();
assert( id != -1 );
if( id == 0 ) //子进程,子进程读取。
{
close(pipefd[1]);
char buffer[1024];
while( true ) //先将数据读取到缓冲区中打印出来。
{
ssize_t s = read( pipefd[0],buffer,sizeof(buffer)-1);
if( s > 0 )
{
buffer[s] = '\0';
cout << " child get a message[" << getpid() <<"] father#" << buffer << endl;
}
else if( s == 0 )
{
cout << "writer quit, i quit" << endl;
break;
}
}
close(pipefd[0]);
exit(0);
}
close(pipefd[0]); //父进程,父进程写入。
string message = "我是父进程,我正在给你发信息";
int count = 0;
char send_buffer[1024];
while( true )
{
snprintf( send_buffer,sizeof(send_buffer), "%s: %d",message.c_str(),count++ );
write(pipefd[1],send_buffer,strlen(send_buffer));
sleep(1);
cout << count << endl;
if( count == 10 )
{
cout << "writer quit(father)" << endl;
break;
}
}
close( pipefd[1]); //父进程准备退出,关闭文件描述符。
pid_t ret = waitpid(id,nullptr,0);
(void ) ret;
assert(ret > 0 );
return 0;
}
运行结果如下:
5.管道是单项通信,本质上就是半双工通信的一种特殊情况,通信双方中,一方固定为读端,一方固定为写端。
6.两种特殊情况。
- 父进程写端关闭,子进程读0,那么表示读到了文件结尾。
- 子进程读端关闭,父进程继续读取,OS也会终止写进程。
命名管道
命名管道的原理
- 由于匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。一般情况下,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间便可以通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作。我们可以让两个进程打开该文件,此时进而让两个进程看到了同一份“资源”,进而实现进程通信。
注意:
- 命名管道是一种特殊类型的文件,可以被打开,但是不会将内存数据刷新到磁盘中,相对于匿名管道来说,命名管道有文件名,并且存在于系统路径中。
使用相关命令创建命名管道
我们可以通过mkfifo命令创建一个命名管道。
[yzh@yzh test1]$ mkfifo fifo
并且,我们可以看到创建出来的文件类型为p,即代表的是管道文件。
此时,我们便可以通过shell脚本命令行每秒将一个字符串写入到命名管道文件中,再从另一端读取该命名管道中的数据打印到显示器上,这也进而体现了两个毫不相关的进程可以通过命名管道通信。