一、进程通信介绍
1.1进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事( 如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.2 进程间如何通信
进程=内核数据结构+代码和数据
每个进程都拥有自己独立的PCB与数据,由于进程与进程间是相互独立的,互相看不到也不想看到对方的数据,所以进程间通信的前提是让不同进程看到同一份资源。进程间通信一定是某个进程需要通信,让操作系统创建一个共享资源,而用户不能直接操控操作系统,所以操作系统需要提供许多的系统调用,系统调用不同就会创建出不同的共享资源,那么进程间的通信方式也会不同
1.3进程间通信的方式
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
二、匿名管道
2.1匿名管道原理
当一个进程以读和写的方式分别打开一个文件,由于打开方式的不同,就会创建两个struct file,每个文件都有自己的属性、数据、更重要的是存在内核级的文件缓冲区,两个struct file除了打开方式字段不同,其余信息都是相同的,所以操作系统不会将文件的信息在拷贝一份,两个struct file指向的文件信息是一份。当该进程创建子进程,子进程会继承父进程的文件描述符表,此时父子进程可以对同一个文件进行读取操作,该文件中的缓冲区就相当于一个共享资源,这个缓冲区也成为道,两个进程间就可以进行通信了
当我们想让父进程写入数据,子进程读取数据时,我们只需要关闭父进程的读文件,关闭子进程的写文件即可,这样就可以做到子进程向管道中写数据,父进程向文件中读数据了
【注意】:
- 管道只能进行通单向信,及只能一个进程进行读一个进程写,不能让两个进程既可以读也可以写,所以我们需要关闭对应不需要功能的文件描述符(不关闭也是可以的,但是建议关掉,防止误写误读)
- 写端写入的数据会保存在内核级文件缓冲区中,直到被读走
2.2 pipe接口(创建管道)
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
int fd[ ]为输出型参数,会带回以读和写方式打开的文件的fd,由于我们知道文件的fd不知道文件的名字,所以称为匿名管道
2.3 代码理解(父进程读,子进程写)
#include<iostream>
#include<unistd.h>
#include<cerrno>
#include<cstring>
#include <sys/types.h>
#include<string>
#include<sys/wait.h>
#include <sys/types.h>
#define SIZE 1024
void chilewrite(pid_t wfd)
{
while(1)
{
std::string message="father I am your child process!";
write(wfd,message.c_str(),message.size());
sleep(2);
}
}
void fathread(pid_t rfd)
{
char buffer[SIZE];
while(1)
{
int n=read(rfd,buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]='\0';
std::cout<<buffer<<std::endl;
}
else if(n==0)
{
// 如果read的返回值是0,表示写端直接关闭了,我们读到了文件的结尾
std::cout<<"写端关闭了"<<std::endl;
break;
}
else
{
std::cerr<<"read err"<<std::endl;
break;
}
}
}
int main()
{
//创建管道
int pipefd[2];
int n=pipe(pipefd);
if(n<0)
{
std::cerr<<"errno:"<<errno<<" errstring:"<<strerror(errno)<<std::endl;
return 1;
}
//创建子进程
pid_t id=fork();
if(id==0)
{
//子进程
std::cout<<"子进程准备就绪,准备写入数据了"<<std::endl;
close(pipefd[0]);
chilewrite(pipefd[1]);
close(pipefd[1]);
exit(0);
}
//父进程
std::cout<<"父进程准备就绪,准备读取数据了"<<std::endl;
close(pipefd[1]);
fathread(pipefd[0]);
close(pipefd[0]);
//进程等待
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
std::cout << "wait child process done, exit sig: " << (status&0x7f) << std::endl;
std::cout << "wait child process done, exit code(ign): " << ((status>>8)&0xFF) << std::endl;
}
return 0;
}
2.4 管道的四种情况
- 如果管道内部是空的并且写端没有关闭,此时读取条件不具备,读端会被阻塞,等待写入数据
- 如果管道内部被写满了并且读端没有关闭,此时写入条件不具备,写端会被阻塞,等待读端读取数据
- 如果管道一直在读,但是已经关闭了写端,此时读端read返回值会一直读到0,表示读到了文件末尾
- 如果管道一直在写,但是已经关闭了读端,此时OS会直接使用13号信号杀死进程,代表进程异常
2.5 管道的五种特征
- 匿名管道只能进行有血缘关系的进程之间进行通信,因为匿名管道是依靠子进程继承父进程的文件描述符表实现的(通常用于实现父子进程之间的通信)
- 管道内部自带进程的同步机制,多执行流执行代码时具有明显的顺序性,写入管道的数据直到被读取之前会保持在管道缓冲区中(如果缓冲区未满),而读取操作则会等待直到有数据可读,这种机制避免了同时读写导致的数据损坏问题
- 管道文件的生命周期是随进程的,当所有打开该文件的进程都退出后,该文件资源也会被释放
- 管道文件在进行通信时是面向字节流的,读与写的次数不是一 一匹配的,数据没有明确的分割,一次拿多少数据都行
- 管道通信是一种特殊的半双工模式。半双工通信允许数据在两个方向上传输,但不能同时进行。这意味着在任何时候,数据只能在一个方向流动。一旦一方开始发送数据,另一方必须等待接收完毕后才能开始发送。全双工通信允许数据同时在两个方向上进行传输,无需等待。由于半双工模式是可以双向传输数据的,但是管道只能单向通信,所以是特殊的半双工模式
2.6 管道的读写规则
- 当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
- 当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN 如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程 退出
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
PIPE_BUF一般至少为512个字节,在Linux下,PIPE_BUF为4096个字节,当写入的的数据量不大于PIPE_BUF时,写入操作是安全的,不会发生写一半数据就被读走的情况,如果写入的数据量大于PIPE_BUF,则可能会发写入的数据提前被读走一部分
三、命名管道
匿名管道只能实现具有血缘关系进程之间的通信,而命名管道可以实现两个毫无相关的进程之间的通信,下面先介绍一下命名管道的原理
3.1命名管道原理
当一个进程以读的方式打开一个文件,该进程会有自己的进程PCB和文件描述符表,同时会创建一个struct file
当另一个进程以写的方式也打开这个文件,该进程也会有自己的进程PCB、文件描述符表,并且也会创建一个struct file,但是由于这两个进程打开的文件是一样的,而struct file也就是打开文件的方式不同,所以两个struct file都是指向的同一份文件信息,也指向了同一个内核级文件缓冲区,那么这两个毫无关系的进程就指向了同一段空间,就可以进行通信了。
怎么确保两个进程打开的是同一个文件呢?---------文件的路径
3.2 mkfifo接口
- 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename
- 命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
#参数:filename为文件名 mode为创建文件的权限
#头文件:#include <sys/types.h>
#include <sys/stat.h>
#返回值:成功返回0,失败返回-1,并且设置错误码
3.2 代码使用举例
namedpipe.hpp:
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <unistd.h>
#include <iostream>
#include <fcntl.h>
#define Defaultfd -1
#define Creater 1
#define User 2
#define SIZE 128
#define Read O_RDONLY
#define Write O_WRONLY
const std::string path = "./myfifo";
class NamedPipe
{
private:
bool OpenNamePipe(int mode)
{
_fd = open(_fifo_path.c_str(), mode);
if (_fd < 0)
return false;
return true;
}
public:
NamedPipe(const std::string path, int who)
: _fifo_path(path), _id(who), _fd(Defaultfd)
{
if (_id == Creater)
{
int ret = mkfifo(_fifo_path.c_str(), 0666);
if (ret != 0)
{
std::perror("mkfifo");
}
std::cout << "Creater creat namedpipe!" << std::endl;
}
}
bool OpenforRead()
{
return OpenNamePipe(Read);
}
bool OpenforWrite()
{
return OpenNamePipe(Write);
}
int ReadNamedPipe(std::string *out)
{
char buffer[SIZE];
int n = read(_fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
*out = buffer;
}
return n;
}
int WriteNamedPipe(std::string& in)
{
return write(_fd,in.c_str(),in.size());
}
~NamedPipe()
{
if (_id == Creater)
{
int ret = unlink(_fifo_path.c_str());
if (ret != 0)
{
std::perror("unlink");
}
std::cout << "Creater free namedpipe!" << std::endl;
}
}
private:
std::string _fifo_path;
int _id;
int _fd;
};
client.cpp:
#include "namedpipe.hpp"
int main()
{
// 以使用者的身份打开
NamedPipe fifo(path, User);
if (fifo.OpenforWrite())
{
std::cout << "Client open fifopipe for write!" << std::endl;
while (true)
{
std::cout << "Please enter message" << std::endl;
std::string message;
std::getline(std::cin, message);
fifo.WriteNamedPipe(message);
}
}
return 0;
}
server.cpp:
#include "namedpipe.hpp"
int main()
{
// 以创建者的身份打开
NamedPipe fifo(path, Creater);
if (fifo.OpenforRead())
{
std::cout << "Server open fifopipe for read!" << std::endl;
while (true)
{
std::string message;
int n = fifo.ReadNamedPipe(&message);
if (n > 0)
{
std::cout << "Client:" << message << std::endl;
}
else if (n == 0)
{
std::cout << "写端关闭了,读端也要关闭!" << std::endl;
break;
}
else if (n < 0)
{
std::perror("read");
break;
}
}
}
return 0;
}
结果展示: