上篇Linux的博客是有关管道的,今日就让我们继续康康进程间通信的另外一种方法:共享内存
完整代码详见我的gitee仓库 👇
https://gitee.com/ewait/raspberry-practice/tree/master/code/22-11-12_systemV
文章目录
- 1.啥是共享内存?
- 2.相关接口
- 2.1 ftok
- 2.2 shmget
- 2.3 shmat/shmdt
- 2.4 shmctl
- 2.5 ipcs命令
- 消息队列/信号量的接口
- ipcrm
- 3.使用
- 3.1 创建并获取
- File exists
- 设置权限值
- 3.2 挂接/取消挂接
- 取消/删除
- 3.3 写入内容
- 共享内存没有访问控制
- 通过管道进行共享内存的控制
- 运行结果
- 4.相关概念
- 4.0 临界资源
- 4.1 信号量
- 原子性的说明
- 改变信号量的值
- 4.2 扩展 mmap
- 结语
1.啥是共享内存?
进程间通信的基本方式,就是让两个进程看到同一份资源。
共享内存的方式,通过系统接口开辟一段内存,再让多个进程去访问这块内存,就能同时看到一份资源。
这里贴出之前动态库博客中的图,共享内存的方式和该图展示的方式类似。进程需要调用系统接口,将已经开辟好的共享内存映射到自己的页表中,以实现访问。
这里就出现了一个问题:
- 操作系统的接口怎么知道进程要的是那一块共享内存?即共享内存是怎么标识的?
要知道,之前我们打开文件、开辟管道等等,都是具有唯一的文件路径来标识文件的。如果按以前的想法:打开文件->系统返回文件的文件描述符
,共享内存则应该是开辟共享内存->系统返回共享内存的编号
- 这就出现了问题!
假设进程A开辟了一段共享内存,系统返回了编号123,那么进程A要怎么让其他想使用这块共享内存进行通信的进程,知道它开辟的共享内存编号是123呢?总不能开个管道告诉它吧?那岂不是多此一举😂
所以,共享内存的编号其实和命名管道一样,是由用户手动在代码中指定的。只要进程使用这个编号去获取共享内存,他们就能获取到同一份!
2.相关接口
说完了基本概念,现在让我们来康康它的使用
2.1 ftok
ftok - convert a pathname and a project identifier to a System V IPC key
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
前面提到了,共享内存的key是我们自己指定的。Linux系统给定了ftok
接口,将用户提供的pathname
工作路径,以及proj_id
项目编号转换为一个共享内存的key(其实就是int类型)
只要我们的工作路径和项目编号传的是一样的,那么它返回的key就是一样的!
2.2 shmget
shmget - allocates a System V shared memory segment
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数分别为key值,共享内存的大小,以及创建共享内存的方式。
key值需要通过 ftok
函数获取;
其中共享内存的大小最好设置为4kb的整数倍,因为操作系统IO的基本单位是4KB。如果你申请了不是4的整数倍的字节,比如15个字节,其还是会申请16个字节(4个页)交给你,而其中有1kb的内存你是无法使用的,即造成了内存浪费😥
创建共享内存的shmflg
:
-
IPC_CREAT
:创建共享内存。如果存在则获取,如果不存在则创建后获取 -
IPC_EXCL
:必须配合IPC_CREAT
使用,如果不存在指定的共享内存,就进行创建;如果该共享内存存在,则出错返回(即保证获取到的共享内存一定是当前进程创建的,是一个新的共享内存)
返回值是一个共享内存的标识符
RETURN VALUE
On success, a valid shared memory identifier is returned. On errir, -1 is returned, and errno is set to indicate the error.
这些工作都是操作系统做的。其内核中有专门的管理单元来判断一个共享内存是否存在,以及何时被创建、被使用、被什么进程绑定等等…
命令行键入man shmctl
,可以看到下面的内核结构
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
共享内存要被管理,其内核结构中一定有一个唯一的key值来标识该共享内存,即和文件的inode
一样
key_t __key; //共享内存的唯一标识符,由用户在shmget中提供
关于key为何要让用户提供,已经在上面做出过解释👉 回顾一下
2.3 shmat/shmdt
at其实是attach绑定的缩写,这个接口的作用是将一个共享内存和我们当前的进程绑定。
其实就是将这个共享内存映射到进程的页表中(堆栈之间)
shmat, shmdt - System V shared memory operations
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
一共有两个函数,分别为at和dt,用于绑定/解绑共享内存
shmat
的三个参数如下
- shmid:为
shmget
的返回值 - shmaddr:指定共享内存连接到当前进程中的地址位置。通常为空,表示让系统来选择共享内存的地址。
- shmflg:如果指定了
SHM_RDONLY
位,则以只读方式连接此段;否则以读写的方式连接此段;通常设置为0
调用成功的时候,返回指向共享内存第一个字节的指针;出错返回-1
-
shmdt
的参数为shmat
正确调用时的返回值
以下是man手册中对这两个函数返回值的描述👇
RETURN VALUE
On success shmat() returns the address of the attached shared memory segment; on error (void *) -1 is returned, and errno is set to
indicate the cause of the error.
On success shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.
2.4 shmctl
这个函数可以用于操作我们的共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
其中cmd的参数有下面几种
IPC_RMID
删除该共享内存IPC_STAT
把shmid_ds
结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds
的值IPC_SET
如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds
结构中给出的值
最后一个buf参数是一个指向shmid_ds
结构的指针,一般设为NULL
The buf argument is a pointer to a shmid_ds structure
shmid_ds
的基本结构如下
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
以删除为例,其操作如下
shmctl(shmid, IPC_RMID, NULL);//删除shmid的共享内存
2.5 ipcs命令
先来康康几个ipcs
命令的选项,其中我们要用到的是-m
查看共享内存
ipcs -c #查看消息队列/共享内存/信号量
ipcs -s #单独查看信号量
ipcs -q #单独查看消息队列
ipcs -m #单独查看共享内存
执行了之后,会列出当前操作系统中开辟的共享内存,以及它们的基本信息
[muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 1
0x20011ac8 1 muxue 0 1024 0
这里的key和我们使用ftok
获取到的key值是一样的,只不过我们打印的时候是十进制,操作系统列出来的为十六进制
我们可以使用ipcrm -m 共享内存的shmid
来删除共享内存
[muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 1
0x20011ac8 1 muxue 0 1024 0
[muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ipcrm -m 1
[muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 1
可以看到我们自己创建的共享内存已经被删除了
消息队列/信号量的接口
消息队列和信号量的接口和共享内存很相似
消息队列用的不多,信号量的难度很高!😂
//消息队列相关接口
msgget //获取
msgctl //操作
msgsnd //发送信息
msgrcv
//信号量
semget
semctl
semop
ipcrm
这个命令可以用与删除ipc资源,包括共享内存
ipcrm -m shmid #删除共享内存
但是,当我们尝试用该命令删除一个正在被使用的共享内存时,它并不会被立即删除(立即删除会影响进程运行)
此时执行删除,在共享内存的status
列会出现dest
;观察结果,当进程结束的时候,这个共享内存会被直接删除(进程内部并没有调用shmctl
接口)
[muxue@bt-7274:~/git]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 1
0x20011ac8 21 muxue 666 1024 2
[muxue@bt-7274:~/git]$ ipcrm -m 21
[muxue@bt-7274:~/git]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 1
0x00000000 21 muxue 666 1024 2 dest
[muxue@bt-7274:~/git]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 1
相比之下,如果不执行ipcrm
命令+进程内部不调用shmctl
接口,这个共享内存就会一直存在
[muxue@bt-7274:~/git]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 1
0x20011ac8 22 muxue 666 1024 0
结论:使用ipcrm -m
命令删除共享内存之后,其共享内存不一定会立即释放。如果有进程关联了该共享内存,则会在进程去关联之后释放
3.使用
3.1 创建并获取
//头文件实在太多,为了博客篇幅,这里省略了
#define NUM 1024
#define PROJ_ID 0x20
#define PATH_NAME "/home/muxue/git/linux/code/22-11-12_systemV"
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
cerr <<"ftok: "<< strerror(errno) << endl;
exit(1);//key获取错误直接退出程序
}
return key;
}
int main()
{
key_t key = CreateKey();
int id = shmget(key, NUM, IPC_CREAT | IPC_EXCL);
if(id<0)
{
cerr<< "shmget err: " << strerror(errno) << endl;
return 1;
}
cout << "shmget success: " << id << endl;
return 0;
}
File exists
这里会发现,第一次运行代码的时候,程序成功获取了共享内存;但是第二次运行的时候,却报错说File exists(文件存在)
[muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ./test
shmget: 1
[muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ./test
shmget err: File exists
这是因为共享内存的声明周期是随内核的。即只要这个共享内存不被删除,他就会一直存在,直到内核因为某种原因释放掉它,亦或者操作系统关机
通过上面提到的ipcrm -m shmid
命令删除共享内存,才能重新运行代码获取新的共享内存
为了避免这个问题,应该在进程结束后使用
shmctl
接口删除共享内存
[muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ./test
shmget success: 2
[muxue@bt-7274:~/git/linux/code/22-11-12_systemV]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 1
0x20011ac8 2 muxue 0 1024 0
设置权限值
默认情况下,我们创建的共享内存的perms
是0,代表没有用户能访问这个共享内存。所以在创建的时候,我们需要在flag里面直接或上这个共享内存的权限值
代码如下👇
int main()
{
key_t key = CreateKey();
int id = shmget(key, NUM, IPC_CREAT | IPC_EXCL | 0666);
if(id<0)
{
cerr<< "shmget err: " << strerror(errno) << endl;
return 1;
}
cout << "shmget success: " << id << endl;
sleep(5);
shmctl(id,IPC_RMID,nullptr);
return 0;
}
这时候创建的共享内存就有正确的权限值了
[muxue@bt-7274:~/git]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 1
0x20011ac8 4 muxue 666 1024 0
3.2 挂接/取消挂接
//关联共享内存
char *str = (char*)shmat(id, nullptr, 0);
因为shmat函数的返回值是一个void*
指针,我们可以以使用malloc
一样的方式使来挂接共享内存。随后对这个内存的操作就是正常的指针操作了!
同样的,另外一个进程也需要用同样的方式挂接共享内存,才能读取到相同的数据
[muxue@bt-7274:~/git]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00005feb 0 root 666 12000 1
0x20011ac8 4 muxue 666 1024 1
挂接成功后,可以发现nattch
的值从0变为1
取消/删除
取消挂接的方式很简单,直接把shmat
的返回值传入即可
shmdt(str);//取消挂接
如果是服务端,则还需要在取消挂接之后,删除共享内存。避免下次程序运行的时候,无法通过key获取到新的共享内存
shmctl(id,IPC_RMID,nullptr);//删除共享内存
3.3 写入内容
因为共享内存本质就是一个内存,其和malloc出来的内存都是一样的,直接使用即可
这里还是用一个服务端和一个客户端来进行演示
//server.cpp
#include "Mykey.hpp"
int main()
{
//获取key值
key_t key = CreateKey();
//创建共享内存
int id = shmget(key, NUM, IPC_CREAT | IPC_EXCL | 0666);
if(id<0)
{
cerr<< "shmget err: " << strerror(errno) << endl;
return 1;
}
cout << "shmget success: " << id << endl;
sleep(2);
//关联共享内存
char *str = (char*)shmat(id, nullptr, 0);
printf("[server] shmat success\n");
//读取数据,sleep(1)
int i=0;
while(i<=40)
{
printf("[%03d] %s\n",i,str);
i++;
sleep(1);
}
//去关联
shmdt(str);//shmat的返回值
printf("[server] shmdt(str)\n");
//删除共享内存
shmctl(id,IPC_RMID,nullptr);
printf("[server] exit\n");
return 0;
}
//client.cpp
#include "Mykey.hpp"
int main()
{
//获取key值
key_t key = CreateKey();
//获取共享内存
int id = shmget(key, NUM, IPC_CREAT);
if(id<0)
{
cerr<< "shmget err: " << strerror(errno) << endl;
return 1;
}
cout << "shmget success: " << id << endl;
sleep(2);
//关联共享内存
char *str = (char*)shmat(id, nullptr, 0);
printf("[client] shmat success\n");
//写入数据
int i=0;
while(i<26)
{
char base = 'A';
str[i] = base+i;
str[i+1] = '\0';
printf("write times: %02d\n",i);
i++;
sleep(1);
}
//去关联
shmdt(str);//shmat的返回值
printf("[client] shmdt & exit\n");
return 0;
}
跑起来之后,客户端向共享内存中写入数据(注意控制\0
)服务端进行读取。这便实现了我们进程之间的通信
不过我们发现,客户端已经停止写入之后,服务端还是在不停的读取。如果我们不控制while
循环的话,其会一直这么读取下去
这便牵扯出共享内存的一个特性了
共享内存没有访问控制
在管道的博客中提到,管道是有访问控制的进程通信方式,写端没有写入数据的时候,读端会在read
中进行等待
而共享内存因为我们是直接像操作一个malloc出来的空间一样访问,没有使用任何系统接口(相比之下管道需要使用read/write
)所以操作系统没有办法帮我们进行访问控制
也正是因为没有等待,共享内存是进程中通信中最快的一种方式
通过管道进行共享内存的控制
既然共享内存没有访问控制,那么我们可以利用管道来让控制共享内存的读写
- 写端写完后,将完成信号写入管道,由读端读取
- 读端从管道中获取到信号后,访问共享内存读出内容
- 如果写端没有写好,读端就会在管道read内部等待
你可能会说,那为何不直接用管道通信呢?
- 管道仅作访问控制,只需要一个int乃至一个char类型即可;
- 相比直接管道通信,内存的方式更好控制(毕竟使用内存的方式和使用指针一样,管道还需要文件操作)
- 读取很长一串数据的时候,共享内存的速度优势能体现出来
以下是完整代码👇
//mykey.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <fcntl.h>
#include <unistd.h>
#include <cassert>
using namespace std;
#define NUM 1024
#define PROJ_ID 0x20
#define PATH_NAME "/home/muxue/git/linux/code/22-11-12_systemV"
#define FIFO_FILE "sc.pipe"
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
cerr <<"ftok: "<< strerror(errno) << endl;
exit(1);//key获取错误直接退出程序
}
return key;
}
void CreateFifo()
{
umask(0);
if(mkfifo(FIFO_FILE, 0666) < 0)
{
cerr << "fifo: " << strerror(errno) << endl;
exit(2);
}
}
//打开管道文件
int Open(int flags)
{
return open(FIFO_FILE, flags);
}
//让读端通过管道等待
ssize_t Wait(int fd)
{
char val = 0;
//如果写端没有写入,其就会在read中等待
ssize_t s = read(fd, &val, sizeof(val));
return s;
}
//发送完成信息
int Signal(int fd)
{
char sig = 'g';
write(fd, &sig, sizeof(sig));
}
//server.cpp
#include "Mykey.hpp"
int main()
{
//创建管道
CreateFifo();
//获取key值
key_t key = CreateKey();
//创建共享内存
int id = shmget(key, NUM, IPC_CREAT | IPC_EXCL | 0666);
if(id<0)
{
cerr<< "shmget err: " << strerror(errno) << endl;
return 1;
}
cout << "shmget success: " << id << endl;
//获取管道
int fd = Open(O_RDONLY);
cout << "open fifo success: " << fd << endl;
sleep(2);
//关联共享内存
char *str = (char*)shmat(id, nullptr, 0);
printf("[server] shmat success\n");
//读取数据
int i=0;
while(i<=40)
{
ssize_t ret = Wait(fd);//通过管道等待
if(ret!=0)
{
printf("[%03d] %s\n",i,str);
i++;
sleep(1);
}
else
{
cout<<"[server] wait finish, break" << endl;
break;
}
}
//去关联
shmdt(str);//shmat的返回值
printf("[server] shmdt(str)\n");
//删除共享内存
shmctl(id,IPC_RMID,nullptr);
close(fd);
unlink(FIFO_FILE);
printf("[server] exit\n");
return 0;
}
//client.cpp
#include "Mykey.hpp"
int main()
{
//获取key值
key_t key = CreateKey();
//获取共享内存
int id = shmget(key, NUM, IPC_CREAT);
if(id<0)
{
cerr<< "shmget err: " << strerror(errno) << endl;
return 1;
}
cout << "shmget success: " << id << endl;
//获取管道
int fd = Open(O_WRONLY);
cout << "open fifo success: " << fd << endl;
sleep(2);
//关联共享内存
char *str = (char*)shmat(id, nullptr, 0);
printf("[client] shmat success\n");
//写入数据
int i=0;
while(i<26)
{
char base = 'A';
str[i] = base+i;
str[i+1] = '\0';
printf("write times: %02d\n",i);
i++;
Signal(fd);
sleep(1);
}
//去关联
shmdt(str);//shmat的返回值
printf("[client] shmdt & exit\n");
close(fd);
printf("[client] close fifo\n");
return 0;
}
运行结果
管道控制了之后,当客户端退出的时候,管道也不会继续读取,而是在read内等待
如果客户端最后关闭了管道的写段,服务器端就会直接退出。这样我们就实现了通过管道控制共享内存的读写👍
4.相关概念
4.0 临界资源
能被多个进程看到的资源,被称为临界资源
如果不对临界资源进行访问控制,进程对该资源的访问就是乱序的(比如父子进程向显示器打印内容)可能会因为数据交叉导致乱码、数据不可用等情况
以此可见,显示器、管道、共享内存都是临界资源
- 管道是有访问控制的临界资源
进程访问临界资源的代码,称为临界区
- 一个进程中,并不是所有的代码都在访问临界资源。如管道中,其实只有
read/write
接口在访问临界资源
互斥:任何时刻,只允许一个进程访问临界资源
原子性:一件事情只有做完/没做
两种状态,没有中间状态
下面对信号量的概念进行讲解~只用基本理解即可
4.1 信号量
信号量是对临界资源
的控制方式之一,其本质是一个计数器
- 信号量保证不会有多余的进程连接到这份临界资源
- 还需要保证每一个进程的能够访问到临界资源的不同位置(根据上层业务决定)
信号量根据情况的不同分为两种:
- 二元信号量(互斥状态,当进程使用的时候为1,没有进程使用的时候为0)
- 多元信号量(常规)
如果一个进程想访问由信号量控制的临界资源,必须先申请信号量。申请成功,就一定能访问到这个临界资源中的一部分(或者全部)
原子性的说明
先来想想,我们对一个变量+1/-1
需要做什么工作:
- 将这个变量从内存中拿到CPU的寄存器中
- 在寄存器中完成加减操作
- 放回内存
这其中是有很多个中间状态的,设该变量初始值为100
- 假设一个进程A拿走了这个变量,放入CPU的寄存器
- 另外一个进程B也来拿走了这个变量
- 此时A和B拿到的都是100
- A对该变量进行了循环
--
操作,最终该变量变成了50,将其放回内存 - B对该变量
-1
,将其放回内存 - 最终导致A对变量的操作被B覆盖,出现了变量不统一的情况
而我们的信号量为了保证能够正确的控制进程的访问,其就必须维护自身的原子性!不能有中间状态
说人话就是,如果进程A在访问信号量,进程B来了,信号量应该拒绝B的访问,直到A访问结束。不能让B中途插入访问,从而导致可能的数据不统一
共享内存同样可以通过信号量进行访问控制
改变信号量的值
int semop(int semid, struct sembuf *sops, size_t nops);
功能: 操作信号量,P V
操作
参数:
- semid 为信号量集的标识符;
- sops 指向进行操作的结构体数组的首地址;
- nsops 指出将要进行操作的信号的个数;
返回值: 成功返回0,出错返回-1
RETURN VALUE
If successful semop() and semtimedop() return 0; otherwise they return -1 with errno indicating the error.
因为信号量的操作比较复杂,所以这里就木有演示😝
4.2 扩展 mmap
这部分仅供参考,可能有错误😥部分资料参考
前面贴出过IPC
资源的内核结构,它们都有一个共同的特点:第一个成员都相同
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* No. of semaphores in set */
};
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
它们的第一个成员都是一个struct ipc_perm
,其中包含了一个信号量的基本信息
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
而内核中对IPC
资源的管理,是通过一个数组进行的。我们所获取的shmid
,和文件描述符一样,都是一个数组的下标
其中我在测试的时候,便发现了一点:我们每一次获取的新的共享内存,它的编号都会+1
,而不像文件描述符一样,提供第一个没有被使用的下标
struct ipc_ids {
int in_use;//说明已分配的资源个数
int max_id;//在使用的最大的位置索引
unsigned short seq;//下一个分配的位置序列号
unsigned short seq_max;//最大位置使用序列
struct semaphore sem; //保护 ipc_ids的信号量
struct ipc_id_ary nullentry;//如果IPC资源无法初始化,则entries字段指向伪数据结构
struct ipc_id_ary* entries;//指向资源ipc_id_ary数据结构的指针
};
在内核中,struct ipc_id_ary* entries
是一个指向所有ipc_perm
的指针数组。其能够通过该数组找到我们对于id(下标)的资源,对其进行访问
struct ipc_id_ary
{
int size;
struct kern_ipc_perm *p[0];//指针数组
};
那你可能想问了,这里只是第一个元素啊?那如果我想访问shmid_ds
结构的其他成员,岂不是没有办法访问了?
要是这么想,就还是太年轻了😂
(strcut shmid_ds*)
我们只需要对这个指针进行强转,就能直接访问其他成员!
这是因为:C语言中,结构体第一个元素的地址,和结构体整体的地址是一样的!
指针的类型会限制这个指针访问元素的能力,只要我们进行强转,其就能直接访问父结构体的其他成员!
这是一种切片的思想
用这种办法,可以用统一的规则在内核中管理不同的IPC
资源,没有必要再为每一个IPC资源建立一个单独的数组来管理。
不得不说,linus
大佬是真的牛逼!
结语
关于共享内存的操作到这里就OVER了!
最后还了解了一些内核设计上的小妙招,不得不说,真的牛批~
如果本文有什么问题,欢迎在评论区提出