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

【Linux系统编程】System V

目录

共享内存

共享内存的原理

共享内存的接口

创建共享内存

释放共享内存

挂接共享内存

去关联

通信

共享内存的特点

使用命名管道保护共享内存

消息队列

消息队列的概述

消息队列的相关接口

信号量

补充概念

信号量的概念

信号量的接口

System V的IPC原理

应用角度,看IPC属性

内核角度,看IPC结构

再谈共享内存


System V这套通信机制相对于管道来说,并没有那么重要,使用的也比较少。因为这套机制属于操作系统单独定制的一套机制,它管理内存的机制与文件系统是独立开的,虽然有点像,但是与文件描述符等是完全没有关系的,而后面网络通信是基于文件的,所以并没有很重要。System V一共由3个核心组件组成:共享内存、消息队列、信号量。在这篇文章中,重点介绍共享内存,对于消息队列和信号量,只是了解。

共享内存

共享内存的原理

之前学的管道是基于文件的,内核的设计者在使用管道进行进程间通信时,并不需要增加太多的代码。而SystemV不同,因为它属于OS单独设计的同一套通信方案。进程具有独立性。进程=内核数据结构+代码和数据,这两者都是独立的,所以进程就是独立的。

所以,共享内存就是在物理内存中开辟一块空间,然后将其挂接到进程地址空间的共享区中即可。这种通信方法同样满足进程间通信的本质:让不同的进程看到同一份资源。

我们将这种让两个进程,通过各自的地址空间映射到同一块物理内存的技术,称为共享内存。若我们不想要这块区域了,A、B进程都需要将自己与共享内存的关联关系去掉(就是将页表的映射关系去掉),这一步称为去关联,当共享内存没有与进程关联时释放掉这块内存即可。

操作系统内部会有很多的进程,若这些进程两两通信,就会需要非常多的共享内存,既然有这么多的共享内存,操作系统就需要对这些共享内存进行管理。所以,共享内存 = 共享内存的内核数据结构 + 内存块。并且共享内存还是在硬件上创建的,所以操作系统还需要给我们提供创建共享内存的接口。

共享内存的接口

同样是需要两个没有任何关系的进程,当然,父子进程也是可以的。共享内存是OS创建的,进程只是调用了接口,一定是一个进程创建并使用共享内存,一个进程获取并使用共享内存,让Server创建。

创建共享内存

shmget是创建共享内存的接口。size表示的是创建的共享内存的大小,单位是字节。shmflg是标记位,控制共享内存的创建和访问权限,有多个选项,这里看两个比较重要的IPC_CREAT、IPC_EXEC。这两个都是宏,与open是一样的,使用位图传参。

  • IPC_CREAT:如果共享内存不存在,创建;如果存在,获取它,并返回
  • IPC_EXEC:单独使用没有意义
  • IPC_CREAT | IPC_EXEC:如果共享内存不存在,创建;如果存在,出错返回

shmget的返回值:若创建成功,返回一个非负整数,即共享内存标识符;若创建失败,返回-1,并设置errno。 

如何知道共享内存存不存在呢?这说明共享内存对应的内核数据结构中,一定有标识唯一性的标识符 --- key。
如果存在,获取它,并返回是什么意思?两个进程要通信,就需要让一个进程先创建出共享内存,然后另外一个进程去获取它。所以,单独使用IPC_CREAT就需要保证调用进程能拿到共享内存
如果存在,就出错返回是什么意思呢?这句话的意思就是只要成功,就是新的共享内存
所以,单独使用IPC_CREAT的进程就是获取共享内存的,而两个一起使用的就是创建共享内存的。

参数中的key就是标识共享内存唯一性的标识符,是需要用户自己传入的。为什么要让用户自己传入呢?

如果这个key是由操作系统生成的,A进程创建了一个共享内存后,拿到了这个共享内存的key,要怎么将这个key传给进程B呢?B是拿不到的。所以,key不应该由操作系统提供,若由操作系统提供就无法保证不同进程看到同一块资源了。对于key,我们只需要保证唯一性,并不会去修改。在上面命名管道实现两个进程间通信时,我们只需要我们只需要定义一个两个进程都能看到的路径,并在这个路径下创建,即可让两个进程看到同一份资源。现在,我们也可以使用相同的方法,在一个头文件中设置一个const int类型的key,然后让A进程创建共享空间时将这个key设置进内核中,B进程就能够看到这一个共享空间了。

注意:对于一个共享内存,key和创建成功时返回的共享内存标识符都是具有唯一性的

key要如何设置呢?是可以随意设置的,当传入一个值,若是唯一的,则创建成功,若创建失败,则需要程序员手动修改传入的key。为了减少发生冲突的概率,过这个key是需要用户设置,但不需要用户生成

glibc中提供了一个函数ftok,随便填入一个路径,和一个数值,根据算法将路径和数值弄成一个唯一值,并将这个唯一值作为key返回。对于这个路径,不能随便填,需要保证这个路径下的文件存在且可访问,这个值称为项目ID。但是,即使是这样,也是可能会有冲突的,只是概率较低,若冲突了,修改一下路径或者项目ID即可

Linux是如何在应用层面,保证不同进程看到同一份资源的?

  • System V:路径 + 项目ID
  • 命名管道:文件路径
  • 匿名管道:文件描述符

我们来使用一下上面的接口,创建出一个共享内存

Comm.hpp

#pragma once#include<iostream>
#include<string>
#include<sys/ipc.h>const std::string gpath = "/home/cxf/2025/test_6_7";
int gprojId = 0x6666;

Client.cc、Server.cc

#include"Comm.hpp"int main()
{key_t k = ::ftok(gpath.c_str(), gprojId);std::cout << "k: " << k << std::endl;return 0;
}


可以看到,只要路径一样,项目ID一样,两个进程拿到key值就是一样的。可以看到,这个数字会比较大,所以我们将其转换成十六进制。

Comm.hpp

#pragma once#include<iostream>
#include<string>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>const std::string gpath = "/home/cxf/2025/test_6_7";
int gprojId = 0x6666;std::string ToHex(key_t k)
{char buffer[64];snprintf(buffer, sizeof(buffer), "0x%x", k);return buffer;
}

Client.cc、Server.cc

#include"Comm.hpp"int main()
{key_t k = ::ftok(gpath.c_str(), gprojId);std::cout << "k: " << ToHex(k) << std::endl;return 0;
}


这里只是为了方便看,未来真正使用还是直接使用十进制的

我们让Server来创建共享内存。虽然这里说的是Server创建的,但是真正是由操作系统创建的,只是由Server来调用这个函数而已。

在Comm.hpp中定义一个全局变量,表示共享内存的大小

int gshmsize = 4096; // 共享内存的大小

Server.cc

#include"Comm.hpp"int main()
{// 创建keykey_t k = ::ftok(gpath.c_str(), gprojId);if(k < 0){std::cerr << "ftok error" << std::endl;return 1;}std::cout << "k: " << ToHex(k) << std::endl;// 创建共享内存并获取int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL);if(shmid < 0){std::cerr << "shmget error" << std::endl;return 2;}std::cout << "shmid: " << shmid << std::endl;return 0;
}


可以看到,共享内存标识符是0,再创建一次会失败,第二次往后创建都会失败。./server后,打印出shmid:0时,这个进程已经运行完了。我们前面说过malloc或者new出来的堆空间,若我们没有手动释放,进程结束时也是会被自动释放的,这是因为申请的堆空间是在进程地址空间上的,系统判定地址空间上的内容是属于用户的,而现在用户退出了,进程都没了,堆空间也就不需要存在了,所以会被释放掉。而共享内存有点不同,OS并不认为共享内存是进程创建的,OS认为这是OS创建的,虽然堆空间也是OS创建的,但是OS认为的是,堆空间是与进程有关的,而共享空间是与进程无关的,所以,堆空间在进程退出时,是会被释放的,但是共享空间并不会。结论:共享内存的生命周期随内核;文件的生命周期随进程。想要释放共享内存,可以手动释放或重启OS

释放共享内存

用户主动让OS释放共享内存有两种做法:1.使用指令 2.使用代码

使用指令


指令ipcs直接回车,会将消息队列、共享内存、信号量都打印出来


可以使用-m来只查看共享内存。这里是Key就是上面的key,shmid是共享内存标识符

指令ipcrm是用来删除共享内存的

在使用指令删除共享内存时,不能使用key,而需要使用shmid,虽然这两者均有唯一性。

此时再运行就可以成功创建共享内存了

shmid vs key

  • shmid:只给用户使用的一个标识共享内存的标识符
  • key:只作为内核中,区分共享内存唯一性的标识符,不作为用户管理共享内存的id值

shmid就有点像fd和FILE*,而key有点像文件描述符表中,struct file的地址。指令也是用户层,因为指令最终也会变成进程。所以,我们只有在创建共享内存时会使用key,往后对共享内存的各种操作都使用shmid。会发现,我们第一次创建共享内存,shmid是0,下一次就是1,再下一次就是2,实际上,shmid也是数组下标,但是与文件描述符不是一个东西,因为文件描述符的0,1,2是被占用的。

使用系统调用

使用系统调用shmctl对共享内存进行删、改、查

在OS内核中,会有一个结构体来保存共享内存的属性

ipcs指令就是查一下这个结构体。

cmd表示的是要对这个共享内存进行什么操作

所以,我们可以使用shmctl(shmid, IPC_RMID, nullptr) 来删除一个共享内存

挂接共享内存

我们前面是创建出了共享内存,是在物理内存上开辟了一块空间,此时与我们进程的进程地址空间是没有关系的,所以我们还需要将共享内存挂接到自己的地址空间中,其实就是在地址空间的共享区开辟一块空间,然后通过页表映射到物理内存。使用系统调用shmat。

shmaddr:用户指定挂接到什么虚拟地址,可以填入NULL,系统自动分配合适的虚拟地址
shmflg:控制附加行为的标志位。

常用标志:

  • SHM_RDONLY:以只读模式附加(默认读写)
  • SHM_REMAP:若指定shmaddr且地址已占用,重新映射(Linux特有)
  • 0:默认读写模式,无特殊行为

若调用成功,返回值是共享内存映射到地址空间中虚拟地址的起始地址,并且大小是我们申请的,此时就可以正常使用了,这与malloc是类似的,返回值都是void*;若调用失败,返回(void*)-1,并设置错误码。

我们来使用一下shmat

#include"Comm.hpp"int main()
{// 1. 创建keykey_t k = ::ftok(gpath.c_str(), gprojId);if(k < 0){std::cerr << "ftok error" << std::endl;return 1;}std::cout << "k: " << ToHex(k) << std::endl;// 2. 创建共享内存并获取int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL);if(shmid < 0){std::cerr << "shmget error" << std::endl;return 2;}sleep(5);std::cout << "shmid: " << shmid << std::endl;// 3. 将共享内存挂接到进程地址空间中shmat(shmid, nullptr, 0);std::cout << "attach done" << std::endl;sleep(5);// n. 删除共享内存shmctl(shmid, IPC_RMID, nullptr);std::cout << "delete shm done" << std::endl;sleep(5);return 0;
}

在上面的ipcs -m中,nattch表示的是这个共享内存与几个进程相关联

while :; do ipcs -m; sleep 1; done

在命令行中输入上面的指令,让其不断地答疑ipcs -m的结果,然后将上面的程序运行起来

会发现直到共享内存被删除,都没有一个进程与共享内存相关联

我们将shmat的返回值打印出来看看

// 3. 将共享内存挂接到进程地址空间中
void* ret = shmat(shmid, nullptr, 0);
std::cout << "attach done" << (int)ret << std::endl;
sleep(5);

此时编译是会报错的。因为现在使用的Linux系统是64位的,指针是8字节,而int是4字节,强转会有精度损失,所以报错。所以,我们使用long long

// 3. 将共享内存挂接到进程地址空间中
void* ret = shmat(shmid, nullptr, 0);
std::cout << "attach done: " << (long long)ret << std::endl;
sleep(5);



可以看到,返回值是-1,也就是说,调用是不成功的。为什么会调用不成功呢?
上面的perms是共享内存的权限。所以,共享内存也是有权限的。在Comm.hpp中定义一个权限

mode_t gmode = 0600; // 共享内存的权限

在创建共享内存时,是可以指定共享内存的权限的

// 2. 创建共享内存并获取
int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode);

创建共享内存

挂接后

删除共享内存

进程退出后

只要是一个用户创建的共享内存,这个用户一定可以挂接成功,挂接失败就是因为没有设置好权限

去关联

我们除了要将共享内存挂接到进程,还要去关联,去关联的本质就是解除虚拟地址和物理地址通过页表与共享内存的关联,并释放掉虚拟地址的vm_area_struct。使用shmdt进行去关联。

shmdt的参数就是shamt的返回值,也就是这个共享内存在虚拟地址的起始地址,并且大小也是可以拿到的,此时就能够去关联了。

shmdt的返回值:0表示成功;-1表示失败,失败时同时设置errno

// 3. 将共享内存挂接到进程地址空间中
void* ret = shmat(shmid, nullptr, 0);
std::cout << "attach done: " << (long long)ret << std::endl;
sleep(5);
::shmdt(ret);
std::cout << "detach done" << std::endl;

shmdt执行后,nattch从1变成了0。并且,去关联后,再删除共享内存就相当于上面进程退出的效果了,也就是此时不会出现上面删除共享内存的效果。上面是因为虽然调用了shmctl删除共享内存,但是此时仍然有进程与共享内存关联,所以是不会直接删除的。所以,我们在使用完共享内存之后,应该先调用shmdt去关联,然后再调用shmctl删除共享内存。

通信

上面我们只是建立出了共享内存,并没有通信,若要通信,应该在shmat和shmdt之间

再介绍一下共享内存字段中的bytes,OS在申请空间时,是以块为单位的,一般是4KB

我们将我们申请的共享内存的大小设置位4097

为什么是4097呢?实际上,在0S内部会申请4096*2的空间,但是只允许这个用户使用4097的空间。此时是会造成空间浪费的。
为什么要这样呢?因为OS不假设用户的所有行为,OS该怎么做就怎么做。所以,共享内存在申请内存时,尽量申请4096的整数倍。

现在要进行进程间通信会发现客户端和服务器大部分代码是一样的,客户端要将创建共享内存变成获取共享内存,并且最后不删除共享内存,只有这两点差别。所以,将Comm.hpp改成ShareMemory.hpp并将上面的接口封装成类

class ShareMemory
{
public:ShareMemory() {}~ShareMemory() {}int CreateShm() // 创建共享内存{}int GetShm() // 获取共享内存{}void* AttachShm() // 挂接共享内存{}void DetachShm() // 去关联{}void DeleteShm() // 删除共享内存{}void ShmMeta() // 打印共享内存的属性{}
private:
};
int CreateShm() // 创建共享内存
{key_t k = ::ftok(gpath.c_str(), gprojId);if (k < 0){std::cerr << "ftok error" << std::endl;return -1;}int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode);if (shmid < 0){std::cerr << "shmget error" << std::endl;return -2;}std::cout << "shmid: " << shmid << std::endl;return shmid;
}
int GetShm() // 获取共享内存
{key_t k = ::ftok(gpath.c_str(), gprojId);if (k < 0){std::cerr << "ftok error" << std::endl;return -1;}int shmid = ::shmget(k, gshmsize, IPC_CREAT);if (shmid < 0){std::cerr << "shmget error" << std::endl;return -2;}std::cout << "shmid: " << shmid << std::endl;return shmid;
}

这两个函数的重复度太高了

class ShareMemory
{
private:int CreateShmHelper(int shmflg) // 创建/获取共享内存{key_t k = ::ftok(gpath.c_str(), gprojId);if (k < 0){std::cerr << "ftok error" << std::endl;return -1;}int shmid = ::shmget(k, gshmsize, shmflg);if (shmid < 0){std::cerr << "shmget error" << std::endl;return -2;}std::cout << "shmid: " << shmid << std::endl;return shmid;}
public:ShareMemory() {}~ShareMemory() {}int CreateShm(){return CreateShmHelper(IPC_CREAT | IPC_EXCL | gmode);}int GetShm(){return CreateShmHelper(IPC_CREAT);}void* AttachShm(int shmid) // 挂接共享内存{void* ret = shmat(shmid, nullptr, 0);if((long long)ret == 1){return nullptr;}return ret;}void DetachShm(void* ret) // 去关联{::shmdt(ret);std::cout << "detach done" << std::endl;}void DeleteShm(int shmid) // 删除共享内存{shmctl(shmid, IPC_RMID, nullptr);}void ShmMeta() // 打印共享内存的属性{}
private:
};ShareMemory shm;

为了让服务端和客户端都能够看到,可以在ShareMemory.hpp中定义一个全局对象

在通信时,让服务端创建共享内存,最后还要删除共享内存,客户端只需获取共享内存即可 

Server.cc

#include"ShareMemory.hpp"int main()
{int shmid = shm.CreateShm();void* addr = shm.AttachShm(shmid);sleep(5);std::cout << "client attch done" << std::endl;shm.DetachShm(addr);std::cout << "client detact done" << std::endl;sleep(5);shm.DeleteShm(shmid);std::cout << "delete shm" << std::endl;return 0;
}

Client.cc

#include"ShareMemory.hpp"int main()
{int shmid = shm.GetShm();void* addr = shm.AttachShm(shmid);sleep(5);std::cout << "client attach done" << std::endl;// addr -> 写入shm.DetachShm(addr);std::cout << "client datact done" << std::endl;sleep(5);return 0;
}


在这里,我们应该让server先运行,因为它需要创建共享内存,client只是获取。然后就可以看到,这个共享内存被两个进程挂接了。

可以将shmid等定义为成员变量

class ShareMemory
{
private:int CreateShmHelper(int shmflg) // 创建/获取共享内存{_key = ::ftok(gpath.c_str(), gprojId);if (_key < 0){std::cerr << "ftok error" << std::endl;return -1;}_shmid = ::shmget(_key, gshmsize, shmflg);if (_shmid < 0){std::cerr << "shmget error" << std::endl;return -2;}std::cout << "shmid: " << _shmid << std::endl;return _shmid;}
public:ShareMemory():_shmid(-1), _key(0), _addr(nullptr){}~ShareMemory() {}int CreateShm(){return CreateShmHelper(IPC_CREAT | IPC_EXCL | gmode);}int GetShm(){return CreateShmHelper(IPC_CREAT);}void* AttachShm() // 挂接共享内存{_addr = shmat(_shmid, nullptr, 0);if((long long)_addr == 1){return nullptr;}return _addr;}void DetachShm() // 去关联{if(_addr != nullptr)::shmdt(_addr);std::cout << "detach done" << std::endl;}void DeleteShm() // 删除共享内存{shmctl(_shmid, IPC_RMID, nullptr);}void* GetAddr(){return _addr;}void ShmMeta() // 打印共享内存的属性{}
private:int _shmid;key_t _key;void* _addr;
};ShareMemory shm;

但是定义成成员变量后会有一个问题,当不同进程看到同一个全局变量时,并且这个全局变量是类对象,使用这个类里面的方法是互不影响的,因为方法是只读的,但是属性是会发生写时拷贝的。也就是说,服务端和客户端看到的全局变量shm中,属性是不一样的(发生写时拷贝之后)。

为了进行通信,还在里面加了一个获取共享内存虚拟起始地址的接口

Server.cc

int main()
{shm.CreateShm();shm.AttachShm();// 在这里进行IPCchar* strinfo = (char*)shm.GetAddr();//std::cout << "server 虚拟地址: " << strinfo << std::endl;printf("server 虚拟地址:%p\n", strinfo);// 这里必须让server休眠久一点,防止共享内存被释放了sleep(10);shm.DetachShm();shm.DeleteShm();return 0;
}

Client.cc

int main()
{shm.GetShm();shm.AttachShm();// 在这里进行IPCchar* strinfo = (char*)shm.GetAddr();// std::cout << "client 虚拟地址: " << strinfo << std::endl;printf("client 虚拟地址:%p\n", strinfo);shm.DetachShm();return 0;
}


此时,这两个进程都能够拿到共享内存映射到自己的进程地址空间的虚拟地址,并且两个进程中,所映射到的虚拟地址是不一样的。

接下来我们就进行通信了,让客户端写入,服务端读取

Client.cc

int main()
{shm.GetShm();shm.AttachShm();// 在这里进行IPCchar* strinfo = (char*)shm.GetAddr();char ch = 'A';while(ch <= 'Z'){sleep(1);strinfo[ch - 'A'] = ch;ch ++;}shm.DetachShm();return 0;
}

将共享内存当成一个数组,每隔1秒向其中写入一个字符,从下标为0的位置开始写

Server.cc

int main()
{shm.CreateShm();shm.AttachShm();// 在这里进行IPCchar* strinfo = (char*)shm.GetAddr();while(true){printf("%s\n", strinfo);sleep(1);}shm.DetachShm();shm.DeleteShm();return 0;
}

将共享内存中的数据当成字符串打印出来

此时就成功完成了两个进程之间的通信

共享内存是OS申请的一块空间,为什么我们使用共享内存时,没有使用系统调用呢?

可以看到,进程地址空间是分为用户空间和内核空间的。对于用户空间,我们是可以直接使用的,不需要使用系统调用来访问这块空间,而内核空间就需要使用系统调用。我们是将共享内存挂接到了共享区当中,而共享区是位于用户空间的,所以我们是可以直接使用的,不需要使用系统调用。就像malloc出来的空间,在堆区,是可以直接使用的。前面的管道是需要使用系统调用的,因为是向内核级缓冲区写入,这块区域不属于用户空间。

共享内存的特点

1. 是所有进程间通信方式中,通信速度最快的

在共享内存中,我们写入数据时,直接就向物理内存中进行写入,写入完立刻就能够读取。在管道中,我们需要先将要写入的数据写入到一个缓冲区中,再调用write,将其写到管道中,读取也是要先读取到一个缓冲区中,很明显就慢了。

管道有两次拷贝,1次从用户级缓冲区拷贝到内核级缓冲区,1次从内核级缓冲区拷贝到用户级缓冲区。共享内存有零次拷贝,因为共享内存通过直接映射物理内存到多个进程的地址空间,避免了数据在用户态和内核态之间的冗余拷贝。也就是说进程通过虚拟地址直接读写共享内存,无需经过内核缓冲区

2.让两个进程在各自的用户空间共享内存块,但是,没有加任何保护机制

像刚刚的,先将服务端跑起来,它直接就开始从共享内存中读了,即即使客户端还没有运行起来;并且每次都是从头读取的。两个进程的读写是毫不相干的。没有保护可能会出现一些问题,如现在要写入26个字母,要求是要么不写,要么就一次性写完,此时不加保护的共享内存就无法完成,有可能刚写一半就被读了。

3. 共享内存的保护是需要用户来保护的

一般是使用信号量保护,没讲,所以我们待会使用管道来进行保护。共享内存本质就是进程间的共享资源,对于被保护起来的共享资源,也称为临界资源;访问公共资源的代码,称为临界区;没有访问公共资源的代码,称为非临界区。像上面的Server.cc中,while(true)就是临界区。对共享内存进行保护就是给临界区进行加锁。

我们来理顺一下思路,进程是具有独立性的,而我们要进行进程间通信,就需要让不同进程看到同一份资源,看到了同一份资源之后,就会有一些问题,如一个进程还没有写完,另一个进程就将数据读完了,此时处理数据时,数据就是不完整的,这个问题称为数据不一致问题,此时就引入了临界区、临界资源、加锁、同步等来解决数据不一致问题。之前讲管道时,数据不一致问题是存在的,但是不需要我们自己处理,管道本身就是加了保护的,但是共享内存并没有加保护,需要我们自己处理。

刚刚通信时传递的是一个字符串,现在传递一个结构体。在ShareMemory.hpp中定义。来更准确地说明共享内存是需要保护的

struct data
{char status[32];char lasttime[48];char image[4000];
};

再创建一个Time.hpp,获取当前时间的年月日时分秒


这是获取当前时间戳的系统调用


这个库函数可以将时间戳转换成一个结构体并返回

struct tm {int tm_sec;    // 秒(0-60,60用于闰秒)int tm_min;    // 分钟(0-59)int tm_hour;   // 小时(0-23)int tm_mday;   // 月中的第几天(1-31)int tm_mon;    // 月份(0-11,0=1月)int tm_year;   // 年份(从1900开始的偏移量,如 2023 → 123)int tm_wday;   // 星期几(0-6,0=周日)int tm_yday;   // 一年中的第几天(0-365)int tm_isdst;  // 夏令时标志(>0: 夏令时,=0: 非夏令时,<0: 未知)
};

注意:在struct tm中,月份是0-11,星期是0-6,年份是1900开始的偏移量

现在,我们在Time.hpp中,定义一个函数,用来获取当前的时间

std::string GetCurrTime()
{time_t t = time(nullptr);struct tm* curr = ::localtime(&t);char currtime[32];snprintf(currtime, sizeof(currtime), "%d-%d-%d %d:%d:%d",curr->tm_year + 1900,curr->tm_mon + 1,curr->tm_mday,curr->tm_hour,curr->tm_min,curr->tm_sec);return currtime;
}

在Client.cc中,我们将共享内存当成一个结构体数组,这里我们只使用第一个

int main()
{shm.GetShm();shm.AttachShm();// 在这里进行IPCstruct data* image = (struct data*)shm.GetAddr();char ch = 'A';while(ch <= 'Z'){strcpy(image->status, "最新");strcpy(image->lasttime, GetCurrTime().c_str());strcpy(image->image, "abcdefg");sleep(1);}shm.DetachShm();return 0;
}

Server.cc

int main()
{std::cout << "time: " << GetCurrTime() << std::endl;shm.CreateShm();shm.AttachShm();// 在这里进行IPCstruct data* image = (struct data*)shm.GetAddr();while(true){printf("status: %s\n", image->status);printf("lasttime: %s\n", image->lasttime);printf("image: %s\n", image->image);sleep(2);}shm.DetachShm();shm.DeleteShm();return 0;
}

这里说一个注意点:当我们使用ctrl + c终止一个进程,并且这个进程中创建了共享内存,而进程退出时没有释放共享内存,此时是不能再创建共享内存的,需要使用指令释放掉之前的共享内存,才能够再次创建。


所以,共享内存不仅仅能发字符串,还可以发结构化的信息。

像上面,我们不能在Client.cc中,刚执行了一个strcpy后,Server.cc就读,所以要进行保护。

使用命名管道保护共享内存

刚开始创建管道后,管道是空的,读的进程就会阻塞在read,在写的进程访问共享内存期间,没有进程与其一起访问共享内存,等到写的进程向共享内存写完了,再向管道中写入,此时就会唤醒目标进程,读的进程就开始读取数据。站在读的进程的角度,访问共享内存就是安全的,因为只有当共享内存中有新写入的数据,并且数据全部写完后,才会区访问共享内存。但是站在写的进程,仍然是存在隐患的,因为可能读的进程还没有读完,就向里面写了。

若是想要解决写的进程的问题,可以再创建一个管道。刚开始创建管道后,管道是空的,读的进程就会阻塞在read,在写的进程访问共享内存期间,没有进程与其一起访问共享内存,等到写的进程向共享内存写完了,再向管道中写入,此时就会唤醒目标进程,读的进程就开始读取数据,但是此时写的进程会被阻塞再read,等到读的进程读取完毕后,会向第二个管道写入,写入后,因为第
一个管道仍然没有写入,所以又阻塞在read,第二个管道已经写入了,所以写的进程可以read了,然后访问共享内存,并向第一个管道写入。这样,站在读的进程的角度,访问共享内存就是安全的,因为只有当共享内存中有新写入的数据,并且数据全部写完后,才会区访问共享内存。站在写的进程的角度,也是安全的,因为只有当读的进程读完了之后,才会向共享内存中写入。

为什么不直接使用管道通信?
我们可以在这里的管道中写入很少的数据,真正的通信是在共享内存当中,这样仍然是比管道通信要快的。所以,管道不仅仅可以用来传递数据,还可以用来进行事件通知

我们只实现第一个。我们定义一个Fifo.hpp,将管道的相关接口定义在其中。

Fifo.hpp

#pragma once#include <iostream>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>const std::string gpipeFile = "./fifo";
const mode_t gfifomode = 0600;
const int gdefultfd = -1;
const int gsize = 1024;
const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;class Fifo
{
private:void OpenFifo(int flag){_fd = ::open(gpipeFile.c_str(), flag);if(_fd < 0){std::cerr << "open error" << std::endl;}}
public:Fifo() : _fd(-1){umask(0);int n = ::mkfifo(gpipeFile.c_str(), gfifomode);if(n < 0){std::cerr << "mkfifo error" << std::endl;return ;}std::cout << "mkfifo success" << std::endl;}~Fifo(){if(_fd >= 0)::close(_fd);int n = ::unlink(gpipeFile.c_str());if(n < 0){std::cerr << "unlink error" << std::endl;return ;}std::cout << "unlink sucess" << std::endl;}bool OpenPipeForWrite(){OpenFifo(gForWrite);if(_fd < 0)return false;return true;}bool OpenPipeForRead(){OpenFifo(gForRead);if(_fd < 0)return false;return true;}int Wait(){int code = 0;ssize_t n = ::read(_fd, &code, sizeof(code));if(n == sizeof(code)) return 0;else if(n == 0) return 1;else return 2;}void Signal(){// 每次只向管道中写入一个intint code = 1;::write(_fd, &code, sizeof(code));}
private:int _fd;
};Fifo gfifopipe;

Server.cc

int main()
{// 将共享内存创建好,并打开管道std::cout << "time: " << GetCurrTime() << std::endl;shm.CreateShm();shm.AttachShm();gfifopipe.OpenPipeForRead();// 在这里进行IPCstruct data* image = (struct data*)shm.GetAddr();while(true){// 等待,等到被通知了再读gfifopipe.Wait();printf("status: %s\n", image->status);printf("lasttime: %s\n", image->lasttime);printf("image: %s\n", image->image);}shm.DetachShm();shm.DeleteShm();return 0;
}

Client.cc

int main()
{// 获取共享内存和管道shm.GetShm();shm.AttachShm();gfifopipe.OpenPipeForWrite();// 在这里进行IPCstruct data* image = (struct data*)shm.GetAddr();char ch = 'A';while(ch <= 'Z'){strcpy(image->status, "最新");strcpy(image->lasttime, GetCurrTime().c_str());strcpy(image->image, "abcdefg");// 等到将数据都写到共享内存中,再去通知gfifopipe.Signal();sleep(1);}shm.DetachShm();return 0;
}

这里让server一直读,但是client每隔1秒才写入一次。此时就可以做到基本的保护了。让进程访问公共资源有一定的顺序,这叫做进程间同步。使用管道是在模拟进程间同步的过程。

消息队列

消息队列的概述

消息队列是内核提供的一种进程间通信的方式。操作系统内核维护了一个队列,进程A和进程B要进行通信时,可以创建一个这个队列,并创建一个结构体struct data,将要传递的消息写到其中,然后放到队列当中,另一个进程就能够拿到结点,从而获取信息。这个队列就是消息队列。消息队列是支持双向通信的。即可以A写入,B读取,也可以A读取,B写入。

为什么不能直接向队列中写入数据,而是要使用一个结构体?也就是为什么要有type?
若没有type,A向队列写入,B也向队列写入,未来A读取时就分不清那些是B写入的了,B读取也是同理。所以,type是用来标识这个消息是谁发送的
消息队列的本质:一个进程向另一个进程发送有类型数据块的方法

OS内核中可能会同时存在多条消息队列,所以消息队列也是要被OS管理的。

消息队列的相关接口

这里只是介绍一下,不会有具体的使用。

创建消息队列

这里的参数与共享内存是相同的

查看消息队列

在命令行中同样可以使用ipcrm -q删除消息队列

操作消息队列

这里的参数与共享内存是一样的

struct msqid_ds {struct ipc_perm msg_perm;   // 权限信息(所有者、访问权限等)time_t          msg_stime;  // 最后一次发送消息的时间time_t          msg_rtime;  // 最后一次接收消息的时间time_t          msg_ctime;  // 最后一次修改的时间unsigned long   __msg_cbytes; // 当前队列中的字节数msgqnum_t       msg_qnum;    // 当前队列中的消息数量msglen_t        msg_qbytes;  // 队列允许的最大字节数pid_t           msg_lspid;   // 最后一次发送消息的进程PIDpid_t           msg_lrpid;   // 最后一次接收消息的进程PID
};

可以看到,这里各个接口都与共享内存是十分相似的,这是因为它们都是SystemV标准的。但是,也肯定不会完全一样。

struct msgbuf {long mtype;      // 消息类型(必须 > 0)char mtext[1];   // 消息数据(柔性数组,实际长度可变)
};

msgsnd:向消息队列发送信息。第一个参数是消息队列的id,第二个参数是自己定义的msgbuf,第三个参数是消息大小,就是sizeof(msgbuf),最后一个与共享内存的是一样的,这里直接传入0
msgrcv:从消息队列获取信息。第一个参数是消息队列的id,第三个参数是自己定义的msgbuf,作为缓冲区,第三个参数是希望读取消息的大小,第四个参数是要获取消息的编号,最后一个参数与共享内存相同。

对于发送和收取消息,要求在用户层定义一个结构体struct msgbuf。mtype自已定义,1表示A进程,1表示B进程都可以,但是必须大于0。下面数组的大小随意,发送消息就是将要发送的内容写到这个数组当中,收取消息就是读取这个数组中的内容。

学到现在应该有这样一个认知:管道、共享内存、消息队列,都是OS提供的一个临界资源,本质就是操作系统提供的一个内核数据结构 + 内存空间,这段内存空间是文件的缓冲区就是管道,如果是一个内存块,就是共享内存,如果将这个内存块弄成一个一个的小块,并放上类型,就是消息队列。

消息队列与共享内存一样,生命周期随内核,所以,使用完了需要手动释放。下面的信号量同样

信号量

创建信号量

操作信号量

查看信号量

在命令行中,同样可以使用ipcrm -s删除信号量

补充概念

为了理解信号量是什么,需要补充一些并发编程的概念。

  • 多个执行流(进程),能看到的同一份公共资源:共享资源
  • 被保护起来的资源称为临界资源
  • 保护的常见方式:互斥与同步
  • 任何时刻,只允许一个执行流访问资源,叫做互斥
  • 多个执行流,访问临界资源的时候,具有一定的顺序性,叫做同步
  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源
  • 在进程中涉及到互斥资源的程序段叫临界区。你写的代码=访问临界资源的代码(临界区)+不访问临界资源的代码(非临界区)
  • 所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护(加锁、解锁)

信号量的概念

对于一个共享资源,是可以整体使用或者不整体使用的

当我们整体使用资源时,若规定任何时刻只允许一个进程访问资源,那么就是互斥访问。

当一块共享内存有几十个进程写入,几十个进程读取时,可以通过某种方式,让不同的进程访问共享内存的不同子区域。此时就能够保证,不同进程访问共享资源时,具有一定的并发性。资源不是整体使用最怕的是过量进程进入临界资源,如临界资源被划分成了16个子区域,但有17个进程要访问。此时可以给这个临界资源定义一个int count=16;,当一个进程要访问这块临界资源时,先判断此时的count是否>0,若>0,则conut--,然后访问,若<=0,说明临界资源中没资源了,此时就wait,当一个进程访问完毕,就count++。这样,就保证了访问临界资源的进程的上限是16个。

此时会发现,临界资源整体使用就是只有1份资源,就是count=1。所以,这两个本质是一样的。

电影院的例子:看电影之前都需要先买票,只要把票买了,电影院中其中的一个座位就是我的,即使我不去,不是说要坐在哪里座位才是我的,所以,买票可以让我获得座位资源,买票的本质:对资源的预订机制。另外,买票还可以防止多卖出去票,防止座位的竞争,如只要16个座位,来了17个人。

在上面,假设一个进程申请count成功了,即使这个进程未来没有访问临界资源,临界资源中一定会有一个子区域是不会有其他进程去访问的,因为这个进程没有访问,count就没办法++,所以,当申请计数器成功了,就相当于预订了临界资源的某一个子区域,所以,计数器不仅仅是用来计数的,也是对于资源的预订。所以,往后进程想要访问临界资源,不再关注资源本身,而是要去竞争计数器,就像看电影不再是去电影院中抢座位,而是抢票。我们将对资源进行预定机制的计数器称为信号量。所以,信号量本质就是一个计数器。

刚刚的资源整体使用,进程要访问时,也是先--计数器,计数器要么是0,要么是1,将这种只有两态的计数器称为二元信号量,也叫做锁。所以,对于临界区,加锁就是申请信号量,解锁就是释放信号量,并且信号量只能是0或1。

计数器是int count吗?

1. int count能在多进程之间被看到、被修改、被访问吗?
若定义成全局变量,即使是父子进程,也会发生写时拷贝,更不用说毫不相干的进程了。所以,信号量这个计数器,必须想办法解决这个问题。

2. 假设现在多个进程能够看到一个计数器了,并且进程访问公共资源前都需要先申请计数器。此时,计数器就变成了公共资源,计数器的存在就是为了保护公共资源,现在计数器也变成了公共资源,谁来保护计数器呢?(虽然计数器只有++和--,但是++和--也是不安全的,因为语言上++和--只有1条语句,但是汇编里有3条,先将变量的值放到寄存器中,因为只有CPU有计算能力,进行++或--,计算完再将结果写会内存,所以不是原子的。是存在计数器还没有--完成,就让其++的不安全情况的)


通过上面的分析,我们知道,计数器无非就是这两个操作,只需要保证这两个操作是安全的,那么信号量本身就是安全的,也就是要保证这两个操作是原子的。综上,信号量必须解决两个问题:

  • 必须能够被多个进程看到
  • 本身的 ++ 和 -- 是原子的

信号量并没有进行IPC,为什么被归类到系统的IPC呢?在前面的管道、共享内存、消息队列中,我们会发现,我们使用这些进行IPC时,大量的代码都是为了让多个进程看到同一份资源,而信号量同样需要让多个进程看到,归类到系统的IPC内,就可以让多个进程看到它了。

信号量的接口

system V中申请信号量,是以信号量集的方式申请的,sem_tsems[N]。若只想使用一个信号量就使用这个信号量集中的1个。OS中会有多个信号量集。


semget是创建信号量集。key和semflg与共享内存等是一样的。nsems表示的是这个信号量集中需要多少个信号量。


semctl是操作信号量。semid和op与共享内存等一样。若要删除信号量,semnum写个0即可,op是IPC_RMID,后面省略号中的参数不填。信号量也是有属性的,若要获取信号量的属性,如想知道某一个信号集semid中的某一个信号量semnum的值是几,op填ICP_STAT。semnum是下标,值从0开始。

struct sembuf {unsigned short sem_num;  // 信号量在信号量集中的索引(从 0 开始)short          sem_op;   // 操作类型(正数、负数、0),-1表示--,1表示++short          sem_flg;  // 操作标志(如 IPC_NOWAIT、SEM_UNDO),直接使用0即可
};

semop是进行PV操作的。

System V的IPC原理

SystemV是如何进行IPc的?和管道为什么不同呢?

应用角度,看IPC属性

OS中,会有多个共享内存、多个消息队列、多个信号量集,所以OS要对它们进行管理,管理的方式就是先描述,再组织。我们来看看在用户层的属性。

共享内存

struct shmid_ds {struct ipc_perm shm_perm; // 权限信息size_t          shm_segsz; // 共享内存大小(字节)time_t          shm_atime; // 最后一次附加(attach)时间time_t          shm_dtime; // 最后一次分离(detach)时间time_t          shm_ctime; // 最后一次修改时间pid_t           shm_cpid;  // 创建者的PIDpid_t           shm_lpid;  // 最后一次操作的PIDshmatt_t        shm_nattch; // 当前附加的进程数// ...(可能有其他内核私有字段)
};

消息队列

struct msqid_ds {struct ipc_perm msg_perm; // 权限信息time_t          msg_stime; // 最后一次发送(msgsnd)时间time_t          msg_rtime; // 最后一次接收(msgrcv)时间time_t          msg_ctime; // 最后一次修改时间unsigned long   __msg_cbytes; // 当前队列中的总字节数msgqnum_t       msg_qnum;    // 当前队列中的消息数量msglen_t        msg_qbytes;  // 队列的最大字节数限制pid_t           msg_lspid;   // 最后一次发送消息的PIDpid_t           msg_lrpid;   // 最后一次接收消息的PID// ...(可能有其他内核私有字段)
};

信号量集

struct semid_ds {struct ipc_perm sem_perm; // 权限信息time_t          sem_otime; // 最后一次 semop() 时间time_t          sem_ctime; // 最后一次修改时间unsigned short  sem_nsems; // 信号量集中的信号量数量// ...(可能有其他内核私有字段)
};

IPC对象的权限信息

struct ipc_perm {key_t          __key;    // IPC 对象的键值(ftok() 生成)uid_t          uid;      // 所有者的用户IDgid_t          gid;      // 所有者的组IDuid_t          cuid;     // 创建者的用户IDgid_t          cgid;     // 创建者的组IDunsigned short mode;     // 权限(如 0666)unsigned short __seq;    // 序列号(内部使用)
};

这些数据结构并不是OS内核真正的数据结构,是OS给用户提供的。在这三个结构中,每一个都有struct ipc_perm,并且ipc_perm中有_key。推测:在OS中,这3个是同类资源。

OS在应用层提供了这个结构体,未来用户就可以通过这个结构体来获取共享内存、消息队列、信号量集的相关属性。我们现在来获取一下,以共享内存为例。在前面,ShareMemoty.hpp中,类ShareMemory中有一个成员函数ShmMeta是获取共享内存相关属性,当时并没有实现,现在将其实现。

获取属性是调用shmctl这个函数。需要定义一个struct shmid_ds类型的对象,op传入IPC_STAT,将共享内存的属性放入定义的struct shmid_ds类型的对象中。IPC_STAT是将操作系统内核当中,共享内存的属性通过shmid_ds这个结构,给用户。所以,shmid_ds是操作系统提供的数据类型。我们能够发现,我们能够使用的类型是由3方提供的,语言提供的int等,叫内置类型,库提供的string、vector、size_t等,前两者都属于语言,系统提供的,系统提供的类型一定是在系统的头文件当中,可以直接转到定义查看。

ShareMemoty.hpp

const std::string gpath = "/home/cxf/2025/test_6_7";
int gprojId = 0x6666;
int gshmsize = 4097; // 共享内存的大小
mode_t gmode = 0600; // 共享内存的权限std::string ToHex(key_t k)
{char buffer[64];snprintf(buffer, sizeof(buffer), "0x%x", k);return buffer;
}class ShareMemory
{
private:int CreateShmHelper(int shmflg) // 创建/获取共享内存{_key = ::ftok(gpath.c_str(), gprojId);if (_key < 0){std::cerr << "ftok error" << std::endl;return -1;}_shmid = ::shmget(_key, gshmsize, shmflg);if (_shmid < 0){std::cerr << "shmget error" << std::endl;return -2;}std::cout << "shmid: " << _shmid << std::endl;return _shmid;}
public:ShareMemory():_shmid(-1), _key(0), _addr(nullptr){}~ShareMemory() {}void CreateShm(){if(_shmid == -1)CreateShmHelper(IPC_CREAT | IPC_EXCL | gmode);// 将key转成十六进制std::cout << "key: " << ToHex(_key) << std::endl;}int GetShm(){return CreateShmHelper(IPC_CREAT);}void* AttachShm() // 挂接共享内存{_addr = shmat(_shmid, nullptr, 0);if((long long)_addr == -1){return nullptr;}return _addr;}void DetachShm() // 去关联{if(_addr != nullptr)::shmdt(_addr);std::cout << "detach done" << std::endl;}void DeleteShm() // 删除共享内存{shmctl(_shmid, IPC_RMID, nullptr);}void* GetAddr(){return _addr;}void ShmMeta() // 打印共享内存的属性{struct shmid_ds buffer; // 系统提供的数据类型int n = ::shmctl(_shmid, IPC_STAT, &buffer);if(n < 0) return ;std::cout << "########################" << std::endl;std::cout << buffer.shm_atime << std::endl;std::cout << buffer.shm_cpid << std::endl;std::cout << buffer.shm_ctime << std::endl;std::cout << buffer.shm_nattch << std::endl;std::cout << ToHex(buffer.shm_perm.__key) << std::endl;std::cout << "########################" << std::endl;}
private:int _shmid;key_t _key;void* _addr;
};ShareMemory shm;struct data
{char status[32];char lasttime[48];char image[4000];
};

这里就是实现了ShmMeta,并修改了CreateShm,让其以十六进制的形式打印出key

Server.cc

int main()
{std::cout << "time: " << GetCurrTime() << std::endl;std::cout << "self pid: " << getpid() << std::endl;shm.CreateShm();shm.AttachShm();shm.ShmMeta();struct data* image = (struct data*)shm.GetAddr();shm.DetachShm();shm.DeleteShm();return 0;
}


可以看到,我们通过接口获取到的值,就是shmid_ds中的值。ipcs指令也是从这些结构体中获取数据,并打印出来。

内核角度,看IPC结构

IPC资源就是指内核数据结构 + 空间。IPC资源一定是全局的资源,能被所有的进程看到。我们现在看到的这些都是内核数据结构。

在OS内核中,会给我们提供一个结构体struct ipc_ids,是一个全局的结构,也就是能够被所有进程看到。ipc_ids的内部有一个ipc_id_ary类型的指针entries。ipc_id_ary中有一个柔性数组,这个柔性数组是kern_ipc_perm*类型的数组,是一个指针数组。kern_ipc_perm里面的很多属性与上面的ipc_perm是类似的,所以ipc_perm里的很多属性就来自于内核中的kern_ipc_perm

我们会发现,在应用层共享内存、消息队列、信号量的结构体中第一个元素都是ipc_perm类型的,在内核中,共享内存是shmid_kernel,消息队列是msg_queue,信号量是sem_array。可以看到,在内核中,这3个结构体的第一个元素就是kern_ipc_perm。而ipc_id_ary是一个kern_ipc_perm*类型的柔性数组,所以ipc_id_ary可以直接指向所有IPC资源。

  • p[0]=(struct kern_ipc_perm*)&msg_queue
  • p[1]=(struct kern_ipc_perm*)&sem_array
  • p[2]=(struct kern_ipc_perm*)&shmid_kernel

所以,就可以使用这个柔性数组来管理所有的IPC资源。所以,数组下标标就是之前的id,就是XXXget的返回值。

在内核中,共享内存、消息队列、信号量的结构体中第一个元素都是kern_ipc_perm类型的,并且这个类型里面就有key,所以可以通过key来区分唯一性。注意:不同类型的IPC的key是可以相同的,系统会区分它们,但是相同类型的IPC的key不可以相同,并且尽量避免不同类型的IPC的key相同。

柔性数组指向的是结构体开头的第一个元素,要如何访问后面的元素呢?因为结构体的地址,和结构体首元素的地址是相同的,所以我们可以将指针直接强转成结构体类型的指针,就可以进行访问了。(struct msg_queue*)p[0] ->???

一个指针指向一个对象,既可以访问局部(真正指向的第一个元素),也可以访问整体(整个对象),这种特性叫做多态。之前在C++中,定义一个基类的指针,并指向子类对象,也是既可以访问基类,也可以访问子类。所以,为什么起始都要是struct kern_ipc_perm呢?因为它是基类,而msg_queue、kern_ipc_perm、shm_perm都是子类。这种做法,就是C语言实现多态。在C++的多态中,基类中会有一个指针指向子类,就可以知道一个基类指向的是哪一个子类对象了。但是在这里,柔性数组中的每一个kern_ipc_perm如何知道自己指向的是哪一个子类对象呢?

struct ipc_ids sem_ids;
struct ipc_ids shm_ids;
struct ipc_ids msg_ids;

实际上,共享内存、消息队列、信号量,每一个都会有一个ipc_ids,只要让每一个的ipc_ids里面的entries指针指向同一张ipc_id_ary即可,未来我们是从那个ipc_ids下来的,都是知道的,所以通过柔性数组找到子类时,是知道当前子类是共享内存、消息队列,还是信号量的,Linux2.6.18是这样的。更新的内核还有这样的,但原理是一样的。

#define SEM 0
#define SHM 1
#define MSG 2struct ipc_ids arr[3];

再谈共享内存

我们会发现,消息队列、信号量创建好之后,访问时使用的是系统调用共享内存的底层是而我们使用的地址是虚拟地址,所以,这个文件必须被映射到地址空间中。

struct shmid_kernel {struct kern_ipc_perm    shm_perm;  // 基本权限和标识(继承自 ipc_perm)struct file            *shm_file;  // 关联的共享内存文件(基于 tmpfs)unsigned long          shm_nattch; // 当前附加(attach)的进程数unsigned long          shm_segsz;  // 共享内存段大小(字节)time_t                 shm_atim;   // 最后一次附加时间time_t                 shm_dtim;   // 最后一次分离时间time_t                 shm_ctim;   // 最后一次修改时间pid_t                  shm_cprid;  // 创建者的 PIDpid_t                  shm_lprid;  // 最后一次操作的 PIDstruct user_struct     *mlock_user; // 内存锁定相关的用户信息// ... 其他内核私有字段(如内存页管理、审计信息等)
};

可以看到,在shmid_kernel中,是有一个struct file*类型的参数的。这个文件的文件缓冲区就是共享内存的内存块。

struct vm_area_struct {/* 1. 内存区间基本信息 */unsigned long vm_start;          // 区域的起始虚拟地址(包含)unsigned long vm_end;            // 区域的结束虚拟地址(不包含)pgprot_t      vm_page_prot;      // 页表项的访问权限(如 PAGE_READONLY)unsigned long vm_flags;          // 权限和属性标志(如 VM_READ | VM_WRITE)/* 2. 关联的文件与映射 */struct file *vm_file;            // 如果是文件映射,指向关联的 file 对象unsigned long vm_pgoff;          // 文件中的偏移量(以页为单位)void *vm_private_data;           // 驱动或子系统的私有数据/* 3. 内存操作回调 */const struct vm_operations_struct *vm_ops;  // 操作函数集(如缺页处理)/* 4. 链表和树结构 */struct vm_area_struct *vm_next;  // 链表中的下一个 VMA(按地址排序)struct vm_area_struct *vm_prev;  // 链表中的前一个 VMAstruct rb_node vm_rb;            // 红黑树节点,用于快速查找/* 5. 反向映射与内存管理 */struct mm_struct *vm_mm;         // 所属进程的内存描述符(mm_struct)struct anon_vma *anon_vma;       // 匿名映射的反向映射结构struct list_head anon_vma_chain; // 用于管理匿名页的链/* 6. NUMA 策略 */struct mempolicy *vm_policy;     // NUMA 内存分配策略/* 7. 其他字段(内核内部使用) */atomic_t vm_ref_count;           // 引用计数unsigned long vm_ra_pages;       // 预读的页数// ... 更多内核私有字段(如缓存对齐、调试信息等)
};

vm_area_struct中有一个字段是struct file* vm_file。当我们创建出了一个共享内存,进程地址空间就会创建一个vm_area_struct,并将里面的vm_file指向文件的struct file,同时,vm_area_struct的vm_start和vm_end指向内存块的起始和结束,vm_start和vm_end保存的是虚拟地址。这样,就建立起了虚拟内存到物理内存的映射,就可以通过虚拟内存去访问共享内存了。

动态库映射到地址空间也是同理,动态库就是一个文件,映射前需要先打开,是文件并且打开就会有struct file,然后也会创建一个vm_area_struct,然后与前面一样。并且,共享内存和动态库里的文件,不占文件描述符,因为不是用户打开的。可不可以打开一个文件,然后不使用文件描述符访问文件,而是通过地址空间访问文件呢?是可以的。只要创建一个vm_area_struct对象,并让vm_file指向文件,vm_start和vm_end存放虚拟地址。此时可以使用mmap

相关文章:

  • 大模型呼叫系统——重塑学校招生问答,提升服务效能
  • 离线部署openstack 2024.1 neutron
  • 曼昆《经济学原理》第九版 第十八章生产要素市场
  • 离线部署openstack 2024.1 nova
  • 火山引擎大模型系列都有什么内容
  • Java高频面试之并发编程-27
  • Ubuntu24.04 onnx 模型转 rknn
  • 大语言模型智能体开发的技术框架与应用前景
  • 频域分析和注意力机制
  • 华测CGI-430配置
  • 离线部署openstack 2024.1 keystone
  • 计组刷题日记(1)
  • Python文件读写操作详解:从基础到实战
  • sssssssssssss
  • ConcurrentHashMap详解:原理、实现与并发控制
  • docker推荐应用汇总及部署实战
  • 基本多线程编译make命令
  • 离线部署openstack 2024.1 glance
  • LLM 系列(二) :基础概念篇
  • ThinkPad 交换 Ctrl 键和 Fn 键
  • 网站建设的常见问题/免费注册个人网站
  • 网站编辑怎么做/长沙关键词优化推荐
  • 广州网站建设网站/深圳做网站的公司有哪些
  • 免费行情软件在线观看/昆明seo排名
  • 怎么做干果网站/今天重大国际新闻
  • 网站建设的实验结论/好用的视频播放器app