• Posix共享内存


    1. 概述

    共享内存是可用IPC机制中最快的,一旦共享内存区映射到共享它的进程地址空间:

    • 进程间的数据传递就不再执行需进入内核的系统调用
    • 各个进程向共享内存读写数据往往需要某种形式的同步
    • 这些进程间的同步通常使用Posix有名信号量或无名信号量

    对比下面两张图所展示的例子:

    • 不使用共享内存,需要4次内核和进程-用户空间的数据拷贝
    • 使用共享内存,只需要2次内核空间-用户空间的数据拷贝


    2. mmap、munmap和msync函数

    mmap

    mmap函数把一个文件或一个Posix共享内存对象映射到调用进程的地址空间,使用该函数有三个目的:

    • 使用普通文件以提供内存映射IO
    • 使用特殊文件以提供匿名内存映射
    • 使用Posix共享内存对象以提供Posix共享内存区
    //成功返回映射内存的起始地址,失败返回MAP_FAILED
    void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
    

    mmap参数解析:

    • addr指定映射内存的起始地址,通常设为NULL,让内核自己决定起始地址
    • len是被映射到调用进程地址空间中的字节数,它从被映射文件fd开头起第offset个字节处开始算,offset通常设为0,下图展示了这个映射关系
    • prot指定对映射内存区的保护,通常设为PROT_READ | PROT_WRITE
    • flags必须在MAP_SHAREDMAP_PRIVATE这两个标志中选择指定一个,进程间共享内存需要使用MAP_SHARED
    • 可移植的代码应把addr设为NULL,并且flags不指定MAP_FIXED

    prot 说明 flags 说明
    PROT_READ 数据可读 MAP_SHARED 变动是共享的
    PROT_WRITE 数据可写 MAP_PRIVATE 变动是私有的
    PROT_EXEC 数据可执行 MAP_FIXED 准确地解释addr参数
    PROT_NONE 数据不可访问

    mmap成功返回后,可以关闭fd,这对已建立的映射关系没有影响。
    注意,不是所有文件都能进行内存映射,例如终端和套接字就不可以。

    munmap

    mmap建立的映射关系通过munmap删除,其中addr是mmap返回的地址,len是映射区的大小,同mmap的参数len。

    //成功返回0,失败返回-1
    int munmap(void *addr, size_len);
    

    msync

    默认情况下,内核采用虚拟内存算法保持内存映射文件与内存映射区的同步,前提是指定了MAP_SHARED标志,但这种同步可能不是立即生效的,而是在随后某个时间进行。
    但有时候我们修改完数据并进行下一步操作之前,需要确认数据已经同步完成,这时可调用msync函数。

    //成功返回0,失败返回-1
    int msync(void *addr, size_t len, int flags);
    

    其中addr和len含义同munmap,flags使用下表中的常值,其中MS_ASYNCMS_SYNC这两个常值中必须选择指定一个。

    flags 说明
    MS_ASYNC 执行异步写,msync立即返回
    MS_SYNC 执行同步写,msync等同步完成才返回
    MS_INVALIDATE 使高速缓存的数据失效

    3. 内存映射IO

    内存映射IO是父子进程之间共享内存区的一种方法,父进程fork前以MAP_SHARED方式调用mmap,其建立的内存映射关系会被子进程继承。
    我们使用这个方法,来实现以下功能:

    • 父子进程通过内存映射IO共享一片内存
    • 父子进程共同给共享内存区中的一个计数器持续加1

    父子进程同步——Posix有名信号量

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <semaphore.h>
    #include <sys/mman.h>
    
    #define MMAP_FILE   "/home/delphi/mmap_file"
    #define SEM_PATH    "/sem_mmap"
    
    struct Shared
    {
        int count;
    };
    
    int main(int argvc, char **argv)
    {
        struct Shared shared;
        struct Shared *ptr; //mmap返回指针类型结构和映射文件数据结构需要一致
        sem_t *mutex;       //因为是进程间同步,不能用互斥锁,因此使用二值信号量模拟互斥锁
        int fd;
        int i;
    
        fd = open(MMAP_FILE, O_CREAT | O_RDWR, 0666);
        memset(&shared, 0, sizeof(shared));
        write(fd, &shared, sizeof(shared)); //将映射文件内容初始化为0
        ptr = mmap(NULL, sizeof(shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        close(fd);
    
        mutex = sem_open(SEM_PATH, O_CREAT, 0666, 1); //二值信号量模拟互斥锁,初始值必须为1
        sem_unlink(SEM_PATH);
    
        setbuf(stdout, NULL); //设置标准输出为无缓冲
    
        if (fork() == 0)
        {
            for (i = 0; i < 10; i++)
            {
                sem_wait(mutex);
                printf("child: %d
    ", ptr->count);
                ptr->count++;
                sem_post(mutex);
            }
    
            exit(0);
        }
    
        for (i = 0; i < 10; i++)
        {
            sem_wait(mutex);
            printf("parent: %d
    ", ptr->count);
            ptr->count++;
            sem_post(mutex);
        }
    
        return 0;
    }
    

    该示例代码使用Posix有名信号量来同步父子进程,共享内存区中仅有一个4字节计数器,信号量不在共享内存区中。

    父子进程同步——Posix无名信号量

    将上面这份代码修改一下,改为使用建立在共享内存区中的Posix无名信号量同步父子进程,示意图如下图所示。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <semaphore.h>
    #include <sys/mman.h>
    
    #define MMAP_FILE   "/home/delphi/mmap_file"
    
    struct Shared
    {
        sem_t mutex;
        int count;
    };
    
    int main(int argvc, char **argv)
    {
        struct Shared shared;
        struct Shared *ptr; //mmap返回指针类型结构和映射文件数据结构需要一致
        int fd;
        int i;
    
        fd = open(MMAP_FILE, O_CREAT | O_RDWR, 0666);
        memset(&shared, 0, sizeof(shared));
        write(fd, &shared, sizeof(shared)); //将映射文件内容初始化为0
        ptr = mmap(NULL, sizeof(shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        close(fd);
    
        sem_init(&ptr->mutex, 1, 1);    //Posix无名信号量建立在ptr指向的共享内存中
        setbuf(stdout, NULL);          //设置标准输出为无缓冲
    
        if (fork() == 0)
        {
            for (i = 0; i < 10; i++)
            {
                sem_wait(&ptr->mutex);
                printf("child: %d
    ", ptr->count);
                ptr->count++;
                sem_post(&ptr->mutex);
            }
    
            exit(0);
        }
    
        for (i = 0; i < 10; i++)
        {
            sem_wait(&ptr->mutex);
            printf("parent: %d
    ", ptr->count);
            ptr->count++;
            sem_post(&ptr->mutex);
        }
    
        sem_destroy(&ptr->mutex);
    
        return 0;
    }
    

    4. 匿名内存映射

    在上面展示的内存映射IO示例代码中,可以看出编码的前两步都是:

    • 调用open创建一个普通文件
    • 调用write将文件内容初始化为0

    如果仅仅是用于父子进程共享内存,可以使用匿名内存映射来避免文件的显式创建打开以及初始化,其办法是:

    • mmap调用时,flags设为MAP_SHARED | MAP_ANON,fd设为-1,offset设为0即可
    • 匿名内存映射保证这样的内存区初始化为0

    把基于Posix无名信号量的示例代码改为匿名内存映射,可以看出,fork前的准备工作明显简化了许多。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <semaphore.h>
    #include <sys/mman.h>
    
    struct Shared
    {
        sem_t mutex;
        int count;
    };
    
    int main(int argvc, char **argv)
    {
        struct Shared *ptr;
        int i;
    
        ptr = mmap(NULL, sizeof(struct Shared), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
        sem_init(&ptr->mutex, 1, 1);
        setbuf(stdout, NULL);
    
        if (fork() == 0)
        {
            for (i = 0; i < 10; i++)
            {
                sem_wait(&ptr->mutex);
                printf("child: %d
    ", ptr->count);
                ptr->count++;
                sem_post(&ptr->mutex);
            }
    
            exit(0);
        }
    
        for (i = 0; i < 10; i++)
        {
            sem_wait(&ptr->mutex);
            printf("parent: %d
    ", ptr->count);
            ptr->count++;
            sem_post(&ptr->mutex);
        }
    
        sem_destroy(&ptr->mutex);
    
        return 0;
    }
    

    5. Posix共享内存

    前面讲的都是在父子进程间使用共享内存的技术,现在把共享内存区的概念扩展到任意进程之间,Posix.1提供了两种在任意进程间共享内存区的方法。

    • 内存映射IO:该方法其实也可以用于无亲缘关系进程间共享内存
    • Posix共享内存:这是Posix IPC的第三种机制

    这两种技术都需要调用mmap,区别在于mmap参数fd的获取手段:

    • 内存映射IO通过open获得
    • Posix共享内存通过shm_open获得

    shm_open和shm_unlink函数

    shm_open用于创建一个新的Posix共享内存对象或打开一个已存在的Posix共享内存对象。
    shm_unlink用于从系统中删除一个Posix共享内存对象。

    //成功返回非负描述符,失败返回-1
    int shm_open(const char *name, int oflag, mode_t mode);
    
    //成功返回0,失败返回-1
    int shun_unlink(const char *name);
    

    shm_open参数说明:

    • oflag参数不能设置O_WRONLY标志
    • 和mq_open、sem_open不同,shm_open的mode参数总是必须指定,当指定了O_CREAT标志时,mode为用户权限位,否则将mode设为0

    shm_open的返回值是一个描述符,它随后用作mmap的第五个参数fd。

    ftruncate和fstat函数

    处理mmap的时候,普通文件或Posix共享内存对象的大小都可以通过调用ftruncate设置。

    #include <unistd.h>
    
    //成功返回0,失败返回-1
    int ftruncate(int fd, off_t length):
    
    • 对于普通文件,若文件长度大于length,额外的数据会被丢弃;若文件长度小于length,则扩展文件大小到length
    • 对于Posix共享内存对象,ftruncate把该对象的大小设置成length字节

    我们调用ftruncate来指定新创建的Posix共享内存对象大小,或者修改已存在的Posix共享内存对象大小。

    • 创建新的Posix共享内存对象时指定大小是必须的,否则访问mmap返回的地址会报bus error错误
    • 当打开一个已存在的Posix共享内存对象时,可以调用fstat来获取该对象的信息
    #include <sys/stat.h>
    #include <sys/types.h>
    
    //成功返回0,失败返回-1
    int fstat(int fd, struct stat *buf);
    

    stat结构有12个或以上的成员,然而当fd指代一个Posix共享内存对象时,只有四个成员含有信息:

    struct stat
    {
        mode_t st_mode;  //用户访问权限
        uid_t  st_uid;   //user id of owner
        gid_t  st_gid;   //group id of owner
        off_t  st_size;  //文件大小
    };
    

    6. Posix共享内存示例代码

    示例代码包括一个.h文件和两个.c文件:

    • common.h定义server和client共同使用的信息
    • server.c创建并初始化Posix共享内存对象,以及同步需要的信号量
    • client.c打开Posix共享内存对象,然后给共享内存中的计数器加1

    进程同步——Posix有名信号量

    common.h

    #ifndef _COMMON_H_
    #define _COMMON_H_
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <semaphore.h>
    #include <sys/mman.h>
    
    #define SHM_FILE    "/shm_file"
    #define SEM_PATH    "/sem_mmap"
    
    struct Shared
    {
        int count;
    };
    
    #endif
    

    server.c

    #include "common.h"
    
    int main()
    {
        struct Shared *ptr;
        sem_t *mutex;
        int fd;
    
        shm_unlink(SHM_FILE);
        fd = shm_open(SHM_FILE, O_RDWR | O_CREAT, 0666);
        ftruncate(fd, sizeof(struct Shared));
        ptr = mmap(NULL, sizeof(struct Shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        close(fd);
    
        sem_unlink(SEM_PATH);
        mutex = sem_open(SEM_PATH, O_CREAT, 0666, 1);
        sem_close(mutex);
    
        pause();
    
        return 0;
    }
    

    client.c

    #include "common.h"
    
    int main(int argc, char **argv)
    {
        struct Shared *ptr;
        struct stat buf;
        sem_t *mutex;
        int fd;
        int nloop;
        int i;
    
        fd = shm_open(SHM_FILE, O_RDWR, 0);
        fstat(fd, &buf);
        ptr = mmap(NULL, buf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        close(fd);
    
        mutex = sem_open(SEM_PATH, 0);
        nloop = atoi(argv[1]);
    
        for (i = 0; i < nloop; i++)
        {
            sem_wait(mutex);
            printf("pid %d: %d
    ", getpid(), ptr->count++);
            sem_post(mutex);
        }
    
        return 0;
    }
    

    编译并启动server,阻塞在pasue()中。

    后台同时运行三个client进程。

    截取进程切换时的部分输出片段,可以看到切换后计数依然是连续的。

    进程同步——Posix无名信号量

    common.h

    #ifndef _COMMON_H_
    #define _COMMON_H_
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <semaphore.h>
    #include <sys/mman.h>
    
    #define SHM_FILE    "/shm_file"
    
    struct Shared
    {
        sem_t mutex;
        int count;
    };
    
    #endif
    

    server.c

    #include "common.h"
    
    int main()
    {
        struct Shared *ptr;
        int fd;
    
        shm_unlink(SHM_FILE);
        fd = shm_open(SHM_FILE, O_RDWR | O_CREAT, 0666);
        ftruncate(fd, sizeof(struct Shared));
        ptr = mmap(NULL, sizeof(struct Shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        close(fd);
    
        sem_init(&ptr->mutex, 1, 1);
    
        pause();
    
        return 0;
    }
    

    client.c

    #include "common.h"
    
    int main(int argc, char **argv)
    {
        struct Shared *ptr;
        struct stat buf;
        int fd;
        int nloop;
        int i;
    
        fd = shm_open(SHM_FILE, O_RDWR, 0);
        fstat(fd, &buf);
        ptr = mmap(NULL, buf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        close(fd);
    
        //共享内存中的mutex已由server初始化,client直接使用就可以了,不能重复初始化
        nloop = atoi(argv[1]);
    
        for (i = 0; i < nloop; i++)
        {
            sem_wait(&ptr->mutex);
            printf("pid %d: %d
    ", getpid(), ptr->count++);
            sem_post(&ptr->mutex);
        }
    
        return 0;
    }
    

    编译并启动server,阻塞在pasue()中。

    后台同时运行三个client进程。

    截取进程切换时的部分输出片段,可以看到切换后计数依然是连续的。

  • 相关阅读:
    假如
    Find the peace with yourself
    Sep 15th 2018
    Sep 10th 2018
    third party sales process 继续说
    成功设置open live writer
    sublime text2 基本配置及结合Python 环境
    Compmgmtlauncher.exe问题解决方法
    nginx 代理服务器
    vmware之linux不重启添加虚拟硬盘
  • 原文地址:https://www.cnblogs.com/songhe364826110/p/11530732.html
Copyright © 2020-2023  润新知