共享内存

共享内存可以被描述成内存一个区域(段)的映射,这个区域可以被更多的进程所共享。1
该方式应该是 SysV IPC 中最快速度形式,减少了不必要的复制拷贝操作,但是 Linux 对共享内存不提供同步操作,一般需要其他方式进行辅助,如信号量。

流程

一般流程2如下:

  1. 通过 ftok 获得确定的 IPC Key
  2. 使用 shmget 获得用于共享内存的特定 IPC ID
  3. 使用 shmat/shmdt 操作共享内存
  4. 使用 shmctl 移除共享内存

函数

shmget

建立/获得共享内存

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg)
  • key:键值,一般使用 ftok 获得
  • size:字节单位,申请的共享内存大小
  • shmflg:使用 | 连接
    • 权限:可使用 0666 等表示,也可使用 openmode
    • IPC_CREAT:若不存在则创建
    • IPC_EXCL:若存在则返回错误(和 IPC_CREAT 一起使用)
  • return:-1,错误;非负数,共享内存 ID

  1. 查看系统共享内存可通过 ipcs -m 命令
  2. 查看系统对共享内存限制通过 ipcs -lm 命令

shmctl

控制共享内存,设置、获取、删除。

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf)
  • shmid:shmget 获得 ID
  • cmd:同信号量类似,以下列出已尝试部分
    • IPC_RMID:命令实际上不从内核删除一个段,而是仅仅把这个段标记为删除,实际的删除发生在最后一个进程离开这个共享段时。3
  • buf:
  • return:-1,错误


删除系统中存在的共享内存可使用 ipcrm -m/M

shmat/shmdt

连接共享内存。

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg)
  • shmid:shmget 获得 ID
  • shmaddr:NULL,表示由系统分配
  • shmflg:标志
    • SHM_RDONLY:只读模式
  • return:-1,错误,共享内存起始地址

断开共享内存。

int shmdt(const void *shmaddr)
  • shmaddr:共享内存其实地址
  • return:-1,错误

实例

所有例子源码文件在 demo/c/ipc/shm/ 中。

未同步

没有添加其他辅助手段多进程读写共享内存,会发生读取错误的可能。
以下例子共享内存使用字符串,因为对字符串的复制拷贝,一般不会是原子操作。

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

#define SHM_TEST_DATALEN 256

struct shm_test_st {
char dat[SHM_TEST_DATALEN];
};

const char g_test_data[][SHM_TEST_DATALEN] = {
{"Nothing gold can stay"},
{"Nature's first green is gold"},
{"Her hardest hue to hold"},
{"Her early leaf's a flower"},
{"But only so an hour"},
{"Then leaf subsides leaf"},
{"So Eden sank to grief"},
{"So down gose down to day"},
{"Ending"}
};

int shm_write(struct shm_test_st *pdata)
{
int num = sizeof(g_test_data) / sizeof(g_test_data[0]);
int i;
for (i = 0; i < num; i++) {
strcpy(pdata->dat, g_test_data[i]);
printf("Write - %s\n", g_test_data[i]);
sleep(1);
}
printf("Write Over!\n");
return 0;
}

int shm_read(struct shm_test_st *pdata)
{
sleep(1);
char tmp[SHM_TEST_DATALEN];
while (1) {
strcpy(tmp, pdata->dat);
int num = sizeof(g_test_data) / sizeof(g_test_data[0]);
int ret = -1;
int i;
for (i = 0; i < num; i++) {
if (strcmp(tmp, g_test_data[i]) == 0) {
ret = 0;
if (i == (num - 1)) {
printf("Read Over\n");
return 0;
}
}
}
if (ret < 0) {
printf("Read Error - %s\n", tmp);
}
sleep(0);
}
return -1;
}

int main (int argc, char **argv)
{
if (argc < 2) {
printf("demo w/r\n");
exit(1);
}
key_t ipckey = ftok("/tmp/test", ':');
int shmid = shmget(ipckey, sizeof(struct shm_test_st), 0600 | IPC_CREAT);
if (shmid < 0) {
printf("Error - %s\n", strerror(errno));
exit(1);
}
void *addr = NULL;
if (argv[1][0] == 'w') {
addr = shmat(shmid, NULL, 0);
} else { // r
addr = shmat(shmid, NULL, SHM_RDONLY);
}
if (addr == (void *)-1) {
printf("Addr Error - %s\n", strerror(errno));
exit(1);
}
struct shm_test_st *p_shared = (struct shm_test_st *)addr;
int ret;
if (argv[1][0] == 'w') {
ret = shm_write(p_shared);
} else { // r
ret = shm_read(p_shared);
}
if (shmctl(shmid, IPC_RMID, NULL) < 0) {
printf("Delete Error - %s\n", strerror(errno));
}
if (ret < 0) {
exit(1);
}
}

执行程序:

./demo w & ./demo r & ./demo r & ./demo r &

执行结果:可能产生读取信息错误,如下

Write - Nothing gold can stay
Write - Nature's first green is gold
Write - Her hardest hue to hold
Read Error - Nature's first gto hold
Write - Her early leaf's a flower
Write - But only so an hour
Write - Then leaf subsides leaf
Read Error - But only so an hes llower
Write - So Eden sank to grief
Write - So down gose down to day
Write - Ending
Read Over
Read Over
Read Over
Write Over!

写优先锁

上个例子中出现问题,很多是在写的过程中读取数据造成错误,查看网上一篇文章「知识讲解Unix操作系统共享内存」中说:

常规的方法是设一个信号量,Unix 操作系统将访问共享内存的程序作为临界区来处理。程序进入时用 p() 操作取得锁,退出时用 v() 操作释放锁。
只有 Server 进行 p()v() 操作,信号量初始值设为 0,p() 操作将它加一,v() 操作将它减一;Client 读共享内存之前要先等待信号量的值为 0,这样 Server 的 p() 操作总是成功,而 Server 的 p() 操作后,尚未进入临界区的 Client 只能等到 Server 执行 v() 操作后才能读。这样 Server 比 Client 优先,Client 之间不互斥。4

当然以上方法其实只适合读很快速(原子)类型,本例应该还会有发生错误的可能,下面验证一下。

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
int shm_write(struct shm_test_st *pdata, int semid)
{
struct sembuf sem[1];
sem[0].sem_num = 0;
sem[0].sem_flg = SEM_UNDO;
int num = sizeof(g_test_data) / sizeof(g_test_data[0]);
int i;
for (i = 0; i < num; i++) {
sem[0].sem_op = 1;
semop(semid, sem, 1); // p()
printf("Write - %s\n", g_test_data[i]);
strcpy(pdata->dat, g_test_data[i]);
sem[0].sem_op = -1;
semop(semid, sem, 1); // v()
sleep(1);
}
printf("Write Over!\n");
return 0;
}

int shm_read(struct shm_test_st *pdata, int semid)
{
struct sembuf sem[1];
sem[0].sem_num = 0;
sem[0].sem_flg = SEM_UNDO;
sleep(1);
char tmp[SHM_TEST_DATALEN];
while (1) {
sem[0].sem_op = 0;
semop(semid, sem, 1); // wait()
strcpy(tmp, pdata->dat);
int num = sizeof(g_test_data) / sizeof(g_test_data[0]);
int ret = -1;
int i;
for (i = 0; i < num; i++) {
if (strcmp(tmp, g_test_data[i]) == 0) {
ret = 0;
if (i == (num - 1)) {
printf("Read Over\n");
return 0;
}
}
}
if (ret < 0) {
printf("Read Error - %s\n", tmp);
}
sleep(0);
}
return -1;
}

执行程序:

./demo w & ./demo r & ./demo r & ./demo r &

执行结果:确实还是会产生错误

Write - Nothing gold can stay
Write - Nature's first green is gold
Read Error - Nothing gold canreen i
Write - Her hardest hue to hold
Write - Her early leaf's a flower
Write - But only so an hour
Write - Then leaf subsides leaf
Write - So Eden sank to grief
Write - So down gose down to day
Write - Ending
Read Over
Read Over
Read Over
Write Over!

互斥锁

看来上面的方法也不行,那么只有读写全部加锁才能保证不出错了,不过这样的缺点就是所有的读也变成了串行,降低了效率。

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
int shm_write(struct shm_test_st *pdata, int semid)
{
struct sembuf sem[2];
sem[0].sem_num = 0;
sem[1].sem_num = 0;
sem[0].sem_flg = SEM_UNDO;
sem[1].sem_flg = SEM_UNDO;
int num = sizeof(g_test_data) / sizeof(g_test_data[0]);
int i;
for (i = 0; i < num; i++) {
sem[0].sem_op = 0;
sem[1].sem_op = 1;
semop(semid, sem, 2);
printf("Write - %s\n", g_test_data[i]);
strcpy(pdata->dat, g_test_data[i]);
sem[0].sem_op = -1;
semop(semid, sem, 1);
sleep(1);
}
printf("Write Over!\n");
return 0;
}

int shm_read(struct shm_test_st *pdata, int semid)
{
struct sembuf sem[2];
sem[0].sem_num = 0;
sem[1].sem_num = 0;
sem[0].sem_flg = SEM_UNDO;
sem[1].sem_flg = SEM_UNDO;
sleep(1);
char tmp[SHM_TEST_DATALEN];
while (1) {
sem[0].sem_op = 0;
sem[1].sem_op = 1;
semop(semid, sem, 2);
strcpy(tmp, pdata->dat);
sem[0].sem_op = -1;
semop(semid, sem, 1);
int num = sizeof(g_test_data) / sizeof(g_test_data[0]);
int ret = -1;
int i;
for (i = 0; i < num; i++) {
if (strcmp(tmp, g_test_data[i]) == 0) {
ret = 0;
if (i == (num - 1)) {
printf("Read Over\n");
return 0;
}
}
}
if (ret < 0) {
printf("Read Error - %s\n", tmp);
}
sleep(0);
}
return -1;
}

结果不会出现读错的情况了。

读写锁

通过「Linux线程同步之读写锁(rwlock)」文章,发现信号量可以实现读写锁。

  • 只要没有写模式下的加锁,任意线程都可以进行读模式下的加锁;
  • 只有读写锁处于不加锁状态时,才能进行写模式下的加锁;5

将之前单锁变成双锁即可,一个表示写,一个表示读。
此方式适合少写多读的场景。

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
int shm_write(struct shm_test_st *pdata, int semid)
{
struct sembuf sem[3];
sem[0].sem_num = 0; // w
sem[1].sem_num = 1; // r
sem[2].sem_num = 0; // w
sem[0].sem_flg = SEM_UNDO;
sem[1].sem_flg = SEM_UNDO;
sem[2].sem_flg = SEM_UNDO;
int num = sizeof(g_test_data) / sizeof(g_test_data[0]);
int i;
for (i = 0; i < num; i++) {
sem[0].sem_op = 0; // wait write free
sem[1].sem_op = 0; // wait read free
sem[2].sem_op = 1; // write +1 -> going
semop(semid, sem, 3);
printf("Write - %s\n", g_test_data[i]);
strcpy(pdata->dat, g_test_data[i]);
sem[0].sem_op = -1;
semop(semid, sem, 1); // write -1 -> over
sleep(1);
}
printf("Write Over!\n");
return 0;
}

int shm_read(struct shm_test_st *pdata, int semid)
{
struct sembuf sem[2];
sem[0].sem_num = 0; // w
sem[1].sem_num = 1; // r
sem[0].sem_flg = SEM_UNDO;
sem[1].sem_flg = SEM_UNDO;
sleep(1);
char tmp[SHM_TEST_DATALEN];
while (1) {
sem[0].sem_op = 0; // write is free
sem[1].sem_op = 1; // read +1 -> going
semop(semid, sem, 2);
strcpy(tmp, pdata->dat);
sem[0].sem_num = 1; // r
sem[0].sem_op = -1; // read -1 -> over
semop(semid, sem, 1);
int num = sizeof(g_test_data) / sizeof(g_test_data[0]);
int ret = -1;
int i;
for (i = 0; i < num; i++) {
if (strcmp(tmp, g_test_data[i]) == 0) {
ret = 0;
if (i == (num - 1)) {
printf("Read Over\n");
return 0;
}
}
}
if (ret < 0) {
printf("Read Error - %s\n", tmp);
}
sleep(0);
}
return -1;
}

共享内存
https://wishlily.github.io/article/linux/2016/09/05/undefined/
作者
Wishlily
发布于
2016年9月5日
许可协议