Linux——进程间通信

很楠不爱 2024-10-15 10:07:01 阅读 62

一.理解进程间通信

进程是一个独立的个体,但是近处也需要某种协同,而协同的前提就是进程间的通信。

进程间通信的前提是

先让不同的进程看到同一份操作系统提供的资源(“一段内存”),一定是某个进程先需要通信,让OS创建一个共享资源,所以OS必须提供很多的系统调用。

OS创建的共享资源的不同,系统调用接口的不同,说明进程间通信会有不同的种类。

进程通信的常见方式

消息队列管道共享内存信号量


 二.管道

 1.匿名管道实现

匿名管道是父子进程之间进行通信的一种方式,父子进程通过对同一份文件进程读写操作,会同时得到文件的文件描述符,通过读写分离的操作,在文件的内核缓冲区——即管道中进行通信,而缓冲区里的数据不需要刷新的磁盘中。 

 命令行中使用的“|”,就是匿名管道

#include<unistd.h>   

//创建匿名管道

int pipe(int pipefd[2]);//输出型参数,0号下标->r,1号下标->w,创建成功返回0,失败返回-1.

<code>#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);//接收

消息队列的这些接口同样与共享内存类似。 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。