当前位置: 首页 > news >正文

进程间通信(27000字超详解)

🌎进程间通信


文章目录:

进程间通信

    进程间通信简介
      进程间通信目的
      初识进程间通信
      进程间通信的分类

    匿名管道通信
      认识管道
      匿名管道
      匿名管道测试
      管道的四种情况
      管道的五种特性
      管道的读写规则

    命名管道
      命名管道通信
      命名管道打开规则

    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端(相反也行)。

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<sys/types.h>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内写入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 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将不再保证写入的 原子性(原子性将在线程篇作详细解释)。

在这里插入图片描述


🚀命名管道

✈️命名管道通信

  命名管道与匿名管道有什么区别,其实在名字上就可以看出来。命名管道的管道文件是有名字的,而不同的是,命名管道可以让不同的进程之间可以通信,让不同的进程看到同一份资源

在这里插入图片描述

  这里不同的进程不仅仅指有血缘关系的进程,没有血缘关系的进程依旧适用。要让两个进程之间进行通信,那么就必定需要让两个进程看到同一份资源!

  而要打开管道文件,那么每个进程就必定要有对应的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;
};#endif

pipe_client.cpp:

#include "Comm.hpp"int main()
{std::cout << "hello client" << std::endl;return 0;
}

pipe_server:

#include "Comm.hpp"int main()
{// 创建文件Fifo fifo("./fifo");sleep(1);return 0;
}

makefile:

.PHONY:all #依次生成多个可执行程序,将all的依赖方法置空即可
all:pipe_client pipe_server pipe_server:PipeServer.ccg++ -o $@ $^ -std=c++11
pipe_client:PipeClient.ccg++ -o $@ $^ -std=c++11.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.hpp"int main()
{int wfd = open(PATH, O_WRONLY);// 客户端为writer,以只写的方式打开文件if(wfd < 0)// 当wfd<0时打印错误信息{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.hpp"int 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 <cstring>const 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.hpp"int 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.hpp"int 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 $@ $^ -std=c++11
shm_client:ShmClient.ccg++ -o $@ $^ -std=c++11
.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)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

  以及另外一个接口shmdt(shm 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 <cstring>const 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.hpp"int 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.hpp"int 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值就是消息队列的唯一标识符。

  和共享内存一样,消息队列有自己的获取、发送、以及销毁接口:

获取消息队列
在这里插入图片描述

msgctl,cmd参数与共享内存相同
在这里插入图片描述
在这里插入图片描述

发送数据到消息队列

在这里插入图片描述

  查看系统中的消息队列也很简单使用 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 结构体。也就是说他们三个是操作系统特意设计的!

  而它们都是可以对进程之间进行通信的方法,而操作系统注定要对 IPC(Inter-Process Communication,进程间通信) 资源做管理!如何管理?先描述,再组织

  接下来我们就看一看进程间通信在 内核中 的表示形式:

在这里插入图片描述

  实际上,在操作系统中,共享内存、消息队列、信号量被视为同一种资源,可以被看成一个整体,而我们内核中的共享内存、消息队列、信号量都存在一个内核结构体:kern_ipc_perm 。而实际在内核当中,所有管理IPC资源的结构体,第一个成员都一样,他们三个都 是由其进行强制类型转换所得到的 三个不同类型的 ipc_perm(sem_perm、shm_perm、q_perm)

  而 kern_ipc_perm 是 ipc_id_ary 结构体中的一个指针数组,指针数组的每一个元素都是指针,每个指针指向你所创建的 共享内存/消息队列/信号量 的 ipc_perm(sem_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这些具有面向对象特性语言要出来的早!所以多态其实就是在我们日常的工程开发当中总结出来的规律。


  以上就是全部内容啦,文章创作不易,如果对您有帮助的话,还望给作者一个小小的三连吧~~

相关文章:

进程间通信(27000字超详解)

&#x1f30e;进程间通信 文章目录&#xff1a; 进程间通信 进程间通信简介       进程间通信目的       初识进程间通信       进程间通信的分类 匿名管道通信       认识管道       匿名管道       匿名管道测试       管道的四种…...

一、大模型推理

https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md https://github.com/hiyouga/LLaMA-Factory/blob/main/examples/README_zh.md 安装 v7.1 https://github.com/hiyouga/LLaMA-Factory/releases/tag/v0.7.1 git clone --depth 1 https://github.com/hiyoug…...

.NET IoC 容器(三)Autofac

目录 .NET IoC 容器&#xff08;三&#xff09;AutofacAutofacNuget 安装实现DI定义接口定义实现类依赖注入 注入方式构造函数注入 | 属性注入 | 方法注入注入实现 接口注册重复注册指定参数注册 生命周期默认生命周期单例生命周期每个周期范围一个生命周期 依赖配置Nuget配置文…...

Day44 动态规划part04

背包问题 01背包问题&#xff1a;每件物品只能用一次完全背包问题&#xff1a;每件物品可以使用无数次 01背包问题 暴力解法&#xff1a;每一件物品其实只有两个状态&#xff0c;取或者不取&#xff0c;所以可以使用回溯法搜索出所有的情况&#xff0c;那么时间复杂度就是 o…...

html期末复习速览

一.基础标签 1.段落标签<p></p> 特点&#xff1a;分段分割 2.标题标签<h1></h1>……<h6></h6> 特点&#xff1a;文字加粗&#xff0c;单独占一行 3.换行标签<br /> 特点&#xff1a;单标签&#xff0c;强制换行 二.文本格式化…...

CTFHUB-信息泄露-目录遍历和PHPINFO

目录 目录遍历 PHPINFO 目录遍历 很简单&#xff0c;挨着把每个目录都点开看一下 发现2目录下有个 flag.txt 文件&#xff0c;点开发现了本关的flag PHPINFO 这关也很简单&#xff0c;进来之后是一个phpinfo页面&#xff0c;按 CTRL F键打开查询&#xff0c;输入flag&#…...

面向Java程序员的Go工程开发入门流程

对于一个像我这样没有go背景的java程序员来说&#xff0c;使用go开发一个可用的程序的速度是肉眼可见的缓慢。 其难点不在于go语言本身&#xff0c;而是搭建整个工程链路的过程&#xff0c;即所谓的“配环境”。 本文主要讲述如何配出一个适合go开发的环境&#xff0c;以免有同…...

vue3开发高德地图

在vue3的index.html 使用动态注入地址名和key <html lang"en"><head><meta charset"UTF-8" /><link rel"icon" type"image/svgxml" href"/vite.svg" /><meta name"viewport" conten…...

通过DLL方式链接glfw3.dll

主要是CMakeLists.txt文件变化 cmake_minimum_required(VERSION 3.10) project(glfwTest) set(CMAKE_CXX_STANDARD 11) aux_source_directory(. SRC_SOURCES) set(GLFW_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/include) set(GLFW_LIBRARY_DIR ${CMAKE_SOURCE_DIR}/lib/glfw) add_ex…...

Python自然语言处理(NLP)库之NLTK使用详解

概要 自然语言处理(NLP)是人工智能和计算机科学中的一个重要领域,涉及对人类语言的计算机理解和处理。Python的自然语言工具包(NLTK,Natural Language Toolkit)是一个功能强大的NLP库,提供了丰富的工具和数据集,帮助开发者进行各种NLP任务,如分词、词性标注、命名实体…...

sqoop操作

介绍 sqoop是隶属于Apache旗下的, 最早是属于cloudera公司的,是一个用户进行数据的导入导出的工具, 主要是将关系型的数据库(MySQL, oracle...)导入到hadoop生态圈(HDFS,HIVE,Hbase...) , 以及将hadoop生态圈数据导出到关系型数据库中 操作 将数据从mysql中导入到HDFS中 1.全量…...

【Qt秘籍】[002]-开始你的Qt之旅-下载

一、Qt的开发工具有哪些&#xff1f; Qt的开发工具概述Qt支持多种开发工具&#xff0c;其中最常见的开发工具是 1.QtCreator 【易上手/有少量bug/适合新手】 2.VisualStudio 【功能强大/易出错/需要更多额外配置】 3.Eclipse 【清朝老兵IDE/不建议使用】 【注意&#xff1…...

【自动驾驶】点与向量从ego系转odometry系

1.点从ego系转odometry系(ego -> odometry) struct Point {float x;float y;float angle; }; Point trans; // is the odom to ego transform Point odom_coord; is the odom coord Point ego_coord; is the ego coordfloat odom_coord.x = (ego_coord.x - trans.x) * st…...

jsmug:一个针对JSON Smuggling技术的测试PoC环境

关于jsmug jsmug是一个代码简单但功能强大的JSON Smuggling技术环境PoC&#xff0c;该工具可以帮助广大研究人员深入学习和理解JSON Smuggling技术&#xff0c;并辅助提升Web应用程序的安全性。 背景内容 JSON Smuggling技术可以利用目标JSON文档中一些“不重要”的字节数据实…...

Qt 控件提升

什么是控件提升&#xff08;Widget Promotion&#xff09; 控件提升是一个在Qt编程中常见但容易被忽视的概念。简单来说&#xff0c;控件提升就是将一个基础控件&#xff08;Base Widget&#xff09;转换为一个更特定、更复杂的自定义控件&#xff08;Custom Widget&#xff09…...

封装一个websocket,支持断网重连、心跳检测,拿来开箱即用

封装一个websocket&#xff0c;支持断网重连、心跳检测 代码封装 编写 WebSocketClient.js import { EventDispatcher } from ./dispatcherexport class WebSocketClient extends EventDispatcher {constructor(url) {console.log(url, urlurl)super()this.url url}// #soc…...

推荐一款开源电子签章/电子合同系统

文章目录 前言一、项目介绍二、项目地址三、技术架构四、代码结构介绍五、功能模块六、功能界面首页面手写签名面板电子印章制作数字证书生成 总结 前言 大家好&#xff01;我是智航云科技&#xff0c;今天为大家分享一个免费开源的电子签字系统。 一、项目介绍 开放签电子签…...

Qt Creator(Qt 6.6)拷贝一行

Edit - Preference - Environment&#xff1a; 可看到&#xff0c;拷贝一行的快捷键是&#xff1a; ctrl Ins...

红队内网攻防渗透:内网渗透之数据库权限提升技术

红队内网攻防渗透 1. 内网权限提升技术1.1 数据库权限提升技术1.1.1 数据库提权流程1.1.1.1 先获取到数据库用户密码1.1.1.2 利用数据库提权工具进行连接1.1.1.3 利用建立代理解决不支持外联1.1.1.4 利用数据库提权的条件及技术1.1.2 Web到Win-数据库提权-MSSQL1.1.3 Web到Win-…...

从0开始制作微信小程序

目录 前言 正文 需要事先准备的 需要事先掌握的 什么是uniapp 平台应用的分类方式 什么是TypeScript 创建项目 项目文件作用 源码地址 尾声 &#x1f52d; Hi,I’m Pleasure1234&#x1f331; I’m currently learning Vue.js,SpringBoot,Computer Security and so on.&#x1…...

Linux学习笔记:日志文件的编写

日志文件Log.hpp 日志文件的作用简单的日志文件编写 日志文件的作用 日志文件可以很好的帮我们显示出程序运行的信息,例如,进程pid,运行时间,运行状况等,通过日志记录程序的执行路径、变量值、函数调用等&#xff0c;可以帮助我们快速定位和修复代码中的错误。 简单的日志文件…...

为什么要保持方差为1

1.数值稳定性&#xff1a; 在机器学习和深度学习中&#xff0c;维持激活函数输入的方差在一个合理范围内&#xff08;如1&#xff09;是很重要的&#xff0c;这有助于防止在训练过程中发生梯度消失或梯度爆炸的问题。如果方差过大或过小&#xff0c;经过多层网络后输出结果的方…...

Wpf 使用 Prism 实战开发Day31

登录数据绑定 1.首先在LoginViewModel 登录逻辑处理类中&#xff0c;创建登录要绑定属性和命令 public class LoginViewModel : BindableBase, IDialogAware {public LoginViewModel(){ExecuteCommand new DelegateCommand<string>(Execure);}public string Title { ge…...

Linux权限提升二

#应用场景&#xff1a; 获取到Web权限或普通用户在Linux服务器上时进行的SUID&SUDO提权 SUID (Set owner User ID up on execution)是给予文件的一个特殊类型的文件权限。在Linux/Unix中&#xff0c;当一个程序运行的时候&#xff0c;程序将从登录用户处继承权限。SUID被定…...

[AI OpenAI] 推出ChatGPT Edu

一种负担得起的解决方案&#xff0c;帮助大学将AI负责任地引入校园。 我们宣布推出ChatGPT Edu&#xff0c;这是一个专为大学设计的ChatGPT版本&#xff0c;旨在负责任地向学生、教职员工、研究人员和校园运营部署AI。ChatGPT Edu由GPT-4o提供支持&#xff0c;能够跨文本和视觉…...

HTML5+CSS3回顾总结

一、HTML5新特性 1.语义化标签 <header> 头部标签<nav> 导航标签<article> 内容标签<section> 定义文档某个区域<aside> 侧边栏标签<footer> 尾部标签 2.多媒体标签 2.1视频标签vedio 》常规写法&#xff08;尽量都使用mp4&#xff0…...

AI推介-多模态视觉语言模型VLMs论文速览(arXiv方向):2024.05.01-2024.05.10

文章目录~ 1.Pseudo-Prompt Generating in Pre-trained Vision-Language Models for Multi-Label Medical Image Classification2.VLSM-Adapter: Finetuning Vision-Language Segmentation Efficiently with Lightweight Blocks3.Memory-Space Visual Prompting for Efficient …...

Python 点云生成高程模型图(DSM)

点云生成高程模型图 一、什么是DSM?二、python代码三、结果可视化一、什么是DSM? DSM(Digital Surface Model)是一种数字高程模型,通常用于描述地表地形的数字化表示。它是由一系列离散的高程数据点组成的三维地形模型,其中每个点都具有其相应的高程值。   DSM主要用于…...

[第五空间 2021]WebFTP

题目是WebFTP 通过标签可以看出git泄露(git泄露是指开发人员利用git进行版本控制) 通过网上了解WebFTP的源码账号admin 密码admin888 进去之后正常思路是我们利用/.git 在githack里面进行复现 查看log看看有没有flag 但是经过我们查询之后不是这样子 通过一段时间摸索在phpinf…...

SQL—DQL(数据查询语言)之小结

一、引言 在前面我们已经学习完了所有的关于DQL&#xff08;数据查询语言&#xff09;的基础语法块部分&#xff0c;现在对DQL语句所涉及的语法&#xff0c;以及需要注意的事项做一个简单的总结。 二、DQL语句 1、基础查询 注意&#xff1a; 基础查询的语法是&#xff1a;SELE…...

找回xmind文件办法:一切意外均可找回(误删/重启关机等)

我周三编辑完&#xff0c;周四下午评审完用例忘记保存 结果到了快乐星期五&#xff0c;由于是周五我太开心了...早上到公司后觉得电脑卡&#xff0c;直接点了重启啥都没保存啊啊啊啊啊 准备上传测试用例时才想起来我的用例找不见了&#xff01;&#xff01;&#xff01;&…...

微信小程序 npm构建+vant-weaap安装

微信小程序&#xff1a;工具-npm构建 报错 解决&#xff1a; 1、新建miniprogram文件后&#xff0c;直接进入到miniprogram目录&#xff0c;再次执行下面两个命令&#xff0c;然后再构建npm成功 npm init -y npm install express&#xff08;Node js后端Express开发&#xff…...

【LeetCode 63】 不同路径 II

1. 题目 2. 分析 这道题比较典型&#xff0c;跟最小路径和 是同样的思想。比较简单。 3. 代码 class Solution:def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:row len(obstacleGrid)col len(obstacleGrid[-1]) dp [[0] *(col) f…...

OpenAI助手API接入-问答对自动生成

支持GPT-3.5-Turbo, GPT-4o, GPT-4-Turbo import json import openai from pathlib import Path import os client openai.OpenAI(base_urlbase_url, api_keyapi_key) file client.files.create( fileopen("H3.pdf", "rb"), purposeassistants ) …...

9. C++通过epoll+fork的方式实现高性能网络服务器

epollfork 实现高性能网络服务器 一般在服务器上&#xff0c;CPU是多核的&#xff0c;上述epoll实现方式只使用了其中的一个核&#xff0c;造成了资源的大量浪费。因此我们可以将epoll和fork结合来实现更高性能的网络服务器。 创建子进程函数–fork( ) 要了解线程我们先来了解…...

【Mac】XMind for mac(XMind思维导图)v24.04.10311软件介绍和安装教程

软件介绍 XMind for Mac是一款功能强大的思维导图软件。它具有以下主要特点&#xff1a; 1.多样化的思维导图功能&#xff1a;XMind for Mac提供了丰富的思维导图编辑功能&#xff0c;用户可以创建各种类型的思维导图&#xff0c;包括组织结构图、逻辑图、时间轴图等&#xf…...

使用 Django ORM 进行数据库操作

文章目录 创建Django项目和应用定义模型查询数据更新和删除数据总结与进阶聚合和注解跨模型查询原始SQL查询 Django是一个流行的Web应用程序框架&#xff0c;它提供了一个强大且易于使用的对象关系映射&#xff08;ORM&#xff09;工具&#xff0c;用于与数据库进行交互。在本文…...

行为型设计模式之模板模式

文章目录 概述原理结构图实现 小结 概述 模板方法模式(template method pattern)原始定义是&#xff1a;在操作中定义算法的框架&#xff0c;将一些步骤推迟到子类中。模板方法让子类在不改变算法结构的情况下重新定义算法的某些步骤。 模板方法中的算法可以理解为广义上的业…...

大泽动力车载柴油发电机的特点和优势有哪些

大泽动力车载柴油发电机具有一系列显著的特点和优势&#xff0c;以下是对其的详细介绍&#xff1a; 低噪音性能&#xff1a;大泽动力车载柴油发电机具备明显的低噪音性能&#xff0c;其噪音限值在距离机组7米处测得为70dB(A)&#xff0c;这为用户提供了一个相对安静的工作环境…...

基于 IP 的 DDOS 攻击实验

一、介绍 基于IP的分布式拒绝服务&#xff08;Distributed Denial of Service, DDoS&#xff09;攻击是一种利用大量受控设备&#xff08;通常是僵尸网络&#xff09;向目标系统发送大量请求或数据包&#xff0c;以耗尽目标系统的资源&#xff0c;导致其无法正常提供服务的攻击…...

GPT-4o如何重塑AI未来!

如何评价GPT-4o? 简介&#xff1a;最近&#xff0c;GPT-4o横空出世。对GPT-4o这一人工智能技术进行评价&#xff0c;包括版本间的对比分析、GPT-4o的技术能力以及个人感受等。 GPT-4o似乎是一个针对GPT-4模型进行优化的版本&#xff0c;它在性能、准确性、资源效率以及安全和…...

window本地域名映射修改

位置 C:\Windows\System32\drivers\etc 文件名 hosts 修改方法 复制一份到桌面 修改桌面的文件 # 前面为ip 后面为域名&#xff0c;域名-》ip的映射 127.0.0.1 link.com最后将修改后的文件保存&#xff0c;复制到C:\Windows\System32\drivers\etc替换...

【退役之重学】为什么要加入多级缓存

一、为什么 加入多级缓存是为了提高数据访问的效率和性能 二、怎么做 在多级访问系统中&#xff0c;数据首先会被存储在速度最快的 L1 缓存中&#xff0c;如果数据在 L1 缓存中未命中&#xff0c;则会继续在 L2 缓存 和 L3 缓存中查找&#xff0c;如果在所有缓存中都未命中&…...

Redis常用命令大全

目录 1、五大数据类型的基本命令 1.1 字符串 1.2 列表 1.3 哈希 1.4 集合 1.5 有序集合 2、与key相关 2.1 查看redis数据的类型 2.2 查看当前redis库中的所有key命令 3、除了五大数据类型外常见命令 3.1 键操作 3.2 服务器操作 3.3 连接操作 3.4 发布/订阅 3.5 事…...

HttpSecurity 是如何组装过滤器链的

有小伙伴们问到这个问题&#xff0c;简单写篇文章和大伙聊一下。 一 SecurityFilterChain 首先大伙都知道&#xff0c;Spring Security 里边的一堆功能都是通过 Filter 来实现的&#xff0c;无论是认证、RememberMe Login、会话管理、CSRF 处理等等&#xff0c;各种功能都是通…...

STM32 入门教程(江科大教材)#笔记2

3-4按键控制LED /** LED.c**/ #include "stm32f10x.h" // Device headervoid LED_Init(void) {/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_I…...

python zip()函数(将多个可迭代对象的元素配对,创建一个元组的迭代器)zip_longest()

文章目录 Python zip() 函数深入解析基本用法函数原型基础示例 处理不同长度的迭代器高级用法多个迭代器使用 zip() 与 dict()解压序列 注意事项内存效率&#xff1a;zip() 返回的是一个迭代器&#xff0c;这意味着直到迭代发生前&#xff0c;元素不会被消耗。这使得 zip() 特别…...

React.forwardRef 使用

React.forwardRef 是一个React提供的高阶组件函数&#xff0c;用于向函数组件传递ref。在函数组件中无法直接访问ref&#xff0c;如果需要在函数组件中操作子组件的DOM元素或组件实例&#xff0c;就可以使用React.forwardRef来转发ref给子组件。 当使用React.forwardRef包裹一…...

C# 中的值类型与引用类型:内存大小解析

在 C# 中&#xff0c;类型可以被归类为值类型或引用类型&#xff0c;它们在内存中的存储和管理方式不同。了解这些差异对于优化程序性能和资源管理至关重要。 值类型 (Value Types) 值类型包括所有内置的数值类型&#xff08;如 int, double 等&#xff09;、char 类型、bool…...

object对象列表使用sorted函数按照对象的某个字段排序

在Python中&#xff0c;如果你想要根据列表中对象的某个属性&#xff08;比如create_time&#xff09;来进行逆序排序&#xff0c;你可以使用sorted()函数并指定一个key参数。key参数应该是一个函数&#xff0c;该函数接受一个列表元素并返回一个用于排序的值。 假设你的objec…...