共享内存

共享内存

共享内存的定义

共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址

共享内存的使用

Linux 中实现“进程间共享数据”的内存映射按照后备存储可以分为有两种方式,一种是后备存储为磁盘文件系统的文件映射共享,而另一种是后备存储为RAM页缓存的内存映射共享

类型典型 API后备存储是否落盘内核实现
① 文件映射共享mmap(fd, MAP_SHARED)磁盘文件系统(ext4/xfs 等)会落盘(脏页异步刷回)Page Cache(页缓存)
② 内存映射共享shm_open / shmget / memfd + mmaptmpfs/shmem(纯 RAM 页缓存)不落盘(仅可能被 Swap 换出)Shmem 子系统

共享内存的API有两种,一种是System V IPC的API,而另一种更现代的是POSIX标准的API

System V

System V的接口如下

shmget()函数

shmget函数用于创建一个共享内存

#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  • 第一个参数是一个整数类型的key,相当于是这个共享内存的命名,最好命名使用16进制整数
  • 第二个参数是这篇共享内存空间的大小
  • 第三个参数是权限标志。如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT或操作共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。
  • shmget()函数成功时返回一个与key相关的共享内存标识符(非负整数),类似于键值对,key是键而返回值是值,用于后续的共享内存函数。调用失败返回-1.

不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget()函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget()函数的返回值),只有shmget()函数才直接使用键,所有其他的信号量函数使用由函数返回的标识符。

shmat()函数

第一次创建完共享内存时,它还不能被任何进程访问,shmat()函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:

#include <sys/shm.h>

void *shmat(int shmid, const void *_Nullable shmaddr, int shmflg);
  • 第一个参数,shm_id是由shmget()函数返回的共享内存标识。
  • 第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
  • 第三个参数,shm_flg是一组标志位,通常为0。
  • 调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.

shmdt()函数

该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:

int shmdt(const void *shmaddr);
  • 参数shmaddrshmat()函数返回的地址指针
  • 调用成功时返回0,失败时返回-1

shmctl()函数

与信号量的semctl()函数一样,用来控制共享内存

#include <sys/shm.h>

int shmctl(int shmid, int op, struct shmid_ds *buf);
  • 第一个参数,shm_idshmget()函数返回的共享内存标识符。
  • 第二个参数,command是要采取的操作,它可以取下面的三个值 :
    • IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
    • IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
    • IPC_RMID:删除共享内存段
  • 第三个参数,buf是一个struct shmid_ds结构指针,它指向共享内存模式和访问权限的结构。
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;   /* Creation time/time of last
                                               modification via shmctl() */
               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 */
               ...
           };

代码实战

shmwrite.cpp

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

struct sharedMem {
    bool isChanged = 0;
    char buf[1023];
};

int main(int argc,char **argv)
{
    int shmid;

    printf("writer PID : %d\r\n",getpid());

    //使用shmget创建共享内存并且得到键值,内存大小为定义的结构体大小
    shmid = shmget((key_t)0x1234,sizeof(sharedMem),0640 | IPC_CREAT);

    if(shmid < 0)
    {
        printf("shmget 0x1234 failed\r\n");
        return -1;
    }

    sharedMem *ptr = NULL;
    ptr = (sharedMem *)shmat(shmid,0,0); //使用有点像malloc,需要强制类型转换成自己想要的类型
    if(ptr == NULL)
    {
        printf("shmat error\r\n");
        return -1;
    }

    sprintf(ptr->buf,"the buf was changed by writer:%d\r\n",getpid());
    ptr->isChanged = 1;

    shmdt(ptr);//将共享内存从当前程序VMA中删除

    return 0;
}

shmread.cpp:

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

struct sharedMem {
    bool isChanged = 0;
    char buf[1023];
};

int main(int argc,char **argv)
{
    int shmid;

    printf("reader PID : %d\r\n",getpid());

    shmid = shmget((key_t)0x1234,sizeof(sharedMem),0640 | IPC_CREAT);

    if(shmid < 0)
    {
        printf("shmget 0x1234 failed\r\n");
        return -1;
    }

    sharedMem *ptr = NULL;
    ptr = (sharedMem *)shmat(shmid,0,0);
    if(ptr == NULL)
    {
        printf("shmat error\r\n");
        return -1;
    }

    printf("the sharedMem %s\r\n",(ptr->isChanged == 0) ? "not change" : "changed");

    sleep(5);

    printf("the sharedMem %s\r\n",(ptr->isChanged == 0) ? "not change" : "changed");
    printf("changed text:%s",ptr->buf);

    shmdt(ptr);

    if(shmctl(shmid,IPC_RMID,0) == -1)
    {
        printf("shmctl failed\r\n");
        return -1;
    }

    return 0;
}

在上面代码中分别创建shmreadshmwrite两个进程,在shmread中首先创建共享缓存然后查看是否有被修改,等待5sshmwrite进程起来后对共享缓存进行写入操作,sleep结束后读取内容

kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory$ ./shmread &
[1] 6741
kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory$ reader PID : 6741
the sharedMem not change
./shmwrite
writer PID : 6742
kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory$ 
the sharedMem changed
changed text:the buf was changed by writer:6742
[1]+  Done                    ./shmread

共享内存本身是不安全的,所以一般需要配合信号量一起使用

shmdt()vs shmctl(IPC_RMID)

shmdt(ptr)删除当前进程的页表映射,使ptr变为野指针

shmctl(IPC_RMID)是标记资源为”待销毁”,当最后一个进程 shmdt 后真正释放

shmctl(shmid, IPC_RMID, NULL) 的作用并不是“立即销毁”共享内存,而是 “标记为删除”。System V 共享内存内部维护了一个附着计数(shm_nattch): 调用 IPC_RMID 后,该共享内存会从系统命名空间中移除(后续其他进程再用相同 key 调用 shmget 将失败或创建新的)。 已经附着(shmat)的进程仍可正常读写,直到该进程的附着计数降为 0(即所有附着进程都调用了 shmdt 或异常退出)。shm_nattch == 0 时,内核才会真正释放底层物理内存。 ⚠️ 如果两个进程都调用会怎样? 第一个调用:成功标记删除,返回 0。 第二个调用:通常会失败,返回 -1 并设置 errno = EINVAL(因为该 shmid 已被标记删除或已从内核哈希表中移除)。

共享内存查看

使用命令ipcs -m:

kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00001234 1          kidwjb     640        1024       1

每个字段分别对应:键,shmid,所有者,权限,字节大小,多少个进程附着,状态

POSIX API

API

POSIX的API接口如下,其实有点像是结合了shmget和mmap等操作步骤:

//创建共享内存
int shm_open(const char *name, int oflag, mode_t mode);
//当共享内存引用计数为0时,删除共享内存
int shm_unlink(const char *name);
//获取文件相关的信息,将获取到的信息放入到statbuf结构体中
int fstat(int fd, struct stat *statbuf);
//调整文件大小,通过裁剪指定字节达到对文件大小的精准控制
int ftruncate(int fd, off_t length);
//将进程空间的文件映射到内存,也可以将进程空间的匿名区域映射到内存
void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);
//解除文件或者匿名映射
int munmap(void *addr, size_t length);

注意事项

1.对于 shm_open()name参数:

规则正确示例错误示例说明
必须以 / 开头/my_shmmy_shm缺少前导 / 在 Linux 上会报 EINVAL
/ 后不能再有 //app_ipc_buf/tmp/my_shm只能有一级名称,不能是路径
长度限制通常 ≤ 255 字节超长字符串NAME_MAX 限制
区分大小写/MyShm/myshm混用大小写导致冲突Linux 严格区分
全局唯一/company_proj_module_v1/test所有进程共享同一命名空间,易冲突

2.必须调用 ftruncate()shm_open 创建的文件初始大小为 0,不截断直接 mmap 会触发 SIGBUS

实战代码

shmread.cpp:

#include <cstddef>
#include <sys/mman.h>
#include <sys/stat.h>        /* For mode constants */
#include <fcntl.h>           /* For O_* constants */
#include <stdio.h>
#include <unistd.h>

#define SHM_NAME "/wjbtest" 

struct sharedMem {
    bool isChanged = 0;
    char buf[1023];
};

int main(int argc,char **argv)
{
    int fd = 0;

    printf("PID : %d\r\n",getpid());

    //创建/打开一个共享缓存,返回一个文件描述符
    fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0640);

    if(ftruncate(fd,sizeof(sharedMem)) != 0)
    {
        perror("ftruncate failed:");
        return -1;
    }

    //获取共享内存文件相关属性信息,这里获取的是文件大小,查看是否是设置的大小
    struct stat filestat;
    fstat(fd, &filestat);
    printf("shm st_size :%ld\n",filestat.st_size);

    //映射共享内存到当前地址空间
    sharedMem *ptr = (sharedMem *)mmap(NULL, sizeof(sharedMem), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == NULL)
    {
        perror("mmap failed:");
        return -1;
    }

    //fd可以立即关闭,不影响已经映射的空间
    close(fd);

    printf("the sharedMem %s\r\n",(ptr->isChanged == 0) ? "not change" : "changed");

    sleep(5);

    printf("the sharedMem %s\r\n",(ptr->isChanged == 0) ? "not change" : "changed");
    printf("changed text:%s",ptr->buf);

    //解除映射
    munmap(ptr, sizeof(sharedMem));

    //解除共享内存空间
    shm_unlink(SHM_NAME);

    return 0;
}

shmwrite.cpp:

#include <cstddef>
#include <sys/mman.h>
#include <sys/stat.h>        /* For mode constants */
#include <fcntl.h>           /* For O_* constants */
#include <stdio.h>
#include <unistd.h>

#define SHM_NAME "/wjbtest" 

struct sharedMem {
    bool isChanged = 0;
    char buf[1023];
};

int main(int argc,char **argv)
{
    int fd = 0;

    printf("PID : %d\r\n",getpid());

    //创建/打开一个共享缓存,返回一个文件描述符
    fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0640);

    if(ftruncate(fd,sizeof(sharedMem)) != 0)
    {
        perror("ftruncate failed:");
        return -1;
    }

    //获取共享内存文件相关属性信息,这里获取的是文件大小,查看是否是设置的大小
    struct stat filestat;
    fstat(fd, &filestat);
    printf("shm st_size :%ld\n",filestat.st_size);

    //映射共享内存到当前地址空间
    sharedMem *ptr = (sharedMem *)mmap(NULL, sizeof(sharedMem), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == NULL)
    {
        perror("mmap failed:");
        return -1;
    }

    //fd可以立即关闭,不影响已经映射的空间
    close(fd);

    //进行写操作
    sprintf(ptr->buf,"the buf was changed by writer:%d\r\n",getpid());
    ptr->isChanged = 1;

    munmap(ptr, sizeof(sharedMem));

    return 0;
}

上面代码和Sysem V代码功能一样,打印现象:

kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory/POSIX$ ./shmread &
[1] 8645
kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory/POSIX$ PID : 8645
shm st_size :1024
the sharedMem not change

kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory/POSIX$ ./shmwrite
PID : 8646
shm st_size :1024
kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory/POSIX$ the sharedMem changed
changed text:the buf was changed by writer:8646

[1]+  Done                    ./shmread

调试

POSIX API的共享内存可以查看/dev/shm下的相应内容:

kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory/POSIX$ ./shmread &
[1] 8652
kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory/POSIX$ PID : 8652
shm st_size :1024
the sharedMem not change
ls /dev/shm/
wjbtest
kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory/POSIX$ the sharedMem not change
changed text:
[1]+  Done                    ./shmread
kidwjb@dshanpi-a1:~/sourceCode/OS_code/sharedMemory/POSIX$
上一篇
下一篇