【Linux】进程通信 | 共享内存 | 信号量
慕雪年华

上篇Linux的博客是有关管道的,今日就让我们继续康康进程间通信的另外一种方法:共享内存

完整代码详见我的gitee仓库 👇

https://gitee.com/musnow/raspberry-practice/tree/master/code/22-11-12_systemV

[TOC]

1.啥是共享内存?

进程间通信的基本方式,就是让两个进程看到同一份资源。

共享内存的方式,通过系统接口开辟一段内存,再让多个进程去访问这块内存,就能同时看到一份资源。

image

这里贴出之前动态库博客中的图,共享内存的方式和该图展示的方式类似。进程需要调用系统接口,将已经开辟好的共享内存映射到自己的页表中,以实现访问。

这里就出现了一个问题:

  • 操作系统的接口怎么知道进程要的是那一块共享内存?即共享内存是怎么标识的?

要知道,之前我们打开文件、开辟管道等等,都是具有唯一的文件路径来标识文件的。如果按以前的想法:打开文件->系统返回文件的文件描述符,共享内存则应该是开辟共享内存->系统返回共享内存的编号

  • 这就出现了问题!

假设进程A开辟了一段共享内存,系统返回了编号123,那么进程A要怎么让其他想使用这块共享内存进行通信的进程,知道它开辟的共享内存编号是123呢?总不能开个管道告诉它吧?那岂不是多此一举😂

image

所以,共享内存的编号其实和命名管道一样,是由用户手动在代码中指定的。只要进程使用这个编号去获取共享内存,他们就能获取到同一份!


2.相关接口

说完了基本概念,现在让我们来康康它的使用

2.1 ftok

ftok - convert a pathname and a project identifier to a System V IPC key

1
2
3
#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类型)

image

只要我们的工作路径和项目编号传的是一样的,那么它返回的key就是一样的!

2.2 shmget

shmget - allocates a System V shared memory segment

1
2
3
4
#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使用,如果不存在指定的共享内存,就进行创建;如果该共享内存存在,则出错返回(即保证获取到的共享内存一定是当前进程创建的,是一个新的共享内存)

返回值是一个共享内存的标识符

1
2
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,可以看到下面的内核结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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一样

1
key_t     __key; //共享内存的唯一标识符,由用户在shmget中提供

关于key为何要让用户提供,已经在上面做出过解释👉 回顾一下


2.3 shmat/shmdt

at其实是attach绑定的缩写,这个接口的作用是将一个共享内存和我们当前的进程绑定。

其实就是将这个共享内存映射到进程的页表中(堆栈之间)

shmat, shmdt - System V shared memory operations

1
2
3
4
5
#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手册中对这两个函数返回值的描述👇

1
2
3
4
5
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

这个函数可以用于操作我们的共享内存

1
2
3
4
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

其中cmd的参数有下面几种

  • IPC_RMID 删除该共享内存

  • IPC_STATshmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值

  • IPC_SET 如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值

最后一个buf参数是一个指向shmid_ds结构的指针,一般设为NULL

1
The buf argument is a pointer to a shmid_ds structure

shmid_ds的基本结构如下

1
2
3
4
5
6
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};

以删除为例,其操作如下

1
shmctl(shmid, IPC_RMID, NULL);//删除shmid的共享内存

2.5 ipcs命令

先来康康几个ipcs命令的选项,其中我们要用到的是-m查看共享内存

1
2
3
4
ipcs -c #查看消息队列/共享内存/信号量
ipcs -s #单独查看信号量
ipcs -q #单独查看消息队列
ipcs -m #单独查看共享内存

执行了之后,会列出当前操作系统中开辟的共享内存,以及它们的基本信息

1
2
3
4
5
6
[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来删除共享内存

1
2
3
4
5
6
7
8
9
10
11
12
13
[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

可以看到我们自己创建的共享内存已经被删除了


消息队列/信号量的接口

消息队列和信号量的接口和共享内存很相似

消息队列用的不多,信号量的难度很高!😂

1
2
3
4
5
6
7
8
9
10
//消息队列相关接口
msgget //获取
msgctl //操作
msgsnd //发送信息
msgrcv

//信号量
semget
semctl
semop

ipcrm

这个命令可以用与删除ipc资源,包括共享内存

1
ipcrm -m shmid #删除共享内存

但是,当我们尝试用该命令删除一个正在被使用的共享内存时,它并不会被立即删除(立即删除会影响进程运行)

此时执行删除,在共享内存的status列会出现dest;观察结果,当进程结束的时候,这个共享内存会被直接删除(进程内部并没有调用shmctl接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[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接口,这个共享内存就会一直存在

1
2
3
4
5
6
[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 创建并获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//头文件实在太多,为了博客篇幅,这里省略了
#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(文件存在)

1
2
3
4
[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接口删除共享内存

1
2
3
4
5
6
7
8
[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里面直接或上这个共享内存的权限值

代码如下👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

这时候创建的共享内存就有正确的权限值了

1
2
3
4
5
6
[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 挂接/取消挂接

1
2
//关联共享内存
char *str = (char*)shmat(id, nullptr, 0);

因为shmat函数的返回值是一个void*指针,我们可以以使用malloc一样的方式使来挂接共享内存。随后对这个内存的操作就是正常的指针操作了!

同样的,另外一个进程也需要用同样的方式挂接共享内存,才能读取到相同的数据

1
2
3
4
5
6
[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的返回值传入即可

1
shmdt(str);//取消挂接

如果是服务端,则还需要在取消挂接之后,删除共享内存。避免下次程序运行的时候,无法通过key获取到新的共享内存

1
shmctl(id,IPC_RMID,nullptr);//删除共享内存

3.3 写入内容

因为共享内存本质就是一个内存,其和malloc出来的内存都是一样的,直接使用即可

这里还是用一个服务端和一个客户端来进行演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//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)服务端进行读取。这便实现了我们进程之间的通信

image

不过我们发现,客户端已经停止写入之后,服务端还是在不停的读取。如果我们不控制while循环的话,其会一直这么读取下去

image

这便牵扯出共享内存的一个特性了

共享内存没有访问控制

在管道的博客中提到,管道是有访问控制的进程通信方式,写端没有写入数据的时候,读端会在read中进行等待

而共享内存因为我们是直接像操作一个malloc出来的空间一样访问,没有使用任何系统接口(相比之下管道需要使用read/write)所以操作系统没有办法帮我们进行访问控制

也正是因为没有等待,共享内存是进程中通信中最快的一种方式

通过管道进行共享内存的控制

既然共享内存没有访问控制,那么我们可以利用管道来让控制共享内存的读写

  • 写端写完后,将完成信号写入管道,由读端读取
  • 读端从管道中获取到信号后,访问共享内存读出内容
  • 如果写端没有写好,读端就会在管道read内部等待

你可能会说,那为何不直接用管道通信呢?

  • 管道仅作访问控制,只需要一个int乃至一个char类型即可;
  • 相比直接管道通信,内存的方式更好控制(毕竟使用内存的方式和使用指针一样,管道还需要文件操作)
  • 读取很长一串数据的时候,共享内存的速度优势能体现出来

以下是完整代码👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//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内等待

image

如果客户端最后关闭了管道的写段,服务器端就会直接退出。这样我们就实现了通过管道控制共享内存的读写👍

image


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覆盖,出现了变量不统一的情况

而我们的信号量为了保证能够正确的控制进程的访问,其就必须维护自身的原子性!不能有中间状态

image

说人话就是,如果进程A在访问信号量,进程B来了,信号量应该拒绝B的访问,直到A访问结束。不能让B中途插入访问,从而导致可能的数据不统一

共享内存同样可以通过信号量进行访问控制

改变信号量的值

1
int semop(int semid, struct sembuf *sops, size_t nops);

功能: 操作信号量,P V 操作

参数:

  • semid 为信号量集的标识符;
  • sops 指向进行操作的结构体数组的首地址;
  • nsops 指出将要进行操作的信号的个数;

返回值: 成功返回0,出错返回-1

1
2
RETURN VALUE
If successful semop() and semtimedop() return 0; otherwise they return -1 with errno indicating the error.

4.2 扩展 mmap

这部分仅供参考,可能有错误😥部分资料参考

前面贴出过IPC资源的内核结构,它们都有一个共同的特点:第一个成员都相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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,其中包含了一个信号量的基本信息

1
2
3
4
5
6
7
8
9
10
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,而不像文件描述符一样,提供第一个没有被使用的下标

1
2
3
4
5
6
7
8
9
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(下标)的资源,对其进行访问

1
2
3
4
5
struct ipc_id_ary
{
int size;
struct kern_ipc_perm *p[0];//指针数组
};

image

那你可能想问了,这里只是第一个元素啊?那如果我想访问shmid_ds结构的其他成员,岂不是没有办法访问了?

要是这么想,就还是太年轻了😂

1
(strcut shmid_ds*)

我们只需要对这个指针进行强转,就能直接访问其他成员!

这是因为:C语言中,结构体第一个元素的地址,和结构体整体的地址是一样的!

指针的类型会限制这个指针访问元素的能力,只要我们进行强转,其就能直接访问父结构体的其他成员!

这是一种切片的思想

用这种办法,可以用统一的规则在内核中管理不同的IPC资源,没有必要再为每一个IPC资源建立一个单独的数组来管理。

image

不得不说,linus大佬是真的牛逼!


结语

关于共享内存的操作到这里就OVER了!

最后还了解了一些内核设计上的小妙招,不得不说,真的牛批~

如果本文有什么问题,欢迎在评论区提出

image