【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的倒数第二座大山,那么便是信号,那么我会持续更新,希望你能够多多关注,如果本文有帮组到你,还请三连加关注哦,你的支持就是我创作的最大的动力!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/904884.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CogView4 文本生成图像

CogView4 文本生成图像 flyfish 基于 CogView4Pipeline 的图像生成程序&#xff0c;其主要目的是依据 JSON 文件里的文本提示信息来生成图像&#xff0c;并且把生成的图像保存到指定文件夹。 JSON 文件格式 [{"prompt": "your first prompt"},{"pr…

路由重发布

路由重发布 实验目标&#xff1a; 掌握路由重发布的配置方法和技巧&#xff1b; 掌握通过路由重发布方式实现网络的连通性&#xff1b; 熟悉route-pt路由器的使用方法&#xff1b; 实验背景&#xff1a;假设学校的某个分区需要配置简单的rip协议路由信息&#xff0c;而主校…

机器人领域和心理学领域 恐怖谷 是什么

机器人领域和心理学领域 恐怖谷 是什么 恐怖谷是一个在机器人领域和心理学领域备受关注的概念,由日本机器人专家森政弘于1970年提出。 含义 当机器人与人类的相似度达到一定程度时,人类对它们的情感反应会突然从积极变为消极,产生一种毛骨悚然、厌恶恐惧的感觉。这种情感…

Go-GJSON 组件,解锁 JSON 读取新姿势

现在的通义灵码不但全面支持 Qwen3&#xff0c;还支持配置自己的 MCP 工具&#xff0c;还没体验过的小伙伴&#xff0c;马上配置起来啦~ https://click.aliyun.com/m/1000403618/ 在 Go 语言开发领域&#xff0c;json 数据处理是极为常见的任务。Go 标准库提供了 encoding/jso…

数据分析_数据预处理

1 数据预处理流程 ①数据清洗:处理数据缺失、数据重复、数据异常等问题,提升数据质量. ②数据转换:涵盖基本数据转换、语义数据转换、衍生数据转换和隐私数据转换,适配分析需求. ③数据集成:整合多源数据. 2 数据清洗 2.1 数据缺失 2.1.1 数值型数据缺失 数值型列的部分数值不…

vue +xlsx+exceljs 导出excel文档

实现功能&#xff1a;分标题行导出数据过多&#xff0c;一个sheet表里表格条数有限制&#xff0c;需要分sheet显示。 步骤1:安装插件包 npm install exceljs npm install xlsx 步骤2&#xff1a;引用包 import XLSX from xlsx; import ExcelJS from exceljs; 步骤3&am…

ThinkPad T440P如何从U盘安装Ubuntu24.04系统

首先制作一个安装 U 盘。我使用的工具是 Rufus &#xff0c;它的官网是 rufus.ie &#xff0c;去下载最新版就可以了。直接打开这个工具&#xff0c;选择自己从ubuntu官网下载Get Ubuntu | Download | Ubuntu的iso镜像制作U盘安装包即可。 其次安装之前&#xff0c;还要对 Thi…

第十七次博客打卡

今天学习的内容是动态规划算法。 动态规划算法&#xff08;Dynamic Programming&#xff0c;简称 DP&#xff09;是一种通过将复杂问题分解为更小的子问题来求解的算法思想。它主要用于解决具有重叠子问题和最优子结构特性的问题。 一、动态规划的基本概念 1. 最优子结构 一个复…

视觉革命来袭!ComfyUI-LTXVideo 让视频创作更高效

探索LTX-Video 支持的ComfyUI 在数字化视频创作领域&#xff0c;视频制作效果的提升对创作者来说无疑是一项重要的突破。LTX-Video支持的ComfyUI便是这样一款提供自定义节点的工具集&#xff0c;它专为改善视频质量、提升生成速度而开发。接下来&#xff0c;我们将详细介绍其功…

Java版ERP管理系统源码(springboot+VUE+Uniapp)

ERP系统是企业资源计划&#xff08;Enterprise Resource Planning&#xff09;系统的缩写&#xff0c;它是一种集成的软件解决方案&#xff0c;用于协调和管理企业内各种关键业务流程和功能&#xff0c;如财务、供应链、生产、人力资源等。它的目标是帮助企业实现资源的高效利用…

CenOS7切换使用界面

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

Dify使用总结

最近完成了一个Dify的项目简单进行总结下搭建服务按照官方文档操作就行就不写了。 进入首页之后由以下组成&#xff1a; 探索、工作室、知识库、工具 探索&#xff1a; 可以展示自己创建的所有应用&#xff0c;一个应用就是一个APP&#xff0c;可以进行测试使用 工作室包含…

计网学习笔记———网络

&#x1f33f;网络是泛化的概念 网络是泛化的概念 &#x1f342;泛化理解 网络的概念在生活中无处不在举例&#xff1a;社交网络、电话网路、电网、计算机网络 &#x1f33f;网络的定义 定义&#xff1a; 离散的个体通过通讯手段连成群体&#xff0c;实现资源的共享与交流、个…

《Python星球日记》 第53天:卷积神经网络(CNN)入门

名人说&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。—— 屈原《离骚》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、图像表示与通道概念1. 数字图像的本质2. RGB颜色模型3. 图像预处理 二、卷积…

SpringBoot2集成xxl-job详解

官方教程 搭建调度中心 Github Gitee 注&#xff1a;版本3.x开始要求Jdk17&#xff1b;版本2.x及以下支持Jdk1.8。如对Jdk版本有诉求&#xff0c;可选择接入不同版本 clone源代码执行xxl-job\doc\db\tables_xxl_job.sql # # XXL-JOB v2.4.1 # Copyright (c) 2015-present, x…

HashMap中put()方法的执行流程

HashMap 是 Java 中最常用的数据结构之一&#xff0c;用于存储键值对。其 put() 方法是向哈希表中插入或更新键值对的核心操作。本文将详细解析 put() 方法的执行过程&#xff0c;涵盖哈希值计算、桶定位、冲突处理和扩容等步骤。 一、put() 方法的执行过程 put() 方法通过一系…

【Oracle认证】MySQL 8.0 OCP 认证考试英文版(MySQL30 周年版)

文章目录 1、MySQL OCP考试介绍2、考试注册流程3、考试复习题库 Oracle 为庆祝 MySQL 30 周年&#xff0c;截止到2025.07.31 之前。所有人均可以免费考取原价245美元 &#xff08;约1500&#xff09;的MySQL OCP 认证。 1、MySQL OCP考试介绍 OCP考试 OCP认证是Oracle公司推…

SpringBoot框架开发网络安全科普系统开发实现

概述 基于SpringBoot框架的网络安全科普系统开发指南&#xff0c;该系统集知识科普、案例学习、在线测试等功能于一体&#xff0c;本文将详细介绍系统架构设计、功能实现及技术要点&#xff0c;帮助开发者快速构建专业的网络安全教育平台。 主要内容 系统功能架构 本系统采…

浏览器HTTP错误、前端常见报错 和 Java后端报错

以下是 浏览器HTTP错误、前端常见报错 和 Java后端报错 的综合整理&#xff0c;包括原因和解决方法&#xff0c;帮助你快速排查问题。 一、HTTP 错误&#xff08;浏览器报错&#xff09; 错误码原因解决方法400 Bad Request请求语法错误&#xff08;如参数格式错误、请求体过…

TypeScript简介

&#x1f31f; TypeScript入门 TypeScript 是 JavaScript 的超集&#xff0c;由微软开发并维护&#xff0c;通过静态类型检查和现代语言特性&#xff0c;让大型应用开发变得更加可靠和高效。 // 一个简单的 TypeScript 示例 interface User {name: string;age: number;greet():…