进程间通信介绍:
进程间通信的概念:
进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
进程间通信的目的:
数据传输: 一个进程需要将它的数据发送给另一个进程。
资源共享: 多个进程之间共享同样的资源。
通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。
进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信的本质:
进程间通信的本质就是让不同的进程看到同一份资源。
进程间通信的发展:
管道
System V进程间通信
POSIX进程间通信
进程间通信的分类:
管道
- 匿名管道
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道:
认识管道:
由于进程间具有独立性,想要实现进程间通信非常困难,想要实现进程间通信就必须借助第三方资源,让两个需要通信的进程都可访问这个第三方资源,早期管道就是这样的第三方资源来实现进程间通信。
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“
演示:
先来介绍两个命令:
1.who
who指令可以用来显示当前云服务器登录的用户数,一行显示一个用户。
如图所示现在有2个用户。
2.wc
wc指令可以查指定文件的计算文件的Byte数、字数、或是列数,若不指定文件名称、或是所给予的文件名为"-",则wc指令会从标准输入设备读取数据
wc加上-l指令,计算指定文件的行数。
将上述两个命令通过管道连接,就可以更准确地查出当前云服务器的登录用户:
who进程将数据写入管道,wc从管道中读取到数据,-l指令计算数据的行数,从而得出当前云服务器的登录数。
匿名管道:
匿名管道性质:
匿名管道仅支持父子间进程通信。
当我们创建一个进程,在linux系统中它被如下图进行管理:
我们再通过这个进程创建一个子进程,子进程继承父进程的代码和数据:
没错,此时我们的父子进程能看到同一份资源,我们可以模拟一下通信,父进程往缓冲区写入,子进程往缓冲区读取,早期的工程师发现了这种现象,并且认为这是一种很好的进程间通信的方法,就在这种方法的基础上进行了一下改动,创造了管道。
注意:
我们在进程间通信时,是没必要对磁盘中的文件进行操作的,所以我们的管道没必要与磁盘中的文件产生关联。
文件级缓冲区是由操作系统来维护的,所以当父进程对其写入时,是不会发生写时拷贝的。
pipe函数:
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd中的两个元素分别用来返回管道读端和写端的文件描述符:
数组元素 | 含义 |
pipefd[0] | 管道读端文件描述符 |
pipefd[1] | 管道写端文件描述符 |
匿名管道的使用:
注意下图中的fd均指pipefd。
1.父进程用pipe函数创建管道。
2.父进程通过fork函数创建子进程。
3.假设我们让子进程写,父进程读,所以我们要关闭不用的文件描述符,父进程关闭写端,子进程关闭读端。
我们再站在文件描述符的角度深入理解:
匿名管道测试:
现在用下述代码测试匿名管道,父进程进行一直读取,子进程进行一直写入:
#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
const int size = 1024;
//子进程进行写入
void SubProcessWrite(int wrd)
{
std::string message = "father,i am your childen process! ";
while(true)
{
sleep(1);
std::cout<<"childen begin write........."<<std::endl;
static int cent = 0;
pid_t id = getpid();
//拼接消息
std::string info = message;
info += "my pid is ";
info += std::to_string(id);
info += ", cent: ";
info += std::to_string(cent);
//写入
write(wrd,info.c_str(),info.size());
cent++;
}
}
//父进程进行读取
void FatherProcessReader(int rfd)
{
char inbuffer[size];
while(true)
{
std::cout<<"father begin read,message:"<<std::endl;
ssize_t n = read(rfd,inbuffer,sizeof(inbuffer) - 1);//读取消息
if(n > 0)
{
inbuffer[n] = 0;//语言限制,在字符串最后加\0
std::cout<<inbuffer<<std::endl;//打印消息
}
}
}
int main()
{
int pipefd[2];
int n = pipe(pipefd);//管道创建成功,返回0
if(n != 0)//管道创建失败
{
std::cerr<<"errno "<<errno<<"cerrstring: "<<strerror(errno)<<std::endl;
}
std::cout<<"读端->pipefd[0]"<<pipefd[0]<<"写端->pipefd[1]"<<pipefd[1]<<std::endl;
sleep(1);
pid_t id = fork();//创建子进程
if(id == 0)
{
//子进程进行写入
std::cout<<"子进程关闭不需要的fd了,准备写消息了"<<std::endl;
close(pipefd[0]);//关闭读端
SubProcessWrite(pipefd[1]);//子进程写
close(pipefd[1]);//任务完成关闭写端
exit(0);
}
//父进程
std::cout<<"父进程关闭不需要的fd了,准备读消息了"<<std::endl;
close(pipefd[1]);//关闭写端
FatherProcessReader(pipefd[0]);//父进程读
close(pipefd[0]);//任务完成关闭读端
pid_t rid = waitpid(id,NULL,0);//父进程等待子进程,并回收
return 0;
}
来看看运行结果:
管道的4种情况:
1.写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
2.读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
3.写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
4.读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
管道的大小:
管道是有容量的,当管道被写满了,写端将会阻塞或者失败,查询管道大小的方法有如下:
ulimit -a指令,查看当前资源限制。
从上图可以算出管道的大小为512*8 = 4096字节。
命名管道:
刚才介绍的匿名管道,只可用于父子进程间通信,如果两个毫不相干的进程要实现通信该怎么办呢?接下来就需要介绍一下命名管道了。
mkfifo函数:
mkfifo函数用于创建一个命名管道。
mkfifo的第一个参数表示要创建的命令管道文件,如果不带路径默认再当前文件夹下。
mkfifo的第二个参数表示管道的文件权限。
例如文件权限设置为0666,则理论创建的管道权限为
但实际文件权限还会受文件默认掩码umask影响,默认的umask是0002,我们实际的文件权限会先0666&(~umask),所以实际管道权限为0664:
mkfifo的返回值:
管道创建成功返回0。
创建管道失败返回-1,错误码被设置。
用命名管道实现serve&client通信
serve管理管道负责创建,销毁和读取消息,client负责往管道中写入消息:
serve.cc:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
const std::string comm_name = "./myfifo";
int main()
{
//服务端创建命名管道
int res = mkfifo(comm_name.c_str(),0666);
if(res != 0)//创建失败
{
perror("mkfifo");
}
//serve端打开管道
int fd = open(comm_name.c_str(),O_RDONLY);
if(fd < 0)
{
std::cout<<"open file"<<errno<<std::endl;
}
//serve接受消息并打印
char buffer[1024];
while(true)
{
std::cout<<"server begin read:"<<std::endl;
ssize_t n = read(fd,buffer,sizeof(buffer)-1);
if(n > 0)//读取成功
{
buffer[n] = 0;
std::cout<<buffer<<std::endl;
}
else if( n == 0)
{
std::cout<<"read done"<<std::endl;
break;
}
else
{
std::cout<<"read fail"<<errno<<std::endl;
break;
}
}
int n = unlink(comm_name.c_str());
if(n != 0)
{
perror("unlink");
}
return 0;
}
client.cc:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
const std::string comm_name = "./myfifo";
int main()
{
//client以写打开管道
int fd = open(comm_name.c_str(),O_WRONLY);
if(fd < 0)
{
std::cout<<"open fail"<<errno<<std::endl;
}
int cent = 100;
sleep(5);
while(cent--)
{
sleep(1);
std::cout<<"client begin write"<<std::endl;
//消息拼接
std::string message = "i sent a message ,cnet: ";
message += std::to_string(cent);
//写入消息
ssize_t n = write(fd,message.c_str(),sizeof(message));
}
return 0;
}
来看看运行结果: