OS学习之路——进程间通信
- 前言
- 为什么需要进程间通信
- 什么是进程间通信
- 进程间通信
- 数据拷贝类型
- 基于共享内存
- 基于信号
前言
- 之前我们学习了进程的相关话题,这次我们来学习和进程相关的一个话题——进程间通信
为什么需要进程间通信
- 假设你想从你的朋友那里获取一些信息,你就会想在微信或者现实中去询问他,反之亦然。你可以把你和你的朋友想象成两个进程,此时你们为了获取信息就需要和对方通信。这就是一个进程间通信的例子
- 还有在网络世界中,我们从浏览器中获取信息,首先需要从浏览器发请求,浏览器也是一个进程,服务器收到我们的请求后,他会有专门的程序处理,这也是一个进程。这也是一个进程间通信的例子
- 还有许多的例子,在计算机的世界中也是这样,同一台机器上的一个进程可能也需要另一个进程提供的信息完成工作
什么是进程间通信
- 通过前面的文章,我们知道进程实际上是计算机中正在运行程序的一个代表,一个进程内部的pcb包含了这个程序的程序计数器、打开的文件、地址空间等信息。一个进程不可能自己完成所有的 工作,总会有一些进程完成工作需要配合外部进程
- 进程间通信顾名思义发生在进程间进行通信的过程,那么又有哪些形式呢?
- 基于数据拷贝:管道,消息队列,套接字
- 基于共享内存:共享内存
- 基于信号:信号,信号量
- 下面我们来逐个介绍上述内容
进程间通信
数据拷贝类型
管道
- 匿名管道:只可以发生在有亲缘关系的进程(父子进程),依赖于
pipe函数,一个大小为2的数组,1代表写端,0代表读端,可以使用read和write函数进行读写数据。 - 原理:
pipe调用成功后,内核会创建一个管道对象,这里包含了缓冲区 、读写指针、等待队列等,并返回两个文件描述符,当前进程以及和当前进程有亲缘关系的进程都可以继承这两个描述符,这时就可以使用这两个描述符通过read和write函数进行读写了,匿名管道是单向的,读写方式在程序编写是就固定了,确定哪方读/写后就不可以改变
intfd[2];pipe(fd);if(fork()==0){write(fd[1],"hello",5);}else{charbuf[10];read(fd[0],buf,5);}- 命名管道:可以发生在无亲缘关系的进程,可以使用
mkfifo(const char *pathname, mode_t mode)函数或者使用mkfifo filename创建一个管道文件,管道文件使用p进行标识 - 原理:在内核创建一个
缓冲区,表现形式为一个管道文件,这个管道文件仍然可以使用系统调用进行open,read和write,但是其内部的数据并不会持久化到磁盘内,但是仍然可以使用ls -i命令查询到其inode号,且不能使用cat或者>、>>查看文件或者写入内容。需要注意的是不能仅以读或者写方式打开这个文件,在只有一种方式访问文件的时候,在另一种方式未到来前当前操作是阻塞的,读者可以打开两个终端,一个使用cat命令查看文件,一个使用echo "hello" >> pipefile,在没有第二步前第一个终端是一直阻塞的 - 命名管道是
半双工的,双方都可以担任读/写的角色,但是同一时刻数据只能向一端流动 - 套接字:可以发生在任何进程,可以跨主机,使用
socket函数创建一个套接字描述符,通信双方依赖套接字处理读写事件 - 原理:数据会存储在内核的
socket缓冲区,当要读取数据时,会从内核缓冲区拷贝到用户自定义的缓冲区,当发送数据时,从用户的缓冲区拷贝到内核的socket缓冲区准备发送。 - 套接字是全双工的,数据可以同时双向流动
- 匿名管道:只可以发生在有亲缘关系的进程(父子进程),依赖于
基于共享内存
- 共享内存:使用于进程间通信,上面讲的几个至少要经过两次拷贝,共享内存可以减少至0次,也叫零拷贝。
- 原理:使用
shm_open创建一个共享内存, 使用mmap进行映射,当需要读取或写入内容时,直接对这块内存操作即可 - 共享内存是全双工的,但是因为任何知道这个共享内存存在的进程都可以读取其内容,所以需要注意使用同步机制保护数据的完整性
#include<sys/mman.h>#include<fcntl.h>#include<unistd.h>#include<string.h>intmain(){constchar*name="/myshm";constsize_t SIZE=4096;// 1. 创建共享内存对象(读写,如果存在则打开,模式0644)intfd=shm_open(name,O_CREAT|O_RDWR,0644);// 2. 调整大小ftruncate(fd,SIZE);// 3. 映射到进程地址空间char*ptr=mmap(0,SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);// 4. 写入数据strcpy(ptr,"Hello from writer");// 5. 解除映射munmap(ptr,SIZE);// 6. 关闭描述符(数据仍然保留在共享内存中)close(fd);// 注意:不要 shm_unlink,否则读进程无法打开return0;}基于信号
- 信号
- 实际上是内核向某个进程发送了中断通知,然后进程响应中断做出的答复,使用
signal注册信号以及信号的处理方式,这个方法是异步的,不需要缓冲区
#include<signal.h>#include<iostream>voidhandler(intsig){std::cout<<"received signal\n";}intmain(){signal(SIGUSR1,handler);while(1){}} - 实际上是内核向某个进程发送了中断通知,然后进程响应中断做出的答复,使用
- 信号量:本质上是一个原子的计数器,可以用来控制资源访问和实现进程间同步。当需要获取资源时,自减计数器,归还资源时,自增计数器
- 下面这个程序得到的结果是990001
#include<iostream>#include<sys/semaphore.h>#include<thread>intmain(){volatilestd::size_t idx=1;// int num = 1;sem_t x;sem_init(&x,0,1);std::threadth1([&](){for(inti=0;i<1000000;++i){sem_wait(&x);++idx;sem_post(&x);}});std::threadth2([&](){for(inti=0;i<10000;++i){sem_wait(&x);--idx;sem_post(&x);}});th1.join();th2.join();sem_destroy(&x);std::cout<<idx<<std::endl;return0;}通过这些介绍,你对进程间通信是否有了一个全新的认识?如果觉得写的还不错的话,欢迎点赞关注,如果有不对的地方,欢迎批评指正。