1.进程间通信
进程间通信的方法有:管道、System V进程间通信、POSIX进程间通信。
管道:匿名管道、命名管道。
System V进程间通信: System V消息队列、System V共享内存、System V信号量。
POSIX进程间通信:消息队列、共享内存、信号量、互斥量、条件变量、读写锁。
进程间通信可以通过1.内核中的缓冲区 2.文件 3.网络通信的方式实现。
1.1进程间通信的本质理解
进程之间具有独立性,拥有自己的地址空间,因此无法通过各自的虚拟地址进行通信。
- 前提:让不同的进程看到同一块“内存”(特定的结构组织)。
- 所谓的进程看到同一块“内存”,这个“内存”不能隶属于任何一个进程,而应该更强调共享。
1.2管道
计算机领域设计者,设计了一种单向通信的方式——管道。
1.2.1管道的原理
- 分别以读写的方式打开同一个文件
- fork()创建子进程
- 双方进程各自关闭自己不需要的文件描述符。
父进程创建子进程时,将自己的pcb结构体拷贝一份给子进程,从而让父子进程都能看到同一份资源(内存),因为files_struct结构体中的fd_arraryp[],所以打开的文件无需拷贝一份。管道也是文件,这整个过程属于内存级别的操作,不需要向磁盘进行操作。
1.2.2匿名管道通信demo代码演示
int pipe(int pipefd[2]);
pipefd[2] 是输出型参数 会将传入的数组赋值成文件描述符
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
using namespace std;
int main()
{
//1、创建管道
int pipefd[2]={0};//默认是 pipefd[0]为 读入端 pipefd[1]为写入端 放的是文件描述符
int n=pipe(pipefd);
assert(n!=-1);
(void)n;//assert在release端下没用 为了防止n只定义未使用而在release下报错
//cout<<"pipefd[0]:"<<pipe[0]<<endl<<"pipefd[1]:"<<pipefd[1]<<endl; 文件描述符为 3 和 4
//2、创建子进程
pid_t id=fork();
assert(id!=-1)
if(id==0)
{
//子进程 --- 读
//3、构建单向通信的通道,父进程写入,子进程读取。
//3.1 关闭子进程不需要的fd
close(pipefd[1]);
//
char buffer[1024];
while(true)
{
ssize_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;//因为系统调用的Read不添加\0 我们手动添加
cout<<"Father#"<<buffer<<endl;
}
}
exit();
}
//父进程
close(pipefd[0]);
string message="现在是父进程,正在发送消息";
int count=0;
char sendbuffer[1024];
while(true)
{
//构建一个变化的字符串
snprintf(sendbuffer,sizeof(sendbuffer),"%s:%d",message.c_str(),count);
//写入
write(pipefd[1],sendbuffer,strlen(sendbuffer));
sleep(1);
}
pid_t ret=waitpid(id,nullptr,0);
assert(ret<0);
(void)ret;
close(pipefd[1]);
return 0;
}
1.2.3管道的读写规则
当没有数据可读时,read调用阻塞,进程跟着停止,一直等到有数据来到为止。
当管道写满时(管道也是文件,有大小),write调用阻塞,直到有进程读走数据。
如果管道写入端对应的文件描述符被关闭,则read会把数据全部读完,最后返回0。
如果管道读入端对应的文件描述符被关闭,则操作系统会终止写入端的程序。
1.3管道的特点
- 管道是用来进行具有血缘关系的进程间进程通信——父子进程
- 管道具有通过让进程间协同的作用,提供了访问控制——比如读写规则。管道自带同步(没有数据读阻塞,缓冲区写满写阻塞)与互斥。(显示器也是一个文件,但父子进程同时向其进行打印操作时,不会有阻塞——缺乏访问控制)
- 管道提供的是面向流式的通信服务——面向字节流——协议
- 管道是基于文件的,文件的生命周期是随进程的,管道的生命周期是随进程的
- 管道是单向通信的,是半双工通信的一种特殊情况。(半双工:“要么读,要么写”)
2.命名管道文件
int mkfifo(const char *filename,mode_t mode);
使用mkfifo系统调用接口可以创建一个管道文件,参数(文件路径,管道文件的权限)
创建失败返回-1
删除管道文件
unlink(const char* filename);
2.1命名管道的原理
我们程序一般可能是用于向磁盘读取或写入,如果我们每次操作都进行一次IO操作将临时文件刷新到磁盘上,过程会非常慢效率低,于是操作系统就在磁盘上创建了一种文件,叫做管道文件。
管道文件:可以被打开,但是不会将内存数据进行刷新到磁盘,称为命名管道文件。
特点:有名字,该文件一定在系统路径中。
路径具有唯一性。所以双方进程就可以通过路径看到同一份资源。
2.2匿名管道与命名管道的区别
让不同进程看到同一份文件的手段不同
2.3文件管道通信demo
comm.hpp
中定义了几个宏常量,定义了管道文件的filename
server.cc
#include "comm.hpp"
int main()
{
//1.创建管道文件
if(mkfifo(ipcPath.c_str(),MODE)<0)
{
perror("mkfifo");
exit(1);
}
Log("创建管道文件成功",Debug)<<"step 1"<<endl;
//2.正常的文件操作
int fd=open(ipcPath.c_str(),O_RDONLY);
if(fd<0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功",Debug)<<"step 2"<<endl;
//3.编写正常的通信代码
char buffer[SIZE];
while(true)
{
memset(buffer,'\0',sizeof(buffer));
ssize_t s=read(fd,buffer,sizeof(buffer)-1);//读取到内存要注意最后一个是/0
if(s>0)
{
cout<<"client say: "<<buffer<<endl;
}
else if(s==0)
{
//读到了文件结尾
cerr<<"read end of file, client quit ,server quit too"<<endl;
break;
}
else
{
//读取失败
perror("read");
break;
}
}
//4.关闭文件
close(fd);
Log("关闭管道文件成功",Debug)<<"step 3"<<endl;
unlink(ipcPath.c_str());//通信完毕 就删除管道文件
Log("删除管道文件成功",Debug)<<"step 4"<<endl;
return 0;
}
client.cc
#include "comm.hpp"
int main()
{
//1、读取管道文件
int fd=open(ipcPath.c_str(),O_WRONLY);
if(fd<0)
{
//打开失败
perror("open");
exit(1);
}
//2.ipc过程
string buffer;
while(true)
{
cout<<"Please Enter Message Line :>";
getline(cin,buffer);//命名写到了buffer里面
write(fd,buffer.c_str(),sizeof(buffer));//写到文件里 这里不用-1 因为文件里面不看/0
}
//3.关闭
close(fd);
return 0;
}
运行结果
3.问答
1.匿名管道只能用于具有亲缘关系的进程间通信,命名管道可以用于同一主机上的任意进程间通信
2.使用int pipe(int pipefd[2])接口创建匿名管道,pipefd[0]用于从管道读取数据,pipefd[1]用于向管道写入数据。
3.匿名管道需要在创建子进程之前创建,因为只有这样才能复制到管道的操作句柄,与具有亲缘关系的进程实现访问同一个管道通信。
4.管道的本质是内核中的缓冲区,通过内核缓冲区实现通信,命名管道的文件虽然可见于文件系统,但是只是标识符,并非通信介质。
5.管道特性:半双工通信,自带同步与互斥,生命周期随进程,提供字节流传输服务。
6.多个进程只要能够访问同一管道就可以实现通信,不限于读写个数
7.在同步的体现中,若管道所有写段关闭,则从管道中读取完所有数据后,继续read会返回0,不再阻塞;若所有读端关闭,则继续write写入会触发异常导致进程退出。