引言:
在一个操作系统中,我们可以创建非常多的进程,但是进程具有独立性,不同的进程之间是非常独立的,即使是父子进程,由于写时拷贝的存在,在大部分的情况下,他们之间的独立性也非常高。所以,如果我们想要实现进程之间的通信,应该通过怎样的方式呢?
管道
管道其实就是文件,我们想要实现进程之间的通信,那么我们就要找到不同进程之间公共的部分,不同的进程共享文件系统,意思就是不同的进程可以访问同一个文件,在这种情况下,如果我们一个进程向一个文件写入信息,另一个进程读取这个文件的信息,这样其实就是实现了进程之间的通信。
匿名管道
匿名管道:匿名管道是UNIX系统进程间通信(IPC)的一种基本形式,它允许具有血缘关系的进程之间进行数据传输。在Linux环境下,匿名管道是通过内核中的缓冲区实现的,这个缓冲区的大小是有限的,通常由操作系统决定。匿名管道是半双工的通信方式,即数据只能单向流动,要么是父进程向子进程发送数据,要么是子进程向父进程发送数据。
简而言之:匿名管道就是父进程以读和写的方式打开一个文件,然后创建子进程,子进程继承了父进程的读取和写入的权限,通过我们合理的设计(比如关闭父进程的读端,关闭子进程的写端),这样就实现了进程之间的通信。
创建匿名管道的方式
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
const int gbuffer_size = 1024;
bool createPipe()
{
int pipefd[2] = { 0 };
int ret = ::pipe(pipefd);
if(ret == -1)
{
std::cerr << "创建管道失败" << std::endl;
return false;
}
pid_t pid = ::fork();
if(pid == -1)
{
std::cerr << "创建子进程失败" << std::endl;
return false;
}
if(pid == 0)
{
// 子进程
// 关闭子进程的写端
::close(pipefd[1]);
char buffer[gbuffer_size] = { 0 };
int n = ::read(pipefd[0],buffer,sizeof(buffer) - 1);
buffer[n] = 0; // 将字符流转换成字符串
std::cout << "子进程读到了:" << buffer << std::endl;
exit(0);
}
else
{
//父进程
::close(pipefd[0]);
char buffer[gbuffer_size] = "你读到了吗?";
::write(pipefd[1],buffer,sizeof(buffer));
// 回收子进程
pid_t rid = ::waitpid(pid,nullptr,0);
if(rid > 0) std::cout << "等待成功" << std::endl;
}
return true;
}
int main()
{
createPipe();
// 创建管道的方式
return 0;
}
总的来说,创建匿名管道的方式其实本质就是子进程继承了父进程的读写端,通过文件系统实现进程之间的通信。
有名管道
我们刚才所说的匿名管道是管道的一种情况,他并不具有泛用性。他只能针对于父子继承之间进行通信,但是如果是两个陌生的进程应该如何进行通信呢?
这里使用的就是我们的有名管道
创建有名管道
创建有名管道有系统函数和命令行两种方式,其实操作方式大差不差。
创建一个有名管道后,一个进程通过读端进行访问,一个进程通过写端进行访问。
管道的深度理解
这里我们在理解的时候,我们可以将创建管道的过程理解成创建文件的过程,但是并不完全正确,试想如果创建管道后都是直接写入文件,我们写入文件的时候调用系统调用访问磁盘这样不会太过耗时吗? 所以我们的内核设计者同样考虑了这一点,管道看似是文件,其实只是拥有文件描述符的缓冲区而已。
现在我们直接给一段通过管道实现进程之间通信的示例代码。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
const std::string gpathname = "fifo";
const int gbuffersize = 1024;
void createNewFifo()
{
int n = ::mkfifo(gpathname.c_str(),0644);
if(n > 0) std::cout << "create fifo fail!" << std::endl;
if(n < 0){
std::cerr << "make fifo fail!" << std::endl;
::exit(1);
}
}
int main()
{
createNewFifo();
int fd = open(gpathname.c_str(),O_WRONLY);
char buffer[gbuffersize] = "我是Client进程";
::write(fd,buffer,sizeof(buffer) - 1);
while(true);
return 0;
}
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
const std::string gpathname = "fifo";
const int gbuffersize = 1024;
int main()
{
int fd = ::open(gpathname.c_str(),O_RDONLY | O_NONBLOCK);
char buffer[gbuffersize] = { 0 };
int n = ::read(fd,buffer,sizeof(buffer) - 1);
buffer[n] = 0;
std::cout << "I am Server,我读到了:" << buffer << std::endl;
return 0;
}
我们通过这样的代码就可以类似的实现进程之间的通信
system V共享内存
共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递 不再涉及到内核,换句话说是进程不再通过执⾏进⼊内核的系统调⽤来传递
共享内存是什么意思内,我们每个进程都有着自己独立的虚拟地址空间,然后通过页表映射到具体的物理地址空间,具体是什么意思呢?就是说,A进程中有个变量的地址是0xFFFFFFFF,B进程中也有个变量的地址的0xFFFFFFFF,但是呢在内存中这两个变量的存储位置显然是不同的,这就是涉及到虚拟内存和物理内存通过页表进行映射。
所以,我们的内核的设计者设计了一个系统调用,让我们的不同的进程映射同一块物理地址空间,然后通过这段物理地址空间实现进程之间的通信。
这种进程间通信的方式就被我们称作共享内存。
创建共享内存的系统级接口
shmget() 创建共享内存
这里的shmflg的原理和::open中的flag差不多
shmat()
将我们的进程链接到对应的共享内存上,返回的虚拟起始地址。类似于malloc的返回值。
shmdt()
shmctl
实现包括关闭共享内存的多重功能的系统级调用
共享内存的特点
IPC资源必须删除,否则不会⾃动清除,除⾮重启,所以system V ipc的资源必须进行手动的回收,并且我们称他的生命周期是随内核的。
system V信号量
当我们的多个执行流同时访问同一块内存空间的时候,同步和互斥使我们必须面对的问题,所以,共享内存可以让多个进程进行挂接,因此,同步和互斥就是共享内存需要处理的问题。
信号量
信号量的本质是锁和计数器形成的结构体,锁的作用是为了保证我们不同执行流之间的互斥的关系,避免不同的执行流在同一时间访问到同样的内存。计数器的作用是为了保证同步的关系。在生产者-消费者模型中,只有生产者创建了数据,消费者才能获取,因此,这里引入计数器,为了方便告知我们的消费者是否还有数据。
把这些内容进行的统一的封装我们就叫做信号量。
申请资源,计数器--,P操作
释放资源,计数器++,V操作
总结:
这里只是本地状态下实现的进程之间的通信的方式,在计算机网络中,我们其实也可以有着其他的方式访问实现本地的进程间的通信。