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

【Linux实践系列】:进程间通信:万字详解共享内存实现通信

🔥 本文专栏:Linux Linux实践项目
🌸作者主页:努力努力再努力wz

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

💪 今日博客励志语录人生就像一场马拉松,重要的不是起点,而是坚持到终点的勇气

★★★ 本文前置知识:

匿名管道

命名管道


前置知识大致回顾(对此十分熟悉的读者可以跳过)

那么我们知道进程之间具有通信的需求,因为某项任务需要几个进程共同来完成,那么这时候就需要进程之间协同分工合作,那么进程之间就需要知道彼此之间的完成的进度以及完成的情况,那么此时进程之间就需要通信来告知彼此,而由于进程之间具有独立性,那么进程无法直接访问对方的task_struct结构体以及页表来获取其数据,那么操作系统为了满足进程之间通信的需求又保证进程的独立性,那么采取的核心思想就是创建一份公共的内存区域,然后让通信的进程双方能够看到这份公共的内存区域,从而能够实现通信

那么对于父子进程来说,由于子进程是通过拷贝父进程的task_struct结构体得到自己的一份task_struct结构体,那么意味着子进程会拷贝父进程的文件描述表,从而子进程会继承父进程打开的文件,而操作系统让进程通信的核心思想就是创建一块公共的内存区域,那么这里对于父子进程来说,那么文件就可以作为这个公共的内存区域,来保存进程之间通信的信息,所以这里就要求父进程在创建子进程之前,先自己创建一份文件,这样再调用fork创建出子进程,这样子进程就能继承到该文件,那么双方就都持有该文件的文件描述符,然后通过文件描述符向该文件进行写入以及读取,而我们知道该文件只是用来保存进程之间通信的临时数据,而不需要刷新到磁盘中长时间保存,那么必定该文件的性质是一个内存级别的文件,那么创建一个内存级别的文件就不能在调用open接口,因为open接口是用来创建一个磁盘级文件,其次就是双方通过该文件来进行进程之间通信的时候,那么双方不能同时对该文件进行读写,因为会造成偏移量错位以及文件内容混乱的问题,所以该文件只能用来实现单向通信,也就是智能一个进程向该文件写入,然后另一个进程从该文件中进行读取,那么由于该文件单向通信的特点,并且进程双方是通过文件描述符来访问,所以该文件其没有路径名以及文件名,因此该文件被称作匿名管道文件,那么我们要创建匿名管道文件,就需要调用pipe接口,那么pipe接口的返回值就是该匿名管道文件读写端对应的file结构体的文件描述符

而对于非父子进程来说,此时他们无法再看到彼此的文件描述表,那么意味着对于非父子进程来说,那么这里只能采取建立一个普通的文件,该普通的文件作为公共区域,那么一个进程向该文件中写入,另一个进程从该文件读取,根据父子进程通信的原理,我们知道该普通文件肯定不是一般的普通文件,它一定也得是内存级别文件,其次也只能实现单向通信,而对于匿名管道来说,通信进程双方看到该匿名管道是通过文件描述符来访问到这块资源,而对于命名管道则是通过通过路径加文件名的方式来访问命名管道,那么访问的方式就是通信进程的双方各自通过open接口以只读和只写的权限分别打开该命名管道文件,获取其文件描述符,然后通信进程双方通过文件描述符然后调用write以及read接口来写入以及读取数据,而创建一个命名管道就需要我们调用mkfifo接口

那么这就是对前置知识的一个大致回顾,如果读者对于上面讲的内容感到陌生或者想要知道其中的更多细节,那么可以看我之前的博客


共享内存

那么此前我们已经学习了两种通信方式,分别是匿名管道以及命名管道来实现进程的通信,那么这期博客,我便会介绍第三种通信方式,便是共享内存,那么我会从三个维度来解析共享内存,分别是什么是共享内存以及共享内存的底层相关的原理和结合前面两个维度的理论知识,如何利用共享内存来实现进程的通信,也就是文章的末尾我们会写一个用共享内存实现通信的小项目

什么是共享内存以及共享内存的底层原理

那么我们知道进程间通信的核心思想就是通过开辟一块公共的区域,然后让进程双方能够看到这份资源从而实现通信,所以这里的共享内存其实本质就是操作系统为其通信进程双方分配的一个物理内存,那么这份物理内存就是共享内存,所以共享内存的概念其实很简单与直接

根据进程间通信的核心思想,那么这里的公共的区域已经有了,那么下一步操作系统要解决的问题便是创建好了共享内存,如何让进程双方能够看到这份共享内存资源

那么对于进程来说,按照进程的视角,那么它手头上只持有虚拟地址,那么进程访问各种数据都只能通过虚拟地址去访问,然后系统再借助页表将虚拟地址转换为物理地址从而访问到相关数据,所以要让通信进程双方看到共享内存,那么此时操作系统的任务就是提供给通信进程双方各自一个指向共享内存的虚拟地址,然后通信进程双方就可以通过该虚拟地址来向共享内存中写入以及读取数据了,那么这个时候操作系统要进行的工作,就是创建通信的进程的同时,设置好该进程对应的mm_struct结构体中的共享内存段,并且在其对应的页表添加其共享内存的虚拟地址到物理地址的映射的条目

那么知道了共享内存来实现进程双方通信的一个大致的原理,那么现在的问题就是如何请求让操作系统来为该通信进程双方创建共享内存

那么这里就要让操作系统为该其创建一份共享内存,就需要我们在代码层面上调用shmget接口,那么该接口的作用就是让内核为我们创建一份共享内存,但是在介绍这个接口如何使用之前,我们还得补充一些相关的理论基础,有了这些理论基础,我们才能够认识到shmget这些参数的意义是什么

  • shmget
  • 头文件:<sys/shm.h> 和<sys/ipc.h>
  • 函数声明:int shmget(ket_t key,size_t size,int shmflg);
  • 返回值:调用成功返回shmid,调用失败则返回-1,并设置errno

key/shmid

那么这里的shmget的一个参数就是一个key,那么读者对于key的疑问无非就是这两个方面:这个key是什么?key的作用是什么?

那么接下来的讲解会以这两个问题为核心,来为你解析这个key究竟是何方神圣

首先我们一定要清楚的是系统中存在不只有一个共享内存,因为系统中需要通信的进程不只有一对,所以此时系统中的共享内存就不只有一个,那么系统中存在这么多的共享内存,那么每一个共享内存都会涉及到创建以及读取和写入以及最后的销毁,那么操作系统肯定就要管理存在的所有的共享内存,那么管理的方式就是我们熟悉的先描述再组织的方式来管理这些共享内存,也就是为每一个共享内存创建一个struct shm_kernel结构体,那么该结构体就记录了该共享内存的相关的属性,比如共享内存的大小以及共享内存的权限以及挂载时间等等,那么每一个共享内存都有对应的结构体,那么内核会持有这些结构体,并且会采取特定的数据结构将这些结构体组织起来,比如链表或者哈希表,那么系统中每一个共享内存肯定是不相同的,那么为了区分这些不同的共享内存,那么系统就得给这些共享内存分配一个标识符,通过标识符来区分这些共享内存

而进程要用共享内存实现通信,那么进程首先得请求操作系统为我们该进程创建一份共享内存,然后获取到指向该共享内存的虚拟地址,而进程间的通信,涉及的进程的数量至少为两个,那么以两个进程为例子,假设进程A和进程B要进行通信,那么此时需要为这对进程提供一个共享内存,那么就需要A进程或者B进程告诉操作系统来为其创建一份共享内存

那么这里你可以看到我将或者这两个字加粗,那么就是为了告诉读者,那么这里我们只需要一个进程来告诉内核创建一份共享内存,不需要两个进程都向操作系统发出创建共享内存的请求,所以只需要一个进程请求内核创建一份共享内存,然后另一个进程直接访问创建好的共享内存即可

那么知道了这点之后,那么假设这里创建共享内存的任务交给了A进程,那么此时A进程请求内核创建好了一份共享内存,那么对于B进程来说,它如何知道获取到A进程创建好的共享内存呢,由于系统内存在那么多的共享内存,那么B进程怎么知道哪一个共享内存是A进程创建的,那么这个时候就需要key值,那么这个key值就是共享内存的标识符

key就好比酒店房间的一个门牌号,那么A和B进程只需要各自持有该房间的门牌号,那么就能够找到该房间,但是这里要注意的就是这里的key值不是由内核自己生成的,而是由用户自己来生成一个key值

那么有些读者可能就会感到疑问,那么标识符这个概念对于大部分的读者来说都不会感到陌生,早在学习进程的时候,我们就已经接触到标识符这个概念,那么对内核为了管理进程,那么会为每一个进程分配一个标识符,那么就是进程的PID,而在文件系统中,任何类型的文件都有自己对应的inode结构体,那么内核为了管理inode结构体,那么也为每一个文件对应的inode结构体分配了标识符,也就是inode编号,所以读者可能会感到疑惑:那么在这里共享内存也会存在标识符,但是这里的标识符为什么是用户来提供而不是内核来提供呢,是内核无法做到为每一个共享内存分配标识符还是说因为其他什么原因?

那么这个疑问是理解这个key的关键,首先我要明确的就是内核肯定能够做到为每一个共享内存提供标识符,这个工作对于内核来说,并不难完成,并且事实上,内核也的确为每一个共享内存提供了标识符,那么这个标识符就是shmid

在引入了shmid之后,可能有的读者又会产生新的疑问:按照你这么说的话,那么实际上内核为每一个创建好的共享内存分配好了标识符,但是这里还需要用户自己在创建一个标识符,那么理论上来说,岂不是一个共享内存会存在两个所谓的标识符,一个是key,另一个是shmid,而我们访问共享内存只需要一个标识符就够了,那么这里共享内存拥有两个标识符,岂不是会存在冗余的问题?并且为什么不直接使用内核的标识符来访问呢?


那么接下来我就来依次解答读者的这些疑问,那么首先关于为什么我们进程双方为什么不直接通过shmid来访问内存

那么我们知道内核在创建共享内存的同时会为该共享内存创建对应的struct shm_kernel结构体,那么其中就会涉及到为其分配一个唯一的shmid,而假设请求内核创建共享内存的任务是交给A进程来完成,而B进程只需要访问A进程请求操作系统创建好的共享内存,而对于B进程来说,它首先得知道哪个共享内存是提供给我们两个A个B两个进程使用的,意味着B进程就得通过共享内存的标识符得知,因为每一个共享内存对应着一个唯一且不重复的标识符,对于A进程来说,由于它来完成共享内存的创建,而shmget接口是用来创建共享内存并且返回值就是共享内存的shmid,那么此时A进程能够知道并且获取进程的shmid标识符,但是它能否将这个shmget的返回值也就是shmid告诉该B进程吗,毫无疑问,肯定是不可能的,因为进程之间就有独立性!那么如果直接使用shmid来访问共享内存,那么必然只能对于创建共享内存的那一方进程可以看到而另一个进程无法看到,那么无法看到就会让该进程不知道哪一个共享内存是用来给我们A和B进程通信的,所以这就是为什么要有key存在

那么A和B进程双方事先会持有一个相同的key,那么A进程是创建共享内存的一方,那么它会将将key传递给shmget接口,那么shmget接口获取到key,会将key作为共享内存中其中一个字段填入,最终给A进程返回一个shmid,而对于B进程来说,那么它拿着同一个key值然后也调用shmget接口,而此时对于B进程来说,它的shmget的行为则不是创建共享内存,而是内核会拿着它传递进来的key,到组织共享内存所有结构体的数据结构中依次遍历,找到匹配该key的共享内存,然后返回其shmid

而至于为什么A和B进程都调用shmget函数,但是shmget函数有着不同的行为,对于A来说是创建,对于B来说则可以理解为是“查询”,那么这就和shmget的第三个参数有关,那么第三个参数会接受一个宏,该宏决定了shmget行为,所以A和B进程调用shmget接口传递的宏肯定是不一样的,那么我会在下文会讲解shmget接口的第三个参数,这里就先埋一个伏笔

所以综上所述,这里的key虽然也是和shmid一样是作为标识符,但是是给用户态提供使用的,是用户态的两个进程在被创建之前的事先约定,而内核操作则是通过shmid,那么key的值没有任何的意义,所以理论上我们用户可以自己来生成任意一个无符号的整形作为key,但是要注意的就是由于这里key是用户自己生成自己决定的,那么有可能会出现这样的场景,那么就是用户自己生成的key和已经创建好的共享内存的key的值一样或者说冲突,所以这里系统为我们提供了ftok函数,那么该函数的返回值就是key值,那么我们可以不调用该函数,自己随便生成一个key值,但是造成冲突的后果就得自己承担,所以这里更推荐调用ftok函数生成一个key值

这里推荐使用ftok函数来生成的key,不是因为ftok函数生成的key完全不会与存在的共享内存的key造成冲突,而是因为其冲突的概率相比于我们自己随手生成一个的key是很低的

  • ftok
  • 头文件:<sys/types.h> 和<sys/ipc.h>
  • 函数声明:key_t ftok(const char* pathname,int proj_id);
  • 返回值:调用成功返回key值,调用失败则返回-1

那么这里ftok会接收两个参数,首先是一个文件的路径名以及文件名,那么这里注意的就是这里的文件的路径名以及文件名一定是系统中存在的文件,因为它会解析这个路径以及文件名从而获取该文件的inode编号,然后得到对应的inode结构体,从中再获取其设备编号,那么这里的proj_id的作用就是用来降低冲突的概率,因为到时候ftok函数获取到文件的inode编号以及设备号和proj_id,然后会进行位运算,得到一个32位的无符号整形,那么其位运算就是:
ftok 通过文件系统元数据生成 key 的算法如下:

key = (st_dev & 0xFF) << 24 | (st_ino & 0xFFFF) << 8 | (proj_id & 0xFF)

• st_dev:文件所在设备的设备号(取低8位)

• st_ino:文件的inode编号(取低16位)

• proj_id:用户指定的项目ID(取低8位)

共享内存的大小

那么shmget函数的第二个参数便是指定的就是共享内存的大小,那么这里至于内核在申请分配物理内存的单位是以页为单位,也就是以4KB为单位来分配物理内存,而这里shmget的第二个参数是以字节为单位,那么这里我建议我们开辟的共享内存是以4096的整数倍来开辟,因为假设你申请一个4097个字节,那么此时内核实际上为你分配的物理内存是2*4096,也就是8kb的空间,虽然人家内核给你分配了8kb的空间,但是它只允许你使用其中的4097个字节,也就是剩下的空间就全部浪费了,所以这就是为什么建议申请的空间大小是4096的整数倍,那么这就是shmget的第二个参数

shmget的宏

那么shmget的第三个参数便是宏来指定其行为,那么上文我们就埋了一个伏笔,就是两个进程都调用了shmget但是却有着不同的行为,那么就和这里的宏有关:

  • IPC_CREAT (01000):如果共享内存不存在则创建
  • IPC_EXCL (02000):不能单独使用,与IPC_CREAT一起使用,若共享内存已存在则失败
  • SHM_HUGETLB (04000):使用大页内存(Linux特有)

那么这里我们着重要掌握的便是IPC_CREAT以及IPC_EXEC这两个宏
IPC_CREAT:
那么传递IPC_CREAT这个宏给shmget接口,其底层涉及到工作,就是内核首先会持有我们传递的key值,然后去遍历组织所有共享内存的结构体的数据结构,如果遍历完了所有共享内存对应的结构体并且发现没有匹配的key值,那么这里就会创建一个新的共享内存并且同时创建对应的结构体,然后对其结构体的属性进行初始化其中就包括填入key值并将其放入组织共享内存结构体的数据结构中,那么最后创建完成后,会返回该共享内存的shmid,而如果说发现有匹配的key值的共享内存,那么就直接返回该共享内存的shmid
IPC_EXCL:
而IPC_EXCL则是和IPC_CREAT一起使用,那么传递IPC_CREAT| IPC_EXCL这个宏给shmget接口,其底层涉及到工作,j就是如果内核发现了有匹配的key值的共享内存,那么这里就不会返回该共享内存的shmid而是返回-1并设置errno,没有的话就创建新的共享内存并返回器shmid,所以这个选项就是保证了创建的共享内存是最新的共享内存

而这里的宏本质上就是一个特定值的32位的二进制序列,那么他们的每一个比特位代表着特定含义的标记位,而该标记位则是分布在二进制序列的高24位,而低8位则是表示该共享内存的权限,所以在上文所举的例子中,对于A进程来说,它是创建共享内存的一方,那么它传递的宏就应该是IPC_CREAT|IPC_EXCL|0666,而对于B进程来说他是访问的一方,那么它传递的就是IPC_CREAT|0666

那么shmget会根据其宏进行相应的行为,并且还会核对其权限是否一致,不一致则返回-1,调用失败


shmat

那么此时上面所讲的所有内容都是关于创建共享内存的一些理论知识,那么我们现在已经知道如何创建共享内存,那么下一步就是如何让通信的进程双方看到该共享内存,那么从上文的共享内存实现通信的大致原理,我们知道创建完共享内存的下一个环节就是让进程双方持有指向该共享内存的虚拟地址,那么这个时候就需要请求操作系统来设置通信进程的双方的虚拟地址空间的共享内存段以及在页表中添加共享内存的虚拟地址到物理地址的映射条目,所以此时就需要我们在代码层面上调用shmat系统调用接口,那么该系统调用接口的背后所做的工作就是刚才所说的内容,而shmat所做的工作也叫做挂载

  • shmat
  • 头文件:<sys/shm.h> 和<sys/types.h>
  • 函数声明:void* shmat(int shmid,void *shmadder,int shmflg);
  • 返回值:调用成功返回指向共享内存起始位置的虚拟地址,调用失败则返回(void*)-1,并设置errno

那么这里shmat接口会接收之前我们调用shmget接口获取的共享内存的shmid,然后内核会根据该shmid遍历共享内存对应的结构体,然后找到匹配的共享内存,接着在将共享内存挂载到通信的进程双方,那么这里第二个参数就是我们可以指定内核挂载到共享内存段的哪个具体区域,但是这第二个参数最好设置为NULL,那么设置为NULL意味着会让内核来在共享内存段中选择并且分配一个合适的虚拟地址,那么该虚拟地址就会作为返回值

而shmat的第三个参数则是会接收一个宏,那么这个宏就是用来指定该进程对于该共享内存的一个读写权限:

  • SHM_RDONLY (只读模式):以只读方式附加共享内存
  • SHM_RND (地址对齐):当指定了非NULL的shmaddr时,自动将地址向下对齐到SHMLBA边界
  • SHM_REMAP (Linux特有,重新映射): 替换指定地址的现有映射,需要与具体的shmaddr配合使用

那么这里对于我们来说,那么我们不用传递任何宏就进去,就传递一个NULL或者0,那么我们该进程就能够正常的写入以及读取该共享内存的内容,那么这三个宏的使用场景,在目前现阶段的学习来说,我们暂时还使用不到。

那么这就是shmat接口,那么认识了shmat接口之后,那么我们就可以来利用共享内存来实现正常的进程之间的通信了,那么首先第一个环节就是先让各自通信的进程双方持有key,然后一个进程通过key来调用shmget接口来创建共享内存并且获得其shmid,而另一个进程也是同样通过key值来调用shmge接口来获取已经创建好的共享内存的shmid,那么下一个环节就是挂载,那么此时就需要请求系统设置通信的进程双方的虚拟地址空间的共享内存段,并且添加相应的关于该共享内存的虚拟地址到物理地址的映射的条目,并且返回给进程双方该共享内存的起始位置的虚拟地址,那么此时进程双方就可以持有该虚拟地址去访问共享内存了

shmdet

那么进程通信完之后,那此时就要清理相关的资源,其中就包括打开的共享内存,那么我们要注意的就是共享内存对应的shm_kernel结构体中会有一个属性,那么该属性便是引用计数,记录了有多少个进程指向它或者说有多少个进程的页表中有关于该共享内存的虚拟地址到物理地址的映射条目,那么此时shmdet接口的作用就是删除该进程对应的页表中共享内存的映射条目或者将该页表的条目设置为无效,从而解除该进程与共享内存之间的绑定,让该进程无法再访问到共享内存的资源,并且还会减少该共享内存的引用计数

  • shmdet
  • 头文件:<sys/shm.h> 和<sys/ipc.h>
  • 函数声明:int shmdet(const void *shmadder);
  • 返回值:成功返回0,失败返回-1,并设置errno

那么可能会有的读者会感到疑惑的就是,这里shmdet接口只接收一个虚拟地址,而该虚拟地址是共享内存的起始位置的虚拟地址,那么内核可以通过该虚拟地址借助页表来访问到共享内存,而引用计数这个属性是存储在共享内存对应的结构体中,那么意味着这里shmdet能够通过虚拟地址来访问到共享内存对应的物理结构体,而共享内存中存储的内容去啊不是通信的消息,那么这里内核是如何通过该虚拟地址访问到共享内存对应的结构体的呢?

那么我们知道进程对应的task_struct结构体中会有一个字段mm_struct结构体其中会维护一个vma(虚拟内存区域)的数据结构,那么该数据结构一般是采取链表来实现,其中该链表的每一个节点是一个结构体,用来描述以及记录该虚拟内存区域的相关属性,其中就包括该虚拟内存区域的虚拟地址的起始位置以及虚拟地址的结束位置,以及相关的读写权限以及其文件的大小和文件的类型

struct mm_struct {struct vm_area_struct *mmap;       // VMA 链表的头节点(单链表)struct rb_root mm_rb;               // VMA 红黑树的根节点(用于快速查找)// ...其他字段(如页表、内存计数器等)
};struct vm_area_struct {// 内存范围unsigned long vm_start;unsigned long vm_end;// 权限与标志unsigned long vm_flags;// 文件与偏移struct file *vm_file;unsigned long vm_pgoff;// 操作函数const struct vm_operations_struct *vm_ops;// 链表与树结构struct vm_area_struct *vm_next;struct rb_node vm_rb;// 其他元数据struct mm_struct *vm_mm;// ...
};

其中在vma的视角下,那么它将每一个虚拟内存区域比如栈或者堆,以文件的形式来看待,那么其中这里的vm_file字段会指向该虚拟内存区域创建的一个file结构体,其中就会包含该共享内存对应的struct shm_kernal结构体,所以这里shmdet接口会获取到虚拟地址,然后会查询mm_struct结构体中记录的vma链表根据该虚拟地址确定落在哪一个vma结构体中,那么该vma结构体就是共享内存段区域所对应的vma结构体,然后通过vm_file来间接获取到共享内存的shmid,最后再拿着shmid从保存共享内存对应的数据结构中找到对应匹配的共享内存对应的结构体,然后让其引用计数减一

shmctl

而shmdet只是解除了进程与共享内存之间的挂载,那么shmdet的作用就好比指向一个动态数组的首元素的指针,那么我们只是将该指针置空,让我们无法在之后的代码中通过该指针来访问该动态数组,但是该动态数组所对应的空间并没有释放,而对于共享内存来说,那么内核要真正释放共享内存的资源得满足两个前提条件,那么就是该共享内存对应的引用计数为0并且该共享内存还得被标记为已删除,因为内核没有规定该共享内存只能给创建它的进程双方通信用,那么一旦该进程双方结束通信了,那么可以让该进程双方解除与该共享内存的挂载,然后让其他进程与该共享内存挂载,从而通过再次利用该共享内存来进行通信,所以这里就是为什么要设计一个删除标志

所以说这里的shmctl接口的作用就是用来控制共享内存,那么我们可以通过调用该接口将共享内存标记为可删除,那么一旦该共享内存对应的引用计数为0,那么此时内核就会释放该共享内存的资源

  • shmctl
  • 头文件:<sys/shm.h> 和<sys/ipc.h>
  • 函数声明:int shmctl(int shmid,int cmd,struct shmid_ds* buffer);
  • 返回值:调用成功返回0,调用失败则返回-1,并设置errno

那么这里对于shmctl来说,其第一个参数是shmid,那么到时内核会持有该参数去寻找对应的共享内存的结构体,而shmctl的第二个参数则是控制shmctl接口的行为,那么这里还是通过宏以及位运算的方式来指定shmctl接口的行为:

  • IPC_STAT:获取共享内存段的状态
  • IPC_RMID:删除共享内存段
  • IPC_SET:设置共享内存段的状态

那么这里IPC_SET这个宏,我们目前还应用不到,那么这里我们如果要将共享内存标记为删除,那么就传入IPC_RMID即可

而如果我们要获取共享内存的状态,那么我们可以传入IPC_STAT这个宏,此时shmctl的第三个参数就有意义,那么它会接收一个指向struct shm_ds的结构体,那么该结构体的定义是存放在sys/shm.h头文件中,那么这里内核会通过shmid然后访问到该共享内存对应的结构体,根据其结构体来初始化struct shm_ds

struct shmid_ds {struct ipc_perm shm_perm;   // 共享内存段的权限信息size_t          shm_segsz;  // 共享内存段的大小(字节)time_t          shm_atime;  // 最后一次附加的时间time_t          shm_dtime;  // 最后一次断开的时间time_t          shm_ctime;  // 最后一次修改的时间pid_t           shm_cpid;   // 创建共享内存段的进程 IDpid_t           shm_lpid;   // 最后一次操作的进程 IDshmatt_t        shm_nattch; // 当前附加到共享内存段的进程数(引用计数)// ... 其他字段(可能因系统而异)
};
/* 定义在 sys/ipc.h 中 */
struct ipc_perm {key_t          __key;     /* 用于标识 IPC 对象的键值 */uid_t          uid;       /* 共享内存段的所有者用户 ID */gid_t          gid;       /* 共享内存段的所有者组 ID */uid_t          cuid;      /* 创建该 IPC 对象的用户 ID */gid_t          cgid;      /* 创建该 IPC 对象的组 ID */unsigned short mode;      /* 权限位(类似于文件权限) */unsigned short __seq;     /* 序列号,用于防止键值冲突 */
};

那么我们就可以通过访问该结构体中的相关成员变量来获取共享内存的相关属性信息


利用共享内存来实现进程间的通信

那么在上文我介绍了共享内存相关的理论基础以及关于共享内存相关的系统调用接口,那么这里我们就会结合前面所学的知识以及系统调用接口来实现两个进程间通信的一个小项目,那么这里介绍这个项目之前,我们还是来梳理一下大致的框架,然后再具体落实具体各个模块的代码怎么去写

大体框架

那么这里既然要实现进程间的通信,那么我首先就得准备两个陌生进程,分别是processA以及processB,那么processA进程的任务就是就是负责创建共享内存,然后将创建好的共享内存挂载,然后processA作为共享内存的写入方,向共享内存写入数据,最后解除挂载,然后清理共享内存资源,而processB的任务则是访问processA创建好的共享内存,然将该共享内存挂载到其虚拟地址空间,然后processB作为共享内存的读取法,读取数据,最后解除挂载

comm.hpp

那么这里我们知道到时候A和B进程会接收到一个共同的key,然后一个进程用这个key来创建共享内存,而另一个进程则是用该key来获取该共享内存的shmid,所以到时候这两个进程对应的源文件会各自引用该comm.hpp头文件,那么comm.hpp中就会定义一个全局变量的key,然后其中会定义一个Creatkey函数,那么该函数内部就会调用ftok接口来生成一个key值并且返回,而comm.hpp中还会定义CreaShm函数和Getshm函数,那么从这两个函数名我们就知道它们各自的作用,那么CreatShm函数就是提供给A进程使用的,它的作用是创建共享内存,并且返回其shmid,而GetShm则是提供给process B使用,那么它的作用就是获取process A打开的共享内存并且返回其shmid,而这里只是梳理大致框架,那么具体的实现会在后文给出

processA.cpp

1.创建共享内存

那么这里对于process A来说,那么它的第一个环节就是创建共享内存,也就是调用CreatShm函数来获取shmid

2.将共享内存挂载到虚拟地址空间

那么接下来获取到共享内存的shmid之后,那么下一步便是调用shmat接口来将该共享内存给挂载到processA进程的虚拟地址空间,然后获取其共享内存的起始位置的虚拟地址

3.向共享内存中写入数据

那么这个环节就是根据之前获取到的共享内存的虚拟地址,然后通过该虚拟地址向共享内存中写入数据,其中写入数据会回封装到一个死循环的逻辑当中

4.解除挂载

那么这个环节就是解除共享内存与process A的关联,其中涉及调用shmdet

5.清理共享内存资源

那么这里清理共享内存资源会调用shmctl接口,因为shmdet只是减少引用计数以及删除该进程关于该共享内存的映射条目

processB.cpp

1.获取process A进程创建的共享内存

那么这里就会通过调用GetShm来获取process A进程创建的共享内存的shmid

2.将共享内存挂载

那么这个环节和上文的process A所做的内容是一样的,就是调用shmat接口,然后获取该共享内存的起始位置的虚拟地址

3.读取process A向共享内存写入的数据

那么这里我们会同样会根据上一个环节获取到的虚拟地址,而通过该虚拟地址读取共享内存的内容

4.解除挂载

那么这里对于process A进程来说,那么由于process A进程来完成的共享内存的删除,所以这里对于B进程来说,那么这里它只需解除与共享内存的挂载即可

各个模块的具体实现

comm.hpp

那么这里的comm.hpp的内容就包括process A进程以及process B进程会持有的key,以及Creatkey函数,该函数内部会调用ftok函数来获取到要创建的共享内存的key值,而ftok函数会接收一个已存在的路径以及文件名,和project_id,那么这里就得保证传递给ftok函数的路径名以及文件名一致,那么这里我们将文件的路径以及文件名定义为全局的string类型的变量同时将project_id也定义为了全局变量,而CreatShm函数则是创建共享内存,那么这里内部实现就会涉及到调用shmget接口,那么GetShm函数则是获取到process A创建的共享内存的shmid,那么这里内部也要调用shmget,只不过传递给shmget接口的宏不一样

而这里我进行一个巧妙的处理,那么这里我直接函数的复用,那么直接在GetShm函数内部直接复用定义好的CreatShm函数,那么这里就得利用缺省参数,那么这里的默认缺省参数就是IPC_CREAT|IPC_EXCL|066,那么这里调用GetShm函数中就会显示传递一个IPC_CREAT的宏,那么此时GetShm函数就会返回一个与相同key值的共享内存的shmid,也就是process A创建的共享内存的shmid

const std::string pathname="/home/WangZhe";
int key;
log a;
void CreatKey()
{key=ftok(pathname.c_str(),ProjectId);if(key<0){a.logmessage(Fatal,"ftoke调用失败");exit(EXIT_FAILURE);}
}size_t CreatShm(int flag=IPC_CREAT|IPC_EXCL|0666)
{CreatKey();int shmid=shmget(key,SHM_SIZE,flag);if(shmid<0){a.logmessage(Fatal,"shemget fail:%s",strerror(errno));exit(EXIT_FAILURE);}return shmid;
}
size_t GetShm()
{int shmid=CreatShm(IPC_CREAT|0666);return shmid;
}

那么这里在函数内部还进行了相应的日志打印逻辑,那么如果对日志不熟悉的读者,那么建议看我之前的一期博客

processA.cpp

1.创建共享内存

那么这里对于processA.cpp来说,第一个环节就是调用CreatShm函数来创建大小为4096字节的共享内存并且获取返回值,那么这里我们还要对返回值进行检查,如果shmget接口调用失败,那么返回值是-1,那么这个错误是致命的,那么程序就无法再继续往下正常运行,然后进行相应的日志打印,并且退出

int shmid=CreatShm();a.logmessage(debug,"processA creat Shm successfully:%d",shmid);int n=mkfifo(FIFO_FILE,0666);if(n<0){a.logmessage(Fatal,"creat fifo fail:%s",strerror(errno));exit(EXIT_FAILURE);}
2.将共享内存挂载到虚拟地址空间

那么该环节会利用上一个步骤创建的共享内存的shmid,那么这里会调用shmat接口将共享内存挂载到process A的地址空间,并且此时会返回一个void* 的指针,那么该指针就是指向共享内存起始位置的虚拟地址,那么这里接下来process A进程向共享内存中写入数据就会利用该虚拟地址,那么这里我们使用该虚拟地址可以联系我们通过调用malloc函数在堆上申请了一个连续的空间,然后得到该空间的首元素的地址,然后我通过该首元素地址来访问该空间并且写入的过程

那么这里由于之后我们要写入的消息是字符串,那么这里我们就可以将共享内存视作一个字符数组,那么这里我们就要将void*的指针强制转化为char *类型

而如果shmat调用失败,那么此时会返回(void*)-1的指针,那么这里注意的就是这里的-1是指针类型,也就是说这里的-1不能按照所谓的数值为-1来理解,而是得按照一个值为-1的二进制序列,那么这里我们比较返回值与(void *)-1,判断shmat是否调用成功

char* Shmadder=(char*)shmat(shmid,NULL,NULL);if(Shmadder==(void*)-1){a.logmessage(Fatal,"processA attach Fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA attch successfully:0x%x",Shmadder);
3.向共享内存中写入数据

那么这里就是根据之前获取到的共享内存的虚拟地址,然后通过该虚拟地址向共享内存中写入数据,而我们将写入的操作封装到一个死循环中,那么这里我们就得注意一个同步机制的问题

同步:

那么这里由于A进程和B进程都是一个死循环的读取以及打印的逻辑,那么这里就会导致一个问题,那么我们知道A进程是写入方进程,那么在A进程在写入的过程中,那么在同一个时刻下的B进程会一直从共享内存中读取数据,那么就会出现这样的场景,那么假设此时A进程向共享内存写入了一条消息,那么同一个时刻下的B进程读取到了这条消息,那么接着A进程便会等待获取用户的键盘输入的下一条消息,而我们知道此时对于共享内存来说,它里面存储的数据还是之前上一个时刻的A进程写入的,那么数据没有被覆盖,而与此同时对于B进程来说,它根本不管A进程此时是否正在写入下一条消息,那么它只是无脑的从共享内存中不停的读取,那么此时它在当前时刻会获取到的消息则是A进程在上一个时刻写的消息,而此时A进程还在等待用户的键盘输入,没有向共享内存中写入,那么此时共享内存中的数据还未被覆盖,那么此时B进程的视角下,那么B进程在当前时刻读取的消息就会被视作是A进程在当前时刻写入的消息,但是事实上,A还没有往共享内存中写入所以这个场景就是典型的一个读写不同步带来的问题

其次如果说此时B进程正在读取拷贝共享内存中的数据,但是此时在同一时刻的A进程正在向共享内存中写入数据,那么会导致数据被覆盖,那么B进程最终读取的消息就是混乱的,这也是读写不同步带来的问题

所以我希望的就是,当A还没写或者说正在往共享内存中写入一条消息的时候,那么此时B进程就站住不要动,也就是不要向共享内存中读取数据,那么一旦A进程消息写完了,然后你B进程在动,开始从共享内存或者读取数据,那么这样就是读写同步,那么这里实现读写同步,可以通过锁来实现,但是对于初学真来说,可能当前没有学过或者接触过锁,那么这里我们就采取的就是命名管道来实现同步机制


那么这里可能有的读者会有疑惑,这里我知道此时A和B进程采取共享内存通信,会有读写不同步的问题,但是这里你采取的措施是通过命名管道来实现读写同步,而我们知道命名管道的作用就是可以实现非父子进程的通信,那么你干脆就直接用命名管道通信就结束了,那么还搞一个共享内存,岂不是多次一举?

那么对于这个疑问,那么首先我想说的就是,命名管道确实可以传递消息,但是对于共享内存来说,我们是直接向物理内存中写入以及读取数据,虽然A和B进程双方持有的是虚拟地址,但是我们只需要经历一次虚拟地址到物理地址的映射转换便能直接访问到物理内存,而这里通过命名管道写入消息,那么就会涉及到调用系统调用接口,比如write以及read接口,而系统接口的调用是有成本有代价的,那么这里你比如调用write接口向共享内存中写入数据,那么其中涉及到的工作,就是会首先找到该文件描述符对应的file结构体,然后还要定位其物理内存,最后再拷贝写入,那么这个时间代价明显比共享内存更大,所以说这里采取共享内存是更优秀的


所以这里首先A进程需要先调用mkfifo接口来创建一个命名管道,然后再调用open接口打开该命名管道,获取到该命名管道文件的文件描述符,那么命名管道的内容就是一个字符,那么这个字符的内容代表的就是当前是否继续读取以及是否退出,那么这里字符x表示退出,如果进程B从管道读取到了字符x,那么代表着此时进程A结束通信,那么就直接退出,而如果读取到的是字符a,那么代表这此时进程A向共享内存中写入了一条有效消息,那么需要B进程去读取

那么接下来就有一个问题,那么这里我们是A进程是先向管道文件中写入信息,还是先向共享内存中写入信息,那么可能有的读者会有这样的感觉,那么就是这里A进程先向管道文件中写入信息,先告诉b进程我现在是要给你发送一条消息还是说我要结束通信了,那么发送完信息之后,此时我A进程在向共享内存中写入消息,然后让B进程去读

那么这里就得注意一个问题,那么如果采取的是这种方式,对于B进程来说,那么它毫无疑问肯定是得先读取管道文件的信息,确定A进程的意图是要我读取还是说结束通信,然后下一步再来读取共享内存,那么如果此时A没有向管道文件中写入信息,那么此时B进程作为读取端,由于调用了read接口读取管道文件,此时B进程会陷入阻塞,如果此时A进程先向管道文件中直接写入了信息,那么在同一时刻下,B进程读取到管道文件的信息,那么它从立即阻塞状态切换为运行,那么它就会立即执行后面的读取共享内存的代码,而在同一个时刻下,A进程此时还在等待用户的键盘输入的消息,还没有往共享内存中写入,而此时你B进程就已经开始读了,那么读的消息就是之前A进程写入的消息,那么还是会导致读写不同步的问题

所以这里就得梳理清楚这个关键的细节,那么就是这里A进程得先向共享内存中写入消息,然后再写向管道写入信息,这里对于B进程来说,它会一直陷入阻塞直到A进程向管道写入了消息,然后开始读取,这样就可以做到读写同步

 while(true){std::cout<<"Please Enter the messasge:"<<std::endl;fgets(Shmadder,1024,stdin);size_t len=strlen(Shmadder);if(len>0&&Shmadder[len-1]=='\n'){Shmadder[len-1]='\0';}char c;std::cout<<"Please Enter the code(Press a:continue to send message/Press x:stop sending):"<<std::endl;std::cin>>c;getchar();int n=write(fd,&c,1);if(c=='x'){break;}if(n<0){a.logmessage(Fatal,"write fail:%s",strerror(errno));exit(EXIT_FAILURE);}sleep(1);}

那么这里我采取的就是fets函数往共享内存中写入数据,因为它会首先会读取空格,直到读取换行符结束,那么这里注意的就是fets会读取换行符,并且还会再数据的最后添加一个’\0’标记,,这样就能够方便B进程来确定消息的结尾,但是由于fets会读取换行符,而到时我们B进程通过日志打印终端消息的时候,也会输入一个换行符,所以这里就要处理末尾的换行符,用’\0’来覆盖

而这里要注意的就是我们这里向管道文件写入字符c的时候,那么这里我们是从标准输入中读取将其赋值给字符c,而这里我们最后会敲一个回车键也就是换行符,而这里cin读取标准输入和fets不同的是,它这里不会读取换行符,读到换行符就结束,那么就会导致缓冲区会遗留一个换行符,那么这里我们就通过getchar来将这个换行符给读取出来

4.解除挂载

那么最后剩下的两个环节就很简单了,那么这里就是调用shmdet接口解除挂载,然后判断一下返回值,然后进行相应的日志打印

n=shmdt(Shmadder);if(n<0){a.logmessage(Fatal,"processA detach FAILER:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA detach successfully");
5.清理资源

那么最后一步就是清理资源,包括之前创建的管道文件以及共享内存

close(fd);
unlink(FIFO_FILE);
n=shmctl(shmid,IPC_RMID,NULL);if(n<0){a.logmessage(Fatal,"processA shmctl fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(info,"processA quit successfully");

processB.cpp

1.获取process A进程创建的共享内存

那么这里这个环节调用GetShm来获取进程A创建的共享内存,获取其shmid

 int shmid=GetShm();a.logmessage(debug,"processB get Shm successfully:%d",shmid);
2.将共享内存挂载

那么这个环节和上文的process A所做的内容是一样的,就是调用shmat接口,然后获取该共享内存的起始位置的虚拟地址

 a.logmessage(debug,"processB open fifo successfully");char* Shmadder=(char*)shmat(shmid,NULL,NULL);if(Shmadder==(void*)-1){a.logmessage(Fatal,"attch fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processB attch successfully:0x%x",Shmadder);
读取process A向共享内存中写入的数据

那么这里由于在上文,我介绍了进程双方的读写同步的机制,那么这里对于B进程来说,那么它首先就要读取管道中的信息,确定A进程的意图,如果读取到的字符是a,说明A进程此时向共享内存写入了一条消息,然后我定义了一个临时的字符数组,从共享内存中读取1024个字节数据拷贝到该字符数组中,而如果此时读到的字符是x,说明A进程此时结束通信,那么就退出循环

 while(true){char c;int n=read(fd,&c,1);if(c=='x'){break;}else if(n==0){break;}else if(n<0){a.logmessage(Fatal," processB read fail:%s",strerror(errno));exit(EXIT_FAILURE);}else {char buffer[1024]={0};memcpy(buffer,Shmadder,1024);a.logmessage(info,"processB get a message:%s",buffer);}}
5.清理资源

那么这里对于B进程来说,那么它只需要关闭管道文件的读端以及解除挂载即可,因为管道文件以及共享内存的删除都交给了A进程

  close(fd);int n=shmdt(Shmadder);if(n<0){a.logmessage(Fatal,"processB detach fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processB detach successfully");a.logmessage(info,"processB quit successfully");

源码

comm.hpp

#pragma once
#include<sys/ipc.h>
#include<iostream>
#include<sys/shm.h>
#include<sys/types.h>
#include<sys/stat.h>
#include"log.hpp"
#include<cerrno>
#include<cstring>
#include<cstdio>
#define SHM_SIZE 4096
#define FIFO_FILE "./myfifo"
#define EXIT_FAILURE  1
#define ProjectId 110
const std::string pathname="/home/WangZhe";
int key;
log a;
void CreatKey()
{key=ftok(pathname.c_str(),ProjectId);if(key<0){a.logmessage(Fatal,"ftoke调用失败");exit(EXIT_FAILURE);}
}size_t CreatShm(int flag=IPC_CREAT|IPC_EXCL|0666)
{CreatKey();int shmid=shmget(key,SHM_SIZE,flag);if(shmid<0){a.logmessage(Fatal,"shemget fail:%s",strerror(errno));exit(EXIT_FAILURE);}return shmid;
}
size_t GetShm()
{int shmid=CreatShm(IPC_CREAT|0666);return shmid;
}

log.hpp

#pragma once
#include<iostream>
#include<string>
#include<time.h>
#include<stdarg.h>
#include<fcntl.h>
#include<unistd.h>
#define SIZE 1024
#define screen 0
#define File 1
#define ClassFile 2
enum
{info,debug,warning,Fatal,
};
class log
{private:std::string memssage;int method;public:log(int _method=screen):method(_method){}void logmessage(int leval,char* format,...){char* _leval;switch(leval){case info:_leval="info";break;case debug:_leval= "debug";break;case warning:_leval="warning";break;case Fatal:_leval="Fatal";break;}char timebuffer[SIZE];time_t t=time(NULL);struct tm* localTime=localtime(&t);snprintf(timebuffer,SIZE,"[%d-%d-%d-%d:%d]",localTime->tm_year+1900,localTime->tm_mon+1,localTime->tm_mday,localTime->tm_hour,localTime->tm_min);char rightbuffer[SIZE];va_list arg;va_start(arg,format);vsnprintf(rightbuffer,SIZE,format,arg);char finalbuffer[2*SIZE];snprintf(finalbuffer,sizeof(finalbuffer),"[%s]%s:%s",_leval,timebuffer,rightbuffer);int fd;switch(method){case screen:std::cout<<finalbuffer<<std::endl;break;case File:fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd>=0){write(fd,finalbuffer,sizeof(finalbuffer));close(fd);           }break;case ClassFile:switch(leval){case info:fd=open("log/info.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case debug:fd=open("log/debug.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case warning:fd=open("log/Warning.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case Fatal:fd=open("log/Fatal.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);break;}if(fd>0){write(fd,finalbuffer,sizeof(finalbuffer));close(fd);}}}
};

processA.cpp

#include"comm.hpp"
int main()
{int shmid=CreatShm();a.logmessage(debug,"processA creat Shm successfully:%d",shmid);int n=mkfifo(FIFO_FILE,0666);if(n<0){a.logmessage(Fatal,"creat fifo fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA creat fifo successfully");a.logmessage(info,"processA is waiting for processB open");int fd=open(FIFO_FILE,O_WRONLY);if(fd<0){a.logmessage(Fatal,"processA open fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA open fifo successfully");char* Shmadder=(char*)shmat(shmid,NULL,NULL);if(Shmadder==(void*)-1){a.logmessage(Fatal,"processA attach Fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA attch successfully:0x%x",Shmadder);while(true){std::cout<<"Please Enter the messasge:"<<std::endl;fgets(Shmadder,1024,stdin);size_t len=strlen(Shmadder);if(len>0&&Shmadder[len-1]=='\n'){Shmadder[len-1]='\0';}char c;std::cout<<"Please Enter the code(Press a:continue to send message/Press x:stop sending):"<<std::endl;std::cin>>c;getchar();int n=write(fd,&c,1);if(c=='x'){break;}if(n<0){a.logmessage(Fatal,"write fail:%s",strerror(errno));exit(EXIT_FAILURE);}sleep(1);}close(fd);unlink(FIFO_FILE);n=shmdt(Shmadder);if(n<0){a.logmessage(Fatal,"processA detach FAILER:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA detach successfully");n=shmctl(shmid,IPC_RMID,NULL);if(n<0){a.logmessage(Fatal,"processA shmctl fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(info,"processA quit successfully");exit(0);
}

processB.cpp

#include"comm.hpp"
int main()
{int shmid=GetShm();a.logmessage(debug,"processB get Shm successfully:%d",shmid);int fd=open(FIFO_FILE,O_RDONLY);if(fd<0){a.logmessage(Fatal,"processB open fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processB open fifo successfully");char* Shmadder=(char*)shmat(shmid,NULL,NULL);if(Shmadder==(void*)-1){a.logmessage(Fatal,"attch fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processB attch successfully:0x%x",Shmadder);while(true){char c;int n=read(fd,&c,1);if(c=='x'){break;}else if(n==0){break;}else if(n<0){a.logmessage(Fatal," processB read fail:%s",strerror(errno));exit(EXIT_FAILURE);}else {char buffer[1024]={0};memcpy(buffer,Shmadder,1024);a.logmessage(info,"processB get a message:%s",buffer);}}close(fd);int n=shmdt(Shmadder);if(n<0){a.logmessage(Fatal,"processB detach fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processB detach successfully");a.logmessage(info,"processB quit successfully");exit(0);
}

运行截图:
在这里插入图片描述

结语

那么这就是本篇关于共享内存的全面介绍了,带你从多个维度来全面剖析共享内存,那么下一期博客将会进入Linux的倒数第二座大山,那么便是信号,那么我会持续更新,希望你能够多多关注,如果本文有帮组到你,还请三连加关注哦,你的支持就是我创作的最大的动力!
在这里插入图片描述

相关文章:

【Linux实践系列】:进程间通信:万字详解共享内存实现通信

&#x1f525; 本文专栏&#xff1a;Linux Linux实践项目 &#x1f338;作者主页&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客励志语录&#xff1a; 人生就像一场马拉松&#xff0c;重要的不是起点&#xff0c;而是坚持到终点的勇气 ★★★ 本文前置知识&#xff1a; …...

无法更新Google Chrome的解决问题

解决问题&#xff1a;原文链接&#xff1a;【百分百成功】Window 10 Google Chrome无法启动更新检查&#xff08;错误代码为1&#xff1a;0x80004005&#xff09; google谷歌chrome浏览器无法更新Chrome无法更新至最新版本&#xff1f; 下载了 就是更新Google Chrome了...

CenOS7切换使用界面

永久切换 在开始修改之前&#xff0c;我们首先需要查看当前的启动模式。可以通过以下命令来实现&#xff1a; systemctl get-default执行此命令后&#xff0c;系统会返回当前的默认启动模式&#xff0c;例如graphical.target表示当前默认启动为图形界面模式。 获取root权限&…...

# YOLOv3:深度学习中的目标检测利器

YOLOv3&#xff1a;深度学习中的目标检测利器 引言 在计算机视觉领域&#xff0c;目标检测是一项核心任务&#xff0c;它涉及到识别图像或视频中的物体&#xff0c;并确定它们的位置。随着深度学习技术的快速发展&#xff0c;目标检测算法也在不断进步。YOLO&#xff08;You …...

2025数维杯数学建模A题完整参考论文(共36页)(含模型、可运行代码、数据)

2025数维杯数学建模A题完整参考论文 目录 摘要 一、问题重述 二、问题分析 三、模型假设 四、符号定义与说明 五、 模型建立与求解 5.1问题1 5.1.1问题1思路分析 5.1.2问题1模型建立 5.1.3问题1求解结果 5.2问题2 5.2.1问题2思路分析 5.2.2问题2模型…...

在 Flink + Kafka 实时数仓中,如何确保端到端的 Exactly-Once

在 Flink Kafka 构建实时数仓时&#xff0c;确保端到端的 Exactly-Once&#xff08;精确一次&#xff09; 需要从 数据消费&#xff08;Source&#xff09;、处理&#xff08;Processing&#xff09;、写入&#xff08;Sink&#xff09; 三个阶段协同设计&#xff0c;结合 Fli…...

Python数据分析

目录 一、数据分析的核心流程 &#xff08;一&#xff09;明确数据分析目标 &#xff08;二&#xff09;数据收集 &#xff08;三&#xff09;数据清洗 1. 处理缺失值 2. 去除重复值 3. 修正错误值和异常值 &#xff08;四&#xff09;数据探索与可视化 1. 计算描述性…...

Java单例模式总结

说明&#xff1a;单例模式的核心是确保一个类只有一个实例&#xff0c;并提供全局访问点。饿汉式和懒汉式是两种常见的实现方式 一、饿汉式和懒汉式 1. 饿汉式&#xff08;Eager Initialization&#xff09; public class EagerSingleton {// 类加载时直接初始化实例private…...

《P7167 [eJOI 2020] Fountain (Day1)》

题目描述 大家都知道喷泉吧&#xff1f;现在有一个喷泉由 N 个圆盘组成&#xff0c;从上到下以此编号为 1∼N&#xff0c;第 i 个喷泉的直径为 Di​&#xff0c;容量为 Ci​&#xff0c;当一个圆盘里的水大于了这个圆盘的容量&#xff0c;那么水就会溢出往下流&#xff0c;直到…...

Pycharm(二十)张量的运算与操作

一、张量的数据类型转换 1.演示data.type(trch.DoubleTensor) #1.创建张量对象 [6 6 6;6 6 6] datatorch.full([2,3],6) print(data.dtype)#默认为torch.int64(LongTensor) #2.转化为double类型 datadata.type(torch.DoubleTensor) print(data.dtype) #3.转换成int类型 datad…...

JVM之内存管理(二)

部分内容来源&#xff1a;JavaGuide二哥Java 说⼀下 JDK1.6、1.7、1.8 内存区域的变化&#xff1f; JDK1.6、1.7/1.8 内存区域发⽣了变化&#xff0c;主要体现在⽅法区的实现&#xff1a; JDK1.6 常量池在方法区 JDK1.7 JDK1.6 使⽤永久代实现⽅法区&#xff1a;JDK1.7 时发…...

蓝桥杯嵌入式第十一届省赛真题

&#xff08;一&#xff09;题目 首先要知道P37对应的CubeMx上面的引脚是PB15,给PB15设置成ADC采集。使用到的PA6和PA7的端口要进行定时器配置 100Hz80 000 000/800 *1000 200Hz80 000 000/400 *1000 ADC采集只需要选择好adc1、adc2 再选择好它的通道就可以了&#xff0c;不…...

LLMs之ChatGPT:《Connecting GitHub to ChatGPT deep research》翻译与解读

LLMs之ChatGPT&#xff1a;《Connecting GitHub to ChatGPT deep research》翻译与解读 导读&#xff1a;这篇OpenAI帮助文档全面介绍了将GitHub连接到ChatGPT进行深度代码研究的方法、优势和注意事项。通过连接GitHub&#xff0c;用户可以充分利用ChatGPT强大的代码理解和生成…...

多环境开发

# 应用环境(公共环境,写一些公共配置) spring:profiles:active: dev --- # 设置环境 # 生产环境 spring:config:activate:on-profile: pro server:port: 80 --- # 开发环境 spring:config:activate:on-profile: dev server:port: 81 --- # 测试环境 spring:config:activate:on-…...

游戏引擎学习第269天:清理菜单绘制

回顾并为今天的工作设定目标 昨天我们对调试系统中的菜单处理做了一些清理工作&#xff0c;今天我想继续对它们的展示和使用方式进行一些打磨和改进。今天的计划还不完全确定&#xff0c;我还没有完全决定要做什么&#xff0c;但是我希望能够完成这部分工作&#xff0c;所以我…...

《解锁React Native与Flutter:社交应用启动速度优化秘籍》

React Native和Flutter作为当下热门的跨平台开发框架&#xff0c;在优化应用启动性能方面各有千秋。今天&#xff0c;我们就深入剖析它们独特的策略与方法。 React Native应用的初始包大小对启动速度影响显著。在打包阶段&#xff0c;通过精准分析依赖&#xff0c;去除未使用的…...

Web3 初学者的第一个实战项目:留言上链 DApp

目录 &#x1f4cc; 项目简介&#xff1a;留言上链 DApp&#xff08;MessageBoard DApp&#xff09; &#x1f9e0; 技术栈 &#x1f536; 1. Solidity 智能合约代码&#xff08;MessageBoard.sol&#xff09; &#x1f537; 2. 前端代码&#xff08;index.html script.js…...

Innovus 25.1 版本更新:助力数字后端物理设计新飞跃

在数字后端物理设计领域&#xff0c;每一次工具的更新迭代都可能为项目带来巨大的效率提升与品质优化。今天&#xff0c;就让我们一同聚焦 Innovus 25.1 版本&#xff08;即 25.10 版本&#xff09;的更新要点&#xff0c;探寻其中蕴藏的创新能量。 一、核心功能的强势进 AI…...

Git简介和发展

Git 简介 Git是一个开源的分布式版本控制系统&#xff0c;跨平台&#xff0c;支持Windows、Linux、MacOS。主要是用于项目的版本管理&#xff0c;是由林纳斯托瓦兹(Linux Torvalds)在2005年为Linux内核开发而创建。 起因 在2002年至2005年间&#xff0c;Linux内核开发团队使…...

adb 实用命令汇总

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ 基础adb命令 # 重启adb adb kill-server# 查看已连接的设备 adb devices# 进入命令行 adb shell# 使用 -s 参数来指定设备 adb -s <设备序列号> shell…...

DAX 权威指南1:DAX计算、表函数与计算上下文

参考《DAX 权威指南 第二版》 文章目录 二、DAX简介2.1 理解 DAX 计算2.2 计算列和度量值2.3 变量2.3.1 VAR简介2.3.2 VAR的特性 2.4 DAX 错误处理2.4.1 DAX 错误类型2.4.1.1 转换错误2.4.1.2 算术运算错误2.4.1.3 空值或 缺失值 2.4.2 使用IFERROR函数拦截错误2.4.2.1 安全地进…...

使用fdisk 、gdisk管理分区

用 fdisk 管理分区 fdisk 命令工具默认将磁盘划分为 mbr 格式的分区 命令&#xff1a; fdisk 设备名 fdisk 命令以交互方式进行操作的&#xff0c;在菜单中选择相应功能键即可 [rootlocalhost ~]# fdisk /dev/sda # 对 sda 进行分区 Command (m for help): # 进入 fdis…...

Python----神经网络(《Deep Residual Learning for Image Recognition》论文和ResNet网络结构)

一、论文 1.1、论文基本信息 标题&#xff1a;Deep Residual Learning for Image Recognition 作者&#xff1a;Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun 单位&#xff1a;Microsoft Research 会议&#xff1a;CVPR 2016 主要贡献&#xff1a;提出了一种深度残…...

PostgreSQL 的 pg_collation_actual_version 函数

PostgreSQL 的 pg_collation_actual_version 函数 pg_collation_actual_version 是 PostgreSQL 中用于检查排序规则实际版本信息的函数&#xff0c;主要与 ICU (International Components for Unicode) 排序规则相关。 函数基本概念 函数定义 pg_collation_actual_version(…...

使用Simulink开发Autosar Nvm存储逻辑

文章目录 前言Autosar Nvm接口设计模型及接口生成代码及arxmlRTE接口mappingRTE代码分析总结 前言 之前介绍过Simulink开发Dem故障触发逻辑&#xff0c;本文接着介绍另外一个常用的功能-Nvm存储的实现。 Autosar Nvm接口 Autosar Nvm中一般在上电初始化的时调用Nvm_ReadAll获…...

嵌入式STM32学习——继电器

继电器模块引脚说明 VCC&#xff08;&#xff09;&#xff1a; 供电正极。连接此引脚到电源&#xff08;通常是直流电源&#xff09;&#xff0c;以提供继电器线圈所需的电流。 GND&#xff08;-&#xff09;&#xff1a; 地。连接此引脚到电源的负极或地。 IN&#xff08;或…...

更换内存条会影响电脑的IP地址吗?——全面解析

在日常电脑维护和升级过程中&#xff0c;许多用户都会遇到需要更换内存条的情况。与此同时&#xff0c;不少用户也担心硬件更换是否会影响电脑的网络配置&#xff0c;特别是IP地址的设置。本文将详细探讨更换内存条与IP地址之间的关系&#xff0c;帮助读者理解这两者之间的本质…...

AWS SNS:解锁高并发消息通知与系统集成的云端利器

导语 在分布式系统架构中&#xff0c;如何实现高效、可靠的消息通知与跨服务通信&#xff1f;AWS Simple Notification Service&#xff08;SNS&#xff09;作为全托管的发布/订阅&#xff08;Pub/Sub&#xff09;服务&#xff0c;正在成为企业构建弹性系统的核心组件。本文深度…...

【Java ee初阶】网络编程 TCP

TCP的socket api 两个核心的类 ServerSocket 创建一个这样的对象&#xff0c;就相当于打开了一个socket文件。 这个socket对象是给服务器专门使用的 这个类本身不负责发送接收。 主要负责“建立连接” Socket 创建一个这样的对象&#xff0c;也就相当于打开了一个socket文…...

达索MODSIM实施成本高吗?哪家服务商靠谱?

在数字化转型的浪潮中&#xff0c;越来越多的制造企业开始关注达索系统的MODSIM技术。记得去年参加行业峰会时&#xff0c;一位来自汽车零部件企业的技术总监向我倾诉了他的困扰&#xff1a;"我们都知道MODSIM能提升研发效率&#xff0c;但听说实施成本很高&#xff0c;又…...

ISP接口隔离原则

任何层次的软件设计如果依赖了它并不需要的东西&#xff0c;就会带来意料之外的麻烦。ISP强调使用多个特定的接口&#xff0c;而不是一个总接口&#xff0c;避免依赖不需要的接口。即不需要则不应该知道。 ISP特点 降低耦合度&#xff1a;客户端只依赖它需要的接口&#xff0…...

AI Agent(8):安全与伦理考量

引言 AI Agent作为具有一定自主性的智能系统,其行为可能产生深远影响。确保这些系统安全、可靠、符合伦理标准,并遵守相关法规,不仅是技术挑战,也是社会责任。 随着AI Agent能力的增强,其潜在风险也在增加,从数据泄露到决策偏见,从自主性滥用到责任归属不清,这些问题…...

Python3虚拟环境与包管理:项目隔离的艺术

Python3虚拟环境与包管理 为什么需要虚拟环境&#xff1f;虚拟环境工具&#xff1a;你的岛屿建设者一、使用venv创建虚拟环境创建虚拟环境激活虚拟环境退出虚拟环境 二、 包管理&#xff1a;岛上的补给系统2.1 pip&#xff1a;Python的包安装工具基本用法依赖管理 2.2 高级包管…...

23、DeepSeekMath论文笔记(GRPO)

DeepSeekMath论文笔记 0、研究背景与目标1、GRPO结构GRPO结构PPO知识点**1. PPO的网络模型结构****2. GAE&#xff08;广义优势估计&#xff09;原理****1. 优势函数的定义**2.GAE&#xff08;广义优势估计&#xff09; 2、关键技术与方法3、核心实验结果4、结论与未来方向关键…...

Python自动化-python基础(下)

六、带参数的装饰器 七、函数生成器 运行结果&#xff1a; 八、通过反射操作对象方法 1.添加和覆盖对象方法 2.删除对象方法 通过使用内建函数: delattr() # 删除 x.a() print("通过反射删除之后") delattr(x, "a") x.a()3 通过反射判断对象是否有指定…...

用Python绘制动态彩色ASCII爱心:技术深度与创意结合

引言 在技术博客的世界里&#xff0c;代码不仅仅是解决问题的工具&#xff0c;更可以是表达创意的媒介。今天我将分享一个独特的Python爱心代码项目&#xff0c;它结合了数学之美、ASCII艺术和动态效果&#xff0c;展示了Python编程的无限可能。这个项目不仅能运行展示出漂亮的…...

【C++】红黑树

1.红黑树的概念 是一种二叉搜索树&#xff0c;在每个节点上增加一个存储位表示节点的颜色&#xff0c;Red或black&#xff0c;通过对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c;确保没有一条路径会比其他路径长出俩倍&#xff0c;是接近平衡的。 2.红黑树…...

链表头插法的优化补充、尾插法完结!

头插法的优化补充 这边我们将考虑到可以将动态创建链表&#xff0c;和插入新链表到链表头前方&#xff0c;成为新链表头的方法分开&#xff0c;使其自由度上升&#xff0c;在创建完链表后&#xff0c;还可以添加链表元素到成为新的链表头。 就是说可以单独的调用这个insertHea…...

Java多线程(超详细版!!)

Java多线程(超详细版&#xff01;&#xff01;) 文章目录 Java多线程(超详细版&#xff01;&#xff01;)1. 线程 进程 多线程2.线程实现2.1线程创建2.1.1 继承Thread类2.1.2 实现runnable接口2.1.2.1 思考&#xff1a;为什么推荐使用runnable接口&#xff1f;2.1.2.1.1 更高的…...

超详细fish-speech本地部署教程

本人配置&#xff1a; windows x64系统 cuda12.6 rtx4070 一、下载fish-speech模型 注意&#xff1a;提前配置好git&#xff0c;教程可在自行搜索 git clone https://gitclone.com/github.com/fishaudio/fish-speech.git cd fish-speech 或者直接进GitHub中下载也可以 …...

Flink和Spark的选型

在Flink和Spark的选型中&#xff0c;需要综合考虑多个技术维度和业务需求&#xff0c;以下是在项目中会重点评估的因素及实际案例说明&#xff1a; 一、核心选型因素 处理模式与延迟要求 Flink&#xff1a;基于事件驱动的流处理优先架构&#xff0c;支持毫秒级低延迟、高吞吐的…...

解锁 DevOps 新境界 :使用 Flux 进行 GitOps 现场演示 – 自动化您的 Kubernetes 部署

前言 GitOps 是实现持续部署的云原生方式。它的名字来源于标准且占主导地位的版本控制系统 Git。GitOps 的 Git 在某种程度上类似于 Kubernetes 的 etcd&#xff0c;但更进一步&#xff0c;因为 etcd 本身不保存版本历史记录。毋庸置疑&#xff0c;任何源代码管理服务&#xf…...

【从零实现JsonRpc框架#1】Json库介绍

1.JsonCpp第三方库 JSONCPP 是一个开源的 C 库&#xff0c;用于解析和生成 JSON&#xff08;JavaScript Object Notation&#xff09;数据。它提供了简单易用的接口&#xff0c;支持 JSON 的序列化和反序列化操作&#xff0c;适用于处理配置文件、网络通信数据等场景。 2.Jso…...

使用FastAPI和React以及MongoDB构建全栈Web应用02 前言

Who this book is for 本书适合哪些人阅读 This book is designed for web developers who aspire to build robust, scalable, and efficient web applications. It caters to a broad spectrum of developers, from those with foundational knowledge to experienced prof…...

JavaScript中的数据类型

目录 前言 基本类型 Number 特殊的数值NaN Infinity和-Infinity String Boolean Undefined null Symbol Undefined和Null的区别 引用类型 Object&#xff08;对象&#xff09; Array&#xff08;数组&#xff09; Function&#xff08;函数&#xff09; 函数声…...

AI 助力,轻松进行双语学术论文翻译!

在科技日新月异的今天&#xff0c;学术交流中的语言障碍仍然是科研工作者面临的一大挑战。尤其是对于需要查阅大量外文文献的学生、科研人员和学者来说&#xff0c;如何高效地理解和翻译复杂的学术论文成为了一大难题。然而&#xff0c;由Byaidu团队推出的开源项目PDFMathTrans…...

第3.2.3节 Android动态调用链路的获取

3.2.3 Android App动态调用链路 在Android应用中&#xff0c;动态调用链路指的是应用在运行时的调用路径。这通常涉及到方法调用的顺序和调用关系&#xff0c;特别是在应用的复杂逻辑中&#xff0c;理解这些调用链路对于调试和性能优化非常重要。 1&#xff0c;动态调用链路获…...

【Android】文件分块上传尝试

【Android】文件分块上传 在完成一个项目时&#xff0c;遇到了需要上传长视频的场景&#xff0c;尽管可以手动限制视频清晰度和视频的码率帧率&#xff0c;但仍然避免不了视频大小过大的问题&#xff0c;且由于服务器原因&#xff0c;网络不太稳定。这个时候想到了可以将文件分…...

大模型中的三角位置编码实现

Transformer中嵌入表示 位置编码的实现 import torch import math from torch import nn# 词嵌入位置编码实现 class EmbeddingWithPosition(nn.Module):"""vocab_size:词表大小emb_size: 词向量维度seq_max_len: 句子最大长度 (人为设定&#xff0c;例如GPT2…...

深入详解人工智能数学基础——微积分中的自动微分及其在PyTorch中的实现原理

🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,高级开发工程师,数学专业,10年以上C/C++, C#, Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C++、C#等开发语言,熟悉Java常用开发技术,能熟练应用常用数据库SQL server,Oracle,mysql,postgresql等进行开发应用…...