共享内存

共享内存可以被描述成内存一个区域(段)的映射,这个区域可以被更多的进程所共享。1
该方式应该是 SysV IPC 中最快速度形式,减少了不必要的复制拷贝操作,但是 Linux 对共享内存不提供同步操作,一般需要其他方式进行辅助,如信号量。
流程
一般流程2如下:
- 通过
ftok
获得确定的 IPC Key - 使用
shmget
获得用于共享内存的特定 IPC ID - 使用
shmat/shmdt
操作共享内存 - 使用
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
等表示,也可使用open
中mode
宏 - IPC_CREAT:若不存在则创建
- IPC_EXCL:若存在则返回错误(和
IPC_CREAT
一起使用)
- 权限:可使用
- return:-1,错误;非负数,共享内存 ID
注:
- 查看系统共享内存可通过
ipcs -m
命令 - 查看系统对共享内存限制通过
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/
中。
未同步
没有添加其他辅助手段多进程读写共享内存,会发生读取错误的可能。
以下例子共享内存使用字符串,因为对字符串的复制拷贝,一般不会是原子操作。
#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
当然以上方法其实只适合读很快速(原子)类型,本例应该还会有发生错误的可能,下面验证一下。
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!
互斥锁
看来上面的方法也不行,那么只有读写全部加锁才能保证不出错了,不过这样的缺点就是所有的读也变成了串行,降低了效率。
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
将之前单锁变成双锁即可,一个表示写,一个表示读。
此方式适合少写多读的场景。
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;
}