目录

共享内存

共享内存可以被描述成内存一个区域(段)的映射,这个区域可以被更多的进程所共享。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/ 中。

未同步

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

#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;
}