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

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

2025-05-13 08:00:07 0 阅读

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


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

★★★ 本文前置知识:

匿名管道

命名管道


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

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

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

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

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


共享内存

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

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

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

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

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

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

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

  • shmget
  • 头文件
  • 函数声明: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
  • 头文件
  • 函数声明: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
  • 头文件
  • 函数声明: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
  • 头文件
  • 函数声明: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
  • 头文件
  • 函数声明: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;   // 创建共享内存段的进程 ID
    pid_t           shm_lpid;   // 最后一次操作的进程 ID
    shmatt_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]=='
')
      {
           Shmadder[len-1]='';
      }
      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会读取换行符,并且还会再数据的最后添加一个’’标记,,这样就能够方便B进程来确定消息的结尾,但是由于fets会读取换行符,而到时我们B进程通过日志打印终端消息的时候,也会输入一个换行符,所以这里就要处理末尾的换行符,用’’来覆盖

而这里要注意的就是我们这里向管道文件写入字符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
#include
#include
#include
#include
#include"log.hpp"
#include
#include
#include
#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
#include
#include
#include
#include
#include
#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]=='
')
      {
           Shmadder[len-1]='';
      }
      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的倒数第二座大山,那么便是信号,那么我会持续更新,希望你能够多多关注,如果本文有帮组到你,还请三连加关注哦,你的支持就是我创作的最大的动力!

本文地址:https://www.vps345.com/10199.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP docker 容器 运维 java-rabbitmq java 服务器安全 网络安全策略 防御服务器攻击 安全威胁和解决方案 程序员博客保护 数据保护 安全最佳实践 服务器 linux 游戏 云计算 网络工程师 网络管理 软考 2024 2024年上半年 下午真题 答案 物联网 ubuntu Deepseek Deepseek-R1 大模型 私有化部署 推理模型 deepseek DeepSeek-R1 API接口 英语 RTSP xop RTP RTSPServer 推流 视频 DNS 人工智能 redis Ollama Qwen2.5-coder 离线部署 c语言 网络 ssh macos windows conda YOLO pytorch vscode python yolov5 ddos android 网络安全 web安全 php 开发语言 神经网络 深度学习 计算机视觉 卷积神经网络 CH340 单片机 嵌入式硬件 串口驱动 CH341 uart 485 django centos gcc centos 7 tcp/ip 网络协议 ip协议 阿里云 llama 算法 opencv 自然语言处理 语言模型 vue.js spring boot 前端 nginx 经验分享 学习 笔记 学习方法 MCP mysql adb https arm开发 架构 数据库系统 pycharm ide 网络结构图 负载均衡 tomcat 游戏引擎 计算机外设 bug ai nlp Ubuntu 开发环境 部署 进程 操作系统 进程控制 harmonyos 华为 javascript chrome edge 模型联网 API CherryStudio jvm 虚拟机 ubuntu 18.04 安装教程 typescript 计算机网络 云原生 ip apache 智能路由器 外网访问 内网穿透 端口映射 数据库 oracle 关系型 安全 分布式 高级IO epoll 像素流送api 像素流送UE4 像素流送卡顿 像素流送并发支持 科技 个人开发 dify kubernetes prometheus grafana mcu virtualenv asm 策略模式 c++ node.js 知识库 本地化部署 USB转串口 鸿蒙 后端 机器学习 ui jellyfin nas fastapi mcp mcp-proxy mcp-inspector fastapi-mcp agent sse eureka debian PVE live555 rtsp rtp word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 并查集 leetcode fstab udp 本地环回 bind jenkins gitee gnu 自动化 HarmonyOS Next json outlook 错误代码2603 无网络连接 2603 虚拟显示器 远程控制 聚类 spring cloud compose 持续部署 AI编程 xcode windows 服务器安装 Linux PID c# 课程设计 springsecurity6 oauth2 授权服务器 前后端分离 虚拟现实 qt QT 5.12.12 QT开发环境 Ubuntu18.04 JAVA Java EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 Ubuntu20.04 GLIBC 2.35 maven svn 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 ue4 着色器 ue5 虚幻 目标跟踪 目标检测 OpenVINO 推理应用 cron crontab日志 kylin 运维开发 思科 react.js 前端面试题 zotero WebDAV 同步失败 代理模式 git C 环境变量 进程地址空间 arm Doris搭建 docker搭建Doris Doris搭建过程 linux搭建Doris Doris搭建详细步骤 Doris部署 UEFI Legacy MBR GPT U盘安装操作系统 集成学习 集成测试 LDAP Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 数据分析 spring ollama llm 进程优先级 调度队列 进程切换 matplotlib fonts-noto-cjk Dify yum docker-compose docker compose AI pip opengl 快捷键 旋转屏幕 自动操作 多线程 C# MQTTS 双向认证 emqx github pygame 大数据 spark hive vnc transformer 数据结构 链表 uni-app Linux 维护模式 openvpn server openvpn配置教程 centos安装openvpn kafka 华为云 媒体 安卓模拟器 mac websocket 监控k8s集群 集群内prometheus pillow ffmpeg 音视频 视频编解码 ssh漏洞 ssh9.9p2 CVE-2025-23419 grub 版本升级 扩容 TCP WebServer devops elasticsearch perf AIGC 人工智能生成内容 cpu 内存 实时 使用 VMware AI大模型 大模型技术 本地部署大模型 dubbo 面试 ubuntu20.04 Linux 开机黑屏 软件工程 软件构建 java-ee 电脑 软件需求 开源 sql rust http 私有化 本地部署 编辑器 vim 实时音视频 实时互动 Trae IDE AI 原生集成开发环境 Trae AI ssl VMware安装mocOS macOS系统安装 驱动开发 硬件工程 嵌入式实习 多层架构 解耦 agi 深度求索 私域 html golang Cline 产品经理 microsoft 微信 微信分享 Image wxopensdk 浪潮信息 AI服务器 主从复制 etcd 数据安全 RBAC CPU 主板 电源 网卡 IIS .net core Hosting Bundle .NET Framework vs2022 嵌入式 linux驱动开发 报错 信息与通信 SRS 流媒体 直播 性能优化 dell服务器 华为od gateway Clion Nova ResharperC++引擎 Centos7 远程开发 gpu算力 MacMini Mac 迷你主机 mini Apple .netcore openEuler 区块链 visual studio code vite vue3 云电竞 云电脑 todesk bash elk AI-native Docker Desktop Alexnet TrueLicense 虚拟局域网 C语言 rust腐蚀 milvus maxkb ARG 前端框架 flask centos-root /dev/mapper yum clean all df -h / du -sh notepad jupyter shell 读写锁 vSphere vCenter 软件定义数据中心 sddc DeepSeek hadoop 腾讯云 3d 数学建模 DevEco Studio 计算生物学 生物信息学 生物信息 基因组 互信 大语言模型 LLMs audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 国产数据库 瀚高数据库 数据迁移 下载安装 jmeter 软件测试 stm32项目 stm32 unix ocr 1024程序员节 onlyoffice gitlab unity sublime text Cursor IM即时通讯 QQ 企业微信 剪切板对通 HTML FORMAT 硬件架构 系统架构 热榜 指令 进程信号 eclipse 远程连接 rdp 实验 Kali Linux 黑客 渗透测试 信息收集 离线部署dify numpy flutter Google pay Apple pay 程序人生 VMware安装Ubuntu Ubuntu安装k8s k8s ragflow filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 intellij-idea intellij idea lvm 磁盘挂载 磁盘分区 远程工作 创意 社区 ruoyi VMware创建虚拟机 webdav rpc vr list ip命令 新增网卡 新增IP 启动网卡 fork wait waitpid exit 串口服务器 大模型入门 大模型教程 tcp WSL2 Ubuntu22.04 虚拟化 开发人员主页 redhat python2 ubuntu24.04 上传视频文件到服务器 uniApp本地上传视频并预览 uniapp移动端h5网页 uniapp微信小程序上传视频 uniapp app端视频上传 uniapp uview组件库 ESP32 camera Arduino 电子信息 axure 富文本编辑器 WebUI DeepSeek V3 EtherCAT转Modbus ECT转Modbus协议 EtherCAT转485网关 ECT转Modbus串口网关 EtherCAT转485协议 ECT转Modbus网关 cursor rabbitmq iftop 网络流量监控 chatgpt oneapi efficientVIT YOLOv8替换主干网络 TOLOv8 Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 pdf iot Agent LLM CrewAI deepseek-r1 大模型本地部署 ros 机器人 话题通信 服务通信 linux内核 网络药理学 生信 gromacs 分子动力学模拟 MD 动力学模拟 MacOS录屏软件 Hyper-V WinRM TrustedHosts 华为认证 交换机 小程序 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK ecmascript nextjs react reactjs 命令 oracle fusion oracle中间件 database vmware 卡死 GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 rancher open webui remote-ssh burp suite 抓包 压测 ECS HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 升级 CVE-2024-7347 漏洞 缓存 powerpoint Dell R750XS GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 wsl odoo 服务器动作 Server action protobuf 序列化和反序列化 安装 nftables 防火墙 温湿度数据上传到服务器 Arduino HTTP oceanbase 传统数据库升级 银行 游戏程序 ios Flask FastAPI Waitress Gunicorn uWSGI Uvicorn 多线程服务器 Linux网络编程 postman 测试工具 灵办AI aws 工业4.0 程序 编程 性能分析 KingBase YOLOv8 NPU Atlas800 A300I pro asi_bench 客户端 博客 windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 clickhouse 政务 分布式系统 监控运维 Prometheus Grafana yaml Ultralytics 可视化 v10 镜像源 软件 nvm 沙盒 matlab 系统安全 postgresql mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 远程桌面 远程服务 大模型应用 智能手机 Ubuntu共享文件夹 共享目录 Linux共享文件夹 CPU 使用率 系统监控工具 linux 命令 tcpdump cuda Kylin-Server 国产操作系统 服务器安装 kali 共享文件夹 7z rocketmq retry 重试机制 express p2p 交互 seatunnel 腾讯云大模型知识引擎 Web服务器 多线程下载工具 网络编程 PYTHON 搜索引擎 frp dns SSH 密钥生成 SSH 公钥 私钥 生成 googlecloud Samba NAS selenium 网络爬虫 jdk Linux的基础指令 Python glibc top Linux top top命令详解 top命令重点 top常用参数 docker搭建pg docker搭建pgsql pg授权 postgresql使用 postgresql搭建 iperf3 带宽测试 云原生开发 K8S k8s管理系统 数据挖掘 zabbix mariadb 开发 docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos 宠物 毕业设计 免费学习 宠物领养 宠物平台 毕设 html5 firefox kamailio sip VoIP 大数据平台 DIFY YOLOv12 ubuntu24.04.1 HiCar CarLife+ CarPlay QT RK3588 linux环境变量 sqlserver AI写作 程序员创富 rc.local 开机自启 systemd 麒麟 burpsuite 安全工具 mac安全工具 burp安装教程 渗透工具 webrtc Termux 匿名管道 命名管道 Chatbox 客户端与服务端通信 Invalid Host allowedHosts vue 功能测试 自动化测试 firewalld ping++ IO Redis Desktop 隐藏文件 隐藏目录 文件系统 管理器 通配符 jar 低代码 samba tar 鲲鹏 昇腾 npu 远程 执行 sshpass 操作 Portainer搭建 Portainer使用 Portainer使用详解 Portainer详解 Portainer portainer MCP server C/S mongodb 计算机系统 rag ragflow 源码启动 sysctl.conf vm.nr_hugepages Reactor 设计模式 C++ vmamba 中间件 iis 移动云 智慧农业 开源鸿蒙 团队开发 安防软件 磁盘清理 cudnn anaconda 微服务 springcloud H3C android studio 显示器 网易邮箱大师 云服务 OpenManus 版本 个人博客 浏览器自动化 .net 微信小程序 miniapp 真机调试 调试 debug 断点 网络API请求调试方法 CLion ros1 Noetic 20.04 apt 安装 云桌面 微软 AD域控 证书服务器 系统 黑苹果 VMware Tools vmware tools安装 vmwaretools安装步骤 vmwaretools安装失败 vmware tool安装步骤 vm tools安装步骤 vm tools安装后不能拖 vmware tools安装步骤 visual studio CentOS c/c++ 串口 豆瓣 追剧助手 迅雷 ollama下载加速 unity3d ESXi 银河麒麟 kylin v10 麒麟 v10 DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 alias unalias 别名 王者荣耀 端口号 开放端口 访问列表 WSL2 上安装 Ubuntu 僵尸进程 软链接 硬链接 模拟实现 springboot ArkTs ArkUI 设置代理 实用教程 大模型部署 ux 代码调试 ipdb 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 vu大文件秒传跨域报错cors systemctl ci/cd composer GameFramework HybridCLR Unity编辑器扩展 自动化工具 权限 Mac内存不够用怎么办 HCIE 数通 string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap Hive环境搭建 hive3环境 Hive远程模式 threejs 3D okhttp 压力测试 DevOps 软件交付 数据驱动 应用场景 minicom 串口调试工具 飞牛nas fnos MQTT协议 消息服务器 代码 视觉检测 log4j flash-attention 源码剖析 rtsp实现步骤 流媒体开发 n8n 工作流 机柜 1U 2U Windsurf CosyVoice netty 远程看看 远程协助 qps 高并发 直播推流 sqlite3 5G 3GPP 卫星通信 harmonyOS面试题 ROS PX4 MAVROS 四旋翼无人机 mq chatbox Cookie su sudo GPU 工具 Linux的权限 ansible gradle 物联网开发 MQTT 正则表达式 r语言 云耀服务器 统信UOS bonding 链路聚合 KylinV10 麒麟操作系统 Vmware Docker Hub docker pull daemon.json 执法记录仪 智能安全帽 smarteye IPMI 大屏端 NFS 监控k8s 监控kubernetes shell脚本免交互 expect linux免交互 键盘 kind WLAN 嵌入式系统开发 ebpf uprobe curl wget 无人机 HP Anyware SSH Xterminal iBMC UltraISO windows日志 爬虫 数据集 信号 内核 zip unzip GCC aarch64 编译安装 HPC 基础环境 自动化运维 流水线 脚本式流水线 程序员 gpt 智能体 autogen openai coze 安装MySQL edge浏览器 rime 磁盘监控 内网渗透 靶机渗透 大模型微调 路径解析 go c MS Materials 进程程序替换 execl函数 execv函数 execvp函数 execvpe函数 putenv函数 file server http server web server linux上传下载 状态管理的 UDP 服务器 Arduino RTOS JDK LInux Windows nohup 异步执行 x64 SIGSEGV SSE xmm0 自动化编程 openwrt 镜像下载 freebsd RAGFLOW 硅基流动 ChatBox 回显服务器 UDP的API使用 ros2 moveit 机器人运动 图像处理 自动驾驶 armbian u-boot UOS 统信操作系统 bcompare Beyond Compare LLM Web APP Streamlit ai小智 语音助手 ai小智配网 ai小智教程 智能硬件 esp32语音助手 diy语音助手 selete mybatis WSL win11 无法解析服务器的名称或地址 copilot llama3 Chatglm 开源大模型 全文检索 图搜索算法 环境配置 通信工程 毕业 Mermaid 可视化图表 自动化生成 wireshark 显示过滤器 ICMP Wireshark安装 RustDesk自建服务器 rustdesk服务器 docker rustdesk linux安装配置 网络用户购物行为分析可视化平台 大数据毕业设计 webstorm mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 统信 高德地图 鸿蒙接入高德地图 HarmonyOS5.0 实战案例 迁移指南 SWAT 配置文件 服务管理 网络共享 vscode1.86 1.86版本 ssh远程连接 Docker Compose IIS服务器 IIS性能 日志监控 强制清理 强制删除 mac废纸篓 服务器扩容没有扩容成功 安卓 npm openstack Xen KVM Mac软件 中兴光猫 换光猫 网络桥接 自己换光猫 鸿蒙系统 高效I/O xml playbook 剧本 FTP 服务器 一切皆文件 nac 802.1 portal ipython 视频平台 录像 视频转发 性能测试 视频流 存储 宝塔面板无法访问 矩阵 TRAE fpga开发 VSCode 社交电子 RAID RAID技术 磁盘 web Socket 图形化界面 mamba 换源 国内源 Debian 技能大赛 Obsidian Dataview cmos 硬件 vscode 1.86 网站搭建 serv00 ceph 重启 排查 系统重启 日志 原因 服务器部署 本地拉取打包 直流充电桩 充电桩 微信开放平台 微信公众平台 微信公众号配置 AI代码编辑器 wsl2 远程登录 telnet 可信计算技术 安全威胁分析 蓝桥杯 lb 协议 trae python3.11 框架搭建 服务器时间 chfs ubuntu 16.04 具身智能 强化学习 显示管理器 lightdm gdm 显卡驱动 Webserver 异步 CNNs 图像分类 NAT转发 NAT Server 网络穿透 云服务器 交叉编译 pyicu Nuxt.js linuxdeployqt 打包部署程序 appimagetool gpt-3 文心一言 HarmonyOS NEXT 原生鸿蒙 lighttpd安装 Ubuntu配置 Windows安装 服务器优化 其他 相机 nacos kernel 弹性计算 裸金属服务器 弹性裸金属服务器 banner 安全架构 网络攻击模型 毕昇JDK 小智 致远OA OA服务器 服务器磁盘扩容 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 uv 容器技术 游戏机 序列化反序列化 jina IPv4 子网掩码 公网IP 私有IP 系统开发 binder 车载系统 framework 源码环境 Linux24.04 deepin openssl css 分析解读 rustdesk netlink libnl3 常用命令 文本命令 目录命令 金融 网卡的名称修改 eth0 ens33 边缘计算 线程 本地部署AI大模型 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 半虚拟化 硬件虚拟化 Hypervisor 能力提升 面试宝典 技术 IT信息化 Web应用服务器 服务器配置 行情服务器 股票交易 速度慢 切换 股票量化接口 股票API接口 Ardupilot micropython esp32 mqtt 大版本升 升级Ubuntu系统 Isaac Sim 虚拟仿真 ruby excel 工具分享 密码学 Office 业界资讯 端口测试 模拟退火算法 autoware 田俊楠 mosquitto 消息队列 双系统 GRUB引导 Linux技巧 dash sublime text3 教程 环境搭建 Maven 计算机 springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 小艺 Pura X pyside6 界面 umeditor粘贴word ueditor粘贴word ueditor复制word ueditor上传word图片 ueditor导入word ueditor导入pdf ueditor导入ppt AISphereButler hibernate CentOS Stream lio-sam SLAM NVML nvidia-smi UDP 联网 easyconnect 代理 代码托管服务 VR手套 数据手套 动捕手套 动捕数据手套 生活 FTP服务器 自学笔记 小米 澎湃OS Android Carla 智能驾驶 Java Applet URL操作 服务器建立 Socket编程 网络文件读取 多进程 Node-Red 编程工具 流编程 ukui 麒麟kylinos openeuler wps W5500 OLED u8g2 TCP服务器 论文阅读 rnn Pyppeteer Playwright fast 火绒安全 Xinference VPS workflow HarmonyOS ROS2 输入法 图片增强 增强数据 av1 电视盒子 机顶盒ROM 魔百盒刷机 asp.net大文件上传 asp.net大文件上传下载 asp.net大文件上传源码 ASP.NET断点续传 智能电视 minio 办公自动化 pdf教程 neo4j 数据仓库 数据库开发 数据库架构 OD机试真题 华为OD机试真题 服务器能耗统计 DeepSeek行业应用 Heroku 网站部署 big data opensearch helm arcgis WebRTC uniapp 恒源云 keepalived sonoma 自动更新 js 聊天服务器 套接字 智能音箱 智能家居 IPMITOOL BMC 硬件管理 rclone AList fnOS pyautogui chrome devtools chromedriver ShenTong 国产化 DBeaver 空间 查错 next.js 部署next.js 开机自启动 大模型训练/推理 推理问题 mindie 向日葵 ArcTS 登录 ArcUI GridItem arkUI asp.net上传文件夹 asp.net上传大文件 .net core断点续传 nvidia 繁忙 服务器繁忙 解决办法 替代网站 汇总推荐 AI推理 CDN token sas dba 实时内核 跨平台 kotlin iphone Vmamba bot Docker SSL 域名 skynet Kubernetes grep 代码规范 safari Linux权限 权限命令 特殊权限 服务器数据恢复 数据恢复 存储数据恢复 raid5数据恢复 磁盘阵列数据恢复 embedding DeepSeek r1 Open WebUI 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制 历史版本 下载 cd 目录切换 sudo原理 su切换 银河麒麟操作系统 etl wsgiref Web 服务器网关接口 OpenHarmony nfs 服务器部署ai模型 sdkman iTerm2 终端 NLP模型 NLP 单例模式 Kylin OS java-rocketmq 机架式服务器 1U工控机 国产工控机 Jellyfin 宝塔面板 同步 备份 建站 LORA apt langchain deep learning 图形渲染 ELF加载 Netty tensorflow 小游戏 五子棋 监控 yolov8 黑客技术 xpath定位元素 URL bat api pyqt 源代码 termux 跨域 内网环境 混合开发 环境安装 fd 文件描述符 linux 命令 sed 命令 华为OD 可以组成网络的服务器 RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 大文件秒传跨域报错cors cpp-httplib 网工 用户缓冲区 服务器主板 AI芯片 MI300x mysql离线安装 ubuntu22.04 mysql8.0 支付 微信支付 开放平台 CORS 源码 less 设备 PCI-Express SenseVoice SSH 服务 SSH Server OpenSSH Server 考研 在线office vasp安装 京东云 k8s部署 MySQL8.0 高可用集群(1主2从) AutoDL 命令行 基础入门 自动化任务管理 职场和发展 游戏服务器 TrinityCore 魔兽世界 prompt easyui webgl code-server SVN Server tortoise svn 群晖 文件分享 pppoe radius 推荐算法 蓝耘科技 元生代平台工作流 ComfyUI Docker快速入门 NPS 雨云服务器 雨云 stable diffusion cmake Ubuntu 22.04 MySql 算家云 算力租赁 db yum源切换 更换国内yum源 Kali 渗透 chrome 浏览器下载 chrome 下载安装 谷歌浏览器下载 桌面环境 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 ArkTS 移动端开发 echarts 信息可视化 网页设计 ftp服务 文件上传 玩游戏 RAGFlow saltstack 相差8小时 UTC 时间 Qualcomm WoS QNN AppBuilder web3.py 远程过程调用 Windows环境 diskgenius 华为证书 HarmonyOS认证 华为证书考试 飞牛NAS 飞牛OS MacBook Pro DocFlow 虚拟机安装 cnn trea idea 邮件APP 免费软件 三级等保 服务器审计日志备份 ubuntu24 vivado24 数据可视化 Ubuntu Server Ubuntu 22.04.5 免费域名 域名解析 risc-v docker命令大全 EMQX 通信协议 openjdk React Next.js 开源框架 联想开天P90Z装win10 加解密 Yakit yaklang beautifulsoup bootstrap ShapeFile GeoJSON Nginx 影刀 #影刀RPA# 小番茄C盘清理 便捷易用C盘清理工具 小番茄C盘清理的优势尽显何处? 教你深度体验小番茄C盘清理 C盘变红?!不知所措? C盘瘦身后电脑会发生什么变化? RTMP 应用层 宕机切换 服务器宕机 宝塔 终端工具 远程工具 数码 联机 僵尸毁灭工程 游戏联机 开服 make命令 makefile文件 idm 代理服务器 MobaXterm 文件传输 adobe 实习 es6 qt6.3 g726 大模型面经 大模型学习 Minecraft opcua opcda KEPServer安装 AnythingLLM AnythingLLM安装 DOIT 四博智联 Xshell 数据管理 数据治理 数据编织 数据虚拟化 rpa iDRAC R720xd 域名服务 DHCP 符号链接 配置 ufw 镜像 powerbi 图文教程 VMware虚拟机 macOS系统安装教程 macOS最新版 虚拟机安装macOS Sequoia Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 AI作画 树莓派 VNC kvm qemu libvirt AD域 thingsboard WebVM 测试用例 金仓数据库 2025 征文 数据库平替用金仓 conda配置 conda镜像源 CUPS 打印机 Qt5 can 线程池 rsyslog 考试 日志分析 系统取证 scapy 多产物 自定义客户端 SAS Reactor反应堆 电视剧收视率分析与可视化平台 XFS xfs文件系统损坏 I_O error visualstudio FunASR ASR virtualbox VPN wireguard Attention 网络文件系统 X11 Xming 信号处理 稳定性 看门狗 Typore ssh远程登录 Spring Security EasyConnect AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 AimRT k8s集群资源管理 流式接口 RAG 检索增强生成 文档解析 大模型垂直应用 export env 变量 docker run 数据卷挂载 交互模式 Wi-Fi Tabs组件 TabContent TabBar TabsController 导航页签栏 滚动导航栏 医疗APP开发 app开发 软件卸载 系统清理 SysBench 基准测试 蓝牙 模拟器 scikit-learn css3 ssrf 失效的访问控制 MNN Qwen SSL证书 ftp Linux环境 深度优先 图论 并集查找 换根法 树上倍增 语音识别 dity make eNSP 网络规划 VLAN 企业网络 xrdp Dell HPE 联想 浪潮 k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm Linux Vim 即时通信 NIO searxng Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 gaussdb import save load 迁移镜像 llama.cpp ajax 环境迁移 .net mvc断点续传 cfssl 拓扑图 产测工具框架 IMX6ULL 管理框架 思科模拟器 Cisco nuxt3 firewall 服务器管理 配置教程 网站管理 gitea muduo P2P HDLC ollama api ollama外网访问 Ark-TS语言 飞牛 HistoryServer Spark YARN jobhistory Linux find grep 健康医疗 互联网医院 银河麒麟服务器操作系统 系统激活 单元测试 算力 NVIDIA 项目部署 Qwen2.5-VL vllm VS Code dock 加速 crosstool-ng 小智AI服务端 xiaozhi TTS proxy模式 AD 域管理 nosql SEO ecm bpm deepseak 豆包 KIMI 腾讯元宝 web3 区块链项目 Apache Beam 批流统一 案例展示 数据分区 容错机制 rsync OpenSSH deekseek 星河版 vsxsrv 7-zip 我的世界服务器搭建 minecraft 软考设计师 中级设计师 SQL 软件设计师 知识图谱 HTTP 服务器控制 ESP32 DeepSeek 备选 网站 调用 示例 Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 查询数据库服务IP地址 SQL Server seleium EVE-NG comfyui comfyui教程 计算机科学与技术 底层实现 卸载 列表 弹性服务器 子系统 智能合约 哈希算法 sqlite pgpool react native 对比 meld DiffMerge 达梦 DM8 电路仿真 multisim 硬件工程师 硬件工程师学习 电路图 电路分析 仪器仪表 csrf 银河麒麟高级服务器 外接硬盘 Kylin UOS1070e flink initramfs Linux内核 Grub WINCC GRE 软负载 服务器ssl异常解决 新盘添加 partedUtil 动静态库 Alist mount 挂载 网盘 端口 查看 ss 超融合 deployment daemonset statefulset cronjob 内网服务器 内网代理 内网通信 xshell termius iterm2 数据采集 Crawlee g++ g++13 ranger MySQL8.0 银河麒麟桌面操作系统 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 飞书 视频监控 反向代理 冯诺依曼体系 shard word IMX317 MIPI H265 VCU nohup后台启动 运维监控 可用性测试 服务网格 istio k8s二次开发 集群管理 XCC Lenovo 钉钉 合成模型 扩散模型 图像生成 mysql安装报错 windows拒绝安装 进程间通信 分子对接 autodock mgltools PDB PubChem Trae叒更新了? 端口聚合 windows11 音乐服务器 Navidrome 音流 Headless Linux Echarts图表 折线图 柱状图 异步动态数据 鸿蒙开发 可视化效果 System V共享内存 进程通信 Anolis nginx安装 linux插件下载 移动开发 根服务器 语法 zookeeper 抓包工具 junit qt5 客户端开发 网络建设与运维 分布式账本 信任链 共识算法 ai工具 阿里云ECS 捆绑 链接 谷歌浏览器 youtube google gmail ldap MAC SecureCRT docker desktop image lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 h.264 prometheus数据采集 prometheus数据模型 prometheus特点 需求分析 规格说明书 支持向量机 vpn 项目部署到linux服务器 项目部署过程 iNode Macos 机械臂 架构与原理 安全漏洞 信息安全 IO模型 pthread open Euler dde 阻塞队列 生产者消费者模型 服务器崩坏原因 regedit 开机启动 kubeless gunicorn post.io 企业邮箱 搭建邮箱 MDK 嵌入式开发工具 论文笔记 管道 显卡驱动持久化 GPU持久化 docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 MacOS xfce perl 企业网络规划 华为eNSP cocoapods zerotier Unity Dedicated Server Host Client 无头主机 Qwen3 qwen3 32b chromium dpi SystemV 大模型压力测试 EvalScope 锁屏不生效 GeneCards OMIM TTD 玩机技巧 软件分享 软件图标 wpf 崖山数据库 YashanDB client-go midjourney 信创 信创终端 中科方德 商用密码产品体系 sentinel 搭建个人相关服务器 佛山戴尔服务器维修 佛山三水服务器维修 swoole wordpress 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 laravel 无桌面 做raid 装系统 Radius 移动魔百盒 VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 macbook IP配置 netplan glm4 智能体开发 干货分享 黑客工具 密码爆破 tailscale derp derper 中转 autodl webpack triton 模型分析 线性代数 电商平台 docker部署Python 流量运营 Helm k8s集群 deepseek r1 Python基础 Python教程 Python技巧 IDEA 孤岛惊魂4 录音麦克风权限判断检测 录音功能 录音文件mp3播放 小程序实现录音及播放功能 RecorderManager 解决录音报错播放没声音问题 docker search 欧标 OCPP 软件开发 IP 地址 lua 单一职责原则 llamafactory 微调 vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 csrutil mac恢复模式进入方法 SIP 恢复模式 Claude java-zookeeper IMM LVM lvresize 磁盘扩容 pvcreate ECT转485串口服务器 ECT转Modbus485协议 ECT转Modbus串口服务器 音乐库 DenseNet 源代码管理 figma 笔灵AI AI工具 增强现实 沉浸式体验 技术实现 案例分析 AR navicat GoogLeNet mac设置host iventoy VmWare OpenEuler IPv4/IPv6双栈 双栈技术 网路规划设计 ensp综合实验 IPv4过渡IPv6 IPv4与IPv6 开源软件 僵尸世界大战 游戏服务器搭建 es 虚幻引擎 GIS 遥感 WebGIS 蓝桥杯C++组 c/s 问题解决 用户管理 uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 USB网络共享 多个客户端访问 IO多路复用 TCP相关API 技术共享 我的世界 我的世界联机 Charles NVM Node Yarn PM2 查看显卡进程 fuser 打不开xxx软件 无法检查其是否包含恶意软件 ArtTS 浏览器开发 AI浏览器 arkTs Sealos whistle 烟花代码 烟花 元旦 jetty undertow 性能调优 安全代理 本地知识库部署 DeepSeek R1 模型 ISO镜像作为本地源 游戏开发 负载测试 网页服务器 web服务器 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 免密 公钥 私钥 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 网络搭建 神州数码 神州数码云平台 云平台 Erlang OTP gen_server 热代码交换 事务语义 Ubuntu 24.04.1 轻量级服务器 材料工程 win服务器架设 windows server hugo 嵌入式Linux IPC PPI String Cytoscape CytoHubba EMUI 回退 降级 AI员工 GPU训练 dns是什么 如何设置电脑dns dns应该如何设置 kerberos Zoertier 内网组网 分布式训练 TCP协议 聊天室 抗锯齿 postgres Dify重启后重新初始化 localhost 多端开发 智慧分发 应用生态 鸿蒙OS 券商 股票交易接口api 类型 特点 多路转接 AI agent 裸机装机 linux磁盘分区 裸机安装linux 裸机安装ubuntu 裸机安装kali 裸机 可执行程序 qt项目 qt项目实战 qt教程 国标28181 监控接入 语音广播 流程 SDP hosts 状态模式 授时服务 北斗授时 Logstash 日志采集 watchtower MVS 海康威视相机 物理地址 页表 虚拟地址 计算虚拟化 弹性裸金属 hexo 桌面快捷方式 PyQt PySide6 风扇控制软件 搜狗输入法 中文输入法 飞腾处理器 solidworks安装 ubuntu安装 linux入门小白 Bug解决 Qt platform OpenCV ABAP authing 配置原理 存储维护 NetApp存储 EMC存储 Masshunter 质谱采集分析软件 使用教程 科研软件 海康 大模型推理 AI Agent 字节智能运维 servlet 网络原理 创业创新 北亚数据恢复 oracle数据恢复 动态规划 西门子PLC 通讯 WireGuard 异地组网 resolv.conf 程序化交易 量化交易 高频交易 Python 视频爬取教程 Python 视频爬取 Python 视频教程 hdc Qt QModbus mcp服务器 client close AzureDataStudio HAProxy RDP mybase 大大通 第三代半导体 碳化硅 sequoiaDB yashandb ardunio BLE paddle 接口优化 Claude Desktop Claude MCP Windows Cli MCP 内存管理 文件存储服务器组件 IPv6 IPv6测试 IPv6测速 IPv6检测 IPv6查询 NFC 近场通讯 智能门锁 tidb 解决方案 设备树 静态IP 火山引擎 OpenCore 框架 linux子系统 忘记密码 RK3568 rtc 李心怡 Putty 花生壳 充电桩平台 充电桩开源平台 C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 fiddler VGG网络 卷积层 池化层 qwen2vl yum换源 mvc 电脑桌面出现linux图标 电脑桌面linux图标删除不了 电脑桌面Liunx图标删不掉 linux图标删不掉 极限编程 免费 solr Unity插件 数字证书 签署证书 easyTier 组网 粘包问题 brew 线程同步 线程互斥 条件变量 node 华为机试 带外管理 计算机学习路线 编程语言选择 massa sui aptos sei UFW 桥接模式 windows虚拟机 虚拟机联网 pycharm安装 服务器正确解析请求体 macOS xss 输入系统 mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 金仓数据库概述 金仓数据库的产品优化提案 photoshop finebi 元服务 应用上架 element-ui 上传视频并预览视频 vue上传本地视频及进度条功能 vue2选择视频上传到服务器 upload上传视频组件插件 批量上传视频 限制单个上传视频 STL lrzsz 三次握手 vm webview 知行EDI 电子数据交换 知行之桥 EDI AWS 华为昇腾910b3 CAD瓦片化 栅格瓦片 矢量瓦片 Web可视化 DWG解析 金字塔模型 gru d3d12 logstash Linux的基础开发工具 百度云 矩池云 数据下载 数据传输 MinIO pythonai PlaywrightMCP 华为鸿蒙系统 ArkTS语言 Component 生命周期 条件渲染 Image图片组件 鸿蒙NEXT 向量数据库 安装部署 milvus安装 命令模式 archlinux kde plasma 设计规范 caddy vCenter服务器 ESXi主机 监控与管理 故障排除 日志记录 零售 VM虚拟机 脚本 跨域请求 Maxkb RAG技术 本地知识库 homeassistant DrissionPage win向maOS迁移数据 android-studio 网络库 hosts文件管理工具 ACL 流量控制 基本ACL 规则配置 web开发 cpolar #STC8 #STM32 deepseek-v3 ktransformers 欧拉系统 finalsheel cocos2d 3dcoat PTrade QMT 量化股票 anythingllm open-webui docker国内镜像 Github加速 Mac上Github加速 Chrome浏览器插件 bigdata qtcreator 数据库管理 进程池实现 匿名FTP 邮件传输代理 SSL支持 chroot监狱技术 GRANT REVOKE OS 接口返回 aac Metastore Catalog