一.理解进程间通信
进程是一个独立的个体,但是近处也需要某种协同,而协同的前提就是进程间的通信。
进程间通信的前提是:
先让不同的进程看到同一份操作系统提供的资源(“一段内存”),一定是某个进程先需要通信,让OS创建一个共享资源,所以OS必须提供很多的系统调用。
OS创建的共享资源的不同,系统调用接口的不同,说明进程间通信会有不同的种类。
进程通信的常见方式:
- 消息队列
- 管道
- 共享内存
- 信号量
二.管道
1.匿名管道实现
匿名管道是父子进程之间进行通信的一种方式,父子进程通过对同一份文件进程读写操作,会同时得到文件的文件描述符,通过读写分离的操作,在文件的内核缓冲区——即管道中进行通信,而缓冲区里的数据不需要刷新的磁盘中。
** 命令行中使用的“|”,就是匿名管道**。
**#include<unistd.h> **
//创建匿名管道
int pipe(int pipefd[2]);//输出型参数,0号下标->r,1号下标->w,创建成功返回0,失败返回-1.
#include<iostream>
#include<cerrno>
#include<cstring>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
const int size = 1024;
string GetOtherMessage()
{
static int cnt = 0;
string messageid = to_string(cnt);
cnt++;
pid_t self_id = getpid();
string stringpid = to_string(self_id);
string message = "messageid:";
message += messageid;
message += "my pid is:";
message += stringpid;
return message;
}
void SubProcessRead(int rfd)
{
char inbuffer[size];
while(true)
{
ssize_t n = read(rfd,inbuffer,sizeof(inbuffer) - 1);//子进程收到父进程发来的消息
if(n > 0)
{
inbuffer[n] = 0;
cout << "child get message:" << inbuffer << endl;
}
}
}
void FatherProcessWrite(int wfd)
{
string message = "I am father.";
while(true)
{
string info = message + GetOtherMessage();//父进程发给子进程的消息
write(wfd,info.c_str(),info.size());//写入管道时,没有写入/0
sleep(1);
}
}
int main()
{
//创建管道
int pipefd[2];
int n = pipe(pipefd);
if(n != 0)
{
cerr << "errno:" << errno << ":"
<< "errstring:" << strerror(errno) << endl;
}
//创建子进程
pid_t id = fork();
if(id == 0)
{
sleep(1);
//子进程读
close(pipefd[1]);
SubProcessRead(pipefd[0]);
close(pipefd[0]);
exit(0);
}
sleep(1);
//父进程写
close(pipefd[0]);
FatherProcessWrite(pipefd[1]);
close(pipefd[1]);
pid_t rid = waitpid(id,nullptr,0);
if(rid > 0)
{
cout << "wait child process done." << endl;
}
return 0;
}
2.管道的五种特征
- 匿名管道:只用来进行具有血缘关系的进程之间 进行通信,常用于父子进程之间的通信。
- 管道内部,自带进程之间的同步机制。多执行流执行代码时,具有明显的顺序性。
- 管道文件的生命周期是随进程的。
- 管道文件在通信的时候,是面向字节流的。write的次数和read的次数不是一一对应的。
- 管道的通信模式,是一种特殊的半双工模式。
3.管道的四种情况
- 如果管道内部是空并且write fd没有关闭,读取条件不具备,读进程会被阻塞。可以通过使用wait函数,等待读取条件具备时再进行写入数据。
- 管道被写满并且read fd不读且没有关闭,管道会被写满,写进程会被阻塞。同样使用wait函数,等待数据被读取,等写条件具备再进行写入。
- 管道一直在读并且写端关闭了wfd,读端read返回值会读到0,表示读到了文件末尾。
- rfd直接关闭,写段wfd一直在进行写入,写端进程会被操作系统直接用13号信号关掉,相当于进程出现了异常。
4.命名管道
两个毫不相干的进程,可以通过文件路径来打开同一个文件,此文件路径即为命名管道。
**mkfifo指令:创建一个管道文件 **
#include<sys/types.h>
#include<sys/stat.h>
*int mkfifo(const char pathname,mode_t mode)函数:创建代码级管道文件
#include<unistd.h>
*int unlink(const char pathname)函数:删除管道文件
1 .client
#include"namedPipe.hpp"
//write
int main()
{
NamedPipe fifo(comm_path,User);
if(fifo.OpenForWrite())
{
while(true)
{
cout << "Please Enter: ";
string massage;
getline(cin,massage);
fifo.WriteNamedPipe(massage);
}
}
return 0;
}
2.server
#include"namedPipe.hpp"
//read 管理命名管道的整个生命周期
int main()
{
NamedPipe fifo(comm_path,Creater);
if(fifo.OpenForRead())
{
while(true)
{
string massage;
int n = fifo.ReadNamedPipe(&massage);
if(n > 0)
{
cout << "Client Say: " << massage << endl;
}
else if(n == 0)
{
cout << "Client quit, Server Too!" << endl;
break;
}
else
{
cout << "fifo.ReadNamedPipe Error" << endl;
break;
}
}
}
return 0;
}
3. namesPipe
#pragma once
#include<iostream>
#include<cerrno>
#include<cstdio>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
using namespace std;
const string comm_path = "./myfifo";
#define Creater 1
#define User 2
#define DefaultFd -1
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096
class NamedPipe
{
private:
bool OpenNamedPipe(int mode)
{
_fd = open(_fifo_path.c_str(),mode);
if(_fd < 0)
return false;
return true;
}
public:
NamedPipe(const string &path,int who)
:_fifo_path(path),_id(who),_fd(DefaultFd)
{
if(_id == Creater)
{
int res = mkfifo(_fifo_path.c_str(),0666);
if(res != 0)
{
perror("mkfifo");
}
cout << "creater create named pipe." << endl;
}
}
~NamedPipe()
{
sleep(5);
if(_id == Creater)
{
int res = unlink(_fifo_path.c_str());
if(res != 0)
{
perror("unlink");
}
cout << "creater free named pipe." << endl;
}
if(_fd != DefaultFd) close(_fd);
}
bool OpenForRead()
{
return OpenNamedPipe(Read);
}
bool OpenForWrite()
{
return OpenNamedPipe(Write);
}
int ReadNamedPipe(string *out)
{
char buffer[BaseSize];
int n = read(_fd,buffer,sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
int WriteNamedPipe(string &in)
{
return write(_fd,in.c_str(),in.size());
}
private:
const string _fifo_path;
int _id;
int _fd;
};
三.共享内存
共享内存是由OS在物理内存中申请一块空间,通过页表映射到不同进程的虚拟地址空间中,从而使不同的进程之间可以进行通信。
深入理解共享内存:
1.上述共享内存的各种操作,都是由OS完成的。
2.OS需要提供必要的系统调用,供用户来进行调用。
3.共享内存可以在系统中同时存在多份,供不同个数,不同对进程同时进行通信。
4.OS要对所有的共享内存进行管理,要有对应的数据结构和匹配算法。
5.共享内存 = 内存空间(数据) + 共享内存的属性。
1.共享内存实现
共享内存创建函数:
#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);
参数:
key,由用户给出,标识该共享内存的唯一性。
size,共享内存的大小。
shmflg,位图,拥有两种写法IPC_CREAT和IPC_EXCL。
返回值:
创建成功返回共享内存的标识符,失败返回-1。
该返回值称为shmid,是内核给用户返回的一个标识符,用来进行用户级对共享内存进行管理的id值。
IPC_CREAT:如果你创建的共享内存不存在,就创建,如果存在,获取该共享内存并返回。
IPC_EXCL:单独使用没有意义,只有和IPC_CREAT组合使用才有意义。
IPC_CREAT|IPC_EXCL:如果你要创建的共享内存不存在,就创建,如果存在,就出错返回,如果成功返回,就说明创建一个全新的共享内存。
共享内存共享函数:
** #include<sys/types.h>**
#include<sys/ipc.h>
*key_t ftok(const char pathname,int proj_id);
参数:
pathname:已存在的路径名,用于生成key值,用户随机定义。
proj_id:项目ID,用户随机定义即可。
返回值:
key:属于用户形成,内核使用的一个字段,用户不能使用key来进行shm的管理,是内核进行区分shm的唯一性标识。
** 多个不同的进程通过调用该函数,传入完全相同的参数,从而得到相同的返回值key,再作为shmgat函数的参数传入,即可完成共享内存的创建**。
共享内存控制(删除)函数:
#include<sys/ipc.h>
#include<sys/shm.h>
*int shmctl(int shmid,int cmd,struct shmid_ds buf);
参数:
shmid:即共享内存唯一标识符。
cmd:指令集,其中IPC_RMID为删除共享内存,IPC_STAT为获取共享内存属性。
buf:输出型参数,获取共享内存的属性。
共享内存挂接函数:
#include<sys/types.h>
#include<sys/shm.h>
**void shmat(int shmid,const void shmaddr,int shmflg):为进程挂接共享内存。
*int shmdt(const void shmaddr):为进程取消挂接共享内存。
参数:
shmid:即共享内存唯一标识符。
shmaddr:共享内存要挂接的地址,不使用置为nullptr。
shmfig:共享内存访问权限。
返回值:
viod *:成功返回共享内存在地址空间中的起始地址,失败返回nullptr。
** 共享内存的注意事项:**
共享内存不随着进程的结束而自动释放,会一直存在,直到系统重启,或手动进行释放。
共享内存是所有进程通信方式中,速度最快的,因为大大减少了数据的拷贝次数。
共享内存本身不保护共享内存数据,可以借助管道进行保护。
2.IPC指令
ipcs -m:查看当前所有的共享内存
ipcrm -m + shmid:删除某共享内存
四.信号量
1.五个概念
- 多个执行流能看到的一份资源,称为共享资源。
- 被保护起来的资源称为临界资源,用互斥的方式保护的共享资源,也是临界资源。
- 互斥:任何时刻只能有一个进程在访问共享资源。
- 资源要被程序员通过代码访问。代码 = 访问共享资源的代码 + 不访问共享资源的代码
- 所谓对共享资源进行保护,本质是对访问共享资源的代码进行保护。
信号量的本质,就是用来保护临界资源的一个计数器,一个公共资源。
对信号量的操作即PV操作,P为申请,V为释放。
信号量获取函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key,int nsems,int shmflg);
** 信号量控制函数:**
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semctl(int seqid,int semnum, int cmd,...);
信号量操作函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>*int semop(int semid,struct sembuf sops,size_t nsops);
信号量的这些接口与共享内存类似。
五.消息队列
消息队列的通信方式为:一个进程向另一个进程发送数据块的方式,所有的数据块由队列管理。
消息队列创建函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int shmget(key_t key,size_t size,int shmflg);
消息队列控制函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>*int msgctl(int msqid, int cmd, struct msqid_ds buf);
消息队列收/发函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>*int msgsnd(int msqid, const void msgp, size_t msgsz, int msgflg);//发送
*ssize_t msgrcv(int msqid, void msgp, size_t msgsz, long msgtyp,int msgflg);//接收
消息队列的这些接口同样与共享内存类似。
版权归原作者 很楠不爱 所有, 如有侵权,请联系我们删除。