通信的本质
因为进程具有独立性,我们要进行通信的成本一定不低,我们要先让不同的进程看到同一份资源,之后再进行通信。所以,通信的本质是:1.操作系统直接或间接给通信双方的进程提供内存空间2.要通信的进程,必须看到同一份资源!不同的通信类型的本质就是:上面所说的资源是OS中哪一个模块提供的(比如公共资源是文件系统提供的那么就是管道通信)。
匿名管道
工作原理
每个进程都有自己的task_struct结构体,其中又有一个指针,指向自己的文件描述符表,文件描述符表中又保存了所有文件描述符,通过文件描述符就可以对文件进行读写操作。有了这些前置知识,我们直接先上原理图(省去了task_struct)
第一步:在父进程中调用pipe函数,该函数会生成一个管道文件,并开启读端和写端,将读写操作符通过传入的参数进行返回。
第二步:创建子进程,子进程会继承父进程的文件描述符表,同样可以对管道文件有读写操作。
第三步:分别关闭父进程的读端和子进程的写端,使得形成只能父进程往里写,子进程从中读的半双匿名管道
代码实现
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<cstdlib>
#include<cstring>
using namespace std;
int main() {
//第一步,创建管道文件,打开读写端
int pipefd[2];
int n = pipe(pipefd);
assert(n == 0);
//第二步,fork
int fd = fork();
assert(fd >= 0);
if (fd == 0) {
//子进程进行写入
int cnt = 0;
close(pipefd[0]);//关闭读
char buffer[1024];
const char* s = "你好我是子进程,我正在写入";
while (true) {
snprintf(buffer, sizeof(buffer) - 1, "pit->%d:%s\n", getpid(), s);
write(pipefd[1], buffer, strlen(buffer));
cout << "count: " << ++cnt << endl;
sleep(2);
}
exit(0);
}
close(pipefd[1]);
char buffer[1024];
while (true) {
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s > 0) { //读成功
cout << "get message: " << buffer << endl;
}
}
return 0;
}
读写规则
当没有数据可读时
O_NONBLOCK disable(阻塞调用):read调用阻塞,即进程暂停执行,一直等到有数据来为止
O_NONBLOCK enable(非阻塞调用):read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable(阻塞调用):write调用阻塞,直到有进程读走数据。
O_NONBLOCK enable(非阻塞调用):write调用返回-1,errno值为EAGAIN。
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,write操作会产生SIGPIPE信号,进而导致write进程退出
管道特性
1.管道的生命进程随进程
2.管道可以用来进行具有血缘关系的进程之间通信,常用于父与子
3.管道是面向字节流的
4.管道是半双工通信(单向通信)
5.互斥与同步机制--对共享资源进行保护的方案
命名管道
工作原理
由于匿名管道的使用有一个局限,只能应用于具有血缘关系的进程间通信。我们想在不相关进程之间交换数据,就可以使用FIFO文件来做这项工作,它经常被称为命名管道。那么命名管道又是如何让不同的进程,看到同一份资源的?答案是让不同的进程打开指定名称(路径+文件名)的同一个文件,这就是命名管道的工作原理
代码实现
命名管道代码思路更加简单,只需让通信的进程通过mkfifo函数创建一个管道文件,后续进行正常的读写就可以了。
//创建fifo文件的代码
bool creatFifo(const string& s) { //s是文件路径+文件名
umask(0);
int n = mkfifo(s.c_str(), 0600); //给定文件名、权限
if (n == 0)return true;
cout << "errno:" << errno << " string er:" << strerror(errno) << endl;
return false;
}
void removeFifo(string& s) { //s是文件路径+文件名
int n = unlink(s.c_str());
assert(n == 0);
(void)n;
}
共享内存
工作原理
通过让不同进程,看到同一个内存块的方式就是共享内存。
我们直接上图,步骤先后顺序在图中有标号
认识接口
notice:未提及的参数如想了解可自行从手册查找,在此篇文章中不重要。
#include<sys/types.h>
#include<sys/ipc.h>
ket_t ftok(const char*pathname,int projectid)
函数功能:通信的进程通过相同的位置和进程id即可获取相同的key值
返回值:用于后续创建共享内存
pathname:路径
projectid:进程id
#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(ket_t key,size_t size,int shmflg)
函数功能:申请一块共享内存的空间
返回值:是数组下标,但是跟文件系统是两套,只需知道后续操作用它作标识符即可
key:通过ftok函数获取的key值
size:共享内存的大小
shmflag:位图,如:IPC_CREAT表示不存在则创建,存在则获取;IPC_EXCL无法单独使用,可以IPC_CREAT|IPC_EXCL表示不存在则创建,存在就返回错误,可以保证给用户的一定是一个新的shm
//shmid是shmget的返回值,如果将cmd设置为IPC_RMID表示释放共享内存
int shmctl(int shmid,int cmd,shmid_ds *buf)
//关联操作,返回值为地址,后续直接一端向该地址写,另一端读,即可完成通信
void* shmat(int shmid,const void*shmaddr,int shmflg)
//去关联操作,shmaddr为关联操作获取的地址,该函数失败返回-1
int shmdt(const void*shmaddr)
代码实现
//file name: comm.hpp
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cerrno>
#include<cstring>
#include<cstdbool>
#include<unistd.h>
#define PATH_NAME "."
#define PROH_ID 0x66
using namespace std;
//通过ftok函数获取key
int getKey() {
key_t key = ftok(PATH_NAME, PROH_ID);
if (key < 0) {
cerr << errno << ":" << strerror(errno) << endl;
exit(1);
}
return key;
}
//创建共享内存
int creatShm(key_t key) {
int shmid = shmget(key, 1024, IPC_CREAT | IPC_EXCL | 0600);
if (shmid < 0) {
cerr << errno << ":" << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int getShm(key_t key) {
int shmid = shmget(key, 1024, IPC_CREAT);
if (shmid < 0) {
cerr << errno << ":" << strerror(errno) << endl;
exit(2);
}
return shmid;
}
//关联
void* attchShm(int shmid) {
void* ad = shmat(shmid, nullptr, 0);
if ((long long)ad < 0) {
cerr << errno << ":" << strerror(errno) << endl;
exit(3);
}
return ad;
}
//去关联
void detchShm(void* ad) {
int n = shmdt(ad);
if (n < 0) {
cerr << errno << ":" << strerror(errno) << endl;
exit(4);
}
}
//删除共享内存
void desShm(int shmid) {
if (shmctl(shmid, IPC_RMID, nullptr) == -1) {
cerr << errno << ":" << strerror(errno) << endl;
exit(5);
}
}
这个文件主要是对上面接口进行封装,以便服务端和客户端进行使用
#include"comm.hpp"
int main() {
key_t key = getKey();
cout << "获取key成功" << endl;
int shmid = creatShm(key);
cout << "创建共享内存成功" << endl;
void* ad = attchShm(shmid);
cout << "进程和共享内存挂接成功" << endl;
///发消息
char s[1024] = "我是client我在发送消息";
int cnt = 1;
while (true) {
sprintf(s, "%s:pid[%d]cnt[%d]\n", "我是client我在发送消息", getpid(),
cnt++);
memcpy(ad, s, sizeof(s));
sleep(2);
}
detchShm(ad);
cout << "去关联成功" << endl;
return 0;
}
该文件为客户端的代码,就是按照共享内存的原理,通过事先规定好的路径和projectid获取到key值、通过key值创建共享内存、关联、向共享内存中发送数据、去关联、删除共享内存
#include"comm.hpp"
int main() {
key_t key = getKey();
cout << "获取key成功" << endl;
int shmid = getShm(key);
cout << "创建共享内存成功" << endl;
void* ad = attchShm(shmid);
cout << "进程和共享内存挂接成功" << endl;
///收消息
while (true) {
printf("%s", ad);
sleep(2);
}
detchShm(ad);
cout << "去关联成功" << endl;
desShm(shmid);
cout << "删除共享内存成功" << endl;
return 0;
}
这个文件即为服务器的代码,通过与客户端相同的路径和projectid获取到key值、通过key值创建共享内存、关联、在共享内存中读取数据、去关联、删除共享内存
注意:运行代码时,应先运行客户端创建好共享内存,再运行服务器。
其他进程间通信方法
除了上述两种方法之外,进程间通信的方法还有,信号量、信号、socket用于不同主机间进程通信等。其中信号量在生产者消费者模型精讲-CSDN博客中有讲解和使用,另外的的方法由于篇幅问题,先不进行讲解。如有需要可以评论区留言。
标签:剖析,include,int,间通信,管道,key,进程,共享内存 From: https://blog.csdn.net/qq_55008871/article/details/141428485