装置艺术那个网站做的好,乐清网页设计公司哪家好,小程序加盟代理优势,上海滕州建设集团网站#x1f30e;进程间通信 文章目录#xff1a;
进程间通信 进程间通信简介 进程间通信目的 初识进程间通信 进程间通信的分类 匿名管道通信 认识管道 匿名管道 匿名管道测试 管道的四种…进程间通信 文章目录
进程间通信 进程间通信简介 进程间通信目的 初识进程间通信 进程间通信的分类 匿名管道通信 认识管道 匿名管道 匿名管道测试 管道的四种情况 管道的五种特性 管道的读写规则 命名管道 命名管道通信 命名管道打开规则 System V 共享内存 工作原理 共享内存接口 shmget接口 ftok接口 共享内存编码模拟 编码初步构建 删除共享内存 共享内存各个属性 共享内存正式代码 System V 消息队列 System V 信号量 信号量相关概念铺垫 信号量 信号量相关接口 System V 共享内存、消息队列、信号量的共性 进程间通信简介
✈️进程间通信目的 数据传输一个进程需要将它的数据发送给另一个进程 资源共享多个进程之间共享同样的资源。 通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止时要通知父进程。 进程控制有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变。 通过之前的学习我们知道进程之间具有独立性为了保持这个特性所以进程之间不存在数据直接传递的情况。在许多场景下需要进程之间相互配合所以需要进程间通信。
✈️初识进程间通信 进程间通信最朴素的说法是一个进程把数据交给另一个进程即可。而想要进程之间进行通信必须保证每个进程的独立性。所以在进程之间就需要一个交换数据的空间并且该 空间内存不能由通信双方任何一个提供。 由此可知进程间通信的本质是 先让不同的进程看到同一份资源通常为操作系统提供。而具体的做法如下几种。
✈️进程间通信的分类 操作系统提供的“空间” 有不同样式就决定了有不同的通信方式分为以下几种
管道通信 匿名管道pipe 命名管道
System V IPC System V 消息队列 System V 共享内存 System V 信号量
POSIX IPC 消息队列 共享内存 信号量 互斥量 条件变量 读写锁 匿名管道通信
✈️认识管道 管道是Unix中最古老的进程间通信方式我们把一个进程连接到另外一个数据流称为一个 “管道”。比如我们层学过的管道符号‘|’。 在详细谈论管道的概念之前先来回顾一下文件描述符与缓冲区文件描述符表的前三位分别指向标注输入、标准输出、标准错误。进程自己创建的文件则从3号下标为初始点位。 如今我们使用open()接口分别以 ‘r’ 和 ‘w’ 的方式打开同一个文件虽然是同一个文件但是 操作系统会分配两个文件描述符分别指向同一个文件。 每个文件都有自己的缓冲区每个文件在读写之前都需要把数据从磁盘先加载到内存当中再有内核加载到缓冲区中而log.txt文件只有一份所以两个文件指向同一个缓冲区。 接着父进程进行fork创建子进程我们知道子进程创建时会对父进程页表、文件描述符表等数据进行 浅拷贝而他们指向的内存空间还是同一个。 有人会问这跟进程间通信有什么关系别忘了进程间通信的本质是 让不同进程看到同一份资源而上述这种方式就做到了双方看到同一份资源所以 管道 就是基于文件的让不同进程看到同一份资源方式 就是管道。 管道在设计时为了让管道更简单所以管道被设计为只能单向通信所以我们可以把两个进程一个负责读数据一个负责写数据也就是设置读写端。假设父进程为reader子进程为writer 而为什么我们两个文件一个为读端一个为写端这样设计因为当父进程fork出子进程的时候同时把文件描述符表也拷贝下来这样父子进程的两个文件描述符都分别是读端和写端这时候只需要父子进程禁用掉不同的一个端就可以构建管道通信了 ✈️匿名管道 操作系统不让用户直接操作管道文件因为用户可能会造成权限问题、文件覆盖数据泄露等问题。所以给我们提供了一个用于管道通信的接口
int pipe(int pipefd[2]);pipefd[2]输出型参数文件描述符数组,其中pipefd[0]表示读端, pipefd[1]表示写端。返回值成功返回0失败返回错误代码。 pipe接口不需要向磁盘中刷新且磁盘中并不存在的文件。通过调用pipe接口系统会 生成一个内存级的文件。这种文件没有文件名所以也叫匿名文件、而这种使用方式则被称为 匿名管道 那么匿名管道如何让不同进程看到同一份资源呢原理就是有父进程创建子进程子进程继承父进程的相关属性信息。通过相同的文件描述符表从而将两个进程联系起来。
匿名管道特点只能与有血缘关系的进程来进行进程间通信。常常用于父子进程。 为了更加深刻理解匿名管道通信我们站在文件描述符的角度来理解管道通信。因为管道通信需要有血缘关系的进程之间通信所以无法避免的我们需要使用fork创建子进程来通信:
1.父进程创建管道文件 2.父进程fork出子进程 3.父进程关闭pipefd[0]子进程关闭pipefd[1] ✈️匿名管道测试 管道究竟该怎么使用我们不妨编写一段代码熟悉一下在编写之前先确定几个事项父子进程读写问题这里我以父进程为w端子进程为r端相反也行。
#includestdio.h
#includestring.h
#includestdlib.h
#includeunistd.h
#includesys/stat.h
#includesys/wait.h
#includesys/types.hvoid writer(int wfd)//写端调用
{const char* str hello father, I am child;char buffer[128];int cnt 0;pid_t pid getpid();while(1){snprintf(buffer, sizeof(buffer), messge:%s, pid: %d, count: %d\n, str, pid, cnt);//向buffer内写入strwrite(wfd, buffer, sizeof(buffer));//通过系统调用对管道文件进行写入cnt;sleep(1);}
}void reader(int rfd)//读端调用
{char buffer[1024];while(1){ssize_t n read(rfd, buffer, sizeof(buffer) - 1);//系统文件与C语言没关系所以不算 \0(void)n;//返回值用不到避免警告制造的假应用场景printf(father get a message: %s, buffer);}
}int main()
{// 创建管道int pipefd[2];int n pipe(pipefd);if(n 0) return 1;printf(pipefd[0]: %d, pipefd[1]: %d\n, pipefd[0]/*reader*/, pipefd[1]/*writer*/);// fork子进程pid_t id fork();if(id 0){// child w端close(pipefd[0]);writer(pipefd[1]);exit(0);}//father r端close(pipefd[1]);reader(pipefd[0]);//通过系统调用 对管道文件读取wait(NULL);return 0;
}整体的代码结构还是比较简单易懂的我们通过循环脚本来监视代码观察是否按预期运行 ✈️管道的四种情况 管道作为最古老的一种进程间通信方式其优点与弊端也早就被程序员们挖掘出来了我们来看看管道通信有哪些特性吧。 情况一 还是上述匿名管道测试代码子进程一直在写父进程一直在读子进程写的数据现在我们让子进程等待五秒之后再对管道文件进行写入 那么问题就来了在子进程休眠的这五秒期间父进程在干吗实际上在子进程休眠的这5秒父进程在等待子进程休眠结束直到子进程再次写入数据时父进程才会读取。 所以我们的 结论 就是管道内部没有数据的时候并且其中的写端不关闭自己的文件描述符时读端就要进行阻塞等待直到管道文件有数据。 情况二 第二中情况当写端一直在对管道文件进行写入而读端却不再对管道文件一直执行sleep进行读取我们修改写端接口如下
void writer(int wfd)
{const char* str hello father, I am child;char buffer[128];int cnt 0;pid_t pid getpid();while(1){// snprintf(buffer, sizeof(buffer), messge:%s, pid: %d, count: %d\n, str, pid, cnt);//向buffer内写入str// write(wfd, buffer, sizeof(buffer));//通过系统调用对管道文件进行写入char* ch X;write(wfd, ch, 1);cnt;printf(cnt: %d\n, cnt);}
}如果我们编译运行程序我们会发现写端对管道文件一直写入一个字符但是到了第65536个字符时却卡在这里了。 其实这个时候 写端在阻塞这是因为我们写入的对象也就是 管道文件 被写满了从计数器我们可以看出一个管道文件的大小为 65536 个字节ubuntu20.04也就是 64KB 大小 注意管道文件的大小依据平台的不同也各不相同。 所以我们得到的 结论 是当管道内部被写满且读端不关闭自己的文件描述符写端写满之后就要进行阻塞等待 情况三 当写端对管道文件缓冲区进行了有限次的写入并且把写端的文件描述符关闭而读端我们保持正常读取内容读端多的仅仅把读端的返回值打印出来。 我们发现当10读取执行完成之后就一直在执行读取操作而我们读取使用的 read 接口的返回值却从0变为了1。我们接着用监视窗口来监视一下 当写端写了10个数据之后将文件描述符关闭那么读端进程就会变为僵尸状态。由此我们可以得出read接口返回值的含义 是当写端停止写入并关闭了文件描述符read的返回值为0正常读取的返回值 0。
所以我们可以这样修改读端的代码
void reader(int rfd)
{char buffer[1024];while (1){ssize_t n read(rfd, buffer, sizeof(buffer) - 1);if (n 0)printf(father get a message: %s, ret: %ld\n, buffer, n);else if (n 0){printf(read pipe done, read file done!\n);break;}elsebreak;}
}所以我们就能得出 结论 对于读端而言当读端不再写入并且关闭了pipe那么读端将会把管道内的内容读完最后就会读到返回值为0表示读取结束类似于读到了文件的结尾。 情况四 我们把情况三最后的代码变换一下读端读取改为有次数限制并且读取一定次数之后关闭读的文件描述符而写端无限制对管道文件写入那么我们会看到什么现象呢 而我们发现似乎也没什么不对啊读取完之后不就直接退出了吗你应该仔细想想我们仅仅是关闭了读的文件描述符但是没有关闭写的文件描述符啊。 这就是最后一个 结论当读端不再进行读取操作并且关闭自己的文件描述符fd而写端依旧在写。那么OS就会通过信号SIGPIPE的方式直接终止写端的进程。 如何证明读端是被13号信号杀死的我们采用的是父进程读子进程写的方式也就是说将来子进程被杀死而父进程则可以通过wait的方式来获取子进程退出时的异常
int status 0;
pid_t rid waitpid(id, status, 0);if(rid id)
{printf(exit code : %d, exit signal : %d\n, WEXITSTATUS(status), status 0x7F);
}✈️管道的五种特性 根据管道的4种特殊情况也就间接的创造了管道的5个特性分别来认识管道的5种特性。 第一、二种 根据情况一和情况二两者结合来看当管道文件有数据时读端就读有空间写端就进行写入。而当管道缓冲区没有空间时写端停止写入当管道没有数据时读端就不读了。 换句话说父子进程w 和 r之间是具有明显的执行顺序的。父子进程之间会协调他们之间的步调。这样我们的第一个特性也就出来了 特性一父子进程读写端自带同步机制。 特性二管道是以具有血缘关系的进程通信的常见于父子关系。 第三种 我们让写端一直向管道内写而读端控制在特定时间内进行读取。也就是让写端一直写读端间断读。 我们可以发现写端在写满了之后就等待读端读取当读取一部分之后写端就又会 从刚才停止的地方继续对管道内进行写入 虽然写端写满了但是为何读端一次性会读取那么多的数据呢其实这个情况现在并不好解释以后在学习网络时会有详细解读这里我们只需要知道 特性三管道是面向字节流的。 第四种 普通文件退出时操作系统会自动释放掉这个文件而我们管道文件也是文件所以我们第四种特性就是 特性四父子进程退出管道将会自动释放这也就说明文件的声明周期是跟随进程的。 第五种 其实最后一种我们潜移默化的已经知道了从我们写的第一份管道代码起管道的通信都是一个进程读一个进程写所以我们的最后一种特性就是 特性五管道只能单向通信并且管道通信是一种半双工的特殊情况。
全双工数据可以在两个方向上同时传输允许通信双方同时发送和接收数据。比如网络中 tcp 协议就是采用 全双工通信方式。
半双工数据只可以在两个方向的其中一个方向上传输但是不能两个方向都传输。比如我们日常对话就是半双工模式。 ✈️管道的读写规则 当没有数据可读时 O_NONBLOCK disableread调用阻塞即进程暂停执行一直等到有数据来到为止。 O_NONBLOCK enableread调用返回-1errno值为EAGAIN。
当管道满的时候 O_NONBLOCK disable write调用阻塞直到有进程读走数据 O_NONBLOCK enable调用返回-1errno值为EAGAIN 如果所有管道写端对应的文件描述符被关闭则read返回0如果所有管道读端对应的文件描述符被关闭则write操作会产生信号SIGPIPE,进而可能导致write进程退出。当要写入的数据量不大于 PIPE_BUF 时linux将保证写入的 原子性。当要写入的数据量大于 PIPE_BUF 时linux将不再保证写入的 原子性原子性将在线程篇作详细解释。 命名管道
✈️命名管道通信 命名管道与匿名管道有什么区别其实在名字上就可以看出来。命名管道的管道文件是有名字的而不同的是命名管道可以让不同的进程之间可以通信让不同的进程看到同一份资源。 这里不同的进程不仅仅指有血缘关系的进程没有血缘关系的进程依旧适用。要让两个进程之间进行通信那么就必定需要让两个进程看到同一份资源 而要打开管道文件那么每个进程就必定要有对应的struct file结构体对象但是OS不会让一个文件存在两个属性和两个重复的缓冲区所以实际上 两个file的inode是同一个文件的inode而它们的缓冲区也指向同一个缓冲区 但是这样的话怎么能保证两个不同的进程打开的是同一个文件呢在平常我们是通过 文件路径 文件名 来找到文件的。而命名管道文件也是如此
我们使用如下命令创建命名管道文件
mkfifo pipe_name #创建命名管道文件FIFO表示先进先出而管道其实就是一种队列它的字节流就是先进先出。管道文件在创建完成之后我们在Shell中可以发现 管道文件创建出来之后OS甚至会在文件名后面加上 ‘|’ 来表示这是一个管道文件并且在文件权限那里我们能够看到开头为 ‘p’也表示pipe文件。 那么如何使用代码创建管道文件呢我们来认识一个接口
int mkfifo(const char*pathname, mode_t mode);pathname参数需要生成管道文件的路径信息。 mode参数生成管道文件的权限位受权限掩码的影响。 返回值成功创建管道返回0创建失败返回-1并且设置错误码。 基于此我们来写一个不同进程之间使用命名管道的简单通信
Comm.hpp
#ifndef __COM_HPP__
#define __COM_HPP__#include iostream
#include sys/types.h
#include cerrno
#include cstring
#include sys/stat.h
#include unistd.h
#include string#define Mode 0666 // 设置权限位// 把管道通信封装为一个类
class Fifo
{
public:Fifo(const std::string path):_path(path)// 构造函数创建管道文件{umask(0);// 消除权限掩码的影响int n mkfifo(_path.c_str(), Mode);// 调用接口创建管道文件if(n 0)// 根据返回值做判断{std::cout mkfifo sucess std::endl;}else{// 创建失败则打印出错误信息并且导出错误码std::cerr mkfifo failed, errno: errno , errstring: strerror(errno) std::endl;}}~Fifo(){}
private:std::string _path;
};#endifpipe_client.cpp
#include Comm.hppint main()
{std::cout hello client std::endl;return 0;
}pipe_server
#include Comm.hppint main()
{// 创建文件Fifo fifo(./fifo);sleep(1);return 0;
}makefile
.PHONY:all #依次生成多个可执行程序将all的依赖方法置空即可
all:pipe_client pipe_server pipe_server:PipeServer.ccg -o $ $^ -stdc11
pipe_client:PipeClient.ccg -o $ $^ -stdc11.PHONY:clean
clean:rm -f pipe_client pipe_server我们执行了两次可执行程序第二次就报错了报错信息也打印出来了报错原因是文件已经存在。如果我们想在代码里让创建的管道析构那么可以调用下面接口
int unlink(const char* pathname);pathname需要删除的文件名文件路径。 返回值与mkfifo返回值含义相同。
~Fifo()
{sleep(10);// 等待10s 再析构int n unlink(_path.c_str());// 删除管道文件if(n 0){std::cout remove fifo file _path sucess std::endl;}else{std::cerr remove failed, errno: errno , errstring: strerror(errno) std::endl;}
}这样文件就可以删除了至此我们就初步搭建好管道文件了接下来就可以写通信的代码了。 这里我以 客户端为写端writer服务器端为读端reader并且由服务端创建好管道文件那么代码编写如下
pipe_client
#include Comm.hppint main()
{int wfd open(PATH, O_WRONLY);// 客户端为writer以只写的方式打开文件if(wfd 0)// 当wfd0时打印错误信息{std::cerr open failed, errno: errno , errstring: strerror(errno) std::endl;return 1;}std::string inbuffer;while(true){std::cout Please enter your message# ;std::getline(std::cin, inbuffer);// 从标准输入里获取信息到inbuffer里// 消息为quit则退出if(inbuffer quit) break;// 发消息ssize_t n write(wfd, inbuffer.c_str(), inbuffer.size());// 对inbuffer数组进行写入操作if(n 0)// 当n 0 时我们需要将对应的报错信息打印出来{std::cerr write failed, errno: errno , errstring: strerror(errno) std::endl;break;}}close(wfd);// 执行完毕关闭文件描述符return 0;
}pipe_server
#include Comm.hppint main()
{Fifo fifo(PATH);// 创建管道文件int rfd open(PATH, O_RDONLY); // 服务端为读端以只读的方式打开文件if(rfd 0)// 文件打开失败打印错误信息以及错误码{std::cerr open failed, errno: errno , errstring: strerror(errno) std::endl;return 1;}char buffer[1024];while(true)// 一直对客户端进行读取{ssize_t n read(rfd, buffer, sizeof(buffer) - 1);if(n 0){buffer[n] 0;std::cout client say : buffer std::endl;}else if(n 0){std::cout client quit, me too! std::endl;break;}else// 读取文件失败时打印错误信息{std::cerr read failed, errno: errno , errstring: strerror(errno) std::endl;break;}}close(rfd);// 关闭文件fdreturn 0;
}完整的源代码戳这里命名管道通信。 这里还有一个点需要注意当仅仅运行服务器端时会卡在那里这是因为 调用open接口的时候就会阻塞等待直到写端对管道文件进行写入时 open 才会返回。 ✈️命名管道打开规则
如果当前打开操作是为读reader而打开FIFO时 O_NONBLOCK disable阻塞直到有相应进程为写而打开该FIFO O_NONBLOCK enable立刻返回成功
如果当前打开操作是为写writer而打开FIFO时 O_NONBLOCK disable阻塞直到有相应进程为读而打开该FIFO O_NONBLOCK enable立刻返回失败错误码为ENXIO System V 共享内存
✈️工作原理 首先我们要明白共享内存是为了让进程之间进行通信所以共享内存一定也遵守着 让不同进程看到同一份资源 的原则而共享内存可以让毫不相干的进程之间进行通信。 当两个进程之间使用共享内存进行通信的时。首先操作系统在内存中开辟一段物理空间作为 共享内存然后在通过页表建立映射关系将共享内存映射到进程地址空间的共享区。最后将 地址空间共享区映射位置的起始地址返回给用户。 于是用户就可以拿到虚拟地址在经由页表映射到共享内存的起始地址。而不论是mm_struct进程地址空间还是页表都属于内核数据结构所以构建映射以及返回虚拟地址等操作都是由操作系统来完成的。 当两个进程都对同一块共享内存建立了映射关系那么它们就可以 通过共享内存块来看到同一份资源于是就满足进程间通信的条件。以上就是共享内存的工作原理。 ✈️共享内存接口
shmget接口 多说无益码上见真章在实现System V 共享内存的代码之前我们需要先认识一个接口 shmget 用来 申请共享内存
int shmget(key_t key, size_t size, int shmflag);参数及返回值含义
参数/返回值含义key共享内存段的标识符与进程id类似size共享内存大小shmflag由九个权限标志构成它们的用法和创建文件时使用的mode模式标志是一样的返回值成功返回一个非负整数即该共享内存段的标识符失败返回 -1同时错误码被设置。 参数key和参数shmflag需要单独来解释一下。首先我们要明白共享内存进程间通信并不仅仅局限于一对进程未来我们可以在 内存中创建多个共享内存 从而支持多对进程都可以进行通信。 所以说 共享内存再内存中可以存在很多个那么 多个共享内存是一定要被操作系统管理的。操作系统如何管理共享内存先描述再组织 将每一个共享内存的属性抽离用结构体将属性组织于是对共享内存属性的管理就变为了对共享内存结构体的管理。而有那么多的进程操作系统怎么知道那两个进程是在使用同一个共享内存的呢 所以OS为了识别不同进程进行通信的共享内存于是也给共享内存添加了一个 标识符key其与进程的标识符类似不同的共享内存key值具有唯一性。 shmflag 参数是 用来指定创建共享内存的的权限其存在多个参数这些参数都是由宏构成而我们最常用的不过一下两个参数 IPC_CREAT选项如果共享内存不存在则创建。如果共享内存已经存在则获取这个共享内存。 IPC_EXCL选项此选项不能单独使用无意义。 IPC_CREAT | IPC_EXCL如果共享内存不存在则创建共享内存。如果已经存在则报错。 而我们使用这两个选项尽量两个选项一起使用也就是第三种形式这样的好处就是只要我们共享内存创建成功了就一定是最新创建的 ftok接口 可是为什么共享内存标识符需要我们手动的去设置呢为何不能像进程那样分配一个标识符呢其实如果让操作系统来给我们传key这个参数是做不到的如果操作系统能将同一个key值传递给两个不同的进程 那还需要共享内存来做通信吗 基于此所以我们需要手动传参key值但是key值我们传什么呢其实key这个参数有专门的接口提供给用户使用
key_t ftok(const char* pathname, int proj_id);pathname路径名。 proj_id传入任意一个整数。 ftok的返回值就是key的类型而ftok接口其实是一个算法由我们传入的文件名和一个整数进行算法返回一个数字这个数字就是key值。至于这个值是多少并不重要只要能够标识唯一性即可。我们进程想要找到对应的共享内存拿上这个key值就可以找到对应的共享内存了。 ✈️共享内存编码模拟
编码初步构建 要想进行共享内存方式的进程间通信首先需要获取共享内存并且需要两个测试进程来获取共享内存Comm.hpp用来编写接口供客户端和服务端直接来调用。
Comm.hpp
#pragma once #include iostream
#include cerrno
#include sys/shm.h
#include string
#include sys/types.h
#include sys/ipc.h
#include cstdlib
#include cstringconst char* pathname /home/xzy/work/name_pipe/shm_ipc;// 创建路径
const int proj_id 0x100;// 任意整数
const int defaultsize 4096; // 字节为单位std::string ToHex(key_t k)//转换16进制
{char buffer[1024];snprintf(buffer, sizeof(buffer), %x, k);return buffer;
}key_t GetShmKeyOrDie()// 获取共享内存key值
{key_t key ftok(pathname, proj_id);if(key 0){std::cerr ftok error, errno: errno , errno string: strerror(errno) std::endl;exit(1);}return key;
}int CreateShmOrDie(key_t key, int size, int flag)// 共享内存创建方式用于二级调用
{int shmid shmget(key, size, flag);if(shmid 0){std::cerr shmget error, errno: errno , error string: strerror(errno) std::endl;exit(2);}return shmid;
}int CreateShm(key_t key, int size)// 仅创建共享内存
{return CreateShmOrDie(key, size, IPC_CREAT | IPC_EXCL);
}int GetShm(key_t key, int size)// 仅获取共享内存可能会创建
{return CreateShmOrDie(key, size, IPC_CREAT);
}ShmClient.cpp
#include Comm.hppint main()
{key_t key GetShmKeyOrDie();// 获取key值std::cout key: ToHex(key) std::endl;int shmid GetShm(key, defaultsize);// 获取共享内存key值客户端并不需要创建std::cout shmid: shmid std::endl;return 0;
}ShmServer.cpp
#include Comm.hppint main()
{key_t key GetShmKeyOrDie();std::cout key: ToHex(key) std::endl;// 将数字转换为16进制更美观int shmid CreateShm(key, defaultsize);// 创建共享内存std::cout shmid: shmid std::endl;return 0;
}Makefile
.PHONY:all
all:shm_client shm_servershm_server:ShmServer.ccg -o $ $^ -stdc11
shm_client:ShmClient.ccg -o $ $^ -stdc11
.PHONY:clean
clean:rm -f shm_client shm_server上述共享内存的代码还是很简单的但是我们再来看下面这个现象 为什么我们再次运行服务端想要创建一个共享内存却不行呢而报错信息显示的是文件已经存在说到底是共享内存已经存在。 删除共享内存 一个文件当我们对一个文件进行操作时一个进程打开一个文件进程退出的时候这个被打开的文件就会被系统自动释放掉也就是说 文件的生命周期随进程。 而我们在上述代码运行了共享内存运行的两个进程客户端、服务端都已经退出了当我们想再次创建共享内存时就被告知共享内存已存在。其实当我们 创建了共享内存如果 没有主动释放它则一直存在。 也就是说共享内存的生命周期随内核。除非重启系统。 虽然系统不能帮助我们自动释放共享内存但是系统给我们提供了删除共享内存的命令而在删除共享内存之前我们需要先查看系统中的共享内存
ipcs -m #查看系统中指定用户创建的共享内存删除共享内存在Linux中也有相对的指令只不过删除共享内存是通过shmid来删除的并不是通过key值来删除的原因我们稍后会提
ipcrm -m shmid #删除指定的共享内存删除共享内存并不仅仅只有指令级操作也有代码级操作我们同样可以调用删除接口shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);shmid由shmget返回的共享内存标识码。 cmd将要采取的动作三个可取值。 buf指向一个保存着共享内存的模式状态和访问权限的数据结构。 返回值成功返回0失败返回-1。
共享内存在内核中的数据结构
struct shmid_ds {struct ipc_perm shm_perm; /* operation perms */int shm_segsz; /* size of segment (bytes) */__kernel_time_t shm_atime; /* last attach time */__kernel_time_t shm_dtime; /* last detach time */__kernel_time_t shm_ctime; /* last change time */__kernel_ipc_pid_t shm_cpid; /* pid of creator */__kernel_ipc_pid_t shm_lpid; /* pid of last operator */unsigned short shm_nattch; /* no. of current attaches */unsigned short shm_unused; /* compatibility */void *shm_unused2; /* ditto - used by DIPC */void *shm_unused3; /* unused */
};// 可通过shmid_ds结构体对象调用ipc_perm
struct ipc_perm {key_t __key; /* Key supplied to shmget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions SHM_DEST andSHM_LOCKED flags */unsigned short __seq; /* Sequence number */
};cmd参数的三个动作 当然cmd参数的选项并不只有这三项但是最常用的就是这三个选项我们也可以看看man手册里对cmd这个参数的介绍 还记得我们使用指令删除共享内存吗为什么我们指定key来删除呢其实 不论是指令级还是代码级别最后对共享内存进行控制使用的都是shmid而key值站在内核的角度是 仅仅 用来区分shm的唯一性。而key值和shmid之间的关系就类似于打开文件的struct file* 和 文件fd。 共享内存各个属性 我们可以使用ipcs -m来查看共享内存但是我们在查看时会发现共享内存有一些我们并不认识的选项 key共享内存段的键值它是一个标识符进程通过key值来访问共享内存段key值常常使用ftok接口生成。 shmid共享内存段的标识符系统分配给共享内存的唯一标识。 owner指定共享内存创建的用户名。 perms共享内存段的权限位8进制在创建共享内存时shmflag参数可以添加共享内存权限。 bytes共享内存段大小字节为单位在Ubuntu20.04下最小单位为4096字节也就是4kb。 nattch共享内存进程使用数量表示有多少个进程正在使用该共享内存。 status共享内存段的状态。 为什么字节数和我上面给出的并不一致呢不是说好以4kb为单位的吗其实虽然在这里写的是4097但是内核会给我们开辟8kb的空间并且我们仅仅使用4097字节。而剩下的字节就会被浪费掉所以我们尽量将字节数写为4kb的整数倍。 共享内存正式代码 在写代码之前还需要认识两个接口shmat(shm attach)
int shmat(int shmid, const void *shmaddr, int shmflg);功能将共享内存段连接到进程地址空间。 shmid共享内存标识符。 shmaddr指定连接的地址即用户指定将shm挂接到哪里。 shmflag其两个可能取值是 SHM_RND 和 SHM_RDONLY。 返回值成功返回一个指针(地址空间的虚拟地址)指向共享内存的首地址失败返回-1并且设置错误码。
shmaddr说明 shmaddr为NULL核心自动选择一个地址 shmaddr不为NULL且shmflg无SHM_RND标记则以shmaddr为连接地址。 shmaddr不为NULL且shmflg设置了SHM_RND标记则连接的地址会自动向下调整为SHMLBA的整数倍。公式shmaddr - (shmaddr % SHMLBA) shmflgSHM_RDONLY表示连接操作用来只读共享内存 以及另外一个接口shmdtshm detach
int shmdt(const void *shmaddr);功能将共享内存段与当前进程脱离联系切断 shmaddr: 由shmat所返回的指针虚拟地址 返回值成功返回0失败返回-1并设置错误码
注意将共享内存段与当前进程脱离不等于删除共享内存段。 共享内存同样分为三个文件客户端、服务器端、头文件。头文件提供客户端和服务器端所需要的接口。
Comm.hpp
#pragma once #include iostream
#include cerrno
#include sys/shm.h
#include unistd.h
#include string
#include sys/types.h
#include sys/ipc.h
#include cstdlib
#include cstringconst char* pathname /home/xzy/work/shm_ipc;// 创建路径
const int proj_id 0x100;// 任意整数
const int defaultsize 4096; // 字节为单位std::string ToHex(key_t k)//转换16进制
{char buffer[1024];snprintf(buffer, sizeof(buffer), %x, k);return buffer;
}key_t GetShmKeyOrDie()// 获取共享内存key值
{key_t key ftok(pathname, proj_id);if(key 0){std::cerr ftok error, errno: errno , errno string: strerror(errno) std::endl;exit(1);}return key;
}int CreateShmOrDie(key_t key, int size, int flag)// 共享内存创建方式用于二级调用
{int shmid shmget(key, size, flag);if(shmid 0){std::cerr shmget error, errno: errno , error string: strerror(errno) std::endl;exit(2);}return shmid;
}int CreateShm(key_t key, int size)// 仅创建共享内存
{return CreateShmOrDie(key, size, IPC_CREAT | IPC_EXCL | 0666);// 权限设置为0666
}int GetShm(key_t key, int size)// 仅获取共享内存可能会创建
{return CreateShmOrDie(key, size, IPC_CREAT);
}void DeleteShm(int shmid)// 删除共享内存
{int n shmctl(shmid, IPC_RMID, nullptr);if(n 0){std::cerr shmctl error std::endl;}else// 成功删除{std::cout shmctl delete shm sucess, shmid: shmid std::endl;}
}void ShmDebug(int shmid)
{struct shmid_ds shmds;int n shmctl(shmid, IPC_STAT, shmds);if(n 0){std::cerr shmctl error std::endl;return;}//Debug 日志std::cout shmds.shm_segsz: shmds.shm_segsz std::endl;std::cout shmds.shm_nattch: shmds.shm_nattch std::endl;std::cout shmds.shm_ctime: shmds.shm_ctime std::endl;std::cout shmds.shm_perm.__key ToHex(shmds.shm_perm.__key) std::endl;
}void* ShmAttach(int shmid)
{void* addr shmat(shmid, nullptr, 0);// 连接进程 返回虚拟地址if((long long)addr -1)// 连接失败打印错误信息{std::cerr shmat error std::endl;return nullptr;}return addr;
}void ShmDetach(void *addr)// 解除关联
{int n shmdt(addr);if(n 0){std::cerr shmdt error std::endl;return;}
}ShmServer
#include Comm.hppint CreateShm()
{// 获取keykey_t key GetShmKeyOrDie();std::cout key: ToHex(key) std::endl;// 将数字转换为16进制更美观// 创建共享内存int shmid CreateShm(key, defaultsize);std::cout shmid: shmid std::endl;return shmid;
}int main()
{// 创建共享内存int shmid CreateShm();// 挂接共享内存char* addr (char*)ShmAttach(shmid);std::cout Attach shm sucess, addr: ToHex((uint64_t)addr) std::endl;// Server 通信for(;;){std::cout shm content: addr std::endl;sleep(1);}ShmDetach(addr);std::cout Detach shm sucess, addr: ToHex((uint64_t)addr) std::endl;sleep(5);// 删除共享内促DeleteShm(shmid);return 0;
}ShmClient
#include Comm.hppint main()
{key_t key GetShmKeyOrDie();// 获取key值std::cout key: ToHex(key) std::endl;int shmid GetShm(key, defaultsize);// 获取共享内存std::cout shmid: shmid std::endl;// 将客户端挂接到共享内存char* addr (char*)ShmAttach(shmid);std::cout Attach shm sucess, addr: ToHex((uint64_t)addr) std::endl;memset(addr, 0, defaultsize);// 通信开始for(char ch A; ch Z; ch){addr[ch-A] ch;sleep(1);}// 将与共享内存的挂接取消ShmDetach(addr);std::cout Detach shm sucess, addr: ToHex((uint64_t)addr) std::endl;sleep(5);return 0;
}首先在运行之前将监控脚本打起来一直检测是否连接成功然后运行服务器端读端再运行客户端写端 我们可以看到当我们仅仅运行服务器端的时候服务器端一直在进行读取并没有进行写入这个现象就很奇怪我们前面在运行管道文件的时候当管道内没有数据时读端是会阻塞等待的会与写端做一个协同。 其实这就是共享内存的一个 缺点共享内存不提供进程间通信协同的任何机制导致数据不一致但是它也有自己的 优点共享内存是所有进程间通信最快的 为什么说共享内存是进程间通信最快的一种通信方式呢其实如果你仔细品共享内存和用户之间是如何传递信息的就可以知道为什么共享内存会这么快了 共享内存是在内存中开辟的而我们前面说过共享内存会将数据从内存中加载到进程地址空间的共享区中这个过程只需要拷贝一次而用户则会通过页表获取加载进共享区的共享内存的起始地址整个过程并不需要过多的拷贝 而管道在运行时写端会先将数据从用户端拷贝写入到内核的管道文件中而读端读取数据时需要将数据从管道文件在拷贝到本地这样拷贝次数增多开销成本就变大自然比不过共享内存了。 为了保证数据的一致性只能由我们用户自己来实现我们可以 使用 信号量 的方式来实现共享内存但是我们还没有接触到。还有一种方式就是 使用管道来同步我们的共享内存因为 管道自带同步机制 而恰好我们前面也学习了管道文件我们可以复用上面写的命名管道并且添加一些同步机制让共享内存可以同步起来
#pragma once#include iostream
#include sys/stat.h
#include sys/types.h
#include unistd.h
#include cstring
#include fcntl.h
#include assert.h
#include cerrno
#include string#define Path fifo
#define Mode 0666// 创建管道文件
class Fifo
{
public:Fifo(const std::string path Path) :_path(path){umask(0);int n mkfifo(_path.c_str(), Mode);if(n 0){std::cerr mkfifo failed, errno: errno failed result: strerror(errno) std::endl;}std::cout mkfifo sucess, fifo pipe be created... std::endl;}~Fifo(){int n unlink(_path.c_str());if(n 0){std::cerr unlink fifo failed, errno: errno failed result: strerror(errno) std::endl;}std::cout unlink fifo sucess... std::endl;}private:std::string _path;
};// 同步机制
class Sync
{
public:Sync() :_wfd(-1), _rfd(-1){}void OpenRead()// 以读的方式打开文件{_rfd open(Path, O_RDONLY);if(_rfd 0){std::cerr open read failed, errno: errno failed result: strerror(errno) std::endl;exit(1);}}void OpenWrite()// 以写的方式打开文件{_wfd open(Path, O_WRONLY);if(_wfd 0){std::cerr open write failed, errno: errno failed result: strerror(errno) std::endl;exit(1);}}bool Wait()// 等待{bool ret true;uint32_t c;ssize_t n read(_rfd, c, sizeof(uint32_t));// 根据管道文件的特性读端在没有写端写入之前会一直处于等待状态if(n sizeof(uint32_t)){std::cout wakeup the process std::endl;return ret;}else if(n 0){std::cerr Wait failed, errno: errno failed result: strerror(errno) std::endl;ret false;}return ret;}void wakeup()// 唤醒{uint32_t c;ssize_t n write(_wfd, c, sizeof(uint32_t));// 同样根据管道的特性当写端对管道文件进行写入的时候我们的读端才能解除等待状态开始对管道文件内容进行读取if(n 0){std::cerr wakeup failed, errno: errno failed result: strerror(errno) std::endl;}std::cout wakeup server... std::endl;}~Sync(){}
private:int _wfd;// 写端fdint _rfd;// 读端fd
};将读写端设置完成之后我们就可以在客户端和服务器端对其进行调用了 这样再次运行其客户端和服务器端效果如下 对于共享内存内存以管道的方式实现同步的完整源代码点击以下链接共享内存通信管道实现同步机制 System V 消息队列 随着时代的进步System V 的本地通信逐渐被淘汰除了共享内存现在依旧存有不少应用场景其他类似消息队列这种技术已经被逐渐淘汰我们在这里只需要简单了解即可。 消息队列属于内核数据结构用户层不可对其随意修改只能通过系统提供的接口对消息队列的内容进行写入和读取。 用户层的 每个进程都可以是读写端每个既可以向消息队列中写入数据也可以从消息队列中读取数据。 系统中的消息队列那么多我怎么知道你给我发送数据是在哪一个块上呢我怎么能保证自己不会读取到自己在消息队列中写的信息呢 其实消息队列的内核数据结构就说明了一些因为和共享内存都属于System V类型的通信所以他们的内核数据结构会有很强的相似性 通过消息队列的数据结构我们可以看到消息队列也有 ipc_perm 这个结构体其也有自己的key值而这个 key值就是消息队列的唯一标识符。 和共享内存一样消息队列有自己的获取、发送、以及销毁接口
获取消息队列
msgctlcmd参数与共享内存相同
发送数据到消息队列 查看系统中的消息队列也很简单使用 ipcs -q 即可查询系统中的消息队列的情况了 System V 信号量
✈️信号量相关概念铺垫 前面我们介绍了共享内存我们直到共享内存不具有同步机制所以后面我们使用管道来为共享内存构建的进程间通信读写端做同步工作。如果我们没有对共享内存使用管道做一个同步机制那么可能会出现下面这样的问题 我们使用管道让两个进程分别处于读写端如果不加任何同步我们可以让不同的进程同时访问同一块内存资源如果两个进程对该资源为只读那么就不会有任何影响。但是如果 不同进程对同一块内存资源进行修改这样就会造成 数据不一致的问题。 对于公共资源进行保护是一个多执行流场景下一个比较常见和重要的话题。而 对公共资源进行保护有两种方法 同步 和 互斥。 同步访问公共资源安全的前提下具有一定的顺序性。 互斥访问公共资源的时候在任何时刻只有一方对公共资源进行访问。 资源在操作系统并发编程中是很重要的概念而有些公共资源又被称为临界资源 临界资源被保护起来的任何时刻只允许一个执行流访问的公共资源。 共享内存、管道都是被多个进程看到同一份资源而这份公共资源就属于一种临界资源。常见还有打印机、文件等。 临界区访问临界资源的代码叫做临界区。 同一个程序中临界区是需要进行同步的部分确保同一时间只有一个 进程/线程 可以进入临界区访问临界资源。比如在共享内存中Client端调用Wakeup就属于临街区而其他未访问到临界资源的代码就是 非临界区。 由此可以看出保护公共资源的本质程序员保护临界区。 原子性操作对象的时候不会中中断。只有两种状态要么完全执行要么完全不执行。 ✈️信号量 信号量Semaphore 是用于 进程/线程 间的 同步机制。信号量可以控制多个进程对共享资源的访问。 通俗来说我们日常在预定火车票在火车真正开来之前这个票会一直给你留着也就是说资源不一定是我持有才是我的我预定了那么这个资源在将来也是我的。而我多少资源我就卖多少票并保证每一份资源都不会被并发访问。 那么我们可以把整个火车看作一份资源一份资源只能有一个人抢票这个人哪里都可以坐但是这样效率很低。而我们把火车切割为无数个小资源这样每个小资源都可以对应一个人抢票把所有座位的票卖出去这样资源利用率就会比前者高。 操作系统也是如此对临界资源的分配有自己的规则而这种规则就叫做 信号量。 信号量本质是一个计数器描述临界资源数量的计数器。 也就是说我们进程之间的通信可以采用信号量的方式来时间对资源的同步访问如何访问呢实际上如果我们使用信号量的方式来获取资源进程就需要先申请信号量信号量申请成功就一定会有该 进程/线程 的资源和预定和车票类似。 申请完成信号量等待资源的分配。接着找到对应访问资源进行访问访问完成最后一步释放信号量。就比如阿熊坐火车到站了出站的那一刻火车票就算是失效了不然难不成这趟火车的这个座位一直是阿熊的专座显然不合常理。释放完的信号量后面就可以再次被别人申请了。 而信号量的使用非常简单其实就是一个计数器开始有一个可分配数值。遇到 进程/线程 申请信号量则计数器 -1遇到信号量被释放则计数器 1如果信号量 0 则之后的 进程/线程 则需要进行等待。 话虽如此但是我们使用一个整数作为信号量对其进行增加删除来对资源计数这样的方式对于多进程的场景真的可行吗 实际上这种场景是没办法使用一个整数来当做计数器的就拿父子进程来说我们都知道子进程被fork出来之后任何一个进程对自己的数据进行增删改的时候就会发生写时拷贝其中一个进程保留原始数据另外一个进程保留改动后的数据这样就造成了数据不一致的问题。 而今天我们想要使用一个整数作为信号量不也是如此吗如何才能保证进程之间数据一致性的问题呢所以解决方法一定是让不同的进程看到同一份计数器资源 综上所述我们可以得出信号量也是一种进程间通信因为它 保证了不同进程看到同一份资源而这就是进程间通信的前提。 只不过我们并不是通过信号量来传递消息而是 使用信号量来实现不同进程之间的协同操作
其实为什么使用整数不能作为信号量还有一个原因 假设信号量计数器为一个变量 int count; 那么对于 count、count- -这样的操作也是不能使用整数的一个原因因为其不能保证原子性 在这里我写了一份简单的代码对于第一条语句对count进行赋值操作在汇编层面只有一条语句第一句就是原子性的。 但是第二句和第三局就不同了因为都是后置- -而这样的操作转换成汇编层面实际上是由六条汇编语句来完成的所以操作上并非是原子性的。这样就可能会导致有一方执行流正在做但是另一方执行流在期间还没进行时已经做了- -了这样就会产生数据不一致的问题。 上面的部分会详细在线程篇讲述。 程序员既然要实现多进程并发的场景所有的进程需要访问临界资源在申请 Sem(信号量) 和释放 Sem 的时候都必须要保证 申请() 和 释放(- -) 操作是原子的而对信号量和- - 的操作我们就叫做PV操作
信号量PV操作 P操作wait操作将信号量的值减一信号量的值大于0时进程继续执行信号量小于等于0时进入阻塞状态进入等待队列等待信号量的值再次大于0。 V操作signal操作将信号量的值加一当信号量的值小于等于0时则会唤醒一个阻塞中的进程移除阻塞队列并 开始/继续 执行。 信号量的P操作用于请求资源资源无可分配时进程则被阻塞。V操作用于释放资源唤醒阻塞的进程。但是今天如果我们信号量的初始值是1呢也就是说开始就只有一份资源的情况下会有什么不同吗其实如果 信号量只有1的话一定是互斥的我们称其为 二元信号量 二元信号量Binary Semaphore也被称为 互斥量Mutex也是一种控制对共享资源访问的同步机制。二元信号量的取值只有0和1。主要用于实现互斥访问防止 多线程 同时访问临界资源从而导致数据不一致的问题。 但是在这里我们并不对二元信号量做深入了解因为其也是在线程篇很重要所以在线程篇我们会详细谈论。 ✈️信号量相关接口 一个临界资源可以申请一个信号量而在多数并发场景中临界资源不止一个所以 申请信号量资源定然一次性申请多个信号量这与信号量是几 定要做区分。 理论知识我们说的也差不多了那么我们在程序中如何申请信号量呢如何对信号量进行操作呢我们一般使用 semget 接口
int semget(key_t key, int nsem, int semflg);key参数指定信号量集的键值该键值用于标识唯一的信号量集同样使用ftok函数生成key值。 nsems参数表示指定信号量集信号量的数量如果需要获取信号量集该参数设置为0如果要创建信号量集需要设置对应的参数。 semflg参数与共享内存的flag标志位相同有IPC_CREAT、IPC_EXEC等选项以及权限位。 返回值成功返回信号量集的一个标识符失败返回-1并设置错误码。 概念中我们不止一次的提到了信号量集其实就可以把信号量集看作为一个数组数组里可以有多个信号量。而删除信号量接口 semctl
int semctl(int semid, int semnum, int cmd, ...);semid参数信号量集的标识符。 semnum参数信号量集中信号量编号从0开始类似数组下标。 cmd参数与共享内存cmd些许选项一致使用 IPC_RMID 选项可删除共享内存。 第四个参数信号量集的属性可传入semid_ds的结构体与共享内存和管道类似。 返回值与cmd选项相关大部分选项成功则返回0失败返回-1并设置错误码。 而我们能创建和删除信号量之后我们还需要对信号量进行增删控制也就是需要 对信号量进行 PV操作我们可以使用 semop 接口
int semop(int semid, struct sembuf* sops, size_t nsops);semid参数与前面两个接口一致。 sops参数表示指向 struct sembuf 数组指针。其为操作数组每个数组元素定义了对信号量的一个操作。
struct sembuf {unsigned short sem_num; // 信号量集中的信号量编号,指定信号量集中的哪个信号量进行操作从 0 开始计数short sem_op;// 操作类型指定要执行的操作类型。常见的操作类型包括
/*正数将信号量的值增加sem_op 的值。
负数将信号量的值减少 -sem_op 的值。
如果减少后的值小于 0则调用进程将被阻塞直到信号量的值为非负数。
0等待信号量的值变为 0*/short sem_flg;// 操作标志
/*sem_flg操作标志可以是以下值的组合
IPC_NOWAIT如果操作不能立即完成则 semop 调用会立即返回错误而不是阻塞。
SEM_UNDO操作会被记录下来以便在进程终止时自动撤销。*/
};nsops参数操作数组中的操作数目表示 sops 数组中包含的 struct sembuf 结构体数量。 返回值0表示返回成功-1为失败并设置错误码。 在系统中查看信号量使用 ipcs -s 即可查看系统中信号量情况 System V 共享内存、消息队列、信号量的共性 我们学完了共享内存、消息队列以及信号量就不难发现他们有非常多的相似之处首先是在系统中分别查看他们三个的状态用到的命令都是 ipcs 并且他们的程序调用接口都有cmd参数并且都可调用 xxxid_ds 结构体 和 ipc_perm 结构体。也就是说他们三个是操作系统特意设计的 而它们都是可以对进程之间进行通信的方法而操作系统注定要对 IPCInter-Process Communication进程间通信 资源做管理如何管理先描述再组织 接下来我们就看一看进程间通信在 内核中 的表示形式 实际上在操作系统中共享内存、消息队列、信号量被视为同一种资源可以被看成一个整体而我们内核中的共享内存、消息队列、信号量都存在一个内核结构体kern_ipc_perm 。而实际在内核当中所有管理IPC资源的结构体第一个成员都一样他们三个都 是由其进行强制类型转换所得到的 三个不同类型的 ipc_perm(sem_perm、shm_perm、q_perm)。 而 kern_ipc_perm 是 ipc_id_ary 结构体中的一个指针数组指针数组的每一个元素都是指针每个指针指向你所创建的 共享内存/消息队列/信号量 的 ipc_permsem_perm/shm_perm/q_perm结构体 而我们学过C语言的都知道结构体中数组指针的地址是该数组指针指向数组首元素的地址。所以我们就可以拿到不同类型 ipc_perm 的地址那么就可以 通过 起始地址偏移量 的方式访问内核数据结构成员 那么从此以后操作系统对IPC资源的管理就转化为了对数组的增删查改但是问题来了我们IPC有多种方式进行通信而且IPC不同它们的 ipc_perm 的类型就不同那么操作系统如何转换 kern_ipc_perm* 指针数组的每一个元素让其与IPC的类型对应呢 很简单我们使用强制类型转换将对应IPC 类型的 ipc_perm 强制类型转换即可
// 例子以下全是假设
kern_id_perm* ipc[n];(sem_array*)ipc[0]-sem_base[0].semval--;// 强制类型转换为信号量ipc_perm再基于此对信号量数目做--
(msg_queue*)ipc[1]-q_time;// 强转为消息队列的ipc_perm访问其成员
(shmid_kernel)ipc[2]-id;// 强转为共享内存...现在我们知道了如何对不同类型IPC的ipc_perm进行类型转换但是有个更重要的问题我们怎么确定你是谁呢怎么知道你是IPC的哪个类型呢不知道哪个类型我们也没办法做强制类型转换啊 其实这个问题也非常简单内核中的IPC类型无非就 共享内存、信号量、消息队列 这三个类型而我们写三个接口每个接口的作用就是强转为它们三个的类型一一进行匹配成功则返回强转后的结果失败则返回nullptr接着继续强转试错终是可以找到对应的类型的。 可是计算机怎么知道你需要强转为什么类型呢不用担心在kern_ipc_perm中有一个叫做mode的属性成员其记录着你需要转换结构体的类型所以我们就可以通过上述方式对不同IPC类型进行识别并强转了例如
#define IPC_TYPE_SHM 0x1
#define IPC_TYPE_SEM (0x1 1)
#define IPC_TYPE_MSG (0x1 2)shmid_kernel* (kern_ipc_perm *p)
{if(p-mode IPC_TYPE_SHM)return (shmid_kernel*)p;// 是则强转elsereturn nullptr;
}
...如果你学习过像java、C、python、rust…具有面向对象的高级语言那么你一定对上面那张图有疑问这张图怎么这么像我学过的 多态 呢但是它是C语言啊并没有多态啊没错这就是 使用 C语言实现的多态。 每个结构体的第一个成员就是基类指针而基类就可以通过指针对子类进行访问所以就间接形成了我们今天的多态但是注意操作系统是要比C、Java、Python这些具有面向对象特性语言要出来的早所以多态其实就是在我们日常的工程开发当中总结出来的规律。 以上就是全部内容啦文章创作不易如果对您有帮助的话还望给作者一个小小的三连吧~~